@auth0/auth0-server-js 1.6.1 → 1.8.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.cjs CHANGED
@@ -30,16 +30,22 @@ __export(index_exports, {
30
30
  MfaEnrollmentError: () => import_auth0_auth_js2.MfaEnrollmentError,
31
31
  MfaListAuthenticatorsError: () => import_auth0_auth_js2.MfaListAuthenticatorsError,
32
32
  MfaVerifyError: () => import_auth0_auth_js2.MfaVerifyError,
33
- MissingClientAuthError: () => import_auth0_auth_js3.MissingClientAuthError,
33
+ MissingClientAuthError: () => import_auth0_auth_js4.MissingClientAuthError,
34
34
  MissingRequiredArgumentError: () => MissingRequiredArgumentError,
35
35
  MissingSessionError: () => MissingSessionError,
36
36
  MissingTransactionError: () => MissingTransactionError,
37
+ OrganizationValidationError: () => import_auth0_auth_js3.OrganizationValidationError,
38
+ PasskeyChallengeError: () => import_auth0_auth_js3.PasskeyChallengeError,
39
+ PasskeyGetTokenError: () => import_auth0_auth_js3.PasskeyGetTokenError,
40
+ PasskeyRegisterError: () => import_auth0_auth_js3.PasskeyRegisterError,
37
41
  ServerClient: () => ServerClient,
38
42
  ServerMfaClient: () => ServerMfaClient,
43
+ ServerPasskeyClient: () => ServerPasskeyClient,
44
+ SessionExpiredError: () => SessionExpiredError,
39
45
  StartLinkUserError: () => StartLinkUserError,
40
46
  StatefulStateStore: () => StatefulStateStore,
41
47
  StatelessStateStore: () => StatelessStateStore,
42
- TokenExchangeError: () => import_auth0_auth_js3.TokenExchangeError,
48
+ TokenExchangeError: () => import_auth0_auth_js4.TokenExchangeError,
43
49
  isMfaRequiredError: () => import_auth0_auth_js2.isMfaRequiredError
44
50
  });
45
51
  module.exports = __toCommonJS(index_exports);
@@ -94,14 +100,55 @@ var IssuerValidationError = class extends Error {
94
100
  this.name = "IssuerValidationError";
95
101
  }
96
102
  };
103
+ var SessionExpiredError = class extends Error {
104
+ code = "session_expired";
105
+ constructor(message) {
106
+ super(
107
+ message ?? "The session has expired because the upstream identity provider session ceiling was reached. The user needs to re-authenticate."
108
+ );
109
+ this.name = "SessionExpiredError";
110
+ }
111
+ };
97
112
 
98
113
  // src/state/utils.ts
114
+ var SESSION_EXPIRY_LEEWAY = 30;
115
+ var MAX_PLAUSIBLE_UNIX_SECONDS = 1e10;
116
+ function isPlausibleUnixSeconds(value) {
117
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < MAX_PLAUSIBLE_UNIX_SECONDS;
118
+ }
119
+ function extractSessionExpiry(claims) {
120
+ const value = claims?.session_expiry;
121
+ return isPlausibleUnixSeconds(value) ? value : void 0;
122
+ }
123
+ function isSessionExpiryReached(sessionExpiresAt, nowSeconds) {
124
+ if (sessionExpiresAt === void 0) {
125
+ return false;
126
+ }
127
+ const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
128
+ return now >= sessionExpiresAt - SESSION_EXPIRY_LEEWAY;
129
+ }
99
130
  var createUpdatedTokenSet = (audience, response) => ({
100
131
  audience,
101
132
  accessToken: response.accessToken,
102
133
  scope: response.scope,
103
134
  expiresAt: response.expiresAt
104
135
  });
