@absolutejs/auth 0.69.0 → 0.69.1

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.
@@ -46,396 +46,102 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = import.meta.require;
48
48
 
49
- // src/client/createAuthClient.ts
50
- var DEFAULT_ROUTES = {
51
- emailVerify: "/auth/verify-email",
52
- emailVerifyRequest: "/auth/verify-email/request",
53
- login: "/auth/login",
54
- magicLinkRequest: "/auth/passwordless/magic-link",
55
- magicLinkVerify: "/auth/passwordless/magic-link/verify",
56
- mfaChallenge: "/auth/mfa/totp/challenge",
57
- mfaManagement: "/auth/mfa",
58
- mfaSetup: "/auth/mfa/totp/setup",
59
- mfaVerifySetup: "/auth/mfa/totp/verify",
60
- passkeyAuthenticateOptions: "/auth/webauthn/authenticate/options",
61
- passkeyAuthenticateVerify: "/auth/webauthn/authenticate/verify",
62
- passkeyList: "/auth/webauthn/credentials",
63
- passkeyRegisterOptions: "/auth/webauthn/register/options",
64
- passkeyRegisterVerify: "/auth/webauthn/register/verify",
65
- passkeyRemove: "/auth/webauthn/credentials",
66
- passwordReset: "/auth/reset-password",
67
- passwordResetRequest: "/auth/reset-password/request",
68
- register: "/auth/register",
69
- sessions: "/auth/sessions",
70
- signout: "/oauth2/signout",
71
- status: "/oauth2/status"
49
+ // src/client/mobile.ts
50
+ class MobileAuthError extends Error {
51
+ code;
52
+ cause;
53
+ constructor(code, message, options) {
54
+ super(message);
55
+ this.name = "MobileAuthError";
56
+ this.code = code;
57
+ this.cause = options?.cause;
58
+ }
59
+ }
60
+ var PENDING_KEY = "oidc.pending";
61
+ var REFRESH_KEY = "oidc.refresh";
62
+ var DEFAULT_CLOCK_SKEW_MS = 30000;
63
+ var PENDING_TTL_MS = 10 * 60000;
64
+ var RANDOM_BYTES = 32;
65
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66
+ var base64Url = (value) => {
67
+ let binary = "";
68
+ for (const byte of value)
69
+ binary += String.fromCharCode(byte);
70
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
72
71
  };
73
- var succeed = (data) => ({
74
- data,
75
- error: null
76
- });
77
- var fail = (error) => ({
78
- data: null,
79
- error
80
- });
81
- var errorFor = (response, body) => ({
82
- body,
83
- message: typeof body === "string" ? body : readMessage(body) ?? response.statusText,
84
- status: response.status
85
- });
86
- var createAuthClient = ({
87
- baseUrl = "",
88
- credentials = "same-origin",
89
- fetch: fetchImpl = fetch,
90
- routes,
91
- transport
92
- } = {}) => {
93
- const resolvedFetch = transport?.fetch ?? fetchImpl;
94
- const signInEmail = transport?.signInEmail;
95
- const signOut = transport?.signOut;
96
- const signUpEmail = transport?.signUpEmail;
97
- const transportStatus = transport?.status;
98
- const resolvedRoutes = {
99
- ...DEFAULT_ROUTES,
100
- ...routes
101
- };
102
- const request = async (path, init) => {
103
- try {
104
- const response = await resolvedFetch(`${baseUrl}${path}`, {
105
- credentials,
106
- ...init
107
- });
108
- const text = await response.text();
109
- if (!response.ok)
110
- return fail(errorFor(response, safeJson(text)));
111
- const data = JSON.parse(text === "" ? "null" : text);
112
- return succeed(data);
113
- } catch (caught) {
114
- const message = caught instanceof Error ? caught.message : "network";
115
- return fail({ body: null, message, status: 0 });
116
- }
117
- };
118
- const runTransport = async (operation) => {
119
- try {
120
- return succeed(await operation());
121
- } catch (caught) {
122
- const message = caught instanceof Error ? caught.message : "authentication";
123
- return fail({ body: null, message, status: 0 });
124
- }
125
- };
126
- const post = (path, body, method = "POST") => request(path, {
127
- body: body === undefined ? undefined : JSON.stringify(body),
128
- headers: body === undefined ? undefined : { "content-type": "application/json" },
129
- method
130
- });
131
- const get = (path) => request(path, { method: "GET" });
132
- const del = (path) => request(path, { method: "DELETE" });
133
- return {
134
- emailVerification: {
135
- request: (body) => post(resolvedRoutes.emailVerifyRequest, body),
136
- verify: (body) => post(resolvedRoutes.emailVerify, body)
137
- },
138
- mfa: {
139
- challenge: (body) => post(resolvedRoutes.mfaChallenge, body),
140
- disable: () => del(resolvedRoutes.mfaManagement),
141
- setup: () => post(resolvedRoutes.mfaSetup),
142
- status: () => get(resolvedRoutes.mfaManagement),
143
- verifySetup: (body) => post(resolvedRoutes.mfaVerifySetup, body)
144
- },
145
- passkeys: {
146
- authenticateOptions: () => post(resolvedRoutes.passkeyAuthenticateOptions),
147
- authenticateVerify: (response) => post(resolvedRoutes.passkeyAuthenticateVerify, response),
148
- list: () => get(resolvedRoutes.passkeyList),
149
- registerOptions: () => post(resolvedRoutes.passkeyRegisterOptions),
150
- registerVerify: (response) => post(resolvedRoutes.passkeyRegisterVerify, response),
151
- remove: (credentialId) => del(`${resolvedRoutes.passkeyRemove}/${encodeURIComponent(credentialId)}`)
152
- },
153
- passwordless: {
154
- requestMagicLink: (body) => post(resolvedRoutes.magicLinkRequest, body),
155
- verifyMagicLink: (body) => post(resolvedRoutes.magicLinkVerify, body)
156
- },
157
- passwordReset: {
158
- confirm: (body) => post(resolvedRoutes.passwordReset, body),
159
- request: (body) => post(resolvedRoutes.passwordResetRequest, body)
160
- },
161
- sessions: {
162
- list: () => get(resolvedRoutes.sessions),
163
- revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
164
- },
165
- signIn: {
166
- email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
167
- },
168
- signUp: {
169
- email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
170
- },
171
- signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
172
- status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
173
- };
72
+ var decodeBase64Url = (value) => {
73
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
74
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
75
+ const binary = atob(padded);
76
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
174
77
  };
175
- var safeJson = (text) => {
78
+ var randomValue = () => {
79
+ const value = new Uint8Array(RANDOM_BYTES);
80
+ crypto.getRandomValues(value);
81
+ return base64Url(value);
82
+ };
83
+ var pkceChallenge = async (verifier) => base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
84
+ var normalizeIssuer = (value) => {
85
+ const issuer = new URL(value);
86
+ const loopback = issuer.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(issuer.hostname);
87
+ if (issuer.protocol !== "https:" && !loopback)
88
+ throw new TypeError("Mobile auth issuer must use HTTPS.");
89
+ if (issuer.username || issuer.password || issuer.search || issuer.hash)
90
+ throw new TypeError("Mobile auth issuer cannot contain credentials, query, or fragment.");
91
+ issuer.pathname = issuer.pathname.replace(/\/$/u, "");
92
+ return issuer.href.replace(/\/$/u, "");
93
+ };
94
+ var exactRedirect = (actualValue, expectedValue) => {
95
+ const actual = new URL(actualValue);
96
+ const expected = new URL(expectedValue);
97
+ return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname && actual.username === "" && actual.password === "";
98
+ };
99
+ var parsePending = (value) => {
100
+ if (value === null)
101
+ return;
176
102
  try {
177
- return JSON.parse(text);
103
+ const parsed = JSON.parse(value);
104
+ if (!isRecord(parsed) || typeof parsed.createdAt !== "number" || typeof parsed.nonce !== "string" || typeof parsed.state !== "string" || typeof parsed.verifier !== "string")
105
+ return;
106
+ return {
107
+ createdAt: parsed.createdAt,
108
+ nonce: parsed.nonce,
109
+ state: parsed.state,
110
+ verifier: parsed.verifier
111
+ };
178
112
  } catch {
179
- return text;
113
+ return;
180
114
  }
181
115
  };
182
- var readMessage = (body) => {
183
- if (typeof body !== "object" || body === null)
184
- return;
185
- const message = Reflect.get(body, "message");
186
- return typeof message === "string" ? message : undefined;
116
+ var parseTokenResponse = (value) => {
117
+ if (!isRecord(value) || typeof value.access_token !== "string" || typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in) || value.expires_in <= 0 || typeof value.id_token !== "string" || typeof value.refresh_token !== "string" || typeof value.token_type !== "string")
118
+ throw new MobileAuthError("token", "The token response is malformed.");
119
+ if (value.token_type.toLowerCase() !== "bearer")
120
+ throw new MobileAuthError("token", `Unsupported mobile token type ${value.token_type}; DPoP is not enabled for this client.`);
121
+ return {
122
+ access_token: value.access_token,
123
+ expires_in: value.expires_in,
124
+ id_token: value.id_token,
125
+ refresh_token: value.refresh_token,
126
+ scope: typeof value.scope === "string" ? value.scope : undefined,
127
+ token_type: value.token_type
128
+ };
187
129
  };
188
- // src/client/sessionExpiry.ts
189
- var DEFAULT_CHECK_INTERVAL_MS = 30000;
190
- var DEFAULT_REASON = "session_expired";
191
- var DEFAULT_REASON_PARAM = "reason";
192
- var DEFAULT_RETURN_URL_PARAM = "returnUrl";
193
- var DEFAULT_SIGN_IN_PATH = "/signin";
194
- var DEFAULT_STATUS_PATH = "/oauth2/status";
195
- var HTTP_UNAUTHORIZED = 401;
196
- var activeGuard = null;
197
- var requestUrl = (input, origin) => {
198
- const raw = input instanceof Request ? input.url : String(input);
130
+ var responseBody = async (response) => {
131
+ const text = await response.text();
199
132
  try {
200
- return new URL(raw, origin);
133
+ return JSON.parse(text);
201
134
  } catch {
202
- return null;
135
+ return text;
203
136
  }
204
137
  };
205
- var buildSessionExpiredSignInUrl = ({
206
- currentHref,
207
- reason = DEFAULT_REASON,
208
- reasonParam = DEFAULT_REASON_PARAM,
209
- returnUrlParam = DEFAULT_RETURN_URL_PARAM,
210
- signInPath = DEFAULT_SIGN_IN_PATH
211
- }) => {
212
- const current = new URL(currentHref);
213
- const returnTo = `${current.pathname}${current.search}${current.hash}`;
214
- const destination = new URL(signInPath, current.origin);
215
- destination.searchParams.set(reasonParam, reason);
216
- destination.searchParams.set(returnUrlParam, returnTo);
217
- return destination.origin === current.origin ? `${destination.pathname}${destination.search}${destination.hash}` : destination.toString();
218
- };
219
- var installSessionExpiryGuard = (config = {}) => {
220
- if (typeof window === "undefined" || typeof document === "undefined") {
221
- return { check: async () => false, dispose: () => {
222
- return;
223
- } };
224
- }
225
- if (activeGuard)
226
- return activeGuard;
227
- const {
228
- checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
229
- isProtectedRequest,
230
- onExpired,
231
- protectedPaths = [],
232
- reason = DEFAULT_REASON,
233
- reasonParam = DEFAULT_REASON_PARAM,
234
- returnUrlParam = DEFAULT_RETURN_URL_PARAM,
235
- signInPath = DEFAULT_SIGN_IN_PATH,
236
- statusPath = DEFAULT_STATUS_PATH
237
- } = config;
238
- const nativeFetch = window.fetch.bind(window);
239
- const originalFetch = window.fetch;
240
- let checking = false;
241
- let disposed = false;
242
- let expired = false;
243
- let lastCheckedAt = Date.now();
244
- const expire = () => {
245
- if (expired || disposed)
246
- return;
247
- expired = true;
248
- const currentHref = window.location.href;
249
- const signInUrl = buildSessionExpiredSignInUrl({
250
- currentHref,
251
- reason,
252
- reasonParam,
253
- returnUrlParam,
254
- signInPath
255
- });
256
- const current = new URL(currentHref);
257
- const returnTo = `${current.pathname}${current.search}${current.hash}`;
258
- if (onExpired) {
259
- onExpired({ returnTo, signInUrl });
260
- return;
261
- }
262
- window.location.assign(signInUrl);
263
- };
264
- const protectedRequest = (input) => {
265
- const url = requestUrl(input, window.location.origin);
266
- if (!url || url.origin !== window.location.origin)
267
- return false;
268
- if (new URL(statusPath, window.location.origin).pathname === url.pathname)
269
- return false;
270
- return isProtectedRequest?.(url) === true || protectedPaths.some((path) => url.pathname.startsWith(path));
271
- };
272
- const guardedFetch = new Proxy(originalFetch, {
273
- apply: async (target, thisArg, args) => {
274
- const [input] = args;
275
- const response = await Reflect.apply(target, thisArg, args);
276
- if (response.status === HTTP_UNAUTHORIZED && protectedRequest(input)) {
277
- expire();
278
- }
279
- return response;
280
- }
281
- });
282
- window.fetch = guardedFetch;
283
- const check = async () => {
284
- if (checking || expired || disposed)
285
- return false;
286
- checking = true;
287
- lastCheckedAt = Date.now();
288
- try {
289
- const response = await nativeFetch(statusPath, {
290
- cache: "no-store",
291
- credentials: "include",
292
- headers: { accept: "application/json" }
293
- });
294
- if (!response.ok)
295
- return false;
296
- const payload = await response.json();
297
- const sessionExpired = typeof payload === "object" && payload !== null && Reflect.get(payload, "user") === null;
298
- if (!sessionExpired)
299
- return false;
300
- expire();
301
- return true;
302
- } catch {
303
- return false;
304
- } finally {
305
- checking = false;
306
- }
307
- };
308
- const checkIfDue = () => {
309
- if (document.visibilityState !== "visible" || Date.now() - lastCheckedAt < checkIntervalMs)
310
- return;
311
- check();
312
- };
313
- const checkPersistedSession = (event) => {
314
- if (!event.persisted)
315
- return;
316
- check();
317
- };
318
- const dispose = () => {
319
- if (disposed)
320
- return;
321
- disposed = true;
322
- document.removeEventListener("visibilitychange", checkIfDue);
323
- window.removeEventListener("focus", checkIfDue);
324
- window.removeEventListener("pageshow", checkPersistedSession);
325
- if (window.fetch === guardedFetch)
326
- window.fetch = originalFetch;
327
- activeGuard = null;
328
- };
329
- document.addEventListener("visibilitychange", checkIfDue);
330
- window.addEventListener("focus", checkIfDue);
331
- window.addEventListener("pageshow", checkPersistedSession);
332
- activeGuard = { check, dispose };
333
- return activeGuard;
334
- };
335
- var isProtectedSessionRequest = ({
336
- input,
337
- origin,
338
- protectedPaths
339
- }) => {
340
- const url = requestUrl(input, origin);
341
- return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
342
- };
343
- // src/client/mobile.ts
344
- class MobileAuthError extends Error {
345
- code;
346
- cause;
347
- constructor(code, message, options) {
348
- super(message);
349
- this.name = "MobileAuthError";
350
- this.code = code;
351
- this.cause = options?.cause;
352
- }
353
- }
354
- var PENDING_KEY = "oidc.pending";
355
- var REFRESH_KEY = "oidc.refresh";
356
- var DEFAULT_CLOCK_SKEW_MS = 30000;
357
- var PENDING_TTL_MS = 10 * 60000;
358
- var RANDOM_BYTES = 32;
359
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
360
- var base64Url = (value) => {
361
- let binary = "";
362
- for (const byte of value)
363
- binary += String.fromCharCode(byte);
364
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
365
- };
366
- var decodeBase64Url = (value) => {
367
- const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
368
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
369
- const binary = atob(padded);
370
- return Uint8Array.from(binary, (character) => character.charCodeAt(0));
371
- };
372
- var randomValue = () => {
373
- const value = new Uint8Array(RANDOM_BYTES);
374
- crypto.getRandomValues(value);
375
- return base64Url(value);
376
- };
377
- var pkceChallenge = async (verifier) => base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
378
- var normalizeIssuer = (value) => {
379
- const issuer = new URL(value);
380
- const loopback = issuer.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(issuer.hostname);
381
- if (issuer.protocol !== "https:" && !loopback)
382
- throw new TypeError("Mobile auth issuer must use HTTPS.");
383
- if (issuer.username || issuer.password || issuer.search || issuer.hash)
384
- throw new TypeError("Mobile auth issuer cannot contain credentials, query, or fragment.");
385
- issuer.pathname = issuer.pathname.replace(/\/$/u, "");
386
- return issuer.href.replace(/\/$/u, "");
387
- };
388
- var exactRedirect = (actualValue, expectedValue) => {
389
- const actual = new URL(actualValue);
390
- const expected = new URL(expectedValue);
391
- return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname && actual.username === "" && actual.password === "";
392
- };
393
- var parsePending = (value) => {
394
- if (value === null)
395
- return;
396
- try {
397
- const parsed = JSON.parse(value);
398
- if (!isRecord(parsed) || typeof parsed.createdAt !== "number" || typeof parsed.nonce !== "string" || typeof parsed.state !== "string" || typeof parsed.verifier !== "string")
399
- return;
400
- return {
401
- createdAt: parsed.createdAt,
402
- nonce: parsed.nonce,
403
- state: parsed.state,
404
- verifier: parsed.verifier
405
- };
406
- } catch {
407
- return;
408
- }
409
- };
410
- var parseTokenResponse = (value) => {
411
- if (!isRecord(value) || typeof value.access_token !== "string" || typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in) || value.expires_in <= 0 || typeof value.id_token !== "string" || typeof value.refresh_token !== "string" || typeof value.token_type !== "string")
412
- throw new MobileAuthError("token", "The token response is malformed.");
413
- if (value.token_type.toLowerCase() !== "bearer")
414
- throw new MobileAuthError("token", `Unsupported mobile token type ${value.token_type}; DPoP is not enabled for this client.`);
415
- return {
416
- access_token: value.access_token,
417
- expires_in: value.expires_in,
418
- id_token: value.id_token,
419
- refresh_token: value.refresh_token,
420
- scope: typeof value.scope === "string" ? value.scope : undefined,
421
- token_type: value.token_type
422
- };
423
- };
424
- var responseBody = async (response) => {
425
- const text = await response.text();
426
- try {
427
- return JSON.parse(text);
428
- } catch {
429
- return text;
430
- }
431
- };
432
- var requireEndpoint = (value, name, issuer) => {
433
- if (typeof value !== "string")
434
- throw new MobileAuthError("discovery", `OIDC discovery is missing ${name}.`);
435
- const endpoint = new URL(value);
436
- if (endpoint.protocol !== "https:" && new URL(issuer).protocol === "https:")
437
- throw new MobileAuthError("discovery", `OIDC discovery ${name} must use HTTPS.`);
438
- return endpoint.href;
138
+ var requireEndpoint = (value, name, issuer) => {
139
+ if (typeof value !== "string")
140
+ throw new MobileAuthError("discovery", `OIDC discovery is missing ${name}.`);
141
+ const endpoint = new URL(value);
142
+ if (endpoint.protocol !== "https:" && new URL(issuer).protocol === "https:")
143
+ throw new MobileAuthError("discovery", `OIDC discovery ${name} must use HTTPS.`);
144
+ return endpoint.href;
439
145
  };
440
146
  var parseDiscovery = (value, issuer) => {
441
147
  if (!isRecord(value) || value.issuer !== issuer)
@@ -716,146 +422,441 @@ var createMobileAuthClient = (config) => {
716
422
  reject(new MobileAuthError("aborted", "Authorization was cancelled."));
717
423
  }, { once: true });
718
424
  });
719
- try {
720
- await config.links.openExternal(url.href);
721
- } catch (error) {
722
- pendingSignIns.delete(pending.state);
723
- await config.storage.remove(PENDING_KEY);
724
- throw error;
425
+ try {
426
+ await config.links.openExternal(url.href);
427
+ } catch (error) {
428
+ pendingSignIns.delete(pending.state);
429
+ await config.storage.remove(PENDING_KEY);
430
+ throw error;
431
+ }
432
+ return result;
433
+ };
434
+ const authenticatedFetch = async (input, init) => {
435
+ const original = new Request(input, init);
436
+ if (!allowedOrigins.has(new URL(original.url).origin))
437
+ throw new MobileAuthError("origin", "Mobile auth refused to send a credential to an unregistered origin.");
438
+ const send = async (forceRefresh) => {
439
+ if (forceRefresh)
440
+ access = undefined;
441
+ const token = await refreshAccessToken();
442
+ const request = original.clone();
443
+ const headers = new Headers(request.headers);
444
+ headers.set("authorization", `Bearer ${token}`);
445
+ return fetchImpl(new Request(request, { credentials: "omit", headers }));
446
+ };
447
+ const response = await send(false);
448
+ return response.status === 401 ? send(true) : response;
449
+ };
450
+ const optionalAuthenticatedFetch = async (input, init) => {
451
+ const request = new Request(input, init);
452
+ if (!allowedOrigins.has(new URL(request.url).origin))
453
+ throw new MobileAuthError("origin", "Mobile auth refused to send a request outside an allowed origin.");
454
+ const refreshToken = await config.storage.get(REFRESH_KEY);
455
+ if (access || refreshToken)
456
+ return authenticatedFetch(request);
457
+ return fetchImpl(new Request(request, { credentials: "omit" }));
458
+ };
459
+ const status = async () => {
460
+ try {
461
+ const metadata = await discovery();
462
+ const response = await authenticatedFetch(metadata.userinfo_endpoint);
463
+ if (response.status === 401)
464
+ return null;
465
+ if (!response.ok)
466
+ throw new MobileAuthError("network", `User info failed with HTTP ${response.status}.`);
467
+ const user = await response.json();
468
+ if (!isRecord(user) || typeof user.sub !== "string")
469
+ throw new MobileAuthError("token", "The user-info response is malformed.");
470
+ return { ...user, sub: user.sub };
471
+ } catch (error) {
472
+ if (error instanceof MobileAuthError && (error.code === "oauth" || error.code === "token"))
473
+ return null;
474
+ throw error;
475
+ }
476
+ };
477
+ const socketTicket = async (audience = resource) => {
478
+ const metadata = await discovery();
479
+ if (!metadata.socket_ticket_endpoint)
480
+ throw new MobileAuthError("discovery", "The authorization server does not advertise WebSocket tickets.");
481
+ const response = await authenticatedFetch(metadata.socket_ticket_endpoint, {
482
+ body: JSON.stringify({ audience }),
483
+ headers: { "content-type": "application/json" },
484
+ method: "POST"
485
+ });
486
+ const body = await responseBody(response);
487
+ if (!response.ok || !isRecord(body) || typeof body.ticket !== "string")
488
+ throw new MobileAuthError(response.ok ? "token" : "oauth", `WebSocket ticket request failed with HTTP ${response.status}.`);
489
+ return body.ticket;
490
+ };
491
+ const revokeRefreshToken = async (refreshToken) => {
492
+ const metadata = await discovery();
493
+ if (metadata.revocation_endpoint)
494
+ await fetchImpl(metadata.revocation_endpoint, {
495
+ body: new URLSearchParams({
496
+ client_id: config.clientId,
497
+ token: refreshToken,
498
+ token_type_hint: "refresh_token"
499
+ }),
500
+ credentials: "omit",
501
+ headers: {
502
+ "content-type": "application/x-www-form-urlencoded"
503
+ },
504
+ method: "POST"
505
+ });
506
+ };
507
+ const signOut = async () => {
508
+ const refreshToken = await config.storage.get(REFRESH_KEY);
509
+ try {
510
+ if (refreshToken)
511
+ await revokeRefreshToken(refreshToken);
512
+ } finally {
513
+ access = undefined;
514
+ await Promise.all([
515
+ config.storage.remove(PENDING_KEY),
516
+ config.storage.remove(REFRESH_KEY)
517
+ ]);
518
+ }
519
+ };
520
+ const stop = async () => {
521
+ await Promise.all([stopLinks?.(), stopResume?.()]);
522
+ stopLinks = undefined;
523
+ stopResume = undefined;
524
+ startPromise = undefined;
525
+ };
526
+ return {
527
+ fetch: authenticatedFetch,
528
+ fetchOptional: optionalAuthenticatedFetch,
529
+ handleCallback,
530
+ refresh: refreshAccessToken,
531
+ signIn,
532
+ signOut,
533
+ socketTicket,
534
+ start,
535
+ status,
536
+ stop
537
+ };
538
+ };
539
+ var createMobileAuthTransport = (client) => ({
540
+ fetch: client.fetch,
541
+ signInEmail: async ({ email }) => {
542
+ await client.signIn({
543
+ authorizationParameters: { login_hint: email }
544
+ });
545
+ return { status: "authenticated" };
546
+ },
547
+ signOut: async () => {
548
+ await client.signOut();
549
+ return null;
550
+ },
551
+ signUpEmail: async ({ email }) => {
552
+ await client.signIn({
553
+ authorizationParameters: {
554
+ login_hint: email,
555
+ screen_hint: "signup"
556
+ }
557
+ });
558
+ return { status: "authenticated" };
559
+ },
560
+ status: async () => {
561
+ const user = await client.status();
562
+ return { user };
563
+ }
564
+ });
565
+
566
+ // src/client/createAuthClient.ts
567
+ var DEFAULT_ROUTES = {
568
+ emailVerify: "/auth/verify-email",
569
+ emailVerifyRequest: "/auth/verify-email/request",
570
+ login: "/auth/login",
571
+ magicLinkRequest: "/auth/passwordless/magic-link",
572
+ magicLinkVerify: "/auth/passwordless/magic-link/verify",
573
+ mfaChallenge: "/auth/mfa/totp/challenge",
574
+ mfaManagement: "/auth/mfa",
575
+ mfaSetup: "/auth/mfa/totp/setup",
576
+ mfaVerifySetup: "/auth/mfa/totp/verify",
577
+ passkeyAuthenticateOptions: "/auth/webauthn/authenticate/options",
578
+ passkeyAuthenticateVerify: "/auth/webauthn/authenticate/verify",
579
+ passkeyList: "/auth/webauthn/credentials",
580
+ passkeyRegisterOptions: "/auth/webauthn/register/options",
581
+ passkeyRegisterVerify: "/auth/webauthn/register/verify",
582
+ passkeyRemove: "/auth/webauthn/credentials",
583
+ passwordReset: "/auth/reset-password",
584
+ passwordResetRequest: "/auth/reset-password/request",
585
+ register: "/auth/register",
586
+ sessions: "/auth/sessions",
587
+ signout: "/oauth2/signout",
588
+ status: "/oauth2/status"
589
+ };
590
+ var succeed = (data) => ({
591
+ data,
592
+ error: null
593
+ });
594
+ var fail = (error) => ({
595
+ data: null,
596
+ error
597
+ });
598
+ var errorFor = (response, body) => ({
599
+ body,
600
+ message: typeof body === "string" ? body : readMessage(body) ?? response.statusText,
601
+ status: response.status
602
+ });
603
+ var createAuthClient = ({
604
+ baseUrl = "",
605
+ credentials = "same-origin",
606
+ fetch: fetchImpl = fetch,
607
+ routes,
608
+ transport
609
+ } = {}) => {
610
+ const resolvedFetch = transport?.fetch ?? fetchImpl;
611
+ const signInEmail = transport?.signInEmail;
612
+ const signOut = transport?.signOut;
613
+ const signUpEmail = transport?.signUpEmail;
614
+ const transportStatus = transport?.status;
615
+ const resolvedRoutes = {
616
+ ...DEFAULT_ROUTES,
617
+ ...routes
618
+ };
619
+ const request = async (path, init) => {
620
+ try {
621
+ const response = await resolvedFetch(`${baseUrl}${path}`, {
622
+ credentials,
623
+ ...init
624
+ });
625
+ const text = await response.text();
626
+ if (!response.ok)
627
+ return fail(errorFor(response, safeJson(text)));
628
+ const data = JSON.parse(text === "" ? "null" : text);
629
+ return succeed(data);
630
+ } catch (caught) {
631
+ const message = caught instanceof Error ? caught.message : "network";
632
+ return fail({ body: null, message, status: 0 });
633
+ }
634
+ };
635
+ const runTransport = async (operation) => {
636
+ try {
637
+ return succeed(await operation());
638
+ } catch (caught) {
639
+ const message = caught instanceof Error ? caught.message : "authentication";
640
+ return fail({ body: null, message, status: 0 });
641
+ }
642
+ };
643
+ const post = (path, body, method = "POST") => request(path, {
644
+ body: body === undefined ? undefined : JSON.stringify(body),
645
+ headers: body === undefined ? undefined : { "content-type": "application/json" },
646
+ method
647
+ });
648
+ const get = (path) => request(path, { method: "GET" });
649
+ const del = (path) => request(path, { method: "DELETE" });
650
+ return {
651
+ emailVerification: {
652
+ request: (body) => post(resolvedRoutes.emailVerifyRequest, body),
653
+ verify: (body) => post(resolvedRoutes.emailVerify, body)
654
+ },
655
+ mfa: {
656
+ challenge: (body) => post(resolvedRoutes.mfaChallenge, body),
657
+ disable: () => del(resolvedRoutes.mfaManagement),
658
+ setup: () => post(resolvedRoutes.mfaSetup),
659
+ status: () => get(resolvedRoutes.mfaManagement),
660
+ verifySetup: (body) => post(resolvedRoutes.mfaVerifySetup, body)
661
+ },
662
+ passkeys: {
663
+ authenticateOptions: () => post(resolvedRoutes.passkeyAuthenticateOptions),
664
+ authenticateVerify: (response) => post(resolvedRoutes.passkeyAuthenticateVerify, response),
665
+ list: () => get(resolvedRoutes.passkeyList),
666
+ registerOptions: () => post(resolvedRoutes.passkeyRegisterOptions),
667
+ registerVerify: (response) => post(resolvedRoutes.passkeyRegisterVerify, response),
668
+ remove: (credentialId) => del(`${resolvedRoutes.passkeyRemove}/${encodeURIComponent(credentialId)}`)
669
+ },
670
+ passwordless: {
671
+ requestMagicLink: (body) => post(resolvedRoutes.magicLinkRequest, body),
672
+ verifyMagicLink: (body) => post(resolvedRoutes.magicLinkVerify, body)
673
+ },
674
+ passwordReset: {
675
+ confirm: (body) => post(resolvedRoutes.passwordReset, body),
676
+ request: (body) => post(resolvedRoutes.passwordResetRequest, body)
677
+ },
678
+ sessions: {
679
+ list: () => get(resolvedRoutes.sessions),
680
+ revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
681
+ },
682
+ signIn: {
683
+ email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
684
+ },
685
+ signUp: {
686
+ email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
687
+ },
688
+ signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
689
+ status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
690
+ };
691
+ };
692
+ var safeJson = (text) => {
693
+ try {
694
+ return JSON.parse(text);
695
+ } catch {
696
+ return text;
697
+ }
698
+ };
699
+ var readMessage = (body) => {
700
+ if (typeof body !== "object" || body === null)
701
+ return;
702
+ const message = Reflect.get(body, "message");
703
+ return typeof message === "string" ? message : undefined;
704
+ };
705
+ // src/client/sessionExpiry.ts
706
+ var DEFAULT_CHECK_INTERVAL_MS = 30000;
707
+ var DEFAULT_REASON = "session_expired";
708
+ var DEFAULT_REASON_PARAM = "reason";
709
+ var DEFAULT_RETURN_URL_PARAM = "returnUrl";
710
+ var DEFAULT_SIGN_IN_PATH = "/signin";
711
+ var DEFAULT_STATUS_PATH = "/oauth2/status";
712
+ var HTTP_UNAUTHORIZED = 401;
713
+ var activeGuard = null;
714
+ var requestUrl = (input, origin) => {
715
+ const raw = input instanceof Request ? input.url : String(input);
716
+ try {
717
+ return new URL(raw, origin);
718
+ } catch {
719
+ return null;
720
+ }
721
+ };
722
+ var buildSessionExpiredSignInUrl = ({
723
+ currentHref,
724
+ reason = DEFAULT_REASON,
725
+ reasonParam = DEFAULT_REASON_PARAM,
726
+ returnUrlParam = DEFAULT_RETURN_URL_PARAM,
727
+ signInPath = DEFAULT_SIGN_IN_PATH
728
+ }) => {
729
+ const current = new URL(currentHref);
730
+ const returnTo = `${current.pathname}${current.search}${current.hash}`;
731
+ const destination = new URL(signInPath, current.origin);
732
+ destination.searchParams.set(reasonParam, reason);
733
+ destination.searchParams.set(returnUrlParam, returnTo);
734
+ return destination.origin === current.origin ? `${destination.pathname}${destination.search}${destination.hash}` : destination.toString();
735
+ };
736
+ var installSessionExpiryGuard = (config = {}) => {
737
+ if (typeof window === "undefined" || typeof document === "undefined") {
738
+ return { check: async () => false, dispose: () => {
739
+ return;
740
+ } };
741
+ }
742
+ if (activeGuard)
743
+ return activeGuard;
744
+ const {
745
+ checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
746
+ isProtectedRequest,
747
+ onExpired,
748
+ protectedPaths = [],
749
+ reason = DEFAULT_REASON,
750
+ reasonParam = DEFAULT_REASON_PARAM,
751
+ returnUrlParam = DEFAULT_RETURN_URL_PARAM,
752
+ signInPath = DEFAULT_SIGN_IN_PATH,
753
+ statusPath = DEFAULT_STATUS_PATH
754
+ } = config;
755
+ const nativeFetch = window.fetch.bind(window);
756
+ const originalFetch = window.fetch;
757
+ let checking = false;
758
+ let disposed = false;
759
+ let expired = false;
760
+ let lastCheckedAt = Date.now();
761
+ const expire = () => {
762
+ if (expired || disposed)
763
+ return;
764
+ expired = true;
765
+ const currentHref = window.location.href;
766
+ const signInUrl = buildSessionExpiredSignInUrl({
767
+ currentHref,
768
+ reason,
769
+ reasonParam,
770
+ returnUrlParam,
771
+ signInPath
772
+ });
773
+ const current = new URL(currentHref);
774
+ const returnTo = `${current.pathname}${current.search}${current.hash}`;
775
+ if (onExpired) {
776
+ onExpired({ returnTo, signInUrl });
777
+ return;
725
778
  }
726
- return result;
727
- };
728
- const authenticatedFetch = async (input, init) => {
729
- const original = new Request(input, init);
730
- if (!allowedOrigins.has(new URL(original.url).origin))
731
- throw new MobileAuthError("origin", "Mobile auth refused to send a credential to an unregistered origin.");
732
- const send = async (forceRefresh) => {
733
- if (forceRefresh)
734
- access = undefined;
735
- const token = await refreshAccessToken();
736
- const request = original.clone();
737
- const headers = new Headers(request.headers);
738
- headers.set("authorization", `Bearer ${token}`);
739
- return fetchImpl(new Request(request, { credentials: "omit", headers }));
740
- };
741
- const response = await send(false);
742
- return response.status === 401 ? send(true) : response;
779
+ window.location.assign(signInUrl);
743
780
  };
744
- const optionalAuthenticatedFetch = async (input, init) => {
745
- const request = new Request(input, init);
746
- if (!allowedOrigins.has(new URL(request.url).origin))
747
- throw new MobileAuthError("origin", "Mobile auth refused to send a request outside an allowed origin.");
748
- const refreshToken = await config.storage.get(REFRESH_KEY);
749
- if (access || refreshToken)
750
- return authenticatedFetch(request);
751
- return fetchImpl(new Request(request, { credentials: "omit" }));
781
+ const protectedRequest = (input) => {
782
+ const url = requestUrl(input, window.location.origin);
783
+ if (!url || url.origin !== window.location.origin)
784
+ return false;
785
+ if (new URL(statusPath, window.location.origin).pathname === url.pathname)
786
+ return false;
787
+ return isProtectedRequest?.(url) === true || protectedPaths.some((path) => url.pathname.startsWith(path));
752
788
  };
753
- const status = async () => {
754
- try {
755
- const metadata = await discovery();
756
- const response = await authenticatedFetch(metadata.userinfo_endpoint);
757
- if (response.status === 401)
758
- return null;
759
- if (!response.ok)
760
- throw new MobileAuthError("network", `User info failed with HTTP ${response.status}.`);
761
- const user = await response.json();
762
- if (!isRecord(user) || typeof user.sub !== "string")
763
- throw new MobileAuthError("token", "The user-info response is malformed.");
764
- return { ...user, sub: user.sub };
765
- } catch (error) {
766
- if (error instanceof MobileAuthError && (error.code === "oauth" || error.code === "token"))
767
- return null;
768
- throw error;
789
+ const guardedFetch = new Proxy(originalFetch, {
790
+ apply: async (target, thisArg, args) => {
791
+ const [input] = args;
792
+ const response = await Reflect.apply(target, thisArg, args);
793
+ if (response.status === HTTP_UNAUTHORIZED && protectedRequest(input)) {
794
+ expire();
795
+ }
796
+ return response;
769
797
  }
770
- };
771
- const socketTicket = async (audience = resource) => {
772
- const metadata = await discovery();
773
- if (!metadata.socket_ticket_endpoint)
774
- throw new MobileAuthError("discovery", "The authorization server does not advertise WebSocket tickets.");
775
- const response = await authenticatedFetch(metadata.socket_ticket_endpoint, {
776
- body: JSON.stringify({ audience }),
777
- headers: { "content-type": "application/json" },
778
- method: "POST"
779
- });
780
- const body = await responseBody(response);
781
- if (!response.ok || !isRecord(body) || typeof body.ticket !== "string")
782
- throw new MobileAuthError(response.ok ? "token" : "oauth", `WebSocket ticket request failed with HTTP ${response.status}.`);
783
- return body.ticket;
784
- };
785
- const revokeRefreshToken = async (refreshToken) => {
786
- const metadata = await discovery();
787
- if (metadata.revocation_endpoint)
788
- await fetchImpl(metadata.revocation_endpoint, {
789
- body: new URLSearchParams({
790
- client_id: config.clientId,
791
- token: refreshToken,
792
- token_type_hint: "refresh_token"
793
- }),
794
- credentials: "omit",
795
- headers: {
796
- "content-type": "application/x-www-form-urlencoded"
797
- },
798
- method: "POST"
799
- });
800
- };
801
- const signOut = async () => {
802
- const refreshToken = await config.storage.get(REFRESH_KEY);
798
+ });
799
+ window.fetch = guardedFetch;
800
+ const check = async () => {
801
+ if (checking || expired || disposed)
802
+ return false;
803
+ checking = true;
804
+ lastCheckedAt = Date.now();
803
805
  try {
804
- if (refreshToken)
805
- await revokeRefreshToken(refreshToken);
806
+ const response = await nativeFetch(statusPath, {
807
+ cache: "no-store",
808
+ credentials: "include",
809
+ headers: { accept: "application/json" }
810
+ });
811
+ if (!response.ok)
812
+ return false;
813
+ const payload = await response.json();
814
+ const sessionExpired = typeof payload === "object" && payload !== null && Reflect.get(payload, "user") === null;
815
+ if (!sessionExpired)
816
+ return false;
817
+ expire();
818
+ return true;
819
+ } catch {
820
+ return false;
806
821
  } finally {
807
- access = undefined;
808
- await Promise.all([
809
- config.storage.remove(PENDING_KEY),
810
- config.storage.remove(REFRESH_KEY)
811
- ]);
822
+ checking = false;
812
823
  }
813
824
  };
814
- const stop = async () => {
815
- await Promise.all([stopLinks?.(), stopResume?.()]);
816
- stopLinks = undefined;
817
- stopResume = undefined;
818
- startPromise = undefined;
825
+ const checkIfDue = () => {
826
+ if (document.visibilityState !== "visible" || Date.now() - lastCheckedAt < checkIntervalMs)
827
+ return;
828
+ check();
819
829
  };
820
- return {
821
- fetch: authenticatedFetch,
822
- fetchOptional: optionalAuthenticatedFetch,
823
- handleCallback,
824
- refresh: refreshAccessToken,
825
- signIn,
826
- signOut,
827
- socketTicket,
828
- start,
829
- status,
830
- stop
830
+ const checkPersistedSession = (event) => {
831
+ if (!event.persisted)
832
+ return;
833
+ check();
834
+ };
835
+ const dispose = () => {
836
+ if (disposed)
837
+ return;
838
+ disposed = true;
839
+ document.removeEventListener("visibilitychange", checkIfDue);
840
+ window.removeEventListener("focus", checkIfDue);
841
+ window.removeEventListener("pageshow", checkPersistedSession);
842
+ if (window.fetch === guardedFetch)
843
+ window.fetch = originalFetch;
844
+ activeGuard = null;
831
845
  };
846
+ document.addEventListener("visibilitychange", checkIfDue);
847
+ window.addEventListener("focus", checkIfDue);
848
+ window.addEventListener("pageshow", checkPersistedSession);
849
+ activeGuard = { check, dispose };
850
+ return activeGuard;
851
+ };
852
+ var isProtectedSessionRequest = ({
853
+ input,
854
+ origin,
855
+ protectedPaths
856
+ }) => {
857
+ const url = requestUrl(input, origin);
858
+ return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
832
859
  };
833
- var createMobileAuthTransport = (client) => ({
834
- fetch: client.fetch,
835
- signInEmail: async ({ email }) => {
836
- await client.signIn({
837
- authorizationParameters: { login_hint: email }
838
- });
839
- return { status: "authenticated" };
840
- },
841
- signOut: async () => {
842
- await client.signOut();
843
- return null;
844
- },
845
- signUpEmail: async ({ email }) => {
846
- await client.signIn({
847
- authorizationParameters: {
848
- login_hint: email,
849
- screen_hint: "signup"
850
- }
851
- });
852
- return { status: "authenticated" };
853
- },
854
- status: async () => {
855
- const user = await client.status();
856
- return { user };
857
- }
858
- });
859
860
  // src/redirect.ts
860
861
  var isSafeLocalPath = (value) => /^\/(?![/\\])/.test(value);
861
862
  var toSafeLocalPath = (value, fallback = "/") => value !== undefined && isSafeLocalPath(value) ? value : fallback;
@@ -921,5 +922,5 @@ export {
921
922
  MobileAuthError
922
923
  };
923
924
 
924
- //# debugId=1DC9AFB1303049A564756E2164756E21
925
+ //# debugId=E661DCC8D887DF4364756E2164756E21
925
926
  //# sourceMappingURL=index.js.map