@auth0/auth0-server-js 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1"
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,234 @@ function applyEnterpriseConnectRestrictions(instance) {
489
507
  }
490
508
  }
491
509
 
510
+ // src/anonymous/server-anonymous-client.ts
511
+ import { decodeJwt } from "jose";
512
+ import { AnonymousSessionError } from "@auth0/auth0-auth-js";
513
+ var DEFAULT_AUDIENCE_CACHE_KEY = "default";
514
+ var readAnonymousSub = (accessToken) => {
515
+ try {
516
+ const { sub } = decodeJwt(accessToken);
517
+ return typeof sub === "string" ? sub : void 0;
518
+ } catch {
519
+ return void 0;
520
+ }
521
+ };
522
+ var cacheKeyScope = (tokenSet) => tokenSet.requestedScope ?? tokenSet.scope;
523
+ var upsertTokenSet = (tokenSets, tokenSet) => {
524
+ const matches = (candidate) => candidate.audience === tokenSet.audience && cacheKeyScope(candidate) === cacheKeyScope(tokenSet);
525
+ return tokenSets.some(matches) ? tokenSets.map((candidate) => matches(candidate) ? tokenSet : candidate) : [...tokenSets, tokenSet];
526
+ };
527
+ var ServerAnonymousClient = class {
528
+ #options;
529
+ /**
530
+ * @internal
531
+ */
532
+ constructor(options) {
533
+ this.#options = options;
534
+ }
535
+ /**
536
+ * Creates an anonymous session and stores it, returning the first anonymous access token.
537
+ *
538
+ * Replaces any anonymous session already stored for this visitor. Metadata can only be
539
+ * attached here, because Auth0 rejects a request that carries both metadata and an
540
+ * existing session.
541
+ *
542
+ * The anonymous identity (`sub`) is read off the first access token and stored alongside
543
+ * the metadata, so {@link ServerAnonymousClient.getSession} can hand both back without the
544
+ * application decoding a token. It stays `undefined` for an audience with token encryption
545
+ * (`token_encryption`) enabled, whose access token is an encrypted JWE only the API can
546
+ * read.
547
+ *
548
+ * @param options Optional audience, scope and metadata for the new session.
549
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
550
+ *
551
+ * @throws {AnonymousSessionError} If Auth0 rejected the request. Common codes are
552
+ * `feature_not_enabled` (the tenant flag is off), `unauthorized_client` (the client is
553
+ * not enabled for anonymous sessions), `invalid_target` (the resource server does not
554
+ * allow anonymous access), `invalid_request` (metadata over 1024 bytes, or not all
555
+ * strings) and `access_denied`. `code` is `server_error` when Auth0 answers with a body
556
+ * that is not JSON.
557
+ *
558
+ * Only call this once you have established the visitor has no anonymous session (check
559
+ * {@link ServerAnonymousClient.getSession} first), rather than on every request.
560
+ *
561
+ * @returns The anonymous access token for the requested audience.
562
+ */
563
+ async createSession(options, storeOptions) {
564
+ const domain = await this.#options.resolveDomain(storeOptions);
565
+ const requestedAudience = options?.audience ?? this.#options.defaultAudience;
566
+ const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
567
+ const scope = options?.scope;
568
+ const session = await this.#options.getAuthClient(domain).anonymous.createSession({
569
+ ...requestedAudience && { audience: requestedAudience },
570
+ ...scope && { scope },
571
+ ...options?.metadata && { metadata: options.metadata }
572
+ });
573
+ const tokenSet = {
574
+ audience,
575
+ accessToken: session.accessToken,
576
+ scope: session.scope,
577
+ ...scope && scope !== session.scope && { requestedScope: scope },
578
+ expiresAt: session.expiresAt
579
+ };
580
+ const stateData = {
581
+ sessionToken: session.sessionToken,
582
+ sessionTokenExpiresAt: session.sessionTokenExpiresAt,
583
+ sub: readAnonymousSub(session.accessToken),
584
+ ...options?.metadata && { metadata: options.metadata },
585
+ createdAt: Math.floor(Date.now() / 1e3),
586
+ tokenSets: [tokenSet],
587
+ domain
588
+ };
589
+ await this.#options.anonymousStore.set(
590
+ this.#options.anonymousStoreIdentifier,
591
+ stateData,
592
+ true,
593
+ storeOptions
594
+ );
595
+ return tokenSet;
596
+ }
597
+ /**
598
+ * Returns an anonymous access token for the stored anonymous session, fetching a fresh
599
+ * one from Auth0 when the cached one has expired.
600
+ *
601
+ * Tokens are cached per audience and scope, so requesting a second audience returns a
602
+ * second token for the same anonymous identity without replacing the first.
603
+ *
604
+ * Auth0 may grant fewer scopes than requested — a scope the anonymous identity is not
605
+ * entitled to is silently dropped and the response is still a success. Always check
606
+ * `tokenSet.scope` before calling your API; do not assume the token carries every scope
607
+ * you asked for.
608
+ *
609
+ * This never creates a session. If the anonymous session has expired, the stored session
610
+ * is deleted and `AnonymousSessionExpiredError` is thrown, so a visitor is never moved
611
+ * onto a fresh anonymous identity behind your back.
612
+ *
613
+ * @param options Optional audience and scope for the requested token.
614
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
615
+ *
616
+ * @throws {MissingAnonymousSessionError} When there is no anonymous session stored, or the stored one belongs to another Auth0 domain (resolver mode).
617
+ * @throws {AnonymousSessionExpiredError} When the anonymous session has expired or Auth0 rejected the session token. The stored session is deleted first.
618
+ *
619
+ * @returns The anonymous access token for the requested audience.
620
+ */
621
+ async getAccessToken(options, storeOptions) {
622
+ const stateData = await this.#options.anonymousStore.get(
623
+ this.#options.anonymousStoreIdentifier,
624
+ storeOptions
625
+ );
626
+ if (!stateData?.sessionToken) {
627
+ throw new MissingAnonymousSessionError();
628
+ }
629
+ const domain = await this.#options.resolveDomain(storeOptions);
630
+ if (this.#options.isResolverMode() && stateData.domain !== domain) {
631
+ throw new MissingAnonymousSessionError(
632
+ "The stored anonymous session was created for a different Auth0 domain."
633
+ );
634
+ }
635
+ const requestedAudience = options?.audience ?? this.#options.defaultAudience;
636
+ const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
637
+ const scope = options?.scope;
638
+ const cachedTokenSet = stateData.tokenSets.find(
639
+ (tokenSet2) => tokenSet2.audience === audience && (!scope || cacheKeyScope(tokenSet2) === scope || compareScopes(tokenSet2.scope, scope))
640
+ );
641
+ if (cachedTokenSet && cachedTokenSet.expiresAt > Date.now() / 1e3) {
642
+ return cachedTokenSet;
643
+ }
644
+ let renewed;
645
+ try {
646
+ renewed = await this.#options.getAuthClient(domain).anonymous.getAccessToken({
647
+ sessionToken: stateData.sessionToken,
648
+ ...requestedAudience && { audience: requestedAudience },
649
+ ...scope && { scope }
650
+ });
651
+ } catch (e) {
652
+ if (e instanceof AnonymousSessionError && e.code === "session_expired") {
653
+ await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
654
+ throw new AnonymousSessionExpiredError();
655
+ }
656
+ if (e instanceof AnonymousSessionError && e.code === "invalid_session_token") {
657
+ await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
658
+ }
659
+ throw e;
660
+ }
661
+ const tokenSet = {
662
+ audience,
663
+ accessToken: renewed.accessToken,
664
+ scope: renewed.scope,
665
+ ...scope && scope !== renewed.scope && { requestedScope: scope },
666
+ expiresAt: renewed.expiresAt
667
+ };
668
+ const existingStateData = await this.#options.anonymousStore.get(
669
+ this.#options.anonymousStoreIdentifier,
670
+ storeOptions
671
+ );
672
+ if (existingStateData?.sessionToken === stateData.sessionToken) {
673
+ await this.#options.anonymousStore.set(
674
+ this.#options.anonymousStoreIdentifier,
675
+ {
676
+ ...existingStateData,
677
+ ...existingStateData.sub === void 0 && { sub: readAnonymousSub(renewed.accessToken) },
678
+ tokenSets: upsertTokenSet(existingStateData.tokenSets, tokenSet)
679
+ },
680
+ false,
681
+ storeOptions
682
+ );
683
+ }
684
+ return tokenSet;
685
+ }
686
+ /**
687
+ * Returns the stored anonymous session, or `undefined` when there is none.
688
+ *
689
+ * Two common uses:
690
+ * - **Gate `createSession`** — call this first; only create a session when the result is `undefined`.
691
+ * - **Read identity for a merge** — `sub` and `metadata` are available here without decoding a token.
692
+ *
693
+ * This is a local read of the store with no request to Auth0. It cannot tell you whether
694
+ * Auth0 still considers the session valid — only {@link ServerAnonymousClient.getAccessToken} can.
695
+ * The session token is never included in the result.
696
+ *
697
+ * @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
698
+ *
699
+ * @returns The anonymous session, or `undefined` when there is none for this visitor (or it belongs to another Auth0 domain in resolver mode).
700
+ */
701
+ async getSession(storeOptions) {
702
+ const stateData = await this.#options.anonymousStore.get(
703
+ this.#options.anonymousStoreIdentifier,
704
+ storeOptions
705
+ );
706
+ if (!stateData) {
707
+ return;
708
+ }
709
+ if (this.#options.isResolverMode()) {
710
+ const domain = await this.#options.resolveDomain(storeOptions);
711
+ if (stateData.domain !== domain) {
712
+ return;
713
+ }
714
+ }
715
+ const { sessionToken, ...sessionData } = stateData;
716
+ return sessionData;
717
+ }
718
+ /**
719
+ * Clears the anonymous session from the store.
720
+ *
721
+ * Does not call `POST /anonymous/logout` — that endpoint only clears the `auth0_anon`
722
+ * browser cookie, which your server never holds. Any access tokens already issued remain
723
+ * valid until they expire (~2 hours by default).
724
+ *
725
+ * You rarely need to call this directly: every method that establishes a user session
726
+ * clears the anonymous session automatically (unless `clearAnonymousSessionOnLogin: false`
727
+ * is set), and `serverClient.logout()` clears it too. Call this to explicitly reset a
728
+ * visitor's anonymous identity, or to clean up after your own post-login merge logic when
729
+ * automatic clearing is disabled.
730
+ *
731
+ * @param storeOptions Optional options used to pass to the anonymous store.
732
+ */
733
+ async logout(storeOptions) {
734
+ await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
735
+ }
736
+ };
737
+
492
738
  // src/server-client.ts