136
+ function isSessionExpiryInPast(sessionExpiresAt, issuedAt) {
137
+ if (sessionExpiresAt === void 0) {
138
+ return false;
139
+ }
140
+ const reference = isPlausibleUnixSeconds(issuedAt) ? issuedAt : Math.floor(Date.now() / 1e3);
141
+ return sessionExpiresAt <= reference + SESSION_EXPIRY_LEEWAY;
142
+ }
143
+ function applySessionExpiryAtLogin(stateData, claims) {
144
+ const sessionExpiresAt = extractSessionExpiry(claims);
145
+ if (isSessionExpiryInPast(sessionExpiresAt, claims?.iat)) {
146
+ throw new SessionExpiredError(
147
+ "The upstream identity provider session_expiry is at or before the issued-at time; refusing to create an already-expired session."
148
+ );
149
+ }
150
+ return { ...stateData, sessionExpiresAt };
151
+ }
105
152
  function updateStateData(audience, stateData, tokenEndpointResponse, context) {
106
153
  if (stateData && tokenEndpointResponse.claims) {
107
154
  const newSub = tokenEndpointResponse.claims.sub;
@@ -168,6 +215,18 @@ function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointR
168
215
  var import_auth0_auth_js = require("@auth0/auth0-auth-js");
169
216
 
170
217
  // src/utils.ts
218
+ var DEFAULT_SCOPES = "openid profile email offline_access";
219
+ var ensureOpenIdScope = (scope) => {
220
+ const normalizedScope = scope?.trim();
221
+ if (!normalizedScope) {
222
+ return DEFAULT_SCOPES;
223
+ }
224
+ const scopes = normalizedScope.split(/\s+/);
225
+ if (!scopes.includes("openid")) {
226
+ scopes.unshift("openid");
227
+ }
228
+ return scopes.join(" ");
229
+ };
171
230
  var compareScopes = (scopes, requiredScopes) => {
172
231
  if (scopes === requiredScopes) {
173
232
  return true;
@@ -191,7 +250,7 @@ function getTelemetryConfig(config) {
191
250
  return {
192
251
  enabled: true,
193
252
  name: config?.name ?? "@auth0/auth0-server-js",
194
- version: config?.version ?? "1.6.1"
253
+ version: config?.version ?? "1.8.0"
195
254
  };
196
255
  }
197
256
 
@@ -253,9 +312,12 @@ var ServerMfaClient = class {
253
312
  this.#options.stateStoreIdentifier,
254
313
  storeOptions
255
314
  );
256
- const updatedStateData = updateStateData(audience, existingStateData, tokenResponse, {
257
- domain: this.#options.domain
258
- });
315
+ const updatedStateData = applySessionExpiryAtLogin(
316
+ updateStateData(audience, existingStateData, tokenResponse, {
317
+ domain: this.#options.domain
318
+ }),
319
+ tokenResponse.claims
320
+ );
259
321
  await this.#options.stateStore.set(
260
322
  this.#options.stateStoreIdentifier,
261
323
  updatedStateData,
@@ -275,8 +337,99 @@ var ServerMfaClient = class {
275
337
  }
276
338
  };
277
339
 
340
+ // src/passkey/server-passkey-client.ts
341
+ var ServerPasskeyClient = class {
342
+ #options;
343
+ /**
344
+ * @internal
345
+ */
346
+ constructor(options) {
347
+ this.#options = options;
348
+ }
349
+ /**
350
+ * Requests a passkey signup challenge for a new user.
351
+ *
352
+ * Returns the `authSession` and the WebAuthn credential creation options
353
+ * (`authnParamsPublicKey`). The application must return these to the browser,
354
+ * pass `authnParamsPublicKey` to `navigator.credentials.create()`, and then
355
+ * call `getToken()` with the resulting credential to complete signup.
356
+ *
357
+ * This method does not create a session; no state is persisted.
358
+ *
359
+ * @param options User profile data and optional realm/organization.
360
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
361
+ *
362
+ * @throws {PasskeyRegisterError} If there was an issue requesting the signup challenge.
363
+ *
364
+ * @returns A promise resolving to the signup challenge.
365
+ */
366
+ async register(options, storeOptions) {
367
+ const domain = await this.#options.resolveDomain(storeOptions);
368
+ const authClient = this.#options.getAuthClient(domain);
369
+ return authClient.passkey.register(options);
370
+ }
371
+ /**
372
+ * Requests a passkey login challenge for an existing user.
373
+ *
374
+ * Returns the `authSession` and the WebAuthn credential request options
375
+ * (`authnParamsPublicKey`). The application must return these to the browser,
376
+ * pass `authnParamsPublicKey` to `navigator.credentials.get()`, and then
377
+ * call `getToken()` with the resulting credential to complete login.
378
+ *
379
+ * This method does not create a session; no state is persisted.
380
+ *
381
+ * @param options Optional realm/organization configuration.
382
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
383
+ *
384
+ * @throws {PasskeyChallengeError} If there was an issue requesting the login challenge.
385
+ *
386
+ * @returns A promise resolving to the login challenge.
387
+ */
388
+ async challenge(options, storeOptions) {
389
+ const domain = await this.#options.resolveDomain(storeOptions);
390
+ const authClient = this.#options.getAuthClient(domain);
391
+ return authClient.passkey.challenge(options);
392
+ }
393
+ /**
394
+ * Completes a passkey authentication flow (signup or login) by exchanging the
395
+ * WebAuthn credential for tokens, and persists the resulting session.
396
+ *
397
+ * Call this after obtaining a credential from `navigator.credentials.create()`
398
+ * (signup) or `navigator.credentials.get()` (login), passing the `authSession`
399
+ * returned by `register()` / `challenge()` together with the serialized credential.
400
+ *
401
+ * In resolver (multi-tenant) mode, pass the same `storeOptions` you passed to
402
+ * `register()` / `challenge()` so the token exchange resolves the same tenant
403
+ * that issued the `authSession`; otherwise the exchange will fail.
404
+ *
405
+ * @param options The auth session, serialized credential, and optional realm/scope/audience/organization.
406
+ * @param storeOptions Optional options used to pass to the State Store (and to resolve the domain in resolver mode).
407
+ *
408
+ * @throws {PasskeyGetTokenError} If there was an issue exchanging the credential for tokens. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`. No session is persisted in this case.
409
+ * @throws {OrganizationValidationError} When `organization` is passed and the returned ID token's organization claim is missing or does not match. The error is thrown before the session is written, so no session is persisted in this case.
410
+ *
411
+ * @returns A promise resolving to an object containing the authorizationDetails (when RAR was used).
412
+ */
413
+ async getToken(options, storeOptions) {
414
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.defaultScope);
415
+ const audience = options.audience ?? this.#options.defaultAudience;
416
+ const domain = await this.#options.resolveDomain(storeOptions);
417
+ const authClient = this.#options.getAuthClient(domain);
418
+ const tokenEndpointResponse = await authClient.passkey.getTokenByPasskey({
419
+ ...options,
420
+ scope,
421
+ audience
422
+ });
423
+ const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
424
+ const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
425
+ await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
426
+ return {
427
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
428
+ };
429
+ }
430
+ };
431
+
278
432
  // src/server-client.ts
279
- var DEFAULT_SCOPES = "openid profile email offline_access";
280
433
  var normalizeDomain = (value) => {
281
434
  const trimmed = value.trim();
282
435
  const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
@@ -290,16 +443,6 @@ var decodeIssuer = (token) => {
290
443
  return void 0;
291
444
  }
292
445
  };
293
- var ensureOpenIdScope = (scope) => {
294
- if (!scope) {
295
- return DEFAULT_SCOPES;
296
- }
297
- const scopes = scope.split(" ");
298
- if (!scopes.includes("openid")) {
299
- scopes.unshift("openid");
300
- }
301
- return scopes.join(" ");
302
- };
303
446
  var ServerClient = class {
304
447
  #options;
305
448
  #transactionStore;
@@ -310,6 +453,7 @@ var ServerClient = class {
310
453
  #staticDomain;
311
454
  #authClient;
312
455
  #mfaClient;
456
+ #passkeyClient;
313
457
  /**
314
458
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
315
459
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -344,6 +488,19 @@ var ServerClient = class {
344
488
  }
345
489
  return this.#mfaClient;
346
490
  }
491
+ /**
492
+ * The passkey client for signing up and logging in users with WebAuthn credentials.
493
+ *
494
+ * Provides `register()` and `challenge()` to request signup/login challenges, and
495
+ * `getToken()` to exchange the resulting credential for tokens and persist the session.
496
+ *
497
+ * Unlike `mfa`, this property is available in both static and resolver (multi-tenant)
498
+ * domain modes. In resolver mode, pass the same `storeOptions` to `register()`/`challenge()`
499
+ * and `getToken()` so the credential is exchanged against the tenant that issued it.
500
+ */
501
+ get passkey() {
502
+ return this.#passkeyClient;
503
+ }
347
504
  constructor(options) {
348
505
  this.#options = options;
349
506
  this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
@@ -385,6 +542,14 @@ var ServerClient = class {
385
542
  defaultAudience: this.#options.authorizationParams?.audience ?? "default"
386
543
  });
387
544
  }
545
+ this.#passkeyClient = new ServerPasskeyClient({
546
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
547
+ getAuthClient: (domain) => this.#getAuthClient(domain),
548
+ stateStore: this.#stateStore,
549
+ stateStoreIdentifier: this.#stateStoreIdentifier,
550
+ defaultScope: this.#options.authorizationParams?.scope,
551
+ defaultAudience: this.#options.authorizationParams?.audience
552
+ });
388
553
  }
