@auth0/auth0-server-js 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -53,6 +53,22 @@ var IssuerValidationError = class extends Error {
53
53
  this.name = "IssuerValidationError";
54
54
  }
55
55
  };
56
+ var MissingAnonymousSessionError = class extends Error {
57
+ code = "missing_anonymous_session_error";
58
+ constructor(message) {
59
+ super(message ?? "There is no anonymous session. Call `anonymous.createSession()` first.");
60
+ this.name = "MissingAnonymousSessionError";
61
+ }
62
+ };
63
+ var AnonymousSessionExpiredError = class extends Error {
64
+ code = "anonymous_session_expired";
65
+ constructor(message) {
66
+ super(
67
+ message ?? "The anonymous session has expired or is no longer valid. Call `anonymous.createSession()` to start a new one."
68
+ );
69
+ this.name = "AnonymousSessionExpiredError";
70
+ }
71
+ };
56
72
  var SessionExpiredError = class extends Error {
57
73
  code = "session_expired";
58
74
  constructor(message) {
@@ -203,7 +219,7 @@ var compareScopes = (scopes, requiredScopes) => {
203
219
  };
204
220
 
205
221
  // src/server-client.ts
206
- import { decodeJwt } from "jose";
222
+ import { decodeJwt as decodeJwt2 } from "jose";
207
223
 
208
224
  // src/telemetry.ts
209
225
  function getTelemetryConfig(config) {
@@ -213,7 +229,7 @@ function getTelemetryConfig(config) {
213
229
  return {
214
230
  enabled: true,
215
231
  name: config?.name ?? "@auth0/auth0-server-js",
216
- version: config?.version ?? "1.15.0"
232
+ version: config?.version ?? "1.16.0"
217
233
  };
218
234
  }
219
235
 
@@ -287,6 +303,7 @@ var ServerMfaClient = class {
287
303
  true,
288
304
  storeOptions
289
305
  );
306
+ await this.#options.onUserSessionEstablished?.(storeOptions);
290
307
  const result = {
291
308
  accessToken: tokenResponse.accessToken,
292
309
  tokenType: tokenResponse.tokenType ?? "bearer",
@@ -395,6 +412,7 @@ var ServerPasskeyClient = class {
395
412
  const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
396
413
  const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
397
414
  await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
415
+ await this.#options.onUserSessionEstablished?.(storeOptions);
398
416
  return {
399
417
  authorizationDetails: tokenEndpointResponse.authorizationDetails
400
418
  };
@@ -489,6 +507,225 @@ function applyEnterpriseConnectRestrictions(instance) {
489
507
  }
490
508
  }
491
509
 
510
+ // src/anonymous/server-anonymous-client.ts
511
+ import { decodeJwt } from "jose";
512
+ var DEFAULT_AUDIENCE_CACHE_KEY = "default";
513
+ var readAnonymousSub = (accessToken) => {
514
+ try {
515
+ const { sub } = decodeJwt(accessToken);
516
+ return typeof sub === "string" ? sub : void 0;
517
+ } catch {
518
+ return void 0;
519
+ }
520
+ };
521
+ var cacheKeyScope = (tokenSet) => tokenSet.requestedScope ?? tokenSet.scope;
522
+ var upsertTokenSet = (tokenSets, tokenSet) => {
523
+ const matches = (candidate) => candidate.audience === tokenSet.audience && cacheKeyScope(candidate) === cacheKeyScope(tokenSet);
524
+ return tokenSets.some(matches) ? tokenSets.map((candidate) => matches(candidate) ? tokenSet : candidate) : [...tokenSets, tokenSet];
525
+ };
526
+ var ServerAnonymousClient = class {
527
+ #options;
528
+ /**
529
+ * @internal
530
+ */
531
+ constructor(options) {
532
+ this.#options = options;
533
+ }
534
+ /**
535
+ * Creates an anonymous session and stores it, returning the first anonymous access token.
536
+ *
537
+ * Replaces any anonymous session already stored for this visitor. Metadata can only be
538
+ * attached here, because Auth0 rejects a request that carries both metadata and an
539
+ * existing session.
540
+ *
541
+ * The anonymous identity (`sub`) is read off the first access token and stored alongside
542
+ * the metadata, so {@link ServerAnonymousClient.getSession} can hand both back without the
543
+ * application decoding a token. It stays `undefined` for an audience with token encryption
544
+ * (`token_encryption`) enabled, whose access token is an encrypted JWE only the API can
545
+ * read.
546
+ *
547
+ * @param options Optional audience, scope and metadata for the new session.
548
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
549
+ *
550
+ * @throws {AnonymousSessionError} If Auth0 rejected the request. Common codes are
551
+ * `feature_not_enabled` (the tenant flag is off), `unauthorized_client` (the client is
552
+ * not enabled for anonymous sessions), `invalid_target` (the resource server does not
553
+ * allow anonymous access), `invalid_request` (metadata over 1024 bytes, or not all
554
+ * strings) and `access_denied`. `code` is `server_error` when Auth0 answers with a body
555
+ * that is not JSON.
556
+ *
557
+ * Only call this once you have established the visitor has no anonymous session (check
558
+ * {@link ServerAnonymousClient.getSession} first), rather than on every request.
559
+ *
560
+ * @returns The anonymous access token for the requested audience.
561
+ */
562
+ async createSession(options, storeOptions) {
563
+ const domain = await this.#options.resolveDomain(storeOptions);
564
+ const requestedAudience = options?.audience ?? this.#options.defaultAudience;
565
+ const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
566
+ const scope = options?.scope;
567
+ const session = await this.#options.getAuthClient(domain).anonymous.createSession({
568
+ ...requestedAudience && { audience: requestedAudience },
569
+ ...scope && { scope },
570
+ ...options?.metadata && { metadata: options.metadata }
571
+ });
572
+ const tokenSet = {
573
+ audience,
574
+ accessToken: session.accessToken,
575
+ scope: session.scope,
576
+ ...scope && scope !== session.scope && { requestedScope: scope },
577
+ expiresAt: session.expiresAt
578
+ };
579
+ const stateData = {
580
+ sessionToken: session.sessionToken,
581
+ sessionTokenExpiresAt: session.sessionTokenExpiresAt,
582
+ sub: readAnonymousSub(session.accessToken),
583
+ ...options?.metadata && { metadata: options.metadata },
584
+ createdAt: Math.floor(Date.now() / 1e3),
585
+ tokenSets: [tokenSet],
586
+ domain
587
+ };
588
+ await this.#options.anonymousStore.set(
589
+ this.#options.anonymousStoreIdentifier,
590
+ stateData,
591
+ true,
592
+ storeOptions
593
+ );
594
+ return tokenSet;
595
+ }
596
+ /**
597
+ * Returns an anonymous access token for the stored anonymous session, fetching a fresh
598
+ * one from Auth0 when the cached one has expired.
599
+ *
600
+ * Tokens are cached per audience and scope, so requesting a second audience returns a
601
+ * second token for the same anonymous identity without replacing the first.
602
+ *
603
+ * Auth0 may grant fewer scopes than requested — a scope the anonymous identity is not
604
+ * entitled to is silently dropped and the response is still a success. Always check
605
+ * `tokenSet.scope` before calling your API; do not assume the token carries every scope
606
+ * you asked for.
607
+ *
608
+ * This never creates a session. If the anonymous session has expired, the stored session
609
+ * is deleted and `AnonymousSessionExpiredError` is thrown, so a visitor is never moved
610
+ * onto a fresh anonymous identity behind your back.
611
+ *
612
+ * @param options Optional audience and scope for the requested token.
613
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
614
+ *
615
+ * @throws {MissingAnonymousSessionError} When there is no anonymous session stored, or the stored one belongs to another Auth0 domain (resolver mode).
616
+ * @throws {AnonymousSessionExpiredError} When the anonymous session has expired or Auth0 rejected the session token. The stored session is deleted first.
617
+ *
618
+ * @returns The anonymous access token for the requested audience.
619
+ */
620
+ async getAccessToken(options, storeOptions) {
621
+ const stateData = await this.#options.anonymousStore.get(
622
+ this.#options.anonymousStoreIdentifier,
623
+ storeOptions
624
+ );
625
+ if (!stateData?.sessionToken) {
626
+ throw new MissingAnonymousSessionError();
627
+ }
628
+ const domain = await this.#options.resolveDomain(storeOptions);
629
+ if (this.#options.isResolverMode() && stateData.domain !== domain) {
630
+ throw new MissingAnonymousSessionError(
631
+ "The stored anonymous session was created for a different Auth0 domain."
632
+ );
633
+ }
634
+ const requestedAudience = options?.audience ?? this.#options.defaultAudience;
635
+ const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
636
+ const scope = options?.scope;
637
+ const cachedTokenSet = stateData.tokenSets.find(
638
+ (tokenSet2) => tokenSet2.audience === audience && (!scope || cacheKeyScope(tokenSet2) === scope || compareScopes(tokenSet2.scope, scope))
639
+ );
640
+ if (cachedTokenSet && cachedTokenSet.expiresAt > Date.now() / 1e3) {
641
+ return cachedTokenSet;
642
+ }
643
+ const renewed = await this.#options.getAuthClient(domain).anonymous.getAccessToken({
644
+ sessionToken: stateData.sessionToken,
645
+ ...requestedAudience && { audience: requestedAudience },
646
+ ...scope && { scope }
647
+ });
648
+ if (renewed.sessionReplaced) {
649
+ await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
650
+ throw new AnonymousSessionExpiredError();
651
+ }
652
+ const tokenSet = {
653
+ audience,
654
+ accessToken: renewed.accessToken,
655
+ scope: renewed.scope,
656
+ ...scope && scope !== renewed.scope && { requestedScope: scope },
657
+ expiresAt: renewed.expiresAt
658
+ };
659
+ const existingStateData = await this.#options.anonymousStore.get(
660
+ this.#options.anonymousStoreIdentifier,
661
+ storeOptions
662
+ );
663
+ if (existingStateData?.sessionToken === stateData.sessionToken) {
664
+ await this.#options.anonymousStore.set(
665
+ this.#options.anonymousStoreIdentifier,
666
+ {
667
+ ...existingStateData,
668
+ ...existingStateData.sub === void 0 && { sub: readAnonymousSub(renewed.accessToken) },
669
+ tokenSets: upsertTokenSet(existingStateData.tokenSets, tokenSet)
670
+ },
671
+ false,
672
+ storeOptions
673
+ );
674
+ }
675
+ return tokenSet;
676
+ }
677
+ /**
678
+ * Returns the stored anonymous session, or `undefined` when there is none.
679
+ *
680
+ * Two common uses:
681
+ * - **Gate `createSession`** — call this first; only create a session when the result is `undefined`.
682
+ * - **Read identity for a merge** — `sub` and `metadata` are available here without decoding a token.
683
+ *
684
+ * This is a local read of the store with no request to Auth0. It cannot tell you whether
685
+ * Auth0 still considers the session valid — only {@link ServerAnonymousClient.getAccessToken} can.
686
+ * The session token is never included in the result.
687
+ *
688
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
689
+ *
690
+ * @returns The anonymous session, or `undefined` when there is none for this visitor (or it belongs to another Auth0 domain in resolver mode).
691
+ */
692
+ async getSession(storeOptions) {
693
+ const stateData = await this.#options.anonymousStore.get(
694
+ this.#options.anonymousStoreIdentifier,
695
+ storeOptions
696
+ );
697
+ if (!stateData) {
698
+ return;
699
+ }
700
+ if (this.#options.isResolverMode()) {
701
+ const domain = await this.#options.resolveDomain(storeOptions);
702
+ if (stateData.domain !== domain) {
703
+ return;
704
+ }
705
+ }
706
+ const { sessionToken, ...sessionData } = stateData;
707
+ return sessionData;
708
+ }
709
+ /**
710
+ * Clears the anonymous session from the store.
711
+ *
712
+ * Does not call `POST /anonymous/logout` — that endpoint only clears the `auth0_anon`
713
+ * browser cookie, which your server never holds. Any access tokens already issued remain
714
+ * valid until they expire (~2 hours by default).
715
+ *
716
+ * You rarely need to call this directly: every method that establishes a user session
717
+ * clears the anonymous session automatically (unless `clearAnonymousSessionOnLogin: false`
718
+ * is set), and `serverClient.logout()` clears it too. Call this to explicitly reset a
719
+ * visitor's anonymous identity, or to clean up after your own post-login merge logic when
720
+ * automatic clearing is disabled.
721
+ *
722
+ * @param storeOptions Optional options used to pass to the anonymous store.
723
+ */
724
+ async logout(storeOptions) {
725
+ await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
726
+ }
727
+ };
728
+
492
729
  // src/server-client.ts