493
739
  var normalizeDomain = (value) => {
494
740
  const trimmed = value.trim();
@@ -497,7 +743,7 @@ var normalizeDomain = (value) => {
497
743
  };
498
744
  var decodeIssuer = (token) => {
499
745
  try {
500
- const { iss } = decodeJwt(token);
746
+ const { iss } = decodeJwt2(token);
501
747
  return typeof iss === "string" ? iss : void 0;
502
748
  } catch {
503
749
  return void 0;
@@ -512,7 +758,7 @@ var actorUnavailableError = (message) => {
512
758
  };
513
759
  var isTokenExpired = (token) => {
514
760
  try {
515
- const { exp } = decodeJwt(token);
761
+ const { exp } = decodeJwt2(token);
516
762
  if (typeof exp !== "number") {
517
763
  return true;
518
764
  }
@@ -534,6 +780,7 @@ var ServerClient = class {
534
780
  #mfaClient;
535
781
  #passkeyClient;
536
782
  #databaseClient;
783
+ #anonymousClient;
537
784
  /**
538
785
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
539
786
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -596,6 +843,42 @@ var ServerClient = class {
596
843
  get database() {
597
844
  return this.#databaseClient;
598
845
  }
846
+ /**
847
+ * The anonymous session client, for giving a visitor who has not logged in a stable
848
+ * identity and an access token for your API.
849
+ *
850
+ * Provides `createSession()` to establish the anonymous identity, `getAccessToken()` to
851
+ * obtain and renew anonymous access tokens, `getSession()` to check whether a visitor
852
+ * already has one, and `logout()` to discard it.
853
+ *
854
+ * Requires `anonymousStore` on the `ServerClient`, and a tenant and client configured for
855
+ * anonymous sessions. Anonymous sessions are kept in that store, never in the state
856
+ * store, so `getSession()` and `getUser()` still report no user while one is active.
857
+ *
858
+ * The anonymous session ends when the visitor logs in: every login method clears it once
859
+ * the user session is written, unless you set `clearAnonymousSessionOnLogin: false`.
860
+ * {@link ServerClient.logout} clears it too.
861
+ *
862
+ * For `/authorize` flows, `startInteractiveLogin()` links the anonymous session to
863
+ * the user automatically via a Session Transfer Ticket — no extra configuration needed.
864
+ * For logins that bypass `/authorize` (passkey, passwordless, backchannel, custom token
865
+ * exchange), the anonymous session is not linked. Read `anonymous.getSession()` before
866
+ * the login to get the anonymous `sub` for a manual merge, or set
867
+ * `clearAnonymousSessionOnLogin: false` and clear it yourself afterwards.
868
+ *
869
+ * Like `passkey` and `database`, this works in both static and resolver (multi-tenant)
870
+ * domain modes.
871
+ *
872
+ * @throws {InvalidConfigurationError} When no `anonymousStore` is configured.
873
+ */
874
+ get anonymous() {
875
+ if (!this.#anonymousClient) {
876
+ throw new InvalidConfigurationError(
877
+ "anonymous is only available when an `anonymousStore` is configured on the ServerClient."
878
+ );
879
+ }
880
+ return this.#anonymousClient;
881
+ }
599
882
  constructor(options) {
600
883
  this.#options = options;
601
884
  this.#enterpriseConnect = !!options.enterpriseConnect;
@@ -654,7 +937,8 @@ var ServerClient = class {
654
937
  domain,
655
938
  stateStore: this.#stateStore,
656
939
  stateStoreIdentifier: this.#stateStoreIdentifier,
657
- defaultAudience: this.#options.authorizationParams?.audience ?? "default"
940
+ defaultAudience: this.#options.authorizationParams?.audience ?? "default",
941
+ onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
658
942
  });
659
943
  }
660
944
  this.#passkeyClient = new ServerPasskeyClient({
@@ -663,12 +947,26 @@ var ServerClient = class {
663
947
  stateStore: this.#stateStore,
664
948
  stateStoreIdentifier: this.#stateStoreIdentifier,
665
949
  defaultScope: this.#options.authorizationParams?.scope,
666
- defaultAudience: this.#options.authorizationParams?.audience
950
+ defaultAudience: this.#options.authorizationParams?.audience,
951
+ onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
667
952
  });
668
953
  this.#databaseClient = new ServerDatabaseClient({
669
954
  resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
670
955
  getAuthClient: (domain) => this.#getAuthClient(domain)
671
956
  });
957
+ if (this.#options.anonymousStore) {
958
+ this.#anonymousClient = new ServerAnonymousClient({
959
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
960
+ getAuthClient: (domain) => this.#getAuthClient(domain),
961
+ isResolverMode: () => this.#isResolverMode(),
962
+ anonymousStore: this.#options.anonymousStore,
963
+ anonymousStoreIdentifier: this.#options.anonymousSessionIdentifier || "__a0_anon",
964
+ // Only the audience is inherited from the client-level authorizationParams. The
965
+ // scope there is written for a user session (`openid profile email offline_access`)
966
+ // and none of it applies to an anonymous identity.
967
+ defaultAudience: this.#options.authorizationParams?.audience
968
+ });
969
+ }
672
970
  }
