@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.
- package/README.md +523 -1
- package/dist/blocks.d.ts +19 -0
- package/dist/blocks.js +3 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-FAA2FTF4.js +110 -0
- package/dist/chunk-FAA2FTF4.js.map +1 -0
- package/dist/chunk-T2HGFC4C.js +37 -0
- package/dist/chunk-T2HGFC4C.js.map +1 -0
- package/dist/index-BSGz3RGk.d.ts +317 -0
- package/dist/index.d.ts +4 -78
- package/dist/index.js +63 -2
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +233 -5
- package/dist/native.js +196 -1
- package/dist/native.js.map +1 -1
- package/functions/genesis-apple-revoke/apple.ts +237 -0
- package/functions/genesis-apple-revoke/handler.ts +227 -0
- package/functions/genesis-apple-revoke/index.ts +55 -0
- package/functions/genesis-push-send/expo.ts +117 -0
- package/functions/genesis-push-send/handler.ts +462 -0
- package/functions/genesis-push-send/index.ts +54 -0
- package/functions/genesis-support-cleanup/handler.ts +80 -0
- package/functions/genesis-support-cleanup/index.ts +39 -0
- package/functions/genesis-support-intake/contract.ts +193 -0
- package/functions/genesis-support-intake/handler.ts +170 -0
- package/functions/genesis-support-intake/index.ts +47 -0
- package/functions/genesis-support-notify/handler.ts +167 -0
- package/functions/genesis-support-notify/index.ts +15 -0
- package/functions/tests/genesis-apple-revoke_test.ts +81 -0
- package/functions/tests/genesis-push-send_test.ts +53 -0
- package/migrations/0001_genesis_account_deletion.sql +133 -0
- package/migrations/0002_genesis_app_release_policy.sql +82 -0
- package/migrations/0003_genesis_apple_token_revocation.sql +205 -0
- package/migrations/0004_genesis_tenant_features.sql +333 -0
- package/migrations/0005_genesis_push.sql +524 -0
- package/migrations/0006_genesis_support.sql +846 -0
- package/migrations/tests/0001_genesis_account_deletion.test.sql +138 -0
- package/migrations/tests/0002_genesis_app_release_policy.test.sql +148 -0
- package/migrations/tests/0003_genesis_apple_token_revocation.test.sql +230 -0
- package/migrations/tests/0004_genesis_tenant_features.test.sql +418 -0
- package/migrations/tests/0005_genesis_push.test.sql +407 -0
- package/migrations/tests/0006_genesis_support.test.sql +552 -0
- package/package.json +92 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { installWebCrypto, assertPkceCrypto, isDigestOnlyPolyfill, claimsViaServer } from './chunk-FAA2FTF4.js';
|
|
2
|
+
export { accountBlock, tenantFeaturesBlock } from './chunk-T2HGFC4C.js';
|
|
3
|
+
import { createClient, AuthInvalidJwtError } from '@supabase/supabase-js';
|
|
4
|
+
|
|
5
|
+
// src/apple-tokens.ts
|
|
6
|
+
var APPLE_REVOKE_FUNCTION = "genesis-apple-revoke";
|
|
7
|
+
var STORE_TIMEOUT_MS = 2e4;
|
|
8
|
+
var REVOKE_TIMEOUT_MS = 5e3;
|
|
9
|
+
async function storeAppleToken(client, { authorizationCode, clientId, functionName = APPLE_REVOKE_FUNCTION }) {
|
|
10
|
+
try {
|
|
11
|
+
const { error } = await client.functions.invoke(functionName, { body: { action: "store", code: authorizationCode, ...clientId ? { clientId } : {} }, timeout: STORE_TIMEOUT_MS });
|
|
12
|
+
if (error) return { ok: false, error: error instanceof Error ? error : new Error(String(error.message ?? error)) };
|
|
13
|
+
return { ok: true };
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function requestAppleRevocation(client, functionName = APPLE_REVOKE_FUNCTION) {
|
|
19
|
+
try {
|
|
20
|
+
const { error } = await client.functions.invoke(functionName, { body: { action: "revoke" }, timeout: REVOKE_TIMEOUT_MS });
|
|
21
|
+
return !error;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/tenant-features.ts
|
|
28
|
+
async function tenantFeatures(client, tenantId) {
|
|
29
|
+
const data = must(await client.rpc("tenant_features_off", { tenant: tenantId }));
|
|
30
|
+
const off = new Set(Array.isArray(data) ? data.filter((id) => typeof id === "string") : []);
|
|
31
|
+
return Object.freeze({ tenantId, off, isOn: (featureId) => !off.has(featureId) });
|
|
32
|
+
}
|
|
33
|
+
async function setTenantFeature(client, input) {
|
|
34
|
+
must(
|
|
35
|
+
await client.rpc("set_tenant_feature", {
|
|
36
|
+
tenant: input.tenantId,
|
|
37
|
+
feature: input.featureId,
|
|
38
|
+
enabled: input.enabled,
|
|
39
|
+
reason: input.reason
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
async function tenantFeatureSettings(client, tenantId) {
|
|
44
|
+
return must(await client.rpc("tenant_feature_settings", { tenant: tenantId })) ?? [];
|
|
45
|
+
}
|
|
46
|
+
async function tenantFeatureHistory(client, tenantId, options = {}) {
|
|
47
|
+
const args = options.maxRows === void 0 ? { tenant: tenantId } : { tenant: tenantId, max_rows: options.maxRows };
|
|
48
|
+
return must(await client.rpc("tenant_feature_history", args)) ?? [];
|
|
49
|
+
}
|
|
2
50
|
|
|
3
51
|
// src/index.ts
|
|
4
52
|
function createGenesisSupabase({
|
|
@@ -7,8 +55,11 @@ function createGenesisSupabase({
|
|
|
7
55
|
storage,
|
|
8
56
|
platform = "web",
|
|
9
57
|
detectSessionInUrl = platform === "web",
|
|
58
|
+
crypto,
|
|
10
59
|
options = {}
|
|
11
60
|
}) {
|
|
61
|
+
if (crypto) installWebCrypto(crypto);
|
|
62
|
+
if ((options.auth?.flowType ?? "pkce") === "pkce") assertPkceCrypto();
|
|
12
63
|
const client = createClient(url, publishableKey, {
|
|
13
64
|
...options,
|
|
14
65
|
auth: {
|
|
@@ -20,6 +71,10 @@ function createGenesisSupabase({
|
|
|
20
71
|
...options.auth
|
|
21
72
|
}
|
|
22
73
|
});
|
|
74
|
+
if (isDigestOnlyPolyfill()) {
|
|
75
|
+
const auth = client.auth;
|
|
76
|
+
auth.getClaims = claimsViaServer(auth, (message) => new AuthInvalidJwtError(message));
|
|
77
|
+
}
|
|
23
78
|
return client;
|
|
24
79
|
}
|
|
25
80
|
var SupabaseQueryError = class extends Error {
|
|
@@ -38,7 +93,13 @@ function must(result) {
|
|
|
38
93
|
if (result.error) throw new SupabaseQueryError(result.error);
|
|
39
94
|
return result.data;
|
|
40
95
|
}
|
|
96
|
+
async function deleteMyAccount(client, options = {}) {
|
|
97
|
+
must(await client.rpc("delete_my_account"));
|
|
98
|
+
const revoke = options.revokeAppleTokens;
|
|
99
|
+
if (revoke && client.functions) await requestAppleRevocation({ functions: client.functions }, typeof revoke === "string" ? revoke : APPLE_REVOKE_FUNCTION);
|
|
100
|
+
await client.auth.signOut({ scope: "local" });
|
|
101
|
+
}
|
|
41
102
|
|
|
42
|
-
export { SupabaseQueryError, createGenesisSupabase, must };
|
|
103
|
+
export { APPLE_REVOKE_FUNCTION, SupabaseQueryError, createGenesisSupabase, deleteMyAccount, must, requestAppleRevocation, setTenantFeature, storeAppleToken, tenantFeatureHistory, tenantFeatureSettings, tenantFeatures };
|
|
43
104
|
//# sourceMappingURL=index.js.map
|
|
44
105
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAwCO,SAAS,qBAAA,CAA0C;AAAA,EACxD,GAAA;AAAA,EACA,cAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA,GAAW,KAAA;AAAA,EACX,qBAAqB,QAAA,KAAa,KAAA;AAAA,EAClC,UAAU;AACZ,CAAA,EAAqD;AAGnD,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,EAAK,cAAA,EAAgB;AAAA,IAC/C,GAAG,OAAA;AAAA,IACH,IAAA,EAAM;AAAA,MACJ,QAAA,EAAU,MAAA;AAAA,MACV,cAAA,EAAgB,IAAA;AAAA,MAChB,gBAAA,EAAkB,IAAA;AAAA,MAClB,kBAAA;AAAA,MACA,GAAI,OAAA,GAAU,EAAE,OAAA,KAAY,EAAC;AAAA,MAC7B,GAAG,OAAA,CAAQ;AAAA;AACb,GACD,CAAA;AACD,EAAA,OAAO,MAAA;AACT;AAkBO,IAAM,kBAAA,GAAN,cAAiC,KAAA,CAAM;AAAA,EACnC,IAAA,GAAO,oBAAA;AAAA,EACP,IAAA;AAAA,EACA,OAAA;AAAA,EACA,IAAA;AAAA,EACT,YAAY,KAAA,EAA2B;AACrC,IAAA,KAAA,CAAM,KAAA,CAAM,OAAA,EAAS,EAAE,KAAA,EAAO,OAAO,CAAA;AACrC,IAAA,IAAA,CAAK,OAAO,KAAA,CAAM,IAAA;AAClB,IAAA,IAAA,CAAK,UAAU,KAAA,CAAM,OAAA;AACrB,IAAA,IAAA,CAAK,OAAO,KAAA,CAAM,IAAA;AAAA,EACpB;AACF;AASO,SAAS,KAAQ,MAAA,EAAiF;AACvG,EAAA,IAAI,OAAO,KAAA,EAAO,MAAM,IAAI,kBAAA,CAAmB,OAAO,KAAK,CAAA;AAC3D,EAAA,OAAO,MAAA,CAAO,IAAA;AAChB","file":"index.js","sourcesContent":["/**\n * Supabase for Genesis apps. Platform-neutral: web, React Native and the\n * server import it. Native-only helpers (Keychain storage, foreground\n * refresh, reinstall wipe) are on `@marktiderman/genesis-supabase/native`.\n */\nimport { createClient, type SupabaseClient, type SupabaseClientOptions } from \"@supabase/supabase-js\";\n\n/** The async key-value store Supabase auth keeps its session in. */\nexport interface SessionStorage {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\n/** @stability Beta */\nexport interface GenesisSupabaseOptions {\n url: string;\n /** The publishable (anon) key. Never the secret key in an app. */\n publishableKey: string;\n /** Where the session lives. Native: `keychainStorage(SecureStore)`. Web: leave out for supabase-js's own default. */\n storage?: SessionStorage;\n /** `\"native\"` sets the React Native defaults (no session from the URL). Default `\"web\"`. */\n platform?: \"web\" | \"native\";\n /**\n * Read a session from the URL after an OAuth redirect. Default: `true` on\n * web, `false` on native, where the app hands the redirect URL to\n * `exchangeCodeForSession` itself.\n */\n detectSessionInUrl?: boolean;\n /** Anything else for supabase-js, merged last. */\n options?: SupabaseClientOptions<\"public\">;\n}\n\n/**\n * A Supabase client with Genesis's defaults: PKCE sign-in (the code flow, not\n * tokens in a redirect URL), a persisted session that refreshes itself, and\n * the session storage you pass.\n *\n * @stability Beta\n */\nexport function createGenesisSupabase<Database = unknown>({\n url,\n publishableKey,\n storage,\n platform = \"web\",\n detectSessionInUrl = platform === \"web\",\n options = {},\n}: GenesisSupabaseOptions): SupabaseClient<Database> {\n // supabase-js's generics over the schema don't survive being forwarded, so the\n // options are built untyped and the client is typed by the caller's Database.\n const client = createClient(url, publishableKey, {\n ...options,\n auth: {\n flowType: \"pkce\",\n persistSession: true,\n autoRefreshToken: true,\n detectSessionInUrl,\n ...(storage ? { storage } : {}),\n ...options.auth,\n },\n });\n return client as unknown as SupabaseClient<Database>;\n}\n\n/** What a Supabase or PostgREST error carries. */\nexport interface PostgrestErrorLike {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\n\n/**\n * A failed query, with the database's `code` (`23505` unique violation,\n * `42501` permission denied, `PGRST116` no rows…), `details` and `hint`, so a\n * caller can tell an RLS refusal from a conflict. The original error is the\n * `cause`.\n *\n * @stability Beta\n */\nexport class SupabaseQueryError extends Error {\n override name = \"SupabaseQueryError\";\n readonly code?: string;\n readonly details?: string | null;\n readonly hint?: string | null;\n constructor(error: PostgrestErrorLike) {\n super(error.message, { cause: error });\n this.code = error.code;\n this.details = error.details;\n this.hint = error.hint;\n }\n}\n\n/**\n * The data, or a thrown {@link SupabaseQueryError}: for query results where an\n * error should fail the caller (a TanStack Query `queryFn`). The return type\n * keeps `null` when the query can return no row (`.maybeSingle()`).\n *\n * @stability Beta\n */\nexport function must<T>(result: { data: T; error: null } | { data: null; error: PostgrestErrorLike }): T {\n if (result.error) throw new SupabaseQueryError(result.error);\n return result.data as T;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/apple-tokens.ts","../src/tenant-features.ts","../src/index.ts"],"names":[],"mappings":";;;;;AASO,IAAM,qBAAA,GAAwB;AAGrC,IAAM,gBAAA,GAAmB,GAAA;AAEzB,IAAM,iBAAA,GAAoB,GAAA;AAiC1B,eAAsB,gBAAgB,MAAA,EAAyB,EAAE,mBAAmB,QAAA,EAAU,YAAA,GAAe,uBAAsB,EAA2D;AAC5L,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,MAAA,CAAO,UAAU,MAAA,CAAO,YAAA,EAAc,EAAE,IAAA,EAAM,EAAE,MAAA,EAAQ,SAAS,IAAA,EAAM,iBAAA,EAAmB,GAAI,QAAA,GAAW,EAAE,QAAA,EAAS,GAAI,EAAC,EAAG,EAAG,OAAA,EAAS,gBAAA,EAAkB,CAAA;AAClL,IAAA,IAAI,OAAO,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,MAAM,MAAA,CAAQ,KAAA,CAAgC,OAAA,IAAW,KAAK,CAAC,CAAA,EAAE;AAC5I,IAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AAAA,EACpB,SAAS,KAAA,EAAO;AACd,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA,EAAE;AAAA,EACvF;AACF;AAQA,eAAsB,sBAAA,CAAuB,MAAA,EAAyB,YAAA,GAAe,qBAAA,EAAyC;AAC5H,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,MAAA,CAAO,UAAU,MAAA,CAAO,YAAA,EAAc,EAAE,IAAA,EAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,OAAA,EAAS,mBAAmB,CAAA;AACxH,IAAA,OAAO,CAAC,KAAA;AAAA,EACV,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;AC9BA,eAAsB,cAAA,CAAe,QAA8B,QAAA,EAA2C;AAC5G,EAAA,MAAM,IAAA,GAAO,IAAA,CAAM,MAAM,MAAA,CAAO,GAAA,CAAI,uBAAuB,EAAE,MAAA,EAAQ,QAAA,EAAU,CAAe,CAAA;AAC9F,EAAA,MAAM,MAAM,IAAI,GAAA,CAAI,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,CAAC,OAAqB,OAAO,EAAA,KAAO,QAAQ,CAAA,GAAI,EAAE,CAAA;AACxG,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,QAAA,EAAU,GAAA,EAAK,IAAA,EAAM,CAAC,SAAA,KAAsB,CAAC,GAAA,CAAI,GAAA,CAAI,SAAS,GAAG,CAAA;AAC1F;AAmBA,eAAsB,gBAAA,CAAiB,QAA8B,KAAA,EAA6C;AAChH,EAAA,IAAA;AAAA,IACG,MAAM,MAAA,CAAO,GAAA,CAAI,oBAAA,EAAsB;AAAA,MACtC,QAAQ,KAAA,CAAM,QAAA;AAAA,MACd,SAAS,KAAA,CAAM,SAAA;AAAA,MACf,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,QAAQ,KAAA,CAAM;AAAA,KACf;AAAA,GACH;AACF;AAYA,eAAsB,qBAAA,CAAsB,QAA8B,QAAA,EAAmD;AAC3H,EAAA,OAAQ,IAAA,CAAM,MAAM,MAAA,CAAO,GAAA,CAAI,yBAAA,EAA2B,EAAE,MAAA,EAAQ,QAAA,EAAU,CAAe,CAAA,IAAK,EAAC;AACrG;AAcA,eAAsB,oBAAA,CACpB,MAAA,EACA,QAAA,EACA,OAAA,GAAgC,EAAC,EACD;AAChC,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,KAAY,MAAA,GAAY,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,EAAE,MAAA,EAAQ,QAAA,EAAU,QAAA,EAAU,QAAQ,OAAA,EAAQ;AAClH,EAAA,OAAQ,IAAA,CAAM,MAAM,MAAA,CAAO,GAAA,CAAI,0BAA0B,IAAI,CAAe,KAAK,EAAC;AACpF;;;AC1CO,SAAS,qBAAA,CAA0C;AAAA,EACxD,GAAA;AAAA,EACA,cAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA,GAAW,KAAA;AAAA,EACX,qBAAqB,QAAA,KAAa,KAAA;AAAA,EAClC,MAAA;AAAA,EACA,UAAU;AACZ,CAAA,EAAqD;AACnD,EAAA,IAAI,MAAA,mBAAyB,MAAM,CAAA;AAEnC,EAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,QAAA,IAAY,MAAA,MAAY,QAAQ,gBAAA,EAAiB;AAGpE,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,EAAK,cAAA,EAAgB;AAAA,IAC/C,GAAG,OAAA;AAAA,IACH,IAAA,EAAM;AAAA,MACJ,QAAA,EAAU,MAAA;AAAA,MACV,cAAA,EAAgB,IAAA;AAAA,MAChB,gBAAA,EAAkB,IAAA;AAAA,MAClB,kBAAA;AAAA,MACA,GAAI,OAAA,GAAU,EAAE,OAAA,KAAY,EAAC;AAAA,MAC7B,GAAG,OAAA,CAAQ;AAAA;AACb,GACD,CAAA;AACD,EAAA,IAAI,sBAAqB,EAAG;AAC1B,IAAA,MAAM,OAAO,MAAA,CAAO,IAAA;AACpB,IAAA,IAAA,CAAK,SAAA,GAAY,gBAAgB,IAAA,EAAM,CAAC,YAAY,IAAI,mBAAA,CAAoB,OAAO,CAAC,CAAA;AAAA,EACtF;AACA,EAAA,OAAO,MAAA;AACT;AAkBO,IAAM,kBAAA,GAAN,cAAiC,KAAA,CAAM;AAAA,EACnC,IAAA,GAAO,oBAAA;AAAA,EACP,IAAA;AAAA,EACA,OAAA;AAAA,EACA,IAAA;AAAA,EACT,YAAY,KAAA,EAA2B;AACrC,IAAA,KAAA,CAAM,KAAA,CAAM,OAAA,EAAS,EAAE,KAAA,EAAO,OAAO,CAAA;AACrC,IAAA,IAAA,CAAK,OAAO,KAAA,CAAM,IAAA;AAClB,IAAA,IAAA,CAAK,UAAU,KAAA,CAAM,OAAA;AACrB,IAAA,IAAA,CAAK,OAAO,KAAA,CAAM,IAAA;AAAA,EACpB;AACF;AASO,SAAS,KAAQ,MAAA,EAAiF;AACvG,EAAA,IAAI,OAAO,KAAA,EAAO,MAAM,IAAI,kBAAA,CAAmB,OAAO,KAAK,CAAA;AAC3D,EAAA,OAAO,MAAA,CAAO,IAAA;AAChB;AAwCA,eAAsB,eAAA,CAAgB,MAAA,EAA6B,OAAA,GAAkC,EAAC,EAAkB;AACtH,EAAA,IAAA,CAAM,MAAM,MAAA,CAAO,GAAA,CAAI,mBAAmB,CAAgF,CAAA;AAC1H,EAAA,MAAM,SAAS,OAAA,CAAQ,iBAAA;AAEvB,EAAA,IAAI,MAAA,IAAU,MAAA,CAAO,SAAA,EAAW,MAAM,uBAAuB,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAU,EAAG,OAAO,MAAA,KAAW,QAAA,GAAW,SAAS,qBAAqB,CAAA;AAEzJ,EAAA,MAAM,OAAO,IAAA,CAAK,OAAA,CAAQ,EAAE,KAAA,EAAO,SAAS,CAAA;AAC9C","file":"index.js","sourcesContent":["/**\n * The app's side of Sign in with Apple token revocation: hand the sign-in's\n * authorization code to the `genesis-apple-revoke` Edge Function (it keeps\n * Apple's refresh token server-side), and ask it to revoke after the account\n * is deleted. The function and its migration ship in this package; see the\n * README (\"Revoking Apple's tokens\").\n */\n\n/** The Edge Function's default name, as shipped in `functions/genesis-apple-revoke`. */\nexport const APPLE_REVOKE_FUNCTION = \"genesis-apple-revoke\";\n\n/** The store call waits on Apple's token endpoint (10 s timeout in the function). */\nconst STORE_TIMEOUT_MS = 20_000;\n/** The function answers the revoke call at once and works in the background; don't hold the delete screen for longer. */\nconst REVOKE_TIMEOUT_MS = 5_000;\n\n/** The parts of a Supabase client these helpers use. A `SupabaseClient` fits. */\nexport interface FunctionsClient {\n functions: {\n // Loose on purpose, so any supabase-js version's `invoke` fits.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n invoke: (name: string, options?: any) => PromiseLike<{ data: unknown; error: unknown }>;\n };\n}\n\n/** @stability Beta */\nexport interface StoreAppleTokenOptions {\n /** `authorizationCode` from `signInWithApple`'s result. Single-use, valid for five minutes: send it straight after sign-in. */\n authorizationCode: string;\n /** The client id the code was issued for: your bundle id on iOS. Must be in the function's APPLE_CLIENT_IDS; default its first. */\n clientId?: string;\n /** The Edge Function's name. Default `genesis-apple-revoke`. */\n functionName?: string;\n}\n\n/** @stability Beta */\nexport type StoreAppleTokenResult = { ok: true } | { ok: false; error: Error };\n\n/**\n * After a Sign in with Apple, sends the authorization code to the Edge\n * Function, which exchanges it for Apple's refresh token and keeps it where\n * only the server reads it, so deleting the account can revoke it. Call it\n * with the session from that sign-in. Never throws: the person is signed in\n * either way, so log a failure rather than showing it.\n *\n * @stability Beta\n */\nexport async function storeAppleToken(client: FunctionsClient, { authorizationCode, clientId, functionName = APPLE_REVOKE_FUNCTION }: StoreAppleTokenOptions): Promise<StoreAppleTokenResult> {\n try {\n const { error } = await client.functions.invoke(functionName, { body: { action: \"store\", code: authorizationCode, ...(clientId ? { clientId } : {}) }, timeout: STORE_TIMEOUT_MS });\n if (error) return { ok: false, error: error instanceof Error ? error : new Error(String((error as { message?: unknown }).message ?? error)) };\n return { ok: true };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };\n }\n}\n\n/**\n * Asks the Edge Function to revoke the tokens of deleted accounts now, rather\n * than at its next scheduled run. Never throws and never fails the caller:\n * the account is already gone and the queue keeps the token until a run\n * succeeds.\n */\nexport async function requestAppleRevocation(client: FunctionsClient, functionName = APPLE_REVOKE_FUNCTION): Promise<boolean> {\n try {\n const { error } = await client.functions.invoke(functionName, { body: { action: \"revoke\" }, timeout: REVOKE_TIMEOUT_MS });\n return !error;\n } catch {\n return false;\n }\n}\n","/**\n * Tenant features: the client half of `migrations/0004_genesis_tenant_features.sql`.\n * A tenant's admins turn a shipped feature off (or back on) for their tenant,\n * with a reason; its members read which ids are off and hide them.\n *\n * Exposure only: a switch hides a feature, it never grants or denies access\n * to data (that is RLS) and never sells it (that is an entitlement).\n */\n// A cycle with ./index, used only inside the functions.\nimport { must, type PostgrestErrorLike } from \"./index\";\n\n/** The parts of a Supabase client these helpers use. A `SupabaseClient` fits. */\nexport interface TenantFeaturesClient {\n // Loose on purpose: a client typed by an app's Database narrows `rpc`'s\n // function names, and the app's generated types may not list these yet.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n rpc: (fn: any, ...rest: any[]) => PromiseLike<{ data: unknown; error: PostgrestErrorLike | null }>;\n}\n\ntype RpcResult = { data: unknown; error: null } | { data: null; error: PostgrestErrorLike };\n\n/** Which features a tenant has turned off. A feature not in `off` is on. @stability Beta */\nexport interface TenantFeatures {\n readonly tenantId: string;\n readonly off: ReadonlySet<string>;\n isOn(featureId: string): boolean;\n}\n\n/**\n * The features `tenantId` has turned off, for one of its members (or\n * admins): `tenant_features_off`. Throws a `SupabaseQueryError` (`42501`)\n * for anyone else, so a refusal is never mistaken for \"everything on\".\n *\n * ```ts\n * const features = await tenantFeatures(supabase, teamId);\n * if (features.isOn(\"chat\")) showChatTab();\n * ```\n *\n * @stability Beta\n */\nexport async function tenantFeatures(client: TenantFeaturesClient, tenantId: string): Promise<TenantFeatures> {\n const data = must((await client.rpc(\"tenant_features_off\", { tenant: tenantId })) as RpcResult);\n const off = new Set(Array.isArray(data) ? data.filter((id): id is string => typeof id === \"string\") : []);\n return Object.freeze({ tenantId, off, isOn: (featureId: string) => !off.has(featureId) });\n}\n\n/** @stability Beta */\nexport interface SetTenantFeatureInput {\n tenantId: string;\n featureId: string;\n enabled: boolean;\n /** Why, in at most 500 characters. Required: it goes in the history. */\n reason: string;\n}\n\n/**\n * Turn a feature on or off for a tenant: `set_tenant_feature`. Only the\n * tenant's admins (the app's `private.genesis_tenant_admin` hook); throws a\n * `SupabaseQueryError` otherwise (`42501`), for a blank reason (`22023`) or\n * a malformed feature id (`23514`).\n *\n * @stability Beta\n */\nexport async function setTenantFeature(client: TenantFeaturesClient, input: SetTenantFeatureInput): Promise<void> {\n must(\n (await client.rpc(\"set_tenant_feature\", {\n tenant: input.tenantId,\n feature: input.featureId,\n enabled: input.enabled,\n reason: input.reason,\n })) as RpcResult,\n );\n}\n\n/** One switch a tenant has set. @stability Beta */\nexport interface TenantFeatureSetting {\n feature_id: string;\n enabled: boolean;\n reason: string;\n updated_by: string | null;\n updated_at: string;\n}\n\n/** Every switch a tenant has set, with its reason, for its admins: `tenant_feature_settings`. @stability Beta */\nexport async function tenantFeatureSettings(client: TenantFeaturesClient, tenantId: string): Promise<TenantFeatureSetting[]> {\n return (must((await client.rpc(\"tenant_feature_settings\", { tenant: tenantId })) as RpcResult) ?? []) as TenantFeatureSetting[];\n}\n\n/** One change to a tenant's switches. `enabled` is null when a switch row was deleted (back to on). @stability Beta */\nexport interface TenantFeatureChange {\n feature_id: string;\n enabled: boolean | null;\n reason: string | null;\n /** The person, or null for the service role, a migration, or a person since deleted. */\n changed_by: string | null;\n changed_role: string;\n changed_at: string;\n}\n\n/** A tenant's history of changes, newest first (at most `maxRows`, 1 to 1000, default 100), for its admins. @stability Beta */\nexport async function tenantFeatureHistory(\n client: TenantFeaturesClient,\n tenantId: string,\n options: { maxRows?: number } = {},\n): Promise<TenantFeatureChange[]> {\n const args = options.maxRows === undefined ? { tenant: tenantId } : { tenant: tenantId, max_rows: options.maxRows };\n return (must((await client.rpc(\"tenant_feature_history\", args)) as RpcResult) ?? []) as TenantFeatureChange[];\n}\n","/**\n * Supabase for Genesis apps. Platform-neutral: web, React Native and the\n * server import it. Native-only helpers (Keychain storage, foreground\n * refresh, reinstall wipe) are on `@marktiderman/genesis-supabase/native`.\n */\nimport { AuthInvalidJwtError, createClient, type SupabaseClient, type SupabaseClientOptions } from \"@supabase/supabase-js\";\nimport { APPLE_REVOKE_FUNCTION, requestAppleRevocation, type FunctionsClient } from \"./apple-tokens\";\nimport { assertPkceCrypto, claimsViaServer, installWebCrypto, isDigestOnlyPolyfill, type ExpoCryptoLike } from \"./web-crypto\";\n\nexport type { ExpoCryptoLike } from \"./web-crypto\";\nexport {\n APPLE_REVOKE_FUNCTION,\n requestAppleRevocation,\n storeAppleToken,\n type FunctionsClient,\n type StoreAppleTokenOptions,\n type StoreAppleTokenResult,\n} from \"./apple-tokens\";\n\n/** The async key-value store Supabase auth keeps its session in. */\nexport interface SessionStorage {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\n/** @stability Beta */\nexport interface GenesisSupabaseOptions {\n url: string;\n /** The publishable (anon) key. Never the secret key in an app. */\n publishableKey: string;\n /** Where the session lives. Native: `keychainStorage(SecureStore)`. Web: leave out for supabase-js's own default. */\n storage?: SessionStorage;\n /** `\"native\"` sets the React Native defaults (no session from the URL). Default `\"web\"`. */\n platform?: \"web\" | \"native\";\n /**\n * Read a session from the URL after an OAuth redirect. Default: `true` on\n * web, `false` on native, where the app hands the redirect URL to\n * `exchangeCodeForSession` itself.\n */\n detectSessionInUrl?: boolean;\n /**\n * `expo-crypto` (`import * as ExpoCrypto from \"expo-crypto\"`), on native.\n * Hermes has no `crypto.subtle`, and without it supabase-js sends the PKCE\n * challenge as \"plain\"; this installs `crypto.getRandomValues` and a SHA-256\n * `crypto.subtle.digest` from it (real WebCrypto is kept), so the challenge\n * is S256. A PKCE client made where there is neither throws in development\n * and logs an error in a release build. With the polyfill in place,\n * `auth.getClaims` always has the server check the token (`getUser`), as\n * supabase-js does where WebCrypto is missing: the polyfill can hash but\n * not verify a signature.\n */\n crypto?: ExpoCryptoLike;\n /** Anything else for supabase-js, merged last. */\n options?: SupabaseClientOptions<\"public\">;\n}\n\n/**\n * A Supabase client with Genesis's defaults: PKCE sign-in (the code flow, not\n * tokens in a redirect URL, with an S256 challenge: pass `crypto` on native),\n * a persisted session that refreshes itself, and\n * the session storage you pass.\n *\n * @stability Beta\n */\nexport function createGenesisSupabase<Database = unknown>({\n url,\n publishableKey,\n storage,\n platform = \"web\",\n detectSessionInUrl = platform === \"web\",\n crypto,\n options = {},\n}: GenesisSupabaseOptions): SupabaseClient<Database> {\n if (crypto) installWebCrypto(crypto);\n // The PKCE flow needs WebCrypto for an S256 challenge; never let it fall back to \"plain\" quietly.\n if ((options.auth?.flowType ?? \"pkce\") === \"pkce\") assertPkceCrypto();\n // supabase-js's generics over the schema don't survive being forwarded, so the\n // options are built untyped and the client is typed by the caller's Database.\n const client = createClient(url, publishableKey, {\n ...options,\n auth: {\n flowType: \"pkce\",\n persistSession: true,\n autoRefreshToken: true,\n detectSessionInUrl,\n ...(storage ? { storage } : {}),\n ...options.auth,\n },\n });\n if (isDigestOnlyPolyfill()) {\n const auth = client.auth;\n auth.getClaims = claimsViaServer(auth, (message) => new AuthInvalidJwtError(message)) as typeof auth.getClaims;\n }\n return client as unknown as SupabaseClient<Database>;\n}\n\n/** What a Supabase or PostgREST error carries. */\nexport interface PostgrestErrorLike {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\n\n/**\n * A failed query, with the database's `code` (`23505` unique violation,\n * `42501` permission denied, `PGRST116` no rows…), `details` and `hint`, so a\n * caller can tell an RLS refusal from a conflict. The original error is the\n * `cause`.\n *\n * @stability Beta\n */\nexport class SupabaseQueryError extends Error {\n override name = \"SupabaseQueryError\";\n readonly code?: string;\n readonly details?: string | null;\n readonly hint?: string | null;\n constructor(error: PostgrestErrorLike) {\n super(error.message, { cause: error });\n this.code = error.code;\n this.details = error.details;\n this.hint = error.hint;\n }\n}\n\n/**\n * The data, or a thrown {@link SupabaseQueryError}: for query results where an\n * error should fail the caller (a TanStack Query `queryFn`). The return type\n * keeps `null` when the query can return no row (`.maybeSingle()`).\n *\n * @stability Beta\n */\nexport function must<T>(result: { data: T; error: null } | { data: null; error: PostgrestErrorLike }): T {\n if (result.error) throw new SupabaseQueryError(result.error);\n return result.data as T;\n}\n\n/** The parts of a Supabase client {@link deleteMyAccount} uses. A `SupabaseClient` fits. */\nexport interface DeleteAccountClient {\n // Loose on purpose: a client typed by an app's Database narrows `rpc`'s\n // function names, and the app's generated types may not list this one yet.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n rpc: (fn: any, ...rest: any[]) => PromiseLike<{ data: unknown; error: PostgrestErrorLike | null }>;\n auth: { signOut(options?: { scope?: \"global\" | \"local\" | \"others\" }): Promise<unknown> };\n /** Needed only with `revokeAppleTokens`. */\n functions?: FunctionsClient[\"functions\"];\n}\n\n/** @stability Beta */\nexport interface DeleteMyAccountOptions {\n /**\n * After the delete, ask the `genesis-apple-revoke` Edge Function to revoke\n * the person's Sign in with Apple tokens now (`true`, or the function's\n * name). Best effort: the delete has already queued them and the\n * function's schedule retries, so a failure here never fails the call.\n */\n revokeAppleTokens?: boolean | string;\n}\n\n/**\n * In-app account deletion (App Store Review Guideline 5.1.1(v)), on web and\n * native: calls the `delete_my_account` RPC (this package's\n * `migrations/0001_genesis_account_deletion.sql`), which deletes the login\n * and, through the app's `on delete cascade` foreign keys, its data; then\n * clears the session on this device. Throws a {@link SupabaseQueryError} when\n * the database refuses (`42501`: not signed in) and keeps the session then.\n *\n * With Sign in with Apple, ship the `genesis-apple-revoke` function and\n * migration 0003 and pass `{ revokeAppleTokens: true }`: App Review requires\n * the person's Apple tokens to be revoked.\n *\n * Put it behind a confirmation; see the README.\n *\n * @stability Beta\n */\nexport async function deleteMyAccount(client: DeleteAccountClient, options: DeleteMyAccountOptions = {}): Promise<void> {\n must((await client.rpc(\"delete_my_account\")) as { data: unknown; error: null } | { data: null; error: PostgrestErrorLike });\n const revoke = options.revokeAppleTokens;\n // The function needs no login (it works through the queue the delete just filled), so this runs after it.\n if (revoke && client.functions) await requestAppleRevocation({ functions: client.functions }, typeof revoke === \"string\" ? revoke : APPLE_REVOKE_FUNCTION);\n // The login is gone and its refresh tokens with it: only this device's copy is left to clear.\n await client.auth.signOut({ scope: \"local\" });\n}\nexport {\n tenantFeatures,\n setTenantFeature,\n tenantFeatureSettings,\n tenantFeatureHistory,\n type SetTenantFeatureInput,\n type TenantFeatureChange,\n type TenantFeatureSetting,\n type TenantFeatures,\n type TenantFeaturesClient,\n} from \"./tenant-features\";\nexport { accountBlock, tenantFeaturesBlock } from \"./blocks\";\n"]}
|
package/dist/native.d.ts
CHANGED
|
@@ -1,10 +1,238 @@
|
|
|
1
|
-
import { SessionStorage } from './index.js';
|
|
2
|
-
|
|
1
|
+
import { S as SessionStorage } from './index-BSGz3RGk.js';
|
|
2
|
+
export { E as ExpoCryptoLike, W as WebCryptoSource, a as assertPkceCrypto, h as hasPkceCrypto, i as installWebCrypto, b as isDigestOnlyPolyfill } from './index-BSGz3RGk.js';
|
|
3
|
+
import { Session, SupabaseClient, User } from '@supabase/supabase-js';
|
|
4
|
+
import './blocks.js';
|
|
5
|
+
import '@marktiderman/genesis-features/blocks';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Native sign-in onto a Supabase session: finishing an OAuth redirect (PKCE),
|
|
9
|
+
* Sign in with Apple and native Google sign-in. Nothing here imports Expo or
|
|
10
|
+
* the Google module: pass the modules in.
|
|
11
|
+
* Exported from `@marktiderman/genesis-supabase/native`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Why a sign-in didn't produce a session. `code` is the provider's error
|
|
16
|
+
* (`access_denied`…), Supabase Auth's error code, or one of Genesis's own:
|
|
17
|
+
* `missing_code` (the redirect carried neither a code nor an error),
|
|
18
|
+
* `oauth_error`, `exchange_failed`, `apple_unavailable`, `apple_failed`,
|
|
19
|
+
* `apple_no_token`, `google_failed`, `google_no_token`,
|
|
20
|
+
* `google_nonce_missing`, `bad_digest`, `sign_in_failed`, `no_session`.
|
|
21
|
+
*
|
|
22
|
+
* @stability Beta
|
|
23
|
+
*/
|
|
24
|
+
declare class GenesisAuthError extends Error {
|
|
25
|
+
name: string;
|
|
26
|
+
readonly code: string;
|
|
27
|
+
/**
|
|
28
|
+
* The provider's own `error_description`, as it came in the redirect.
|
|
29
|
+
* UNTRUSTED: any app or web page can open your callback link with any text
|
|
30
|
+
* here, so never show it as an official message. Log it; show `message`.
|
|
31
|
+
*/
|
|
32
|
+
readonly providerMessage?: string;
|
|
33
|
+
constructor(code: string, message: string, options?: {
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
providerMessage?: string;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The parts of a Supabase client the sign-in helpers use. A `SupabaseClient` fits. */
|
|
39
|
+
interface SignInClient {
|
|
40
|
+
auth: Pick<SupabaseClient["auth"], "exchangeCodeForSession" | "signInWithIdToken">;
|
|
41
|
+
}
|
|
42
|
+
/** @stability Beta */
|
|
43
|
+
type OAuthRedirectResult = {
|
|
44
|
+
session: Session;
|
|
45
|
+
/** `"recovery"` after a password-reset link (send them to set a new password), otherwise `null`. */
|
|
46
|
+
redirectType: string | null;
|
|
47
|
+
error?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
session?: undefined;
|
|
50
|
+
redirectType?: undefined;
|
|
51
|
+
error: GenesisAuthError;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The query and fragment parameters of a URL (the query wins for a key in
|
|
55
|
+
* both). Hand-parsed: React Native's `URL` has no reliable `searchParams`.
|
|
56
|
+
*/
|
|
57
|
+
declare function redirectParams(url: string): Record<string, string>;
|
|
58
|
+
/**
|
|
59
|
+
* Finish an OAuth sign-in from the URL the browser came back to. With a `code`
|
|
60
|
+
* it exchanges it for a session (PKCE: the verifier is already in the client's
|
|
61
|
+
* storage), passing the redirect's `sb_flow_id` so the right flow's verifier
|
|
62
|
+
* is used; with an `error` it returns that error and makes no request.
|
|
63
|
+
* Nothing is thrown; every failure is `{ error }`.
|
|
64
|
+
*
|
|
65
|
+
* Pass only the URL `WebBrowser.openAuthSessionAsync` resolved with, for the
|
|
66
|
+
* sign-in this app just started. A code is single-use and the exchange uses
|
|
67
|
+
* up the stored verifier whatever happens, so a stray `?code=` from another
|
|
68
|
+
* link spends the sign-in in progress, and handling the same redirect twice
|
|
69
|
+
* (the auth-session result and a `Linking` listener) fails the second time.
|
|
70
|
+
* Handle each redirect once.
|
|
71
|
+
*
|
|
72
|
+
* The error `message` is fixed copy chosen by `code`; the redirect's own text
|
|
73
|
+
* is only in `providerMessage`, which is untrusted.
|
|
74
|
+
*
|
|
75
|
+
* @stability Beta
|
|
76
|
+
*/
|
|
77
|
+
declare function completeOAuthRedirect(client: SignInClient, url: string): Promise<OAuthRedirectResult>;
|
|
78
|
+
/** A person's name as `expo-apple-authentication` returns it. */
|
|
79
|
+
interface AppleFullNameLike {
|
|
80
|
+
givenName?: string | null;
|
|
81
|
+
middleName?: string | null;
|
|
82
|
+
familyName?: string | null;
|
|
83
|
+
}
|
|
84
|
+
/** The parts of `expo-apple-authentication` that {@link signInWithApple} uses. Pass the module itself. */
|
|
85
|
+
interface AppleAuthenticationLike {
|
|
86
|
+
isAvailableAsync(): Promise<boolean>;
|
|
87
|
+
signInAsync(options: {
|
|
88
|
+
requestedScopes?: number[];
|
|
89
|
+
nonce?: string;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
identityToken: string | null;
|
|
92
|
+
/** Single-use, valid about five minutes: what a server exchanges for the Apple refresh token it revokes on deletion. */
|
|
93
|
+
authorizationCode: string | null;
|
|
94
|
+
fullName: AppleFullNameLike | null;
|
|
95
|
+
email: string | null;
|
|
96
|
+
}>;
|
|
97
|
+
AppleAuthenticationScope: {
|
|
98
|
+
FULL_NAME: number;
|
|
99
|
+
EMAIL: number;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** @stability Beta */
|
|
103
|
+
interface SignInWithAppleOptions {
|
|
104
|
+
/**
|
|
105
|
+
* SHA-256 of a string as lowercase hex, e.g.
|
|
106
|
+
* `(s) => Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, s)` from `expo-crypto`.
|
|
107
|
+
* Apple gets the hash; Supabase gets the raw nonce and checks the token against it.
|
|
108
|
+
*/
|
|
109
|
+
digest(value: string): Promise<string>;
|
|
110
|
+
/**
|
|
111
|
+
* A fresh random nonce. Default: 32 bytes from `crypto.getRandomValues`, as
|
|
112
|
+
* hex. On React Native that is the expo-crypto polyfill
|
|
113
|
+
* `createGenesisSupabase({ platform: "native", crypto })` installs; leave
|
|
114
|
+
* this out rather than pass something with less randomness.
|
|
115
|
+
*/
|
|
116
|
+
randomNonce?: () => string;
|
|
117
|
+
}
|
|
118
|
+
/** @stability Beta */
|
|
119
|
+
type SignInWithAppleResult = {
|
|
120
|
+
session: Session;
|
|
121
|
+
user: User;
|
|
122
|
+
/** The name Apple shared. Apple sends it on the first sign-in only: save it then. */
|
|
123
|
+
fullName: string | null;
|
|
124
|
+
/** The email Apple shared (possibly a private relay address), on the first sign-in only. */
|
|
125
|
+
email: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* Apple's single-use authorization code (valid about five minutes). Send it
|
|
128
|
+
* to your server straight away: it exchanges it for the Apple refresh token
|
|
129
|
+
* that account deletion must revoke. See the README.
|
|
130
|
+
*/
|
|
131
|
+
authorizationCode: string | null;
|
|
132
|
+
canceled?: undefined;
|
|
133
|
+
error?: undefined;
|
|
134
|
+
} | {
|
|
135
|
+
canceled: true;
|
|
136
|
+
session?: undefined;
|
|
137
|
+
error?: undefined;
|
|
138
|
+
} | {
|
|
139
|
+
error: GenesisAuthError;
|
|
140
|
+
/** When Apple shared a name but Supabase then refused: Apple won't send it again, so keep it for the retry. */
|
|
141
|
+
fullName?: string | null;
|
|
142
|
+
session?: undefined;
|
|
143
|
+
canceled?: undefined;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Native Sign in with Apple straight into a Supabase session: checks the
|
|
147
|
+
* device supports it, asks for name and email with a SHA-256 hashed nonce, and
|
|
148
|
+
* signs in with the identity token. Closing Apple's sheet is `{ canceled: true }`,
|
|
149
|
+
* not an error. Nothing is thrown; every failure is `{ error }`.
|
|
150
|
+
*
|
|
151
|
+
* @stability Beta
|
|
152
|
+
*/
|
|
153
|
+
declare function signInWithApple(client: SignInClient, apple: AppleAuthenticationLike, { digest, randomNonce }: SignInWithAppleOptions): Promise<SignInWithAppleResult>;
|
|
154
|
+
/**
|
|
155
|
+
* The part of `@react-native-google-signin/google-signin` that
|
|
156
|
+
* {@link signInWithGoogle} uses: its Universal sign-in module,
|
|
157
|
+
* `GoogleOneTapSignIn`, configured (`configure({ webClientId, iosClientId })`)
|
|
158
|
+
* before the first sign-in. Pass the module itself.
|
|
159
|
+
*
|
|
160
|
+
* Only Universal sign-in passes a nonce into the ID token on Android and iOS.
|
|
161
|
+
* The free package's original `GoogleSignin.signIn` can't (its `nonce` is iOS
|
|
162
|
+
* and paid-version only), so its tokens fail the nonce check below.
|
|
163
|
+
*/
|
|
164
|
+
interface GoogleSignInLike {
|
|
165
|
+
authenticate(params?: {
|
|
166
|
+
nonce?: string;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
user?: {
|
|
169
|
+
idToken?: string | null;
|
|
170
|
+
user?: {
|
|
171
|
+
email?: string | null;
|
|
172
|
+
name?: string | null;
|
|
173
|
+
} | null;
|
|
174
|
+
} | null;
|
|
175
|
+
error?: {
|
|
176
|
+
code?: string | number;
|
|
177
|
+
message?: string;
|
|
178
|
+
} | null;
|
|
179
|
+
isCancelled?: boolean;
|
|
180
|
+
}>;
|
|
181
|
+
}
|
|
182
|
+
/** @stability Beta */
|
|
183
|
+
interface SignInWithGoogleOptions {
|
|
184
|
+
/** SHA-256 of a string as lowercase hex, as for {@link signInWithApple}. Google gets the hash; Supabase gets the raw nonce. */
|
|
185
|
+
digest(value: string): Promise<string>;
|
|
186
|
+
/** A fresh random nonce. Default: 32 bytes from `crypto.getRandomValues`, as hex. */
|
|
187
|
+
randomNonce?: () => string;
|
|
188
|
+
}
|
|
189
|
+
/** @stability Beta */
|
|
190
|
+
type SignInWithGoogleResult = {
|
|
191
|
+
session: Session;
|
|
192
|
+
user: User;
|
|
193
|
+
/** The name Google shared, if any. */
|
|
194
|
+
fullName: string | null;
|
|
195
|
+
email: string | null;
|
|
196
|
+
canceled?: undefined;
|
|
197
|
+
error?: undefined;
|
|
198
|
+
} | {
|
|
199
|
+
canceled: true;
|
|
200
|
+
session?: undefined;
|
|
201
|
+
error?: undefined;
|
|
202
|
+
} | {
|
|
203
|
+
error: GenesisAuthError;
|
|
204
|
+
session?: undefined;
|
|
205
|
+
canceled?: undefined;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Native Google sign-in straight into a Supabase session, as Supabase's
|
|
209
|
+
* Google guide describes: a fresh random nonce, its SHA-256 (hex) to Google,
|
|
210
|
+
* which puts it in the ID token, and the raw nonce to `signInWithIdToken`,
|
|
211
|
+
* which checks the two match, so a token minted for another sign-in, which
|
|
212
|
+
* carries that sign-in's nonce, can't be replayed through this app. Before
|
|
213
|
+
* Supabase sees the token it is checked here too: a token whose `nonce` claim
|
|
214
|
+
* isn't this sign-in's hash (a module that ignored the nonce) is
|
|
215
|
+
* `google_nonce_missing`, never sent. Closing Google's sheet is
|
|
216
|
+
* `{ canceled: true }`. Nothing is thrown; every failure is `{ error }`.
|
|
217
|
+
*
|
|
218
|
+
* Keep the Supabase Google provider's "Skip nonce check" off: this helper
|
|
219
|
+
* needs no exception, and turning it on lets a stolen nonce-bearing ID token
|
|
220
|
+
* sign in. What a nonce can't stop: Supabase still accepts an ID token with
|
|
221
|
+
* no nonce claim when the caller sends none, so a stolen nonce-less token for
|
|
222
|
+
* one of your client ids signs in through a direct API call whatever the app
|
|
223
|
+
* does. Keep the "Authorized Client IDs" list to your own apps.
|
|
224
|
+
*
|
|
225
|
+
* @stability Beta
|
|
226
|
+
*/
|
|
227
|
+
declare function signInWithGoogle(client: SignInClient, google: GoogleSignInLike, { digest, randomNonce }: SignInWithGoogleOptions): Promise<SignInWithGoogleResult>;
|
|
228
|
+
/** "Given Middle Family", or null when Apple shared no name. */
|
|
229
|
+
declare function formatFullName(name: AppleFullNameLike | null | undefined): string | null;
|
|
3
230
|
|
|
4
231
|
/**
|
|
5
232
|
* React Native helpers for a Supabase session. Nothing here imports React
|
|
6
|
-
* Native or Expo: pass in `expo-secure-store`, `AppState
|
|
7
|
-
*
|
|
233
|
+
* Native or Expo: pass in `expo-secure-store`, `AppState`,
|
|
234
|
+
* `expo-apple-authentication` and a plain store, so the package stays light
|
|
235
|
+
* and the helpers are easy to test.
|
|
8
236
|
*/
|
|
9
237
|
|
|
10
238
|
/** The parts of `expo-secure-store` the Keychain storage uses. */
|
|
@@ -81,4 +309,4 @@ declare function wipeSessionOnReinstall({ session, sessionKey, installMarker, ma
|
|
|
81
309
|
/** Split text into pieces of at most `maxBytes` UTF-8 bytes, never inside a character. */
|
|
82
310
|
declare function splitUtf8(text: string, maxBytes: number): string[];
|
|
83
311
|
|
|
84
|
-
export { type AppStateLike, type AutoRefreshAuth, type KeychainStorageOptions, type SecureStoreLike, type WipeOnReinstallOptions, keychainStorage, refreshWhileActive, splitUtf8, wipeSessionOnReinstall };
|
|
312
|
+
export { type AppStateLike, type AppleAuthenticationLike, type AppleFullNameLike, type AutoRefreshAuth, GenesisAuthError, type GoogleSignInLike, type KeychainStorageOptions, type OAuthRedirectResult, type SecureStoreLike, type SignInClient, type SignInWithAppleOptions, type SignInWithAppleResult, type SignInWithGoogleOptions, type SignInWithGoogleResult, type WipeOnReinstallOptions, completeOAuthRedirect, formatFullName, keychainStorage, redirectParams, refreshWhileActive, signInWithApple, signInWithGoogle, splitUtf8, wipeSessionOnReinstall };
|
package/dist/native.js
CHANGED
|
@@ -1,3 +1,198 @@
|
|
|
1
|
+
export { assertPkceCrypto, hasPkceCrypto, installWebCrypto, isDigestOnlyPolyfill } from './chunk-FAA2FTF4.js';
|
|
2
|
+
|
|
3
|
+
// src/sign-in.ts
|
|
4
|
+
var GenesisAuthError = class extends Error {
|
|
5
|
+
name = "GenesisAuthError";
|
|
6
|
+
code;
|
|
7
|
+
/**
|
|
8
|
+
* The provider's own `error_description`, as it came in the redirect.
|
|
9
|
+
* UNTRUSTED: any app or web page can open your callback link with any text
|
|
10
|
+
* here, so never show it as an official message. Log it; show `message`.
|
|
11
|
+
*/
|
|
12
|
+
providerMessage;
|
|
13
|
+
constructor(code, message, options = {}) {
|
|
14
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
15
|
+
this.code = code;
|
|
16
|
+
if (options.providerMessage !== void 0) this.providerMessage = options.providerMessage;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var REDIRECT_MESSAGES = {
|
|
20
|
+
access_denied: "Sign-in was canceled or not allowed.",
|
|
21
|
+
otp_expired: "That sign-in link has expired. Ask for a new one.",
|
|
22
|
+
flow_state_expired: "That sign-in took too long. Try again.",
|
|
23
|
+
flow_state_not_found: "That sign-in has already been used or has expired. Try again.",
|
|
24
|
+
bad_oauth_state: "That sign-in couldn't be checked. Try again.",
|
|
25
|
+
bad_oauth_callback: "That sign-in couldn't be checked. Try again.",
|
|
26
|
+
provider_email_needs_verification: "Verify your email with the provider, then try again.",
|
|
27
|
+
server_error: "The sign-in service had a problem. Try again.",
|
|
28
|
+
temporarily_unavailable: "The sign-in service is busy. Try again in a moment."
|
|
29
|
+
};
|
|
30
|
+
var REDIRECT_FALLBACK = "Sign-in didn't complete. Try again.";
|
|
31
|
+
var hasOwn = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
32
|
+
function redirectParams(url) {
|
|
33
|
+
const params = /* @__PURE__ */ Object.create(null);
|
|
34
|
+
const hash = url.indexOf("#");
|
|
35
|
+
const query = url.indexOf("?");
|
|
36
|
+
const parts = [
|
|
37
|
+
query >= 0 && (hash < 0 || query < hash) ? url.slice(query + 1, hash >= 0 ? hash : void 0) : "",
|
|
38
|
+
hash >= 0 ? url.slice(hash + 1) : ""
|
|
39
|
+
];
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
for (const pair of part.split("&")) {
|
|
42
|
+
if (!pair) continue;
|
|
43
|
+
const eq = pair.indexOf("=");
|
|
44
|
+
const key = decode(eq >= 0 ? pair.slice(0, eq) : pair);
|
|
45
|
+
if (!hasOwn(params, key)) params[key] = decode(eq >= 0 ? pair.slice(eq + 1) : "");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return params;
|
|
49
|
+
}
|
|
50
|
+
function decode(s) {
|
|
51
|
+
try {
|
|
52
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
53
|
+
} catch {
|
|
54
|
+
return s;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function completeOAuthRedirect(client, url) {
|
|
58
|
+
const params = redirectParams(url);
|
|
59
|
+
if (params.error || params.error_description || params.error_code) {
|
|
60
|
+
const code = params.error_code || params.error || "oauth_error";
|
|
61
|
+
const message = REDIRECT_MESSAGES[code] ?? (params.error ? REDIRECT_MESSAGES[params.error] : void 0) ?? REDIRECT_FALLBACK;
|
|
62
|
+
return { error: new GenesisAuthError(code, message, { providerMessage: params.error_description || void 0 }) };
|
|
63
|
+
}
|
|
64
|
+
if (!params.code) return { error: new GenesisAuthError("missing_code", "The sign-in redirect carried no code") };
|
|
65
|
+
try {
|
|
66
|
+
const { data, error } = await client.auth.exchangeCodeForSession(
|
|
67
|
+
params.code,
|
|
68
|
+
params.sb_flow_id ? { flowId: params.sb_flow_id } : void 0
|
|
69
|
+
);
|
|
70
|
+
if (error) return { error: new GenesisAuthError(error.code ?? "exchange_failed", error.message, { cause: error }) };
|
|
71
|
+
if (!data.session) return { error: new GenesisAuthError("no_session", "The code exchange returned no session") };
|
|
72
|
+
const redirectType = data.redirectType ?? null;
|
|
73
|
+
return { session: data.session, redirectType };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { error: new GenesisAuthError("exchange_failed", e instanceof Error ? e.message : "The code exchange failed", { cause: e }) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function signInWithApple(client, apple, { digest, randomNonce = defaultNonce }) {
|
|
79
|
+
const unavailable = (cause) => ({
|
|
80
|
+
error: new GenesisAuthError("apple_unavailable", "Sign in with Apple isn't available on this device", { cause })
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
if (!await apple.isAvailableAsync()) return unavailable();
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return unavailable(e);
|
|
86
|
+
}
|
|
87
|
+
let credential;
|
|
88
|
+
let nonce;
|
|
89
|
+
try {
|
|
90
|
+
nonce = randomNonce();
|
|
91
|
+
const hashed = await digest(nonce);
|
|
92
|
+
if (!/^[0-9a-f]{64}$/.test(hashed)) {
|
|
93
|
+
return { error: new GenesisAuthError("bad_digest", "signInWithApple: digest must return SHA-256 as 64 lowercase hex characters") };
|
|
94
|
+
}
|
|
95
|
+
credential = await apple.signInAsync({
|
|
96
|
+
requestedScopes: [apple.AppleAuthenticationScope.FULL_NAME, apple.AppleAuthenticationScope.EMAIL],
|
|
97
|
+
nonce: hashed
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (e?.code === "ERR_REQUEST_CANCELED") return { canceled: true };
|
|
101
|
+
return { error: new GenesisAuthError("apple_failed", e instanceof Error ? e.message : "Sign in with Apple failed", { cause: e }) };
|
|
102
|
+
}
|
|
103
|
+
const fullName = formatFullName(credential.fullName);
|
|
104
|
+
if (!credential.identityToken) return { error: new GenesisAuthError("apple_no_token", "Apple returned no identity token"), fullName };
|
|
105
|
+
try {
|
|
106
|
+
const { data, error } = await client.auth.signInWithIdToken({ provider: "apple", token: credential.identityToken, nonce });
|
|
107
|
+
if (error) return { error: new GenesisAuthError(error.code ?? "sign_in_failed", error.message, { cause: error }), fullName };
|
|
108
|
+
if (!data.session || !data.user) return { error: new GenesisAuthError("no_session", "Sign-in returned no session"), fullName };
|
|
109
|
+
return {
|
|
110
|
+
session: data.session,
|
|
111
|
+
user: data.user,
|
|
112
|
+
fullName,
|
|
113
|
+
email: credential.email ?? null,
|
|
114
|
+
authorizationCode: credential.authorizationCode ?? null
|
|
115
|
+
};
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { error: new GenesisAuthError("sign_in_failed", e instanceof Error ? e.message : "Sign-in failed", { cause: e }), fullName };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function signInWithGoogle(client, google, { digest, randomNonce = defaultNonce }) {
|
|
121
|
+
let nonce;
|
|
122
|
+
let hashed;
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
nonce = randomNonce();
|
|
126
|
+
hashed = await digest(nonce);
|
|
127
|
+
if (!/^[0-9a-f]{64}$/.test(hashed)) {
|
|
128
|
+
return { error: new GenesisAuthError("bad_digest", "signInWithGoogle: digest must return SHA-256 as 64 lowercase hex characters") };
|
|
129
|
+
}
|
|
130
|
+
response = await google.authenticate({ nonce: hashed });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { error: new GenesisAuthError("google_failed", e instanceof Error ? e.message : "Google sign-in failed", { cause: e }) };
|
|
133
|
+
}
|
|
134
|
+
if (response.isCancelled) return { canceled: true };
|
|
135
|
+
if (response.error) {
|
|
136
|
+
return { error: new GenesisAuthError("google_failed", response.error.message ?? `Google sign-in failed (${String(response.error.code ?? "unknown")})`, { cause: response.error }) };
|
|
137
|
+
}
|
|
138
|
+
const token = response.user?.idToken;
|
|
139
|
+
if (!token) return { error: new GenesisAuthError("google_no_token", "Google returned no ID token") };
|
|
140
|
+
if (jwtClaim(token, "nonce") !== hashed) {
|
|
141
|
+
return {
|
|
142
|
+
error: new GenesisAuthError(
|
|
143
|
+
"google_nonce_missing",
|
|
144
|
+
"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."
|
|
145
|
+
)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const { data, error } = await client.auth.signInWithIdToken({ provider: "google", token, nonce });
|
|
150
|
+
if (error) return { error: new GenesisAuthError(error.code ?? "sign_in_failed", error.message, { cause: error }) };
|
|
151
|
+
if (!data.session || !data.user) return { error: new GenesisAuthError("no_session", "Sign-in returned no session") };
|
|
152
|
+
const profile = response.user?.user;
|
|
153
|
+
return { session: data.session, user: data.user, fullName: profile?.name?.trim() || null, email: profile?.email ?? null };
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return { error: new GenesisAuthError("sign_in_failed", e instanceof Error ? e.message : "Sign-in failed", { cause: e }) };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function jwtClaim(token, claim) {
|
|
159
|
+
const payload = token.split(".")[1];
|
|
160
|
+
if (!payload) return void 0;
|
|
161
|
+
try {
|
|
162
|
+
const value = JSON.parse(decodeBase64Url(payload))?.[claim];
|
|
163
|
+
return typeof value === "string" ? value : void 0;
|
|
164
|
+
} catch {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
169
|
+
function decodeBase64Url(input) {
|
|
170
|
+
let escaped = "";
|
|
171
|
+
let buffer = 0;
|
|
172
|
+
let bits = 0;
|
|
173
|
+
for (const ch of input.replace(/=+$/, "")) {
|
|
174
|
+
const v = B64.indexOf(ch === "+" ? "-" : ch === "/" ? "_" : ch);
|
|
175
|
+
if (v < 0) throw new Error("not base64url");
|
|
176
|
+
buffer = buffer << 6 | v;
|
|
177
|
+
bits += 6;
|
|
178
|
+
if (bits >= 8) {
|
|
179
|
+
bits -= 8;
|
|
180
|
+
escaped += `%${(buffer >> bits & 255).toString(16).padStart(2, "0")}`;
|
|
181
|
+
buffer &= (1 << bits) - 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return decodeURIComponent(escaped);
|
|
185
|
+
}
|
|
186
|
+
function formatFullName(name) {
|
|
187
|
+
if (!name) return null;
|
|
188
|
+
return [name.givenName, name.middleName, name.familyName].map((p) => p?.trim()).filter(Boolean).join(" ") || null;
|
|
189
|
+
}
|
|
190
|
+
function defaultNonce() {
|
|
191
|
+
const c = globalThis.crypto;
|
|
192
|
+
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");
|
|
193
|
+
return Array.from(c.getRandomValues(new Uint8Array(32)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
194
|
+
}
|
|
195
|
+
|
|
1
196
|
// src/native.ts
|
|
2
197
|
function keychainStorage(secureStore, options = {}) {
|
|
3
198
|
const size = options.chunkSize ?? 1800;
|
|
@@ -106,6 +301,6 @@ function parseFlowIndex(raw) {
|
|
|
106
301
|
}
|
|
107
302
|
}
|
|
108
303
|
|
|
109
|
-
export { keychainStorage, refreshWhileActive, splitUtf8, wipeSessionOnReinstall };
|
|
304
|
+
export { GenesisAuthError, completeOAuthRedirect, formatFullName, keychainStorage, redirectParams, refreshWhileActive, signInWithApple, signInWithGoogle, splitUtf8, wipeSessionOnReinstall };
|
|
110
305
|
//# sourceMappingURL=native.js.map
|
|
111
306
|
//# sourceMappingURL=native.js.map
|