@authsome/ui-core 1.6.0 → 1.6.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.d.ts +126 -2
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -1448,6 +1448,23 @@ type AuthState = {
|
|
|
1448
1448
|
} | {
|
|
1449
1449
|
status: "unauthenticated";
|
|
1450
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* A session exists in storage but could not be validated because the auth
|
|
1453
|
+
* server was unreachable or errored — as distinct from the server rejecting
|
|
1454
|
+
* it, which yields "unauthenticated".
|
|
1455
|
+
*
|
|
1456
|
+
* This is deliberately NOT "authenticated": nothing has proven the token is
|
|
1457
|
+
* still good, so `isAuthenticated` is false and protected UI must stay
|
|
1458
|
+
* hidden. The session is retained and a refresh is scheduled, so a
|
|
1459
|
+
* transient outage resolves itself without signing the user out.
|
|
1460
|
+
*
|
|
1461
|
+
* Treat it as "signed out, but don't discard their session yet". An app that
|
|
1462
|
+
* wants to render optimistically can branch on it explicitly.
|
|
1463
|
+
*/
|
|
1464
|
+
| {
|
|
1465
|
+
status: "unknown";
|
|
1466
|
+
session: Session;
|
|
1467
|
+
}
|
|
1451
1468
|
/**
|
|
1452
1469
|
* Sign-in returned the MFA gate (HTTP 403 with type:"mfa_required").
|
|
1453
1470
|
* The user must complete the second factor against the same ticket
|
|
@@ -1498,7 +1515,14 @@ interface AuthConfig {
|
|
|
1498
1515
|
initialClientConfig?: ClientConfig;
|
|
1499
1516
|
/** Custom fetch implementation (defaults to global fetch). */
|
|
1500
1517
|
fetch?: typeof fetch;
|
|
1501
|
-
/**
|
|
1518
|
+
/**
|
|
1519
|
+
* Storage implementation for persisting tokens.
|
|
1520
|
+
*
|
|
1521
|
+
* Defaults to in-memory, so tokens do not survive a reload and no other
|
|
1522
|
+
* script on the page can read them. Pass createLocalStorage() to persist
|
|
1523
|
+
* across reloads, accepting that an XSS can then read the refresh token;
|
|
1524
|
+
* prefer backend-set httpOnly cookies where available.
|
|
1525
|
+
*/
|
|
1502
1526
|
storage?: TokenStorage;
|
|
1503
1527
|
/** Callback invoked when the auth state changes. */
|
|
1504
1528
|
onStateChange?: (state: AuthState) => void;
|
|
@@ -2704,6 +2728,25 @@ declare class AuthClient extends AuthClient$1 {
|
|
|
2704
2728
|
* class and expose reactive state.
|
|
2705
2729
|
*/
|
|
2706
2730
|
|
|
2731
|
+
/**
|
|
2732
|
+
* Storage key under which the whole Session (tokens + expiry) is persisted.
|
|
2733
|
+
*
|
|
2734
|
+
* Exported because a cookie-backed TokenStorage writes it as a cookie name,
|
|
2735
|
+
* and server-side readers have to know what to look for. See
|
|
2736
|
+
* @authsome/ui-nextjs readSessionToken.
|
|
2737
|
+
*/
|
|
2738
|
+
declare const SESSION_STORAGE_KEY = "authsome:session";
|
|
2739
|
+
/**
|
|
2740
|
+
* Token storage backed by window.localStorage, falling back to memory where
|
|
2741
|
+
* localStorage is unavailable (SSR, restricted contexts).
|
|
2742
|
+
*
|
|
2743
|
+
* Opt in only if you understand the exposure: everything written here,
|
|
2744
|
+
* including the refresh token, is readable by any script running on the page.
|
|
2745
|
+
* Prefer backend-set httpOnly cookies where you can.
|
|
2746
|
+
*
|
|
2747
|
+
* new AuthManager({ baseURL, storage: createLocalStorage() })
|
|
2748
|
+
*/
|
|
2749
|
+
declare function createLocalStorage(): TokenStorage;
|
|
2707
2750
|
/**
|
|
2708
2751
|
* AuthManager is the core state machine that drives authentication.
|
|
2709
2752
|
*
|
|
@@ -2739,6 +2782,22 @@ declare class AuthManager {
|
|
|
2739
2782
|
* Call this once on app start.
|
|
2740
2783
|
*/
|
|
2741
2784
|
initialize(): Promise<void>;
|
|
2785
|
+
/**
|
|
2786
|
+
* Resolves the state when hydrating a stored session failed.
|
|
2787
|
+
*
|
|
2788
|
+
* The two failure modes must not be conflated. A rejected credential means
|
|
2789
|
+
* the user is signed out. An unreachable server means we simply do not know,
|
|
2790
|
+
* and signing them out over a transient blip is its own bug — which is what
|
|
2791
|
+
* the previous behaviour tried to avoid, by reporting "authenticated" with a
|
|
2792
|
+
* null user. That kept people signed in through an outage but told every
|
|
2793
|
+
* consumer gating on `isAuthenticated` to render protected UI for a session
|
|
2794
|
+
* nothing had validated, and made `user.email` a crash.
|
|
2795
|
+
*
|
|
2796
|
+
* So: rejection signs out; unavailability yields "unknown", which is not
|
|
2797
|
+
* authenticated but retains the session and keeps a refresh scheduled so the
|
|
2798
|
+
* app recovers on its own.
|
|
2799
|
+
*/
|
|
2800
|
+
private handleInitFailure;
|
|
2742
2801
|
/** Sign in with email & password. */
|
|
2743
2802
|
signIn(credentials: SignInRequest): Promise<void>;
|
|
2744
2803
|
/** Sign up with email & password. */
|
|
@@ -2812,6 +2871,18 @@ declare class AuthManager {
|
|
|
2812
2871
|
/** Tear down: clear timers and listeners. */
|
|
2813
2872
|
destroy(): void;
|
|
2814
2873
|
private handleAuthResponse;
|
|
2874
|
+
/**
|
|
2875
|
+
* Exchanges the refresh token for a new session.
|
|
2876
|
+
*
|
|
2877
|
+
* On failure the two cases are separated, as they are in initialize: a
|
|
2878
|
+
* rejected token means signed out, anything else means no verdict and is
|
|
2879
|
+
* worth retrying.
|
|
2880
|
+
*
|
|
2881
|
+
* `attempt` was previously accepted and never read, and the retry path
|
|
2882
|
+
* called back in without it — so a permanently-invalid token retried every
|
|
2883
|
+
* 30s for as long as the tab stayed open, and the user was never signed out.
|
|
2884
|
+
* It now bounds the retries and backs off between them.
|
|
2885
|
+
*/
|
|
2815
2886
|
private refreshSession;
|
|
2816
2887
|
private scheduleRefresh;
|
|
2817
2888
|
private clearRefreshTimer;
|
|
@@ -2822,6 +2893,59 @@ declare class AuthManager {
|
|
|
2822
2893
|
private handleError;
|
|
2823
2894
|
}
|
|
2824
2895
|
|
|
2896
|
+
/**
|
|
2897
|
+
* Redirect-target validation.
|
|
2898
|
+
*
|
|
2899
|
+
* Sign-in and sign-up flows accept a `?redirect=` parameter so a user who was
|
|
2900
|
+
* bounced to the auth page lands back where they started. That parameter is
|
|
2901
|
+
* attacker-controlled: without validation, `?redirect=https://evil.com` sends
|
|
2902
|
+
* the user to a hostile page *immediately after they authenticate*, which is
|
|
2903
|
+
* the most convincing possible moment to present a fake "confirm your
|
|
2904
|
+
* password" screen. It can also carry `javascript:` payloads.
|
|
2905
|
+
*
|
|
2906
|
+
* The check here resolves the candidate against the current origin and
|
|
2907
|
+
* compares origins, rather than inspecting the string. String checks are the
|
|
2908
|
+
* usual approach and they leak: `startsWith("/")` accepts `//evil.com` and
|
|
2909
|
+
* `/\evil.com`, both of which browsers resolve to a different host.
|
|
2910
|
+
*/
|
|
2911
|
+
/** Options controlling which redirect targets are acceptable. */
|
|
2912
|
+
interface SafeRedirectOptions {
|
|
2913
|
+
/**
|
|
2914
|
+
* Origin the app is currently served from — `window.location.origin` in a
|
|
2915
|
+
* browser. Redirects to this origin are always allowed.
|
|
2916
|
+
*/
|
|
2917
|
+
currentOrigin: string;
|
|
2918
|
+
/**
|
|
2919
|
+
* Extra origins accepted as redirect targets, for deployments where the auth
|
|
2920
|
+
* UI and the app live on different hosts. Empty by default: same-origin only.
|
|
2921
|
+
*
|
|
2922
|
+
* Entries are matched **exactly**, by origin. Wildcards and subdomain
|
|
2923
|
+
* patterns are deliberately unsupported — every tenant host must be listed
|
|
2924
|
+
* in full. Pattern matching here is a well-known source of account-takeover
|
|
2925
|
+
* bugs, and an explicit list is worth the verbosity.
|
|
2926
|
+
*
|
|
2927
|
+
* Each entry must be an absolute URL ("https://portal.example.com"); it is
|
|
2928
|
+
* reduced to its origin, so a trailing slash, a path, or a capitalized host
|
|
2929
|
+
* are all tolerated. Entries that cannot be parsed are ignored and will
|
|
2930
|
+
* never match.
|
|
2931
|
+
*/
|
|
2932
|
+
allowedOrigins?: string[];
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Reports whether `candidate` is safe to navigate to after authentication.
|
|
2936
|
+
*
|
|
2937
|
+
* Accepts relative paths ("/dashboard", "/a/b?x=1#f") and absolute URLs whose
|
|
2938
|
+
* origin is the current origin or an allowed one. Refuses everything else,
|
|
2939
|
+
* including a malformed candidate.
|
|
2940
|
+
*/
|
|
2941
|
+
declare function isSafeRedirect(candidate: string | null | undefined, options: SafeRedirectOptions): boolean;
|
|
2942
|
+
/**
|
|
2943
|
+
* Returns `candidate` when it passes {@link isSafeRedirect}, otherwise
|
|
2944
|
+
* `fallback`. Use this at navigation sites so an unsafe value degrades to a
|
|
2945
|
+
* known-good destination instead of blocking the user.
|
|
2946
|
+
*/
|
|
2947
|
+
declare function safeRedirectTarget(candidate: string | null | undefined, fallback: string, options: SafeRedirectOptions): string;
|
|
2948
|
+
|
|
2825
2949
|
/**
|
|
2826
2950
|
* WebAuthn encoding helpers.
|
|
2827
2951
|
*
|
|
@@ -2862,4 +2986,4 @@ declare function prepareRequestOptions(options: Record<string, unknown>): Public
|
|
|
2862
2986
|
*/
|
|
2863
2987
|
declare function serializeCredential(credential: Credential): Record<string, unknown>;
|
|
2864
2988
|
|
|
2865
|
-
export { type APIError, type KeyListItem as APIKey, type AddMemberRequest, type AdminBanUserRequest, AuthClient, type AuthClientConfig, AuthClientError, type AuthConfig, AuthManager, type AuthResponse, type AuthState, type ChallengeMFARequest, type ChangePasswordRequest, type ClientConfig, type CreateAPIKeyRequest, type CreateInvitationRequest, type CreateOrganizationRequest as CreateOrgRequest, type CreateOrganizationRequest, type Device, type EnrollMFARequest, type EnrollResponse, type ForgotPasswordRequest, type Invitation, type KeyListItem, type ListOptions, type ListResponse, type EnrollResponse as MFAEnrollment, type SendMagicLinkRequest as MagicLinkSendRequest, type VerifyMagicLinkRequest as MagicLinkVerifyRequest, type Member, type ChallengeMFARequest as MfaChallengeRequest, type EnrollMFARequest as MfaEnrollRequest, type SendSMSCodeRequest as MfaSMSSendRequest, type VerifySMSCodeRequest as MfaSMSVerifyRequest, type VerifyMFARequest as MfaVerifyRequest, type Organization, type RefreshRequest, type ResetPasswordRequest, type SSOConnectionConfig, type SendMagicLinkRequest, type SendSMSCodeRequest, type Session, type SignInRequest, type SignUpRequest, type SignupFieldConfig, type SignupFieldOption, type SignupFieldValidation, type SocialProviderConfig, type SsoACSRequest, type SsoCallbackRequest, type TokenResponse, type TokenStorage, type UpdateMeRequest, type UpdateOrganizationRequest as UpdateOrgRequest, type UpdateOrganizationRequest, type User, type VerifyEmailRequest, type VerifyMFARequest, type VerifyMagicLinkRequest, type VerifyRecoveryCodeRequest, type VerifySMSCodeRequest, base64urlToBuffer, bufferToBase64url, prepareCreationOptions, prepareRequestOptions, serializeCredential };
|
|
2989
|
+
export { type APIError, type KeyListItem as APIKey, type AddMemberRequest, type AdminBanUserRequest, AuthClient, type AuthClientConfig, AuthClientError, type AuthConfig, AuthManager, type AuthResponse, type AuthState, type ChallengeMFARequest, type ChangePasswordRequest, type ClientConfig, type CreateAPIKeyRequest, type CreateInvitationRequest, type CreateOrganizationRequest as CreateOrgRequest, type CreateOrganizationRequest, type Device, type EnrollMFARequest, type EnrollResponse, type ForgotPasswordRequest, type Invitation, type KeyListItem, type ListOptions, type ListResponse, type EnrollResponse as MFAEnrollment, type SendMagicLinkRequest as MagicLinkSendRequest, type VerifyMagicLinkRequest as MagicLinkVerifyRequest, type Member, type ChallengeMFARequest as MfaChallengeRequest, type EnrollMFARequest as MfaEnrollRequest, type SendSMSCodeRequest as MfaSMSSendRequest, type VerifySMSCodeRequest as MfaSMSVerifyRequest, type VerifyMFARequest as MfaVerifyRequest, type Organization, type RefreshRequest, type ResetPasswordRequest, SESSION_STORAGE_KEY, type SSOConnectionConfig, type SafeRedirectOptions, type SendMagicLinkRequest, type SendSMSCodeRequest, type Session, type SignInRequest, type SignUpRequest, type SignupFieldConfig, type SignupFieldOption, type SignupFieldValidation, type SocialProviderConfig, type SsoACSRequest, type SsoCallbackRequest, type TokenResponse, type TokenStorage, type UpdateMeRequest, type UpdateOrganizationRequest as UpdateOrgRequest, type UpdateOrganizationRequest, type User, type VerifyEmailRequest, type VerifyMFARequest, type VerifyMagicLinkRequest, type VerifyRecoveryCodeRequest, type VerifySMSCodeRequest, base64urlToBuffer, bufferToBase64url, createLocalStorage, isSafeRedirect, prepareCreationOptions, prepareRequestOptions, safeRedirectTarget, serializeCredential };
|
package/dist/index.js
CHANGED
|
@@ -2661,11 +2661,15 @@ var AuthClient2 = class extends AuthClient {
|
|
|
2661
2661
|
};
|
|
2662
2662
|
|
|
2663
2663
|
// src/auth.ts
|
|
2664
|
-
var
|
|
2664
|
+
var SESSION_STORAGE_KEY = "authsome:session";
|
|
2665
|
+
var SESSION_KEY = SESSION_STORAGE_KEY;
|
|
2665
2666
|
var CONFIG_KEY = "authsome:client_config";
|
|
2666
2667
|
var REFRESH_BEFORE_MS = 6e4;
|
|
2667
2668
|
var CONFIG_TTL_MS = 5 * 6e4;
|
|
2668
|
-
var
|
|
2669
|
+
var DEFAULT_SESSION_TTL_MS = 36e5;
|
|
2670
|
+
var MAX_REFRESH_ATTEMPTS = 5;
|
|
2671
|
+
var REFRESH_RETRY_BASE_MS = 3e4;
|
|
2672
|
+
function createMemoryStorage() {
|
|
2669
2673
|
const store = /* @__PURE__ */ new Map();
|
|
2670
2674
|
return {
|
|
2671
2675
|
getItem: (key) => store.get(key) ?? null,
|
|
@@ -2676,15 +2680,28 @@ var memoryStorage = /* @__PURE__ */ (() => {
|
|
|
2676
2680
|
store.delete(key);
|
|
2677
2681
|
}
|
|
2678
2682
|
};
|
|
2679
|
-
}
|
|
2680
|
-
function
|
|
2683
|
+
}
|
|
2684
|
+
function createLocalStorage() {
|
|
2681
2685
|
try {
|
|
2682
2686
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
2683
2687
|
return window.localStorage;
|
|
2684
2688
|
}
|
|
2685
2689
|
} catch {
|
|
2686
2690
|
}
|
|
2687
|
-
return
|
|
2691
|
+
return createMemoryStorage();
|
|
2692
|
+
}
|
|
2693
|
+
function toSession(res) {
|
|
2694
|
+
return {
|
|
2695
|
+
session_token: res.session_token,
|
|
2696
|
+
refresh_token: res.refresh_token,
|
|
2697
|
+
expires_at: res.expires_at ?? new Date(Date.now() + DEFAULT_SESSION_TTL_MS).toISOString()
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function isAuthRejection(err) {
|
|
2701
|
+
if (err instanceof AuthClientError) {
|
|
2702
|
+
return err.code === 401 || err.code === 403;
|
|
2703
|
+
}
|
|
2704
|
+
return false;
|
|
2688
2705
|
}
|
|
2689
2706
|
var AuthManager = class {
|
|
2690
2707
|
client;
|
|
@@ -2699,7 +2716,7 @@ var AuthManager = class {
|
|
|
2699
2716
|
configFetchPromise = null;
|
|
2700
2717
|
constructor(config) {
|
|
2701
2718
|
this.client = new AuthClient2(config);
|
|
2702
|
-
this.storage = config.storage ??
|
|
2719
|
+
this.storage = config.storage ?? createMemoryStorage();
|
|
2703
2720
|
this.onError = config.onError;
|
|
2704
2721
|
this.publishableKey = config.publishableKey;
|
|
2705
2722
|
if (config.initialClientConfig) {
|
|
@@ -2750,27 +2767,50 @@ var AuthManager = class {
|
|
|
2750
2767
|
const user = await this.client.getMe(session.session_token);
|
|
2751
2768
|
this.setState({ status: "authenticated", user, session });
|
|
2752
2769
|
this.scheduleRefresh(session);
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
await this.handleInitFailure(err);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Resolves the state when hydrating a stored session failed.
|
|
2776
|
+
*
|
|
2777
|
+
* The two failure modes must not be conflated. A rejected credential means
|
|
2778
|
+
* the user is signed out. An unreachable server means we simply do not know,
|
|
2779
|
+
* and signing them out over a transient blip is its own bug — which is what
|
|
2780
|
+
* the previous behaviour tried to avoid, by reporting "authenticated" with a
|
|
2781
|
+
* null user. That kept people signed in through an outage but told every
|
|
2782
|
+
* consumer gating on `isAuthenticated` to render protected UI for a session
|
|
2783
|
+
* nothing had validated, and made `user.email` a crash.
|
|
2784
|
+
*
|
|
2785
|
+
* So: rejection signs out; unavailability yields "unknown", which is not
|
|
2786
|
+
* authenticated but retains the session and keeps a refresh scheduled so the
|
|
2787
|
+
* app recovers on its own.
|
|
2788
|
+
*/
|
|
2789
|
+
async handleInitFailure(err) {
|
|
2790
|
+
const stored = await this.storage.getItem(SESSION_KEY);
|
|
2791
|
+
if (!stored) {
|
|
2792
|
+
this.setState({ status: "unauthenticated" });
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
let session;
|
|
2796
|
+
try {
|
|
2797
|
+
session = JSON.parse(stored);
|
|
2753
2798
|
} catch {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
this.setState({ status: "unauthenticated" });
|
|
2761
|
-
}
|
|
2799
|
+
this.setState({ status: "unauthenticated" });
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (isAuthRejection(err)) {
|
|
2803
|
+
this.setState({ status: "unauthenticated" });
|
|
2804
|
+
return;
|
|
2762
2805
|
}
|
|
2806
|
+
this.setState({ status: "unknown", session });
|
|
2807
|
+
this.scheduleRefresh(session);
|
|
2763
2808
|
}
|
|
2764
2809
|
/** Sign in with email & password. */
|
|
2765
2810
|
async signIn(credentials) {
|
|
2766
2811
|
try {
|
|
2767
2812
|
const res = await this.client.signIn(credentials);
|
|
2768
|
-
const session =
|
|
2769
|
-
session_token: res.session_token,
|
|
2770
|
-
refresh_token: res.refresh_token,
|
|
2771
|
-
expires_at: new Date(Date.now() + 36e5).toISOString()
|
|
2772
|
-
// fallback
|
|
2773
|
-
};
|
|
2813
|
+
const session = toSession(res);
|
|
2774
2814
|
await this.handleAuthResponse(res.user, session);
|
|
2775
2815
|
} catch (err) {
|
|
2776
2816
|
if (err instanceof AuthClientError && err.type === "mfa_required" && err.details) {
|
|
@@ -2862,11 +2902,7 @@ var AuthManager = class {
|
|
|
2862
2902
|
mfa_ticket: state.mfaTicket,
|
|
2863
2903
|
code
|
|
2864
2904
|
});
|
|
2865
|
-
const session =
|
|
2866
|
-
session_token: res.session_token,
|
|
2867
|
-
refresh_token: res.refresh_token,
|
|
2868
|
-
expires_at: res.expires_at ?? new Date(Date.now() + 36e5).toISOString()
|
|
2869
|
-
};
|
|
2905
|
+
const session = toSession(res);
|
|
2870
2906
|
await this.handleAuthResponse(res.user, session);
|
|
2871
2907
|
} catch (err) {
|
|
2872
2908
|
this.onError?.({
|
|
@@ -2889,11 +2925,7 @@ var AuthManager = class {
|
|
|
2889
2925
|
this.setState({ status: "loading" });
|
|
2890
2926
|
try {
|
|
2891
2927
|
const res = await this.client.verifyRecoveryCode(code);
|
|
2892
|
-
const session =
|
|
2893
|
-
session_token: res.session_token,
|
|
2894
|
-
refresh_token: res.refresh_token,
|
|
2895
|
-
expires_at: new Date(Date.now() + 36e5).toISOString()
|
|
2896
|
-
};
|
|
2928
|
+
const session = toSession(res);
|
|
2897
2929
|
await this.handleAuthResponse(res.user, session);
|
|
2898
2930
|
} catch (err) {
|
|
2899
2931
|
this.handleError(err);
|
|
@@ -2912,11 +2944,7 @@ var AuthManager = class {
|
|
|
2912
2944
|
const token = this.getSessionToken();
|
|
2913
2945
|
if (!token) throw new Error("No session token available");
|
|
2914
2946
|
const res = await this.client.verifySMSCodeForMFA(code, token);
|
|
2915
|
-
const session =
|
|
2916
|
-
session_token: res.session_token,
|
|
2917
|
-
refresh_token: res.refresh_token,
|
|
2918
|
-
expires_at: new Date(Date.now() + 36e5).toISOString()
|
|
2919
|
-
};
|
|
2947
|
+
const session = toSession(res);
|
|
2920
2948
|
await this.handleAuthResponse(res.user, session);
|
|
2921
2949
|
} catch (err) {
|
|
2922
2950
|
this.handleError(err);
|
|
@@ -3013,6 +3041,18 @@ var AuthManager = class {
|
|
|
3013
3041
|
this.setState({ status: "authenticated", user, session });
|
|
3014
3042
|
this.scheduleRefresh(session);
|
|
3015
3043
|
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Exchanges the refresh token for a new session.
|
|
3046
|
+
*
|
|
3047
|
+
* On failure the two cases are separated, as they are in initialize: a
|
|
3048
|
+
* rejected token means signed out, anything else means no verdict and is
|
|
3049
|
+
* worth retrying.
|
|
3050
|
+
*
|
|
3051
|
+
* `attempt` was previously accepted and never read, and the retry path
|
|
3052
|
+
* called back in without it — so a permanently-invalid token retried every
|
|
3053
|
+
* 30s for as long as the tab stayed open, and the user was never signed out.
|
|
3054
|
+
* It now bounds the retries and backs off between them.
|
|
3055
|
+
*/
|
|
3016
3056
|
async refreshSession(refreshToken, attempt = 0) {
|
|
3017
3057
|
try {
|
|
3018
3058
|
const newSession = await this.client.refresh(refreshToken);
|
|
@@ -3020,11 +3060,30 @@ var AuthManager = class {
|
|
|
3020
3060
|
await this.persistSession(newSession);
|
|
3021
3061
|
this.setState({ status: "authenticated", user, session: newSession });
|
|
3022
3062
|
this.scheduleRefresh(newSession);
|
|
3023
|
-
} catch {
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
if (isAuthRejection(err)) {
|
|
3065
|
+
this.setState({ status: "unauthenticated" });
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
if (attempt >= MAX_REFRESH_ATTEMPTS) {
|
|
3069
|
+
const stored = await this.storage.getItem(SESSION_KEY);
|
|
3070
|
+
if (stored) {
|
|
3071
|
+
try {
|
|
3072
|
+
this.setState({ status: "unknown", session: JSON.parse(stored) });
|
|
3073
|
+
return;
|
|
3074
|
+
} catch {
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
this.setState({ status: "unauthenticated" });
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3024
3080
|
this.clearRefreshTimer();
|
|
3025
|
-
this.refreshTimer = setTimeout(
|
|
3026
|
-
|
|
3027
|
-
|
|
3081
|
+
this.refreshTimer = setTimeout(
|
|
3082
|
+
() => {
|
|
3083
|
+
void this.refreshSession(refreshToken, attempt + 1);
|
|
3084
|
+
},
|
|
3085
|
+
REFRESH_RETRY_BASE_MS * 2 ** attempt
|
|
3086
|
+
);
|
|
3028
3087
|
}
|
|
3029
3088
|
}
|
|
3030
3089
|
scheduleRefresh(session) {
|
|
@@ -3078,6 +3137,40 @@ var AuthManager = class {
|
|
|
3078
3137
|
}
|
|
3079
3138
|
};
|
|
3080
3139
|
|
|
3140
|
+
// src/redirect.ts
|
|
3141
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
3142
|
+
function isSafeRedirect(candidate, options) {
|
|
3143
|
+
if (typeof candidate !== "string" || candidate.trim() === "") {
|
|
3144
|
+
return false;
|
|
3145
|
+
}
|
|
3146
|
+
let resolved;
|
|
3147
|
+
try {
|
|
3148
|
+
resolved = new URL(candidate, options.currentOrigin);
|
|
3149
|
+
} catch {
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
if (!ALLOWED_PROTOCOLS.has(resolved.protocol)) {
|
|
3153
|
+
return false;
|
|
3154
|
+
}
|
|
3155
|
+
if (resolved.origin === options.currentOrigin) {
|
|
3156
|
+
return true;
|
|
3157
|
+
}
|
|
3158
|
+
return (options.allowedOrigins ?? []).some(
|
|
3159
|
+
(allowed) => toOrigin(allowed) === resolved.origin
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
function toOrigin(value) {
|
|
3163
|
+
try {
|
|
3164
|
+
const { origin } = new URL(value);
|
|
3165
|
+
return origin === "null" ? null : origin;
|
|
3166
|
+
} catch {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function safeRedirectTarget(candidate, fallback, options) {
|
|
3171
|
+
return isSafeRedirect(candidate, options) ? candidate : fallback;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3081
3174
|
// src/webauthn.ts
|
|
3082
3175
|
function base64urlToBuffer(base64url) {
|
|
3083
3176
|
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -3174,10 +3267,14 @@ export {
|
|
|
3174
3267
|
AuthClient2 as AuthClient,
|
|
3175
3268
|
AuthClientError,
|
|
3176
3269
|
AuthManager,
|
|
3270
|
+
SESSION_STORAGE_KEY,
|
|
3177
3271
|
base64urlToBuffer,
|
|
3178
3272
|
bufferToBase64url,
|
|
3273
|
+
createLocalStorage,
|
|
3274
|
+
isSafeRedirect,
|
|
3179
3275
|
prepareCreationOptions,
|
|
3180
3276
|
prepareRequestOptions,
|
|
3277
|
+
safeRedirectTarget,
|
|
3181
3278
|
serializeCredential
|
|
3182
3279
|
};
|
|
3183
3280
|
//# sourceMappingURL=index.js.map
|