@ai-matrx/auth 0.2.1 → 0.2.2

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/dist/index.js CHANGED
@@ -1,3 +1,370 @@
1
+ // src/claims.ts
2
+ import {
3
+ AuthError,
4
+ AuthSessionMissingError
5
+ } from "@supabase/supabase-js";
6
+ async function getClaimsUser(client, jwt, options) {
7
+ const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
8
+ if (error) {
9
+ if (isSignedOutError(error)) return { data: { user: null }, error: null };
10
+ return { data: { user: null }, error };
11
+ }
12
+ if (!data?.claims) return { data: { user: null }, error: null };
13
+ const user = userFromClaims(data.claims);
14
+ if (!user) {
15
+ return {
16
+ data: { user: null },
17
+ error: new AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
18
+ };
19
+ }
20
+ return { data: { user }, error: null };
21
+ }
22
+ var BUDGET_EXHAUSTED_STATUS = 408;
23
+ var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
24
+ function isAuthTransportFailure(error) {
25
+ if (!error || typeof error !== "object") return false;
26
+ const name = String(error.name ?? "");
27
+ if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
28
+ const status = error.status;
29
+ if (status === 0 || status === 504 || status === 503) return true;
30
+ return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
31
+ }
32
+ function isSignedOutError(error) {
33
+ return error instanceof AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
34
+ }
35
+ function userFromClaims(claims) {
36
+ if (!claims || typeof claims.sub !== "string") return null;
37
+ return {
38
+ ...claims,
39
+ id: claims.sub,
40
+ app_metadata: claims.app_metadata ?? {},
41
+ user_metadata: claims.user_metadata ?? {}
42
+ };
43
+ }
44
+ function accountFlagsFor(claims) {
45
+ if (!claims) return { isGuest: false, hasAccount: false };
46
+ const isGuest = claims.is_anonymous === true;
47
+ return { isGuest, hasAccount: !isGuest };
48
+ }
49
+ var PROJECT_SIGNING_KEY_AUTHORITIES = [
50
+ "db.matrxserver.com",
51
+ "brsgrqvjdzwihsvnfqkf.supabase.co"
52
+ ];
53
+ var PROJECT_SIGNING_KEYS = [
54
+ {
55
+ alg: "ES256",
56
+ crv: "P-256",
57
+ ext: true,
58
+ key_ops: ["verify"],
59
+ kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
60
+ kty: "EC",
61
+ use: "sig",
62
+ x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
63
+ y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
64
+ }
65
+ ];
66
+ function pinnedSigningKeysFor(supabaseUrl) {
67
+ if (!supabaseUrl) return [];
68
+ let host;
69
+ try {
70
+ host = new URL(supabaseUrl).host.toLowerCase();
71
+ } catch {
72
+ return [];
73
+ }
74
+ return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
75
+ }
76
+ function isUnverifiableTokenRefusal(result) {
77
+ if (result.status !== 401) return false;
78
+ if (result.error?.code !== "PGRST301") return false;
79
+ return !/expired/i.test(result.error?.message ?? "");
80
+ }
81
+ function isUnverifiableTokenError(error) {
82
+ if (!error) return false;
83
+ const name = String(error.name ?? "");
84
+ if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return false;
85
+ if (name === "AuthSessionMissingError" || error.code === "session_not_found") return false;
86
+ const status = error.status;
87
+ if (status === 0 || status === 408 || status === 503 || status === 504) return false;
88
+ if (/expired/i.test(error.message ?? "")) return false;
89
+ return error.code === "bad_jwt" || name === "AuthInvalidJwtError";
90
+ }
91
+ async function confirmUnverifiableSession(auth, supabaseUrl) {
92
+ try {
93
+ const { error } = await auth.getClaims(void 0, { jwks: { keys: pinnedSigningKeysFor(supabaseUrl) } });
94
+ return isUnverifiableTokenError(error);
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ function createClaimsCache(client, options) {
100
+ const ttl = options.ttlMs ?? 2e3;
101
+ let cached = null;
102
+ let inflight = null;
103
+ let listening = false;
104
+ return () => {
105
+ if (!listening) {
106
+ listening = true;
107
+ client.auth.onAuthStateChange((event) => {
108
+ if (event === "SIGNED_OUT" || event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
109
+ cached = null;
110
+ }
111
+ });
112
+ }
113
+ if (cached && cached.expiresAt > Date.now()) return Promise.resolve(cached.value);
114
+ if (inflight) return inflight;
115
+ inflight = getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } }).then((result) => {
116
+ if (isUnverifiableTokenError(result.error)) options.onUnverifiable?.();
117
+ cached = { value: result, expiresAt: Date.now() + ttl };
118
+ return result;
119
+ }).finally(() => {
120
+ inflight = null;
121
+ });
122
+ return inflight;
123
+ };
124
+ }
125
+
126
+ // src/session-barrier.ts
127
+ var SESSION_ATTACH_BUDGET_MS = 3e3;
128
+ var SESSION_RECOVERY_BUDGET_MS = 5e3;
129
+ var WAIT_COOLDOWN_MS = 3e4;
130
+ function isSessionRefusal(result) {
131
+ const code = result.error?.code;
132
+ if (!code) return false;
133
+ if (result.status !== 401) return false;
134
+ return code === "42501" || code === "PGRST301" || code === "28000";
135
+ }
136
+ function isAuthLike(value) {
137
+ if (!value || typeof value !== "object") return false;
138
+ const candidate = value;
139
+ return typeof candidate.getSession === "function" && typeof candidate.onAuthStateChange === "function";
140
+ }
141
+ function readStorageKey(auth) {
142
+ if (!auth || typeof auth !== "object" || !("storageKey" in auth)) return null;
143
+ const key = auth.storageKey;
144
+ return typeof key === "string" && key.length > 0 ? key : null;
145
+ }
146
+ function defaultReadCookie() {
147
+ if (typeof document === "undefined") return null;
148
+ try {
149
+ return document.cookie;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ function createSessionBarrier(options) {
155
+ const attachBudget = options.attachBudgetMs ?? SESSION_ATTACH_BUDGET_MS;
156
+ const recoveryBudget = options.recoveryBudgetMs ?? SESSION_RECOVERY_BUDGET_MS;
157
+ const cooldown = options.waitCooldownMs ?? WAIT_COOLDOWN_MS;
158
+ const readCookie = options.readCookie ?? defaultReadCookie;
159
+ const isBrowser = options.isBrowser ?? (() => typeof window !== "undefined");
160
+ const now = options.now ?? (() => Date.now());
161
+ const warn = options.warn ?? ((m) => console.warn(m));
162
+ let boundAuth = null;
163
+ let storageKey = null;
164
+ let sessionPresent = null;
165
+ let waitSuppressedUntil = 0;
166
+ const waiting = /* @__PURE__ */ new Set();
167
+ const doorName = (ctx) => ctx.relation ? `${ctx.schema ?? "public"}.${ctx.relation}` : "(unknown)";
168
+ function drain() {
169
+ if (waiting.size === 0) return;
170
+ const pending = [...waiting];
171
+ waiting.clear();
172
+ for (const listener of pending) {
173
+ try {
174
+ listener();
175
+ } catch {
176
+ }
177
+ }
178
+ }
179
+ function markSession(present) {
180
+ const had = sessionPresent;
181
+ sessionPresent = present;
182
+ if (present === true && had !== true) drain();
183
+ }
184
+ function authCookiePresent() {
185
+ if (!storageKey) return null;
186
+ const cookie = readCookie();
187
+ if (cookie === null) return null;
188
+ if (!cookie) return false;
189
+ for (const pair of cookie.split(";")) {
190
+ const name = pair.slice(0, pair.indexOf("=")).trim() || pair.trim();
191
+ if (name === storageKey) return true;
192
+ if (name.startsWith(`${storageKey}.`)) {
193
+ const chunk = name.slice(storageKey.length + 1);
194
+ if (/^(0|[1-9][0-9]*)$/.test(chunk)) return true;
195
+ }
196
+ }
197
+ return false;
198
+ }
199
+ function sessionStateNow() {
200
+ if (sessionPresent === true) return "attached";
201
+ const cookie = authCookiePresent();
202
+ if (cookie === true) return "expected";
203
+ if (cookie === false && sessionPresent === false) return "absent";
204
+ return "unknown";
205
+ }
206
+ function sessionStateMarker() {
207
+ switch (sessionStateNow()) {
208
+ case "attached":
209
+ return "attached";
210
+ case "expected":
211
+ return "pre_attach";
212
+ case "absent":
213
+ return "signed_out";
214
+ default:
215
+ return "unknown";
216
+ }
217
+ }
218
+ function whenSessionAttached(listener) {
219
+ if (sessionPresent === true) {
220
+ try {
221
+ listener();
222
+ } catch {
223
+ }
224
+ return () => {
225
+ };
226
+ }
227
+ waiting.add(listener);
228
+ return () => {
229
+ waiting.delete(listener);
230
+ };
231
+ }
232
+ function waitForAttach(budgetMs) {
233
+ if (sessionPresent === true) return Promise.resolve(true);
234
+ return new Promise((resolve) => {
235
+ let settled = false;
236
+ const done = (attached) => {
237
+ if (settled) return;
238
+ settled = true;
239
+ clearTimeout(timer);
240
+ unsubscribe();
241
+ resolve(attached);
242
+ };
243
+ const unsubscribe = whenSessionAttached(() => done(true));
244
+ const timer = setTimeout(() => done(false), budgetMs);
245
+ });
246
+ }
247
+ async function probeSession() {
248
+ if (!boundAuth) return false;
249
+ try {
250
+ const { data, error } = await boundAuth.getSession();
251
+ const token = Boolean(data?.session?.access_token) && !error;
252
+ if (token) markSession(true);
253
+ return token;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ function announceRecovered(ctx) {
259
+ const door = doorName(ctx);
260
+ warn(
261
+ `[sessionBarrier] ${door} was refused because the request carried no session (42501/401). The session re-resolved and the retry was served. This firing means a real session-availability bug got past the proactive wait \u2014 it is not routine.`
262
+ );
263
+ options.report({
264
+ code: "SESSION_BARRIER_RECOVERED",
265
+ schema: ctx.schema,
266
+ relation: ctx.relation,
267
+ message: `${door} was refused for having no session, the session re-resolved, and the read was retried once and served. Nothing was lost; the race that caused it is DD-237.`,
268
+ status: 401,
269
+ sessionState: "attached",
270
+ durable: false
271
+ });
272
+ }
273
+ function announceUnrecovered(ctx, why) {
274
+ const door = doorName(ctx);
275
+ options.report({
276
+ code: "SESSION_BARRIER_UNRECOVERED",
277
+ schema: ctx.schema,
278
+ relation: ctx.relation,
279
+ message: why === "refused_again" ? `${door} was refused for having no session, the session re-resolved, and the retry was refused again. The read was NOT served and its data is missing \u2014 sign in again to repair it.` : `${door} was refused for having no session and the session did not come back within ${recoveryBudget} ms. The auth cookie is still in this browser, so the session is lapsed rather than absent \u2014 sign in again to repair it. The read was NOT retried and its data is missing.`,
280
+ status: 401,
281
+ sessionState: sessionStateMarker(),
282
+ durable: true
283
+ });
284
+ }
285
+ return {
286
+ install(client) {
287
+ if (!isBrowser()) return;
288
+ if (boundAuth) return;
289
+ if (!client || typeof client !== "object") return;
290
+ const auth = client.auth;
291
+ if (!isAuthLike(auth)) return;
292
+ boundAuth = auth;
293
+ storageKey = readStorageKey(auth);
294
+ options.setSessionStateProbe?.(() => sessionStateMarker());
295
+ try {
296
+ auth.onAuthStateChange((_event, session) => {
297
+ markSession(Boolean(session));
298
+ });
299
+ } catch {
300
+ }
301
+ },
302
+ reset() {
303
+ options.setSessionStateProbe?.(null);
304
+ waiting.clear();
305
+ boundAuth = null;
306
+ storageKey = null;
307
+ sessionPresent = null;
308
+ waitSuppressedUntil = 0;
309
+ },
310
+ announceSession: markSession,
311
+ authCookiePresent,
312
+ sessionStateNow,
313
+ sessionStateMarker,
314
+ hasAttachedSession: () => sessionPresent === true,
315
+ whenSessionAttached,
316
+ canSendImmediately(ctx) {
317
+ if (!isBrowser()) return true;
318
+ if (sessionPresent === true) return true;
319
+ if (options.bypassesSessionWait?.(ctx.relation)) return true;
320
+ if (now() < waitSuppressedUntil) return true;
321
+ return authCookiePresent() !== true;
322
+ },
323
+ async awaitSessionBeforeSend() {
324
+ if (await probeSession()) return;
325
+ if (await waitForAttach(attachBudget)) return;
326
+ waitSuppressedUntil = now() + cooldown;
327
+ },
328
+ isSessionRefusal,
329
+ shouldRecoverSession(ctx) {
330
+ if (!isBrowser()) return false;
331
+ if (options.isAnonymousByDesign?.(ctx.schema, ctx.relation)) return false;
332
+ return authCookiePresent() === true;
333
+ },
334
+ async endSessionIfUnverifiable(ctx, result) {
335
+ if (!isUnverifiableTokenRefusal(result)) return false;
336
+ const auth = boundAuth;
337
+ if (!auth?.getClaims || !auth.signOut || !options.endUnverifiable) return false;
338
+ if (!await confirmUnverifiableSession({ getClaims: auth.getClaims.bind(auth) }, options.supabaseUrl)) {
339
+ return false;
340
+ }
341
+ markSession(false);
342
+ await options.endUnverifiable(
343
+ { signOut: auth.signOut.bind(auth) },
344
+ { door: doorName(ctx), via: "postgrest" }
345
+ );
346
+ return true;
347
+ },
348
+ async recoverSessionForRetry(ctx) {
349
+ if (sessionPresent === true) markSession(false);
350
+ let recovered = await probeSession();
351
+ if (!recovered && now() >= waitSuppressedUntil) {
352
+ recovered = await waitForAttach(recoveryBudget);
353
+ if (!recovered) waitSuppressedUntil = now() + cooldown;
354
+ }
355
+ if (!recovered) announceUnrecovered(ctx, "no_session");
356
+ return recovered;
357
+ },
358
+ reportSessionRetryOutcome(ctx, retried) {
359
+ if (isSessionRefusal(retried)) {
360
+ announceUnrecovered(ctx, "refused_again");
361
+ return;
362
+ }
363
+ announceRecovered(ctx);
364
+ }
365
+ };
366
+ }
367
+
1
368
  // src/session.ts
2
369
  var SignInError = class extends Error {
3
370
  door;
@@ -181,152 +548,186 @@ function createAuthClient(options) {
181
548
  return createClient(options.url, options.key, { auth });
182
549
  }
183
550
 
184
- // src/claims.ts
185
- import {
186
- AuthError,
187
- AuthSessionMissingError
188
- } from "@supabase/supabase-js";
189
- async function getClaimsUser(client, jwt, options) {
190
- const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
191
- if (error) {
192
- if (isSignedOutError(error)) return { data: { user: null }, error: null };
193
- return { data: { user: null }, error };
194
- }
195
- if (!data?.claims) return { data: { user: null }, error: null };
196
- const user = userFromClaims(data.claims);
197
- if (!user) {
198
- return {
199
- data: { user: null },
200
- error: new AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
201
- };
551
+ // src/matrx-oauth.ts
552
+ var MatrxOAuthError = class extends Error {
553
+ code;
554
+ constructor(code, message) {
555
+ super(message);
556
+ this.name = "MatrxOAuthError";
557
+ this.code = code;
202
558
  }
203
- return { data: { user }, error: null };
204
- }
205
- var BUDGET_EXHAUSTED_STATUS = 408;
206
- var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
207
- function isAuthTransportFailure(error) {
208
- if (!error || typeof error !== "object") return false;
209
- const name = String(error.name ?? "");
210
- if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
211
- const status = error.status;
212
- if (status === 0 || status === 504 || status === 503) return true;
213
- return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
214
- }
215
- function isSignedOutError(error) {
216
- return error instanceof AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
217
- }
218
- function userFromClaims(claims) {
219
- if (!claims || typeof claims.sub !== "string") return null;
220
- return {
221
- ...claims,
222
- id: claims.sub,
223
- app_metadata: claims.app_metadata ?? {},
224
- user_metadata: claims.user_metadata ?? {}
225
- };
226
- }
227
- function accountFlagsFor(claims) {
228
- if (!claims) return { isGuest: false, hasAccount: false };
229
- const isGuest = claims.is_anonymous === true;
230
- return { isGuest, hasAccount: !isGuest };
559
+ };
560
+ function base64UrlDecodeToString(input) {
561
+ const b64 = input.replace(/-/g, "+").replace(/_/g, "/");
562
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
563
+ const bin = atob(padded);
564
+ const bytes = new Uint8Array(bin.length);
565
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
566
+ return new TextDecoder().decode(bytes);
231
567
  }
232
- var PROJECT_SIGNING_KEY_AUTHORITIES = [
233
- "db.matrxserver.com",
234
- "brsgrqvjdzwihsvnfqkf.supabase.co"
235
- ];
236
- var PROJECT_SIGNING_KEYS = [
237
- {
238
- alg: "ES256",
239
- crv: "P-256",
240
- ext: true,
241
- key_ops: ["verify"],
242
- kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
243
- kty: "EC",
244
- use: "sig",
245
- x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
246
- y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
247
- }
248
- ];
249
- function pinnedSigningKeysFor(supabaseUrl) {
250
- if (!supabaseUrl) return [];
251
- let host;
568
+ function personIdFromAccessToken(token) {
569
+ const part = token.split(".")[1];
570
+ if (!part) throw new MatrxOAuthError("invalid_token", "the access token is not a JWT");
571
+ let payload;
252
572
  try {
253
- host = new URL(supabaseUrl).host.toLowerCase();
573
+ payload = JSON.parse(base64UrlDecodeToString(part));
254
574
  } catch {
255
- return [];
575
+ throw new MatrxOAuthError("invalid_token", "the access token payload is unreadable");
256
576
  }
257
- return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
258
- }
259
- function isUnverifiableTokenRefusal(result) {
260
- if (result.status !== 401) return false;
261
- if (result.error?.code !== "PGRST301") return false;
262
- return !/expired/i.test(result.error?.message ?? "");
577
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
578
+ throw new MatrxOAuthError("invalid_token", "the access token has no subject");
579
+ }
580
+ return {
581
+ personId: payload.sub,
582
+ email: typeof payload.email === "string" && payload.email.length > 0 ? payload.email : null,
583
+ expiresAtMs: typeof payload.exp === "number" ? payload.exp * 1e3 : null
584
+ };
263
585
  }
264
- function isUnverifiableTokenError(error) {
265
- if (!error) return false;
266
- const name = String(error.name ?? "");
267
- if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return false;
268
- if (name === "AuthSessionMissingError" || error.code === "session_not_found") return false;
269
- const status = error.status;
270
- if (status === 0 || status === 408 || status === 503 || status === 504) return false;
271
- if (/expired/i.test(error.message ?? "")) return false;
272
- return error.code === "bad_jwt" || name === "AuthInvalidJwtError";
586
+
587
+ // src/oauth-session.ts
588
+ var NOT_SUPPORTED = (what) => ({
589
+ data: { user: null, session: null },
590
+ error: { name: "AuthApiError", message: `${what} is not available on a "Sign in with AI Matrx" session`, status: 400 }
591
+ });
592
+ function userFromToken(accessToken) {
593
+ const claims = personIdFromAccessToken(accessToken);
594
+ return {
595
+ id: claims.personId,
596
+ aud: "authenticated",
597
+ role: "authenticated",
598
+ email: claims.email ?? void 0,
599
+ app_metadata: {},
600
+ user_metadata: {},
601
+ created_at: ""
602
+ };
273
603
  }
274
- async function confirmUnverifiableSession(auth, supabaseUrl) {
275
- try {
276
- const { error } = await auth.getClaims(void 0, { jwks: { keys: pinnedSigningKeysFor(supabaseUrl) } });
277
- return isUnverifiableTokenError(error);
278
- } catch {
279
- return false;
280
- }
604
+ async function toSession(s, readUser) {
605
+ const user = readUser ? await readUser(s.accessToken) : userFromToken(s.accessToken);
606
+ if (!user) return null;
607
+ return {
608
+ access_token: s.accessToken,
609
+ refresh_token: "",
610
+ token_type: "bearer",
611
+ expires_at: Math.floor(s.expiresAtMs / 1e3),
612
+ expires_in: Math.max(0, Math.floor((s.expiresAtMs - Date.now()) / 1e3)),
613
+ user
614
+ };
281
615
  }
282
- function createClaimsCache(client, options) {
283
- const ttl = options.ttlMs ?? 2e3;
284
- let cached = null;
285
- let inflight = null;
286
- let listening = false;
287
- return () => {
288
- if (!listening) {
289
- listening = true;
290
- client.auth.onAuthStateChange((event) => {
291
- if (event === "SIGNED_OUT" || event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
292
- cached = null;
293
- }
616
+ function oauthAuthClient(oauth, options = {}) {
617
+ const read = async () => {
618
+ const stored = await oauth.getSession();
619
+ return stored ? toSession(stored, options.readUser) : null;
620
+ };
621
+ const auth = {
622
+ async getSession() {
623
+ try {
624
+ return { data: { session: await read() }, error: null };
625
+ } catch (e) {
626
+ return { data: { session: null }, error: { name: "AuthUnknownError", message: e.message } };
627
+ }
628
+ },
629
+ async getUser() {
630
+ const stored = await oauth.getSession();
631
+ if (!stored) return { data: { user: null }, error: { name: "AuthSessionMissingError", message: "Auth session missing!", status: 400 } };
632
+ try {
633
+ const user = options.readUser ? await options.readUser(stored.accessToken) : userFromToken(stored.accessToken);
634
+ return { data: { user }, error: null };
635
+ } catch (e) {
636
+ return { data: { user: null }, error: { name: "AuthUnknownError", message: e.message } };
637
+ }
638
+ },
639
+ async refreshSession() {
640
+ try {
641
+ const token = await oauth.refresh();
642
+ const session = token ? await read() : null;
643
+ return { data: { session, user: session?.user ?? null }, error: null };
644
+ } catch (e) {
645
+ return { data: { session: null, user: null }, error: { name: "AuthRetryableFetchError", message: e.message, status: 0 } };
646
+ }
647
+ },
648
+ async signOut() {
649
+ await oauth.signOut();
650
+ return { error: null };
651
+ },
652
+ onAuthStateChange(callback) {
653
+ const deliver = (event, stored) => {
654
+ void (stored ? toSession(stored, options.readUser) : Promise.resolve(null)).then(
655
+ (session) => void callback(event, session),
656
+ () => void callback(event, null)
657
+ );
658
+ };
659
+ const stop = oauth.onChange((e) => {
660
+ if (e.type === "signed_out") deliver("SIGNED_OUT", null);
661
+ else deliver(e.type === "signed_in" ? "SIGNED_IN" : "TOKEN_REFRESHED", e.session);
294
662
  });
663
+ void oauth.getSession().then((stored) => deliver("INITIAL_SESSION", stored), () => deliver("INITIAL_SESSION", null));
664
+ return { data: { subscription: { id: "oauth", callback, unsubscribe: stop } } };
665
+ },
666
+ signInWithPassword: async () => NOT_SUPPORTED("Password sign-in"),
667
+ signInWithOAuth: async () => NOT_SUPPORTED("Provider sign-in"),
668
+ updateUser: async () => NOT_SUPPORTED("Editing the person")
669
+ };
670
+ return { auth };
671
+ }
672
+ function withOAuthSession(db, oauth, options = {}) {
673
+ const authClient = oauthAuthClient(oauth, options);
674
+ return new Proxy(db, {
675
+ get(target, prop) {
676
+ if (prop === "auth") return authClient.auth;
677
+ const value = Reflect.get(target, prop, target);
678
+ return typeof value === "function" ? value.bind(target) : value;
679
+ }
680
+ });
681
+ }
682
+ function heldOrganizationInStorage(storage, key = "ai-matrx.auth.active-organization") {
683
+ return {
684
+ async read(userId) {
685
+ try {
686
+ const raw = await storage.getItem(key);
687
+ if (!raw) return null;
688
+ const parsed = JSON.parse(raw);
689
+ return parsed.userId === userId && typeof parsed.organizationId === "string" ? parsed.organizationId : null;
690
+ } catch {
691
+ return null;
692
+ }
693
+ },
694
+ async write(userId, organizationId) {
695
+ await storage.setItem(key, JSON.stringify({ userId, organizationId }));
696
+ },
697
+ async clear() {
698
+ await storage.removeItem(key);
295
699
  }
296
- if (cached && cached.expiresAt > Date.now()) return Promise.resolve(cached.value);
297
- if (inflight) return inflight;
298
- inflight = getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } }).then((result) => {
299
- if (isUnverifiableTokenError(result.error)) options.onUnverifiable?.();
300
- cached = { value: result, expiresAt: Date.now() + ttl };
301
- return result;
302
- }).finally(() => {
303
- inflight = null;
304
- });
305
- return inflight;
306
700
  };
307
701
  }
308
702
  export {
309
703
  PROJECT_SIGNING_KEYS,
310
704
  PROJECT_SIGNING_KEY_AUTHORITIES,
705
+ SESSION_ATTACH_BUDGET_MS,
706
+ SESSION_RECOVERY_BUDGET_MS,
311
707
  SecretKeyRefusedError,
312
708
  SignInError,
313
709
  StorageUnavailableError,
710
+ WAIT_COOLDOWN_MS,
314
711
  accountFlagsFor,
315
712
  browserStorage,
316
713
  confirmUnverifiableSession,
317
714
  createAuthClient,
318
715
  createClaimsCache,
716
+ createSessionBarrier,
319
717
  extensionStorage,
320
718
  getClaimsUser,
321
719
  getPerson,
322
720
  getSession,
721
+ heldOrganizationInStorage,
323
722
  invalidatePerson,
324
723
  isAuthTransportFailure,
724
+ isSessionRefusal,
325
725
  isSignedOutError,
326
726
  isUnverifiableTokenError,
327
727
  isUnverifiableTokenRefusal,
328
728
  keyValueStorage,
329
729
  memoryStorage,
730
+ oauthAuthClient,
330
731
  onSessionChange,
331
732
  pinnedSigningKeysFor,
332
733
  refreshSession,
@@ -336,5 +737,6 @@ export {
336
737
  signOut,
337
738
  updatePerson,
338
739
  userCredentials,
339
- userFromClaims
740
+ userFromClaims,
741
+ withOAuthSession
340
742
  };