389
554
  async #resolveDomain(storeOptions) {
390
555
  if (typeof this.#options.domain === "function") {
@@ -478,6 +643,7 @@ var ServerClient = class {
478
643
  *
479
644
  * @throws {MissingTransactionError} When no transaction was found.
480
645
  * @throws {TokenByCodeError} If there was an issue requesting the access token.
646
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
481
647
  *
482
648
  * @returns A promise resolving to an object, containing the original appState (if present) and the authorizationDetails (when RAR was used).
483
649
  */
@@ -489,14 +655,18 @@ var ServerClient = class {
489
655
  const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
490
656
  const authClient = this.#getAuthClient(domain);
491
657
  const tokenEndpointResponse = await authClient.getTokenByCode(url, {
658
+ // TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
492
659
  codeVerifier: transactionData.codeVerifier
493
660
  });
661
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
494
662
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
495
- const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
496
- domain
497
- });
663
+ const stateData = applySessionExpiryAtLogin(
664
+ updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
665
+ domain
666
+ }),
667
+ tokenEndpointResponse.claims
668
+ );
498
669
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
499
- await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
500
670
  return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
501
671
  }
502
672
  /**
@@ -506,6 +676,7 @@ var ServerClient = class {
506
676
  *
507
677
  * @throws {MissingSessionError} If there is no active session.
508
678
  * @throws {BuildLinkUserUrlError} If there was an issue when building the Authorization URL.
679
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
509
680
  *
510
681
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
511
682
  */
