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