this repo has no description
1import type { 2 DidDocument, 3 InternalStateData, 4 OAuthAuthorizationServerMetadata, 5 OAuthProtectedResourceMetadata, 6 ResolvedHandle, 7 Session, 8 TokenSet, 9 Key, 10} from "@atproto/oauth-client"; 11import { type SimpleStore, type Value } from "@atproto-labs/simple-store"; 12import { MMKV } from "react-native-mmkv"; 13import { JWK } from "./ExpoAtprotoAuth.types"; 14import { ReactNativeKey } from "./react-native-key"; 15 16type Item<V> = { 17 value: V; 18 expiresAt: null | number; 19}; 20 21type CryptoKeyPair = { 22 publicKey: JWK; 23 privateKey: JWK; 24}; 25 26type EncodedKey = { 27 keyId: string; 28 keyPair: CryptoKeyPair; 29}; 30 31function encodeKey(key: Key): EncodedKey { 32 if (!key.privateJwk || !key.publicJwk || !key.kid) { 33 throw new Error("Invalid key object"); 34 } 35 36 const encodedKey = { 37 keyId: key.kid, 38 keyPair: { 39 publicKey: key.publicJwk, 40 privateKey: key.privateJwk, 41 }, 42 }; 43 44 // @ts-expect-error 45 return encodedKey; 46} 47 48async function decodeKey(encoded: EncodedKey): Promise<ReactNativeKey> { 49 console.log(encoded); 50 return new ReactNativeKey(encoded.keyPair.privateKey); 51} 52 53export type Schema = { 54 state: Item<{ 55 dpopKey: EncodedKey; 56 iss: string; 57 verifier?: string; 58 appState?: string; 59 }>; 60 session: Item<{ 61 dpopKey: EncodedKey; 62 tokenSet: TokenSet; 63 }>; 64 didCache: Item<DidDocument>; 65 dpopNonceCache: Item<string>; 66 handleCache: Item<ResolvedHandle>; 67 authorizationServerMetadataCache: Item<OAuthAuthorizationServerMetadata>; 68 protectedResourceMetadataCache: Item<OAuthProtectedResourceMetadata>; 69}; 70 71export type DatabaseStore<V extends Value> = SimpleStore<string, V> & { 72 getKeys: () => Promise<string[]>; 73}; 74 75const STORES = [ 76 "state", 77 "session", 78 "didCache", 79 "dpopNonceCache", 80 "handleCache", 81 "authorizationServerMetadataCaache", 82 "protectedResourceMetadataCache", 83]; 84 85export type ReactNativeOAuthDatabaseOptions = { 86 name?: string; 87 durability?: "strict" | "relaxed"; 88 cleanupInterval?: number; 89}; 90 91export class ReactNativeOAuthDatabase { 92 #cleanupInterval?: ReturnType<typeof setInterval>; 93 #mmkv?: MMKV; 94 95 constructor(options?: ReactNativeOAuthDatabaseOptions) { 96 this.#cleanupInterval = setInterval(() => { 97 void this.cleanup(); 98 }, options?.cleanupInterval ?? 30e3); 99 this.#mmkv = new MMKV({ id: "react-native-oauth-client" }); 100 } 101 102 delete = async (key: string) => { 103 this.#mmkv?.delete(key); 104 this.#mmkv?.delete(`${key}.expiresAt`); 105 }; 106 107 protected createStore<N extends keyof Schema, V extends Value>( 108 name: N, 109 { 110 encode, 111 decode, 112 expiresAt, 113 }: { 114 encode: ( 115 value: V, 116 ) => Schema[N]["value"] | PromiseLike<Schema[N]["value"]>; 117 decode: (encoded: Schema[N]["value"]) => V | PromiseLike<V>; 118 expiresAt: (value: V) => null | number; 119 }, 120 ): DatabaseStore<V> { 121 return { 122 get: async (key) => { 123 console.log(`getting ${name}.${key}`); 124 const item = this.#mmkv?.getString(`${name}.${key}`); 125 126 if (item === undefined) return undefined; 127 128 const expiresAt = this.#mmkv?.getNumber(`${name}.${key}.expiresAt`); 129 if (expiresAt && expiresAt < Date.now()) { 130 await this.delete(`${name}.${key}`); 131 return undefined; 132 } 133 134 const res = decode(JSON.parse(item)); 135 console.log(res); 136 return res; 137 }, 138 139 getKeys: async () => { 140 const keys = this.#mmkv?.getAllKeys() ?? []; 141 return keys.filter((key) => key.startsWith(`${name}.`)); 142 }, 143 144 set: async (key, value) => { 145 let encoded = await encode(value); 146 encoded = JSON.stringify(encoded); 147 148 const _expiresAt = expiresAt(value); 149 150 this.#mmkv?.set(`${name}.${key}`, encoded); 151 if (_expiresAt) { 152 this.#mmkv?.set(`${name}.${key}.expiresAt`, _expiresAt); 153 } 154 }, 155 del: async (key) => { 156 await this.delete(`${name}.${key}`); 157 }, 158 }; 159 } 160 161 getSessionStore(): DatabaseStore<Session> { 162 return this.createStore("session", { 163 expiresAt: ({ tokenSet }) => 164 tokenSet.refresh_token || tokenSet.expires_at == null 165 ? null 166 : new Date(tokenSet.expires_at).valueOf(), 167 encode: ({ dpopKey, ...session }) => ({ 168 ...session, 169 dpopKey: encodeKey(dpopKey), 170 }), 171 // @ts-expect-error 172 decode: async ({ dpopKey, ...encoded }) => ({ 173 ...encoded, 174 dpopKey: await decodeKey(dpopKey), 175 }), 176 }); 177 } 178 179 getStateStore(): DatabaseStore<InternalStateData> { 180 return this.createStore("state", { 181 expiresAt: (_value) => Date.now() + 10 * 60e3, 182 encode: ({ dpopKey, ...session }) => ({ 183 ...session, 184 dpopKey: encodeKey(dpopKey), 185 }), 186 // @ts-expect-error 187 decode: async ({ dpopKey, ...encoded }) => ({ 188 ...encoded, 189 dpopKey: await decodeKey(dpopKey), 190 }), 191 }); 192 } 193 194 getDpopNonceCache(): undefined | DatabaseStore<string> { 195 return this.createStore("dpopNonceCache", { 196 expiresAt: (_value) => Date.now() + 600e3, 197 encode: (value) => value, 198 decode: (encoded) => encoded, 199 }); 200 } 201 202 getDidCache(): undefined | DatabaseStore<DidDocument> { 203 return this.createStore("didCache", { 204 expiresAt: (_value) => Date.now() + 60e3, 205 encode: (value) => value, 206 decode: (encoded) => encoded, 207 }); 208 } 209 210 getHandleCache(): undefined | DatabaseStore<ResolvedHandle> { 211 return this.createStore("handleCache", { 212 expiresAt: (_value) => Date.now() + 60e3, 213 encode: (value) => value, 214 decode: (encoded) => encoded, 215 }); 216 } 217 218 getAuthorizationServerMetadataCache(): 219 | undefined 220 | DatabaseStore<OAuthAuthorizationServerMetadata> { 221 return this.createStore("authorizationServerMetadataCache", { 222 expiresAt: (_value) => Date.now() + 60e3, 223 encode: (value) => value, 224 decode: (encoded) => encoded, 225 }); 226 } 227 228 getProtectedResourceMetadataCache(): 229 | undefined 230 | DatabaseStore<OAuthProtectedResourceMetadata> { 231 return this.createStore("protectedResourceMetadataCache", { 232 expiresAt: (_value) => Date.now() + 60e3, 233 encode: (value) => value, 234 decode: (encoded) => encoded, 235 }); 236 } 237 238 async cleanup() { 239 for (const name of STORES) { 240 const keys = this.#mmkv?.getAllKeys() ?? []; 241 for (const key of keys) { 242 if (key.startsWith(`${name}.`)) { 243 const expiresAt = this.#mmkv?.getNumber(`${name}.${key}.expiresAt`); 244 if (expiresAt && Number(expiresAt) < Date.now()) { 245 this.#mmkv?.delete(key); 246 this.#mmkv?.delete(`${name}.${key}.expiresAt`); 247 } 248 } 249 } 250 } 251 } 252 253 async [Symbol.asyncDispose]() { 254 clearInterval(this.#cleanupInterval); 255 } 256}