@ai-matrx/auth 0.1.4 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog — @ai-matrx/auth
2
2
 
3
+ ## 0.2.0
4
+
5
+ **The apps' own identity, server-door and sign-in copies move in (consolidation U1).**
6
+ - `.` — `getClaimsUser` (local JWKS claims read, never `getUser()`), `pinnedSigningKeysFor` + the live
7
+ project's public signing key, `isAuthTransportFailure`, `isSignedOutError`, `userFromClaims`,
8
+ `accountFlagsFor`, the unverifiable-session detectors (`isUnverifiableTokenRefusal`,
9
+ `isUnverifiableTokenError`, `confirmUnverifiableSession`) and `createClaimsCache` (the browser's
10
+ de-duplicated claims read). From the website's `utils/supabase/{claimsUser,projectSigningKeys,
11
+ clientClaimsCache,unverifiableSession}.ts`; media carried an identical copy.
12
+ - `/next` and `/node` — `createServerAuth` (THE server identity door: claims, one retry on a fresh
13
+ client, React `cache` injected), `verifyRequest` (Bearer-or-cookie for API routes),
14
+ `createServiceClient` (the one server-only secret-key client).
15
+ - `/organizations` — the organization-required refusal: `isOrganizationRequiredError`,
16
+ `OrganizationRequiredServerError`, `organizationRequired`, `readCallerMemberships`,
17
+ `organizationRequiredEnvelope` / `organizationRequiredResponse` (aidream's envelope field for field),
18
+ `extractOrganizationHoldMemberships`.
19
+ - `/matrx-oauth` (new section) — "Sign in with AI Matrx" OAuth 2.1 + PKCE for non-web hosts:
20
+ `createMatrxOAuth(config, ports)` with single-flight refresh, host-injected storage, browser opener,
21
+ lock and refresh-token sealing; PKCE primitives. From the extension's `lib/auth/flow.ts` and the
22
+ desktop's `app/main/auth/{pkce,oauth-client}.ts`.
23
+ Tests: `claims.test.ts` (17), `matrx-oauth.test.ts` (18).
24
+
3
25
  ## 0.1.4
4
26
 
5
27
  **Fix: `getPerson` no longer calls the auth server on every access check (Wave 0).** The verified
package/README.md CHANGED
@@ -31,3 +31,14 @@ Session storage: browser `browserStorage()`, extension `extensionStorage()`, des
31
31
 
32
32
  The active organization is never a list filter: reads show everything the person can see; the
33
33
  active organization is only where new things are saved and which organization a server call runs in.
34
+
35
+ ## Sections added in 0.2.0 — one entry point each
36
+
37
+ | Need | Import |
38
+ |---|---|
39
+ | Who is calling (local claims; browser de-dup) | `getClaimsUser`, `createClaimsCache` from `@ai-matrx/auth` |
40
+ | Server identity in a Next app | `createServerAuth({ createClient, supabaseUrl, cache })` from `@ai-matrx/auth/next` |
41
+ | API route caller (Bearer or cookie) | `verifyRequest(request, { url, publishableKey, cookieClient })` from `@ai-matrx/auth/node` (or `/next`) |
42
+ | Server-only secret-key client | `createServiceClient({ url, secretKey })` from `@ai-matrx/auth/node` (or `/next`) |
43
+ | "Needs an organization" refusal | `isOrganizationRequiredError`, `organizationRequired`, `organizationRequiredResponse` from `@ai-matrx/auth/organizations` |
44
+ | Sign in with AI Matrx (extension, desktop, CLI) | `createMatrxOAuth(config, { storage, openAuthorization })` from `@ai-matrx/auth/matrx-oauth` |
@@ -0,0 +1,138 @@
1
+ import { JwtPayload, UserAppMetadata, UserMetadata, JWK, AuthError } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * THE identity read every server and client path shares (moved from the website's
5
+ * `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
6
+ *
7
+ * Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
8
+ * ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
9
+ * `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
10
+ * up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
11
+ *
12
+ * Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
13
+ * `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
14
+ * are thin wrappers over `getClaimsUser`.
15
+ */
16
+
17
+ /**
18
+ * The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
19
+ * `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
20
+ */
21
+ interface ApiClaimsUser extends JwtPayload {
22
+ /** The `sub` claim — the user id. */
23
+ id: string;
24
+ app_metadata: UserAppMetadata;
25
+ user_metadata: UserMetadata;
26
+ }
27
+ /** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
28
+ type ClaimsCapableClient = {
29
+ auth: {
30
+ getClaims(jwt?: string, options?: {
31
+ jwks?: {
32
+ keys: JWK[];
33
+ };
34
+ }): Promise<{
35
+ data: {
36
+ claims?: JwtPayload;
37
+ } | null;
38
+ error: AuthError | null;
39
+ }>;
40
+ };
41
+ };
42
+ interface ClaimsResult {
43
+ data: {
44
+ user: ApiClaimsUser | null;
45
+ };
46
+ error: AuthError | null;
47
+ }
48
+ /**
49
+ * `client.auth.getUser()` without the auth-server round trip, in the SAME envelope, with one
50
+ * deliberate distinction:
51
+ * - CONFIRMED SIGNED OUT (no session, no token) → `{ user: null, error: null }` — a settled fact.
52
+ * - COULD NOT VERIFY (authority/JWKS unreachable, malformed or badly-signed token, a verified
53
+ * token without `sub`) → `error` kept, so a retry branch fires and a screen never paints
54
+ * "signed out" over an outage.
55
+ * Pass `jwt` when the token is in hand (a Bearer header). `options.jwks` are known public keys
56
+ * tried BEFORE any JWKS fetch (`pinnedSigningKeysFor`) — can only remove a network call.
57
+ */
58
+ declare function getClaimsUser(client: ClaimsCapableClient, jwt?: string, options?: {
59
+ jwks: {
60
+ keys: JWK[];
61
+ };
62
+ }): Promise<ClaimsResult>;
63
+ /**
64
+ * Could we not REACH a verdict (transport failure, spent budget), as opposed to a token we reached
65
+ * a verdict on? Too narrow reads an outage as a logout; too wide holds a person with a forged or
66
+ * rotated-key token on "reload in a moment" forever while the proxy sends them to /login. This is
67
+ * the same line the `/next` middleware pass draws.
68
+ */
69
+ declare function isAuthTransportFailure(error: unknown): boolean;
70
+ /** Is this `getClaims()` error the settled fact "nobody is signed in"? */
71
+ declare function isSignedOutError(error: AuthError | {
72
+ name?: string;
73
+ code?: string;
74
+ }): boolean;
75
+ /** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
76
+ declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
77
+ /**
78
+ * GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
79
+ * `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
80
+ */
81
+ declare function accountFlagsFor(claims: {
82
+ is_anonymous?: boolean;
83
+ } | null | undefined): {
84
+ isGuest: boolean;
85
+ hasAccount: boolean;
86
+ };
87
+ /** The auth authorities the keys below were issued by (live, by every name it answers to). */
88
+ declare const PROJECT_SIGNING_KEY_AUTHORITIES: readonly string[];
89
+ declare const PROJECT_SIGNING_KEYS: JWK[];
90
+ /** The pinned keys for the authority at `supabaseUrl`; none for any other authority. */
91
+ declare function pinnedSigningKeysFor(supabaseUrl: string | null | undefined): JWK[];
92
+ interface AuthErrorLike {
93
+ name?: string | undefined;
94
+ code?: string | undefined;
95
+ status?: number | undefined;
96
+ message?: string | undefined;
97
+ }
98
+ /** Is this PostgREST result the database saying "I cannot verify this token"? (Not expiry.) */
99
+ declare function isUnverifiableTokenRefusal(result: {
100
+ error?: {
101
+ code?: string;
102
+ message?: string;
103
+ } | null;
104
+ status?: number;
105
+ }): boolean;
106
+ /** Is this `getClaims()` error the authority's SETTLED verdict that the token is not its own? */
107
+ declare function isUnverifiableTokenError(error: AuthErrorLike | null | undefined): boolean;
108
+ /** Ask the authority once: is the stored session's token one it can verify? */
109
+ declare function confirmUnverifiableSession(auth: {
110
+ getClaims(jwt?: string, options?: {
111
+ jwks?: {
112
+ keys: JWK[];
113
+ };
114
+ }): Promise<{
115
+ data: unknown;
116
+ error: AuthErrorLike | null;
117
+ }>;
118
+ }, supabaseUrl: string | null | undefined): Promise<boolean>;
119
+ interface ClaimsCacheOptions {
120
+ /** The URL the client talks to — decides which pinned keys apply. */
121
+ supabaseUrl: string | null | undefined;
122
+ /** How long one verdict is shared (default 2s; any identity-changing event clears it at once). */
123
+ ttlMs?: number;
124
+ /** Called when the authority settled that the browser's token is not one of its own. */
125
+ onUnverifiable?: () => void;
126
+ }
127
+ /**
128
+ * The current browser session's verified claims, de-duplicated: callers within `ttlMs` share one
129
+ * verification (and so one JWKS fetch on a cold cache). Browser-only: a server request must never
130
+ * share another request's verdict — servers call `getClaimsUser` / `createServerAuth`.
131
+ */
132
+ declare function createClaimsCache(client: ClaimsCapableClient & {
133
+ auth: {
134
+ onAuthStateChange(cb: (event: string) => void): unknown;
135
+ };
136
+ }, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
137
+
138
+ export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsResult as b, PROJECT_SIGNING_KEY_AUTHORITIES as c, accountFlagsFor as d, confirmUnverifiableSession as e, createClaimsCache as f, getClaimsUser as g, isSignedOutError as h, isAuthTransportFailure as i, isUnverifiableTokenError as j, isUnverifiableTokenRefusal as k, pinnedSigningKeysFor as p, userFromClaims as u };
@@ -0,0 +1,138 @@
1
+ import { JwtPayload, UserAppMetadata, UserMetadata, JWK, AuthError } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * THE identity read every server and client path shares (moved from the website's
5
+ * `utils/supabase/{claimsUser,projectSigningKeys,unverifiableSession}.ts`, 2026-10-12).
6
+ *
7
+ * Verifies the access token LOCALLY (WebCrypto against the project's JWKS; the live project signs
8
+ * ES256) and never asks the auth server who the caller is. `getClaims()`, NEVER `getUser()`:
9
+ * `getUser()` is one auth-server round trip per call, with auth-js retrying a stalled refresh for
10
+ * up to 30s — on 2026-09-20 and 09-21 that put 504s in front of real people.
11
+ *
12
+ * Framework-free: no `next/*`, no `window` at import time. Server doors (`createServerAuth` in
13
+ * `/next`, `verifyRequest` in `/node`) and the browser's de-duplicated read (`createClaimsCache`)
14
+ * are thin wrappers over `getClaimsUser`.
15
+ */
16
+
17
+ /**
18
+ * The verified caller, built from the access token's claims. `id` is `sub`; `app_metadata` /
19
+ * `user_metadata` default to `{}` so a reader never null-checks what `getUser()` always handed it.
20
+ */
21
+ interface ApiClaimsUser extends JwtPayload {
22
+ /** The `sub` claim — the user id. */
23
+ id: string;
24
+ app_metadata: UserAppMetadata;
25
+ user_metadata: UserMetadata;
26
+ }
27
+ /** Any Supabase client — the only thing this door needs is `auth.getClaims`. */
28
+ type ClaimsCapableClient = {
29
+ auth: {
30
+ getClaims(jwt?: string, options?: {
31
+ jwks?: {
32
+ keys: JWK[];
33
+ };
34
+ }): Promise<{
35
+ data: {
36
+ claims?: JwtPayload;
37
+ } | null;
38
+ error: AuthError | null;
39
+ }>;
40
+ };
41
+ };
42
+ interface ClaimsResult {
43
+ data: {
44
+ user: ApiClaimsUser | null;
45
+ };
46
+ error: AuthError | null;
47
+ }
48
+ /**
49
+ * `client.auth.getUser()` without the auth-server round trip, in the SAME envelope, with one
50
+ * deliberate distinction:
51
+ * - CONFIRMED SIGNED OUT (no session, no token) → `{ user: null, error: null }` — a settled fact.
52
+ * - COULD NOT VERIFY (authority/JWKS unreachable, malformed or badly-signed token, a verified
53
+ * token without `sub`) → `error` kept, so a retry branch fires and a screen never paints
54
+ * "signed out" over an outage.
55
+ * Pass `jwt` when the token is in hand (a Bearer header). `options.jwks` are known public keys
56
+ * tried BEFORE any JWKS fetch (`pinnedSigningKeysFor`) — can only remove a network call.
57
+ */
58
+ declare function getClaimsUser(client: ClaimsCapableClient, jwt?: string, options?: {
59
+ jwks: {
60
+ keys: JWK[];
61
+ };
62
+ }): Promise<ClaimsResult>;
63
+ /**
64
+ * Could we not REACH a verdict (transport failure, spent budget), as opposed to a token we reached
65
+ * a verdict on? Too narrow reads an outage as a logout; too wide holds a person with a forged or
66
+ * rotated-key token on "reload in a moment" forever while the proxy sends them to /login. This is
67
+ * the same line the `/next` middleware pass draws.
68
+ */
69
+ declare function isAuthTransportFailure(error: unknown): boolean;
70
+ /** Is this `getClaims()` error the settled fact "nobody is signed in"? */
71
+ declare function isSignedOutError(error: AuthError | {
72
+ name?: string;
73
+ code?: string;
74
+ }): boolean;
75
+ /** The verified claims as a `{ id, ... }` user, or `null` when there are none. */
76
+ declare function userFromClaims(claims: JwtPayload | undefined): ApiClaimsUser | null;
77
+ /**
78
+ * GUEST vs ACCOUNT. A signed-in session is a person with an ACCOUNT or a GUEST (anonymous session,
79
+ * `is_anonymous: true`). Account UI (menu, billing, sign-out, org picker) keys on `hasAccount`.
80
+ */
81
+ declare function accountFlagsFor(claims: {
82
+ is_anonymous?: boolean;
83
+ } | null | undefined): {
84
+ isGuest: boolean;
85
+ hasAccount: boolean;
86
+ };
87
+ /** The auth authorities the keys below were issued by (live, by every name it answers to). */
88
+ declare const PROJECT_SIGNING_KEY_AUTHORITIES: readonly string[];
89
+ declare const PROJECT_SIGNING_KEYS: JWK[];
90
+ /** The pinned keys for the authority at `supabaseUrl`; none for any other authority. */
91
+ declare function pinnedSigningKeysFor(supabaseUrl: string | null | undefined): JWK[];
92
+ interface AuthErrorLike {
93
+ name?: string | undefined;
94
+ code?: string | undefined;
95
+ status?: number | undefined;
96
+ message?: string | undefined;
97
+ }
98
+ /** Is this PostgREST result the database saying "I cannot verify this token"? (Not expiry.) */
99
+ declare function isUnverifiableTokenRefusal(result: {
100
+ error?: {
101
+ code?: string;
102
+ message?: string;
103
+ } | null;
104
+ status?: number;
105
+ }): boolean;
106
+ /** Is this `getClaims()` error the authority's SETTLED verdict that the token is not its own? */
107
+ declare function isUnverifiableTokenError(error: AuthErrorLike | null | undefined): boolean;
108
+ /** Ask the authority once: is the stored session's token one it can verify? */
109
+ declare function confirmUnverifiableSession(auth: {
110
+ getClaims(jwt?: string, options?: {
111
+ jwks?: {
112
+ keys: JWK[];
113
+ };
114
+ }): Promise<{
115
+ data: unknown;
116
+ error: AuthErrorLike | null;
117
+ }>;
118
+ }, supabaseUrl: string | null | undefined): Promise<boolean>;
119
+ interface ClaimsCacheOptions {
120
+ /** The URL the client talks to — decides which pinned keys apply. */
121
+ supabaseUrl: string | null | undefined;
122
+ /** How long one verdict is shared (default 2s; any identity-changing event clears it at once). */
123
+ ttlMs?: number;
124
+ /** Called when the authority settled that the browser's token is not one of its own. */
125
+ onUnverifiable?: () => void;
126
+ }
127
+ /**
128
+ * The current browser session's verified claims, de-duplicated: callers within `ttlMs` share one
129
+ * verification (and so one JWKS fetch on a cold cache). Browser-only: a server request must never
130
+ * share another request's verdict — servers call `getClaimsUser` / `createServerAuth`.
131
+ */
132
+ declare function createClaimsCache(client: ClaimsCapableClient & {
133
+ auth: {
134
+ onAuthStateChange(cb: (event: string) => void): unknown;
135
+ };
136
+ }, options: ClaimsCacheOptions): () => Promise<ClaimsResult>;
137
+
138
+ export { type ApiClaimsUser as A, type ClaimsCacheOptions as C, PROJECT_SIGNING_KEYS as P, type ClaimsCapableClient as a, type ClaimsResult as b, PROJECT_SIGNING_KEY_AUTHORITIES as c, accountFlagsFor as d, confirmUnverifiableSession as e, createClaimsCache as f, getClaimsUser as g, isSignedOutError as h, isAuthTransportFailure as i, isUnverifiableTokenError as j, isUnverifiableTokenRefusal as k, pinnedSigningKeysFor as p, userFromClaims as u };
package/dist/index.cjs CHANGED
@@ -20,25 +20,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ PROJECT_SIGNING_KEYS: () => PROJECT_SIGNING_KEYS,
24
+ PROJECT_SIGNING_KEY_AUTHORITIES: () => PROJECT_SIGNING_KEY_AUTHORITIES,
23
25
  SecretKeyRefusedError: () => SecretKeyRefusedError,
