@atlasauth/js 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/environment.d.ts +79 -0
- package/dist/environment.js +74 -0
- package/dist/environment.js.map +1 -0
- package/dist/frontchannel-logout.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-settings.d.ts +9 -0
- package/dist/instance-settings.js +2 -1
- package/dist/instance-settings.js.map +1 -1
- package/dist/popup-channel.d.ts +95 -0
- package/dist/popup-channel.js +231 -0
- package/dist/popup-channel.js.map +1 -0
- package/dist/redirect.d.ts +45 -4
- package/dist/redirect.js +102 -16
- package/dist/redirect.js.map +1 -1
- package/dist/sign-out-guard.d.ts +114 -0
- package/dist/sign-out-guard.js +197 -0
- package/dist/sign-out-guard.js.map +1 -0
- package/dist/sign-out.d.ts +78 -9
- package/dist/sign-out.js +236 -27
- package/dist/sign-out.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole safer sign-out, once, for every framework provider.
|
|
3
|
+
*
|
|
4
|
+
* `@atlasauth/react` grew this inline; every other provider (Vue, Svelte,
|
|
5
|
+
* Angular, Solid, Qwik, the native SDKs) drives the same steps through a
|
|
6
|
+
* {@link SignOutGuard} so the behaviour cannot drift:
|
|
7
|
+
*
|
|
8
|
+
* - `signOut()` remembers the session id BEFORE revoking (a tombstone), revokes
|
|
9
|
+
* it on the Frontend API (retried once), runs front-channel logout, and
|
|
10
|
+
* reports `{ revoked, activeSessionId? }` — or rejects with a
|
|
11
|
+
* {@link SignOutError} under `{ strict: true }`;
|
|
12
|
+
* - every boot / reload / refresh / sibling-tab broadcast asks
|
|
13
|
+
* {@link SignOutGuard.refuse} before adopting a session, so a session this
|
|
14
|
+
* browser signed out of is never re-adopted from a refresh cookie (or a
|
|
15
|
+
* stored token) the revoke did not clear;
|
|
16
|
+
* - a refused session is revoked again in the background, and its tombstone is
|
|
17
|
+
* dropped once the server confirms;
|
|
18
|
+
* - a parked session the server promoted on revoke is adopted
|
|
19
|
+
* ({@link SignOutGuard.promoted}).
|
|
20
|
+
*
|
|
21
|
+
* The provider keeps its own state and calls these at the matching points.
|
|
22
|
+
*/
|
|
23
|
+
import type { SignOutOptions } from './frontchannel-logout';
|
|
24
|
+
import { SessionTombstones, type RevokeSessionOutcome, type SignOutResult, type TombstoneStore } from './sign-out';
|
|
25
|
+
/** A credentialed Frontend API request (`path` is relative to the FAPI base URL). */
|
|
26
|
+
export type FapiRequest = (path: string, init?: RequestInit) => Promise<Response>;
|
|
27
|
+
/** Options for {@link SignOutGuard}. */
|
|
28
|
+
export interface SignOutGuardOptions {
|
|
29
|
+
/** The provider's FAPI request: cookies (web) or its bearer token (native). */
|
|
30
|
+
request: FapiRequest;
|
|
31
|
+
/** The Frontend API base URL — tombstones are kept per Frontend API origin. */
|
|
32
|
+
frontendApi?: string;
|
|
33
|
+
/** A persistent store for platforms without `localStorage` (see {@link TombstoneStore}). */
|
|
34
|
+
store?: TombstoneStore;
|
|
35
|
+
/** Bring your own tombstones (tests, or a set shared with other code). */
|
|
36
|
+
tombstones?: SessionTombstones;
|
|
37
|
+
/**
|
|
38
|
+
* Delays (ms) between background re-tries of a revoke `signOut()` could not
|
|
39
|
+
* complete. Default `[15_000, 60_000, 300_000]`; `[]` re-tries only when a
|
|
40
|
+
* refused session shows up on a later boot.
|
|
41
|
+
*/
|
|
42
|
+
retryDelaysMs?: number[];
|
|
43
|
+
/** Skip front-channel logout entirely (runtimes without a DOM). */
|
|
44
|
+
frontChannelLogout?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** What {@link SignOutGuard.revoke} did on the server. */
|
|
47
|
+
export interface SignOutAttempt {
|
|
48
|
+
/** The session that was signed out of, or null when there was none. */
|
|
49
|
+
sessionId: string | null;
|
|
50
|
+
outcome: RevokeSessionOutcome;
|
|
51
|
+
}
|
|
52
|
+
/** The cross-tab sign-out signal: session ids (never tokens) a tab signed out of. */
|
|
53
|
+
export interface SignedOutSignal {
|
|
54
|
+
signedOut?: string[];
|
|
55
|
+
}
|
|
56
|
+
export declare class SignOutGuard {
|
|
57
|
+
/** The sessions this browser (or app) signed out of. */
|
|
58
|
+
readonly tombstones: SessionTombstones;
|
|
59
|
+
private readonly request;
|
|
60
|
+
private readonly retryDelaysMs;
|
|
61
|
+
private readonly frontChannelLogout;
|
|
62
|
+
/** Sessions with a background revoke in flight or scheduled. */
|
|
63
|
+
private readonly retrying;
|
|
64
|
+
private readonly timers;
|
|
65
|
+
constructor(options: SignOutGuardOptions);
|
|
66
|
+
/** Settles once persisted tombstones are loaded — await it before the first boot. */
|
|
67
|
+
get ready(): Promise<void>;
|
|
68
|
+
/** Whether this browser signed out of `sessionId`. */
|
|
69
|
+
isSignedOut(sessionId: string | null | undefined): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Call with the session a boot / reload / refresh is about to adopt. True when
|
|
72
|
+
* it must NOT be adopted (this browser signed out of it); the revoke is then
|
|
73
|
+
* re-tried in the background and the tombstone dropped once it lands.
|
|
74
|
+
*/
|
|
75
|
+
refuse(sessionId: string | null | undefined, request?: FapiRequest): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The server half of `signOut()`. Finds the session to end when the provider
|
|
78
|
+
* does not know it (`GET /v1/client` — the revoke works from the cookies, so a
|
|
79
|
+
* missing in-memory token never skips it), tombstones it, revokes it (retried
|
|
80
|
+
* once) and runs front-channel logout. A failed revoke is re-tried in the
|
|
81
|
+
* background. Pass `request` to revoke with a credential the provider is about
|
|
82
|
+
* to drop (a native bearer token).
|
|
83
|
+
*/
|
|
84
|
+
revoke(sessionId: string | null | undefined, options?: SignOutOptions & {
|
|
85
|
+
request?: FapiRequest;
|
|
86
|
+
}): Promise<SignOutAttempt>;
|
|
87
|
+
/** The parked session the server promoted on revoke, when it should be adopted. */
|
|
88
|
+
promoted(attempt: SignOutAttempt): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* What `signOut()` resolves with — or, with `{ strict: true }` and a revoke the
|
|
91
|
+
* server never confirmed, the {@link SignOutError} it rejects with. Call after
|
|
92
|
+
* the local sign-out.
|
|
93
|
+
*/
|
|
94
|
+
settle(attempt: SignOutAttempt, options?: SignOutOptions): SignOutResult;
|
|
95
|
+
/** The cross-tab message announcing a sign-out (`{}` when there was no session). */
|
|
96
|
+
signal(attempt: SignOutAttempt): SignedOutSignal;
|
|
97
|
+
/**
|
|
98
|
+
* Learn the sessions a sibling tab signed out of from its broadcast — call it
|
|
99
|
+
* BEFORE re-checking, so the re-check cannot re-adopt one. Returns the ids.
|
|
100
|
+
*/
|
|
101
|
+
learn(message: unknown): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Call `onSignedOut` when another tab signs out of the session `current()`
|
|
104
|
+
* returns (its tombstone reached the shared store). Returns the unsubscribe.
|
|
105
|
+
*/
|
|
106
|
+
watch(current: () => string | null | undefined, onSignedOut: () => void): () => void;
|
|
107
|
+
/** Cancel scheduled background re-tries (provider teardown). */
|
|
108
|
+
dispose(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Revoke `sessionId` again: now when `delays` is empty, else after each delay
|
|
111
|
+
* in turn until one lands. Drops the tombstone on success. One chain per session.
|
|
112
|
+
*/
|
|
113
|
+
private retryInBackground;
|
|
114
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The whole safer sign-out, once, for every framework provider.
|
|
4
|
+
*
|
|
5
|
+
* `@atlasauth/react` grew this inline; every other provider (Vue, Svelte,
|
|
6
|
+
* Angular, Solid, Qwik, the native SDKs) drives the same steps through a
|
|
7
|
+
* {@link SignOutGuard} so the behaviour cannot drift:
|
|
8
|
+
*
|
|
9
|
+
* - `signOut()` remembers the session id BEFORE revoking (a tombstone), revokes
|
|
10
|
+
* it on the Frontend API (retried once), runs front-channel logout, and
|
|
11
|
+
* reports `{ revoked, activeSessionId? }` — or rejects with a
|
|
12
|
+
* {@link SignOutError} under `{ strict: true }`;
|
|
13
|
+
* - every boot / reload / refresh / sibling-tab broadcast asks
|
|
14
|
+
* {@link SignOutGuard.refuse} before adopting a session, so a session this
|
|
15
|
+
* browser signed out of is never re-adopted from a refresh cookie (or a
|
|
16
|
+
* stored token) the revoke did not clear;
|
|
17
|
+
* - a refused session is revoked again in the background, and its tombstone is
|
|
18
|
+
* dropped once the server confirms;
|
|
19
|
+
* - a parked session the server promoted on revoke is adopted
|
|
20
|
+
* ({@link SignOutGuard.promoted}).
|
|
21
|
+
*
|
|
22
|
+
* The provider keeps its own state and calls these at the matching points.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.SignOutGuard = void 0;
|
|
26
|
+
const frontchannel_logout_1 = require("./frontchannel-logout");
|
|
27
|
+
const sign_out_1 = require("./sign-out");
|
|
28
|
+
const DEFAULT_RETRY_DELAYS_MS = [15_000, 60_000, 300_000];
|
|
29
|
+
class SignOutGuard {
|
|
30
|
+
/** The sessions this browser (or app) signed out of. */
|
|
31
|
+
tombstones;
|
|
32
|
+
request;
|
|
33
|
+
retryDelaysMs;
|
|
34
|
+
frontChannelLogout;
|
|
35
|
+
/** Sessions with a background revoke in flight or scheduled. */
|
|
36
|
+
retrying = new Set();
|
|
37
|
+
timers = new Set();
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.request = options.request;
|
|
40
|
+
this.tombstones =
|
|
41
|
+
options.tombstones ??
|
|
42
|
+
new sign_out_1.SessionTombstones({ frontendApi: options.frontendApi, ...(options.store ? { store: options.store } : {}) });
|
|
43
|
+
this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
44
|
+
this.frontChannelLogout = options.frontChannelLogout !== false;
|
|
45
|
+
}
|
|
46
|
+
/** Settles once persisted tombstones are loaded — await it before the first boot. */
|
|
47
|
+
get ready() {
|
|
48
|
+
return this.tombstones.ready;
|
|
49
|
+
}
|
|
50
|
+
/** Whether this browser signed out of `sessionId`. */
|
|
51
|
+
isSignedOut(sessionId) {
|
|
52
|
+
return this.tombstones.has(sessionId);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Call with the session a boot / reload / refresh is about to adopt. True when
|
|
56
|
+
* it must NOT be adopted (this browser signed out of it); the revoke is then
|
|
57
|
+
* re-tried in the background and the tombstone dropped once it lands.
|
|
58
|
+
*/
|
|
59
|
+
refuse(sessionId, request) {
|
|
60
|
+
if (!this.tombstones.has(sessionId))
|
|
61
|
+
return false;
|
|
62
|
+
this.retryInBackground(sessionId, request ?? this.request, []);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The server half of `signOut()`. Finds the session to end when the provider
|
|
67
|
+
* does not know it (`GET /v1/client` — the revoke works from the cookies, so a
|
|
68
|
+
* missing in-memory token never skips it), tombstones it, revokes it (retried
|
|
69
|
+
* once) and runs front-channel logout. A failed revoke is re-tried in the
|
|
70
|
+
* background. Pass `request` to revoke with a credential the provider is about
|
|
71
|
+
* to drop (a native bearer token).
|
|
72
|
+
*/
|
|
73
|
+
async revoke(sessionId, options = {}) {
|
|
74
|
+
const request = options.request ?? this.request;
|
|
75
|
+
let id = sessionId || null;
|
|
76
|
+
let outcome = { revoked: true, status: 200 };
|
|
77
|
+
if (!id) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await request('/v1/client');
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error('boot failed');
|
|
82
|
+
const body = (await response.json());
|
|
83
|
+
id = typeof body?.session?.id === 'string' ? body.session.id : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
outcome = { revoked: false, status: 0 };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (id) {
|
|
90
|
+
// Remember it first: from here on nothing may re-adopt it.
|
|
91
|
+
this.tombstones.add(id);
|
|
92
|
+
outcome = await (0, sign_out_1.revokeSession)(request, id);
|
|
93
|
+
// OIDC Front-Channel Logout: let the apps signed in through this session
|
|
94
|
+
// clear theirs in this browser (hidden iframes, capped at 5 s). Best-effort.
|
|
95
|
+
if (this.frontChannelLogout && options.frontchannel !== false && typeof document !== 'undefined') {
|
|
96
|
+
await (0, frontchannel_logout_1.runFrontChannelLogoutFromResponse)(outcome.response);
|
|
97
|
+
}
|
|
98
|
+
if (!outcome.revoked)
|
|
99
|
+
this.retryInBackground(id, request, this.retryDelaysMs);
|
|
100
|
+
}
|
|
101
|
+
return { sessionId: id, outcome };
|
|
102
|
+
}
|
|
103
|
+
/** The parked session the server promoted on revoke, when it should be adopted. */
|
|
104
|
+
promoted(attempt) {
|
|
105
|
+
const id = attempt.outcome.activeSessionId;
|
|
106
|
+
return id && !this.tombstones.has(id) ? id : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* What `signOut()` resolves with — or, with `{ strict: true }` and a revoke the
|
|
110
|
+
* server never confirmed, the {@link SignOutError} it rejects with. Call after
|
|
111
|
+
* the local sign-out.
|
|
112
|
+
*/
|
|
113
|
+
settle(attempt, options = {}) {
|
|
114
|
+
const { outcome, sessionId } = attempt;
|
|
115
|
+
if (!outcome.revoked && options.strict)
|
|
116
|
+
throw new sign_out_1.SignOutError(sessionId ?? '', outcome.status);
|
|
117
|
+
return outcome.activeSessionId
|
|
118
|
+
? { revoked: outcome.revoked, activeSessionId: outcome.activeSessionId }
|
|
119
|
+
: { revoked: outcome.revoked };
|
|
120
|
+
}
|
|
121
|
+
/** The cross-tab message announcing a sign-out (`{}` when there was no session). */
|
|
122
|
+
signal(attempt) {
|
|
123
|
+
return attempt.sessionId ? { signedOut: [attempt.sessionId] } : {};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Learn the sessions a sibling tab signed out of from its broadcast — call it
|
|
127
|
+
* BEFORE re-checking, so the re-check cannot re-adopt one. Returns the ids.
|
|
128
|
+
*/
|
|
129
|
+
learn(message) {
|
|
130
|
+
const list = message?.signedOut;
|
|
131
|
+
if (!Array.isArray(list))
|
|
132
|
+
return [];
|
|
133
|
+
const ids = list.filter((id) => typeof id === 'string' && !!id);
|
|
134
|
+
this.tombstones.add(...ids);
|
|
135
|
+
return ids;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Call `onSignedOut` when another tab signs out of the session `current()`
|
|
139
|
+
* returns (its tombstone reached the shared store). Returns the unsubscribe.
|
|
140
|
+
*/
|
|
141
|
+
watch(current, onSignedOut) {
|
|
142
|
+
return this.tombstones.subscribe((ids) => {
|
|
143
|
+
const id = current();
|
|
144
|
+
if (id && ids.includes(id))
|
|
145
|
+
onSignedOut();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Cancel scheduled background re-tries (provider teardown). */
|
|
149
|
+
dispose() {
|
|
150
|
+
for (const t of this.timers)
|
|
151
|
+
clearTimeout(t);
|
|
152
|
+
this.timers.clear();
|
|
153
|
+
this.retrying.clear();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Revoke `sessionId` again: now when `delays` is empty, else after each delay
|
|
157
|
+
* in turn until one lands. Drops the tombstone on success. One chain per session.
|
|
158
|
+
*/
|
|
159
|
+
retryInBackground(sessionId, request, delays) {
|
|
160
|
+
if (this.retrying.has(sessionId))
|
|
161
|
+
return;
|
|
162
|
+
this.retrying.add(sessionId);
|
|
163
|
+
const attempt = async (index) => {
|
|
164
|
+
let revoked = false;
|
|
165
|
+
try {
|
|
166
|
+
revoked = (await (0, sign_out_1.revokeSession)(request, sessionId, { retries: 0 })).revoked;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
revoked = false;
|
|
170
|
+
}
|
|
171
|
+
if (revoked) {
|
|
172
|
+
this.tombstones.remove(sessionId);
|
|
173
|
+
this.retrying.delete(sessionId);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (index >= delays.length || !this.retrying.has(sessionId)) {
|
|
177
|
+
this.retrying.delete(sessionId);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
schedule(index);
|
|
181
|
+
};
|
|
182
|
+
const schedule = (index) => {
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
this.timers.delete(timer);
|
|
185
|
+
void attempt(index + 1);
|
|
186
|
+
}, delays[index]);
|
|
187
|
+
timer.unref?.();
|
|
188
|
+
this.timers.add(timer);
|
|
189
|
+
};
|
|
190
|
+
if (delays.length === 0)
|
|
191
|
+
void attempt(0);
|
|
192
|
+
else
|
|
193
|
+
schedule(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.SignOutGuard = SignOutGuard;
|
|
197
|
+
//# sourceMappingURL=sign-out-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-out-guard.js","sourceRoot":"","sources":["../src/sign-out-guard.ts"],"names":[],"mappings":"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"}
|
package/dist/sign-out.d.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* These helpers close that gap: {@link revokeSession} retries once and reports
|
|
12
12
|
* what the server did (including the parked session it promoted, if any), and
|
|
13
|
-
* {@link SessionTombstones} remembers the ids of sessions this
|
|
14
|
-
* so a reload never re-adopts one, even when
|
|
15
|
-
* server.
|
|
13
|
+
* {@link SessionTombstones} remembers the ids of sessions this browser signed
|
|
14
|
+
* out of so a reload — or a tab opened later — never re-adopts one, even when
|
|
15
|
+
* the revoke never reached the server.
|
|
16
16
|
*/
|
|
17
17
|
/** What `signOut()` resolves with. */
|
|
18
18
|
export interface SignOutResult {
|
|
@@ -58,17 +58,86 @@ export interface RevokeSessionOutcome {
|
|
|
58
58
|
export declare function revokeSession(request: (path: string, init?: RequestInit) => Promise<Response>, sessionId: string, options?: {
|
|
59
59
|
retries?: number;
|
|
60
60
|
}): Promise<RevokeSessionOutcome>;
|
|
61
|
+
/** Options for {@link SessionTombstones}. */
|
|
62
|
+
export interface SessionTombstonesOptions {
|
|
63
|
+
/**
|
|
64
|
+
* The Frontend API URL or origin the sessions belong to. Tombstones are kept
|
|
65
|
+
* per Frontend API origin, so two Atlas instances used on one site never
|
|
66
|
+
* share (or clear) each other's.
|
|
67
|
+
*/
|
|
68
|
+
frontendApi?: string;
|
|
69
|
+
/** Override the storage key entirely (the Frontend API scope is then not appended). */
|
|
70
|
+
storageKey?: string;
|
|
71
|
+
/** Clock, for tests. */
|
|
72
|
+
now?: () => number;
|
|
73
|
+
/**
|
|
74
|
+
* Where to keep the tombstones instead of `localStorage` — for runtimes
|
|
75
|
+
* without one (React Native, Expo, Electron's main process, a Capacitor app,
|
|
76
|
+
* an extension service worker). Pass the platform's persistent store
|
|
77
|
+
* (AsyncStorage, `expo-secure-store`, `chrome.storage`, Capacitor
|
|
78
|
+
* Preferences…) through a two-method adapter. Reads may be async: await
|
|
79
|
+
* {@link SessionTombstones.ready} before trusting `has()` on a cold start.
|
|
80
|
+
*/
|
|
81
|
+
store?: TombstoneStore;
|
|
82
|
+
}
|
|
61
83
|
/**
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
64
|
-
*
|
|
65
|
-
|
|
84
|
+
* A key → string store for {@link SessionTombstones} on a platform without
|
|
85
|
+
* `localStorage`. Either method may be sync or async; errors are swallowed
|
|
86
|
+
* (memory still protects the running process).
|
|
87
|
+
*/
|
|
88
|
+
export interface TombstoneStore {
|
|
89
|
+
getItem(key: string): string | null | undefined | Promise<string | null | undefined>;
|
|
90
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The ids of sessions this browser signed out of, so a reload — or a tab opened
|
|
94
|
+
* later — never re-adopts one from a refresh cookie the revoke did not clear.
|
|
95
|
+
*
|
|
96
|
+
* Kept in `localStorage` (shared by every tab of the site and surviving a
|
|
97
|
+
* restart), at most 20 entries, each for 30 days, pruned whenever it is read,
|
|
98
|
+
* and keyed per Frontend API origin. Tabs see each other's changes on every
|
|
99
|
+
* read, and {@link SessionTombstones.subscribe} reports them as they happen
|
|
100
|
+
* (the `storage` event). `sessionStorage` is used only when `localStorage`
|
|
101
|
+
* throws (storage disabled, sandboxed frames); memory always backs both. A
|
|
102
|
+
* confirmed revoke may {@link SessionTombstones.remove} its tombstone. Never
|
|
103
|
+
* holds a token — a session id is not a credential.
|
|
66
104
|
*/
|
|
67
105
|
export declare class SessionTombstones {
|
|
106
|
+
private entries;
|
|
107
|
+
/** Ids no storage accepted: only memory remembers them, so a sync keeps them. */
|
|
108
|
+
private unsaved;
|
|
68
109
|
private readonly storageKey;
|
|
69
|
-
private readonly
|
|
70
|
-
|
|
110
|
+
private readonly now;
|
|
111
|
+
private readonly store;
|
|
112
|
+
/** False until a custom store has been read: writing earlier would drop what it holds. */
|
|
113
|
+
private hydrated;
|
|
114
|
+
/**
|
|
115
|
+
* Settles once the tombstones a {@link SessionTombstonesOptions.store} holds
|
|
116
|
+
* have been read (at once for `localStorage`). Never rejects.
|
|
117
|
+
*/
|
|
118
|
+
readonly ready: Promise<void>;
|
|
119
|
+
constructor(options?: string | SessionTombstonesOptions);
|
|
71
120
|
has(sessionId: string | null | undefined): boolean;
|
|
72
121
|
add(...sessionIds: (string | null | undefined)[]): void;
|
|
122
|
+
/**
|
|
123
|
+
* Forget tombstones — called once a later revoke of the session is confirmed
|
|
124
|
+
* (the server has ended it, so there is nothing left to re-adopt).
|
|
125
|
+
*/
|
|
126
|
+
remove(...sessionIds: (string | null | undefined)[]): void;
|
|
73
127
|
list(): string[];
|
|
128
|
+
/**
|
|
129
|
+
* Call `listener` when another tab adds or removes a tombstone (the `storage`
|
|
130
|
+
* event). Returns the unsubscribe function; a no-op outside a browser.
|
|
131
|
+
*/
|
|
132
|
+
subscribe(listener: (ids: string[]) => void): () => void;
|
|
133
|
+
/**
|
|
134
|
+
* Re-read storage — the source of truth, so another tab's add or remove is
|
|
135
|
+
* seen here — keeping only what this tab could not store; then drop expired
|
|
136
|
+
* entries. With no readable storage, memory is all there is.
|
|
137
|
+
*/
|
|
138
|
+
private sync;
|
|
139
|
+
private prune;
|
|
140
|
+
/** Read a custom store once, merging anything added before it answered. */
|
|
141
|
+
private hydrate;
|
|
142
|
+
private persist;
|
|
74
143
|
}
|