@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/CHANGELOG.md +47 -0
- package/dist/active-organization-Dy4UtiyR.d.cts +594 -0
- package/dist/active-organization-Dy4UtiyR.d.ts +594 -0
- package/dist/guest.cjs +213 -0
- package/dist/guest.d.cts +189 -0
- package/dist/guest.d.ts +189 -0
- package/dist/guest.js +192 -0
- package/dist/index.cjs +514 -112
- package/dist/index.d.cts +161 -5
- package/dist/index.d.ts +161 -5
- package/dist/index.js +515 -113
- package/dist/matrx-oauth-_Ji8X5Wm.d.cts +264 -0
- package/dist/matrx-oauth-_Ji8X5Wm.d.ts +264 -0
- package/dist/matrx-oauth.cjs +77 -10
- package/dist/matrx-oauth.d.cts +1 -181
- package/dist/matrx-oauth.d.ts +1 -181
- package/dist/matrx-oauth.js +77 -10
- package/dist/organizations.cjs +269 -13
- package/dist/organizations.d.cts +3 -225
- package/dist/organizations.d.ts +3 -225
- package/dist/organizations.js +269 -13
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/package.json +12 -2
- package/dist/session-DdQiStp6.d.cts +0 -112
- package/dist/session-DdQiStp6.d.ts +0 -112
- package/dist/storage-Dqnye4KF.d.cts +0 -51
- package/dist/storage-Dqnye4KF.d.ts +0 -51
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/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
233
|
-
"
|
|
234
|
-
"
|
|
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
|
-
|
|
573
|
+
payload = JSON.parse(base64UrlDecodeToString(part));
|
|
254
574
|
} catch {
|
|
255
|
-
|
|
575
|
+
throw new MatrxOAuthError("invalid_token", "the access token payload is unreadable");
|
|
256
576
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
};
|