@marktiderman/genesis-supabase 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +523 -1
  2. package/dist/blocks.d.ts +19 -0
  3. package/dist/blocks.js +3 -0
  4. package/dist/blocks.js.map +1 -0
  5. package/dist/chunk-FAA2FTF4.js +110 -0
  6. package/dist/chunk-FAA2FTF4.js.map +1 -0
  7. package/dist/chunk-T2HGFC4C.js +37 -0
  8. package/dist/chunk-T2HGFC4C.js.map +1 -0
  9. package/dist/index-BSGz3RGk.d.ts +317 -0
  10. package/dist/index.d.ts +4 -78
  11. package/dist/index.js +63 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/native.d.ts +233 -5
  14. package/dist/native.js +196 -1
  15. package/dist/native.js.map +1 -1
  16. package/functions/genesis-apple-revoke/apple.ts +237 -0
  17. package/functions/genesis-apple-revoke/handler.ts +227 -0
  18. package/functions/genesis-apple-revoke/index.ts +55 -0
  19. package/functions/genesis-push-send/expo.ts +117 -0
  20. package/functions/genesis-push-send/handler.ts +462 -0
  21. package/functions/genesis-push-send/index.ts +54 -0
  22. package/functions/genesis-support-cleanup/handler.ts +80 -0
  23. package/functions/genesis-support-cleanup/index.ts +39 -0
  24. package/functions/genesis-support-intake/contract.ts +193 -0
  25. package/functions/genesis-support-intake/handler.ts +170 -0
  26. package/functions/genesis-support-intake/index.ts +47 -0
  27. package/functions/genesis-support-notify/handler.ts +167 -0
  28. package/functions/genesis-support-notify/index.ts +15 -0
  29. package/functions/tests/genesis-apple-revoke_test.ts +81 -0
  30. package/functions/tests/genesis-push-send_test.ts +53 -0
  31. package/migrations/0001_genesis_account_deletion.sql +133 -0
  32. package/migrations/0002_genesis_app_release_policy.sql +82 -0
  33. package/migrations/0003_genesis_apple_token_revocation.sql +205 -0
  34. package/migrations/0004_genesis_tenant_features.sql +333 -0
  35. package/migrations/0005_genesis_push.sql +524 -0
  36. package/migrations/0006_genesis_support.sql +846 -0
  37. package/migrations/tests/0001_genesis_account_deletion.test.sql +138 -0
  38. package/migrations/tests/0002_genesis_app_release_policy.test.sql +148 -0
  39. package/migrations/tests/0003_genesis_apple_token_revocation.test.sql +230 -0
  40. package/migrations/tests/0004_genesis_tenant_features.test.sql +418 -0
  41. package/migrations/tests/0005_genesis_push.test.sql +407 -0
  42. package/migrations/tests/0006_genesis_support.test.sql +552 -0
  43. package/package.json +92 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/native.ts"],"names":[],"mappings":"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","file":"native.js","sourcesContent":["/**\n * React Native helpers for a Supabase session. Nothing here imports React\n * Native or Expo: pass in `expo-secure-store`, `AppState` and a plain store,\n * so the package stays light and the helpers are easy to test.\n */\nimport type { SessionStorage } from \"./index\";\n\n/** The parts of `expo-secure-store` the Keychain storage uses. */\nexport interface SecureStoreLike {\n getItemAsync(key: string, options?: Record<string, unknown>): Promise<string | null>;\n setItemAsync(key: string, value: string, options?: Record<string, unknown>): Promise<void>;\n deleteItemAsync(key: string, options?: Record<string, unknown>): Promise<void>;\n /** `SecureStore.AFTER_FIRST_UNLOCK`, when available. */\n AFTER_FIRST_UNLOCK?: unknown;\n}\n\n/** @stability Beta */\nexport interface KeychainStorageOptions {\n /** UTF-8 bytes per Keychain item. A Keychain item should stay under about 2 KB; a Supabase session is larger. Default 1800. */\n chunkSize?: number;\n /** Extra options for every SecureStore call. Default: readable after first unlock, so a background refresh works. */\n secureStoreOptions?: Record<string, unknown>;\n}\n\n/**\n * Session storage in the Keychain (iOS) or Keystore (Android).\n *\n * A value is split into numbered chunks. Each write goes to a new generation of\n * chunks, and one manifest item (`<key>.n` = `generation:count`) switches to it\n * only once every chunk is written, so a write cut short leaves the previous\n * session whole. Chunks split between characters (never inside one) and\n * stay under `chunkSize` UTF-8 bytes. A pending marker (`<key>.p`) names a\n * generation while it is being written, so chunks left by a write that was\n * cut short or crashed are swept on the next write or remove. Writes to the\n * same key run one at a time, so two token refreshes can't interleave.\n *\n * @stability Beta\n */\nexport function keychainStorage(secureStore: SecureStoreLike, options: KeychainStorageOptions = {}): SessionStorage {\n const size = options.chunkSize ?? 1800;\n const opts = options.secureStoreOptions ?? (secureStore.AFTER_FIRST_UNLOCK !== undefined ? { keychainAccessible: secureStore.AFTER_FIRST_UNLOCK } : {});\n const chunkKey = (key: string, gen: string, i: number) => `${key}.${gen}.${i}`;\n let counter = 0;\n const queues = new Map<string, Promise<unknown>>();\n\n /** Run `task` after every earlier write to `key` has settled. */\n const serial = <T>(key: string, task: () => Promise<T>): Promise<T> => {\n const run = (queues.get(key) ?? Promise.resolve()).catch(() => undefined).then(task);\n queues.set(key, run);\n return run;\n };\n\n const parse = (raw: string | null) => {\n const [gen, count] = (raw ?? \"\").split(\":\");\n return gen && Number(count) > 0 ? { gen, count: Number(count) } : null;\n };\n const manifest = async (key: string) => parse(await secureStore.getItemAsync(`${key}.n`, opts));\n const pending = async (key: string) => parse(await secureStore.getItemAsync(`${key}.p`, opts));\n const drop = (key: string, gen: string, count: number) =>\n Promise.all(Array.from({ length: count }, (_, i) => secureStore.deleteItemAsync(chunkKey(key, gen, i), opts)));\n\n /** Remove chunks of a write that never completed (unless they are the current generation). */\n const sweepPending = async (key: string, current: string | undefined) => {\n const p = await pending(key);\n if (!p) return;\n if (p.gen !== current) await drop(key, p.gen, p.count);\n await secureStore.deleteItemAsync(`${key}.p`, opts);\n };\n\n return {\n getItem(key) {\n // Reads queue with writes, so a write can't swap the manifest and drop\n // the old chunks halfway through a read.\n return serial(key, async () => {\n const m = await manifest(key);\n if (!m) return null;\n const parts = await Promise.all(Array.from({ length: m.count }, (_, i) => secureStore.getItemAsync(chunkKey(key, m.gen, i), opts)));\n return parts.some((p) => p === null) ? null : parts.join(\"\");\n });\n },\n setItem(key, value) {\n return serial(key, async () => {\n const before = await manifest(key);\n await sweepPending(key, before?.gen);\n const gen = `${Date.now().toString(36)}${(counter++).toString(36)}`;\n const parts = splitUtf8(value, size);\n await secureStore.setItemAsync(`${key}.p`, `${gen}:${parts.length}`, opts);\n // Let every chunk write settle before failing, so a late write can't land\n // after the next operation has swept this generation.\n const written = await Promise.allSettled(parts.map((p, i) => secureStore.setItemAsync(chunkKey(key, gen, i), p, opts)));\n const failed = written.find((r): r is PromiseRejectedResult => r.status === \"rejected\");\n if (failed) throw failed.reason;\n await secureStore.setItemAsync(`${key}.n`, `${gen}:${parts.length}`, opts);\n await secureStore.deleteItemAsync(`${key}.p`, opts);\n if (before) await drop(key, before.gen, before.count);\n });\n },\n removeItem(key) {\n return serial(key, async () => {\n const m = await manifest(key);\n await sweepPending(key, m?.gen);\n // Chunks first: the manifest is what finds them, so it goes last.\n if (m) await drop(key, m.gen, m.count);\n await secureStore.deleteItemAsync(`${key}.n`, opts);\n });\n },\n };\n}\n\n/** The parts of a Supabase client's auth the refresh helper uses. */\nexport interface AutoRefreshAuth {\n auth: { startAutoRefresh(): Promise<void> | void; stopAutoRefresh(): Promise<void> | void };\n}\n\n/** The parts of React Native's `AppState` the refresh helper uses. */\nexport interface AppStateLike {\n currentState?: string | null;\n addEventListener(type: \"change\", listener: (state: string) => void): { remove(): void };\n}\n\n/**\n * Refresh the session only while the app is in front, as Supabase recommends\n * for React Native. Returns the unsubscribe.\n *\n * @stability Beta\n */\nexport function refreshWhileActive(client: AutoRefreshAuth, appState: AppStateLike): () => void {\n const apply = (state: string | null | undefined) => {\n if (state === \"active\") void client.auth.startAutoRefresh();\n else void client.auth.stopAutoRefresh();\n };\n apply(appState.currentState ?? \"active\");\n const sub = appState.addEventListener(\"change\", apply);\n return () => {\n sub.remove();\n void client.auth.stopAutoRefresh();\n };\n}\n\n/** @stability Beta */\nexport interface WipeOnReinstallOptions {\n /** The session storage (usually `keychainStorage`). */\n session: SessionStorage;\n /** The session's storage key. Supabase's default is `sb-<project ref>-auth-token`. */\n sessionKey: string;\n /** A store that an uninstall deletes, e.g. AsyncStorage. The Keychain survives an uninstall; this is how we notice one. */\n installMarker: SessionStorage;\n /** The marker's key. Default `genesis.installed`. */\n markerKey?: string;\n}\n\n/**\n * On iOS the Keychain outlives an uninstall, so a reinstalled app would sign\n * the last person back in. Call this before creating the client: on the first\n * launch after an install it removes any session left in the Keychain, then\n * sets a marker so later launches keep the session. Returns whether it wiped.\n *\n * @stability Beta\n */\nexport async function wipeSessionOnReinstall({ session, sessionKey, installMarker, markerKey = \"genesis.installed\" }: WipeOnReinstallOptions): Promise<boolean> {\n if ((await installMarker.getItem(markerKey)) !== null) return false;\n // The session, plus what supabase-js keeps beside it: the PKCE verifier (the\n // single legacy slot, each indexed flow's slot and the index) and, with\n // userStorage, the user.\n const indexKey = `${sessionKey}-flows-code-verifier`;\n for (const flowId of parseFlowIndex(await session.getItem(indexKey))) await session.removeItem(`${sessionKey}-flow-${flowId}-code-verifier`);\n for (const key of [sessionKey, `${sessionKey}-code-verifier`, indexKey, `${sessionKey}-user`]) await session.removeItem(key);\n await installMarker.setItem(markerKey, \"1\");\n return true;\n}\n\n/** UTF-8 byte length of one code point. */\nconst utf8Length = (codePoint: number) => (codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4);\n\n/** Split text into pieces of at most `maxBytes` UTF-8 bytes, never inside a character. */\nexport function splitUtf8(text: string, maxBytes: number): string[] {\n if (text === \"\") return [\"\"];\n const parts: string[] = [];\n let current = \"\";\n let bytes = 0;\n for (const ch of text) {\n const n = utf8Length(ch.codePointAt(0)!);\n if (bytes + n > maxBytes && current) {\n parts.push(current);\n current = \"\";\n bytes = 0;\n }\n current += ch;\n bytes += n;\n }\n parts.push(current);\n return parts;\n}\n\n/** supabase-js's PKCE flow index: a JSON array of flow ids. Anything else is ignored. */\nfunction parseFlowIndex(raw: string | null): string[] {\n if (!raw) return [];\n try {\n const ids: unknown = JSON.parse(raw);\n return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === \"string\" && /^[\\w-]{1,128}$/.test(id)) : [];\n } catch {\n return [];\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/sign-in.ts","../src/native.ts"],"names":[],"mappings":"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","file":"native.js","sourcesContent":["/**\n * Native sign-in onto a Supabase session: finishing an OAuth redirect (PKCE),\n * Sign in with Apple and native Google sign-in. Nothing here imports Expo or\n * the Google module: pass the modules in.\n * Exported from `@marktiderman/genesis-supabase/native`.\n */\nimport type { Session, SupabaseClient, User } from \"@supabase/supabase-js\";\n\n/**\n * Why a sign-in didn't produce a session. `code` is the provider's error\n * (`access_denied`…), Supabase Auth's error code, or one of Genesis's own:\n * `missing_code` (the redirect carried neither a code nor an error),\n * `oauth_error`, `exchange_failed`, `apple_unavailable`, `apple_failed`,\n * `apple_no_token`, `google_failed`, `google_no_token`,\n * `google_nonce_missing`, `bad_digest`, `sign_in_failed`, `no_session`.\n *\n * @stability Beta\n */\nexport class GenesisAuthError extends Error {\n override name = \"GenesisAuthError\";\n readonly code: string;\n /**\n * The provider's own `error_description`, as it came in the redirect.\n * UNTRUSTED: any app or web page can open your callback link with any text\n * here, so never show it as an official message. Log it; show `message`.\n */\n readonly providerMessage?: string;\n constructor(code: string, message: string, options: { cause?: unknown; providerMessage?: string } = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.code = code;\n if (options.providerMessage !== undefined) this.providerMessage = options.providerMessage;\n }\n}\n\n/** Fixed, safe copy for the redirect errors an app is likely to see; anything else gets the fallback. */\nconst REDIRECT_MESSAGES: Record<string, string> = {\n access_denied: \"Sign-in was canceled or not allowed.\",\n otp_expired: \"That sign-in link has expired. Ask for a new one.\",\n flow_state_expired: \"That sign-in took too long. Try again.\",\n flow_state_not_found: \"That sign-in has already been used or has expired. Try again.\",\n bad_oauth_state: \"That sign-in couldn't be checked. Try again.\",\n bad_oauth_callback: \"That sign-in couldn't be checked. Try again.\",\n provider_email_needs_verification: \"Verify your email with the provider, then try again.\",\n server_error: \"The sign-in service had a problem. Try again.\",\n temporarily_unavailable: \"The sign-in service is busy. Try again in a moment.\",\n};\nconst REDIRECT_FALLBACK = \"Sign-in didn't complete. Try again.\";\n\n/** The parts of a Supabase client the sign-in helpers use. A `SupabaseClient` fits. */\nexport interface SignInClient {\n auth: Pick<SupabaseClient[\"auth\"], \"exchangeCodeForSession\" | \"signInWithIdToken\">;\n}\n\n/** @stability Beta */\nexport type OAuthRedirectResult =\n | {\n session: Session;\n /** `\"recovery\"` after a password-reset link (send them to set a new password), otherwise `null`. */\n redirectType: string | null;\n error?: undefined;\n }\n | { session?: undefined; redirectType?: undefined; error: GenesisAuthError };\n\nconst hasOwn = (o: object, key: string) => Object.prototype.hasOwnProperty.call(o, key);\n\n/**\n * The query and fragment parameters of a URL (the query wins for a key in\n * both). Hand-parsed: React Native's `URL` has no reliable `searchParams`.\n */\nexport function redirectParams(url: string): Record<string, string> {\n const params: Record<string, string> = Object.create(null);\n const hash = url.indexOf(\"#\");\n const query = url.indexOf(\"?\");\n const parts = [\n query >= 0 && (hash < 0 || query < hash) ? url.slice(query + 1, hash >= 0 ? hash : undefined) : \"\",\n hash >= 0 ? url.slice(hash + 1) : \"\",\n ];\n for (const part of parts) {\n for (const pair of part.split(\"&\")) {\n if (!pair) continue;\n const eq = pair.indexOf(\"=\");\n const key = decode(eq >= 0 ? pair.slice(0, eq) : pair);\n if (!hasOwn(params, key)) params[key] =decode(eq >= 0 ? pair.slice(eq + 1) : \"\");\n }\n }\n return params;\n}\n\nfunction decode(s: string): string {\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return s;\n }\n}\n\n/**\n * Finish an OAuth sign-in from the URL the browser came back to. With a `code`\n * it exchanges it for a session (PKCE: the verifier is already in the client's\n * storage), passing the redirect's `sb_flow_id` so the right flow's verifier\n * is used; with an `error` it returns that error and makes no request.\n * Nothing is thrown; every failure is `{ error }`.\n *\n * Pass only the URL `WebBrowser.openAuthSessionAsync` resolved with, for the\n * sign-in this app just started. A code is single-use and the exchange uses\n * up the stored verifier whatever happens, so a stray `?code=` from another\n * link spends the sign-in in progress, and handling the same redirect twice\n * (the auth-session result and a `Linking` listener) fails the second time.\n * Handle each redirect once.\n *\n * The error `message` is fixed copy chosen by `code`; the redirect's own text\n * is only in `providerMessage`, which is untrusted.\n *\n * @stability Beta\n */\nexport async function completeOAuthRedirect(client: SignInClient, url: string): Promise<OAuthRedirectResult> {\n const params = redirectParams(url);\n if (params.error || params.error_description || params.error_code) {\n const code = params.error_code || params.error || \"oauth_error\";\n const message = REDIRECT_MESSAGES[code] ?? (params.error ? REDIRECT_MESSAGES[params.error] : undefined) ?? REDIRECT_FALLBACK;\n return { error: new GenesisAuthError(code, message, { providerMessage: params.error_description || undefined }) };\n }\n if (!params.code) return { error: new GenesisAuthError(\"missing_code\", \"The sign-in redirect carried no code\") };\n try {\n const { data, error } = await client.auth.exchangeCodeForSession(\n params.code,\n params.sb_flow_id ? { flowId: params.sb_flow_id } : undefined,\n );\n if (error) return { error: new GenesisAuthError(error.code ?? \"exchange_failed\", error.message, { cause: error }) };\n if (!data.session) return { error: new GenesisAuthError(\"no_session\", \"The code exchange returned no session\") };\n // supabase-js returns redirectType at run time without typing it.\n const redirectType = (data as { redirectType?: string | null }).redirectType ?? null;\n return { session: data.session, redirectType };\n } catch (e) {\n return { error: new GenesisAuthError(\"exchange_failed\", e instanceof Error ? e.message : \"The code exchange failed\", { cause: e }) };\n }\n}\n\n/** A person's name as `expo-apple-authentication` returns it. */\nexport interface AppleFullNameLike {\n givenName?: string | null;\n middleName?: string | null;\n familyName?: string | null;\n}\n\n/** The parts of `expo-apple-authentication` that {@link signInWithApple} uses. Pass the module itself. */\nexport interface AppleAuthenticationLike {\n isAvailableAsync(): Promise<boolean>;\n signInAsync(options: { requestedScopes?: number[]; nonce?: string }): Promise<{\n identityToken: string | null;\n /** Single-use, valid about five minutes: what a server exchanges for the Apple refresh token it revokes on deletion. */\n authorizationCode: string | null;\n fullName: AppleFullNameLike | null;\n email: string | null;\n }>;\n AppleAuthenticationScope: { FULL_NAME: number; EMAIL: number };\n}\n\n/** @stability Beta */\nexport interface SignInWithAppleOptions {\n /**\n * SHA-256 of a string as lowercase hex, e.g.\n * `(s) => Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, s)` from `expo-crypto`.\n * Apple gets the hash; Supabase gets the raw nonce and checks the token against it.\n */\n digest(value: string): Promise<string>;\n /**\n * A fresh random nonce. Default: 32 bytes from `crypto.getRandomValues`, as\n * hex. On React Native that is the expo-crypto polyfill\n * `createGenesisSupabase({ platform: \"native\", crypto })` installs; leave\n * this out rather than pass something with less randomness.\n */\n randomNonce?: () => string;\n}\n\n/** @stability Beta */\nexport type SignInWithAppleResult =\n | {\n session: Session;\n user: User;\n /** The name Apple shared. Apple sends it on the first sign-in only: save it then. */\n fullName: string | null;\n /** The email Apple shared (possibly a private relay address), on the first sign-in only. */\n email: string | null;\n /**\n * Apple's single-use authorization code (valid about five minutes). Send it\n * to your server straight away: it exchanges it for the Apple refresh token\n * that account deletion must revoke. See the README.\n */\n authorizationCode: string | null;\n canceled?: undefined;\n error?: undefined;\n }\n | { canceled: true; session?: undefined; error?: undefined }\n | {\n error: GenesisAuthError;\n /** When Apple shared a name but Supabase then refused: Apple won't send it again, so keep it for the retry. */\n fullName?: string | null;\n session?: undefined;\n canceled?: undefined;\n };\n\n/**\n * Native Sign in with Apple straight into a Supabase session: checks the\n * device supports it, asks for name and email with a SHA-256 hashed nonce, and\n * signs in with the identity token. Closing Apple's sheet is `{ canceled: true }`,\n * not an error. Nothing is thrown; every failure is `{ error }`.\n *\n * @stability Beta\n */\nexport async function signInWithApple(\n client: SignInClient,\n apple: AppleAuthenticationLike,\n { digest, randomNonce = defaultNonce }: SignInWithAppleOptions,\n): Promise<SignInWithAppleResult> {\n const unavailable = (cause?: unknown) => ({\n error: new GenesisAuthError(\"apple_unavailable\", \"Sign in with Apple isn't available on this device\", { cause }),\n });\n try {\n if (!(await apple.isAvailableAsync())) return unavailable();\n } catch (e) {\n return unavailable(e);\n }\n let credential: Awaited<ReturnType<AppleAuthenticationLike[\"signInAsync\"]>>;\n let nonce: string;\n try {\n nonce = randomNonce();\n const hashed = await digest(nonce);\n if (!/^[0-9a-f]{64}$/.test(hashed)) {\n return { error: new GenesisAuthError(\"bad_digest\", \"signInWithApple: digest must return SHA-256 as 64 lowercase hex characters\") };\n }\n credential = await apple.signInAsync({\n requestedScopes: [apple.AppleAuthenticationScope.FULL_NAME, apple.AppleAuthenticationScope.EMAIL],\n nonce: hashed,\n });\n } catch (e) {\n if ((e as { code?: unknown } | null)?.code === \"ERR_REQUEST_CANCELED\") return { canceled: true };\n return { error: new GenesisAuthError(\"apple_failed\", e instanceof Error ? e.message : \"Sign in with Apple failed\", { cause: e }) };\n }\n const fullName = formatFullName(credential.fullName);\n if (!credential.identityToken) return { error: new GenesisAuthError(\"apple_no_token\", \"Apple returned no identity token\"), fullName };\n try {\n const { data, error } = await client.auth.signInWithIdToken({ provider: \"apple\", token: credential.identityToken, nonce });\n if (error) return { error: new GenesisAuthError(error.code ?? \"sign_in_failed\", error.message, { cause: error }), fullName };\n if (!data.session || !data.user) return { error: new GenesisAuthError(\"no_session\", \"Sign-in returned no session\"), fullName };\n return {\n session: data.session,\n user: data.user,\n fullName,\n email: credential.email ?? null,\n authorizationCode: credential.authorizationCode ?? null,\n };\n } catch (e) {\n return { error: new GenesisAuthError(\"sign_in_failed\", e instanceof Error ? e.message : \"Sign-in failed\", { cause: e }), fullName };\n }\n}\n\n/**\n * The part of `@react-native-google-signin/google-signin` that\n * {@link signInWithGoogle} uses: its Universal sign-in module,\n * `GoogleOneTapSignIn`, configured (`configure({ webClientId, iosClientId })`)\n * before the first sign-in. Pass the module itself.\n *\n * Only Universal sign-in passes a nonce into the ID token on Android and iOS.\n * The free package's original `GoogleSignin.signIn` can't (its `nonce` is iOS\n * and paid-version only), so its tokens fail the nonce check below.\n */\nexport interface GoogleSignInLike {\n authenticate(params?: { nonce?: string }): Promise<{\n user?: { idToken?: string | null; user?: { email?: string | null; name?: string | null } | null } | null;\n error?: { code?: string | number; message?: string } | null;\n isCancelled?: boolean;\n }>;\n}\n\n/** @stability Beta */\nexport interface SignInWithGoogleOptions {\n /** SHA-256 of a string as lowercase hex, as for {@link signInWithApple}. Google gets the hash; Supabase gets the raw nonce. */\n digest(value: string): Promise<string>;\n /** A fresh random nonce. Default: 32 bytes from `crypto.getRandomValues`, as hex. */\n randomNonce?: () => string;\n}\n\n/** @stability Beta */\nexport type SignInWithGoogleResult =\n | {\n session: Session;\n user: User;\n /** The name Google shared, if any. */\n fullName: string | null;\n email: string | null;\n canceled?: undefined;\n error?: undefined;\n }\n | { canceled: true; session?: undefined; error?: undefined }\n | { error: GenesisAuthError; session?: undefined; canceled?: undefined };\n\n/**\n * Native Google sign-in straight into a Supabase session, as Supabase's\n * Google guide describes: a fresh random nonce, its SHA-256 (hex) to Google,\n * which puts it in the ID token, and the raw nonce to `signInWithIdToken`,\n * which checks the two match, so a token minted for another sign-in, which\n * carries that sign-in's nonce, can't be replayed through this app. Before\n * Supabase sees the token it is checked here too: a token whose `nonce` claim\n * isn't this sign-in's hash (a module that ignored the nonce) is\n * `google_nonce_missing`, never sent. Closing Google's sheet is\n * `{ canceled: true }`. Nothing is thrown; every failure is `{ error }`.\n *\n * Keep the Supabase Google provider's \"Skip nonce check\" off: this helper\n * needs no exception, and turning it on lets a stolen nonce-bearing ID token\n * sign in. What a nonce can't stop: Supabase still accepts an ID token with\n * no nonce claim when the caller sends none, so a stolen nonce-less token for\n * one of your client ids signs in through a direct API call whatever the app\n * does. Keep the \"Authorized Client IDs\" list to your own apps.\n *\n * @stability Beta\n */\nexport async function signInWithGoogle(\n client: SignInClient,\n google: GoogleSignInLike,\n { digest, randomNonce = defaultNonce }: SignInWithGoogleOptions,\n): Promise<SignInWithGoogleResult> {\n let nonce: string;\n let hashed: string;\n let response: Awaited<ReturnType<GoogleSignInLike[\"authenticate\"]>>;\n try {\n nonce = randomNonce();\n hashed = await digest(nonce);\n if (!/^[0-9a-f]{64}$/.test(hashed)) {\n return { error: new GenesisAuthError(\"bad_digest\", \"signInWithGoogle: digest must return SHA-256 as 64 lowercase hex characters\") };\n }\n response = await google.authenticate({ nonce: hashed });\n } catch (e) {\n return { error: new GenesisAuthError(\"google_failed\", e instanceof Error ? e.message : \"Google sign-in failed\", { cause: e }) };\n }\n if (response.isCancelled) return { canceled: true };\n if (response.error) {\n return { error: new GenesisAuthError(\"google_failed\", response.error.message ?? `Google sign-in failed (${String(response.error.code ?? \"unknown\")})`, { cause: response.error }) };\n }\n const token = response.user?.idToken;\n if (!token) return { error: new GenesisAuthError(\"google_no_token\", \"Google returned no ID token\") };\n if (jwtClaim(token, \"nonce\") !== hashed) {\n return {\n error: new GenesisAuthError(\n \"google_nonce_missing\",\n \"Google's ID token doesn't carry this sign-in's nonce. Use the Universal sign-in module (GoogleOneTapSignIn) of @react-native-google-signin/google-signin; the original GoogleSignin can't pass one.\",\n ),\n };\n }\n try {\n const { data, error } = await client.auth.signInWithIdToken({ provider: \"google\", token, nonce });\n if (error) return { error: new GenesisAuthError(error.code ?? \"sign_in_failed\", error.message, { cause: error }) };\n if (!data.session || !data.user) return { error: new GenesisAuthError(\"no_session\", \"Sign-in returned no session\") };\n const profile = response.user?.user;\n return { session: data.session, user: data.user, fullName: profile?.name?.trim() || null, email: profile?.email ?? null };\n } catch (e) {\n return { error: new GenesisAuthError(\"sign_in_failed\", e instanceof Error ? e.message : \"Sign-in failed\", { cause: e }) };\n }\n}\n\n/** One string claim of a JWT's payload, unverified (Supabase verifies the signature), or undefined. */\nfunction jwtClaim(token: string, claim: string): string | undefined {\n const payload = token.split(\".\")[1];\n if (!payload) return undefined;\n try {\n const value: unknown = JSON.parse(decodeBase64Url(payload))?.[claim];\n return typeof value === \"string\" ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** base64url to a UTF-8 string, without atob, Buffer or TextDecoder (not on every React Native runtime). */\nfunction decodeBase64Url(input: string): string {\n let escaped = \"\";\n let buffer = 0;\n let bits = 0;\n for (const ch of input.replace(/=+$/, \"\")) {\n const v = B64.indexOf(ch === \"+\" ? \"-\" : ch === \"/\" ? \"_\" : ch);\n if (v < 0) throw new Error(\"not base64url\");\n buffer = (buffer << 6) | v;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n escaped += `%${((buffer >> bits) & 0xff).toString(16).padStart(2, \"0\")}`;\n buffer &= (1 << bits) - 1;\n }\n }\n return decodeURIComponent(escaped);\n}\n\n/** \"Given Middle Family\", or null when Apple shared no name. */\nexport function formatFullName(name: AppleFullNameLike | null | undefined): string | null {\n if (!name) return null;\n return [name.givenName, name.middleName, name.familyName].map((p) => p?.trim()).filter(Boolean).join(\" \") || null;\n}\n\nfunction defaultNonce(): string {\n const c = (globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => Uint8Array } }).crypto;\n // A nonce is a secret: no fallback to anything weaker than the platform's CSPRNG.\n if (!c?.getRandomValues) throw new Error(\"[genesis] sign-in nonce: no crypto.getRandomValues here; install react-native-get-random-values or expo-crypto's getRandomValues polyfill, or pass randomNonce returning 32 random bytes\");\n return Array.from(c.getRandomValues(new Uint8Array(32)), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n","/**\n * React Native helpers for a Supabase session. Nothing here imports React\n * Native or Expo: pass in `expo-secure-store`, `AppState`,\n * `expo-apple-authentication` and a plain store, so the package stays light\n * and the helpers are easy to test.\n */\nimport type { SessionStorage } from \"./index\";\n\nexport {\n assertPkceCrypto,\n hasPkceCrypto,\n installWebCrypto,\n isDigestOnlyPolyfill,\n type ExpoCryptoLike,\n type WebCryptoSource,\n} from \"./web-crypto\";\n\nexport {\n GenesisAuthError,\n completeOAuthRedirect,\n formatFullName,\n redirectParams,\n signInWithApple,\n signInWithGoogle,\n type AppleAuthenticationLike,\n type AppleFullNameLike,\n type GoogleSignInLike,\n type OAuthRedirectResult,\n type SignInClient,\n type SignInWithAppleOptions,\n type SignInWithAppleResult,\n type SignInWithGoogleOptions,\n type SignInWithGoogleResult,\n} from \"./sign-in\";\n\n/** The parts of `expo-secure-store` the Keychain storage uses. */\nexport interface SecureStoreLike {\n getItemAsync(key: string, options?: Record<string, unknown>): Promise<string | null>;\n setItemAsync(key: string, value: string, options?: Record<string, unknown>): Promise<void>;\n deleteItemAsync(key: string, options?: Record<string, unknown>): Promise<void>;\n /** `SecureStore.AFTER_FIRST_UNLOCK`, when available. */\n AFTER_FIRST_UNLOCK?: unknown;\n}\n\n/** @stability Beta */\nexport interface KeychainStorageOptions {\n /** UTF-8 bytes per Keychain item. A Keychain item should stay under about 2 KB; a Supabase session is larger. Default 1800. */\n chunkSize?: number;\n /** Extra options for every SecureStore call. Default: readable after first unlock, so a background refresh works. */\n secureStoreOptions?: Record<string, unknown>;\n}\n\n/**\n * Session storage in the Keychain (iOS) or Keystore (Android).\n *\n * A value is split into numbered chunks. Each write goes to a new generation of\n * chunks, and one manifest item (`<key>.n` = `generation:count`) switches to it\n * only once every chunk is written, so a write cut short leaves the previous\n * session whole. Chunks split between characters (never inside one) and\n * stay under `chunkSize` UTF-8 bytes. A pending marker (`<key>.p`) names a\n * generation while it is being written, so chunks left by a write that was\n * cut short or crashed are swept on the next write or remove. Writes to the\n * same key run one at a time, so two token refreshes can't interleave.\n *\n * @stability Beta\n */\nexport function keychainStorage(secureStore: SecureStoreLike, options: KeychainStorageOptions = {}): SessionStorage {\n const size = options.chunkSize ?? 1800;\n const opts = options.secureStoreOptions ?? (secureStore.AFTER_FIRST_UNLOCK !== undefined ? { keychainAccessible: secureStore.AFTER_FIRST_UNLOCK } : {});\n const chunkKey = (key: string, gen: string, i: number) => `${key}.${gen}.${i}`;\n let counter = 0;\n const queues = new Map<string, Promise<unknown>>();\n\n /** Run `task` after every earlier write to `key` has settled. */\n const serial = <T>(key: string, task: () => Promise<T>): Promise<T> => {\n const run = (queues.get(key) ?? Promise.resolve()).catch(() => undefined).then(task);\n queues.set(key, run);\n return run;\n };\n\n const parse = (raw: string | null) => {\n const [gen, count] = (raw ?? \"\").split(\":\");\n return gen && Number(count) > 0 ? { gen, count: Number(count) } : null;\n };\n const manifest = async (key: string) => parse(await secureStore.getItemAsync(`${key}.n`, opts));\n const pending = async (key: string) => parse(await secureStore.getItemAsync(`${key}.p`, opts));\n const drop = (key: string, gen: string, count: number) =>\n Promise.all(Array.from({ length: count }, (_, i) => secureStore.deleteItemAsync(chunkKey(key, gen, i), opts)));\n\n /** Remove chunks of a write that never completed (unless they are the current generation). */\n const sweepPending = async (key: string, current: string | undefined) => {\n const p = await pending(key);\n if (!p) return;\n if (p.gen !== current) await drop(key, p.gen, p.count);\n await secureStore.deleteItemAsync(`${key}.p`, opts);\n };\n\n return {\n getItem(key) {\n // Reads queue with writes, so a write can't swap the manifest and drop\n // the old chunks halfway through a read.\n return serial(key, async () => {\n const m = await manifest(key);\n if (!m) return null;\n const parts = await Promise.all(Array.from({ length: m.count }, (_, i) => secureStore.getItemAsync(chunkKey(key, m.gen, i), opts)));\n return parts.some((p) => p === null) ? null : parts.join(\"\");\n });\n },\n setItem(key, value) {\n return serial(key, async () => {\n const before = await manifest(key);\n await sweepPending(key, before?.gen);\n const gen = `${Date.now().toString(36)}${(counter++).toString(36)}`;\n const parts = splitUtf8(value, size);\n await secureStore.setItemAsync(`${key}.p`, `${gen}:${parts.length}`, opts);\n // Let every chunk write settle before failing, so a late write can't land\n // after the next operation has swept this generation.\n const written = await Promise.allSettled(parts.map((p, i) => secureStore.setItemAsync(chunkKey(key, gen, i), p, opts)));\n const failed = written.find((r): r is PromiseRejectedResult => r.status === \"rejected\");\n if (failed) throw failed.reason;\n await secureStore.setItemAsync(`${key}.n`, `${gen}:${parts.length}`, opts);\n await secureStore.deleteItemAsync(`${key}.p`, opts);\n if (before) await drop(key, before.gen, before.count);\n });\n },\n removeItem(key) {\n return serial(key, async () => {\n const m = await manifest(key);\n await sweepPending(key, m?.gen);\n // Chunks first: the manifest is what finds them, so it goes last.\n if (m) await drop(key, m.gen, m.count);\n await secureStore.deleteItemAsync(`${key}.n`, opts);\n });\n },\n };\n}\n\n/** The parts of a Supabase client's auth the refresh helper uses. */\nexport interface AutoRefreshAuth {\n auth: { startAutoRefresh(): Promise<void> | void; stopAutoRefresh(): Promise<void> | void };\n}\n\n/** The parts of React Native's `AppState` the refresh helper uses. */\nexport interface AppStateLike {\n currentState?: string | null;\n addEventListener(type: \"change\", listener: (state: string) => void): { remove(): void };\n}\n\n/**\n * Refresh the session only while the app is in front, as Supabase recommends\n * for React Native. Returns the unsubscribe.\n *\n * @stability Beta\n */\nexport function refreshWhileActive(client: AutoRefreshAuth, appState: AppStateLike): () => void {\n const apply = (state: string | null | undefined) => {\n if (state === \"active\") void client.auth.startAutoRefresh();\n else void client.auth.stopAutoRefresh();\n };\n apply(appState.currentState ?? \"active\");\n const sub = appState.addEventListener(\"change\", apply);\n return () => {\n sub.remove();\n void client.auth.stopAutoRefresh();\n };\n}\n\n/** @stability Beta */\nexport interface WipeOnReinstallOptions {\n /** The session storage (usually `keychainStorage`). */\n session: SessionStorage;\n /** The session's storage key. Supabase's default is `sb-<project ref>-auth-token`. */\n sessionKey: string;\n /** A store that an uninstall deletes, e.g. AsyncStorage. The Keychain survives an uninstall; this is how we notice one. */\n installMarker: SessionStorage;\n /** The marker's key. Default `genesis.installed`. */\n markerKey?: string;\n}\n\n/**\n * On iOS the Keychain outlives an uninstall, so a reinstalled app would sign\n * the last person back in. Call this before creating the client: on the first\n * launch after an install it removes any session left in the Keychain, then\n * sets a marker so later launches keep the session. Returns whether it wiped.\n *\n * @stability Beta\n */\nexport async function wipeSessionOnReinstall({ session, sessionKey, installMarker, markerKey = \"genesis.installed\" }: WipeOnReinstallOptions): Promise<boolean> {\n if ((await installMarker.getItem(markerKey)) !== null) return false;\n // The session, plus what supabase-js keeps beside it: the PKCE verifier (the\n // single legacy slot, each indexed flow's slot and the index) and, with\n // userStorage, the user.\n const indexKey = `${sessionKey}-flows-code-verifier`;\n for (const flowId of parseFlowIndex(await session.getItem(indexKey))) await session.removeItem(`${sessionKey}-flow-${flowId}-code-verifier`);\n for (const key of [sessionKey, `${sessionKey}-code-verifier`, indexKey, `${sessionKey}-user`]) await session.removeItem(key);\n await installMarker.setItem(markerKey, \"1\");\n return true;\n}\n\n/** UTF-8 byte length of one code point. */\nconst utf8Length = (codePoint: number) => (codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4);\n\n/** Split text into pieces of at most `maxBytes` UTF-8 bytes, never inside a character. */\nexport function splitUtf8(text: string, maxBytes: number): string[] {\n if (text === \"\") return [\"\"];\n const parts: string[] = [];\n let current = \"\";\n let bytes = 0;\n for (const ch of text) {\n const n = utf8Length(ch.codePointAt(0)!);\n if (bytes + n > maxBytes && current) {\n parts.push(current);\n current = \"\";\n bytes = 0;\n }\n current += ch;\n bytes += n;\n }\n parts.push(current);\n return parts;\n}\n\n/** supabase-js's PKCE flow index: a JSON array of flow ids. Anything else is ignored. */\nfunction parseFlowIndex(raw: string | null): string[] {\n if (!raw) return [];\n try {\n const ids: unknown = JSON.parse(raw);\n return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === \"string\" && /^[\\w-]{1,128}$/.test(id)) : [];\n } catch {\n return [];\n }\n}\n"]}
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Sign in with Apple's REST API, for the genesis-apple-revoke Edge Function:
3
+ * the client secret (an ES256 JWT signed with your .p8 key), the
4
+ * authorization-code exchange (/auth/token) and revocation (/auth/revoke).
5
+ *
6
+ * Plain TypeScript over WebCrypto and fetch, with no Deno or Node APIs, so the
7
+ * same file runs in the Edge Function and in the package's unit tests.
8
+ * Nothing here logs or returns a token, a key or a client secret.
9
+ *
10
+ * Apple's docs: developer.apple.com/documentation/signinwithapplerestapi
11
+ * (generate-and-validate-tokens, revoke-tokens) and
12
+ * developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret.
13
+ */
14
+
15
+ export const APPLE_ISSUER = "https://appleid.apple.com";
16
+ export const APPLE_TOKEN_URL = "https://appleid.apple.com/auth/token";
17
+ export const APPLE_REVOKE_URL = "https://appleid.apple.com/auth/revoke";
18
+
19
+ /** Apple caps a client secret's lifetime at 15777000 seconds (six months). Each call signs a fresh one, so five minutes is plenty. */
20
+ export const CLIENT_SECRET_TTL_SECONDS = 300;
21
+ const CLIENT_SECRET_MAX_TTL_SECONDS = 15_777_000;
22
+ const REQUEST_TIMEOUT_MS = 10_000;
23
+ const CLOCK_SKEW_SECONDS = 30;
24
+
25
+ /** Apple's error codes (ErrorResponse.error). Anything else reads as "unknown". */
26
+ const APPLE_ERRORS = ["invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type", "invalid_scope"] as const;
27
+ export type AppleErrorCode = (typeof APPLE_ERRORS)[number] | "unknown" | "network" | "bad_response";
28
+
29
+ export interface AppleConfig {
30
+ /** APPLE_TEAM_ID: your 10-character Team ID (the secret's `iss`). */
31
+ teamId: string;
32
+ /** APPLE_KEY_ID: the 10-character id of your Sign in with Apple key (the secret's `kid`). */
33
+ keyId: string;
34
+ /** APPLE_PRIVATE_KEY: the .p8 file's contents (PKCS#8 PEM). Escaped `\n` are accepted. */
35
+ privateKey: string;
36
+ /** APPLE_CLIENT_IDS: the bundle ids (App IDs) or Services IDs a code may be exchanged for, comma-separated. The first is the default. */
37
+ clientIds: string[];
38
+ }
39
+
40
+ const TEN_CHARS = /^[A-Z0-9]{10}$/;
41
+ const CLIENT_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
42
+
43
+ /**
44
+ * Reads the Apple settings from the environment. Returns the names that are
45
+ * missing or malformed (never their values), so the function can fail loud.
46
+ */
47
+ export function readAppleConfig(env: (name: string) => string | undefined): { config: AppleConfig; problems?: undefined } | { config?: undefined; problems: string[] } {
48
+ const problems: string[] = [];
49
+ const teamId = (env("APPLE_TEAM_ID") ?? "").trim();
50
+ const keyId = (env("APPLE_KEY_ID") ?? "").trim();
51
+ const privateKey = normalizePem(env("APPLE_PRIVATE_KEY") ?? "");
52
+ const clientIds = (env("APPLE_CLIENT_IDS") ?? "")
53
+ .split(",")
54
+ .map((s) => s.trim())
55
+ .filter(Boolean);
56
+ if (!TEN_CHARS.test(teamId)) problems.push("APPLE_TEAM_ID (10 capital letters or digits)");
57
+ if (!TEN_CHARS.test(keyId)) problems.push("APPLE_KEY_ID (10 capital letters or digits)");
58
+ if (!/-----BEGIN PRIVATE KEY-----[\s\S]+-----END PRIVATE KEY-----/.test(privateKey)) problems.push("APPLE_PRIVATE_KEY (the .p8 file's PEM text)");
59
+ if (clientIds.length === 0 || !clientIds.every((id) => CLIENT_ID.test(id))) problems.push("APPLE_CLIENT_IDS (comma-separated bundle ids or Services IDs)");
60
+ return problems.length ? { problems } : { config: { teamId, keyId, privateKey, clientIds } };
61
+ }
62
+
63
+ /** Accepts a PEM pasted with real newlines, escaped `\n`, or surrounding quotes. */
64
+ function normalizePem(raw: string): string {
65
+ return raw.trim().replace(/^"|"$/g, "").replace(/\\n/g, "\n");
66
+ }
67
+
68
+ const encoder = new TextEncoder();
69
+
70
+ export function base64url(bytes: Uint8Array | string): string {
71
+ const data = typeof bytes === "string" ? encoder.encode(bytes) : bytes;
72
+ let binary = "";
73
+ for (const b of data) binary += String.fromCharCode(b);
74
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
75
+ }
76
+
77
+ function base64urlDecode(text: string): string {
78
+ const b64 = text.replace(/-/g, "+").replace(/_/g, "/");
79
+ const binary = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
80
+ return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
81
+ }
82
+
83
+ const keyCache = new Map<string, Promise<CryptoKey>>();
84
+
85
+ function signingKey(pem: string): Promise<CryptoKey> {
86
+ let key = keyCache.get(pem);
87
+ if (!key) {
88
+ const body = pem.replace(/-----(BEGIN|END) PRIVATE KEY-----/g, "").replace(/\s+/g, "");
89
+ const der = Uint8Array.from(atob(body), (c) => c.charCodeAt(0));
90
+ key = crypto.subtle.importKey("pkcs8", der, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
91
+ // A bad key must not stay cached as a rejected promise.
92
+ key.catch(() => keyCache.delete(pem));
93
+ keyCache.set(pem, key);
94
+ }
95
+ return key;
96
+ }
97
+
98
+ /**
99
+ * The `client_secret` for one client id: a JWT with header `{ alg: "ES256", kid }`
100
+ * and claims `{ iss: teamId, iat, exp, aud: "https://appleid.apple.com", sub: clientId }`,
101
+ * signed with ECDSA P-256 / SHA-256. WebCrypto returns the signature as r‖s
102
+ * (64 bytes), which is the JWS encoding ES256 needs.
103
+ */
104
+ export async function appleClientSecret(config: AppleConfig, clientId: string, options: { now?: number; ttlSeconds?: number } = {}): Promise<string> {
105
+ // Backdated a little, so a clock slightly ahead of Apple's doesn't issue a secret "from the future".
106
+ const iat = Math.floor((options.now ?? Date.now()) / 1000) - CLOCK_SKEW_SECONDS;
107
+ const ttl = Math.min(Math.max(1, options.ttlSeconds ?? CLIENT_SECRET_TTL_SECONDS), CLIENT_SECRET_MAX_TTL_SECONDS);
108
+ const header = base64url(JSON.stringify({ alg: "ES256", kid: config.keyId }));
109
+ const payload = base64url(JSON.stringify({ iss: config.teamId, iat, exp: iat + ttl, aud: APPLE_ISSUER, sub: clientId }));
110
+ const input = `${header}.${payload}`;
111
+ const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, await signingKey(config.privateKey), encoder.encode(input));
112
+ return `${input}.${base64url(new Uint8Array(signature))}`;
113
+ }
114
+
115
+ /** The payload of a JWT, unverified, or null if it isn't one. */
116
+ export function decodeJwtPayload(jwt: string): Record<string, unknown> | null {
117
+ const parts = jwt.split(".");
118
+ if (parts.length !== 3 || !parts[1]) return null;
119
+ try {
120
+ const payload: unknown = JSON.parse(base64urlDecode(parts[1]));
121
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? (payload as Record<string, unknown>) : null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ async function appleError(res: Response): Promise<AppleErrorCode> {
128
+ try {
129
+ const body: unknown = await res.json();
130
+ const code = body && typeof body === "object" ? (body as { error?: unknown }).error : undefined;
131
+ return (APPLE_ERRORS as readonly unknown[]).includes(code) ? (code as AppleErrorCode) : "unknown";
132
+ } catch {
133
+ return "unknown";
134
+ }
135
+ }
136
+
137
+ async function postForm(fetchImpl: typeof fetch, url: string, form: Record<string, string>): Promise<Response | null> {
138
+ try {
139
+ return await fetchImpl(url, {
140
+ method: "POST",
141
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
142
+ body: new URLSearchParams(form).toString(),
143
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
144
+ });
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ export type ExchangeResult =
151
+ | { ok: true; refreshToken: string; appleUserId: string }
152
+ /** `mintedRefreshToken`: Apple issued a token the checks then refused; revoke it rather than drop it. */
153
+ | { ok: false; error: AppleErrorCode; status?: number; mintedRefreshToken?: string };
154
+
155
+ /**
156
+ * Exchanges a sign-in's single-use authorization code (valid for five
157
+ * minutes) for Apple's refresh token: `grant_type=authorization_code` with
158
+ * `client_id`, `client_secret` and `code`. A native sign-in sends no
159
+ * `redirect_uri` (Apple wants one only if the authorization request had one).
160
+ *
161
+ * Also checks the returned id token names Apple as `iss` and this client as
162
+ * `aud`, and returns its `sub` (the person's Apple user id) so the caller can
163
+ * tie the token to the right login. The id token comes straight from Apple
164
+ * over TLS in this response, so its claims are read without checking its
165
+ * signature (OpenID Connect Core 3.1.3.7).
166
+ */
167
+ export async function exchangeAuthorizationCode(config: AppleConfig, clientId: string, code: string, fetchImpl: typeof fetch = fetch): Promise<ExchangeResult> {
168
+ const res = await postForm(fetchImpl, APPLE_TOKEN_URL, {
169
+ client_id: clientId,
170
+ client_secret: await appleClientSecret(config, clientId),
171
+ code,
172
+ grant_type: "authorization_code",
173
+ });
174
+ if (!res) return { ok: false, error: "network" };
175
+ if (!res.ok) return { ok: false, error: await appleError(res), status: res.status };
176
+ let body: { refresh_token?: unknown; id_token?: unknown };
177
+ try {
178
+ body = (await res.json()) as typeof body;
179
+ } catch {
180
+ return { ok: false, error: "bad_response", status: res.status };
181
+ }
182
+ const claims = typeof body.id_token === "string" ? decodeJwtPayload(body.id_token) : null;
183
+ const aud = claims?.aud;
184
+ const audOk = aud === clientId || (Array.isArray(aud) && aud.includes(clientId));
185
+ const minted = typeof body.refresh_token === "string" && body.refresh_token ? body.refresh_token : undefined;
186
+ if (!minted) return { ok: false, error: "bad_response", status: res.status };
187
+ if (!claims || claims.iss !== APPLE_ISSUER || !audOk || typeof claims.sub !== "string" || !claims.sub) {
188
+ return { ok: false, error: "bad_response", status: res.status, mintedRefreshToken: minted };
189
+ }
190
+ return { ok: true, refreshToken: minted, appleUserId: claims.sub };
191
+ }
192
+
193
+ /**
194
+ * Checks a refresh token is still live with this client's secret:
195
+ * `grant_type=refresh_token` at /auth/token (Apple returns new access and id
196
+ * tokens, which are dropped). This is the proof a revocation needs, because
197
+ * /auth/revoke answers 200 to a token it doesn't know even when the client
198
+ * secret is garbage: a 200 here means the secret is right and the token is
199
+ * live; `invalid_grant` means the token is already dead (nothing left to
200
+ * revoke); `invalid_client` means the secrets are wrong.
201
+ */
202
+ export async function checkRefreshToken(config: AppleConfig, clientId: string, refreshToken: string, fetchImpl: typeof fetch = fetch): Promise<RevokeResult> {
203
+ const res = await postForm(fetchImpl, APPLE_TOKEN_URL, {
204
+ client_id: clientId,
205
+ client_secret: await appleClientSecret(config, clientId),
206
+ grant_type: "refresh_token",
207
+ refresh_token: refreshToken,
208
+ });
209
+ if (!res) return { ok: false, error: "network" };
210
+ if (res.status === 200) {
211
+ await res.body?.cancel().catch(() => {});
212
+ return { ok: true };
213
+ }
214
+ return { ok: false, error: await appleError(res), status: res.status };
215
+ }
216
+
217
+ export type RevokeResult = { ok: true } | { ok: false; error: AppleErrorCode; status?: number };
218
+
219
+ /**
220
+ * Revokes a refresh token: `client_id`, `client_secret`, `token` and
221
+ * `token_type_hint=refresh_token`. Apple answers 200 with no body when it
222
+ * revoked the token, and also when the token was already invalid. It also
223
+ * answered 200 to an unknown token with a garbage client secret (seen
224
+ * 2026-10-24), so a 200 alone proves nothing: check the token with
225
+ * {@link checkRefreshToken} first.
226
+ */
227
+ export async function revokeRefreshToken(config: AppleConfig, clientId: string, refreshToken: string, fetchImpl: typeof fetch = fetch): Promise<RevokeResult> {
228
+ const res = await postForm(fetchImpl, APPLE_REVOKE_URL, {
229
+ client_id: clientId,
230
+ client_secret: await appleClientSecret(config, clientId),
231
+ token: refreshToken,
232
+ token_type_hint: "refresh_token",
233
+ });
234
+ if (!res) return { ok: false, error: "network" };
235
+ if (res.status === 200) return { ok: true };
236
+ return { ok: false, error: await appleError(res), status: res.status };
237
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * The genesis-apple-revoke Edge Function's request handling, with its
3
+ * dependencies passed in (index.ts wires the real ones), so it is tested
4
+ * without Deno, a database or Apple.
5
+ *
6
+ * POST { "action": "store", "code": "<authorizationCode>", "clientId"?: "<bundle id>" }
7
+ * Authorization: Bearer <the person's access token>
8
+ * Exchanges the code from a Sign in with Apple at /auth/token and keeps
9
+ * Apple's refresh token against the caller, server-side only.
10
+ *
11
+ * POST { "action": "revoke" }
12
+ * Revokes the tokens of deleted accounts that are due (queued by the
13
+ * delete trigger, retried with backoff). It needs no caller: it only
14
+ * works through the queue, a bounded batch at a time, and answers
15
+ * `{ ok: true }` (202, working in the background, when the runtime can)
16
+ * whatever was in it. The app calls it after `delete_my_account`; a
17
+ * schedule calls it to retry.
18
+ *
19
+ * Responses never carry a token, a secret, or Apple's response body: only an
20
+ * `error` code.
21
+ */
22
+ import { checkRefreshToken, exchangeAuthorizationCode, readAppleConfig, revokeRefreshToken, type AppleConfig } from "./apple.ts";
23
+
24
+ /** The signed-in caller, as the auth server reports it. */
25
+ export interface CallerUser {
26
+ id: string;
27
+ /** The `sub` of each Apple identity linked to this login (Apple's user id). */
28
+ appleUserIds: string[];
29
+ }
30
+
31
+ export interface DueRevocation {
32
+ id: number;
33
+ client_id: string;
34
+ refresh_token: string;
35
+ attempts: number;
36
+ }
37
+
38
+ /** The SQL functions from migration 0003, called with the service role. */
39
+ export interface TokenStore {
40
+ /** `genesis_apple_token_save`: "stored", or "queued" when the login is already gone. */
41
+ save(userId: string, clientId: string, refreshToken: string): Promise<"stored" | "queued">;
42
+ /** `genesis_apple_revocations_claim`: due rows, each leased until its next retry. */
43
+ claim(limit: number): Promise<DueRevocation[]>;
44
+ /** `genesis_apple_revocation_done`: forgets a revoked token. */
45
+ done(id: number): Promise<void>;
46
+ /** `genesis_apple_revocation_failed`: keeps the row for its next retry ("retry"), or drops its token after the last attempt ("gave_up"). */
47
+ failed(id: number, error: string): Promise<"retry" | "gave_up">;
48
+ }
49
+
50
+ export interface HandlerDeps {
51
+ env: (name: string) => string | undefined;
52
+ fetch: typeof fetch;
53
+ /** The user behind an access token (checked with the auth server), or null. */
54
+ getUser(accessToken: string): Promise<CallerUser | null>;
55
+ store: TokenStore;
56
+ log: (message: string) => void;
57
+ /** Keeps work running after the response (`EdgeRuntime.waitUntil`). Without it, the revoke action answers when the batch is done. */
58
+ background?: (work: Promise<unknown>) => void;
59
+ }
60
+
61
+ export const REVOKE_BATCH = 20;
62
+ const MAX_BODY_BYTES = 4096;
63
+ const MAX_REFRESH_TOKEN = 4096;
64
+ const CODE = /^[A-Za-z0-9._-]{1,1024}$/;
65
+
66
+ const CORS = {
67
+ "access-control-allow-origin": "*",
68
+ "access-control-allow-headers": "authorization, x-client-info, apikey, content-type",
69
+ "access-control-allow-methods": "POST, OPTIONS",
70
+ };
71
+
72
+ function json(status: number, body: Record<string, unknown>): Response {
73
+ return new Response(JSON.stringify(body), { status, headers: { ...CORS, "content-type": "application/json", "cache-control": "no-store" } });
74
+ }
75
+
76
+ async function readBody(req: Request): Promise<Record<string, unknown> | null> {
77
+ if (Number(req.headers.get("content-length") ?? 0) > MAX_BODY_BYTES) return null;
78
+ const text = await req.text().catch(() => null);
79
+ if (text === null || text.length > MAX_BODY_BYTES) return null;
80
+ try {
81
+ const body: unknown = JSON.parse(text);
82
+ return body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>) : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function bearer(req: Request): string | null {
89
+ const match = /^Bearer\s+(\S+)$/i.exec(req.headers.get("authorization") ?? "");
90
+ return match?.[1] ?? null;
91
+ }
92
+
93
+ const describe = (error: unknown) => (error instanceof Error ? `${error.name}: ${error.message}` : "unknown error");
94
+
95
+ export async function handle(req: Request, deps: HandlerDeps): Promise<Response> {
96
+ try {
97
+ return await route(req, deps);
98
+ } catch (error) {
99
+ // e.g. APPLE_PRIVATE_KEY isn't a P-256 key, or the database is unreachable.
100
+ deps.log(`genesis-apple-revoke: failed: ${describe(error)}`);
101
+ return json(500, { error: "internal" });
102
+ }
103
+ }
104
+
105
+ async function route(req: Request, deps: HandlerDeps): Promise<Response> {
106
+ if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
107
+ if (req.method !== "POST") return json(405, { error: "method_not_allowed" });
108
+ const body = await readBody(req);
109
+ if (!body) return json(400, { error: "bad_request" });
110
+
111
+ const apple = readAppleConfig(deps.env);
112
+ if (!apple.config) {
113
+ // Loud in the logs, by name only: a missing secret must not pass for a revoked token.
114
+ deps.log(`genesis-apple-revoke: not configured; set ${apple.problems.join(", ")}`);
115
+ return json(500, { error: "not_configured" });
116
+ }
117
+
118
+ if (body.action === "store") return store(req, body, apple.config, deps);
119
+ if (body.action === "revoke") {
120
+ const work = revokeDue(apple.config, deps);
121
+ if (deps.background) {
122
+ // The app waits on this right after deleting the account: answer now, finish the batch after.
123
+ deps.background(work);
124
+ return json(202, { ok: true });
125
+ }
126
+ await work;
127
+ return json(200, { ok: true });
128
+ }
129
+ return json(400, { error: "bad_request" });
130
+ }
131
+
132
+ async function store(req: Request, body: Record<string, unknown>, config: AppleConfig, deps: HandlerDeps): Promise<Response> {
133
+ const token = bearer(req);
134
+ const user = token ? await deps.getUser(token).catch(() => null) : null;
135
+ if (!user) return json(401, { error: "not_signed_in" });
136
+
137
+ const code = body.code;
138
+ if (typeof code !== "string" || !CODE.test(code)) return json(400, { error: "bad_request" });
139
+ const clientId = body.clientId === undefined || body.clientId === null ? config.clientIds[0]! : body.clientId;
140
+ if (typeof clientId !== "string" || !config.clientIds.includes(clientId)) {
141
+ deps.log(`genesis-apple-revoke: a store named a client id outside APPLE_CLIENT_IDS; add every bundle id the app signs in with (each build variant too)`);
142
+ return json(400, { error: "unknown_client" });
143
+ }
144
+ if (user.appleUserIds.length === 0) return json(403, { error: "no_apple_identity" });
145
+
146
+ const exchanged = await exchangeAuthorizationCode(config, clientId, code, deps.fetch);
147
+ if (!exchanged.ok) {
148
+ deps.log(`genesis-apple-revoke: code exchange failed for ${clientId}: ${exchanged.error}${exchanged.status ? ` (HTTP ${exchanged.status})` : ""}`);
149
+ // Apple minted a token whose id token we refused: revoke it rather than drop it.
150
+ if (exchanged.mintedRefreshToken) await revokeMinted(config, clientId, exchanged.mintedRefreshToken, deps);
151
+ return json(502, { error: "exchange_failed", apple: exchanged.error });
152
+ }
153
+
154
+ // The code must belong to this login's Apple identity: otherwise someone
155
+ // could file another person's token under their own account.
156
+ if (!user.appleUserIds.includes(exchanged.appleUserId)) {
157
+ deps.log("genesis-apple-revoke: a code for another Apple account was refused");
158
+ await revokeMinted(config, clientId, exchanged.refreshToken, deps);
159
+ return json(403, { error: "apple_identity_mismatch" });
160
+ }
161
+ if (exchanged.refreshToken.length > MAX_REFRESH_TOKEN) {
162
+ // The column would refuse it (and a refused row's text can reach the database's log).
163
+ deps.log("genesis-apple-revoke: Apple's refresh token is longer than the table allows");
164
+ await revokeMinted(config, clientId, exchanged.refreshToken, deps);
165
+ return json(502, { error: "exchange_failed", apple: "bad_response" });
166
+ }
167
+
168
+ const saved = await deps.store.save(user.id, clientId, exchanged.refreshToken);
169
+ // The login was deleted while this ran: the token went to the queue, so revoke now.
170
+ if (saved === "queued") await revokeDue(config, deps);
171
+ return json(200, { ok: true });
172
+ }
173
+
174
+ /** Revokes a token this request minted and won't keep (it was just issued with these secrets, so a 200 is meaningful). */
175
+ async function revokeMinted(config: AppleConfig, clientId: string, refreshToken: string, deps: HandlerDeps): Promise<void> {
176
+ const revoked = await revokeRefreshToken(config, clientId, refreshToken, deps.fetch);
177
+ if (!revoked.ok) deps.log(`genesis-apple-revoke: couldn't revoke a token it refused to keep: ${revoked.error}`);
178
+ }
179
+
180
+ /**
181
+ * Works through due revocations. Each token is first checked at /auth/token
182
+ * (`checkRefreshToken`), because /auth/revoke's 200 proves nothing on its
183
+ * own: live → revoke, and only a 200 then forgets the row; already dead
184
+ * (`invalid_grant`) → nothing to revoke, forget it; anything else (wrong
185
+ * secrets, Apple down) → retry later. Returns the counts, for logs and tests.
186
+ */
187
+ export async function revokeDue(config: AppleConfig, deps: HandlerDeps): Promise<{ revoked: number; gone: number; failed: number }> {
188
+ const counts = { revoked: 0, gone: 0, failed: 0 };
189
+ let due: DueRevocation[];
190
+ try {
191
+ due = await deps.store.claim(REVOKE_BATCH);
192
+ } catch (error) {
193
+ deps.log(`genesis-apple-revoke: couldn't read the queue: ${describe(error)}`);
194
+ return counts;
195
+ }
196
+ for (const row of due) {
197
+ // One row's trouble never stops the rest; a row whose result wasn't
198
+ // recorded comes round again when its lease ends.
199
+ try {
200
+ const check = await checkRefreshToken(config, row.client_id, row.refresh_token, deps.fetch);
201
+ if (!check.ok && check.error === "invalid_grant") {
202
+ await deps.store.done(row.id);
203
+ counts.gone++;
204
+ continue;
205
+ }
206
+ const result = check.ok ? await revokeRefreshToken(config, row.client_id, row.refresh_token, deps.fetch) : check;
207
+ if (result.ok) {
208
+ await deps.store.done(row.id);
209
+ counts.revoked++;
210
+ continue;
211
+ }
212
+ const why = `${check.ok ? "revoke" : "check"}: ${result.error}${result.status ? ` (HTTP ${result.status})` : ""}`;
213
+ counts.failed++;
214
+ const next = await deps.store.failed(row.id, why);
215
+ deps.log(
216
+ next === "gave_up"
217
+ ? `genesis-apple-revoke: GAVE UP on revocation ${row.id} for ${row.client_id} after ${row.attempts} attempts (${why}); its token is dropped and the row kept as a record`
218
+ : `genesis-apple-revoke: revocation ${row.id} for ${row.client_id} failed (attempt ${row.attempts}): ${why}; it will be retried`,
219
+ );
220
+ } catch (error) {
221
+ counts.failed++;
222
+ deps.log(`genesis-apple-revoke: revocation ${row.id} couldn't be recorded: ${describe(error)}; it will be retried`);
223
+ }
224
+ }
225
+ if (due.length) deps.log(`genesis-apple-revoke: ${counts.revoked} revoked, ${counts.gone} already invalid, ${counts.failed} failed`);
226
+ return counts;
227
+ }