@icp-sdk/auth 8.0.3 → 9.0.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.
Files changed (85) hide show
  1. package/dist/esm/client/app-delegation-source.d.ts +65 -0
  2. package/dist/esm/client/app-delegation-source.js +58 -0
  3. package/dist/esm/client/app-delegation-source.js.map +1 -0
  4. package/dist/esm/client/app-lock.d.ts +63 -0
  5. package/dist/esm/client/app-lock.js +92 -0
  6. package/dist/esm/client/app-lock.js.map +1 -0
  7. package/dist/esm/client/auth-client.d.ts +270 -53
  8. package/dist/esm/client/auth-client.js +1017 -414
  9. package/dist/esm/client/auth-client.js.map +1 -1
  10. package/dist/esm/client/base64.d.ts +8 -0
  11. package/dist/esm/client/base64.js +28 -0
  12. package/dist/esm/client/base64.js.map +1 -0
  13. package/dist/esm/client/cookie-state-storage.d.ts +59 -0
  14. package/dist/esm/client/cookie-state-storage.js +245 -0
  15. package/dist/esm/client/cookie-state-storage.js.map +1 -0
  16. package/dist/esm/client/credential-storage.d.ts +48 -0
  17. package/dist/esm/client/credential-storage.js +2 -0
  18. package/dist/esm/client/credential-storage.js.map +1 -0
  19. package/dist/esm/client/db.d.ts +6 -1
  20. package/dist/esm/client/db.js +56 -15
  21. package/dist/esm/client/db.js.map +1 -1
  22. package/dist/esm/client/foreground-refresh.d.ts +35 -0
  23. package/dist/esm/client/foreground-refresh.js +83 -0
  24. package/dist/esm/client/foreground-refresh.js.map +1 -0
  25. package/dist/esm/client/idb-credential-storage.d.ts +25 -0
  26. package/dist/esm/client/idb-credential-storage.js +69 -0
  27. package/dist/esm/client/idb-credential-storage.js.map +1 -0
  28. package/dist/esm/client/index.d.ts +14 -3
  29. package/dist/esm/client/index.js +13 -3
  30. package/dist/esm/client/index.js.map +1 -1
  31. package/dist/esm/client/local-credential-storage.d.ts +26 -0
  32. package/dist/esm/client/local-credential-storage.js +59 -0
  33. package/dist/esm/client/local-credential-storage.js.map +1 -0
  34. package/dist/esm/client/memory-credential-storage.d.ts +22 -0
  35. package/dist/esm/client/memory-credential-storage.js +35 -0
  36. package/dist/esm/client/memory-credential-storage.js.map +1 -0
  37. package/dist/esm/client/session-delegation.d.ts +40 -0
  38. package/dist/esm/client/session-delegation.js +100 -0
  39. package/dist/esm/client/session-delegation.js.map +1 -0
  40. package/dist/esm/client/session-identity.d.ts +97 -0
  41. package/dist/esm/client/session-identity.js +392 -0
  42. package/dist/esm/client/session-identity.js.map +1 -0
  43. package/dist/esm/client/session-minter.d.ts +94 -0
  44. package/dist/esm/client/session-minter.js +185 -0
  45. package/dist/esm/client/session-minter.js.map +1 -0
  46. package/dist/esm/client/shared-memory-credential-storage.d.ts +44 -0
  47. package/dist/esm/client/shared-memory-credential-storage.js +243 -0
  48. package/dist/esm/client/shared-memory-credential-storage.js.map +1 -0
  49. package/dist/esm/client/slots.d.ts +59 -0
  50. package/dist/esm/client/slots.js +67 -0
  51. package/dist/esm/client/slots.js.map +1 -0
  52. package/dist/esm/client/sso.d.ts +33 -0
  53. package/dist/esm/client/sso.js +120 -0
  54. package/dist/esm/client/sso.js.map +1 -0
  55. package/dist/esm/client/state-storage.d.ts +135 -0
  56. package/dist/esm/client/state-storage.js +178 -0
  57. package/dist/esm/client/state-storage.js.map +1 -0
  58. package/package.json +2 -1
  59. package/src/client/app-delegation-source.ts +86 -0
  60. package/src/client/app-lock.ts +120 -0
  61. package/src/client/auth-client.ts +1300 -487
  62. package/src/client/base64.ts +28 -0
  63. package/src/client/cookie-state-storage.ts +277 -0
  64. package/src/client/credential-storage.ts +56 -0
  65. package/src/client/db.ts +56 -12
  66. package/src/client/foreground-refresh.ts +92 -0
  67. package/src/client/idb-credential-storage.ts +82 -0
  68. package/src/client/index.ts +24 -8
  69. package/src/client/local-credential-storage.ts +68 -0
  70. package/src/client/memory-credential-storage.ts +41 -0
  71. package/src/client/session-delegation.ts +138 -0
  72. package/src/client/session-identity.ts +510 -0
  73. package/src/client/session-minter.ts +286 -0
  74. package/src/client/shared-memory-credential-storage.ts +303 -0
  75. package/src/client/slots.ts +78 -0
  76. package/src/client/sso.ts +122 -0
  77. package/src/client/state-storage.ts +284 -0
  78. package/dist/esm/client/idle-manager.d.ts +0 -83
  79. package/dist/esm/client/idle-manager.js +0 -100
  80. package/dist/esm/client/idle-manager.js.map +0 -1
  81. package/dist/esm/client/storage.d.ts +0 -51
  82. package/dist/esm/client/storage.js +0 -90
  83. package/dist/esm/client/storage.js.map +0 -1
  84. package/src/client/idle-manager.ts +0 -162
  85. package/src/client/storage.ts +0 -114