@@ -522,6 +693,10 @@ var ServerClient = class {
522
693
  throw new MissingSessionError("Session domain does not match the current domain.");
523
694
  }
524
695
  }
696
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
697
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
698
+ throw new SessionExpiredError();
699
+ }
525
700
  const domain = this.#getSessionDomain(stateData);
526
701
  const authClient = this.#getAuthClient(domain);
527
702
  const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
@@ -565,6 +740,7 @@ var ServerClient = class {
565
740
  *
566
741
  * @throws {MissingSessionError} If there is no active session.
567
742
  * @throws {BuildUnlinkUserUrlError} If there was an issue when building the User Unlinking URL.
743
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
568
744
  *
569
745
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
570
746
  */
@@ -581,6 +757,10 @@ var ServerClient = class {
581
757
  throw new MissingSessionError("Session domain does not match the current domain.");
582
758
  }
583
759
  }
760
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
761
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
762
+ throw new SessionExpiredError();
763
+ }
584
764
  const domain = this.#getSessionDomain(stateData);
585
765
  const authClient = this.#getAuthClient(domain);
586
766
  const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
@@ -625,6 +805,7 @@ var ServerClient = class {
625
805
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
626
806
  *
627
807
  * @throws {BackchannelAuthenticationError} If there was an issue when doing backchannel authentication.
808
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
628
809
  *
629
810
  * @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
630
811
  */
@@ -641,6 +822,135 @@ var ServerClient = class {
641
822
  }
642
823
  });
