@absolutejs/auth 0.69.0 → 0.69.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.
@@ -46,396 +46,130 @@ 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"
72
- };
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
49
+ // src/client/runtimeTransport.ts
50
+ var AUTH_TRANSPORT_REGISTRY = Symbol.for("@absolutejs/auth/client-runtime-transport");
51
+ var host = globalThis;
52
+ var isRegistry = (value) => typeof value === "object" && value !== null && Array.isArray(Reflect.get(value, "installations"));
53
+ var registry = (() => {
54
+ const existing = host[AUTH_TRANSPORT_REGISTRY];
55
+ if (isRegistry(existing))
56
+ return existing;
57
+ const created = { installations: [] };
58
+ Object.defineProperty(host, AUTH_TRANSPORT_REGISTRY, {
59
+ configurable: false,
60
+ enumerable: false,
61
+ value: created,
62
+ writable: false
130
63
  });
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)
64
+ return created;
65
+ })();
66
+ var getAuthClientRuntimeTransport = () => registry.installations.at(-1)?.transport;
67
+ var installAuthClientRuntimeTransport = (transport) => {
68
+ const installation = { transport };
69
+ registry.installations.push(installation);
70
+ return () => {
71
+ const index = registry.installations.indexOf(installation);
72
+ if (index >= 0)
73
+ registry.installations.splice(index, 1);
173
74
  };
174
75
  };
175
- var safeJson = (text) => {
76
+
77
+ // src/client/mobile.ts
78
+ class MobileAuthError extends Error {
79
+ code;
80
+ cause;
81
+ constructor(code, message, options) {
82
+ super(message);
83
+ this.name = "MobileAuthError";
84
+ this.code = code;
85
+ this.cause = options?.cause;
86
+ }
87
+ }
88
+ var PENDING_KEY = "oidc.pending";
89
+ var REFRESH_KEY = "oidc.refresh";
90
+ var DEFAULT_CLOCK_SKEW_MS = 30000;
91
+ var PENDING_TTL_MS = 10 * 60000;
92
+ var RANDOM_BYTES = 32;
93
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
94
+ var base64Url = (value) => {
95
+ let binary = "";
96
+ for (const byte of value)
97
+ binary += String.fromCharCode(byte);
98
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
99
+ };
100
+ var decodeBase64Url = (value) => {
101
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
102
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
103
+ const binary = atob(padded);
104
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
105
+ };
106
+ var randomValue = () => {
107
+ const value = new Uint8Array(RANDOM_BYTES);
108
+ crypto.getRandomValues(value);
109
+ return base64Url(value);
110
+ };
111
+ var pkceChallenge = async (verifier) => base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
112
+ var normalizeIssuer = (value) => {
113
+ const issuer = new URL(value);
114
+ const loopback = issuer.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(issuer.hostname);
115
+ if (issuer.protocol !== "https:" && !loopback)
116
+ throw new TypeError("Mobile auth issuer must use HTTPS.");
117
+ if (issuer.username || issuer.password || issuer.search || issuer.hash)
118
+ throw new TypeError("Mobile auth issuer cannot contain credentials, query, or fragment.");
119
+ issuer.pathname = issuer.pathname.replace(/\/$/u, "");
120
+ return issuer.href.replace(/\/$/u, "");
121
+ };
122
+ var exactRedirect = (actualValue, expectedValue) => {
123
+ const actual = new URL(actualValue);
124
+ const expected = new URL(expectedValue);
125
+ return actual.protocol === expected.protocol && actual.host === expected.host && actual.pathname === expected.pathname && actual.username === "" && actual.password === "";
126
+ };
127
+ var parsePending = (value) => {
128
+ if (value === null)
129
+ return;
176
130
  try {
177
- return JSON.parse(text);
131
+ const parsed = JSON.parse(value);
132
+ if (!isRecord(parsed) || typeof parsed.createdAt !== "number" || typeof parsed.nonce !== "string" || typeof parsed.state !== "string" || typeof parsed.verifier !== "string")
133
+ return;
134
+ return {
135
+ createdAt: parsed.createdAt,
136
+ nonce: parsed.nonce,
137
+ state: parsed.state,
138
+ verifier: parsed.verifier
139
+ };
178
140
  } catch {
179
- return text;
141
+ return;
180
142
  }
181
143
  };
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;
144
+ var parseTokenResponse = (value) => {
145
+ 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")
146
+ throw new MobileAuthError("token", "The token response is malformed.");
147
+ if (value.token_type.toLowerCase() !== "bearer")
148
+ throw new MobileAuthError("token", `Unsupported mobile token type ${value.token_type}; DPoP is not enabled for this client.`);
149
+ return {
150
+ access_token: value.access_token,
151
+ expires_in: value.expires_in,
152
+ id_token: value.id_token,
153
+ refresh_token: value.refresh_token,
154
+ scope: typeof value.scope === "string" ? value.scope : undefined,
155
+ token_type: value.token_type
156
+ };
187
157
  };
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);
158
+ var responseBody = async (response) => {
159
+ const text = await response.text();
199
160
  try {
200
- return new URL(raw, origin);
161
+ return JSON.parse(text);
201
162
  } catch {
202
- return null;
163
+ return text;
203
164
  }
204
165
  };
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;
166
+ var requireEndpoint = (value, name, issuer) => {
167
+ if (typeof value !== "string")
168
+ throw new MobileAuthError("discovery", `OIDC discovery is missing ${name}.`);
169
+ const endpoint = new URL(value);
170
+ if (endpoint.protocol !== "https:" && new URL(issuer).protocol === "https:")
171
+ throw new MobileAuthError("discovery", `OIDC discovery ${name} must use HTTPS.`);
172
+ return endpoint.href;
439
173
  };