24
26
  SignInError: () => SignInError,
25
27
  StorageUnavailableError: () => StorageUnavailableError,
28
+ accountFlagsFor: () => accountFlagsFor,
26
29
  browserStorage: () => browserStorage,
30
+ confirmUnverifiableSession: () => confirmUnverifiableSession,
27
31
  createAuthClient: () => createAuthClient,
32
+ createClaimsCache: () => createClaimsCache,
28
33
  extensionStorage: () => extensionStorage,
34
+ getClaimsUser: () => getClaimsUser,
29
35
  getPerson: () => getPerson,
30
36
  getSession: () => getSession,
31
37
  invalidatePerson: () => invalidatePerson,
38
+ isAuthTransportFailure: () => isAuthTransportFailure,
39
+ isSignedOutError: () => isSignedOutError,
40
+ isUnverifiableTokenError: () => isUnverifiableTokenError,
41
+ isUnverifiableTokenRefusal: () => isUnverifiableTokenRefusal,
32
42
  keyValueStorage: () => keyValueStorage,
33
43
  memoryStorage: () => memoryStorage,
34
44
  onSessionChange: () => onSessionChange,
45
+ pinnedSigningKeysFor: () => pinnedSigningKeysFor,
35
46
  refreshSession: () => refreshSession,
36
47
  sessionCredentials: () => sessionCredentials,