643
824
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
825
+ const stateData = applySessionExpiryAtLogin(
826
+ updateStateData(this.#options.authorizationParams?.audience ?? "default", existingStateData, tokenEndpointResponse, {
827
+ domain
828
+ }),
829
+ tokenEndpointResponse.claims
830
+ );
831
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
832
+ return {
833
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
834
+ };
835
+ }
836
+ /**
837
+ * Starts a passwordless flow by sending a one-time code (OTP) or a magic link.
838
+ *
839
+ * Discriminated on `connection` (and, for email, `send`) to mirror the
840
+ * `@auth0/nextjs-auth0` `passwordless.start()` surface:
841
+ * - `{ connection: 'email' }` / `{ connection: 'email', send: 'code' }` — email OTP
842
+ * - `{ connection: 'email', send: 'link', redirectUri }` — email magic link
843
+ * - `{ connection: 'sms' }` — SMS OTP
844
+ *
845
+ * OTP modes are a stateless passthrough to the Authentication API (no session, no transaction);
846
+ * complete them with {@link ServerClient#completePasswordless}.
847
+ *
848
+ * Magic-link mode is stateful: the SDK generates an opaque anti-forgery `state`, sends the link
849
+ * with the OAuth parameters embedded (`redirect_uri`, `response_type=code`, `scope`, `state`),
850
+ * and persists a transaction carrying that `state`. NO PKCE challenge is registered, so the
851
+ * transaction holds no `codeVerifier`. Complete it with
852
+ * {@link ServerClient#completePasswordlessMagicLink}. Requires the tenant setting
853
+ * `allow_magiclink_verify_without_session: true` for server-side completion.
854
+ *
855
+ * @param options Discriminated start options.
856
+ * @param storeOptions Optional options passed to the resolver / stores.
857
+ *
858
+ * @throws {PasswordlessStartError} If the request fails, or if a magic link is requested without a `redirectUri`.
859
+ *
860
+ * @example
861
+ * // Email OTP
862
+ * await serverClient.startPasswordless({ connection: 'email', email: 'user@example.com' });
863
+ * // SMS OTP
864
+ * await serverClient.startPasswordless({ connection: 'sms', phoneNumber: '+14155550100' });
865
+ * // Email magic link
866
+ * await serverClient.startPasswordless({
867
+ * connection: 'email',
868
+ * email: 'user@example.com',
869
+ * send: 'link',
870
+ * redirectUri: 'https://app.example.com/auth/callback',
871
+ * });
872
+ */
873
+ async startPasswordless(options, storeOptions) {
874
+ const domain = await this.#resolveDomain(storeOptions);
875
+ const authClient = this.#getAuthClient(domain);
876
+ if (options.connection === "sms") {
877
+ await authClient.passwordless.sendSms({
878
+ phoneNumber: options.phoneNumber,
879
+ language: options.language
880
+ });
881
+ return;
882
+ }
883
+ if (options.send !== "link") {
884
+ await authClient.passwordless.sendEmail({
885
+ email: options.email,
886
+ send: "code",
887
+ language: options.language
888
+ });
889
+ return;
890
+ }
891
+ if (!options.redirectUri || typeof options.redirectUri !== "string") {
892
+ throw new import_auth0_auth_js.PasswordlessStartError("redirectUri is required to start a passwordless magic-link login.");
893
+ }
894
+ const state = crypto.randomUUID();
895
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.authorizationParams?.scope);
896
+ const audience = options.audience ?? this.#options.authorizationParams?.audience;
897
+ await authClient.passwordless.sendEmail({
898
+ email: options.email,
899
+ send: "link",
900
+ language: options.language,
901
+ authParams: {
902
+ ...options.authParams,
903
+ redirect_uri: options.redirectUri,
904
+ response_type: "code",
905
+ scope,
906
+ ...audience ? { audience } : {},
907
+ state
908
+ }
909
+ });
910
+ const transactionState = {
911
+ audience,
912
+ domain,
913
+ state
914
+ };
915
+ await this.#transactionStore.set(this.#transactionStoreIdentifier, transactionState, false, storeOptions);
916
+ }
917
+ /**
918
+ * Completes a passwordless OTP login and persists the resulting session.
919
+ *
920
+ * Discriminated on `connection` to mirror the `@auth0/nextjs-auth0` `passwordless.verify()`
921
+ * surface. Non-redirect flow: no PKCE and no transaction store (mirrors
922
+ * {@link ServerClient#loginBackchannel}). The `openid` scope is always ensured by this layer.
923
+ *
924
+ * Note: the state store is read-then-written; if your deployment performs concurrent
925
+ * logins for the same session identifier, use a state store with atomic/serializable
926
+ * writes to avoid last-write-wins races.
927
+ *
928
+ * @param options Discriminated completion options (`connection`, identifier, `verificationCode`).
929
+ * @param storeOptions Optional options passed to the resolver / stores.
930
+ *
931
+ * @throws {PasswordlessVerifyError} If the code is invalid, expired, or rate-limited. When the
932
+ * connection requires MFA, the server responds with `mfa_required`; narrow the thrown error
933
+ * with `isMfaRequiredError(error)` to read `cause.mfa_token`.
934
+ *
935
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
936
+ */
937
+ async completePasswordless(options, storeOptions) {
938
+ const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
939
+ const audience = options.authorizationParams?.audience ?? this.#options.authorizationParams?.audience;
940
+ const domain = await this.#resolveDomain(storeOptions);
941
+ const authClient = this.#getAuthClient(domain);
942
+ const tokenEndpointResponse = options.connection === "sms" ? await authClient.getTokenByPasswordlessSms({
943
+ phoneNumber: options.phoneNumber,
944
+ code: options.verificationCode,
945
+ audience,
946
+ scope
947
+ }) : await authClient.getTokenByPasswordlessEmail({
948
+ email: options.email,
949
+ code: options.verificationCode,
950
+ audience,
951
+ scope
952
+ });
953
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
644
954
  const stateData = updateStateData(
645
955
  this.#options.authorizationParams?.audience ?? "default",
646
956
  existingStateData,
@@ -652,6 +962,49 @@ var ServerClient = class {
652
962
  authorizationDetails: tokenEndpointResponse.authorizationDetails
653
963
  };
654
964
  }
965
+ /**
966
+ * Completes a passwordless magic-link login and persists the resulting session.
967
+ *
968
+ * Loads the transaction persisted by {@link ServerClient#startPasswordless} (magic-link mode), validates the
969
+ * `state` returned on the callback URL against the stored `state` (anti-forgery binding), exchanges
970
+ * the authorization code WITHOUT PKCE, writes the session, and deletes the transaction. The existing
971
+ * interactive login path ({@link ServerClient#completeInteractiveLogin}) is not used.
972
+ *
973
+ * @param url The callback URL containing the authorization `code` and `state`.
974
+ * @param storeOptions Optional options passed to the resolver / stores.
975
+ *
976
+ * @throws {MissingTransactionError} If no magic-link transaction was found.
977
+ * @throws {PasswordlessVerifyError} If the returned `state` is missing or does not match.
978
+ * @throws {TokenByCodeError} If the token exchange fails.
979
+ *
980
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
981
+ *
982
+ * @example
983
+ * const result = await serverClient.completePasswordlessMagicLink(callbackUrl, storeOptions);
984
+ */
985
+ async completePasswordlessMagicLink(url, storeOptions) {
986
+ const transactionData = await this.#transactionStore.get(this.#transactionStoreIdentifier, storeOptions);
987
+ if (!transactionData) {
988
+ throw new MissingTransactionError();
989
+ }
990
+ const expectedState = typeof transactionData.state === "string" ? transactionData.state : void 0;
991
+ const returnedState = url.searchParams.get("state");
992
+ if (!returnedState || !expectedState || returnedState !== expectedState) {
993
+ throw new import_auth0_auth_js.PasswordlessVerifyError("State mismatch on magic-link callback");
994
+ }
995
+ const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
996
+ const authClient = this.#getAuthClient(domain);
997
+ const tokenEndpointResponse = await authClient.getTokenByMagicLinkCode(url, { expectedState });
998
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
999
+ const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
1000
+ domain
1001
+ });
1002
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
1003
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
1004
+ return {
1005
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
1006
+ };
1007
+ }
655
1008
  /**
656
1009
  * Retrieves the user from the store, or undefined if no user found.
657
1010
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
@@ -668,6 +1021,10 @@ var ServerClient = class {
668
1021
  return;
669
1022
  }
670
1023
  }
1024
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1025
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1026
+ return;
1027
+ }
671
1028
  return stateData.user;
672
1029
  }
673
1030
  /**
@@ -684,6 +1041,10 @@ var ServerClient = class {
684
1041
  return;
685
1042
  }
686
1043
  }
1044
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1045
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1046
+ return;
1047
+ }
687
1048
  const { internal, ...sessionData } = stateData;
688
1049
  return sessionData;
689
1050
  }
@@ -691,16 +1052,30 @@ var ServerClient = class {
691
1052
  /**
692
1053
  * Retrieves the access token from the store, or calls Auth0 when the access token is expired and a refresh token is available in the store.
693
1054
  * Also updates the store when a new token was retrieved from Auth0.
1055
+ *
1056
+ * When `options.audience` and/or `options.scope` are provided, the SDK uses the session's refresh token to
1057
+ * request an access token for that audience/scope (Multi-Resource Refresh Tokens). Tokens are cached per
1058
+ * audience and scope combination.
1059
+ *
1060
+ * @param options Optional options for requesting a specific audience/scope.
694
1061
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
695
1062
  *
696
1063
  * @throws {TokenByRefreshTokenError} If the refresh token was not found or there was an issue requesting the access token. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`.
1064
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and no refresh is attempted — the user must re-authenticate.
697
1065
  *
698
1066
  * @returns The Token Set, containing the access token, as well as additional information.
699
1067
  */