493
730
  var normalizeDomain = (value) => {
494
731
  const trimmed = value.trim();
@@ -497,7 +734,7 @@ var normalizeDomain = (value) => {
497
734
  };
498
735
  var decodeIssuer = (token) => {
499
736
  try {
500
- const { iss } = decodeJwt(token);
737
+ const { iss } = decodeJwt2(token);
501
738
  return typeof iss === "string" ? iss : void 0;
502
739
  } catch {
503
740
  return void 0;
@@ -512,7 +749,7 @@ var actorUnavailableError = (message) => {
512
749
  };
513
750
  var isTokenExpired = (token) => {
514
751
  try {
515
- const { exp } = decodeJwt(token);
752
+ const { exp } = decodeJwt2(token);
516
753
  if (typeof exp !== "number") {
517
754
  return true;
518
755
  }
@@ -534,6 +771,7 @@ var ServerClient = class {
534
771
  #mfaClient;
535
772
  #passkeyClient;
536
773
  #databaseClient;
774
+ #anonymousClient;
537
775
  /**
538
776
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
539
777
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -596,6 +834,42 @@ var ServerClient = class {
596
834
  get database() {
597
835
  return this.#databaseClient;
598
836
  }
837
+ /**
838
+ * The anonymous session client, for giving a visitor who has not logged in a stable
839
+ * identity and an access token for your API.
840
+ *
841
+ * Provides `createSession()` to establish the anonymous identity, `getAccessToken()` to
842
+ * obtain and renew anonymous access tokens, `getSession()` to check whether a visitor
843
+ * already has one, and `logout()` to discard it.
844
+ *
845
+ * Requires `anonymousStore` on the `ServerClient`, and a tenant and client configured for
846
+ * anonymous sessions. Anonymous sessions are kept in that store, never in the state
847
+ * store, so `getSession()` and `getUser()` still report no user while one is active.
848
+ *
849
+ * The anonymous session ends when the visitor logs in: every login method clears it once
850
+ * the user session is written, unless you set `clearAnonymousSessionOnLogin: false`.
851
+ * {@link ServerClient.logout} clears it too.
852
+ *
853
+ * For `/authorize` flows, `startInteractiveLogin()` links the anonymous session to
854
+ * the user automatically via a Session Transfer Ticket — no extra configuration needed.
855
+ * For logins that bypass `/authorize` (passkey, passwordless, backchannel, custom token
856
+ * exchange), the anonymous session is not linked. Read `anonymous.getSession()` before
857
+ * the login to get the anonymous `sub` for a manual merge, or set
858
+ * `clearAnonymousSessionOnLogin: false` and clear it yourself afterwards.
859
+ *
860
+ * Like `passkey` and `database`, this works in both static and resolver (multi-tenant)
861
+ * domain modes.
862
+ *
863
+ * @throws {InvalidConfigurationError} When no `anonymousStore` is configured.
864
+ */
865
+ get anonymous() {
866
+ if (!this.#anonymousClient) {
867
+ throw new InvalidConfigurationError(
868
+ "anonymous is only available when an `anonymousStore` is configured on the ServerClient."
869
+ );
870
+ }
871
+ return this.#anonymousClient;
872
+ }
599
873
  constructor(options) {
600
874
  this.#options = options;
601
875
  this.#enterpriseConnect = !!options.enterpriseConnect;
@@ -654,7 +928,8 @@ var ServerClient = class {
654
928
  domain,
655
929
  stateStore: this.#stateStore,
656
930
  stateStoreIdentifier: this.#stateStoreIdentifier,
657
- defaultAudience: this.#options.authorizationParams?.audience ?? "default"
931
+ defaultAudience: this.#options.authorizationParams?.audience ?? "default",
932
+ onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
658
933
  });
659
934
  }
660
935
  this.#passkeyClient = new ServerPasskeyClient({
@@ -663,12 +938,26 @@ var ServerClient = class {
663
938
  stateStore: this.#stateStore,
664
939
  stateStoreIdentifier: this.#stateStoreIdentifier,
665
940
  defaultScope: this.#options.authorizationParams?.scope,
666
- defaultAudience: this.#options.authorizationParams?.audience
941
+ defaultAudience: this.#options.authorizationParams?.audience,
942
+ onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
667
943
  });
668
944
  this.#databaseClient = new ServerDatabaseClient({
669
945
  resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
670
946
  getAuthClient: (domain) => this.#getAuthClient(domain)
671
947
  });
948
+ if (this.#options.anonymousStore) {
949
+ this.#anonymousClient = new ServerAnonymousClient({
950
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
951
+ getAuthClient: (domain) => this.#getAuthClient(domain),
952
+ isResolverMode: () => this.#isResolverMode(),
953
+ anonymousStore: this.#options.anonymousStore,
954
+ anonymousStoreIdentifier: this.#options.anonymousSessionIdentifier || "__a0_anon",
955
+ // Only the audience is inherited from the client-level authorizationParams. The
956
+ // scope there is written for a user session (`openid profile email offline_access`)
957
+ // and none of it applies to an anonymous identity.
958
+ defaultAudience: this.#options.authorizationParams?.audience
959
+ });
960
+ }
672
961
  }
