@authsome/ui-core 1.7.1 → 1.8.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
@@ -805,7 +805,7 @@ var AuthClient = class {
805
805
  * List SSO connections for an app (admin)
806
806
  * GET /v1/admin/sso/connections
807
807
  */
808
- async ssoAdminListConnections(app_id, token) {
808
+ async ssoAdminListConnections(token, app_id) {
809
809
  const params = new URLSearchParams();
810
810
  if (app_id !== void 0) params.set("app_id", String(app_id));
811
811
  const qs = params.toString();
@@ -995,6 +995,19 @@ var AuthClient = class {
995
995
  token
996
996
  );
997
997
  }
998
+ /**
999
+ * Unlock user (admin)
1000
+ * POST /v1/admin/users/{userId}/unlock
1001
+ */
1002
+ async adminUnlockUser(userId, token) {
1003
+ const path = `/v1/admin/users/${userId}/unlock`;
1004
+ return this.request(
1005
+ "POST",
1006
+ path,
1007
+ void 0,
1008
+ token
1009
+ );
1010
+ }
998
1011
  /**
999
1012
  * List coupons
1000
1013
  * GET /v1/billing/coupons
@@ -1717,6 +1730,32 @@ var AuthClient = class {
1717
1730
  token
1718
1731
  );
1719
1732
  }
1733
+ /**
1734
+ * List my OAuth2 grants
1735
+ * GET /v1/me/oauth/grants
1736
+ */
1737
+ async oauth2ListMyGrants(token) {
1738
+ const path = "/v1/me/oauth/grants";
1739
+ return this.request(
1740
+ "GET",
1741
+ path,
1742
+ void 0,
1743
+ token
1744
+ );
1745
+ }
1746
+ /**
1747
+ * Revoke an OAuth2 grant
1748
+ * DELETE /v1/me/oauth/grants/{clientId}
1749
+ */
1750
+ async oauth2RevokeMyGrant(clientId, token) {
1751
+ const path = `/v1/me/oauth/grants/${clientId}`;
1752
+ return this.request(
1753
+ "DELETE",
1754
+ path,
1755
+ void 0,
1756
+ token
1757
+ );
1758
+ }
1720
1759
  /**
1721
1760
  * Switch active organization
1722
1761
  * POST /v1/me/switch-org
@@ -1835,7 +1874,7 @@ var AuthClient = class {
1835
1874
  * OAuth2 Authorization
1836
1875
  * GET /v1/oauth/authorize
1837
1876
  */
1838
- async oauth2Authorize(response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, resource) {
1877
+ async oauth2Authorize(response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, resource, prompt) {
1839
1878
  const params = new URLSearchParams();
1840
1879
  if (response_type !== void 0) params.set("response_type", String(response_type));
1841
1880
  if (client_id !== void 0) params.set("client_id", String(client_id));
@@ -1845,6 +1884,7 @@ var AuthClient = class {
1845
1884
  if (code_challenge !== void 0) params.set("code_challenge", String(code_challenge));
1846
1885
  if (code_challenge_method !== void 0) params.set("code_challenge_method", String(code_challenge_method));
1847
1886
  if (resource !== void 0) for (const element of resource) params.append("resource", String(element));
1887
+ if (prompt !== void 0) params.set("prompt", String(prompt));
1848
1888
  const qs = params.toString();
1849
1889
  const path = "/v1/oauth/authorize" + (qs ? `?${qs}` : "");
1850
1890
  return this.request(
@@ -1853,6 +1893,32 @@ var AuthClient = class {
1853
1893
  void 0
1854
1894
  );
1855
1895
  }
1896
+ /**
1897
+ * OAuth2 consent
1898
+ * GET /v1/oauth/consent
1899
+ */
1900
+ async oauth2ConsentPage(token) {
1901
+ const path = "/v1/oauth/consent";
1902
+ return this.request(
1903
+ "GET",
1904
+ path,
1905
+ void 0,
1906
+ token
1907
+ );
1908
+ }
1909
+ /**
1910
+ * OAuth2 consent decision
1911
+ * POST /v1/oauth/consent
1912
+ */
1913
+ async oauth2ConsentDecision(token) {
1914
+ const path = "/v1/oauth/consent";
1915
+ return this.request(
1916
+ "POST",
1917
+ path,
1918
+ void 0,
1919
+ token
1920
+ );
1921
+ }
1856
1922
  /**
1857
1923
  * Device Authorization
1858
1924
  * POST /v1/oauth/device/authorize
@@ -1978,8 +2044,12 @@ var AuthClient = class {
1978
2044
  * List organizations
1979
2045
  * GET /v1/orgs
1980
2046
  */
1981
- async listOrganizations(token) {
1982
- const path = "/v1/orgs";
2047
+ async listOrganizations(token, limit, cursor) {
2048
+ const params = new URLSearchParams();
2049
+ if (limit !== void 0) params.set("limit", String(limit));
2050
+ if (cursor !== void 0) params.set("cursor", String(cursor));
2051
+ const qs = params.toString();
2052
+ const path = "/v1/orgs" + (qs ? `?${qs}` : "");
1983
2053
  return this.request(
1984
2054
  "GET",
1985
2055
  path,
@@ -2086,8 +2156,12 @@ var AuthClient = class {
2086
2156
  * List invitations
2087
2157
  * GET /v1/orgs/{orgId}/invitations
2088
2158
  */
2089
- async listInvitations(orgId, token) {
2090
- const path = `/v1/orgs/${orgId}/invitations`;
2159
+ async listInvitations(orgId, token, limit, cursor) {
2160
+ const params = new URLSearchParams();
2161
+ if (limit !== void 0) params.set("limit", String(limit));
2162
+ if (cursor !== void 0) params.set("cursor", String(cursor));
2163
+ const qs = params.toString();
2164
+ const path = `/v1/orgs/${orgId}/invitations` + (qs ? `?${qs}` : "");
2091
2165
  return this.request(
2092
2166
  "GET",
2093
2167
  path,
@@ -2112,8 +2186,12 @@ var AuthClient = class {
2112
2186
  * List members
2113
2187
  * GET /v1/orgs/{orgId}/members
2114
2188
  */
2115
- async listMembers(orgId, token) {
2116
- const path = `/v1/orgs/${orgId}/members`;
2189
+ async listMembers(orgId, token, limit, cursor) {
2190
+ const params = new URLSearchParams();
2191
+ if (limit !== void 0) params.set("limit", String(limit));
2192
+ if (cursor !== void 0) params.set("cursor", String(cursor));
2193
+ const qs = params.toString();
2194
+ const path = `/v1/orgs/${orgId}/members` + (qs ? `?${qs}` : "");
2117
2195
  return this.request(
2118
2196
  "GET",
2119
2197
  path,
@@ -2164,8 +2242,12 @@ var AuthClient = class {
2164
2242
  * List teams
2165
2243
  * GET /v1/orgs/{orgId}/teams
2166
2244
  */
2167
- async listTeams(orgId, token) {
2168
- const path = `/v1/orgs/${orgId}/teams`;
2245
+ async listTeams(orgId, token, limit, cursor) {
2246
+ const params = new URLSearchParams();
2247
+ if (limit !== void 0) params.set("limit", String(limit));
2248
+ if (cursor !== void 0) params.set("cursor", String(cursor));
2249
+ const qs = params.toString();
2250
+ const path = `/v1/orgs/${orgId}/teams` + (qs ? `?${qs}` : "");
2169
2251
  return this.request(
2170
2252
  "GET",
2171
2253
  path,
@@ -2512,8 +2594,12 @@ var AuthClient = class {
2512
2594
  * List sessions
2513
2595
  * GET /v1/sessions
2514
2596
  */
2515
- async listSessions(token) {
2516
- const path = "/v1/sessions";
2597
+ async listSessions(token, limit, cursor) {
2598
+ const params = new URLSearchParams();
2599
+ if (limit !== void 0) params.set("limit", String(limit));
2600
+ if (cursor !== void 0) params.set("cursor", String(cursor));
2601
+ const qs = params.toString();
2602
+ const path = "/v1/sessions" + (qs ? `?${qs}` : "");
2517
2603
  return this.request(
2518
2604
  "GET",
2519
2605
  path,
@@ -2521,6 +2607,19 @@ var AuthClient = class {
2521
2607
  token
2522
2608
  );
2523
2609
  }
2610
+ /**
2611
+ * Revoke other sessions
2612
+ * DELETE /v1/sessions
2613
+ */
2614
+ async revokeOtherSessions(token) {
2615
+ const path = "/v1/sessions";
2616
+ return this.request(
2617
+ "DELETE",
2618
+ path,
2619
+ void 0,
2620
+ token
2621
+ );
2622
+ }
2524
2623
  /**
2525
2624
  * Revoke session
2526
2625
  * DELETE /v1/sessions/{sessionId}
@@ -2821,6 +2920,19 @@ var AuthClient = class {
2821
2920
  token
2822
2921
  );
2823
2922
  }
2923
+ /**
2924
+ * Rotate webhook secret
2925
+ * POST /v1/webhooks/{webhookId}/rotate-secret
2926
+ */
2927
+ async rotateWebhookSecret(webhookId, token) {
2928
+ const path = `/v1/webhooks/${webhookId}/rotate-secret`;
2929
+ return this.request(
2930
+ "POST",
2931
+ path,
2932
+ void 0,
2933
+ token
2934
+ );
2935
+ }
2824
2936
  // ──────────────────────────────────────────────────
2825
2937
  // Internal request helper
2826
2938
  // ──────────────────────────────────────────────────
@@ -2883,7 +2995,65 @@ var AuthClientError = class extends Error {
2883
2995
  };
2884
2996
 
2885
2997
  // src/client.ts
2998
+ function readOrganizationList(res) {
2999
+ const body = res ?? {};
3000
+ const organizations = body.organizations ?? body.items ?? [];
3001
+ return {
3002
+ organizations,
3003
+ nextCursor: body.next_cursor || void 0,
3004
+ total: typeof body.total === "number" ? body.total : organizations.length
3005
+ };
3006
+ }
2886
3007
  var AuthClient2 = class extends AuthClient {
3008
+ waitlistURL;
3009
+ waitlistFetch;
3010
+ constructor(config) {
3011
+ const fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
3012
+ let baseURL = config.baseURL;
3013
+ while (baseURL.endsWith("/")) baseURL = baseURL.slice(0, -1);
3014
+ const deviceCompletionURL = `${baseURL}/v1/oauth/device/complete`;
3015
+ super({
3016
+ ...config,
3017
+ fetch: (input, init) => {
3018
+ const isDeviceCompletion = input === deviceCompletionURL && init?.method === "POST";
3019
+ return fetchFn(
3020
+ input,
3021
+ isDeviceCompletion ? { ...init, credentials: "include" } : init
3022
+ );
3023
+ }
3024
+ });
3025
+ this.waitlistURL = `${baseURL}/v1/waitlist/join`;
3026
+ this.waitlistFetch = fetchFn;
3027
+ }
3028
+ /** Join the waitlist, or return the existing entry and its approval status. */
3029
+ async joinWaitlist(body) {
3030
+ const headers = {
3031
+ "Content-Type": "application/json",
3032
+ Accept: "application/json"
3033
+ };
3034
+ const key = this.getPublishableKey();
3035
+ if (key) headers["X-Publishable-Key"] = key;
3036
+ const response = await this.waitlistFetch(this.waitlistURL, {
3037
+ method: "POST",
3038
+ headers,
3039
+ body: JSON.stringify({ ...body, email: body.email.trim().toLowerCase() })
3040
+ });
3041
+ const data = await response.json().catch(() => null);
3042
+ if (!response.ok) {
3043
+ throw new AuthClientError(
3044
+ data?.error ?? `Request failed with status ${response.status}`,
3045
+ response.status,
3046
+ data?.type,
3047
+ data ?? void 0
3048
+ );
3049
+ }
3050
+ if (typeof data?.email !== "string" || !["pending", "approved", "rejected"].includes(data?.status)) {
3051
+ throw new AuthClientError(
3052
+ "Unable to check waitlist status. Please try again."
3053
+ );
3054
+ }
3055
+ return data;
3056
+ }
2887
3057
  /**
2888
3058
  * Refresh session tokens.
2889
3059
  *
@@ -2908,97 +3078,44 @@ var AuthClient2 = class extends AuthClient {
2908
3078
  }
2909
3079
  return super.signOut(token);
2910
3080
  }
2911
- /**
2912
- * MFA challenge — bridge for auth.ts.
2913
- *
2914
- * auth.ts calls `mfaChallenge({ enrollment_id, code })` and expects
2915
- * an AuthResponse back. The generated client uses `challengeMFA`.
2916
- */
3081
+ /** @deprecated Use challengeMFA with the ticket from sign-in. */
2917
3082
  async mfaChallenge(body) {
3083
+ if (!("mfa_ticket" in body) || !body.mfa_ticket) {
3084
+ throw new Error(
3085
+ "MFA challenge requires an authentication ticket, not an enrollment ID"
3086
+ );
3087
+ }
2918
3088
  return super.challengeMFA(body);
2919
3089
  }
2920
- /**
2921
- * Verify an MFA recovery code.
2922
- *
2923
- * Accepts a raw code string (used by auth.ts) or the full request body.
2924
- */
3090
+ /** Verify a recovery code in an existing cookie-authenticated session. */
2925
3091
  async verifyRecoveryCode(body) {
2926
- const req = typeof body === "string" ? { code: body } : body;
2927
- return super.verifyRecoveryCode(req);
3092
+ return super.verifyRecoveryCode(
3093
+ typeof body === "string" ? { code: body } : body
3094
+ );
2928
3095
  }
2929
- /**
2930
- * Send an SMS code for MFA — bridge for auth.ts.
2931
- *
2932
- * Simplifies the call-site by accepting just the session token.
2933
- */
3096
+ /** Send an SMS code for the authenticated session. */
2934
3097
  async sendSMSCodeForMFA(token) {
2935
3098
  return super.sendSMSCode({}, token);
2936
3099
  }
2937
- /**
2938
- * Verify an SMS code for MFA — bridge for auth.ts.
2939
- *
2940
- * Simplifies the call-site by accepting code + token directly.
2941
- */
3100
+ /** Verify an SMS code without issuing a session. */
2942
3101
  async verifySMSCodeForMFA(code, token) {
2943
3102
  return super.verifySMSCode({ code }, token);
2944
3103
  }
2945
- /**
2946
- * Complete a device authorization (RFC 8628).
2947
- *
2948
- * Approves or denies a device code. Requires an authenticated session —
2949
- * pass the session token so the backend can identify the user.
2950
- */
3104
+ /** Approve or deny a device using a bearer token or the existing session cookie. */
2951
3105
  async completeDeviceAuthorization(userCode, action, token) {
2952
- const base = this.baseURL;
2953
- const fetchFn = this.fetchFn;
2954
- const headers = {
2955
- "Content-Type": "application/json"
2956
- };
2957
- if (token) {
2958
- headers.Authorization = `Bearer ${token}`;
2959
- }
2960
- const res = await fetchFn(`${base}/v1/oauth/device/complete`, {
2961
- method: "POST",
2962
- headers,
2963
- credentials: "include",
2964
- body: JSON.stringify({ user_code: userCode, action })
2965
- });
2966
- if (!res.ok) {
2967
- const body = await res.json().catch(() => ({}));
2968
- throw new AuthClientError(
2969
- body.error || "Failed to authorize device",
2970
- res.status
2971
- );
2972
- }
2973
- return res.json();
3106
+ return super.oauth2DeviceComplete(
3107
+ { user_code: userCode, action },
3108
+ token ?? ""
3109
+ );
2974
3110
  }
2975
3111
  /**
2976
3112
  * Fetch the client configuration from the backend.
2977
3113
  *
2978
3114
  * The config describes which auth methods are enabled so SDK
2979
3115
  * components can auto-configure without manual props.
2980
- *
2981
- * Reads `baseURL` and `fetchFn` directly off the generated parent
2982
- * class — earlier generator templates kept a `this.config` ref but
2983
- * the current embedded template stores fields individually, so the
2984
- * old `(this as any).config.baseURL` shape produced `undefined` and
2985
- * the URL constructor threw.
2986
3116
  */
2987
3117
  async fetchClientConfig(publishableKey) {
2988
- const baseURL = this.baseURL;
2989
- const fetchFn = this.fetchFn ?? globalThis.fetch;
2990
- const url = new URL("/v1/client-config", baseURL);
2991
- if (publishableKey) {
2992
- url.searchParams.set("key", publishableKey);
2993
- }
2994
- const res = await fetchFn(url.toString(), {
2995
- method: "GET",
2996
- headers: { "Content-Type": "application/json" }
2997
- });
2998
- if (!res.ok) {
2999
- throw new AuthClientError("Failed to fetch client config", res.status);
3000
- }
3001
- return res.json();
3118
+ return super.getClientConfig("", publishableKey);
3002
3119
  }
3003
3120
  };
3004
3121
 
@@ -3007,6 +3124,7 @@ var SESSION_STORAGE_KEY = "authsome:session";
3007
3124
  var SESSION_KEY = SESSION_STORAGE_KEY;
3008
3125
  var CONFIG_KEY = "authsome:client_config";
3009
3126
  var REFRESH_BEFORE_MS = 6e4;
3127
+ var MAX_TIMEOUT_MS = 2147483647;
3010
3128
  var CONFIG_TTL_MS = 5 * 6e4;
3011
3129
  var DEFAULT_SESSION_TTL_MS = 36e5;
3012
3130
  var MAX_REFRESH_ATTEMPTS = 5;
@@ -3045,19 +3163,45 @@ function isAuthRejection(err) {
3045
3163
  }
3046
3164
  return false;
3047
3165
  }
3166
+ var refreshQueues = /* @__PURE__ */ new Map();
3167
+ async function withRefreshLock(key, operation) {
3168
+ if (typeof navigator !== "undefined" && navigator.locks) {
3169
+ await navigator.locks.request(key, operation);
3170
+ return;
3171
+ }
3172
+ const previous = refreshQueues.get(key) ?? Promise.resolve();
3173
+ const next = previous.catch(() => {
3174
+ }).then(operation);
3175
+ refreshQueues.set(key, next);
3176
+ try {
3177
+ await next;
3178
+ } finally {
3179
+ if (refreshQueues.get(key) === next) refreshQueues.delete(key);
3180
+ }
3181
+ }
3048
3182
  var AuthManager = class {
3049
3183
  client;
3050
3184
  storage;
3051
3185
  state = { status: "idle" };
3052
3186
  listeners = /* @__PURE__ */ new Set();
3187
+ initializePromise = null;
3188
+ refreshPromise = null;
3189
+ refreshLockKey;
3190
+ active = true;
3191
+ smsCeremonyVersion = 0;
3053
3192
  refreshTimer = null;
3054
3193
  onError;
3055
3194
  publishableKey;
3195
+ configCacheKey;
3056
3196
  clientConfig = null;
3057
3197
  configListeners = /* @__PURE__ */ new Set();
3058
3198
  configFetchPromise = null;
3059
3199
  constructor(config) {
3060
3200
  this.client = new AuthClient2(config);
3201
+ let baseEnd = config.baseURL.length;
3202
+ while (baseEnd > 0 && config.baseURL[baseEnd - 1] === "/") baseEnd--;
3203
+ this.configCacheKey = `${CONFIG_KEY}:${JSON.stringify([config.baseURL.slice(0, baseEnd), config.publishableKey ?? ""])}`;
3204
+ this.refreshLockKey = `authsome:refresh:${config.baseURL}:${config.publishableKey ?? ""}`;
3061
3205
  this.storage = config.storage ?? createMemoryStorage();
3062
3206
  this.onError = config.onError;
3063
3207
  this.publishableKey = config.publishableKey;
@@ -3089,9 +3233,20 @@ var AuthManager = class {
3089
3233
  * When a publishableKey is set, also fetches client config in parallel.
3090
3234
  * Call this once on app start.
3091
3235
  */
3092
- async initialize() {
3236
+ initialize() {
3237
+ this.active = true;
3238
+ if (!this.initializePromise) {
3239
+ this.initializePromise = this.restoreSession().finally(() => {
3240
+ this.initializePromise = null;
3241
+ });
3242
+ }
3243
+ return this.initializePromise;
3244
+ }
3245
+ async restoreSession() {
3246
+ let session;
3093
3247
  if (this.publishableKey && !this.clientConfig) {
3094
- void this.fetchClientConfig();
3248
+ void this.fetchClientConfig().catch(() => {
3249
+ });
3095
3250
  }
3096
3251
  try {
3097
3252
  const raw = await this.storage.getItem(SESSION_KEY);
@@ -3099,7 +3254,7 @@ var AuthManager = class {
3099
3254
  this.setState({ status: "unauthenticated" });
3100
3255
  return;
3101
3256
  }
3102
- const session = JSON.parse(raw);
3257
+ session = JSON.parse(raw);
3103
3258
  const expiresAt = new Date(session.expires_at).getTime();
3104
3259
  if (Date.now() >= expiresAt) {
3105
3260
  await this.refreshSession(session.refresh_token);
@@ -3110,7 +3265,11 @@ var AuthManager = class {
3110
3265
  this.setState({ status: "authenticated", user, session });
3111
3266
  this.scheduleRefresh(session);
3112
3267
  } catch (err) {
3113
- await this.handleInitFailure(err);
3268
+ if (session?.refresh_token && isAuthRejection(err)) {
3269
+ await this.refreshSession(session.refresh_token);
3270
+ } else {
3271
+ await this.handleInitFailure(err);
3272
+ }
3114
3273
  }
3115
3274
  }
3116
3275
  /**
@@ -3146,7 +3305,12 @@ var AuthManager = class {
3146
3305
  return;
3147
3306
  }
3148
3307
  this.setState({ status: "unknown", session });
3149
- this.scheduleRefresh(session);
3308
+ this.clearRefreshTimer();
3309
+ if (this.active) {
3310
+ this.refreshTimer = setTimeout(() => {
3311
+ void this.initialize();
3312
+ }, REFRESH_RETRY_BASE_MS);
3313
+ }
3150
3314
  }
3151
3315
  /** Sign in with email & password. */
3152
3316
  async signIn(credentials) {
@@ -3179,10 +3343,51 @@ var AuthManager = class {
3179
3343
  throw err;
3180
3344
  }
3181
3345
  }
3346
+ /**
3347
+ * Begin an SSO login against one of the app's configured connections.
3348
+ *
3349
+ * Resolves to the IdP `login_url`; the caller navigates the browser there.
3350
+ * `returnUrl` is where the backend lands the browser after the IdP round
3351
+ * trip, with `?code=` on success or `?sso_error=` on failure. It must be an
3352
+ * allowlisted https origin (or localhost). Omit it to use the server's
3353
+ * default `/sso/callback` landing.
3354
+ */
3355
+ async startSSOLogin(connectionId, returnUrl) {
3356
+ const res = await this.client.startSSOLogin(connectionId, returnUrl);
3357
+ return res.login_url;
3358
+ }
3359
+ /**
3360
+ * Finish an SSO login by exchanging the one-time `code` from the callback
3361
+ * URL for a session. The exchange is publishable-key authed, so no bearer
3362
+ * token is sent.
3363
+ */
3364
+ async completeSSOLogin(code) {
3365
+ try {
3366
+ const res = await this.client.ssoExchange({ code }, "");
3367
+ const session = toSession(res);
3368
+ await this.handleAuthResponse(res.user, session);
3369
+ } catch (err) {
3370
+ this.handleError(err);
3371
+ throw err;
3372
+ }
3373
+ }
3182
3374
  /** Sign up with email & password. */
3183
3375
  async signUp(data) {
3184
3376
  try {
3185
- await this.client.signUp(data);
3377
+ const config = this.clientConfig ?? (this.publishableKey ? await this.fetchClientConfig() : null);
3378
+ if (config?.signup_enabled === false)
3379
+ throw new Error("Signup isn't available for this app.");
3380
+ const res = await this.client.signUp(data);
3381
+ const verification = config?.email_verification;
3382
+ if (verification && (!verification.enabled || !verification.required)) {
3383
+ try {
3384
+ const user = await this.client.getMe(res.session_token);
3385
+ await this.handleAuthResponse(user, toSession(res));
3386
+ return;
3387
+ } catch (err) {
3388
+ if (!isAuthRejection(err)) throw err;
3389
+ }
3390
+ }
3186
3391
  this.setState({
3187
3392
  status: "verification_pending",
3188
3393
  email: data.email
@@ -3213,6 +3418,8 @@ var AuthManager = class {
3213
3418
  }
3214
3419
  /** Sign out and clear the session. */
3215
3420
  async signOut() {
3421
+ this.smsCeremonyVersion++;
3422
+ this.clearRefreshTimer();
3216
3423
  const token = this.getSessionToken();
3217
3424
  if (token) {
3218
3425
  try {
@@ -3237,7 +3444,9 @@ var AuthManager = class {
3237
3444
  async submitMFAChallenge(code) {
3238
3445
  const state = this.state;
3239
3446
  if (state.status !== "mfa_required") {
3240
- throw new Error("auth: submitMFAChallenge called outside the mfa_required state");
3447
+ throw new Error(
3448
+ "auth: submitMFAChallenge called outside the mfa_required state"
3449
+ );
3241
3450
  }
3242
3451
  try {
3243
3452
  const res = await this.client.challengeMFA({
@@ -3262,41 +3471,51 @@ var AuthManager = class {
3262
3471
  async submitMFACode(_enrollmentId, code) {
3263
3472
  return this.submitMFAChallenge(code);
3264
3473
  }
3265
- /** Submit an MFA recovery code. */
3474
+ /** Submit a recovery code against the active login ticket. */
3266
3475
  async submitRecoveryCode(code) {
3267
- this.setState({ status: "loading" });
3268
- try {
3269
- const res = await this.client.verifyRecoveryCode(code);
3270
- const session = toSession(res);
3271
- await this.handleAuthResponse(res.user, session);
3272
- } catch (err) {
3273
- this.handleError(err);
3274
- }
3476
+ return this.submitMFAChallenge(code);
3275
3477
  }
3276
- /** Send an SMS code for MFA verification. Returns masked phone + expiry info. */
3478
+ /** Send an SMS code for an already authenticated session. */
3277
3479
  async sendSMSCode() {
3278
- const token = this.getSessionToken();
3279
- if (!token) throw new Error("No session token available");
3280
- return this.client.sendSMSCodeForMFA(token);
3480
+ const state = this.requireSMSAuthState();
3481
+ const version = this.smsCeremonyVersion;
3482
+ const result = await this.client.sendSMSCodeForMFA(
3483
+ state.session.session_token
3484
+ );
3485
+ this.checkSMSAuthState(state, version);
3486
+ if (!result.sent) throw new Error("SMS code was not sent");
3487
+ return result;
3281
3488
  }
3282
- /** Submit an SMS verification code during MFA challenge. */
3489
+ /** Verify an SMS ceremony without replacing the authenticated session. */
3283
3490
  async submitSMSCode(code) {
3284
- this.setState({ status: "loading" });
3285
- try {
3286
- const token = this.getSessionToken();
3287
- if (!token) throw new Error("No session token available");
3288
- const res = await this.client.verifySMSCodeForMFA(code, token);
3289
- const session = toSession(res);
3290
- await this.handleAuthResponse(res.user, session);
3291
- } catch (err) {
3292
- this.handleError(err);
3491
+ const state = this.requireSMSAuthState();
3492
+ const version = this.smsCeremonyVersion;
3493
+ const result = await this.client.verifySMSCodeForMFA(
3494
+ code,
3495
+ state.session.session_token
3496
+ );
3497
+ this.checkSMSAuthState(state, version);
3498
+ if (!result.verified || result.method !== "sms") {
3499
+ throw new Error("SMS verification failed");
3500
+ }
3501
+ }
3502
+ requireSMSAuthState() {
3503
+ if (!this.active || this.state.status !== "authenticated" || !this.state.session.session_token) {
3504
+ throw new Error("SMS verification requires an authenticated session");
3505
+ }
3506
+ return this.state;
3507
+ }
3508
+ checkSMSAuthState(state, version) {
3509
+ if (!this.active || this.state !== state || this.smsCeremonyVersion !== version) {
3510
+ throw new Error("Session changed during SMS verification");
3293
3511
  }
3294
3512
  }
3295
3513
  /** Refresh the current session manually. */
3296
3514
  async refreshNow() {
3297
- const state = this.state;
3298
- if (state.status !== "authenticated") return;
3299
- await this.refreshSession(state.session.refresh_token);
3515
+ const raw = await this.storage.getItem(SESSION_KEY);
3516
+ if (!raw) return;
3517
+ const session = JSON.parse(raw);
3518
+ if (session.refresh_token) await this.refreshSession(session.refresh_token);
3300
3519
  }
3301
3520
  /**
3302
3521
  * Get the current session token (if authenticated). Returns null
@@ -3346,7 +3565,7 @@ var AuthManager = class {
3346
3565
  return this.configFetchPromise;
3347
3566
  }
3348
3567
  try {
3349
- const cached = await this.storage.getItem(CONFIG_KEY);
3568
+ const cached = await this.storage.getItem(this.configCacheKey);
3350
3569
  if (cached) {
3351
3570
  const { config, fetchedAt } = JSON.parse(cached);
3352
3571
  if (Date.now() - fetchedAt < CONFIG_TTL_MS) {
@@ -3360,7 +3579,7 @@ var AuthManager = class {
3360
3579
  this.setClientConfig(config);
3361
3580
  try {
3362
3581
  await this.storage.setItem(
3363
- CONFIG_KEY,
3582
+ this.configCacheKey,
3364
3583
  JSON.stringify({ config, fetchedAt: Date.now() })
3365
3584
  );
3366
3585
  } catch {
@@ -3373,6 +3592,7 @@ var AuthManager = class {
3373
3592
  }
3374
3593
  /** Tear down: clear timers and listeners. */
3375
3594
  destroy() {
3595
+ this.active = false;
3376
3596
  this.clearRefreshTimer();
3377
3597
  this.listeners.clear();
3378
3598
  this.configListeners.clear();
@@ -3383,43 +3603,46 @@ var AuthManager = class {
3383
3603
  this.setState({ status: "authenticated", user, session });
3384
3604
  this.scheduleRefresh(session);
3385
3605
  }
3386
- /**
3387
- * Exchanges the refresh token for a new session.
3388
- *
3389
- * On failure the two cases are separated, as they are in initialize: a
3390
- * rejected token means signed out, anything else means no verdict and is
3391
- * worth retrying.
3392
- *
3393
- * `attempt` was previously accepted and never read, and the retry path
3394
- * called back in without it — so a permanently-invalid token retried every
3395
- * 30s for as long as the tab stayed open, and the user was never signed out.
3396
- * It now bounds the retries and backs off between them.
3397
- */
3398
- async refreshSession(refreshToken, attempt = 0) {
3606
+ // Keep rotation and persistence in the same lock. Once the server rotates a
3607
+ // token, every later operation must read its replacement from storage.
3608
+ refreshSession(refreshToken, attempt = 0) {
3609
+ if (!this.refreshPromise) {
3610
+ this.refreshPromise = withRefreshLock(
3611
+ this.refreshLockKey,
3612
+ () => this.exchangeSession(refreshToken, attempt)
3613
+ ).finally(() => {
3614
+ this.refreshPromise = null;
3615
+ });
3616
+ }
3617
+ return this.refreshPromise;
3618
+ }
3619
+ async exchangeSession(refreshToken, attempt) {
3620
+ let current;
3621
+ let exchanged = false;
3399
3622
  try {
3400
- const newSession = await this.client.refresh(refreshToken);
3401
- const user = await this.client.getMe(newSession.session_token);
3402
- await this.persistSession(newSession);
3403
- this.setState({ status: "authenticated", user, session: newSession });
3404
- this.scheduleRefresh(newSession);
3405
- } catch (err) {
3406
- if (isAuthRejection(err)) {
3623
+ const raw = await this.storage.getItem(SESSION_KEY);
3624
+ if (!raw) {
3407
3625
  this.setState({ status: "unauthenticated" });
3408
3626
  return;
3409
3627
  }
3410
- if (attempt >= MAX_REFRESH_ATTEMPTS) {
3411
- const stored = await this.storage.getItem(SESSION_KEY);
3412
- if (stored) {
3413
- try {
3414
- this.setState({ status: "unknown", session: JSON.parse(stored) });
3415
- return;
3416
- } catch {
3417
- }
3418
- }
3628
+ current = JSON.parse(raw);
3629
+ if (current.refresh_token === refreshToken || Date.now() >= Date.parse(current.expires_at)) {
3630
+ current = await this.client.refresh(current.refresh_token);
3631
+ exchanged = true;
3632
+ await this.persistSession(current);
3633
+ }
3634
+ const user = await this.client.getMe(current.session_token);
3635
+ this.setState({ status: "authenticated", user, session: current });
3636
+ this.scheduleRefresh(current);
3637
+ } catch (err) {
3638
+ this.clearRefreshTimer();
3639
+ if (!exchanged && isAuthRejection(err) && current?.refresh_token === refreshToken) {
3640
+ await this.clearSession();
3419
3641
  this.setState({ status: "unauthenticated" });
3420
3642
  return;
3421
3643
  }
3422
- this.clearRefreshTimer();
3644
+ if (current) this.setState({ status: "unknown", session: current });
3645
+ if (attempt >= MAX_REFRESH_ATTEMPTS || !this.active) return;
3423
3646
  this.refreshTimer = setTimeout(
3424
3647
  () => {
3425
3648
  void this.refreshSession(refreshToken, attempt + 1);
@@ -3430,15 +3653,19 @@ var AuthManager = class {
3430
3653
  }
3431
3654
  scheduleRefresh(session) {
3432
3655
  this.clearRefreshTimer();
3656
+ if (!this.active) return;
3433
3657
  const expiresAt = new Date(session.expires_at).getTime();
3434
3658
  const delay = expiresAt - Date.now() - REFRESH_BEFORE_MS;
3435
- if (delay <= 0) {
3659
+ if (!Number.isFinite(expiresAt) || delay <= 0) {
3436
3660
  void this.refreshSession(session.refresh_token);
3437
3661
  return;
3438
3662
  }
3439
- this.refreshTimer = setTimeout(() => {
3440
- void this.refreshSession(session.refresh_token);
3441
- }, delay);
3663
+ this.refreshTimer = setTimeout(
3664
+ () => {
3665
+ this.scheduleRefresh(session);
3666
+ },
3667
+ Math.min(delay, MAX_TIMEOUT_MS)
3668
+ );
3442
3669
  }
3443
3670
  clearRefreshTimer() {
3444
3671
  if (this.refreshTimer !== null) {
@@ -3616,6 +3843,7 @@ export {
3616
3843
  isSafeRedirect,
3617
3844
  prepareCreationOptions,
3618
3845
  prepareRequestOptions,
3846
+ readOrganizationList,
3619
3847
  safeRedirectTarget,
3620
3848
  serializeCredential
3621
3849
  };