@passflow/core 0.20.1 → 0.21.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/README.md +54 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +513 -86
- package/dist/index.d.ts +513 -86
- package/dist/index.mjs +2648 -884
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ type Token = {
|
|
|
42
42
|
iss: string;
|
|
43
43
|
jti: string;
|
|
44
44
|
sub: string;
|
|
45
|
+
/** Auth session the refresh lineage is anchored to; kept by every rotation of one sign-in. */
|
|
46
|
+
sid?: string;
|
|
45
47
|
type: string;
|
|
46
48
|
email?: string;
|
|
47
49
|
phonenumber?: string;
|
|
@@ -345,6 +347,12 @@ declare class StorageManager {
|
|
|
345
347
|
* Clear the CSRF token from storage
|
|
346
348
|
*/
|
|
347
349
|
clearCsrfToken(): void;
|
|
350
|
+
/**
|
|
351
|
+
* Every storage key the token session lives in (JSON tokens, scopes, the cookie-mode ID token,
|
|
352
|
+
* CSRF token and delivery mode). The refresh coordinator filters `storage` events by it, so
|
|
353
|
+
* writes to unrelated keys never wake the refresh path.
|
|
354
|
+
*/
|
|
355
|
+
tokenKeys(): string[];
|
|
348
356
|
private getKeyForTokenType;
|
|
349
357
|
}
|
|
350
358
|
declare class DeviceService {
|
|
@@ -586,6 +594,13 @@ interface PlatformAdapter {
|
|
|
586
594
|
* Must be synchronous. Use localStorage on web, MMKV on React Native.
|
|
587
595
|
*/
|
|
588
596
|
readonly storage: PlatformStorage;
|
|
597
|
+
/**
|
|
598
|
+
* True when `storage` is shared by every tab of the origin (the browser's localStorage, or a
|
|
599
|
+
* wrapper over it). Token refresh coordinates tabs only over shared storage
|
|
600
|
+
* (`refreshCoordination: 'web-locks'`); a per-tab storage (sessionStorage, memory) shares no
|
|
601
|
+
* session. Optional: an adapter that leaves it unset is treated as per-tab. WebAdapter sets it.
|
|
602
|
+
*/
|
|
603
|
+
readonly storageSharedAcrossTabs?: boolean;
|
|
589
604
|
/**
|
|
590
605
|
* Returns the current URL of the application.
|
|
591
606
|
* On web: wraps window.location.
|
|
@@ -696,6 +711,11 @@ interface PlatformAdapter {
|
|
|
696
711
|
}
|
|
697
712
|
export declare class WebAdapter implements PlatformAdapter {
|
|
698
713
|
get storage(): PlatformStorage;
|
|
714
|
+
/**
|
|
715
|
+
* `storage` is the browser's localStorage, which every tab of the origin shares. False in
|
|
716
|
+
* SSR/Node (the no-op storage) and where reading localStorage throws (storage blocked).
|
|
717
|
+
*/
|
|
718
|
+
get storageSharedAcrossTabs(): boolean;
|
|
699
719
|
/**
|
|
700
720
|
* Replaces:
|
|
701
721
|
* window.location.origin (passflow.ts:95, axios-client.ts:49)
|
|
@@ -796,6 +816,97 @@ export declare class WebAdapter implements PlatformAdapter {
|
|
|
796
816
|
* Exported so consuming code can reference it without allocating a new instance.
|
|
797
817
|
*/
|
|
798
818
|
export declare const webAdapter: WebAdapter;
|
|
819
|
+
/**
|
|
820
|
+
* Public types of multi-tab-safe token refresh (PFL-792, architecture §2.2-2.5).
|
|
821
|
+
*
|
|
822
|
+
* @module refresh/types
|
|
823
|
+
*/
|
|
824
|
+
/** Options of one lock request: exclusive mode only, as the SDK uses `navigator.locks`. */
|
|
825
|
+
interface RefreshLockOptions {
|
|
826
|
+
ifAvailable?: boolean;
|
|
827
|
+
steal?: boolean;
|
|
828
|
+
signal?: AbortSignal;
|
|
829
|
+
}
|
|
830
|
+
/** The part of `navigator.locks` (LockManager) the SDK uses: exclusive mode only. */
|
|
831
|
+
interface RefreshLockManager {
|
|
832
|
+
request(name: string, options: RefreshLockOptions, callback: (lock: unknown) => unknown): Promise<unknown>;
|
|
833
|
+
}
|
|
834
|
+
/** The part of BroadcastChannel the SDK uses. Must not deliver an instance's own posts to itself. */
|
|
835
|
+
interface RefreshChannel {
|
|
836
|
+
postMessage(message: unknown): void;
|
|
837
|
+
addEventListener(type: 'message', listener: (event: {
|
|
838
|
+
data: unknown;
|
|
839
|
+
}) => void): void;
|
|
840
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
841
|
+
data: unknown;
|
|
842
|
+
}) => void): void;
|
|
843
|
+
close(): void;
|
|
844
|
+
}
|
|
845
|
+
/** Time source for every refresh timer of one instance (heartbeat, silence, retry, waits). */
|
|
846
|
+
interface RefreshClock {
|
|
847
|
+
/** Epoch ms. */
|
|
848
|
+
now(): number;
|
|
849
|
+
/** Runs `fn` after `ms`; returns the cancel. */
|
|
850
|
+
setTimeout(fn: () => void, ms: number): () => void;
|
|
851
|
+
/** `[0, 1)` for backoff jitter. Default `Math.random`. */
|
|
852
|
+
random?(): number;
|
|
853
|
+
}
|
|
854
|
+
/** Transports for the cross-tab protocol, supplied by tests, Electron or custom hosts. */
|
|
855
|
+
interface RefreshCoordinator {
|
|
856
|
+
locks: RefreshLockManager;
|
|
857
|
+
/** One per Passflow instance; closed by `dispose()`. */
|
|
858
|
+
channel: RefreshChannel;
|
|
859
|
+
/** Default: `crypto.randomUUID()` or a random string. */
|
|
860
|
+
tabId?: string;
|
|
861
|
+
/** Another tab wrote the shared token storage. Called once at construction; returns unsubscribe. */
|
|
862
|
+
onStorageChange?: (listener: () => void) => () => void;
|
|
863
|
+
/** Default: `Date.now` / `setTimeout` / `Math.random`. */
|
|
864
|
+
clock?: RefreshClock;
|
|
865
|
+
}
|
|
866
|
+
/** How tabs of this origin coordinate token refresh (`PassflowConfig.refreshCoordination`). */
|
|
867
|
+
type RefreshCoordination = 'web-locks' | 'none' | RefreshCoordinator;
|
|
868
|
+
/** What asked for a refresh (`ErrorPayload.source`). */
|
|
869
|
+
type RefreshTrigger =
|
|
870
|
+
/** `Passflow.refreshToken()` / `AuthService.refreshToken()` */
|
|
871
|
+
'refresh-token' |
|
|
872
|
+
/** `TokenCacheService.getTokensWithRefresh()` */
|
|
873
|
+
'token-check' |
|
|
874
|
+
/** `getTokens(true)` / `session({ doRefresh: true })` */
|
|
875
|
+
'session' |
|
|
876
|
+
/** An SDK API call whose access token expires within 30 s */
|
|
877
|
+
'pre-request' |
|
|
878
|
+
/** Step-up commit */
|
|
879
|
+
'upgrade' |
|
|
880
|
+
/** The scheduled retry, with no new caller */
|
|
881
|
+
'retry' |
|
|
882
|
+
/** The held-tab send */
|
|
883
|
+
'held';
|
|
884
|
+
/** How a failed `/auth/refresh` is read (architecture §2.6). */
|
|
885
|
+
type RefreshFailureKind = 'transient' | 'blocked' | 'auth';
|
|
886
|
+
/** Refresh details on `ErrorPayload` for operations `refresh` and `upgrade.commit`. */
|
|
887
|
+
interface RefreshErrorInfo {
|
|
888
|
+
classification: RefreshFailureKind;
|
|
889
|
+
/** A retry is scheduled (transient, or auth with strikes left). */
|
|
890
|
+
willRetry: boolean;
|
|
891
|
+
/** Epoch ms of the retry, when `willRetry`. */
|
|
892
|
+
nextRetryAt?: number;
|
|
893
|
+
/** Auth failures since the last success, after this one. */
|
|
894
|
+
strikes: number;
|
|
895
|
+
/** Resolved `refreshStrikes`. */
|
|
896
|
+
maxStrikes: number;
|
|
897
|
+
}
|
|
898
|
+
/** Whether an event was caused by this tab's own action or learnt from another tab. */
|
|
899
|
+
type EventOrigin = 'this-tab' | 'other-tab';
|
|
900
|
+
/** Why a session ended (`SessionExpired.reason`). */
|
|
901
|
+
type SessionExpiredReason =
|
|
902
|
+
/** `refreshStrikes` auth failures (this tab, or another tab of the lineage). */
|
|
903
|
+
'refresh_failed' |
|
|
904
|
+
/** `error.token.blocked` and no successor within 20 s; this tab only. */
|
|
905
|
+
'successor_timeout' |
|
|
906
|
+
/** Storage now holds another user's tokens. */
|
|
907
|
+
'identity_changed' |
|
|
908
|
+
/** Legacy members, never emitted. */
|
|
909
|
+
'token_invalid' | 'logged_out';
|
|
799
910
|
/** Factor types the upgrade protocol understands, named as `upgrade.type` on the wire. */
|
|
800
911
|
type UpgradeFactorType = 'totp' | 'recovery_codes' | 'passkey' | 'email_otp' | 'sms_otp';
|
|
801
912
|
/** The prepare round's result — `POST /auth/upgrade/begin`, normalised to camelCase. */
|
|
@@ -1010,6 +1121,26 @@ export type PassflowConfig = {
|
|
|
1010
1121
|
* resource that has to be torn down, which configuration cannot express.
|
|
1011
1122
|
*/
|
|
1012
1123
|
upgrade?: PassflowUpgradeConfig;
|
|
1124
|
+
/**
|
|
1125
|
+
* How tabs of this origin coordinate token refresh.
|
|
1126
|
+
* - `'web-locks'` (default): a Web Lock plus a BroadcastChannel, on a browser page whose
|
|
1127
|
+
* `platform.storage` is shared by every tab (the default web adapter's localStorage) and that
|
|
1128
|
+
* has `navigator.locks` and `BroadcastChannel`. Elsewhere it falls back to `'none'`; on a
|
|
1129
|
+
* browser page with shared storage the fallback writes one `[passflow]` warning, at the first
|
|
1130
|
+
* refresh.
|
|
1131
|
+
* - `'none'`: no cross-tab coordination. Single flight within this instance, compare-and-set
|
|
1132
|
+
* save, per-instance retries.
|
|
1133
|
+
* - a `RefreshCoordinator` object: the same protocol as `'web-locks'` over the transports
|
|
1134
|
+
* you supply (tests, Electron, custom hosts). Never falls back.
|
|
1135
|
+
*/
|
|
1136
|
+
refreshCoordination?: RefreshCoordination;
|
|
1137
|
+
/**
|
|
1138
|
+
* Authentication failures of `/auth/refresh` counted since the last successful refresh or
|
|
1139
|
+
* adoption, after which the session ends. Transient failures in between neither add a strike nor
|
|
1140
|
+
* reset the count. Default 3. A non-integer is floored; a value below 1 or not a finite number
|
|
1141
|
+
* uses the default.
|
|
1142
|
+
*/
|
|
1143
|
+
refreshStrikes?: number;
|
|
1013
1144
|
};
|
|
1014
1145
|
export type PassflowUpgradeConfig = {
|
|
1015
1146
|
/** Relying-party id forwarded verbatim to `POST /auth/upgrade/start` for passkey upgrades. */
|
|
@@ -1725,13 +1856,24 @@ export interface CLIAuthCompleteRequest {
|
|
|
1725
1856
|
* config, so anything registered afterwards is structurally too late.
|
|
1726
1857
|
*/
|
|
1727
1858
|
type AxiosUpgradeHandler = (error: AxiosError, challenge: UpgradeChallenge) => Promise<AxiosResponse>;
|
|
1859
|
+
/**
|
|
1860
|
+
* The pre-request refresh (PFL-792). Installed by `Passflow` on every client it owns; it asks the
|
|
1861
|
+
* instance's refresh controller, which coordinates with the other tabs and owns retries, strikes
|
|
1862
|
+
* and the session end. Resolves with the tokens to send (`undefined` once the session ended).
|
|
1863
|
+
*
|
|
1864
|
+
* `allowStale` is true when the access token is inside the 30 s window but not yet expired: during
|
|
1865
|
+
* a pending retry the controller then resolves at once and the call goes out with the current
|
|
1866
|
+
* bearer instead of waiting for the retry.
|
|
1867
|
+
*/
|
|
1868
|
+
type AxiosRefreshHandler = (request: {
|
|
1869
|
+
trigger: 'pre-request';
|
|
1870
|
+
allowStale: boolean;
|
|
1871
|
+
}) => Promise<Tokens | undefined>;
|
|
1728
1872
|
declare class AxiosClient {
|
|
1729
1873
|
private instance;
|
|
1730
1874
|
protected storageManager: StorageManager;
|
|
1731
1875
|
protected deviceService: DeviceService;
|
|
1732
1876
|
protected tokenDeliveryManager: TokenDeliveryManager;
|
|
1733
|
-
private refreshPromise;
|
|
1734
|
-
private isRefreshing;
|
|
1735
1877
|
private _platform;
|
|
1736
1878
|
tokenService: TokenService;
|
|
1737
1879
|
get origin(): string;
|
|
@@ -1751,6 +1893,8 @@ declare class AxiosClient {
|
|
|
1751
1893
|
private readonly protectedEndpoints;
|
|
1752
1894
|
/** Set when a consumer wires the upgrade feature. Absent by default. */
|
|
1753
1895
|
private upgradeHandler?;
|
|
1896
|
+
/** Set by `Passflow` (its refresh controller). Absent on a client built standalone. */
|
|
1897
|
+
private refreshHandler?;
|
|
1754
1898
|
/**
|
|
1755
1899
|
* Passflow's own API as an interception scope, resolved ONCE here from
|
|
1756
1900
|
* `PassflowConfig.url` so no wrapper or adapter ever derives it. `null` when
|
|
@@ -1769,6 +1913,17 @@ declare class AxiosClient {
|
|
|
1769
1913
|
* upgrade challenge. Passing `undefined` detaches it.
|
|
1770
1914
|
*/
|
|
1771
1915
|
setUpgradeInterceptor(handler: AxiosUpgradeHandler | undefined): void;
|
|
1916
|
+
/**
|
|
1917
|
+
* Installs (or clears) the pre-request refresh. JSON mode only: before a request that needs an
|
|
1918
|
+
* access token (not `/auth/refresh`, not logout) whose access token expires within 30 s while a
|
|
1919
|
+
* refresh token is stored, the handler is awaited and its access token becomes the bearer. On
|
|
1920
|
+
* rejection the call still goes out with the current bearer when that token has not expired yet;
|
|
1921
|
+
* otherwise the call rejects with the handler's error. Nothing is ever deleted here.
|
|
1922
|
+
*
|
|
1923
|
+
* Without a handler (an API client built standalone, outside `Passflow`) no refresh runs: every
|
|
1924
|
+
* call sends the stored bearer as it is, and an expired one gets the server's 401.
|
|
1925
|
+
*/
|
|
1926
|
+
setRefreshHandler(handler: AxiosRefreshHandler | undefined): void;
|
|
1772
1927
|
/**
|
|
1773
1928
|
* Re-issues a request after an elevation. Exposed for the upgrade handler,
|
|
1774
1929
|
* which owns the replay-once decision; the marker preventing a second
|
|
@@ -1787,11 +1942,6 @@ declare class AxiosClient {
|
|
|
1787
1942
|
* configure cookie mode for platforms where cookiesSupported() returns false.
|
|
1788
1943
|
*/
|
|
1789
1944
|
private detectCookieSupport;
|
|
1790
|
-
/**
|
|
1791
|
-
* Refresh tokens using single-flight pattern to prevent race conditions
|
|
1792
|
-
* Supports both cookie mode and JSON mode
|
|
1793
|
-
*/
|
|
1794
|
-
private refreshTokens;
|
|
1795
1945
|
private handleRateLimitError;
|
|
1796
1946
|
private handleAxiosError;
|
|
1797
1947
|
private send;
|
|
@@ -2929,6 +3079,117 @@ export declare class M2MTokenParseError extends M2MError {
|
|
|
2929
3079
|
export declare class M2MConfigError extends M2MError {
|
|
2930
3080
|
constructor(message: string);
|
|
2931
3081
|
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Logger interface for Passflow services
|
|
3084
|
+
*/
|
|
3085
|
+
interface Logger {
|
|
3086
|
+
error(message: string, ...args: unknown[]): void;
|
|
3087
|
+
warn(message: string, ...args: unknown[]): void;
|
|
3088
|
+
info(message: string, ...args: unknown[]): void;
|
|
3089
|
+
debug(message: string, ...args: unknown[]): void;
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Session and user identity pinning for an upgrade flight (§13.4 revision 6).
|
|
3093
|
+
*
|
|
3094
|
+
* ## The rule
|
|
3095
|
+
*
|
|
3096
|
+
* An upgrade flight is bound to the **session and user identity it began
|
|
3097
|
+
* under**, and that identity is verified **at the moment tokens are applied** —
|
|
3098
|
+
* not only around `prepare` and `commit`. On mismatch the rotated tokens are
|
|
3099
|
+
* discarded, never applied, and the flight settles as `session changed
|
|
3100
|
+
* mid-flight`.
|
|
3101
|
+
*
|
|
3102
|
+
* ## Why the epoch counter was not enough
|
|
3103
|
+
*
|
|
3104
|
+
* The round-1 epoch check wraps the two rounds that talk to the server. It does
|
|
3105
|
+
* not wrap token APPLICATION, which happens inside `AuthService.refreshToken`
|
|
3106
|
+
* after the commit returns. A logout and re-login during an in-flight upgrade
|
|
3107
|
+
* therefore let user A's flight write its rotated tokens over user B's live
|
|
3108
|
+
* session: the request was authorized as A, the storage belongs to B, and
|
|
3109
|
+
* nothing in between compared the two principals.
|
|
3110
|
+
*
|
|
3111
|
+
* An epoch tells you something changed. An identity check tells you **whether
|
|
3112
|
+
* it is still the same principal**, which is the property actually required.
|
|
3113
|
+
* Both run, and they are complementary rather than redundant.
|
|
3114
|
+
*
|
|
3115
|
+
* ## Why this module is pure
|
|
3116
|
+
*
|
|
3117
|
+
* It imports nothing and reads no storage, so the token-applying path can be
|
|
3118
|
+
* given a pin by value and the comparison can be tested with literals. Reading
|
|
3119
|
+
* the identity is the host's job — it is the only code that knows where the
|
|
3120
|
+
* session lives in each delivery mode.
|
|
3121
|
+
*/
|
|
3122
|
+
/**
|
|
3123
|
+
* The principal an upgrade flight is pinned to.
|
|
3124
|
+
*
|
|
3125
|
+
* `sub` is the user. `sid` is the session when the server issues one; it is
|
|
3126
|
+
* absent in tokens that carry no session claim, and the field is then simply
|
|
3127
|
+
* not part of the comparison — a missing claim cannot be made to prove
|
|
3128
|
+
* anything, and inventing a value would make two different sessions compare
|
|
3129
|
+
* equal or unequal arbitrarily.
|
|
3130
|
+
*/
|
|
3131
|
+
type UpgradeSessionPin = {
|
|
3132
|
+
readonly sub: string;
|
|
3133
|
+
readonly sid?: string;
|
|
3134
|
+
};
|
|
3135
|
+
/** Lock and channel names for a storage prefix. Empty prefix: the portal's names. */
|
|
3136
|
+
interface RefreshNames {
|
|
3137
|
+
lock: string;
|
|
3138
|
+
takeoverLock: string;
|
|
3139
|
+
channel: string;
|
|
3140
|
+
}
|
|
3141
|
+
/** The transports of the cross-tab protocol for one instance. */
|
|
3142
|
+
interface CrossTabTransports {
|
|
3143
|
+
locks: RefreshLockManager;
|
|
3144
|
+
channel: RefreshChannel;
|
|
3145
|
+
tabId: string;
|
|
3146
|
+
}
|
|
3147
|
+
/** What `refreshCoordination` resolved to for one instance. */
|
|
3148
|
+
interface ResolvedCoordination {
|
|
3149
|
+
/** Cross-tab transports, or null: local (single-instance) coordination. */
|
|
3150
|
+
transports: CrossTabTransports | null;
|
|
3151
|
+
/** Lock and channel names for the storage prefix. */
|
|
3152
|
+
names: RefreshNames;
|
|
3153
|
+
/** The time source of every refresh timer of the instance. */
|
|
3154
|
+
clock: Required<RefreshClock>;
|
|
3155
|
+
/** Another tab wrote the shared token storage. Returns unsubscribe. */
|
|
3156
|
+
onStorageChange?: (listener: () => void) => () => void;
|
|
3157
|
+
/** Write the fallback warning once, when the instance first starts a refresh. */
|
|
3158
|
+
warnOnFirstCycle: boolean;
|
|
3159
|
+
}
|
|
3160
|
+
interface RefreshRequest {
|
|
3161
|
+
trigger: RefreshTrigger;
|
|
3162
|
+
/** `refreshToken()`: satisfied only by a pair from a request sent at or after the call (§2.8). */
|
|
3163
|
+
fresh?: boolean;
|
|
3164
|
+
}
|
|
3165
|
+
interface RefreshController {
|
|
3166
|
+
refresh(request: RefreshRequest): Promise<PassflowAuthorizationResponse>;
|
|
3167
|
+
/** The pre-request refresh (§2.8): the tokens to send the API call with. */
|
|
3168
|
+
ensureFresh(request: {
|
|
3169
|
+
trigger: 'pre-request';
|
|
3170
|
+
allowStale: boolean;
|
|
3171
|
+
}): Promise<Tokens | undefined>;
|
|
3172
|
+
commitUpgrade(upgrade: UpgradeRefreshField, pin?: UpgradeSessionPin): Promise<PassflowAuthorizationResponse>;
|
|
3173
|
+
/**
|
|
3174
|
+
* logOut (§2.8): rejects plain callers, stops the timers, waits for this tab's request in
|
|
3175
|
+
* flight, takes the lock (≤ 5 s), posts signed-out naming the newest stored token, runs
|
|
3176
|
+
* `cleanup(newestRefreshToken)`, ends the session here and releases. Emits nothing: the caller
|
|
3177
|
+
* emits its one SignOut.
|
|
3178
|
+
*/
|
|
3179
|
+
signOut(cleanup: (newest?: string) => Promise<void>): Promise<void>;
|
|
3180
|
+
/** reset() (A2): the same without the lock or an API call, synchronously. */
|
|
3181
|
+
signOutNow(cleanup: (newest?: string) => void): void;
|
|
3182
|
+
/** A new session in this tab (sign-in, register, setTokens): new generation, counters reset. */
|
|
3183
|
+
sessionStarted(): void;
|
|
3184
|
+
/** The `sub` this tab's session belongs to. */
|
|
3185
|
+
identity(): string | undefined;
|
|
3186
|
+
readonly coordinated: boolean;
|
|
3187
|
+
/** This instance has a cycle: waiting for the lock, a request in flight, a retry or a successor pending. */
|
|
3188
|
+
readonly busy: boolean;
|
|
3189
|
+
/** The session ended here (or the instance was disposed): `refresh()` rejects with a PassflowRefreshError. */
|
|
3190
|
+
readonly ended: boolean;
|
|
3191
|
+
dispose(): void;
|
|
3192
|
+
}
|
|
2932
3193
|
/**
|
|
2933
3194
|
* Passflow event types
|
|
2934
3195
|
*/
|
|
@@ -2956,8 +3217,17 @@ export declare enum PassflowEvent {
|
|
|
2956
3217
|
TwoFactorSetupMagicLinkFailed = "2fa:magic_link_failed",
|
|
2957
3218
|
TwoFactorChallengeReceived = "two_factor_challenge_received",
|
|
2958
3219
|
TwoFactorMethodSwitched = "two_factor_method_switched",
|
|
2959
|
-
TwoFactorDeviceTrusted = "two_factor_device_trusted"
|
|
3220
|
+
TwoFactorDeviceTrusted = "two_factor_device_trusted",
|
|
3221
|
+
/** Token refresh entered a transient-failure episode (once per episode, per tab). */
|
|
3222
|
+
RefreshFailing = "refresh:failing",
|
|
3223
|
+
/** The transient-failure episode ended with a successful refresh or adoption. */
|
|
3224
|
+
RefreshRecovered = "refresh:recovered"
|
|
2960
3225
|
}
|
|
3226
|
+
/**
|
|
3227
|
+
* The SDK operation that failed (`ErrorPayload.operation`). The SDK sets it on every `Error` it
|
|
3228
|
+
* emits, so a subscriber can tell a failed sign-in from a failed refresh without parsing messages.
|
|
3229
|
+
*/
|
|
3230
|
+
type PassflowOperation = 'sign-in' | 'sign-up' | 'passwordless.start' | 'passwordless.complete' | 'password-reset.send' | 'password-reset.complete' | 'passkey.register' | 'passkey.authenticate' | 'federated.popup' | 'auth-redirect.url' | 'auth-redirect' | 'auth.is-authenticated' | 'session.check' | 'tokens.get' | 'tokens.load' | 'tokens.from-url' | 'refresh' | 'upgrade.commit' | 'log-out' | 'reset' | 'settings.app' | 'settings.all' | 'settings.password-policy' | 'settings.passkey' | 'passkey.list' | 'passkey.rename' | 'passkey.delete' | 'passkey.add' | 'tenant.create' | 'invitation.join' | 'invitation.request-link' | 'invitation.list' | 'invitation.delete' | 'invitation.resend' | 'invitation.get-link' | '2fa.status' | '2fa.setup.begin' | '2fa.setup.confirm' | '2fa.verify' | '2fa.recovery.use' | '2fa.disable' | '2fa.recovery.regenerate' | '2fa.methods.available' | '2fa.methods.registered' | '2fa.method.setup.start' | '2fa.method.setup.complete' | '2fa.method.remove' | '2fa.challenge.request' | '2fa.challenge.verify' | '2fa.method.switch' | '2fa.trusted-devices.list' | '2fa.trusted-devices.revoke';
|
|
2961
3231
|
/**
|
|
2962
3232
|
* Error payload interface for structured error information
|
|
2963
3233
|
*/
|
|
@@ -2966,7 +3236,17 @@ interface ErrorPayload {
|
|
|
2966
3236
|
code?: string | number;
|
|
2967
3237
|
details?: unknown;
|
|
2968
3238
|
originalError?: unknown;
|
|
3239
|
+
/** The operation that failed. Always set by the SDK. */
|
|
3240
|
+
operation?: PassflowOperation;
|
|
3241
|
+
/** For operation `refresh` | `upgrade.commit`: what asked for the refresh. */
|
|
3242
|
+
source?: RefreshTrigger;
|
|
3243
|
+
/** For operation `refresh` | `upgrade.commit`: how the failure was classified and what follows. */
|
|
3244
|
+
refresh?: RefreshErrorInfo;
|
|
2969
3245
|
}
|
|
3246
|
+
/** An `Error` payload as the SDK reports it: `operation` is required. */
|
|
3247
|
+
type OperationErrorPayload = ErrorPayload & {
|
|
3248
|
+
operation: PassflowOperation;
|
|
3249
|
+
};
|
|
2970
3250
|
/**
|
|
2971
3251
|
* Event-specific payload types
|
|
2972
3252
|
*/
|
|
@@ -2974,6 +3254,8 @@ type PassflowEventPayload = {
|
|
|
2974
3254
|
[PassflowEvent.SignIn]: {
|
|
2975
3255
|
tokens?: Tokens;
|
|
2976
3256
|
parsedTokens?: ParsedTokens;
|
|
3257
|
+
/** `'other-tab'` only when an ended tab revives on a new session found in storage. Absent on this tab's own sign-in. */
|
|
3258
|
+
origin?: EventOrigin;
|
|
2977
3259
|
};
|
|
2978
3260
|
[PassflowEvent.SignInStart]: {
|
|
2979
3261
|
email?: string;
|
|
@@ -2987,7 +3269,10 @@ type PassflowEventPayload = {
|
|
|
2987
3269
|
email?: string;
|
|
2988
3270
|
};
|
|
2989
3271
|
[PassflowEvent.SignOut]: {
|
|
3272
|
+
/** `sub` of the session that ended, when known. */
|
|
2990
3273
|
userId?: string;
|
|
3274
|
+
/** Always set by the SDK. `'other-tab'`: another tab signed out, or shared storage was emptied. */
|
|
3275
|
+
origin?: EventOrigin;
|
|
2991
3276
|
};
|
|
2992
3277
|
[PassflowEvent.SessionRestored]: {
|
|
2993
3278
|
id: string;
|
|
@@ -2996,12 +3281,19 @@ type PassflowEventPayload = {
|
|
|
2996
3281
|
[key: string]: unknown;
|
|
2997
3282
|
};
|
|
2998
3283
|
[PassflowEvent.SessionExpired]: {
|
|
2999
|
-
reason:
|
|
3284
|
+
reason: SessionExpiredReason;
|
|
3285
|
+
/** Always set by the SDK. */
|
|
3286
|
+
origin?: EventOrigin;
|
|
3000
3287
|
};
|
|
3001
3288
|
[PassflowEvent.Error]: ErrorPayload;
|
|
3002
3289
|
[PassflowEvent.Refresh]: {
|
|
3003
3290
|
tokens?: Tokens;
|
|
3004
3291
|
parsedTokens?: ParsedTokens;
|
|
3292
|
+
/**
|
|
3293
|
+
* Always set by the SDK. `'refresh'`: this tab's request; `'upgrade'`: this tab's step-up
|
|
3294
|
+
* commit; `'adopted'`: tokens this tab did not request (another tab's, or found in storage).
|
|
3295
|
+
*/
|
|
3296
|
+
source?: 'refresh' | 'upgrade' | 'adopted';
|
|
3005
3297
|
};
|
|
3006
3298
|
[PassflowEvent.RefreshStart]: {
|
|
3007
3299
|
tokenId?: string;
|
|
@@ -3061,6 +3353,20 @@ type PassflowEventPayload = {
|
|
|
3061
3353
|
alternativeMethods: TwoFactorMethod[];
|
|
3062
3354
|
};
|
|
3063
3355
|
[PassflowEvent.TwoFactorDeviceTrusted]: Record<string, never>;
|
|
3356
|
+
[PassflowEvent.RefreshFailing]: {
|
|
3357
|
+
/** Epoch ms of the episode's first transient failure. */
|
|
3358
|
+
since: number;
|
|
3359
|
+
/** Epoch ms of the shared pending retry, when known. */
|
|
3360
|
+
nextRetryAt?: number;
|
|
3361
|
+
/** `'this-tab'` when this tab's own request failed. */
|
|
3362
|
+
origin: EventOrigin;
|
|
3363
|
+
};
|
|
3364
|
+
[PassflowEvent.RefreshRecovered]: {
|
|
3365
|
+
/** Same value as the `RefreshFailing` that opened the episode. */
|
|
3366
|
+
since: number;
|
|
3367
|
+
/** `'this-tab'`: own request succeeded; `'other-tab'`: adopted another tab's tokens. */
|
|
3368
|
+
origin: EventOrigin;
|
|
3369
|
+
};
|
|
3064
3370
|
};
|
|
3065
3371
|
/**
|
|
3066
3372
|
* Passflow subscriber interface
|
|
@@ -3091,54 +3397,14 @@ declare class PassflowStore {
|
|
|
3091
3397
|
* @param payload Event-specific payload data
|
|
3092
3398
|
*/
|
|
3093
3399
|
notify<E extends PassflowEvent>(eventType: E, payload?: PassflowEventPayload[E]): void;
|
|
3400
|
+
/**
|
|
3401
|
+
* Notify subscribers of an error, at most once per error object (`notifyError` in `error-report.ts`).
|
|
3402
|
+
* @returns whether the event was emitted
|
|
3403
|
+
*/
|
|
3404
|
+
notifyError(payload: OperationErrorPayload): boolean;
|
|
3094
3405
|
}
|
|
3095
|
-
/**
|
|
3096
|
-
* Session and user identity pinning for an upgrade flight (§13.4 revision 6).
|
|
3097
|
-
*
|
|
3098
|
-
* ## The rule
|
|
3099
|
-
*
|
|
3100
|
-
* An upgrade flight is bound to the **session and user identity it began
|
|
3101
|
-
* under**, and that identity is verified **at the moment tokens are applied** —
|
|
3102
|
-
* not only around `prepare` and `commit`. On mismatch the rotated tokens are
|
|
3103
|
-
* discarded, never applied, and the flight settles as `session changed
|
|
3104
|
-
* mid-flight`.
|
|
3105
|
-
*
|
|
3106
|
-
* ## Why the epoch counter was not enough
|
|
3107
|
-
*
|
|
3108
|
-
* The round-1 epoch check wraps the two rounds that talk to the server. It does
|
|
3109
|
-
* not wrap token APPLICATION, which happens inside `AuthService.refreshToken`
|
|
3110
|
-
* after the commit returns. A logout and re-login during an in-flight upgrade
|
|
3111
|
-
* therefore let user A's flight write its rotated tokens over user B's live
|
|
3112
|
-
* session: the request was authorized as A, the storage belongs to B, and
|
|
3113
|
-
* nothing in between compared the two principals.
|
|
3114
|
-
*
|
|
3115
|
-
* An epoch tells you something changed. An identity check tells you **whether
|
|
3116
|
-
* it is still the same principal**, which is the property actually required.
|
|
3117
|
-
* Both run, and they are complementary rather than redundant.
|
|
3118
|
-
*
|
|
3119
|
-
* ## Why this module is pure
|
|
3120
|
-
*
|
|
3121
|
-
* It imports nothing and reads no storage, so the token-applying path can be
|
|
3122
|
-
* given a pin by value and the comparison can be tested with literals. Reading
|
|
3123
|
-
* the identity is the host's job — it is the only code that knows where the
|
|
3124
|
-
* session lives in each delivery mode.
|
|
3125
|
-
*/
|
|
3126
|
-
/**
|
|
3127
|
-
* The principal an upgrade flight is pinned to.
|
|
3128
|
-
*
|
|
3129
|
-
* `sub` is the user. `sid` is the session when the server issues one; it is
|
|
3130
|
-
* absent in tokens that carry no session claim, and the field is then simply
|
|
3131
|
-
* not part of the comparison — a missing claim cannot be made to prove
|
|
3132
|
-
* anything, and inventing a value would make two different sessions compare
|
|
3133
|
-
* equal or unequal arbitrarily.
|
|
3134
|
-
*/
|
|
3135
|
-
type UpgradeSessionPin = {
|
|
3136
|
-
readonly sub: string;
|
|
3137
|
-
readonly sid?: string;
|
|
3138
|
-
};
|
|
3139
3406
|
export declare class TokenCacheService {
|
|
3140
3407
|
private storageManager;
|
|
3141
|
-
private authApi;
|
|
3142
3408
|
private subscribeStore;
|
|
3143
3409
|
private platform;
|
|
3144
3410
|
tokensCache: Tokens | undefined;
|
|
@@ -3147,14 +3413,42 @@ export declare class TokenCacheService {
|
|
|
3147
3413
|
private readonly CHECK_INTERVAL;
|
|
3148
3414
|
private unsubscribeForeground;
|
|
3149
3415
|
private unsubscribeUnload;
|
|
3150
|
-
|
|
3416
|
+
private refreshController;
|
|
3417
|
+
/**
|
|
3418
|
+
* Set when the expiry check (`initialize()`, the 60 s check, foreground) finds the access token
|
|
3419
|
+
* expired, with `TokenCacheExpired{isExpired:true}`. Output only: it never gates a refresh. The
|
|
3420
|
+
* refresh controller clears it on every `Refresh`.
|
|
3421
|
+
*/
|
|
3151
3422
|
tokenExpiredFlag: boolean;
|
|
3152
|
-
|
|
3423
|
+
/**
|
|
3424
|
+
* `authApi` is no longer used here (the refresh controller sends every refresh); the parameter
|
|
3425
|
+
* stays so the constructor's signature does not change.
|
|
3426
|
+
*/
|
|
3427
|
+
constructor(storageManager: StorageManager, _authApi: AuthAPI, subscribeStore: PassflowStore, platform?: PlatformAdapter);
|
|
3428
|
+
/** Whether this instance has a refresh cycle (waiting, in flight, a retry or a successor pending). */
|
|
3429
|
+
get isRefreshing(): boolean;
|
|
3430
|
+
/**
|
|
3431
|
+
* Connects `getTokensWithRefresh()` to the instance's refresh controller. `Passflow` calls it at
|
|
3432
|
+
* construction. Without it, `getTokensWithRefresh()` returns the cache and sends nothing.
|
|
3433
|
+
*/
|
|
3434
|
+
attachRefreshController(controller: Pick<RefreshController, 'refresh' | 'busy'> | undefined): void;
|
|
3435
|
+
/**
|
|
3436
|
+
* Starts the expiry check. `Passflow.subscribe()` calls it on every subscribe, so it must be
|
|
3437
|
+
* safe to repeat:
|
|
3438
|
+
* - A filled cache wins over storage. The cache only moves forward (the refresh controller
|
|
3439
|
+
* and its fence own every later change), while storage can still hold an older pair of the
|
|
3440
|
+
* same session (a held tab). Reading storage here would move the cache back.
|
|
3441
|
+
* - `TokenCacheExpired{isExpired:true}` fires only when the expired state changes, not on
|
|
3442
|
+
* every call.
|
|
3443
|
+
*/
|
|
3153
3444
|
initialize(): void;
|
|
3154
|
-
private refreshTokensCache;
|
|
3155
3445
|
startTokenCheck(): void;
|
|
3156
3446
|
private setupLifecycleListeners;
|
|
3157
|
-
|
|
3447
|
+
/**
|
|
3448
|
+
* Stops the expiry check and its foreground listener. Public so the refresh controller can stop
|
|
3449
|
+
* it when a session ends in this tab (it is called optionally: test fakes may lack it).
|
|
3450
|
+
*/
|
|
3451
|
+
stopTokenCheck(): void;
|
|
3158
3452
|
/**
|
|
3159
3453
|
* Cleanup method to stop all intervals and remove event listeners.
|
|
3160
3454
|
* Should be called when the service is no longer needed.
|
|
@@ -3162,10 +3456,23 @@ export declare class TokenCacheService {
|
|
|
3162
3456
|
destroy(): void;
|
|
3163
3457
|
setTokensCache(tokens: Tokens | undefined): void;
|
|
3164
3458
|
getTokens(): Tokens | undefined;
|
|
3459
|
+
/**
|
|
3460
|
+
* The cached tokens, refreshed first when the access token has expired (trigger `token-check`).
|
|
3461
|
+
*
|
|
3462
|
+
* It asks the refresh controller, which starts a refresh or joins the one pending, and returns
|
|
3463
|
+
* the cache whatever the outcome: the new tokens, the kept ones after a failure that did not end
|
|
3464
|
+
* the session, `undefined` once it ended. It never rejects; the controller reports the failure.
|
|
3465
|
+
*/
|
|
3165
3466
|
getTokensWithRefresh(): Promise<Tokens | undefined>;
|
|
3166
3467
|
getParsedTokens(): ParsedTokens | undefined;
|
|
3167
3468
|
isExpired(): boolean;
|
|
3168
3469
|
}
|
|
3470
|
+
/** How this instance's refresh controller coordinates (architecture §3.3, §3.4). */
|
|
3471
|
+
export type AuthServiceRefreshOptions = {
|
|
3472
|
+
coordination: ResolvedCoordination;
|
|
3473
|
+
/** `PassflowConfig.refreshStrikes`; the controller resolves the default. */
|
|
3474
|
+
strikes?: number;
|
|
3475
|
+
};
|
|
3169
3476
|
/**
|
|
3170
3477
|
* Service for handling authentication related functionality
|
|
3171
3478
|
*/
|
|
@@ -3185,13 +3492,19 @@ export declare class AuthService {
|
|
|
3185
3492
|
private tokenDeliveryManager;
|
|
3186
3493
|
private tokenExchangeConfig?;
|
|
3187
3494
|
private pendingPkce?;
|
|
3495
|
+
/**
|
|
3496
|
+
* The one place this instance sends `/auth/refresh` (PFL-792). Every refresh path asks it: the
|
|
3497
|
+
* explicit `refreshToken()`, the upgrade commit, `getTokens(true)`/`session()` and, once
|
|
3498
|
+
* `Passflow` attaches it, `TokenCacheService.getTokensWithRefresh()`.
|
|
3499
|
+
*/
|
|
3500
|
+
readonly refresh: RefreshController;
|
|
3188
3501
|
constructor(authApi: AuthAPI, deviceService: DeviceService, storageManager: StorageManager, subscribeStore: PassflowStore, tokenCacheService: TokenCacheService, scopes: string[], createTenantForNewUser: boolean, url: string, sessionCallbacks: {
|
|
3189
3502
|
createSession?: ({ tokens, parsedTokens }: {
|
|
3190
3503
|
tokens?: Tokens;
|
|
3191
3504
|
parsedTokens?: ParsedTokens;
|
|
3192
3505
|
}) => Promise<void>;
|
|
3193
3506
|
expiredSession?: () => Promise<void>;
|
|
3194
|
-
}, appId?: string | undefined, tokenExchangeConfig?: TokenExchangeConfig, platform?: PlatformAdapter, readPkceEnabled?: (() => Promise<boolean>) | undefined);
|
|
3507
|
+
}, appId?: string | undefined, tokenExchangeConfig?: TokenExchangeConfig, platform?: PlatformAdapter, readPkceEnabled?: (() => Promise<boolean>) | undefined, refreshOptions?: AuthServiceRefreshOptions);
|
|
3195
3508
|
/**
|
|
3196
3509
|
* Initialize session state on page load for cookie/BFF mode
|
|
3197
3510
|
*/
|
|
@@ -3203,11 +3516,20 @@ export declare class AuthService {
|
|
|
3203
3516
|
*/
|
|
3204
3517
|
restoreSession(): Promise<boolean>;
|
|
3205
3518
|
/**
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3519
|
+
* Applies a sign-in's tokens and starts a new session in the refresh controller.
|
|
3520
|
+
*
|
|
3521
|
+
* Every sign-in reaches the tokens through here: password, passwordless, passkey, federated,
|
|
3522
|
+
* the 2FA code and recovery-code completion, PKCE completion, `joinInvitation` and tokens from a
|
|
3523
|
+
* redirect (`applyTokens`). So this is where the controller learns of a new session (amendment
|
|
3524
|
+
* A1): a tab that signed out, or whose session ended, refreshes again on the new one.
|
|
3209
3525
|
*/
|
|
3210
3526
|
private processAuthResponse;
|
|
3527
|
+
/**
|
|
3528
|
+
* Stores an authentication response: token storage, session state, CSRF token. In BFF mode it
|
|
3529
|
+
* forwards the tokens to the BFF server first. Starts no session: a cookie-mode refresh applies
|
|
3530
|
+
* its answer through here too.
|
|
3531
|
+
*/
|
|
3532
|
+
private applyAuthResponse;
|
|
3211
3533
|
/**
|
|
3212
3534
|
* Reports whether a sign-in response is still waiting on its second factor,
|
|
3213
3535
|
* and when it is, hands the 2FA token on.
|
|
@@ -3269,7 +3591,28 @@ export declare class AuthService {
|
|
|
3269
3591
|
signUp(payload: PassflowSignUpPayload): Promise<PassflowAuthorizationResponse>;
|
|
3270
3592
|
passwordlessSignIn(payload: PassflowPasswordlessSignInPayload): Promise<PassflowPasswordlessResponse>;
|
|
3271
3593
|
passwordlessSignInComplete(payload: PassflowPasswordlessSignInCompletePayload): Promise<PassflowValidationResponse>;
|
|
3272
|
-
|
|
3594
|
+
/**
|
|
3595
|
+
* Signs this tab out under the refresh lock (PFL-792 §2.8): stops this tab's refresh callers and
|
|
3596
|
+
* retry timer, waits at most 5 s for the lock (a request already in flight lands first), posts
|
|
3597
|
+
* `signed-out` to the other tabs, calls the logout API with the NEWEST stored refresh token (best
|
|
3598
|
+
* effort), then deletes the stored tokens, id token and CSRF token and clears the token cache.
|
|
3599
|
+
*
|
|
3600
|
+
* Emits no `SignOut`: `Passflow.logOut()` emits exactly one, after this resolves.
|
|
3601
|
+
*
|
|
3602
|
+
* @returns the `sub` of the session that ended, when known
|
|
3603
|
+
*/
|
|
3604
|
+
logOut(): Promise<string | undefined>;
|
|
3605
|
+
/**
|
|
3606
|
+
* `Passflow.reset()` (PFL-792 amendment A2): the same local sign-out, synchronously, with no
|
|
3607
|
+
* API call and no lock. Emits no `SignOut`; the caller emits one.
|
|
3608
|
+
*
|
|
3609
|
+
* @returns the `sub` of the session that ended, when known
|
|
3610
|
+
*/
|
|
3611
|
+
resetSession(): string | undefined;
|
|
3612
|
+
/** The logout API call. Server-side cleanup is best effort: every failure is ignored. */
|
|
3613
|
+
private callLogoutApi;
|
|
3614
|
+
/** Deletes every local trace of the session (both delivery modes) and clears the token cache. */
|
|
3615
|
+
private clearLocalSession;
|
|
3273
3616
|
/**
|
|
3274
3617
|
* The session and user identity this client currently holds, or `undefined`
|
|
3275
3618
|
* when none is readable.
|
|
@@ -3302,21 +3645,38 @@ export declare class AuthService {
|
|
|
3302
3645
|
* state — two different axes.
|
|
3303
3646
|
*/
|
|
3304
3647
|
private assertSessionPin;
|
|
3648
|
+
/** The delivery mode the refresh transport sends in. BFF without a refresh URL has no endpoint: JSON, as before. */
|
|
3649
|
+
private refreshMode;
|
|
3650
|
+
/**
|
|
3651
|
+
* The controller's events, onto the store. Only `notify` is used (plus the error dedupe), so a
|
|
3652
|
+
* store double with `notify` alone works.
|
|
3653
|
+
*/
|
|
3654
|
+
private createRefreshEmit;
|
|
3655
|
+
/**
|
|
3656
|
+
* One refresh request in the current delivery mode, with no events and no saving (architecture
|
|
3657
|
+
* §3.2): the controller decides what an answer or a failure means.
|
|
3658
|
+
*
|
|
3659
|
+
* - JSON: `AuthAPI.refreshToken(stored refresh token, stored scopes, stored access token[, upgrade])`.
|
|
3660
|
+
* - Cookie: `AuthAPI.refreshToken('', scopes[, undefined, upgrade])`; the server reads the HttpOnly cookie.
|
|
3661
|
+
* - BFF: `POST tokenExchange.refreshUrl`. A non-OK answer becomes a `PassflowError` (status, and
|
|
3662
|
+
* the body's `error.id` or `error.http.<status>`), so it is classified like any other answer.
|
|
3663
|
+
*/
|
|
3664
|
+
private createRefreshTransport;
|
|
3665
|
+
/** The BFF refresh endpoint. A non-OK answer becomes a `PassflowError`; a network failure rejects as is. */
|
|
3666
|
+
private sendBffRefresh;
|
|
3305
3667
|
/**
|
|
3306
3668
|
* Refreshes the session, optionally committing an assurance upgrade.
|
|
3307
3669
|
*
|
|
3308
|
-
*
|
|
3309
|
-
*
|
|
3310
|
-
*
|
|
3311
|
-
*
|
|
3312
|
-
* leave the client holding stale tokens — everything looks fine until the next
|
|
3313
|
-
* request 401s.
|
|
3670
|
+
* Both go through the refresh controller, the one place this instance sends `/auth/refresh`: it
|
|
3671
|
+
* coordinates with other tabs, saves with a compare-and-set, classifies failures and emits the
|
|
3672
|
+
* events (PFL-792). A plain refresh is "fresh": it is satisfied only by tokens from a request
|
|
3673
|
+
* sent at or after the call. A failure keeps the tokens unless it really ends the session.
|
|
3314
3674
|
*
|
|
3315
|
-
*
|
|
3316
|
-
*
|
|
3317
|
-
*
|
|
3318
|
-
* upgrade transport classifies those failures
|
|
3319
|
-
*
|
|
3675
|
+
* The upgrade commit is the one path that is never classified: any failure rejects with the
|
|
3676
|
+
* error unchanged and keeps the tokens, because a 4xx there is the server rejecting the PROOF,
|
|
3677
|
+
* not the session (logging the user out for mistyping six digits is the defect this flow exists
|
|
3678
|
+
* to fix). The upgrade transport classifies those failures itself. The session pin is checked
|
|
3679
|
+
* when the answer arrives, before anything is applied: see `assertSessionPin`.
|
|
3320
3680
|
*/
|
|
3321
3681
|
refreshToken(upgrade?: UpgradeRefreshField, pin?: UpgradeSessionPin): Promise<PassflowAuthorizationResponse>;
|
|
3322
3682
|
sendPasswordResetEmail(payload: PassflowSendPasswordResetEmailPayload): Promise<PassflowSuccessResponse>;
|
|
@@ -3347,6 +3707,13 @@ export declare class AuthService {
|
|
|
3347
3707
|
* Handle session check and callbacks
|
|
3348
3708
|
*/
|
|
3349
3709
|
submitSessionCheck(doRefresh?: boolean): Promise<Tokens | undefined>;
|
|
3710
|
+
/**
|
|
3711
|
+
* `getTokens(true)` / `session({ doRefresh: true })` (architecture §2.8): starts or joins a
|
|
3712
|
+
* refresh, then resolves with the cached tokens whatever the outcome: the kept tokens after a
|
|
3713
|
+
* transient, blocked or auth-with-strikes-left failure, `undefined` once the session ended. The
|
|
3714
|
+
* failure itself was reported by the controller, so it is not reported again here.
|
|
3715
|
+
*/
|
|
3716
|
+
private refreshForSession;
|
|
3350
3717
|
/**
|
|
3351
3718
|
* Get tokens and refresh if needed
|
|
3352
3719
|
* Cookie/BFF mode: Returns ID token only (access/refresh in HttpOnly cookies)
|
|
@@ -3396,15 +3763,6 @@ export declare class InvitationService {
|
|
|
3396
3763
|
*/
|
|
3397
3764
|
getInvitationLink(invitationID: string): Promise<InviteLinkResponse>;
|
|
3398
3765
|
}
|
|
3399
|
-
/**
|
|
3400
|
-
* Logger interface for Passflow services
|
|
3401
|
-
*/
|
|
3402
|
-
interface Logger {
|
|
3403
|
-
error(message: string, ...args: unknown[]): void;
|
|
3404
|
-
warn(message: string, ...args: unknown[]): void;
|
|
3405
|
-
info(message: string, ...args: unknown[]): void;
|
|
3406
|
-
debug(message: string, ...args: unknown[]): void;
|
|
3407
|
-
}
|
|
3408
3766
|
/**
|
|
3409
3767
|
* Flat user representation
|
|
3410
3768
|
*/
|
|
@@ -5116,8 +5474,12 @@ export declare class Passflow {
|
|
|
5116
5474
|
* Creates proper ErrorPayload, distinguishes PassflowError from generic Error,
|
|
5117
5475
|
* notifies error event, and re-throws the error.
|
|
5118
5476
|
*
|
|
5477
|
+
* Reports at most once per error object: when a service already reported it (a 2FA wrapper,
|
|
5478
|
+
* a sign-in), the service's more specific report stands and nothing is added here.
|
|
5479
|
+
*
|
|
5119
5480
|
* @param error - The error to handle
|
|
5120
|
-
* @param
|
|
5481
|
+
* @param operation - The operation that failed (`ErrorPayload.operation`)
|
|
5482
|
+
* @param context - Context description for the fallback message
|
|
5121
5483
|
* @throws The original error after handling
|
|
5122
5484
|
*/
|
|
5123
5485
|
private handleError;
|
|
@@ -5200,11 +5562,31 @@ export declare class Passflow {
|
|
|
5200
5562
|
* ```
|
|
5201
5563
|
*/
|
|
5202
5564
|
reset(error?: string): void;
|
|
5565
|
+
/** The one `SignOut` of `logOut()`/`reset()` in this tab (PFL-792 §2.4). */
|
|
5566
|
+
private notifySignedOut;
|
|
5567
|
+
/**
|
|
5568
|
+
* Releases what this instance holds for token refresh: leaves the refresh lock queue, cancels
|
|
5569
|
+
* every refresh timer and the token check, removes its storage and foreground listeners, closes
|
|
5570
|
+
* its BroadcastChannel, and rejects pending refresh callers with `PassflowRefreshError`
|
|
5571
|
+
* (`error.refresh.disposed`). Afterwards the instance must not be used.
|
|
5572
|
+
*
|
|
5573
|
+
* The SDK never calls it by itself, not on `beforeunload` (it fires before a cancellable
|
|
5574
|
+
* "Leave site?" prompt and before a page enters the back/forward cache) nor on `pagehide`:
|
|
5575
|
+
* document teardown already releases the page's locks and channels. Call it when an SPA tears
|
|
5576
|
+
* the instance down, and in tests.
|
|
5577
|
+
*
|
|
5578
|
+
* @example
|
|
5579
|
+
* ```typescript
|
|
5580
|
+
* passflow.dispose();
|
|
5581
|
+
* ```
|
|
5582
|
+
*/
|
|
5583
|
+
dispose(): void;
|
|
5203
5584
|
/**
|
|
5204
5585
|
* Refresh the access token using the refresh token.
|
|
5205
5586
|
*
|
|
5206
5587
|
* @returns Promise with new authorization response containing refreshed tokens
|
|
5207
|
-
* @throws {Error}
|
|
5588
|
+
* @throws {Error} JSON mode with no stored refresh token (`No tokens found` / `No refresh token found`)
|
|
5589
|
+
* @throws {PassflowRefreshError} `error.refresh.session_ended` and the other ends, once the session ended
|
|
5208
5590
|
* @throws {PassflowError} If refresh request fails
|
|
5209
5591
|
*
|
|
5210
5592
|
* @example
|
|
@@ -5904,6 +6286,51 @@ export declare class Passflow {
|
|
|
5904
6286
|
*/
|
|
5905
6287
|
clearMagicLinkSession(): void;
|
|
5906
6288
|
}
|
|
6289
|
+
/** §2.6 row 2: the session cannot be refreshed, whatever the status. */
|
|
6290
|
+
export declare const REFRESH_AUTH_FAILURE_IDS: readonly string[];
|
|
6291
|
+
/** Ids of {@link PassflowRefreshError}. */
|
|
6292
|
+
export declare const RefreshErrorId: {
|
|
6293
|
+
/** Joined another tab's cycle whose attempt failed transiently (status 503). */
|
|
6294
|
+
readonly unavailable: 'error.refresh.unavailable';
|
|
6295
|
+
/** Joined another tab's cycle whose attempt failed on auth with strikes left (status 401). */
|
|
6296
|
+
readonly authRetry: 'error.refresh.auth_retry';
|
|
6297
|
+
/** The session ended (strikes exhausted here or elsewhere). */
|
|
6298
|
+
readonly sessionEnded: 'error.refresh.session_ended';
|
|
6299
|
+
/** A sign-out (this tab or another tab of the user). */
|
|
6300
|
+
readonly signedOut: 'error.refresh.signed_out';
|
|
6301
|
+
/** `error.token.blocked` and no successor within 20 s. */
|
|
6302
|
+
readonly successorTimeout: 'error.refresh.successor_timeout';
|
|
6303
|
+
/** Storage now holds another user's tokens. */
|
|
6304
|
+
readonly identityChanged: 'error.refresh.identity_changed';
|
|
6305
|
+
/** `Passflow.dispose()` was called. */
|
|
6306
|
+
readonly disposed: 'error.refresh.disposed';
|
|
6307
|
+
};
|
|
6308
|
+
export type RefreshErrorId = (typeof RefreshErrorId)[keyof typeof RefreshErrorId];
|
|
6309
|
+
interface PassflowRefreshErrorOptions {
|
|
6310
|
+
message?: string;
|
|
6311
|
+
/** Epoch ms of the shared pending retry, for `unavailable` / `auth_retry`. */
|
|
6312
|
+
nextRetryAt?: number;
|
|
6313
|
+
/** The failure this error stands for (another tab's attempt, the request's own error). */
|
|
6314
|
+
cause?: unknown;
|
|
6315
|
+
}
|
|
6316
|
+
/** A refresh outcome the SDK decided, not the server: joined a failed cycle, or the session ended. */
|
|
6317
|
+
export declare class PassflowRefreshError extends PassflowError {
|
|
6318
|
+
readonly id: RefreshErrorId;
|
|
6319
|
+
readonly nextRetryAt?: number;
|
|
6320
|
+
readonly cause?: unknown;
|
|
6321
|
+
constructor(id: RefreshErrorId, options?: PassflowRefreshErrorOptions);
|
|
6322
|
+
}
|
|
6323
|
+
/** Why an upgrade commit was not sent, or its answer not applied. */
|
|
6324
|
+
type UpgradeCommitInterruptedReason = 'lock-timeout' | 'superseded';
|
|
6325
|
+
/**
|
|
6326
|
+
* Upgrade commit only: the commit could not be sent (lock wait over 10 s) or its answer arrived
|
|
6327
|
+
* after storage moved to another token. A plain Error with no `status` and no `response`: the
|
|
6328
|
+
* upgrade transport's classifier reads it as `network` (retryable, no invalidation).
|
|
6329
|
+
*/
|
|
6330
|
+
export declare class UpgradeCommitInterruptedError extends Error {
|
|
6331
|
+
readonly reason: UpgradeCommitInterruptedReason;
|
|
6332
|
+
constructor(reason: UpgradeCommitInterruptedReason, message?: string);
|
|
6333
|
+
}
|
|
5907
6334
|
/**
|
|
5908
6335
|
* Validates JWT token format (not signature, just structure)
|
|
5909
6336
|
* Checks that the token has 3 parts separated by dots and each part is valid base64url
|
|
@@ -5941,4 +6368,4 @@ export declare function isValidPhoneNumber(phone: string): boolean;
|
|
|
5941
6368
|
* @returns true if valid username format, false otherwise
|
|
5942
6369
|
*/
|
|
5943
6370
|
export declare function isValidUsername(username: string): boolean;
|
|
5944
|
-
export type { AssuranceBar, AuthCallbackResult, BodyKind, ChallengeChannel, CreateClientConfig, CurrentUrlInfo, ErrorPayload, FetchLike, FetchMiddleware, GroupMembership, InvitationToken, M2MClientConfig, M2MErrorCode, M2MErrorResponse, M2MRateLimitInfo, M2MTokenCache, M2MTokenClaims, M2MTokenRequestInfo, M2MTokenRequestOptions, M2MTokenResponse, OpenAuthUrlOptions, PassflowEventPayload, PassflowSubscriber, PasskeySupport, PlatformAdapter, PlatformStorage, RawUserMembership, RegisterOptions, ReplayPolicy, RequestSnapshot, ResponseInterceptorHost, RetryStrategy, SnapshotHeader, Tenant, TenantMembership, Token, UpgradeAssertContext, UpgradeAssertion, UpgradeChallenge, UpgradeControllerOptions, UpgradeDiagnostic, UpgradeDiagnosticReason, UpgradeDiagnosticSink, UpgradeElevation, UpgradeEnableOptions, UpgradeEndpoint, UpgradeErrorKind, UpgradeFacade, UpgradeFactor, UpgradeFactorInput, UpgradeFactorType, UpgradeFlightState, UpgradeFlightSummary, UpgradeOptions, UpgradePrepared, UpgradePromptHandler, UpgradePromptRequest, UpgradePromptResult, UpgradeProof, UpgradeReplayInfo, UpgradeResult, UpgradeSessionPin, UpgradeTransport, UpgradeTransportErrorInit, UpgradeWaiterSummary, UserMembership, Waiter, WaiterEntry, WaiterOutcome, WaiterReport };
|
|
6371
|
+
export type { AssuranceBar, AuthCallbackResult, BodyKind, ChallengeChannel, CreateClientConfig, CurrentUrlInfo, ErrorPayload, EventOrigin, FetchLike, FetchMiddleware, GroupMembership, InvitationToken, M2MClientConfig, M2MErrorCode, M2MErrorResponse, M2MRateLimitInfo, M2MTokenCache, M2MTokenClaims, M2MTokenRequestInfo, M2MTokenRequestOptions, M2MTokenResponse, OpenAuthUrlOptions, OperationErrorPayload, PassflowEventPayload, PassflowOperation, PassflowRefreshErrorOptions, PassflowSubscriber, PasskeySupport, PlatformAdapter, PlatformStorage, RawUserMembership, RefreshChannel, RefreshClock, RefreshCoordination, RefreshCoordinator, RefreshErrorInfo, RefreshFailureKind, RefreshLockManager, RefreshLockOptions, RefreshTrigger, RegisterOptions, ReplayPolicy, RequestSnapshot, ResponseInterceptorHost, RetryStrategy, SessionExpiredReason, SnapshotHeader, Tenant, TenantMembership, Token, UpgradeAssertContext, UpgradeAssertion, UpgradeChallenge, UpgradeCommitInterruptedReason, UpgradeControllerOptions, UpgradeDiagnostic, UpgradeDiagnosticReason, UpgradeDiagnosticSink, UpgradeElevation, UpgradeEnableOptions, UpgradeEndpoint, UpgradeErrorKind, UpgradeFacade, UpgradeFactor, UpgradeFactorInput, UpgradeFactorType, UpgradeFlightState, UpgradeFlightSummary, UpgradeOptions, UpgradePrepared, UpgradePromptHandler, UpgradePromptRequest, UpgradePromptResult, UpgradeProof, UpgradeReplayInfo, UpgradeResult, UpgradeSessionPin, UpgradeTransport, UpgradeTransportErrorInit, UpgradeWaiterSummary, UserMembership, Waiter, WaiterEntry, WaiterOutcome, WaiterReport };
|