700
- async getAccessToken(storeOptions) {
701
- const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
702
- const audience = this.#options.authorizationParams?.audience ?? "default";
703
- const scope = this.#options.authorizationParams?.scope;
1068
+ async getAccessToken(tokenOptionsOrStoreOptions, storeOptions) {
1069
+ const hasTokenOptions = (
1070
+ // If second arg exists, first arg must be GetAccessTokenOptions
1071
+ storeOptions !== void 0 || // OR if first arg has audience/scope properties
1072
+ !!tokenOptionsOrStoreOptions && typeof tokenOptionsOrStoreOptions === "object" && ("audience" in tokenOptionsOrStoreOptions || "scope" in tokenOptionsOrStoreOptions)
1073
+ );
1074
+ const [resolvedOptions, resolvedStoreOptions] = hasTokenOptions ? [tokenOptionsOrStoreOptions, storeOptions] : [void 0, tokenOptionsOrStoreOptions];
1075
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
1076
+ const requestedAudience = resolvedOptions?.audience ?? this.#options.authorizationParams?.audience;
1077
+ const audience = requestedAudience ?? "default";
1078
+ const scope = resolvedOptions?.scope ?? this.#options.authorizationParams?.scope;
704
1079
  const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
705
1080
  if (this.#isResolverMode()) {
706
1081
  if (!stateData) {
@@ -709,11 +1084,15 @@ var ServerClient = class {
709
1084
  if (!sessionDomain) {
710
1085
  throw new MissingSessionError("Session domain does not match the current domain.");
711
1086
  }
712
- const resolvedDomain = await this.#resolveDomain(storeOptions);
1087
+ const resolvedDomain = await this.#resolveDomain(resolvedStoreOptions);
713
1088
  if (sessionDomain !== resolvedDomain) {
714
1089
  throw new MissingSessionError("Session domain does not match the current domain.");
715
1090
  }
716
1091
  }
1092
+ if (stateData && isSessionExpiryReached(stateData.sessionExpiresAt)) {
1093
+ await this.#stateStore.delete(this.#stateStoreIdentifier, resolvedStoreOptions);
1094
+ throw new SessionExpiredError();
1095
+ }
717
1096
  const tokenSet = stateData?.tokenSets.find(
718
1097
  (tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
719
1098
  );
@@ -726,14 +1105,21 @@ var ServerClient = class {
726
1105
  );
727
1106
  }
728
1107
  const domainForSession = sessionDomain;
729
- const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken({
730
- refreshToken: stateData.refreshToken
731
- });
732
- const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1108
+ const tokenByRefreshTokenOptions = {
1109
+ refreshToken: stateData.refreshToken,
1110
+ // Only forward audience/scope to Auth0 when token options were explicitly supplied, and
1111
+ // never send the synthetic 'default' cache-key audience as a real request parameter.
1112
+ ...hasTokenOptions && {
1113
+ ...requestedAudience && { audience: requestedAudience },
1114
+ ...scope && { scope }
1115
+ }
1116
+ };
1117
+ const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken(tokenByRefreshTokenOptions);
1118
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
733
1119
  const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
734
1120
  domain: domainForSession
735
1121
  });
736
- await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
1122
+ await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, resolvedStoreOptions);
737
1123
  return {
738
1124
  accessToken: tokenEndpointResponse.accessToken,
739
1125
  scope: tokenEndpointResponse.scope,
@@ -1041,7 +1427,7 @@ var AbstractTransactionStore = class extends AbstractStore {
1041
1427
  };
1042
1428
 
1043
1429
  // src/index.ts
1044
- var import_auth0_auth_js3 = require("@auth0/auth0-auth-js");
1430
+ var import_auth0_auth_js4 = require("@auth0/auth0-auth-js");
1045
1431
 
1046
1432
  // src/store/cookie-transaction-store.ts
1047
1433
  var CookieTransactionStore = class extends AbstractTransactionStore {
@@ -1081,14 +1467,14 @@ var AbstractSessionStore = class extends AbstractStateStore {
1081
1467
  }
1082
1468
  /**
1083
1469
  * calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
1470
+ * When sessionExpiresAt is provided, caps the maxAge to not exceed the time until that ceiling.
1084
1471
  */
1085
- calculateMaxAge(createdAt) {
1086
- if (!this.#rolling) {
1087
- return this.#absoluteDuration;
1088
- }
1472
+ calculateMaxAge(createdAt, sessionExpiresAt) {
1089
1473
  const now = Date.now() / 1e3 | 0;
1090
- const expiresAt = Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration);
1091
- const maxAge = expiresAt - now;
1474
+ let maxAge = this.#rolling ? Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration) - now : this.#absoluteDuration;
1475
+ if (sessionExpiresAt !== void 0) {
1476
+ maxAge = Math.min(maxAge, sessionExpiresAt - now);
1477
+ }
1092
1478
  return maxAge > 0 ? maxAge : 0;
1093
1479
  }
1094
1480
  };
@@ -1116,7 +1502,7 @@ var StatefulStateStore = class extends AbstractSessionStore {
1116
1502
  sessionId = generateId();
1117
1503
  }
1118
1504
  sessionId ??= generateId();
1119
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1505
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1120
1506
  const cookieOpts = this.#getCookieOptions({
1121
1507
  maxAge
1122
1508
  });