@@ -0,0 +1,122 @@
1
+ // The identity provider's cap on the domain it will fetch.
2
+ const MAX_AUTHORITY_LENGTH = 255;
3
+
4
+ const WELL_KNOWN_PATH = '/.well-known/ii-openid-configuration';
5
+
6
+ /**
7
+ * `localhost` / `127.0.0.1`, optionally followed by `:<port>`. IPv6 loopback
8
+ * (`[::1]`, etc.) is intentionally not handled — the identity provider doesn't
9
+ * recognise it either, and its e2e setup uses the hostname form.
10
+ */
11
+ function isLoopbackHost(host: string): boolean {
12
+ let url: URL;
13
+ try {
14
+ // Parse as a URL authority so the optional `:<port>` is split off for us
15
+ // rather than by hand. Invalid input throws and is treated as non-loopback.
16
+ url = new URL(`http://${host}`);
17
+ } catch {
18
+ return false;
19
+ }
20
+ // A bare `host[:port]` has no path/query/fragment; reject e.g. `localhost/x`.
21
+ if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
22
+ return false;
23
+ }
24
+ return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
25
+ }
26
+
27
+ /**
28
+ * Normalizes an SSO domain to the authority the identity provider fetches the
29
+ * discovery document from: lowercased, IDNA-encoded, host and optional port.
30
+ *
31
+ * @param domain - The organization domain.
32
+ * @returns The normalized authority.
33
+ * @throws When `domain` is not a domain with an optional port.
34
+ */
35
+ export function normalizeSsoDomain(domain: string): string {
36
+ const trimmed = domain.trim();
37
+ if (trimmed.length === 0) {
38
+ throw new Error('ssoDomain cannot be empty');
39
+ }
40
+ let url: URL;
41
+ try {
42
+ url = new URL(`https://${trimmed}`);
43
+ } catch {
44
+ throw new Error(`ssoDomain is not a domain: ${trimmed}`);
45
+ }
46
+ const authority = url.host;
47
+ // Rebuilding from the host and port alone has to reproduce what was parsed,
48
+ // so anything else the domain carried shows up as a difference.
49
+ if (new URL(`https://${authority}`).href !== url.href) {
50
+ throw new Error(`ssoDomain must be a domain and optional port, nothing else: ${trimmed}`);
51
+ }
52
+ if (authority.length > MAX_AUTHORITY_LENGTH) {
53
+ throw new Error(`ssoDomain exceeds ${MAX_AUTHORITY_LENGTH} characters`);
54
+ }
55
+ // A bare hostname is a half-typed domain, not something worth a request.
56
+ if (!authority.includes('.') && !isLoopbackHost(authority)) {
57
+ throw new Error(`ssoDomain is not a domain name: ${trimmed}`);
58
+ }
59
+ return authority;
60
+ }
61
+
62
+ function publishesSsoConfiguration(value: unknown): boolean {
63
+ if (typeof value !== 'object' || value === null) {
64
+ return false;
65
+ }
66
+ const { client_id, openid_configuration } = value as Record<string, unknown>;
67
+ return typeof client_id === 'string' && typeof openid_configuration === 'string';
68
+ }
69
+
70
+ /**
71
+ * Checks whether an organization domain can be used for SSO sign-in: it is a
72
+ * domain name, and it publishes `/.well-known/ii-openid-configuration` with a
73
+ * `client_id` and an `openid_configuration` URL.
74
+ *
75
+ * The document must be served with `Access-Control-Allow-Origin: *`.
76
+ *
77
+ * The check waits on a third-party server and has no deadline of its own, so
78
+ * `signal` sets one. Pass `AbortSignal.timeout(...)` for a one-shot check, or a
79
+ * controller you abort per keystroke when checking as the user types.
80
+ *
81
+ * @param domain - The organization domain.
82
+ * @param signal - Bounds the check.
83
+ * @returns Whether the domain is usable for SSO sign-in.
84
+ * @throws When `signal` aborts — an abandoned check is not a verdict on the
85
+ * domain, so it must not be read as `false`.
86
+ *
87
+ * @example
88
+ * if (await isValidSsoDomain(input.value, AbortSignal.timeout(5_000))) {
89
+ * const authClient = new AuthClient({ ssoDomain: input.value });
90
+ * await authClient.signIn();
91
+ * }
92
+ */
93
+ export async function isValidSsoDomain(domain: string, signal: AbortSignal): Promise<boolean> {
94
+ signal.throwIfAborted();
95
+
96
+ let normalized: string;
97
+ try {
98
+ normalized = normalizeSsoDomain(domain);
99
+ } catch {
100
+ return false;
101
+ }
102
+
103
+ const scheme = isLoopbackHost(normalized) ? 'http' : 'https';
104
+ try {
105
+ const response = await fetch(`${scheme}://${normalized}${WELL_KNOWN_PATH}`, {
106
+ headers: { Accept: 'application/json' },
107
+ signal,
108
+ });
109
+ if (!response.ok) {
110
+ return false;
111
+ }
112
+ const valid = publishesSsoConfiguration(await response.json());
113
+ signal.throwIfAborted();
114
+ return valid;
115
+ } catch (error) {
116
+ if (signal.aborted) {
117
+ throw error;
118
+ }
119
+ // A DNS, TLS, CORS, or parse failure all leave the domain unusable.
120
+ return false;
121
+ }
122
+ }
@@ -0,0 +1,284 @@
1
+ import { Principal } from '@icp-sdk/core/principal';
2
+
3
+ /**
4
+ * What a client knows about its sign-in without holding a credential: which
5
+ * account is signed in here, and when that ends.
6
+ *
7
+ * Both fields are public. The principal is what an application's canisters see
8
+ * as the caller, and the expiration is what the identity provider granted, so
9
+ * neither is a secret and both can live somewhere a page reads synchronously.
10
+ */
11
+ export interface SessionState {
12
+ principal: Principal;
13
+
14
+ /** Nanoseconds since the epoch, matching `Delegation.expiration`. */
15
+ expiration: bigint;
16
+
17
+ /**
18
+ * Whether this origin holds a credential for that account, or only knows the
19
+ * sign-in exists.
20
+ *
21
+ * Derived on read and never published: where the record reaches further than
22
+ * one origin, the record itself cannot carry a per-origin fact. A sibling
23
+ * subdomain that has not acquired its own credential reads the sign-in with
24
+ * `held: false`, which is what tells it to acquire one.
25
+ */
26
+ held: boolean;
27
+ }
28
+
29
+ /**
30
+ * Holds the state of a sign-in.
31
+ *
32
+ * This is what decides whether this origin is signed in and as whom. It is a
33
+ * store of its own because it has to be readable without awaiting, which a
34
+ * credential store does not.
35
+ *
36
+ * A record can be stale — a session revoked at the canister still reads as
37
+ * signed in until something calls and is refused — so it is the state as this
38
+ * browser knows it rather than an authority against the network.
39
+ */
40
+ export interface StateStorage {
41
+ /** The state, or `null` where nothing is signed in within this store's reach. */
42
+ get(key: string): SessionState | null;
43
+
44
+ set(key: string, state: Omit<SessionState, 'held'>): void;
45
+
46
+ /**
47
+ * Removes the state, and anything this store publishes beyond this origin.
48
+ *
49
+ * What signing out does: the user ended the sign-in, so a sibling reading a
50
+ * shared record must stop seeing one.
51
+ */
52
+ remove(key: string): void;
53
+
54
+ /**
55
+ * Removes what this origin holds, leaving anything shared alone.
56
+ *
57
+ * What finding out does, which is a different act. An origin whose chain turns
58
+ * out to be dead cannot tell whether the session was revoked or replaced by a
59
+ * sibling signing in — and in the second case the shared record was written by
60
+ * that sibling a moment ago, so retracting it would tell it that the session it
61
+ * just obtained is gone.
62
+ *
63
+ * A store that publishes nothing beyond this origin answers it with
64
+ * {@link remove}, which is one line and the right answer.
65
+ */
66
+ discard(key: string): void;
67
+
68
+ /**
69
+ * Whether a sign-in kept in this store may be resumed without a ceremony.
70
+ *
71
+ * True only where the record reaches past this origin, because that is the
72
+ * case a silent re-issue exists for: a sibling reads the sign-in, holds no
73
+ * credential of its own, and needs the identity provider to remember the
74
+ * session it can extend. A store the origin keeps to itself has nothing to
75
+ * gain and would only be asking the provider to persist a sign-in on a device
76
+ * where nothing wanted one kept.
77
+ *
78
+ * Absent means false, which is the answer for a store that never considered
79
+ * the question.
80
+ */
81
+ readonly resumable?: boolean;
82
+
83
+ /**
84
+ * Registers a listener fired when the state under `key` changes, including
85
+ * when something other than this client changed it. Returns a function that
86
+ * unregisters it.
87
+ *
88
+ * Required rather than optional: two clients can share one store, even a
89
+ * store with no medium behind it, and a client that cannot be told the record
90
+ * changed under it goes on answering for a sign-in that is no longer there.
91
+ */
92
+ subscribe(key: string, listener: () => void): () => void;
93
+ }
94
+
95
+ /**
96
+ * State held for the lifetime of the instance and shared with nothing.
97
+ *
98
+ * For tests and for environments with no `localStorage`, where a client is
99
+ * alone and a reload starts from nothing.
100
+ * @see implements {@link StateStorage}
101
+ */
102
+ export class MemoryStateStorage implements StateStorage {
103
+ #state = new Map<string, Omit<SessionState, 'held'>>();
104
+ #subscribers = new Map<string, Set<() => void>>();
105
+
106
+ public get(key: string): SessionState | null {
107
+ // Nothing reaches past this instance, so a record here is always one it holds.
108
+ const state = this.#state.get(key);
109
+ return state === undefined ? null : { ...state, held: true };
110
+ }
111
+
112
+ public set(key: string, state: Omit<SessionState, 'held'>): void {
113
+ this.#state.set(key, state);
114
+ this.#fire(key);
115
+ }
116
+
117
+ public remove(key: string): void {
118
+ this.#state.delete(key);
119
+ this.#fire(key);
120
+ }
121
+
122
+ /** The same as {@link remove}: this store publishes nothing at all. */
123
+ public discard(key: string): void {
124
+ this.remove(key);
125
+ }
126
+
127
+ /**
128
+ * Fires when the state changes, which here means when something writes to
129
+ * this instance.
130
+ *
131
+ * Nothing outside the process can reach a `Map`, so a medium is not what makes
132
+ * this worth having: two clients can be handed the same instance, and this is
133
+ * how the one that did not write finds out. Each listener is told only when the
134
+ * record actually changed since it was last told, so a client's own writes do
135
+ * not come back to it as news.
136
+ */
137
+ public subscribe(key: string, listener: () => void): () => void {
138
+ let last = this.#raw(key);
139
+ const check = (): void => {
140
+ const now = this.#raw(key);
141
+ if (now !== last) {
142
+ last = now;
143
+ listener();
144
+ }
145
+ };
146
+ const listeners = this.#subscribers.get(key) ?? new Set();
147
+ listeners.add(check);
148
+ this.#subscribers.set(key, listeners);
149
+
150
+ return () => {
151
+ listeners.delete(check);
152
+ };
153
+ }
154
+
155
+ #fire(key: string): void {
156
+ for (const check of [...(this.#subscribers.get(key) ?? [])]) check();
157
+ }
158
+
159
+ // The same shape `LocalStateStorage` keeps, so "changed" means the same thing
160
+ // in both: a record is one string, and comparing strings needs no field walk.
161
+ #raw(key: string): string | null {
162
+ const state = this.#state.get(key);
163
+ return state === undefined
164
+ ? null
165
+ : `${state.principal.toText()}|${state.expiration.toString()}`;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * State in `localStorage`, so every tab of an origin agrees on it and it
171
+ * survives a reload.
172
+ *
173
+ * The two fields are written as `<principal>|<expiration>` rather than JSON,
174
+ * because an expiration is a `bigint` and JSON has no way to carry one.
175
+ * @see implements {@link StateStorage}
176
+ */
177
+ export class LocalStateStorage implements StateStorage {
178
+ // Fired on a same-tab write: `storage` reaches the other tabs of an origin and
179
+ // deliberately not the one that wrote. Per key, because one store answers for
180
+ // whatever key it is handed.
181
+ #subscribers = new Map<string, Set<() => void>>();
182
+
183
+ public get(key: string): SessionState | null {
184
+ const raw = this.#localStorage().getItem(key);
185
+ if (raw === null) return null;
186
+
187
+ const [principalText, expiration] = raw.split('|');
188
+ if (principalText === undefined || expiration === undefined) return null;
189
+ try {
190
+ return {
191
+ principal: Principal.fromText(principalText),
192
+ expiration: BigInt(expiration),
193
+ // This record is written by this origin and read by no other, so having
194
+ // one and holding a credential for it are the same thing.
195
+ held: true,
196
+ };
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ public set(key: string, state: Omit<SessionState, 'held'>): void {
203
+ this.#localStorage().setItem(key, `${state.principal.toText()}|${state.expiration.toString()}`);
204
+ this.#fire(key);
205
+ }
206
+
207
+ public remove(key: string): void {
208
+ this.#localStorage().removeItem(key);
209
+ this.#fire(key);
210
+ }
211
+
212
+ /** The same as {@link remove}: this store publishes nothing beyond the origin. */
213
+ public discard(key: string): void {
214
+ this.remove(key);
215
+ }
216
+
217
+ /**
218
+ * Fires when the state changes, here or in another tab of this origin.
219
+ *
220
+ * `localStorage` raises `storage` in every tab except the one that wrote, so
221
+ * that event carries a change between tabs and a same-tab write is announced
222
+ * directly. Either way the record is readable before it is announced, so a
223
+ * listener asking who is signed in sees what it was told about.
224
+ */
225
+ public subscribe(key: string, listener: () => void): () => void {
226
+ let last = this.#raw(key);
227
+ const check = (): void => {
228
+ const now = this.#raw(key);
229
+ if (now !== last) {
230
+ last = now;
231
+ listener();
232
+ }
233
+ };
234
+ const listeners = this.#subscribers.get(key) ?? new Set();
235
+ listeners.add(check);
236
+ this.#subscribers.set(key, listeners);
237
+
238
+ const onStorage = (event: StorageEvent): void => {
239
+ // `key` is null when a tab cleared the whole store, which changes this too.
240
+ if (event.key === key || event.key === null) check();
241
+ };
242
+ globalThis.addEventListener('storage', onStorage);
243
+
244
+ // `storage` is the only notice this store gets, and a page restored from the
245
+ // back-forward cache was not listening while it was frozen. So the record is
246
+ // re-read when the page is shown or its window regains focus, which is when
247
+ // somebody is about to act on the answer.
248
+ const onVisible = (): void => {
249
+ if (document.visibilityState === 'visible') check();
250
+ };
251
+ const hooked = typeof document !== 'undefined';
252
+ if (hooked) {
253
+ document.addEventListener('visibilitychange', onVisible);
254
+ globalThis.addEventListener('pageshow', check);
255
+ globalThis.addEventListener('focus', check);
256
+ }
257
+
258
+ return () => {
259
+ listeners.delete(check);
260
+ globalThis.removeEventListener('storage', onStorage);
261
+ if (hooked) {
262
+ document.removeEventListener('visibilitychange', onVisible);
263
+ globalThis.removeEventListener('pageshow', check);
264
+ globalThis.removeEventListener('focus', check);
265
+ }
266
+ };
267
+ }
268
+
269
+ #fire(key: string): void {
270
+ for (const check of [...(this.#subscribers.get(key) ?? [])]) check();
271
+ }
272
+
273
+ #raw(key: string): string | null {
274
+ return this.#localStorage().getItem(key);
275
+ }
276
+
277
+ #localStorage(): Storage {
278
+ const ls = globalThis.localStorage;
279
+ if (!ls) {
280
+ throw new Error('Could not find local storage.');
281
+ }
282
+ return ls;
283
+ }
284
+ }
@@ -1,83 +0,0 @@
1
- type IdleCB = () => unknown;
2
- export type IdleManagerOptions = {
3
- /**
4
- * Callback after the user has gone idle
5
- */
6
- onIdle?: IdleCB;
7
- /**
8
- * timeout in ms
9
- * @default 30 minutes [600_000]
10
- */
11
- idleTimeout?: number;
12
- /**
13
- * capture scroll events
14
- * @default false
15
- */
16
- captureScroll?: boolean;
17
- /**
18
- * scroll debounce time in ms
19
- * @default 100
20
- */
21
- scrollDebounce?: number;
22
- };
23
- /**
24
- * Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle` and registered callbacks.
25
- * By default, the IdleManager will log a user out after 10 minutes of inactivity.
26
- * To override these defaults, you can pass an `onIdle` callback, or configure a custom `idleTimeout` in milliseconds.
27
- *
28
- * IdleManager is a singleton: multiple calls to `create()` return the same instance,
29
- * registering any new `onIdle` callback. Call `exit()` to tear down the singleton.
30
- */
31
- export declare class IdleManager {
32
- #private;
33
- /**
34
- * Creates or returns the singleton {@link IdleManager}.
35
- * If the instance already exists, any provided `onIdle` callback is registered
36
- * on the existing instance.
37
- * @param {IdleManagerOptions} options Optional configuration
38
- * @see {@link IdleManagerOptions}
39
- * @param options.onIdle Callback once user has been idle. Use to prompt for fresh sign-in, and use `Actor.agentOf(your_actor).invalidateIdentity()` to protect the user
40
- * @param options.idleTimeout timeout in ms
41
- * @param options.captureScroll capture scroll events
42
- * @param options.scrollDebounce scroll debounce time in ms
43
- */
44
- static create(options?: {
45
- /**
46
- * Callback after the user has gone idle
47
- * @see {@link IdleCB}
48
- */
49
- onIdle?: () => unknown;
50
- /**
51
- * timeout in ms
52
- * @default 10 minutes [600_000]
53
- */
54
- idleTimeout?: number;
55
- /**
56
- * capture scroll events
57
- * @default false
58
- */
59
- captureScroll?: boolean;
60
- /**
61
- * scroll debounce time in ms
62
- * @default 100
63
- */
64
- scrollDebounce?: number;
65
- }): IdleManager;
66
- /**
67
- * @param options {@link IdleManagerOptions}
68
- */
69
- private constructor();
70
- /**
71
- * @param {IdleCB} callback function to be called when user goes idle
72
- */
73
- registerCallback(callback: IdleCB): void;
74
- /**
75
- * Tears down listeners, fires all callbacks, and clears the singleton.
76
- */
77
- exit(): void;
78
- /**
79
- * Resets the timeouts during cleanup
80
- */
81
- private _resetTimer;
82
- }
83
- export {};
@@ -1,100 +0,0 @@
1
- const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'wheel'];
2
- /**
3
- * Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle` and registered callbacks.
4
- * By default, the IdleManager will log a user out after 10 minutes of inactivity.
5
- * To override these defaults, you can pass an `onIdle` callback, or configure a custom `idleTimeout` in milliseconds.
6
- *
7
- * IdleManager is a singleton: multiple calls to `create()` return the same instance,
8
- * registering any new `onIdle` callback. Call `exit()` to tear down the singleton.
9
- */
10
- export class IdleManager {
11
- static #instance;
12
- #callbacks = [];
13
- #idleTimeout;
14
- #timeoutID = undefined;
15
- #resetTimer;
16
- /**
17
- * Creates or returns the singleton {@link IdleManager}.
18
- * If the instance already exists, any provided `onIdle` callback is registered
19
- * on the existing instance.
20
- * @param {IdleManagerOptions} options Optional configuration
21
- * @see {@link IdleManagerOptions}
22
- * @param options.onIdle Callback once user has been idle. Use to prompt for fresh sign-in, and use `Actor.agentOf(your_actor).invalidateIdentity()` to protect the user
23
- * @param options.idleTimeout timeout in ms
24
- * @param options.captureScroll capture scroll events
25
- * @param options.scrollDebounce scroll debounce time in ms
26
- */
27
- static create(options = {}) {
28
- if (IdleManager.#instance) {
29
- if (options.onIdle) {
30
- IdleManager.#instance.registerCallback(options.onIdle);
31
- }
32
- return IdleManager.#instance;
33
- }
34
- const instance = new IdleManager(options);
35
- IdleManager.#instance = instance;
36
- return instance;
37
- }
38
- /**
39
- * @param options {@link IdleManagerOptions}
40
- */
41
- constructor(options = {}) {
42
- const { onIdle, idleTimeout = 10 * 60 * 1000 } = options || {};
43
- this.#callbacks = onIdle ? [onIdle] : [];
44
- this.#idleTimeout = idleTimeout;
45
- // Store the bound function once so the same reference is used
46
- // for both addEventListener and removeEventListener.
47
- this.#resetTimer = this._resetTimer.bind(this);
48
- window.addEventListener('load', this.#resetTimer, true);
49
- events.forEach((name) => {
50
- document.addEventListener(name, this.#resetTimer, true);
51
- });
52
- const debounce = (func, wait) => {
53
- let timeout;
54
- return (...args) => {
55
- const context = this;
56
- const later = () => {
57
- timeout = undefined;
58
- func.apply(context, args);
59
- };
60
- clearTimeout(timeout);
61
- timeout = window.setTimeout(later, wait);
62
- };
63
- };
64
- if (options?.captureScroll) {
65
- // debounce scroll events
66
- const scroll = debounce(this.#resetTimer, options?.scrollDebounce ?? 100);
67
- window.addEventListener('scroll', scroll, true);
68
- }
69
- this.#resetTimer();
70
- }
71
- /**
72
- * @param {IdleCB} callback function to be called when user goes idle
73
- */
74
- registerCallback(callback) {
75
- this.#callbacks.push(callback);
76
- }
77
- /**
78
- * Tears down listeners, fires all callbacks, and clears the singleton.
79
- */
80
- exit() {
81
- clearTimeout(this.#timeoutID);
82
- window.removeEventListener('load', this.#resetTimer, true);
83
- events.forEach((name) => {
84
- document.removeEventListener(name, this.#resetTimer, true);
85
- });
86
- this.#callbacks.forEach((cb) => {
87
- cb();
88
- });
89
- IdleManager.#instance = undefined;
90
- }
91
- /**
92
- * Resets the timeouts during cleanup
93
- */
94
- _resetTimer() {
95
- const exit = this.exit.bind(this);
96
- window.clearTimeout(this.#timeoutID);
97
- this.#timeoutID = window.setTimeout(exit, this.#idleTimeout);
98
- }
99
- }
100
- //# sourceMappingURL=idle-manager.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"idle-manager.js","sourceRoot":"","sources":["../../../src/client/idle-manager.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;AAE5E;;;;;;;GAOG;AACH,MAAM,OAAO,WAAW;IACtB,MAAM,CAAC,SAAS,CAA0B;IAE1C,UAAU,GAAa,EAAE,CAAC;IAC1B,YAAY,CAAS;IACrB,UAAU,GAAY,SAAS,CAAC;IAChC,WAAW,CAAa;IAExB;;;;;;;;;;OAUG;IACI,MAAM,CAAC,MAAM,CAClB,UAqBI,EAAE;QAEN,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;gBACnB,WAAW,CAAC,SAAS,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACzD,CAAC;YACD,OAAO,WAAW,CAAC,SAAS,CAAC;QAC/B,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,SAAS,GAAG,QAAQ,CAAC;QACjC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;OAEG;IACH,YAAoB,UAA8B,EAAE;QAClD,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QAE/D,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAEhC,8DAA8D;QAC9D,qDAAqD;QACrD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE/C,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAExD,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACtB,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,CAAC,IAAkC,EAAE,IAAY,EAAE,EAAE;YACpE,IAAI,OAA2B,CAAC;YAChC,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;gBAC5B,MAAM,OAAO,GAAG,IAAI,CAAC;gBACrB,MAAM,KAAK,GAAG,GAAG,EAAE;oBACjB,OAAO,GAAG,SAAS,CAAC;oBACpB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;gBAC5B,CAAC,CAAC;gBACF,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC3C,CAAC,CAAC;QACJ,CAAC,CAAC;QAEF,IAAI,OAAO,EAAE,aAAa,EAAE,CAAC;YAC3B,yBAAyB;YACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,cAAc,IAAI,GAAG,CAAC,CAAC;YAC1E,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QAClD,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IAED;;OAEG;IACI,gBAAgB,CAAC,QAAgB;QACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC;IAED;;OAEG;IACI,IAAI;QACT,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9B,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAE3D,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACtB,QAAQ,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;YAC7B,EAAE,EAAE,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,WAAW,CAAC,SAAS,GAAG,SAAS,CAAC;IACpC,CAAC;IAED;;OAEG;IACK,WAAW;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC/D,CAAC;CACF"}
@@ -1,51 +0,0 @@
1
- import { type DBCreateOptions, IdbKeyVal } from './db.js';
2
- export declare const KEY_STORAGE_KEY = "identity";
3
- export declare const KEY_STORAGE_DELEGATION = "delegation";
4
- export declare const KEY_VECTOR = "iv";
5
- export declare const DB_VERSION = 1;
6
- export type StoredKey = string | CryptoKeyPair;
7
- /**
8
- * Interface for persisting user authentication data
9
- */
10
- export interface AuthClientStorage {
11
- get(key: string): Promise<StoredKey | null>;
12
- set(key: string, value: StoredKey): Promise<void>;
13
- remove(key: string): Promise<void>;
14
- }
15
- /**
16
- * Legacy implementation of AuthClientStorage, for use where IndexedDb is not available
17
- */
18
- export declare class LocalStorage implements AuthClientStorage {
19
- readonly prefix: string;
20
- private readonly _localStorage?;
21
- constructor(prefix?: string, _localStorage?: Storage | undefined);
22
- get(key: string): Promise<string | null>;
23
- set(key: string, value: string): Promise<void>;
24
- remove(key: string): Promise<void>;
25
- private _getLocalStorage;
26
- }
27
- /**
28
- * IdbStorage is an interface for simple storage of string key-value pairs built on {@link IdbKeyVal}
29
- *
30
- * It replaces {@link LocalStorage}
31
- * @see implements {@link AuthClientStorage}
32
- */
33
- export declare class IdbStorage implements AuthClientStorage {
34
- #private;
35
- /**
36
- * @param options - DBCreateOptions
37
- * @param options.dbName - name for the indexeddb database
38
- * @param options.storeName - name for the indexeddb Data Store
39
- * @param options.version - version of the database. Increment to safely upgrade
40
- * @example
41
- * ```ts
42
- * const storage = new IdbStorage({ dbName: 'my-db', storeName: 'my-store', version: 2 });
43
- * ```
44
- */
45
- constructor(options?: DBCreateOptions);
46
- private initializedDb;
47
- get _db(): Promise<IdbKeyVal>;
48
- get<T = string>(key: string): Promise<T | null>;
49
- set<T = string>(key: string, value: T): Promise<void>;
50
- remove(key: string): Promise<void>;
51
- }