@icp-sdk/auth 8.0.2 → 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.
- package/dist/esm/client/app-delegation-source.d.ts +65 -0
- package/dist/esm/client/app-delegation-source.js +58 -0
- package/dist/esm/client/app-delegation-source.js.map +1 -0
- package/dist/esm/client/app-lock.d.ts +63 -0
- package/dist/esm/client/app-lock.js +92 -0
- package/dist/esm/client/app-lock.js.map +1 -0
- package/dist/esm/client/auth-client.d.ts +270 -53
- package/dist/esm/client/auth-client.js +1017 -414
- package/dist/esm/client/auth-client.js.map +1 -1
- package/dist/esm/client/base64.d.ts +8 -0
- package/dist/esm/client/base64.js +28 -0
- package/dist/esm/client/base64.js.map +1 -0
- package/dist/esm/client/cookie-state-storage.d.ts +59 -0
- package/dist/esm/client/cookie-state-storage.js +245 -0
- package/dist/esm/client/cookie-state-storage.js.map +1 -0
- package/dist/esm/client/credential-storage.d.ts +48 -0
- package/dist/esm/client/credential-storage.js +2 -0
- package/dist/esm/client/credential-storage.js.map +1 -0
- package/dist/esm/client/db.d.ts +6 -1
- package/dist/esm/client/db.js +56 -15
- package/dist/esm/client/db.js.map +1 -1
- package/dist/esm/client/foreground-refresh.d.ts +35 -0
- package/dist/esm/client/foreground-refresh.js +83 -0
- package/dist/esm/client/foreground-refresh.js.map +1 -0
- package/dist/esm/client/idb-credential-storage.d.ts +25 -0
- package/dist/esm/client/idb-credential-storage.js +69 -0
- package/dist/esm/client/idb-credential-storage.js.map +1 -0
- package/dist/esm/client/index.d.ts +14 -3
- package/dist/esm/client/index.js +13 -3
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/local-credential-storage.d.ts +26 -0
- package/dist/esm/client/local-credential-storage.js +59 -0
- package/dist/esm/client/local-credential-storage.js.map +1 -0
- package/dist/esm/client/memory-credential-storage.d.ts +22 -0
- package/dist/esm/client/memory-credential-storage.js +35 -0
- package/dist/esm/client/memory-credential-storage.js.map +1 -0
- package/dist/esm/client/session-delegation.d.ts +40 -0
- package/dist/esm/client/session-delegation.js +100 -0
- package/dist/esm/client/session-delegation.js.map +1 -0
- package/dist/esm/client/session-identity.d.ts +97 -0
- package/dist/esm/client/session-identity.js +392 -0
- package/dist/esm/client/session-identity.js.map +1 -0
- package/dist/esm/client/session-minter.d.ts +94 -0
- package/dist/esm/client/session-minter.js +185 -0
- package/dist/esm/client/session-minter.js.map +1 -0
- package/dist/esm/client/shared-memory-credential-storage.d.ts +44 -0
- package/dist/esm/client/shared-memory-credential-storage.js +243 -0
- package/dist/esm/client/shared-memory-credential-storage.js.map +1 -0
- package/dist/esm/client/slots.d.ts +59 -0
- package/dist/esm/client/slots.js +67 -0
- package/dist/esm/client/slots.js.map +1 -0
- package/dist/esm/client/sso.d.ts +33 -0
- package/dist/esm/client/sso.js +120 -0
- package/dist/esm/client/sso.js.map +1 -0
- package/dist/esm/client/state-storage.d.ts +135 -0
- package/dist/esm/client/state-storage.js +178 -0
- package/dist/esm/client/state-storage.js.map +1 -0
- package/package.json +3 -2
- package/src/client/app-delegation-source.ts +86 -0
- package/src/client/app-lock.ts +120 -0
- package/src/client/auth-client.ts +1300 -487
- package/src/client/base64.ts +28 -0
- package/src/client/cookie-state-storage.ts +277 -0
- package/src/client/credential-storage.ts +56 -0
- package/src/client/db.ts +56 -12
- package/src/client/foreground-refresh.ts +92 -0
- package/src/client/idb-credential-storage.ts +82 -0
- package/src/client/index.ts +24 -8
- package/src/client/local-credential-storage.ts +68 -0
- package/src/client/memory-credential-storage.ts +41 -0
- package/src/client/session-delegation.ts +138 -0
- package/src/client/session-identity.ts +510 -0
- package/src/client/session-minter.ts +286 -0
- package/src/client/shared-memory-credential-storage.ts +303 -0
- package/src/client/slots.ts +78 -0
- package/src/client/sso.ts +122 -0
- package/src/client/state-storage.ts +284 -0
- package/dist/esm/client/idle-manager.d.ts +0 -83
- package/dist/esm/client/idle-manager.js +0 -100
- package/dist/esm/client/idle-manager.js.map +0 -1
- package/dist/esm/client/storage.d.ts +0 -51
- package/dist/esm/client/storage.js +0 -90
- package/dist/esm/client/storage.js.map +0 -1
- package/src/client/idle-manager.ts +0 -162
- 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":"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"}
|
|
@@ -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
|
-
}
|