673
962
  async #resolveDomain(storeOptions) {
674
963
  if (typeof this.#options.domain === "function") {
@@ -745,6 +1034,54 @@ var ServerClient = class {
745
1034
  storeOptions
746
1035
  );
747
1036
  }
1037
+ /**
1038
+ * Discards the anonymous session once a login has established a user session.
1039
+ *
1040
+ * Called by every method that writes a user session, always AFTER the state store has been
1041
+ * written. An anonymous session outliving the login would leave a 30-day bearer credential
1042
+ * in a cookie on every request, and `anonymous.getAccessToken()` would keep minting
1043
+ * anonymous tokens for a visitor who is no longer anonymous.
1044
+ *
1045
+ * Two deliberate properties:
1046
+ * - **Best-effort.** The user is authenticated and their session is already persisted by
1047
+ * this point, so a failing anonymous store must not turn a successful login into an
1048
+ * error. The visitor would be unable to log in at all because of leftover state they
1049
+ * cannot see or clear.
1050
+ * - **Unconditional across domains.** In resolver (multi-tenant) mode the anonymous session
1051
+ * is dropped even when it was created against a different Auth0 domain than the one just
1052
+ * logged into. The anonymous store holds a single session per visitor, and "no anonymous
1053
+ * session survives a login" is a rule that can be reasoned about; a domain-dependent one
1054
+ * cannot.
1055
+ *
1056
+ * Skipped entirely when no `anonymousStore` is configured, or when the application opted
1057
+ * out with `clearAnonymousSessionOnLogin: false`.
1058
+ */
1059
+ async #clearAnonymousSessionAfterLogin(storeOptions) {
1060
+ if (this.#options.clearAnonymousSessionOnLogin === false) {
1061
+ return;
1062
+ }
1063
+ await this.#discardAnonymousSession(storeOptions);
1064
+ }
1065
+ /**
1066
+ * Drops the anonymous session, swallowing store failures.
1067
+ *
1068
+ * Shared by the login sites and {@link ServerClient.logout}. Both run after the user
1069
+ * session has already been written or deleted, so the outcome the caller cares about is
1070
+ * settled: a failing anonymous store must not fail a login the user completed, nor block a
1071
+ * logout redirect and leave the visitor unable to log out. When the store is the
1072
+ * cookie-backed default this cannot fail, since deleting is a `Set-Cookie` on the response.
1073
+ *
1074
+ * No-op when no `anonymousStore` is configured.
1075
+ */
1076
+ async #discardAnonymousSession(storeOptions) {
1077
+ if (!this.#anonymousClient) {
1078
+ return;
1079
+ }
1080
+ try {
1081
+ await this.#anonymousClient.logout(storeOptions);
1082
+ } catch {
1083
+ }
1084
+ }
748
1085
  /**
749
1086
  * Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
750
1087
  *
@@ -780,6 +1117,17 @@ var ServerClient = class {
780
1117
  }
781
1118
  const domain = await this.#resolveDomain(storeOptions);
782
1119
  const authClient = this.#getAuthClient(domain);
1120
+ let anonTransferToken;
1121
+ if (this.#options.anonymousStore) {
1122
+ try {
1123
+ const identifier = this.#options.anonymousSessionIdentifier || "__a0_anon";
1124
+ const stateData = await this.#options.anonymousStore.get(identifier, storeOptions);
1125
+ if (stateData?.sessionToken) {
1126
+ anonTransferToken = await authClient.anonymous.mintTransferToken(stateData.sessionToken) ?? void 0;
1127
+ }
1128
+ } catch {
1129
+ }
1130
+ }
783
1131
  const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
784
1132
  pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
785
1133
  authorizationParams: {
@@ -787,7 +1135,8 @@ var ServerClient = class {
787
1135
  redirect_uri: redirectUri,
788
1136
  scope,
789
1137
  ...resolvedOrganization ? { organization: resolvedOrganization } : {},
790
- ...options?.invitation ? { invitation: options.invitation } : {}
1138
+ ...options?.invitation ? { invitation: options.invitation } : {},
1139
+ ...anonTransferToken ? { anon_transfer_token: anonTransferToken } : {}
791
1140
  }
792
1141
  });
793
1142
  const transactionState = {
@@ -857,6 +1206,7 @@ var ServerClient = class {
857
1206
  tokenEndpointResponse.claims
858
1207
  );
859
1208
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1209
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
860
1210
  return {
861
1211
  appState: transactionData.appState,
862
1212
  authorizationDetails: tokenEndpointResponse.authorizationDetails
@@ -1024,6 +1374,7 @@ var ServerClient = class {
1024
1374
  tokenEndpointResponse.claims
1025
1375
  );
1026
1376
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1377
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1027
1378
  const result = {
1028
1379
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1029
1380
  };
@@ -1170,6 +1521,7 @@ var ServerClient = class {
1170
1521
  { domain }
1171
1522
  );
1172
1523
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1524
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1173
1525
  const result = {
1174
1526
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1175
1527
  };
@@ -1220,6 +1572,7 @@ var ServerClient = class {
1220
1572
  domain
1221
1573
  });
1222
1574
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1575
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1223
1576
  await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
1224
1577
  return {
1225
1578
  authorizationDetails: tokenEndpointResponse.authorizationDetails
@@ -1527,6 +1880,29 @@ var ServerClient = class {
1527
1880
  }
1528
1881
  /**
1529
1882
  * Logs the user out and returns a URL to redirect the user-agent to after they log out.
1883
+ *
1884
+ * Clears the anonymous session as well, whenever an `anonymousStore` is configured. Logging
1885
+ * out means the visitor is done, so leaving a 30-day anonymous credential behind in a
1886
+ * cookie would be surprising, and `anonymous.getAccessToken()` would keep working right
1887
+ * after the user logged out. This happens even when there is no user session to clear: a
1888
+ * visitor who only ever had an anonymous session can log out. Unlike the automatic clearing
1889
+ * at login, it is not affected by `clearAnonymousSessionOnLogin`, which exists so an
1890
+ * application can finish its post-login work.
1891
+ *
1892
+ * In resolver (multi-tenant) mode the one exception is a stored user session belonging to a
1893
+ * different Auth0 domain than the request resolves to. Nothing local is cleared in that
1894
+ * case, anonymous session included, exactly as today: that state belongs to another tenant.
1895
+ *
1896
+ * The anonymous session is only ever cleared locally. The SDK does not call
1897
+ * `POST /anonymous/logout`: that endpoint exists to clear the `auth0_anon` cookie in a
1898
+ * browser, and it revokes nothing server-side. Your server never holds that cookie, so
1899
+ * calling it would achieve nothing. If the visitor's browser created an anonymous session
1900
+ * of its own (through `@auth0/auth0-spa-js`, for example), that cookie is not covered by
1901
+ * this SDK and has to be cleared from the browser.
1902
+ *
1903
+ * Anonymous access tokens already handed out stay valid until they expire; there is no
1904
+ * anonymous session to revoke them against.
1905
+ *
1530
1906
  * @param options Options used to configure the logout process.
1531
1907
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
1532
1908
  * @param requestOptions Optional per-request options (signal, headers, customFetch). Applied to the token revocation ONLY. Building the logout URL is local string work and issues no request, so nothing here can affect it.
@@ -1547,12 +1923,14 @@ var ServerClient = class {
1547
1923
  } catch {
1548
1924
  }
1549
1925
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1926
+ await this.#discardAnonymousSession(storeOptions);
1550
1927
  return this.authClient.buildLogoutUrl(options);
1551
1928
  }
1552
1929
  const resolvedDomain = await this.#resolveDomain(storeOptions);
1553
1930
  const authClient = this.#getAuthClient(resolvedDomain);
1554
1931
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1555
1932
  if (!stateData) {
1933
+ await this.#discardAnonymousSession(storeOptions);
1556
1934
  return authClient.buildLogoutUrl(options);
1557
1935
  }
1558
1936
  const sessionDomain = this.#getSessionDomain(stateData);
@@ -1563,6 +1941,7 @@ var ServerClient = class {
1563
1941
  } catch {
1564
1942
  }
1565
1943
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1944
+ await this.#discardAnonymousSession(storeOptions);
1566
1945
  }
1567
1946
  return authClient.buildLogoutUrl(options);
1568
1947
  }
@@ -1594,6 +1973,7 @@ var ServerClient = class {
1594
1973
  { domain }
1595
1974
  );
1596
1975
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1976
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1597
1977
  const result = {
1598
1978
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1599
1979
  };
@@ -2169,6 +2549,88 @@ var StatelessStateStore = class extends AbstractSessionStore {
2169
2549
  }
2170
2550
  };
2171
2551
 
2552
+ // src/store/abstract-anonymous-store.ts
2553
+ var AbstractAnonymousStore = class extends AbstractStore {
2554
+ constructor(options) {
2555
+ super(options);
2556
+ }
2557
+ };
2558
+
2559
+ // src/store/stateless-anonymous-store.ts
2560
+ var DEFAULT_ANONYMOUS_SESSION_LIFETIME = 60 * 60 * 24 * 30;
2561
+ var CHUNK_SIZE = 3072;
2562
+ var StatelessAnonymousStore = class extends AbstractAnonymousStore {
2563
+ #cookieOptions;
2564
+ #cookieHandler;
2565
+ #sessionTokenLifetime;
2566
+ constructor(options, cookieHandler) {
2567
+ super(options);
2568
+ this.#cookieOptions = options.cookie;
2569
+ this.#cookieHandler = cookieHandler;
2570
+ this.#sessionTokenLifetime = options.sessionTokenLifetime ?? DEFAULT_ANONYMOUS_SESSION_LIFETIME;
2571
+ }
2572
+ async set(identifier, anonymousStateData, removeIfExists, options) {
2573
+ const expiration = this.#getExpiration(anonymousStateData);
2574
+ const maxAge = expiration - Math.floor(Date.now() / 1e3);
2575
+ if (maxAge <= 0) {
2576
+ await this.delete(identifier, options);
2577
+ return;
2578
+ }
2579
+ const cookieOpts = this.#getCookieOptions({ maxAge });
2580
+ const encryptedStateData = await this.encrypt(identifier, anonymousStateData, expiration);
2581
+ const chunkCount = Math.ceil(encryptedStateData.length / CHUNK_SIZE);
2582
+ const chunks = [...Array(chunkCount).keys()].map((i) => ({
2583
+ value: encryptedStateData.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
2584
+ name: `${identifier}.${i}`
2585
+ }));
2586
+ chunks.forEach((chunk) => {
2587
+ this.#cookieHandler.setCookie(chunk.name, chunk.value, cookieOpts, options);
2588
+ });
2589
+ const existingCookieKeys = this.#getCookieKeys(identifier, options);
2590
+ existingCookieKeys.filter((key) => !chunks.some((chunk) => chunk.name === key)).forEach((key) => {
2591
+ this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
2592
+ });
2593
+ }
2594
+ async get(identifier, options) {
2595
+ const cookieKeys = this.#getCookieKeys(identifier, options);
2596
+ const encryptedStateData = cookieKeys.map((key) => ({
2597
+ index: parseInt(key.split(".")[1], 10),
2598
+ value: this.#cookieHandler.getCookie(key, options)
2599
+ })).sort((a, b) => a.index - b.index).map((item) => item.value).join("");
2600
+ if (encryptedStateData) {
2601
+ return await this.decrypt(identifier, encryptedStateData);
2602
+ }
2603
+ }
2604
+ async delete(identifier, options) {
2605
+ const cookieKeys = this.#getCookieKeys(identifier, options);
2606
+ for (const key of cookieKeys) {
2607
+ this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
2608
+ }
2609
+ }
2610
+ /**
2611
+ * Absolute expiry of the anonymous session, in Unix seconds.
2612
+ *
2613
+ * Prefers the value Auth0 reports. `sessionTokenExpiresAt` is not populated yet, because
2614
+ * `auth0-auth-js` does not surface `session_expires_in` from the token response; until it
2615
+ * does, this falls back to the configured lifetime measured from creation.
2616
+ */
2617
+ #getExpiration(anonymousStateData) {
2618
+ return anonymousStateData.sessionTokenExpiresAt ?? anonymousStateData.createdAt + this.#sessionTokenLifetime;
2619
+ }
2620
+ #getCookieKeys(identifier, options) {
2621
+ return Object.keys(this.#cookieHandler.getCookies(options)).filter((key) => key.startsWith(identifier));
2622
+ }
2623
+ #getCookieOptions(partial) {
2624
+ return {
2625
+ httpOnly: true,
2626
+ sameSite: this.#cookieOptions?.sameSite ?? "lax",
2627
+ path: this.#cookieOptions?.path ?? "/",
2628
+ secure: this.#cookieOptions?.secure ?? true,
2629
+ ...partial
2630
+ };
2631
+ }
2632
+ };
2633
+
2172
2634
  // src/mfa/index.ts
