diff --git a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts deleted file mode 100644 index c4375015a7..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {CryptoImplementation, Key} from '@atproto/oauth-client' - -import {RnCryptoKey} from './rn-crypto-key' - -// It loads the native module object from the JSI or falls back to -// the bridge module (from NativeModulesProxy) if the remote debugger is on. -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -export class CryptoSubtle implements CryptoImplementation { - // We won't use the `algos` parameter here, as we will always use `ES256`. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async createKey(algos: string[] = ['ES256']): Promise { - return await RnCryptoKey.generate(undefined, ['ES256']) - } - - getRandomValues(byteLength: number): Uint8Array { - return NativeModule.getRandomValues(byteLength) - } - - async digest(bytes: Uint8Array): Promise { - return await NativeModule.digest(bytes) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts deleted file mode 100644 index 14936f65df..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {CryptoImplementation, DigestAlgorithm, Key} from '@atproto/oauth-client' - -// @ts-ignore web only, this silences some warnings -const crypto = global.crypto - -export class CryptoSubtle implements CryptoImplementation { - constructor(_: any) { - if (!crypto?.subtle) { - throw new Error( - 'Crypto with CryptoSubtle is required. If running in a browser, make sure the current page is loaded over HTTPS.', - ) - } - } - - async createKey(algs: string[]): Promise { - return WebcryptoKey.generate(undefined, algs) - } - - getRandomValues(byteLength: number): Uint8Array { - const bytes = new Uint8Array(byteLength) - crypto.getRandomValues(bytes) - return bytes - } - - async digest( - bytes: Uint8Array, - algorithm: DigestAlgorithm, - ): Promise { - const buffer = await crypto.subtle.digest( - digestAlgorithmToSubtle(algorithm), - bytes, - ) - return new Uint8Array(buffer) - } -} - -// @ts-ignore web only type -function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier { - switch (name) { - case 'sha256': - case 'sha384': - case 'sha512': - return `SHA-${name.slice(-3)}` - default: - throw new Error(`Unknown hash algorithm ${name}`) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/jose-key.ts b/modules/expo-bluesky-oauth-client/src-old/jose-key.ts deleted file mode 100644 index fcf401e1dc..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/jose-key.ts +++ /dev/null @@ -1,106 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {jwkSchema} from '@atproto/jwk' -import {Key} from '@atproto/jwk' -import { - exportJWK, - importJWK, - importPKCS8, - JWK, - KeyLike, - VerifyOptions, -} from 'jose' -import {JwtHeader, JwtPayload} from 'jwt-decode' - -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -export class JoseKey extends Key { - #keyObj?: KeyLike | Uint8Array - - protected async getKey() { - return (this.#keyObj ||= await importJWK(this.jwk as JWK)) - } - - async createJwt(header: JwtHeader, payload: JwtPayload): Promise { - if (header.kid && header.kid !== this.kid) { - throw new TypeError( - `Invalid "kid" (${header.kid}) used to sign with key "${this.kid}"`, - ) - } - - if (!header.alg || !this.algorithms.includes(header.alg)) { - throw new TypeError( - `Invalid "alg" (${header.alg}) used to sign with key "${this.kid}"`, - ) - } - - return await NativeModule.createJwt( - JSON.stringify(this.privateJwk), - JSON.stringify(header), - JSON.stringify(payload), - ) - } - - async verifyJwt< - P extends VerifyPayload = JwtPayload, - C extends string = string, - >(token: Jwt, options?: VerifyOptions): Promise> { - const result = await NativeModule.verifyJwt( - JSON.stringify(this.publicJwk), - token, - JSON.stringify(options), - ) - return result - // return result as VerifyResult - } - - static async fromImportable( - input: Importable, - kid?: string, - ): Promise { - if (typeof input === 'string') { - // PKCS8 - if (input.startsWith('-----')) { - return this.fromPKCS8(input, kid) - } - - // Jwk (string) - if (input.startsWith('{')) { - return this.fromJWK(input, kid) - } - - throw new TypeError('Invalid input') - } - - if (typeof input === 'object') { - // Jwk - if ('kty' in input || 'alg' in input) { - return this.fromJWK(input, kid) - } - - // KeyLike - return this.fromJWK(await exportJWK(input), kid) - } - - throw new TypeError('Invalid input') - } - - static async fromPKCS8(pem: string, kid?: string): Promise { - const keyLike = await importPKCS8(pem, '', {extractable: true}) - return this.fromJWK(await exportJWK(keyLike), kid) - } - - static async fromJWK( - input: string | Record, - inputKid?: string, - ): Promise { - const jwk = jwkSchema.parse( - typeof input === 'string' ? JSON.parse(input) : input, - ) - - const kid = either(jwk.kid, inputKid) - const alg = jwk.alg - const use = jwk.use || 'sig' - - return new JoseKey({...jwk, kid, alg, use}) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/native-types.ts b/modules/expo-bluesky-oauth-client/src-old/native-types.ts deleted file mode 100644 index 2ac1d7fa40..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/native-types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface CryptoKey { - algorithm: { - name: 'ECDSA' - namedCurve: 'P-256' - } - extractable: boolean - type: 'public' | 'private' - usages: ('sign' | 'verify')[] -} - -export interface NativeJWKKey { - crv: 'P-256' - ext: boolean - kty: 'EC' - x: string - y: string - use: 'sig' - alg: 'ES256' - kid: string -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts deleted file mode 100644 index d5e4360889..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {Jwk, jwkSchema} from '@atproto/jwk' - -import {JoseKey} from './jose-key' -import {CryptoKey, NativeJWKKey} from './native-types' - -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -interface CryptoKeyPair { - privateKey: CryptoKey - publicKey: CryptoKey -} - -interface NativeJWKKeyPair { - privateKey: NativeJWKKey - publicKey: NativeJWKKey -} - -export class RnCryptoKey extends JoseKey { - static async generate( - kid: string | undefined, - _: string[] = ['ES256'], - __ = false, - ) { - const {privateKey, publicKey} = await NativeModule.generateKeyPair(kid) - - const nativeKeyPair = { - privateKey: JSON.parse(privateKey), - publicKey: JSON.parse(publicKey), - } - - return this.fromKeypair(nativeKeyPair.privateKey.kid, nativeKeyPair) - } - - static fromKeypair(kid: string, cryptoKeyPair: NativeJWKKeyPair) { - const use = cryptoKeyPair.privateKey.use ?? 'sig' - const alg = cryptoKeyPair.privateKey.alg ?? 'ES256' - - if (use !== 'sig') { - throw new TypeError('Unsupported JWK use') - } - - const webCryptoKeyPair: CryptoKeyPair = { - privateKey: { - algorithm: { - name: 'ECDSA', - namedCurve: 'P-256', - }, - extractable: true, - type: 'private', - usages: ['sign'], - }, - publicKey: { - algorithm: { - name: 'ECDSA', - namedCurve: 'P-256', - }, - extractable: true, - type: 'public', - usages: ['verify'], - }, - } - - return new RnCryptoKey( - jwkSchema.parse({...cryptoKeyPair.privateKey, use, kid, alg}), - webCryptoKeyPair, - ) - } - - constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { - super(jwk) - } - - get isPrivate() { - return true - } - - get privateJwk(): Jwk | undefined { - if (super.isPrivate) return this.jwk - throw new Error('Private key is not exportable.') - } - - protected async getKey() { - return this.cryptoKeyPair.privateKey - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts deleted file mode 100644 index b9fbd05758..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Jwk, jwkSchema} from '@atproto/jwk' -import {JoseKey} from '@atproto/jwk-jose' - -import {CryptoKey} from './native-types' -import {generateKeyPair, isSignatureKeyPair} from './util.web' - -// @ts-ignore web only, stops warnings for crypto being missing -const crypto = global.crypto - -// Global has this, but this stops the warnings -interface CryptoKeyPair { - privateKey: CryptoKey - publicKey: CryptoKey -} - -export class RNCryptoKey extends JoseKey { - static async generate( - kid: string = crypto.randomUUID(), - allowedAlgos: string[] = ['ES256'], - exportable = false, - ) { - const cryptoKeyPair: CryptoKeyPair = await generateKeyPair( - allowedAlgos, - exportable, - ) - return this.fromKeypair(kid, cryptoKeyPair) - } - - static async fromKeypair( - kid: string, - cryptoKeyPair: CryptoKeyPair, - ): Promise { - if (!isSignatureKeyPair(cryptoKeyPair)) { - throw new TypeError('CryptoKeyPair must be compatible with sign/verify') - } - - // https://datatracker.ietf.org/doc/html/rfc7517 - // > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...] - // > Applications should specify which of these members they use. - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {key_ops: _, ...jwk} = await crypto.subtle.exportKey( - 'jwk', - cryptoKeyPair.privateKey.extractable - ? cryptoKeyPair.privateKey - : cryptoKeyPair.publicKey, - ) - - const use = jwk.use ?? 'sig' - const alg = jwk.alg ?? 'ES256' - - if (use !== 'sig') { - throw new TypeError('Unsupported JWK use') - } - - return new RNCryptoKey( - jwkSchema.parse({...jwk, use, kid, alg}), - cryptoKeyPair, - ) - } - - constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { - super(jwk) - } - - get isPrivate() { - return true - } - - get privateJwk(): Jwk | undefined { - if (super.isPrivate) return this.jwk - throw new Error('Private key is not exportable.') - } - - protected async getKey() { - return this.cryptoKeyPair.privateKey - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts deleted file mode 100644 index 81713e140c..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts +++ /dev/null @@ -1,147 +0,0 @@ -import {Fetch} from '@atproto/fetch' -import {UniversalIdentityResolver} from '@atproto/identity-resolver' -import { - OAuthAuthorizeOptions, - OAuthClientFactory, - OAuthResponseMode, - OAuthResponseType, - Session, -} from '@atproto/oauth-client' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' - -import {CryptoSubtle} from './crypto-subtle' -import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database' - -export type RNOAuthClientOptions = { - responseMode?: OAuthResponseMode - responseType?: OAuthResponseType - clientMetadata: OAuthClientMetadata - fetch?: Fetch - crypto?: Crypto -} - -const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' - -export class RNOAuthClientFactory extends OAuthClientFactory { - readonly sessionStore: DatabaseStore - - constructor({ - clientMetadata, - // "fragment" is safer as it is not sent to the server - responseMode = 'fragment', - responseType, - crypto = {subtle: CryptoSubtle}, - fetch = globalThis.fetch, - }: RNOAuthClientOptions) { - const database = new RNOAuthDatabase() - - super({ - clientMetadata, - responseMode, - responseType, - fetch, - cryptoImplementation: new CryptoSubtle(crypto), - sessionStore: database.getSessionStore(), - stateStore: database.getStateStore(), - metadataResolver: new IsomorphicOAuthServerMetadataResolver({ - fetch, - cache: database.getMetadataCache(), - }), - identityResolver: UniversalIdentityResolver.from({ - fetch, - didCache: database.getDidCache(), - handleCache: database.getHandleCache(), - }), - dpopNonceCache: database.getDpopNonceCache(), - }) - - this.sessionStore = database.getSessionStore() - } - - async restoreAll() { - const sessionIds = await this.sessionStore.getKeys() - return Object.fromEntries( - await Promise.all( - sessionIds.map( - async sessionId => - [sessionId, await this.restore(sessionId, false)] as const, - ), - ), - ) - } - - async init(sessionId?: string, forceRefresh = false) { - const signInResult = await this.signInCallback() - if (signInResult) { - return signInResult - } else if (sessionId) { - const client = await this.restore(sessionId, forceRefresh) - return {client} - } else { - // TODO: we could restore any session from the store ? - } - } - - async signIn(input: string, options?: OAuthAuthorizeOptions) { - return await this.authorize(input, options) - } - - async signInCallback() { - const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) - if (location.pathname !== redirectUri.pathname) return null - - const params = - this.responseMode === 'query' - ? new URLSearchParams(location.search) - : new URLSearchParams(location.hash.slice(1)) - - // Only if the query string contains oauth callback params - if ( - !params.has('iss') || - !params.has('state') || - !(params.has('code') || params.has('error')) - ) { - return null - } - - // Replace the current history entry without the query string (this will - // prevent this 'if' branch to run again if the user refreshes the page) - history.replaceState(null, '', location.pathname) - - return this.callback(params) - .then(async result => { - if (result.state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'fulfilled', - value: result.client.sessionId, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - return result - }) - .catch(async err => { - // TODO: Throw a proper error from parent class to actually detect - // oauth authorization errors - const state = typeof (err as any)?.state - if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'rejected', - reason: err, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - throw err - }) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts deleted file mode 100644 index a8e2dc179e..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {Fetch} from '@atproto/fetch' -import {UniversalIdentityResolver} from '@atproto/identity-resolver' -import { - OAuthAuthorizeOptions, - OAuthClientFactory, - OAuthResponseMode, - OAuthResponseType, - Session, -} from '@atproto/oauth-client' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' - -import {CryptoSubtle} from './crypto-subtle' -import { - DatabaseStore, - PopupStateData, - RNOAuthDatabase, -} from './rn-oauth-database' - -export type RNOAuthClientOptions = { - responseMode?: OAuthResponseMode - responseType?: OAuthResponseType - clientMetadata: OAuthClientMetadata - fetch?: Fetch - crypto?: Crypto -} - -const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' - -export class RNOAuthClientFactory extends OAuthClientFactory { - readonly popupStore: DatabaseStore - readonly sessionStore: DatabaseStore - - constructor({ - clientMetadata, - // "fragment" is safer as it is not sent to the server - responseMode = 'fragment', - responseType, - crypto = globalThis.crypto, - fetch = globalThis.fetch, - }: RNOAuthClientOptions) { - const database = new RNOAuthDatabase() - - super({ - clientMetadata, - responseMode, - responseType, - fetch, - cryptoImplementation: new CryptoSubtle(crypto), - sessionStore: database.getSessionStore(), - stateStore: database.getStateStore(), - metadataResolver: new IsomorphicOAuthServerMetadataResolver({ - fetch, - cache: database.getMetadataCache(), - }), - identityResolver: UniversalIdentityResolver.from({ - fetch, - didCache: database.getDidCache(), - handleCache: database.getHandleCache(), - }), - dpopNonceCache: database.getDpopNonceCache(), - }) - - this.sessionStore = database.getSessionStore() - this.popupStore = database.getPopupStore() - } - - async restoreAll() { - const sessionIds = await this.sessionStore.getKeys() - return Object.fromEntries( - await Promise.all( - sessionIds.map( - async sessionId => - [sessionId, await this.restore(sessionId, false)] as const, - ), - ), - ) - } - - async init(sessionId?: string, forceRefresh = false) { - const signInResult = await this.signInCallback() - if (signInResult) { - return signInResult - } else if (sessionId) { - const client = await this.restore(sessionId, forceRefresh) - return {client} - } else { - // TODO: we could restore any session from the store ? - } - } - - async signIn(input: string, options?: OAuthAuthorizeOptions) { - return await this.authorize(input, options) - } - - async signInCallback() { - const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) - if (location.pathname !== redirectUri.pathname) return null - - const params = - this.responseMode === 'query' - ? new URLSearchParams(location.search) - : new URLSearchParams(location.hash.slice(1)) - - // Only if the query string contains oauth callback params - if ( - !params.has('iss') || - !params.has('state') || - !(params.has('code') || params.has('error')) - ) { - return null - } - - // Replace the current history entry without the query string (this will - // prevent this 'if' branch to run again if the user refreshes the page) - history.replaceState(null, '', location.pathname) - - return this.callback(params) - .then(async result => { - if (result.state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'fulfilled', - value: result.client.sessionId, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - return result - }) - .catch(async err => { - // TODO: Throw a proper error from parent class to actually detect - // oauth authorization errors - const state = typeof (err as any)?.state - if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'rejected', - reason: err, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - throw err - }) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts deleted file mode 100644 index 96a7ae74d7..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts +++ /dev/null @@ -1,214 +0,0 @@ -import {GenericStore, Value} from '@atproto/caching' -import {DidDocument} from '@atproto/did' -import {ResolvedHandle} from '@atproto/handle-resolver' -import {Key} from '@atproto/jwk' -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' -import Storage from '@react-native-async-storage/async-storage' - -type Item = { - value: string - expiresAt: null | Date -} - -type EncodedKey = { - keyId: string - keyPair: CryptoKeyPair -} - -function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { - throw new Error('Invalid key object') - } - return { - keyId: key.kid, - keyPair: key.cryptoKeyPair, - } -} - -async function decodeKey(encoded: EncodedKey): Promise { - return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) -} - -export type Schema = { - state: Item<{ - dpopKey: EncodedKey - - iss: string - nonce: string - verifier?: string - appState?: string - }> - session: Item<{ - dpopKey: EncodedKey - - tokenSet: TokenSet - }> - - didCache: Item - dpopNonceCache: Item - handleCache: Item - metadataCache: Item -} - -export type DatabaseStore = GenericStore & { - getKeys: () => Promise -} - -const STORES = [ - 'state', - 'session', - - 'didCache', - 'dpopNonceCache', - 'handleCache', - 'metadataCache', -] as const - -export class RNOAuthDatabase { - async delete(key: string) { - await Storage.removeItem(key) - } - - protected createStore( - dbName: N, - { - encode, - decode, - maxAge, - }: { - encode: (value: V) => Schema[N]['value'] | PromiseLike - decode: (encoded: Schema[N]['value']) => V | PromiseLike - maxAge?: number - }, - ): DatabaseStore { - return { - get: async key => { - const itemJson = await Storage.getItem(`${dbName}.${key}`) - if (itemJson == null) return undefined - - const item = JSON.parse(itemJson) as Schema[N] - - // Too old, proactively delete - if (item.expiresAt != null && item.expiresAt < new Date()) { - await this.delete(`${dbName}.${key}`) - return undefined - } - - // Item found and valid. Decode - return decode(item.value) - }, - - getKeys: async () => { - const keys = await Storage.getAllKeys() - return keys.filter(key => key.startsWith(`${dbName}.`)) as string[] - }, - - set: async (key, value) => { - const item = { - value: await encode(value), - expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), - } as Schema[N] - - await Storage.setItem(`${dbName}.${key}`, JSON.stringify(item)) - }, - - del: async key => { - await this.delete(`${dbName}.${key}`) - }, - } - } - - getSessionStore(): DatabaseStore { - return this.createStore('session', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getStateStore(): DatabaseStore { - return this.createStore('state', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getDpopNonceCache(): undefined | DatabaseStore { - return this.createStore('dpopNonceCache', { - // No time limit. It is better to try with a potentially outdated nonce - // and potentially succeed rather than make requests without a nonce and - // 100% fail. - encode: value => value, - decode: encoded => encoded, - }) - } - - getDidCache(): undefined | DatabaseStore { - return this.createStore('didCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getHandleCache(): undefined | DatabaseStore { - return this.createStore('handleCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getMetadataCache(): undefined | DatabaseStore { - return this.createStore('metadataCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - async cleanup() { - await Promise.all( - STORES.map( - async storeName => - [ - storeName, - await tx - .objectStore(storeName) - .index('expiresAt') - .getAllKeys(query), - ] as const, - ), - ) - - const storesWithInvalidKeys = res.filter(r => r[1].length > 0) - - await db.transaction( - storesWithInvalidKeys.map(r => r[0]), - 'readwrite', - tx => - Promise.all( - storesWithInvalidKeys.map(async ([name, keys]) => - tx.objectStore(name).delete(keys), - ), - ), - ) - } - - async [Symbol.asyncDispose]() { - await this.cleanup() - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts deleted file mode 100644 index 8da68b8138..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts +++ /dev/null @@ -1,269 +0,0 @@ -import {GenericStore, Value} from '@atproto/caching' -import {DidDocument} from '@atproto/did' -import {ResolvedHandle} from '@atproto/handle-resolver' -import {DB, DBObjectStore} from '@atproto/indexed-db' -import {Key} from '@atproto/jwk' -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' - -type Item = { - value: V - expiresAt: null | Date -} - -type EncodedKey = { - keyId: string - keyPair: CryptoKeyPair -} - -function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { - throw new Error('Invalid key object') - } - return { - keyId: key.kid, - keyPair: key.cryptoKeyPair, - } -} - -async function decodeKey(encoded: EncodedKey): Promise { - return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) -} - -export type PopupStateData = - | PromiseRejectedResult - | PromiseFulfilledResult - -export type Schema = { - popup: Item - state: Item<{ - dpopKey: EncodedKey - - iss: string - nonce: string - verifier?: string - appState?: string - }> - session: Item<{ - dpopKey: EncodedKey - - tokenSet: TokenSet - }> - - didCache: Item - dpopNonceCache: Item - handleCache: Item - metadataCache: Item -} - -export type DatabaseStore = GenericStore & { - getKeys: () => Promise -} - -const STORES = [ - 'popup', - 'state', - 'session', - - 'didCache', - 'dpopNonceCache', - 'handleCache', - 'metadataCache', -] as const - -export class RNOAuthDatabase { - #dbPromise = DB.open( - '@atproto-oauth-client', - [ - db => { - for (const name of STORES) { - const store = db.createObjectStore(name) - store.createIndex('expiresAt', 'expiresAt', {unique: false}) - } - }, - ], - {durability: 'strict'}, - ) - - protected async run( - storeName: N, - mode: 'readonly' | 'readwrite', - fn: (s: DBObjectStore) => R | Promise, - ): Promise { - const db = await this.#dbPromise - return await db.transaction([storeName], mode, tx => - fn(tx.objectStore(storeName)), - ) - } - - protected createStore( - name: N, - { - encode, - decode, - maxAge, - }: { - encode: (value: V) => Schema[N]['value'] | PromiseLike - decode: (encoded: Schema[N]['value']) => V | PromiseLike - maxAge?: number - }, - ): DatabaseStore { - return { - get: async key => { - // Find item in store - const item = await this.run(name, 'readonly', dbStore => { - return dbStore.get(key) - }) - - // Not found - if (item === undefined) return undefined - - // Too old, proactively delete - if (item.expiresAt != null && item.expiresAt < new Date()) { - await this.run(name, 'readwrite', dbStore => { - return dbStore.delete(key) - }) - return undefined - } - - // Item found and valid. Decode - return decode(item.value) - }, - - getKeys: async () => { - const keys = await this.run(name, 'readonly', dbStore => { - return dbStore.getAllKeys() - }) - return keys.filter(key => typeof key === 'string') as string[] - }, - - set: async (key, value) => { - // Create encoded item record - const item = { - value: await encode(value), - expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), - } as Schema[N] - - // Store item record - await this.run(name, 'readwrite', dbStore => { - return dbStore.put(item, key) - }) - }, - - del: async key => { - // Delete - await this.run(name, 'readwrite', dbStore => { - return dbStore.delete(key) - }) - }, - } - } - - getSessionStore(): DatabaseStore { - return this.createStore('session', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getStateStore(): DatabaseStore { - return this.createStore('state', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getPopupStore(): DatabaseStore { - return this.createStore('popup', { - encode: value => value, - decode: encoded => encoded, - }) - } - - getDpopNonceCache(): undefined | DatabaseStore { - return this.createStore('dpopNonceCache', { - // No time limit. It is better to try with a potentially outdated nonce - // and potentially succeed rather than make requests without a nonce and - // 100% fail. - encode: value => value, - decode: encoded => encoded, - }) - } - - getDidCache(): undefined | DatabaseStore { - return this.createStore('didCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getHandleCache(): undefined | DatabaseStore { - return this.createStore('handleCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getMetadataCache(): undefined | DatabaseStore { - return this.createStore('metadataCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - async cleanup() { - const db = await this.#dbPromise - const query = IDBKeyRange.lowerBound(new Date()) - const res = await db.transaction(STORES, 'readonly', tx => - Promise.all( - STORES.map( - async storeName => - [ - storeName, - await tx - .objectStore(storeName) - .index('expiresAt') - .getAllKeys(query), - ] as const, - ), - ), - ) - - const storesWithInvalidKeys = res.filter(r => r[1].length > 0) - - await db.transaction( - storesWithInvalidKeys.map(r => r[0]), - 'readwrite', - tx => - Promise.all( - storesWithInvalidKeys.map(async ([name, keys]) => - tx.objectStore(name).delete(keys), - ), - ), - ) - } - - async [Symbol.asyncDispose]() { - // TODO: call cleanup at a constant interval ? - await this.cleanup() - - const db = await this.#dbPromise - await (db[Symbol.asyncDispose] || db[Symbol.dispose]).call(db) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/store.ts b/modules/expo-bluesky-oauth-client/src-old/store.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/expo-bluesky-oauth-client/src-old/store.web.ts b/modules/expo-bluesky-oauth-client/src-old/store.web.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/expo-bluesky-oauth-client/src-old/util.web.ts b/modules/expo-bluesky-oauth-client/src-old/util.web.ts deleted file mode 100644 index 654365aae5..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/util.web.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-ignore web only, this silences errors throughout the whole file for crypto being missing -const crypto = global.crypto - -export async function generateKeyPair(algs: string[], extractable = false) { - const errors: unknown[] = [] - try { - return await crypto.subtle.generateKey( - { - name: 'ECDSA', - namedCurve: `P-256`, - }, - extractable, - ['sign', 'verify'], - ) - } catch (err) { - errors.push(err) - } - - console.log(errors) - throw new AggregateError(errors, 'Failed to generate keypair') -} - -export function isSignatureKeyPair( - v: unknown, - extractable?: boolean, -): v is CryptoKeyPair { - return ( - typeof v === 'object' && - v !== null && - 'privateKey' in v && - v.privateKey instanceof CryptoKey && - v.privateKey.type === 'private' && - (extractable == null || v.privateKey.extractable === extractable) && - v.privateKey.usages.includes('sign') && - 'publicKey' in v && - v.publicKey instanceof CryptoKey && - v.publicKey.type === 'public' && - v.publicKey.extractable === true && - v.publicKey.usages.includes('verify') - ) -}