37
48
  signInWithOAuth: () => signInWithOAuth,
38
49
  signInWithPassword: () => signInWithPassword,
39
50
  signOut: () => signOut,
40
51
  updatePerson: () => updatePerson,
41
- userCredentials: () => userCredentials
52
+ userCredentials: () => userCredentials,
53
+ userFromClaims: () => userFromClaims
42
54
  });
43
55
  module.exports = __toCommonJS(src_exports);
44
56
 
@@ -224,3 +236,125 @@ function createAuthClient(options) {
224
236
  if (options.detectSessionInUrl !== void 0) auth["detectSessionInUrl"] = options.detectSessionInUrl;
225
237
  return (0, import_supabase_js.createClient)(options.url, options.key, { auth });
226
238
  }
239
+
240
+ // src/claims.ts
241
+ var import_supabase_js2 = require("@supabase/supabase-js");
242
+ async function getClaimsUser(client, jwt, options) {
243
+ const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
244
+ if (error) {
245
+ if (isSignedOutError(error)) return { data: { user: null }, error: null };
246
+ return { data: { user: null }, error };
247
+ }
248
+ if (!data?.claims) return { data: { user: null }, error: null };
249
+ const user = userFromClaims(data.claims);
250
+ if (!user) {
251
+ return {
252
+ data: { user: null },
253
+ error: new import_supabase_js2.AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
254
+ };
255
+ }
256
+ return { data: { user }, error: null };
257
+ }
258
+ var BUDGET_EXHAUSTED_STATUS = 408;
259
+ var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
260
+ function isAuthTransportFailure(error) {
261
+ if (!error || typeof error !== "object") return false;
262
+ const name = String(error.name ?? "");
263
+ if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
264
+ const status = error.status;
265
+ if (status === 0 || status === 504 || status === 503) return true;
266
+ return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
267
+ }
268
+ function isSignedOutError(error) {
269
+ return error instanceof import_supabase_js2.AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
270
+ }
271
+ function userFromClaims(claims) {
272
+ if (!claims || typeof claims.sub !== "string") return null;
273
+ return {
274
+ ...claims,
275
+ id: claims.sub,
276
+ app_metadata: claims.app_metadata ?? {},
277
+ user_metadata: claims.user_metadata ?? {}
278
+ };
279
+ }
280
+ function accountFlagsFor(claims) {
281
+ if (!claims) return { isGuest: false, hasAccount: false };
282
+ const isGuest = claims.is_anonymous === true;
283
+ return { isGuest, hasAccount: !isGuest };
284
+ }
285
+ var PROJECT_SIGNING_KEY_AUTHORITIES = [
286
+ "db.matrxserver.com",
287
+ "brsgrqvjdzwihsvnfqkf.supabase.co"
288
+ ];
289
+ var PROJECT_SIGNING_KEYS = [
290
+ {
291
+ alg: "ES256",
292
+ crv: "P-256",
293
+ ext: true,
294
+ key_ops: ["verify"],
295
+ kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
296
+ kty: "EC",
297
+ use: "sig",
298
+ x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
299
+ y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
300
+ }
301
+ ];
302
+ function pinnedSigningKeysFor(supabaseUrl) {
303
+ if (!supabaseUrl) return [];
304
+ let host;
305
+ try {
306
+ host = new URL(supabaseUrl).host.toLowerCase();
307
+ } catch {
308
+ return [];
309
+ }
310
+ return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
311
+ }
312
+ function isUnverifiableTokenRefusal(result) {
313
+ if (result.status !== 401) return false;
314
+ if (result.error?.code !== "PGRST301") return false;
315
+ return !/expired/i.test(result.error?.message ?? "");
316
+ }
317
+ function isUnverifiableTokenError(error) {
318
+ if (!error) return false;
319
+ const name = String(error.name ?? "");
320
+ if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return false;
321
+ if (name === "AuthSessionMissingError" || error.code === "session_not_found") return false;
322
+ const status = error.status;
323
+ if (status === 0 || status === 408 || status === 503 || status === 504) return false;
324
+ if (/expired/i.test(error.message ?? "")) return false;
325
+ return error.code === "bad_jwt" || name === "AuthInvalidJwtError";
326
+ }
327
+ async function confirmUnverifiableSession(auth, supabaseUrl) {
328
+ try {
329
+ const { error } = await auth.getClaims(void 0, { jwks: { keys: pinnedSigningKeysFor(supabaseUrl) } });
330
+ return isUnverifiableTokenError(error);
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+ function createClaimsCache(client, options) {
336
+ const ttl = options.ttlMs ?? 2e3;
337
+ let cached = null;
338
+ let inflight = null;
339
+ let listening = false;
340
+ return () => {
341
+ if (!listening) {
342
+ listening = true;
343
+ client.auth.onAuthStateChange((event) => {
344
+ if (event === "SIGNED_OUT" || event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
345
+ cached = null;
346
+ }
347
+ });
348
+ }
349
+ if (cached && cached.expiresAt > Date.now()) return Promise.resolve(cached.value);
350
+ if (inflight) return inflight;
351
+ inflight = getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } }).then((result) => {
352
+ if (isUnverifiableTokenError(result.error)) options.onUnverifiable?.();
353
+ cached = { value: result, expiresAt: Date.now() + ttl };
354
+ return result;
355
+ }).finally(() => {
356
+ inflight = null;
357
+ });
358
+ return inflight;
359
+ };
360
+ }
package/dist/index.d.cts CHANGED
@@ -1,57 +1,10 @@
1
1
  export { A as AuthClient, S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './session-DdQiStp6.cjs';
2
+ import { S as SessionStorage } from './storage-Dqnye4KF.cjs';
3
+ export { C as ChromeStorageAreaLike, a as StorageUnavailableError, b as browserStorage, e as extensionStorage, k as keyValueStorage, m as memoryStorage } from './storage-Dqnye4KF.cjs';
2
4
  import { SupabaseClient } from '@supabase/supabase-js';
3
5
  export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
4
6
  export { CredentialsPort, MatrxCredential, MatrxCredentialKind } from './credentials.cjs';
5
-
6
- /**
7
- * Session storage per platform — where a Supabase session lives between runs.
8
- *
9
- * supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
10
- * every Matrx client needs, written once:
11
- *
12
- * browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
13
- * Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
14
- * extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
15
- * service worker; `localStorage` does not exist in a service worker)
16
- * desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
17
- * Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
18
- *
19
- * None of these is ever silently swapped for another: a storage the platform does not have throws
20
- * a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
21
- */
22
- /** The storage contract supabase-js reads (sync or async methods). */
23
- interface SessionStorage {
24
- getItem(key: string): string | null | Promise<string | null>;
25
- setItem(key: string, value: string): void | Promise<void>;
26
- removeItem(key: string): void | Promise<void>;
27
- }
28
- declare class StorageUnavailableError extends Error {
29
- readonly storage: string;
30
- constructor(storage: string, reason: string);
31
- }
32
- /** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
33
- declare function memoryStorage(): SessionStorage & {
34
- readonly size: () => number;
35
- };
36
- /** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
37
- declare function browserStorage(kind?: "local" | "session"): SessionStorage;
38
- /** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
39
- interface ChromeStorageAreaLike {
40
- get(keys: string): Promise<Record<string, unknown>>;
41
- set(items: Record<string, unknown>): Promise<void>;
42
- remove(keys: string): Promise<void>;
43
- }
44
- /**
45
- * A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
46
- * panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
47
- */
48
- declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
49
- /** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
50
- declare function keyValueStorage(store: {
51
- get(key: string): Promise<string | null | undefined>;
52
- set(key: string, value: string): Promise<void>;
53
- remove(key: string): Promise<void>;
54
- }): SessionStorage;
7
+ export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsResult, P as PROJECT_SIGNING_KEYS, c as PROJECT_SIGNING_KEY_AUTHORITIES, d as accountFlagsFor, e as confirmUnverifiableSession, f as createClaimsCache, g as getClaimsUser, i as isAuthTransportFailure, h as isSignedOutError, j as isUnverifiableTokenError, k as isUnverifiableTokenRefusal, p as pinnedSigningKeysFor, u as userFromClaims } from './claims-DXdbe3cW.cjs';
55
8
 
56
9
  /**
57
10
  * `createAuthClient` — the one Supabase client factory OUTSIDE Next.js (Next apps use
@@ -83,4 +36,4 @@ declare class SecretKeyRefusedError extends Error {
83
36
  }
84
37
  declare function createAuthClient<Database = any>(options: CreateAuthClientOptions): SupabaseClient<Database>;
85
38
 
86
- export { type ChromeStorageAreaLike, type CreateAuthClientOptions, SecretKeyRefusedError, type SessionStorage, StorageUnavailableError, browserStorage, createAuthClient, extensionStorage, keyValueStorage, memoryStorage };
39
+ export { type CreateAuthClientOptions, SecretKeyRefusedError, SessionStorage, createAuthClient };
package/dist/index.d.ts CHANGED
@@ -1,57 +1,10 @@
1
1
  export { A as AuthClient, S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, i as invalidatePerson, o as onSessionChange, r as refreshSession, s as sessionCredentials, c as signInWithOAuth, d as signInWithPassword, e as signOut, u as updatePerson, f as userCredentials } from './session-DdQiStp6.js';
2
+ import { S as SessionStorage } from './storage-Dqnye4KF.js';
3
+ export { C as ChromeStorageAreaLike, a as StorageUnavailableError, b as browserStorage, e as extensionStorage, k as keyValueStorage, m as memoryStorage } from './storage-Dqnye4KF.js';
2
4
  import { SupabaseClient } from '@supabase/supabase-js';
3
5
  export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
4
6
  export { CredentialsPort, MatrxCredential, MatrxCredentialKind } from './credentials.js';
5
-
6
- /**
7
- * Session storage per platform — where a Supabase session lives between runs.
8
- *
9
- * supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
10
- * every Matrx client needs, written once:
11
- *
12
- * browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
13
- * Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
14
- * extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
15
- * service worker; `localStorage` does not exist in a service worker)
16
- * desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
17
- * Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
18
- *
19
- * None of these is ever silently swapped for another: a storage the platform does not have throws
20
- * a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
21
- */
22
- /** The storage contract supabase-js reads (sync or async methods). */
23
- interface SessionStorage {
24
- getItem(key: string): string | null | Promise<string | null>;
25
- setItem(key: string, value: string): void | Promise<void>;
26
- removeItem(key: string): void | Promise<void>;
27
- }
28
- declare class StorageUnavailableError extends Error {
29
- readonly storage: string;
30
- constructor(storage: string, reason: string);
31
- }
32
- /** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
33
- declare function memoryStorage(): SessionStorage & {
34
- readonly size: () => number;
35
- };
36
- /** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
37
- declare function browserStorage(kind?: "local" | "session"): SessionStorage;
38
- /** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
39
- interface ChromeStorageAreaLike {
40
- get(keys: string): Promise<Record<string, unknown>>;
41
- set(items: Record<string, unknown>): Promise<void>;
42
- remove(keys: string): Promise<void>;
43
- }
44
- /**
45
- * A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
46
- * panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
47
- */
48
- declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
49
- /** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
50
- declare function keyValueStorage(store: {
51
- get(key: string): Promise<string | null | undefined>;
52
- set(key: string, value: string): Promise<void>;
53
- remove(key: string): Promise<void>;
54
- }): SessionStorage;
7
+ export { A as ApiClaimsUser, C as ClaimsCacheOptions, a as ClaimsCapableClient, b as ClaimsResult, P as PROJECT_SIGNING_KEYS, c as PROJECT_SIGNING_KEY_AUTHORITIES, d as accountFlagsFor, e as confirmUnverifiableSession, f as createClaimsCache, g as getClaimsUser, i as isAuthTransportFailure, h as isSignedOutError, j as isUnverifiableTokenError, k as isUnverifiableTokenRefusal, p as pinnedSigningKeysFor, u as userFromClaims } from './claims-DXdbe3cW.js';
55
8
 
56
9
  /**
57
10
  * `createAuthClient` — the one Supabase client factory OUTSIDE Next.js (Next apps use
@@ -83,4 +36,4 @@ declare class SecretKeyRefusedError extends Error {
83
36
  }
84
37
  declare function createAuthClient<Database = any>(options: CreateAuthClientOptions): SupabaseClient<Database>;
85
38
 
86
- export { type ChromeStorageAreaLike, type CreateAuthClientOptions, SecretKeyRefusedError, type SessionStorage, StorageUnavailableError, browserStorage, createAuthClient, extensionStorage, keyValueStorage, memoryStorage };
39
+ export { type CreateAuthClientOptions, SecretKeyRefusedError, SessionStorage, createAuthClient };