@ai-matrx/auth 0.1.3 → 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/dist/index.d.ts CHANGED
@@ -1,57 +1,10 @@
1
- export { A as AuthClient, S as SessionSourceLike, a as SignInError, U as UserCredentialsPort, g as getPerson, b as getSession, 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-DhRO4euL.js';
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 };
package/dist/index.js CHANGED
@@ -19,10 +19,43 @@ async function getSession(client) {
19
19
  const { data, error } = await client.auth.getSession();
20
20
  return check("getSession", data, error).session;
21
21
  }
22
+ var personCache = /* @__PURE__ */ new WeakMap();
23
+ function personEntry(client) {
24
+ let entry = personCache.get(client);
25
+ if (!entry) {
26
+ const fresh = { read: null, epoch: 0 };
27
+ entry = fresh;
28
+ personCache.set(client, fresh);
29
+ const subscribe = client.auth?.onAuthStateChange;
30
+ if (typeof subscribe === "function") {
31
+ subscribe.call(client.auth, async (event) => {
32
+ if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") invalidatePerson(client);
33
+ });
34
+ }
35
+ }
36
+ return entry;
37
+ }
38
+ function invalidatePerson(client) {
39
+ const entry = personCache.get(client);
40
+ if (!entry) return;
41
+ entry.epoch += 1;
42
+ entry.read = null;
43
+ }
22
44
  async function getPerson(client) {
23
- const { data, error } = await client.auth.getUser();
24
- if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
25
- return check("getPerson", data, error).user;
45
+ const entry = personEntry(client);
46
+ if (entry.read) return entry.read;
47
+ const epoch = entry.epoch;
48
+ const read = (async () => {
49
+ const { data, error } = await client.auth.getUser();
50
+ if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
51
+ return check("getPerson", data, error).user;
52
+ })();
53
+ entry.read = read;
54
+ const settle = (keep) => {
55
+ if (entry.epoch === epoch && entry.read === read && !keep) entry.read = null;
56
+ };
57
+ read.then((user) => settle(user !== null), () => settle(false));
58
+ return read;
26
59
  }
27
60
  function onSessionChange(client, cb) {
28
61
  const { data } = client.auth.onAuthStateChange(cb);
@@ -30,6 +63,7 @@ function onSessionChange(client, cb) {
30
63
  }
31
64
  async function signInWithPassword(client, credentials) {
32
65
  const { data, error } = await client.auth.signInWithPassword(credentials);
66
+ invalidatePerson(client);
33
67
  const ok = check("signInWithPassword", data, error);
34
68
  return { session: ok.session, person: ok.user };
35
69
  }
@@ -41,10 +75,12 @@ async function signInWithOAuth(client, options) {
41
75
  }
42
76
  async function signOut(client, scope = "global") {
43
77
  const { error } = await client.auth.signOut({ scope });
78
+ invalidatePerson(client);
44
79
  check("signOut", null, error);
45
80
  }
46
81
  async function updatePerson(client, attributes) {
47
82
  const { data, error } = await client.auth.updateUser(attributes);
83
+ invalidatePerson(client);
48
84
  return check("updatePerson", data, error).user;
49
85
  }
50
86
  function sessionCredentials(client) {
@@ -144,23 +180,161 @@ function createAuthClient(options) {
144
180
  if (options.detectSessionInUrl !== void 0) auth["detectSessionInUrl"] = options.detectSessionInUrl;
145
181
  return createClient(options.url, options.key, { auth });
146
182
  }
183
+
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
+ };
202
+ }
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 };
231
+ }
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;
252
+ try {
253
+ host = new URL(supabaseUrl).host.toLowerCase();
254
+ } catch {
255
+ return [];
256
+ }
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 ?? "");
263
+ }
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";
273
+ }
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
+ }
281
+ }
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
+ }
294
+ });
295
+ }
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
+ };
307
+ }
147
308
  export {
309
+ PROJECT_SIGNING_KEYS,
310
+ PROJECT_SIGNING_KEY_AUTHORITIES,
148
311
  SecretKeyRefusedError,
149
312
  SignInError,
150
313
  StorageUnavailableError,
314
+ accountFlagsFor,
151
315
  browserStorage,
316
+ confirmUnverifiableSession,
152
317
  createAuthClient,
318
+ createClaimsCache,
153
319
  extensionStorage,
320
+ getClaimsUser,
154
321
  getPerson,
155
322
  getSession,
323
+ invalidatePerson,
324
+ isAuthTransportFailure,
325
+ isSignedOutError,
326
+ isUnverifiableTokenError,
327
+ isUnverifiableTokenRefusal,
156
328
  keyValueStorage,
157
329
  memoryStorage,
158
330
  onSessionChange,
331
+ pinnedSigningKeysFor,
159
332
  refreshSession,
160
333
  sessionCredentials,
161
334
  signInWithOAuth,
162
335
  signInWithPassword,
163
336
  signOut,
164
337
  updatePerson,
165
- userCredentials
338
+ userCredentials,
339
+ userFromClaims
166
340
  };