@absolutejs/auth 0.68.2 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,10 +37,36 @@ export type AuthClientRoutes = {
37
37
  export type AuthClientConfig = {
38
38
  baseUrl?: string;
39
39
  credentials?: RequestCredentials;
40
- fetch?: typeof fetch;
40
+ fetch?: AuthClientFetch;
41
41
  routes?: AuthClientRoutes;
42
+ transport?: AuthClientTransport;
42
43
  };
43
- export declare const createAuthClient: ({ baseUrl, credentials, fetch: fetchImpl, routes }?: AuthClientConfig) => {
44
+ export type AuthClientFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
45
+ export type AuthClientTransport = {
46
+ fetch?: AuthClientFetch;
47
+ signInEmail?: (body: {
48
+ email: string;
49
+ password: string;
50
+ }) => Promise<{
51
+ passwordCompromised?: boolean;
52
+ status: 'authenticated' | 'mfa_required';
53
+ }>;
54
+ signOut?: () => Promise<null>;
55
+ signUpEmail?: (body: {
56
+ email: string;
57
+ password: string;
58
+ [extra: string]: unknown;
59
+ }) => Promise<{
60
+ status: 'authenticated';
61
+ } | {
62
+ status: 'verification_required';
63
+ }>;
64
+ status?: () => Promise<{
65
+ impersonator?: unknown;
66
+ user: unknown | null;
67
+ }>;
68
+ };
69
+ export declare const createAuthClient: ({ baseUrl, credentials, fetch: fetchImpl, routes, transport }?: AuthClientConfig) => {
44
70
  emailVerification: {
45
71
  request: (body: {
46
72
  email: string;
@@ -1,4 +1,5 @@
1
1
  export * from './createAuthClient';
2
2
  export * from './sessionExpiry';
3
+ export * from './mobile';
3
4
  export * from '../redirect';
4
5
  export { runConditionalAuthentication, runPasskeyRegistration } from './passkeyHelpers';
@@ -87,15 +87,21 @@ var createAuthClient = ({
87
87
  baseUrl = "",
88
88
  credentials = "same-origin",
89
89
  fetch: fetchImpl = fetch,
90
- routes
90
+ routes,
91
+ transport
91
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;
92
98
  const resolvedRoutes = {
93
99
  ...DEFAULT_ROUTES,
94
100
  ...routes
95
101
  };
96
102
  const request = async (path, init) => {
97
103
  try {
98
- const response = await fetchImpl(`${baseUrl}${path}`, {
104
+ const response = await resolvedFetch(`${baseUrl}${path}`, {
99
105
  credentials,
100
106
  ...init
101
107
  });
@@ -109,6 +115,14 @@ var createAuthClient = ({
109
115
  return fail({ body: null, message, status: 0 });
110
116
  }
111
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
+ };
112
126
  const post = (path, body, method = "POST") => request(path, {
113
127
  body: body === undefined ? undefined : JSON.stringify(body),
114
128
  headers: body === undefined ? undefined : { "content-type": "application/json" },
@@ -149,13 +163,13 @@ var createAuthClient = ({
149
163
  revoke: (sessionId) => del(`${resolvedRoutes.sessions}/${encodeURIComponent(sessionId)}`)
150
164
  },
151
165
  signIn: {
152
- email: (body) => post(resolvedRoutes.login, body)
166
+ email: (body) => signInEmail ? runTransport(() => signInEmail(body)) : post(resolvedRoutes.login, body)
153
167
  },
154
168
  signUp: {
155
- email: (body) => post(resolvedRoutes.register, body)
169
+ email: (body) => signUpEmail ? runTransport(() => signUpEmail(body)) : post(resolvedRoutes.register, body)
156
170
  },
157
- signOut: () => del(resolvedRoutes.signout),
158
- status: () => get(resolvedRoutes.status)
171
+ signOut: () => signOut ? runTransport(() => signOut()) : del(resolvedRoutes.signout),
172
+ status: () => transportStatus ? runTransport(() => transportStatus()) : get(resolvedRoutes.status)
159
173
  };
160
174
  };
161
175
  var safeJson = (text) => {
@@ -326,6 +340,522 @@ var isProtectedSessionRequest = ({
326
340
  const url = requestUrl(input, origin);
327
341
  return url?.origin === origin && protectedPaths.some((path) => url.pathname.startsWith(path));
328
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;
439
+ };
440
+ var parseDiscovery = (value, issuer) => {
441
+ if (!isRecord(value) || value.issuer !== issuer)
442
+ throw new MobileAuthError("discovery", "OIDC discovery issuer does not match the configured issuer.");
443
+ const methods = Array.isArray(value.code_challenge_methods_supported) ? value.code_challenge_methods_supported.filter((method) => typeof method === "string") : undefined;
444
+ if (!methods?.includes("S256"))
445
+ throw new MobileAuthError("discovery", "OIDC provider does not advertise S256 PKCE.");
446
+ const authMethods = Array.isArray(value.token_endpoint_auth_methods_supported) ? value.token_endpoint_auth_methods_supported.filter((method) => typeof method === "string") : undefined;
447
+ if (!authMethods?.includes("none"))
448
+ throw new MobileAuthError("discovery", "OIDC provider does not accept public clients at the token endpoint.");
449
+ return {
450
+ authorization_endpoint: requireEndpoint(value.authorization_endpoint, "authorization_endpoint", issuer),
451
+ code_challenge_methods_supported: methods,
452
+ issuer,
453
+ jwks_uri: requireEndpoint(value.jwks_uri, "jwks_uri", issuer),
454
+ revocation_endpoint: typeof value.revocation_endpoint === "string" ? requireEndpoint(value.revocation_endpoint, "revocation_endpoint", issuer) : undefined,
455
+ socket_ticket_endpoint: typeof value.socket_ticket_endpoint === "string" ? requireEndpoint(value.socket_ticket_endpoint, "socket_ticket_endpoint", issuer) : undefined,
456
+ token_endpoint: requireEndpoint(value.token_endpoint, "token_endpoint", issuer),
457
+ token_endpoint_auth_methods_supported: authMethods,
458
+ userinfo_endpoint: requireEndpoint(value.userinfo_endpoint, "userinfo_endpoint", issuer)
459
+ };
460
+ };
461
+ var verifyIdToken = async ({
462
+ clientId,
463
+ fetchImpl,
464
+ idToken,
465
+ issuer,
466
+ jwksUri,
467
+ nonce,
468
+ now
469
+ }) => {
470
+ const [encodedHeader, encodedPayload, encodedSignature, ...extra] = idToken.split(".");
471
+ if (!encodedHeader || !encodedPayload || !encodedSignature || extra.length)
472
+ throw new MobileAuthError("id-token", "The ID token is malformed.");
473
+ let header;
474
+ let payload;
475
+ try {
476
+ header = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedHeader)));
477
+ payload = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedPayload)));
478
+ } catch (cause) {
479
+ throw new MobileAuthError("id-token", "The ID token is malformed.", {
480
+ cause
481
+ });
482
+ }
483
+ if (!isRecord(header) || header.alg !== "ES256" || typeof header.kid !== "string" || !isRecord(payload))
484
+ throw new MobileAuthError("id-token", "The ID token header or claims are invalid.");
485
+ const response = await fetchImpl(jwksUri, { cache: "no-store" });
486
+ if (!response.ok)
487
+ throw new MobileAuthError("network", "Unable to fetch OIDC signing keys.");
488
+ const body = await response.json();
489
+ const keys = isRecord(body) && Array.isArray(body.keys) ? body.keys : [];
490
+ const jwk = keys.find((candidate) => isRecord(candidate) && candidate.kid === header.kid);
491
+ if (!jwk)
492
+ throw new MobileAuthError("id-token", "The ID token signing key is unknown.");
493
+ const key = await crypto.subtle.importKey("jwk", jwk, { hash: "SHA-256", name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
494
+ const valid = await crypto.subtle.verify({ hash: "SHA-256", name: "ECDSA" }, key, decodeBase64Url(encodedSignature), new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`));
495
+ const audience = payload.aud;
496
+ if (!valid || payload.iss !== issuer || !(audience === clientId || Array.isArray(audience) && audience.includes(clientId)) || payload.nonce !== nonce || typeof payload.exp !== "number" || payload.exp * 1000 <= now)
497
+ throw new MobileAuthError("id-token", "The ID token signature or claims are invalid.");
498
+ };
499
+ var createMobileAuthClient = (config) => {
500
+ const issuer = normalizeIssuer(config.issuer);
501
+ const redirectUri = new URL(config.redirectUri).href;
502
+ const fetchImpl = config.fetch ?? globalThis.fetch;
503
+ const now = config.now ?? Date.now;
504
+ const clockSkewMs = config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS;
505
+ const scopes = [...new Set(config.scopes ?? ["openid", "profile"])];
506
+ const resource = config.resource ?? issuer;
507
+ if (!scopes.includes("openid"))
508
+ scopes.unshift("openid");
509
+ const allowedOrigins = new Set((config.allowedOrigins ?? [new URL(issuer).origin]).map((value) => new URL(value).origin));
510
+ let discoveryPromise;
511
+ let access;
512
+ let refreshPromise;
513
+ let stopLinks;
514
+ let stopResume;
515
+ let startPromise;
516
+ const pendingSignIns = new Map;
517
+ const fetchDiscovery = async () => {
518
+ const url = new URL("/.well-known/openid-configuration", `${issuer}/`);
519
+ const response = await fetchImpl(url, {
520
+ cache: "no-store",
521
+ headers: { accept: "application/json" }
522
+ });
523
+ if (!response.ok)
524
+ throw new MobileAuthError("discovery", `OIDC discovery failed with HTTP ${response.status}.`);
525
+ return parseDiscovery(await response.json(), issuer);
526
+ };
527
+ const discovery = () => {
528
+ discoveryPromise ??= fetchDiscovery();
529
+ return discoveryPromise;
530
+ };
531
+ const assertSecureStorage = async () => {
532
+ const capability = await config.storage.capability?.();
533
+ if (capability && !capability.available)
534
+ throw new MobileAuthError("secure-storage", capability.message ?? "Native secure credential storage is unavailable.");
535
+ };
536
+ const tokenRequest = async (metadata, body, nonce) => {
537
+ let response;
538
+ try {
539
+ response = await fetchImpl(metadata.token_endpoint, {
540
+ body,
541
+ credentials: "omit",
542
+ headers: {
543
+ accept: "application/json",
544
+ "content-type": "application/x-www-form-urlencoded"
545
+ },
546
+ method: "POST"
547
+ });
548
+ } catch (cause) {
549
+ throw new MobileAuthError("network", "The token request failed.", {
550
+ cause
551
+ });
552
+ }
553
+ const value = await responseBody(response);
554
+ if (!response.ok) {
555
+ const oauthCode = isRecord(value) && typeof value.error === "string" ? value.error : `HTTP ${response.status}`;
556
+ throw new MobileAuthError("oauth", `The authorization server rejected the token request (${oauthCode}).`);
557
+ }
558
+ const tokens = parseTokenResponse(value);
559
+ if (nonce)
560
+ await verifyIdToken({
561
+ clientId: config.clientId,
562
+ fetchImpl,
563
+ idToken: tokens.id_token,
564
+ issuer,
565
+ jwksUri: metadata.jwks_uri,
566
+ nonce,
567
+ now: now()
568
+ });
569
+ return tokens;
570
+ };
571
+ const acceptTokens = async (tokens) => {
572
+ try {
573
+ await config.storage.set(REFRESH_KEY, tokens.refresh_token);
574
+ } catch (cause) {
575
+ access = undefined;
576
+ await config.storage.remove(REFRESH_KEY).catch(() => {
577
+ return;
578
+ });
579
+ throw new MobileAuthError("secure-storage", "The rotated refresh credential could not be saved securely.", { cause });
580
+ }
581
+ access = {
582
+ accessToken: tokens.access_token,
583
+ expiresAt: now() + tokens.expires_in * 1000,
584
+ idToken: tokens.id_token,
585
+ scope: tokens.scope?.split(" ").filter(Boolean) ?? scopes,
586
+ tokenType: "Bearer"
587
+ };
588
+ return { ...access, refreshToken: tokens.refresh_token };
589
+ };
590
+ const rotateAccessToken = async () => {
591
+ await assertSecureStorage();
592
+ const refreshToken = await config.storage.get(REFRESH_KEY);
593
+ if (!refreshToken)
594
+ throw new MobileAuthError("token", "This app has no renewable mobile session.");
595
+ const metadata = await discovery();
596
+ const body = new URLSearchParams({
597
+ client_id: config.clientId,
598
+ grant_type: "refresh_token",
599
+ refresh_token: refreshToken
600
+ });
601
+ body.set("resource", resource);
602
+ const tokens = await tokenRequest(metadata, body);
603
+ return (await acceptTokens(tokens)).accessToken;
604
+ };
605
+ const refreshAccessToken = async () => {
606
+ if (access && access.expiresAt - clockSkewMs > now())
607
+ return access.accessToken;
608
+ if (refreshPromise)
609
+ return refreshPromise;
610
+ refreshPromise = rotateAccessToken().finally(() => {
611
+ refreshPromise = undefined;
612
+ });
613
+ return refreshPromise;
614
+ };
615
+ const handleCallback = async (value) => {
616
+ if (!exactRedirect(value, redirectUri))
617
+ throw new MobileAuthError("callback", "The authorization callback does not match this app registration.");
618
+ const url = new URL(value);
619
+ const pending = parsePending(await config.storage.get(PENDING_KEY));
620
+ if (!pending || now() - pending.createdAt > PENDING_TTL_MS) {
621
+ await config.storage.remove(PENDING_KEY).catch(() => {
622
+ return;
623
+ });
624
+ throw new MobileAuthError("callback", "The authorization transaction is missing or expired.");
625
+ }
626
+ const deferred = pendingSignIns.get(pending.state);
627
+ const reject = (error) => {
628
+ pendingSignIns.delete(pending.state);
629
+ config.storage.remove(PENDING_KEY);
630
+ deferred?.reject(error);
631
+ throw error;
632
+ };
633
+ if (url.searchParams.get("state") !== pending.state)
634
+ return reject(new MobileAuthError("callback", "The authorization callback state does not match."));
635
+ if (url.searchParams.get("iss") !== issuer)
636
+ return reject(new MobileAuthError("callback", "The authorization callback issuer does not match."));
637
+ const oauthError = url.searchParams.get("error");
638
+ if (oauthError) {
639
+ await config.storage.remove(PENDING_KEY);
640
+ return reject(new MobileAuthError("oauth", `Authorization failed (${oauthError}).`));
641
+ }
642
+ const code = url.searchParams.get("code");
643
+ if (!code)
644
+ return reject(new MobileAuthError("callback", "The authorization callback contains no code."));
645
+ await config.storage.remove(PENDING_KEY);
646
+ try {
647
+ const metadata = await discovery();
648
+ const body = new URLSearchParams({
649
+ client_id: config.clientId,
650
+ code,
651
+ code_verifier: pending.verifier,
652
+ grant_type: "authorization_code",
653
+ redirect_uri: redirectUri
654
+ });
655
+ body.set("resource", resource);
656
+ const tokens = await acceptTokens(await tokenRequest(metadata, body, pending.nonce));
657
+ pendingSignIns.delete(pending.state);
658
+ deferred?.resolve(tokens);
659
+ return tokens;
660
+ } catch (error) {
661
+ return reject(error);
662
+ }
663
+ };
664
+ const initialize = async () => {
665
+ await assertSecureStorage();
666
+ stopLinks = await config.links.onOpen((url) => {
667
+ if (exactRedirect(url, redirectUri))
668
+ handleCallback(url).catch(() => {
669
+ return;
670
+ });
671
+ });
672
+ if (config.lifecycle?.onResume)
673
+ stopResume = await config.lifecycle.onResume(() => {
674
+ refreshAccessToken().catch(() => {
675
+ return;
676
+ });
677
+ });
678
+ const launchUrl = await config.links.getLaunchUrl();
679
+ if (launchUrl && exactRedirect(launchUrl, redirectUri))
680
+ await handleCallback(launchUrl);
681
+ };
682
+ const start = () => {
683
+ startPromise ??= initialize();
684
+ return startPromise;
685
+ };
686
+ const signIn = async (options = {}) => {
687
+ await start();
688
+ if (options.signal?.aborted)
689
+ throw new MobileAuthError("aborted", "Authorization was cancelled.");
690
+ const metadata = await discovery();
691
+ const pending = {
692
+ createdAt: now(),
693
+ nonce: randomValue(),
694
+ state: randomValue(),
695
+ verifier: randomValue()
696
+ };
697
+ await config.storage.set(PENDING_KEY, JSON.stringify(pending));
698
+ const url = new URL(metadata.authorization_endpoint);
699
+ url.search = new URLSearchParams({
700
+ client_id: config.clientId,
701
+ code_challenge: await pkceChallenge(pending.verifier),
702
+ code_challenge_method: "S256",
703
+ nonce: pending.nonce,
704
+ redirect_uri: redirectUri,
705
+ response_type: "code",
706
+ scope: scopes.join(" "),
707
+ state: pending.state,
708
+ ...options.authorizationParameters
709
+ }).toString();
710
+ url.searchParams.set("resource", resource);
711
+ const result = new Promise((resolve, reject) => {
712
+ pendingSignIns.set(pending.state, { reject, resolve });
713
+ options.signal?.addEventListener("abort", () => {
714
+ pendingSignIns.delete(pending.state);
715
+ config.storage.remove(PENDING_KEY);
716
+ reject(new MobileAuthError("aborted", "Authorization was cancelled."));
717
+ }, { once: true });
718
+ });
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;
725
+ }
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;
743
+ };
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" }));
752
+ };
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;
769
+ }
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);
803
+ try {
804
+ if (refreshToken)
805
+ await revokeRefreshToken(refreshToken);
806
+ } finally {
807
+ access = undefined;
808
+ await Promise.all([
809
+ config.storage.remove(PENDING_KEY),
810
+ config.storage.remove(REFRESH_KEY)
811
+ ]);
812
+ }
813
+ };
814
+ const stop = async () => {
815
+ await Promise.all([stopLinks?.(), stopResume?.()]);
816
+ stopLinks = undefined;
817
+ stopResume = undefined;
818
+ startPromise = undefined;
819
+ };
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
831
+ };
832
+ };
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
+ });
329
859
  // src/redirect.ts
330
860
  var isSafeLocalPath = (value) => /^\/(?![/\\])/.test(value);
331
861
  var toSafeLocalPath = (value, fallback = "/") => value !== undefined && isSafeLocalPath(value) ? value : fallback;
@@ -384,9 +914,12 @@ export {
384
914
  isSafeLocalPath,
385
915
  isProtectedSessionRequest,
386
916
  installSessionExpiryGuard,
917
+ createMobileAuthTransport,
918
+ createMobileAuthClient,
387
919
  createAuthClient,
388
- buildSessionExpiredSignInUrl
920
+ buildSessionExpiredSignInUrl,
921
+ MobileAuthError
389
922
  };
390
923
 
391
- //# debugId=E8018F05FE86589464756E2164756E21
924
+ //# debugId=1DC9AFB1303049A564756E2164756E21
392
925
  //# sourceMappingURL=index.js.map