@@ -1181,7 +1567,7 @@ var StatelessStateStore = class extends AbstractSessionStore {
1181
1567
  this.#cookieHandler = cookieHandler;
1182
1568
  }
1183
1569
  async set(identifier, stateData, removeIfExists, options) {
1184
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1570
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1185
1571
  const cookieOpts = this.#getCookieOptions({
1186
1572
  maxAge
1187
1573
  });
@@ -1239,6 +1625,9 @@ var StatelessStateStore = class extends AbstractSessionStore {
1239
1625
 
1240
1626
  // src/mfa/index.ts
1241
1627
  var import_auth0_auth_js2 = require("@auth0/auth0-auth-js");
1628
+
1629
+ // src/passkey/index.ts
1630
+ var import_auth0_auth_js3 = require("@auth0/auth0-auth-js");
1242
1631
  // Annotate the CommonJS export names for ESM import in node:
1243
1632
  0 && (module.exports = {
1244
1633
  AbstractStateStore,
@@ -1255,8 +1644,14 @@ var import_auth0_auth_js2 = require("@auth0/auth0-auth-js");
1255
1644
  MissingRequiredArgumentError,
1256
1645
  MissingSessionError,
1257
1646
  MissingTransactionError,
1647
+ OrganizationValidationError,
1648
+ PasskeyChallengeError,
1649
+ PasskeyGetTokenError,
1650
+ PasskeyRegisterError,
1258
1651
  ServerClient,
1259
1652
  ServerMfaClient,
1653
+ ServerPasskeyClient,
1654
+ SessionExpiredError,
1260
1655
  StartLinkUserError,
1261
1656
  StatefulStateStore,
1262
1657
  StatelessStateStore,