440
174
  var parseDiscovery = (value, issuer) => {
441
175
  if (!isRecord(value) || value.issuer !== issuer)
@@ -831,7 +565,7 @@ var createMobileAuthClient = (config) => {
831
565
  };
832
566
  };
833
567
  var createMobileAuthTransport = (client) => ({
834
- fetch: client.fetch,
568
+ fetch: client.fetchOptional,
835
569
  signInEmail: async ({ email }) => {
836
570
  await client.signIn({
837
571
  authorizationParameters: { login_hint: email }
@@ -856,6 +590,302 @@ var createMobileAuthTransport = (client) => ({
856
590
  return { user };
857
591
  }
858
592
  });
593
+
594
+ // src/client/createAuthClient.ts
595
+ var DEFAULT_ROUTES = {
596
+ emailVerify: "/auth/verify-email",
597
+ emailVerifyRequest: "/auth/verify-email/request",
598
+ login: "/auth/login",
599
+ magicLinkRequest: "/auth/passwordless/magic-link",
600
+ magicLinkVerify: "/auth/passwordless/magic-link/verify",
601
+ mfaChallenge: "/auth/mfa/totp/challenge",
602
+ mfaManagement: "/auth/mfa",
603
+ mfaSetup: "/auth/mfa/totp/setup",
604
+ mfaVerifySetup: "/auth/mfa/totp/verify",
605
+ passkeyAuthenticateOptions: "/auth/webauthn/authenticate/options",
606
+ passkeyAuthenticateVerify: "/auth/webauthn/authenticate/verify",
607
+ passkeyList: "/auth/webauthn/credentials",
608
+ passkeyRegisterOptions: "/auth/webauthn/register/options",
609
+ passkeyRegisterVerify: "/auth/webauthn/register/verify",
610
+ passkeyRemove: "/auth/webauthn/credentials",
611
+ passwordReset: "/auth/reset-password",
612
+ passwordResetRequest: "/auth/reset-password/request",
613
+ register: "/auth/register",
614
+ sessions: "/auth/sessions",
615
+ signout: "/oauth2/signout",
616
+ status: "/oauth2/status"
617
+ };
618
+ var succeed = (data) => ({
619
+ data,
620
+ error: null
621
+ });
622
+ var fail = (error) => ({
623
+ data: null,
624
+ error
625
+ });
626
+ var errorFor = (response, body) => ({
627
+ body,
628
+ message: typeof body === "string" ? body : readMessage(body) ?? response.statusText,
629
+ status: response.status
630
+ });
631
+ var createAuthClient = ({
632
+ baseUrl = "",
633
+ credentials = "same-origin",
634
+ fetch: fetchImpl = fetch,
635
+ routes,
636
+ transport: configuredTransport
637
+ } = {}) => {
638
+ const transport = configuredTransport ?? getAuthClientRuntimeTransport();
639
+ const resolvedFetch = transport?.fetch ?? fetchImpl;
640
+ const signInEmail = transport?.signInEmail;
641
+ const signOut = transport?.signOut;
642
+ const signUpEmail = transport?.signUpEmail;
643
+ const transportStatus = transport?.status;
644
+ const resolvedRoutes = {
645
+ ...DEFAULT_ROUTES,
646
+ ...routes
647
+ };
648
+ const request = async (path, init) => {
649
+ try {
650
+ const response = await resolvedFetch(`${baseUrl}${path}`, {
651
+ credentials,
652
+ ...init
653
+ });
654
+ const text = await response.text();
655
+ if (!response.ok)
656
+ return fail(errorFor(response, safeJson(text)));
657
+ const data = JSON.parse(text === "" ? "null" : text);
658
+ return succeed(data);
659
+ } catch (caught) {
660
+ const message = caught instanceof Error ? caught.message : "network";
661
+ return fail({ body: null, message, status: 0 });
662
+ }
663
+ };
664
+ const runTransport = async (operation) => {
665
+ try {
666
+ return succeed(await operation());
667
+ } catch (caught) {
668
+ const message = caught instanceof Error ? caught.message : "authentication";
669
+ return fail({ body: null, message, status: 0 });
670
+ }
671
+ };
672
+ const post = (path, body, method = "POST") => request(path, {
673
+ body: body === undefined ? undefined : JSON.stringify(body),
674
+ headers: body === undefined ? undefined : { "content-type": "application/json" },
675
+ method
676
+ });
677
+ const get = (path) => request(path, { method: "GET" });
678
+ const del = (path) => request(path, { method: "DELETE" });
679
+ return {
680
+ emailVerification: {
681
+ request: (body) => post(resolvedRoutes.emailVerifyRequest, body),
682
+ verify: (body) => post(resolvedRoutes.emailVerify, body)
683
+ },
684
+ mfa: {
685
+ challenge: (body) => post(resolvedRoutes.mfaChallenge, body),
686
+ disable: () => del(resolvedRoutes.mfaManagement),
687
+ setup: () => post(resolvedRoutes.mfaSetup),
688
+ status: () => get(resolvedRoutes.mfaManagement),
689
+ verifySetup: (body) => post(resolvedRoutes.mfaVerifySetup, body)
690
+ },
691
+ passkeys: {
692
+ authenticateOptions: () => post(resolvedRoutes.passkeyAuthenticateOptions),
693
+ authenticateVerify: (response) => post(resolvedRoutes.passkeyAuthenticateVerify, response),
694
+ list: () => get(resolvedRoutes.passkeyList),
695
+ registerOptions: () => post(resolvedRoutes.passkeyRegisterOptions),
696
+ registerVerify: (response) => post(resolvedRoutes.passkeyRegisterVerify, response),
697
+ remove: (credentialId) => del(`${resolvedRoutes.passkeyRemove}/${encodeURIComponent(credentialId)}`)
698
+ },
699
+ passwordless: {
700
+ requestMagicLink: (body) => post(resolvedRoutes.magicLinkRequest, body),
701
+ verifyMagicLink: (body) => post(resolvedRoutes.magicLinkVerify, body)
702
+ },
703
+ passwordReset: {
704
+ confirm: (body) => post(resolvedRoutes.passwordReset, body),
705
+ request: (body) => post(resolvedRoutes.passwordResetRequest, body)
706
+ },
707
+ sessions: {
708
+ list: () => get(resolvedRoutes.sessions),
709
+ revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
710
+ },
711
+ signIn: {
712
+ email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
713
+ },
714
+ signUp: {
715
+ email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
716
+ },
717
+ signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
718
+ status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
719
+ };
720
+ };
721
+ var safeJson = (text) => {
722
+ try {
723
+ return JSON.parse(text);
724
+ } catch {
725
+ return text;
726
+ }
727
+ };
728
+ var readMessage = (body) => {
729
+ if (typeof body !== "object" || body === null)
730
+ return;
731
+ const message = Reflect.get(body, "message");
732
+ return typeof message === "string" ? message : undefined;
733
+ };
734
+ // src/client/sessionExpiry.ts
735
+ var DEFAULT_CHECK_INTERVAL_MS = 30000;
736
+ var DEFAULT_REASON = "session_expired";
737
+ var DEFAULT_REASON_PARAM = "reason";
738
+ var DEFAULT_RETURN_URL_PARAM = "returnUrl";
739
+ var DEFAULT_SIGN_IN_PATH = "/signin";
740
+ var DEFAULT_STATUS_PATH = "/oauth2/status";
741
+ var HTTP_UNAUTHORIZED = 401;
742
+ var activeGuard = null;
743
+ var requestUrl = (input, origin) => {
744
+ const raw = input instanceof Request ? input.url : String(input);
745
+ try {
746
+ return new URL(raw, origin);
747
+ } catch {
748
+ return null;
749
+ }
750
+ };
751
+ var buildSessionExpiredSignInUrl = ({
752
+ currentHref,
753
+ reason = DEFAULT_REASON,
754
+ reasonParam = DEFAULT_REASON_PARAM,
755
+ returnUrlParam = DEFAULT_RETURN_URL_PARAM,
756
+ signInPath = DEFAULT_SIGN_IN_PATH
757
+ }) => {
758
+ const current = new URL(currentHref);
759
+ const returnTo = `${current.pathname}${current.search}${current.hash}`;
760
+ const destination = new URL(signInPath, current.origin);
761
+ destination.searchParams.set(reasonParam, reason);
762
+ destination.searchParams.set(returnUrlParam, returnTo);
763
+ return destination.origin === current.origin ? `${destination.pathname}${destination.search}${destination.hash}` : destination.toString();
764
+ };
765
+ var installSessionExpiryGuard = (config = {}) => {
766
+ if (typeof window === "undefined" || typeof document === "undefined") {
767
+ return { check: async () => false, dispose: () => {
768
+ return;
769
+ } };
770
+ }
771
+ if (activeGuard)
772
+ return activeGuard;
773
+ const {
774
+ checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS,
775
+ isProtectedRequest,
776
+ onExpired,
777
+ protectedPaths = [],
778
+ reason = DEFAULT_REASON,
779
+ reasonParam = DEFAULT_REASON_PARAM,
780
+ returnUrlParam = DEFAULT_RETURN_URL_PARAM,
781
+ signInPath = DEFAULT_SIGN_IN_PATH,
782
+ statusPath = DEFAULT_STATUS_PATH
783
+ } = config;
784
+ const nativeFetch = window.fetch.bind(window);
785
+ const originalFetch = window.fetch;
786
+ let checking = false;
787
+ let disposed = false;
788
+ let expired = false;
789
+ let lastCheckedAt = Date.now();
790
+ const expire = () => {
791
+ if (expired || disposed)
792
+ return;
793
+ expired = true;
794
+ const currentHref = window.location.href;
795
+ const signInUrl = buildSessionExpiredSignInUrl({
796
+ currentHref,
797
+ reason,
798
+ reasonParam,
799
+ returnUrlParam,
800
+ signInPath
801
+ });
802
+ const current = new URL(currentHref);
803
+ const returnTo = `${current.pathname}${current.search}${current.hash}`;
804
+ if (onExpired) {
805
+ onExpired({ returnTo, signInUrl });
806
+ return;
807
+ }
808
+ window.location.assign(signInUrl);
809
+ };
810
+ const protectedRequest = (input) => {
811
+ const url = requestUrl(input, window.location.origin);
812
+ if (!url || url.origin !== window.location.origin)
813
+ return false;
814
+ if (new URL(statusPath, window.location.origin).pathname === url.pathname)
815
+ return false;
816
+ return isProtectedRequest?.(url) === true || protectedPaths.some((path) => url.pathname.startsWith(path));
817
+ };
818
+ const guardedFetch = new Proxy(originalFetch, {
819
+ apply: async (target, thisArg, args) => {
820
+ const [input] = args;
821
+ const response = await Reflect.apply(target, thisArg, args);
822
+ if (response.status === HTTP_UNAUTHORIZED && protectedRequest(input)) {
823
+ expire();
824
+ }
825
+ return response;
826
+ }
827
+ });
828
+ window.fetch = guardedFetch;
829
+ const check = async () => {
830
+ if (checking || expired || disposed)
831
+ return false;
832
+ checking = true;
833
+ lastCheckedAt = Date.now();
834
+ try {
835
+ const response = await nativeFetch(statusPath, {
836
+ cache: "no-store",
837
+ credentials: "include",
838
+ headers: { accept: "application/json" }
839
+ });
840
+ if (!response.ok)
841
+ return false;
842
+ const payload = await response.json();
843
+ const sessionExpired = typeof payload === "object" && payload !== null && Reflect.get(payload, "user") === null;
844
+ if (!sessionExpired)
845
+ return false;
846
+ expire();
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ } finally {
851
+ checking = false;
852
+ }
853
+ };
854
+ const checkIfDue = () => {
855
+ if (document.visibilityState !== "visible" || Date.now() - lastCheckedAt < checkIntervalMs)
856
+ return;
857
+ check();
858
+ };
859
+ const checkPersistedSession = (event) => {
860
+ if (!event.persisted)
861
+ return;
862
+ check();
863
+ };
864
+ const dispose = () => {
865
+ if (disposed)
866
+ return;
867
+ disposed = true;
868
+ document.removeEventListener("visibilitychange", checkIfDue);
869
+ window.removeEventListener("focus", checkIfDue);
870
+ window.removeEventListener("pageshow", checkPersistedSession);
871
+ if (window.fetch === guardedFetch)
872
+ window.fetch = originalFetch;
873
+ activeGuard = null;
874
+ };
875
+ document.addEventListener("visibilitychange", checkIfDue);
876
+ window.addEventListener("focus", checkIfDue);
877
+ window.addEventListener("pageshow", checkPersistedSession);
878
+ activeGuard = { check, dispose };
879
+ return activeGuard;
880
+ };
881
+ var isProtectedSessionRequest = ({
882
+ input,
883
+ origin,
884
+ protectedPaths
885
+ }) => {
886
+ const url = requestUrl(input, origin);
887
+ return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
888
+ };
859
889
  // src/redirect.ts
860
890
  var isSafeLocalPath = (value) => /^\/(?![/\\])/.test(value);
861
891
  var toSafeLocalPath = (value, fallback = "/") => value !== undefined && isSafeLocalPath(value) ? value : fallback;
@@ -914,6 +944,8 @@ export {
914
944
  isSafeLocalPath,
915
945
  isProtectedSessionRequest,
916
946
  installSessionExpiryGuard,
947
+ installAuthClientRuntimeTransport,
948
+ getAuthClientRuntimeTransport,
917
949
  createMobileAuthTransport,
918
950
  createMobileAuthClient,
919
951
  createAuthClient,
@@ -921,5 +953,5 @@ export {
921
953
  MobileAuthError
922
954
  };
923
955
 
924
- //# debugId=1DC9AFB1303049A564756E2164756E21
956
+ //# debugId=45D0F7CD268E6FD764756E2164756E21
925
957
  //# sourceMappingURL=index.js.map