673
971
  async #resolveDomain(storeOptions) {
674
972
  if (typeof this.#options.domain === "function") {
@@ -745,6 +1043,54 @@ var ServerClient = class {
745
1043
  storeOptions
746
1044
  );
747
1045
  }
1046
+ /**
1047
+ * Discards the anonymous session once a login has established a user session.
1048
+ *
1049
+ * Called by every method that writes a user session, always AFTER the state store has been
1050
+ * written. An anonymous session outliving the login would leave a 30-day bearer credential
1051
+ * in a cookie on every request, and `anonymous.getAccessToken()` would keep minting
1052
+ * anonymous tokens for a visitor who is no longer anonymous.
1053
+ *
1054
+ * Two deliberate properties:
1055
+ * - **Best-effort.** The user is authenticated and their session is already persisted by
1056
+ * this point, so a failing anonymous store must not turn a successful login into an
1057
+ * error. The visitor would be unable to log in at all because of leftover state they
1058
+ * cannot see or clear.
1059
+ * - **Unconditional across domains.** In resolver (multi-tenant) mode the anonymous session
1060
+ * is dropped even when it was created against a different Auth0 domain than the one just
1061
+ * logged into. The anonymous store holds a single session per visitor, and "no anonymous
1062
+ * session survives a login" is a rule that can be reasoned about; a domain-dependent one
1063
+ * cannot.
1064
+ *
1065
+ * Skipped entirely when no `anonymousStore` is configured, or when the application opted
1066
+ * out with `clearAnonymousSessionOnLogin: false`.
1067
+ */
1068
+ async #clearAnonymousSessionAfterLogin(storeOptions) {
1069
+ if (this.#options.clearAnonymousSessionOnLogin === false) {
1070
+ return;
1071
+ }
1072
+ await this.#discardAnonymousSession(storeOptions);
1073
+ }
1074
+ /**
1075
+ * Drops the anonymous session, swallowing store failures.
1076
+ *
1077
+ * Shared by the login sites and {@link ServerClient.logout}. Both run after the user
1078
+ * session has already been written or deleted, so the outcome the caller cares about is
1079
+ * settled: a failing anonymous store must not fail a login the user completed, nor block a
1080
+ * logout redirect and leave the visitor unable to log out. When the store is the
1081
+ * cookie-backed default this cannot fail, since deleting is a `Set-Cookie` on the response.
1082
+ *
1083
+ * No-op when no `anonymousStore` is configured.
1084
+ */
1085
+ async #discardAnonymousSession(storeOptions) {
1086
+ if (!this.#anonymousClient) {
1087
+ return;
1088
+ }
1089
+ try {
1090
+ await this.#anonymousClient.logout(storeOptions);
1091
+ } catch {
1092
+ }
1093
+ }
748
1094
  /**
749
1095
  * Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
750
1096
  *
@@ -780,6 +1126,17 @@ var ServerClient = class {
780
1126
  }
781
1127
  const domain = await this.#resolveDomain(storeOptions);
782
1128
  const authClient = this.#getAuthClient(domain);
1129
+ let anonTransferToken;
1130
+ if (this.#options.anonymousStore) {
1131
+ try {
1132
+ const identifier = this.#options.anonymousSessionIdentifier || "__a0_anon";
1133
+ const stateData = await this.#options.anonymousStore.get(identifier, storeOptions);
1134
+ if (stateData?.sessionToken) {
1135
+ anonTransferToken = await authClient.anonymous.mintTransferToken(stateData.sessionToken) ?? void 0;
1136
+ }
1137
+ } catch {
1138
+ }
1139
+ }
783
1140
  const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
784
1141
  pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
785
1142
  authorizationParams: {
@@ -787,7 +1144,8 @@ var ServerClient = class {
787
1144
  redirect_uri: redirectUri,
788
1145
  scope,
789
1146
  ...resolvedOrganization ? { organization: resolvedOrganization } : {},
790
- ...options?.invitation ? { invitation: options.invitation } : {}
1147
+ ...options?.invitation ? { invitation: options.invitation } : {},
1148
+ ...anonTransferToken ? { anon_transfer_token: anonTransferToken } : {}
791
1149
  }
792
1150
  });
793
1151
  const transactionState = {
@@ -857,6 +1215,7 @@ var ServerClient = class {
857
1215
  tokenEndpointResponse.claims
858
1216
  );
859
1217
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1218
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
860
1219
  return {
861
1220
  appState: transactionData.appState,
862
1221
  authorizationDetails: tokenEndpointResponse.authorizationDetails
@@ -1024,6 +1383,7 @@ var ServerClient = class {
1024
1383
  tokenEndpointResponse.claims
1025
1384
  );
1026
1385
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1386
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1027
1387
  const result = {
1028
1388
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1029
1389
  };
@@ -1170,6 +1530,7 @@ var ServerClient = class {
1170
1530
  { domain }
1171
1531
  );
1172
1532
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1533
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1173
1534
  const result = {
1174
1535
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1175
1536
  };
@@ -1220,6 +1581,7 @@ var ServerClient = class {
1220
1581
  domain
1221
1582
  });
1222
1583
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1584
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1223
1585
  await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
1224
1586
  return {
1225
1587
  authorizationDetails: tokenEndpointResponse.authorizationDetails
@@ -1527,6 +1889,29 @@ var ServerClient = class {
1527
1889
  }
1528
1890
  /**
1529
1891
  * Logs the user out and returns a URL to redirect the user-agent to after they log out.
1892
+ *
1893
+ * Clears the anonymous session as well, whenever an `anonymousStore` is configured. Logging
1894
+ * out means the visitor is done, so leaving a 30-day anonymous credential behind in a
1895
+ * cookie would be surprising, and `anonymous.getAccessToken()` would keep working right
1896
+ * after the user logged out. This happens even when there is no user session to clear: a
1897
+ * visitor who only ever had an anonymous session can log out. Unlike the automatic clearing
1898
+ * at login, it is not affected by `clearAnonymousSessionOnLogin`, which exists so an
1899
+ * application can finish its post-login work.
1900
+ *
1901
+ * In resolver (multi-tenant) mode the one exception is a stored user session belonging to a
1902
+ * different Auth0 domain than the request resolves to. Nothing local is cleared in that
1903
+ * case, anonymous session included, exactly as today: that state belongs to another tenant.
1904
+ *
1905
+ * The anonymous session is only ever cleared locally. The SDK does not call
1906
+ * `POST /anonymous/logout`: that endpoint exists to clear the `auth0_anon` cookie in a
1907
+ * browser, and it revokes nothing server-side. Your server never holds that cookie, so
1908
+ * calling it would achieve nothing. If the visitor's browser created an anonymous session
1909
+ * of its own (through `@auth0/auth0-spa-js`, for example), that cookie is not covered by
1910
+ * this SDK and has to be cleared from the browser.
1911
+ *
1912
+ * Anonymous access tokens already handed out stay valid until they expire; there is no
1913
+ * anonymous session to revoke them against.
1914
+ *
1530
1915
  * @param options Options used to configure the logout process.
1531
1916
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
1532
1917
  * @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 +1932,14 @@ var ServerClient = class {
1547
1932
  } catch {
1548
1933
  }
1549
1934
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1935
+ await this.#discardAnonymousSession(storeOptions);
1550
1936
  return this.authClient.buildLogoutUrl(options);
1551
1937
  }
1552
1938
  const resolvedDomain = await this.#resolveDomain(storeOptions);
1553
1939
  const authClient = this.#getAuthClient(resolvedDomain);
1554
1940
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1555
1941
  if (!stateData) {
1942
+ await this.#discardAnonymousSession(storeOptions);
1556
1943
  return authClient.buildLogoutUrl(options);
1557
1944
  }
1558
1945
  const sessionDomain = this.#getSessionDomain(stateData);
@@ -1563,6 +1950,7 @@ var ServerClient = class {
1563
1950
  } catch {
1564
1951
  }
1565
1952
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1953
+ await this.#discardAnonymousSession(storeOptions);
1566
1954
  }
1567
1955
  return authClient.buildLogoutUrl(options);
1568
1956
  }
@@ -1594,6 +1982,7 @@ var ServerClient = class {
1594
1982
  { domain }
1595
1983
  );
1596
1984
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1985
+ await this.#clearAnonymousSessionAfterLogin(storeOptions);
1597
1986
  const result = {
1598
1987
  authorizationDetails: tokenEndpointResponse.authorizationDetails
1599
1988
  };
@@ -2169,6 +2558,88 @@ var StatelessStateStore = class extends AbstractSessionStore {
2169
2558
  }
2170
2559
  };
2171
2560
 
2561
+ // src/store/abstract-anonymous-store.ts
2562
+ var AbstractAnonymousStore = class extends AbstractStore {
2563
+ constructor(options) {
2564
+ super(options);
2565
+ }
2566
+ };
2567
+
2568
+ // src/store/stateless-anonymous-store.ts
2569
+ var DEFAULT_ANONYMOUS_SESSION_LIFETIME = 60 * 60 * 24 * 30;
2570
+ var CHUNK_SIZE = 3072;
2571
+ var StatelessAnonymousStore = class extends AbstractAnonymousStore {
2572
+ #cookieOptions;
2573
+ #cookieHandler;
2574
+ #sessionTokenLifetime;
2575
+ constructor(options, cookieHandler) {
2576
+ super(options);
2577
+ this.#cookieOptions = options.cookie;
2578
+ this.#cookieHandler = cookieHandler;
2579
+ this.#sessionTokenLifetime = options.sessionTokenLifetime ?? DEFAULT_ANONYMOUS_SESSION_LIFETIME;
2580
+ }
2581
+ async set(identifier, anonymousStateData, removeIfExists, options) {
2582
+ const expiration = this.#getExpiration(anonymousStateData);
2583
+ const maxAge = expiration - Math.floor(Date.now() / 1e3);
2584
+ if (maxAge <= 0) {
2585
+ await this.delete(identifier, options);
2586
+ return;
2587
+ }
2588
+ const cookieOpts = this.#getCookieOptions({ maxAge });
2589
+ const encryptedStateData = await this.encrypt(identifier, anonymousStateData, expiration);
2590
+ const chunkCount = Math.ceil(encryptedStateData.length / CHUNK_SIZE);
2591
+ const chunks = [...Array(chunkCount).keys()].map((i) => ({
2592
+ value: encryptedStateData.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
2593
+ name: `${identifier}.${i}`
2594
+ }));
2595
+ chunks.forEach((chunk) => {
2596
+ this.#cookieHandler.setCookie(chunk.name, chunk.value, cookieOpts, options);
2597
+ });
2598
+ const existingCookieKeys = this.#getCookieKeys(identifier, options);
2599
+ existingCookieKeys.filter((key) => !chunks.some((chunk) => chunk.name === key)).forEach((key) => {
2600
+ this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
2601
+ });
2602
+ }
2603
+ async get(identifier, options) {
2604
+ const cookieKeys = this.#getCookieKeys(identifier, options);
2605
+ const encryptedStateData = cookieKeys.map((key) => ({
2606
+ index: parseInt(key.split(".")[1], 10),
2607
+ value: this.#cookieHandler.getCookie(key, options)
2608
+ })).sort((a, b) => a.index - b.index).map((item) => item.value).join("");
2609
+ if (encryptedStateData) {
2610
+ return await this.decrypt(identifier, encryptedStateData);
2611
+ }
2612
+ }
2613
+ async delete(identifier, options) {
2614
+ const cookieKeys = this.#getCookieKeys(identifier, options);
2615
+ for (const key of cookieKeys) {
2616
+ this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
2617
+ }
2618
+ }
2619
+ /**
2620
+ * Absolute expiry of the anonymous session, in Unix seconds.
2621
+ *
2622
+ * Prefers the value Auth0 reports. `sessionTokenExpiresAt` is not populated yet, because
2623
+ * `auth0-auth-js` does not surface `session_expires_in` from the token response; until it
2624
+ * does, this falls back to the configured lifetime measured from creation.
2625
+ */
2626
+ #getExpiration(anonymousStateData) {
2627
+ return anonymousStateData.sessionTokenExpiresAt ?? anonymousStateData.createdAt + this.#sessionTokenLifetime;
2628
+ }
2629
+ #getCookieKeys(identifier, options) {
2630
+ return Object.keys(this.#cookieHandler.getCookies(options)).filter((key) => key.startsWith(identifier));
2631
+ }
2632
+ #getCookieOptions(partial) {
2633
+ return {
2634
+ httpOnly: true,
2635
+ sameSite: this.#cookieOptions?.sameSite ?? "lax",
2636
+ path: this.#cookieOptions?.path ?? "/",
2637
+ secure: this.#cookieOptions?.secure ?? true,
2638
+ ...partial
2639
+ };
2640
+ }
2641
+ };
2642
+
2172
2643
  // src/mfa/index.ts
