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 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: (value: V) => Schema[N]['value'] | PromiseLike<Schema[N]['value']> 115 decode: (encoded: Schema[N]['value']) => V | PromiseLike<V> 116 expiresAt: (value: V) => null | number 117 } 118 ): DatabaseStore<V> { 119 return { 120 get: async (key) => { 121 console.log(`getting ${name}.${key}`) 122 const item = this.#mmkv?.getString(`${name}.${key}`) 123 124 if (item === undefined) return undefined 125 126 const storedExpiresAt = this.#mmkv?.getNumber( 127 `${name}.${key}.expiresAt` 128 ) 129 if (storedExpiresAt && storedExpiresAt < 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}