2173
2635
  import {
2174
2636
  MfaListAuthenticatorsError,
@@ -2188,12 +2650,19 @@ import {
2188
2650
 
2189
2651
  // src/database/index.ts
2190
2652
  import { SignUpError, ChangePasswordError } from "@auth0/auth0-auth-js";
2653
+
2654
+ // src/anonymous/index.ts
2655
+ import { AnonymousSessionError } from "@auth0/auth0-auth-js";
2191
2656
  export {
2657
+ AbstractAnonymousStore,
2192
2658
  AbstractStateStore,
2193
2659
  AbstractTransactionStore,
2660
+ AnonymousSessionError,
2661
+ AnonymousSessionExpiredError,
2194
2662
  BackchannelLogoutError,
2195
2663
  ChangePasswordError,
2196
2664
  CookieTransactionStore,
2665
+ DEFAULT_ANONYMOUS_SESSION_LIFETIME,
2197
2666
  EnterpriseConnectNotSupportedError2 as EnterpriseConnectNotSupportedError,
2198
2667
  InvalidConfigurationError,
2199
2668
  IssuerValidationError,
@@ -2201,6 +2670,7 @@ export {
2201
2670
  MfaEnrollmentError,
2202
2671
  MfaListAuthenticatorsError,
2203
2672
  MfaVerifyError,
2673
+ MissingAnonymousSessionError,
2204
2674
  MissingClientAuthError,
2205
2675
  MissingRequiredArgumentError,
2206
2676
  MissingSessionError,
@@ -2211,6 +2681,7 @@ export {
2211
2681
  PasskeyRegisterError,
2212
2682
  PasswordlessStartError2 as PasswordlessStartError,
2213
2683
  PasswordlessVerifyError2 as PasswordlessVerifyError,
2684
+ ServerAnonymousClient,
2214
2685
  ServerClient,
2215
2686
  ServerDatabaseClient,
2216
2687
  ServerMfaClient,
@@ -2219,6 +2690,7 @@ export {
2219
2690
  SignUpError,
2220
2691
  StartLinkUserError,
2221
2692
  StatefulStateStore,
2693
+ StatelessAnonymousStore,
2222
2694
  StatelessStateStore,
2223
2695
  TokenExchangeError2 as TokenExchangeError,
2224
2696
  TokenExchangeErrorCode,