2173
2644
  import {
2174
2645
  MfaListAuthenticatorsError,
@@ -2188,12 +2659,19 @@ import {
2188
2659
 
2189
2660
  // src/database/index.ts
2190
2661
  import { SignUpError, ChangePasswordError } from "@auth0/auth0-auth-js";
2662
+
2663
+ // src/anonymous/index.ts
2664
+ import { AnonymousSessionError as AnonymousSessionError2 } from "@auth0/auth0-auth-js";
2191
2665
  export {
2666
+ AbstractAnonymousStore,
2192
2667
  AbstractStateStore,
2193
2668
  AbstractTransactionStore,
2669
+ AnonymousSessionError2 as AnonymousSessionError,
2670
+ AnonymousSessionExpiredError,
2194
2671
  BackchannelLogoutError,
2195
2672
  ChangePasswordError,
2196
2673
  CookieTransactionStore,
2674
+ DEFAULT_ANONYMOUS_SESSION_LIFETIME,
2197
2675
  EnterpriseConnectNotSupportedError2 as EnterpriseConnectNotSupportedError,
2198
2676
  InvalidConfigurationError,
2199
2677
  IssuerValidationError,
@@ -2201,6 +2679,7 @@ export {
2201
2679
  MfaEnrollmentError,
2202
2680
  MfaListAuthenticatorsError,
2203
2681
  MfaVerifyError,
2682
+ MissingAnonymousSessionError,
2204
2683
  MissingClientAuthError,
2205
2684
  MissingRequiredArgumentError,
2206
2685
  MissingSessionError,
@@ -2211,6 +2690,7 @@ export {
2211
2690
  PasskeyRegisterError,
2212
2691
  PasswordlessStartError2 as PasswordlessStartError,
2213
2692
  PasswordlessVerifyError2 as PasswordlessVerifyError,
2693
+ ServerAnonymousClient,
2214
2694
  ServerClient,
2215
2695
  ServerDatabaseClient,
2216
2696
  ServerMfaClient,
@@ -2219,6 +2699,7 @@ export {
2219
2699
  SignUpError,
2220
2700
  StartLinkUserError,
2221
2701
  StatefulStateStore,
2702
+ StatelessAnonymousStore,
2222
2703
  StatelessStateStore,
2223
2704
  TokenExchangeError2 as TokenExchangeError,
2224
2705
  TokenExchangeErrorCode,