@icp-sdk/auth 5.0.0 → 6.1.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/README.md +96 -12
- package/dist/esm/client/auth-client.d.ts +121 -125
- package/dist/esm/client/auth-client.js +376 -315
- package/dist/esm/client/auth-client.js.map +1 -1
- package/dist/esm/client/db.js +73 -73
- package/dist/esm/client/db.js.map +1 -1
- package/dist/esm/client/idle-manager.d.ts +12 -10
- package/dist/esm/client/idle-manager.js +97 -78
- package/dist/esm/client/idle-manager.js.map +1 -1
- package/dist/esm/client/index.d.ts +4 -4
- package/dist/esm/client/index.js +8 -15
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/storage.d.ts +1 -1
- package/dist/esm/client/storage.js +86 -79
- package/dist/esm/client/storage.js.map +1 -1
- package/dist/esm/index.js +3 -4
- package/dist/esm/index.js.map +1 -1
- package/package.json +7 -8
- package/src/client/auth-client.ts +431 -472
- package/src/client/db.ts +1 -1
- package/src/client/idle-manager.ts +44 -26
- package/src/client/index.ts +4 -4
- package/src/client/storage.ts +1 -1
|
@@ -1,341 +1,402 @@
|
|
|
1
|
-
import { AnonymousIdentity } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { AnonymousIdentity } from '@icp-sdk/core/agent';
|
|
2
|
+
import { DelegationChain, DelegationIdentity, ECDSAKeyIdentity, Ed25519KeyIdentity, isDelegationValid, PartialDelegationIdentity, } from '@icp-sdk/core/identity';
|
|
3
|
+
import { Signer } from '@icp-sdk/signer';
|
|
4
|
+
import { PostMessageTransport } from '@icp-sdk/signer/web';
|
|
5
|
+
import { IdleManager } from './idle-manager.js';
|
|
6
|
+
import { IdbStorage, KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY, KEY_VECTOR, LocalStorage, } from './storage.js';
|
|
7
|
+
const NANOSECONDS_PER_SECOND = BigInt(1_000_000_000);
|
|
8
|
+
const SECONDS_PER_HOUR = BigInt(3_600);
|
|
7
9
|
const NANOSECONDS_PER_HOUR = NANOSECONDS_PER_SECOND * SECONDS_PER_HOUR;
|
|
8
|
-
const IDENTITY_PROVIDER_DEFAULT =
|
|
9
|
-
const IDENTITY_PROVIDER_ENDPOINT = "#authorize";
|
|
10
|
+
const IDENTITY_PROVIDER_DEFAULT = 'https://id.ai/authorize';
|
|
10
11
|
const DEFAULT_MAX_TIME_TO_LIVE = BigInt(8) * NANOSECONDS_PER_HOUR;
|
|
11
|
-
const ECDSA_KEY_LABEL =
|
|
12
|
-
const ED25519_KEY_LABEL =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
static async create(options = {}) {
|
|
47
|
-
const storage = options.storage ?? new IdbStorage();
|
|
48
|
-
const keyType = options.keyType ?? ECDSA_KEY_LABEL;
|
|
49
|
-
let key = null;
|
|
50
|
-
if (options.identity) {
|
|
51
|
-
key = options.identity;
|
|
52
|
-
} else {
|
|
53
|
-
let maybeIdentityStorage = await storage.get(KEY_STORAGE_KEY);
|
|
54
|
-
if (!maybeIdentityStorage) {
|
|
55
|
-
try {
|
|
56
|
-
const fallbackLocalStorage = new LocalStorage();
|
|
57
|
-
const localChain = await fallbackLocalStorage.get(KEY_STORAGE_DELEGATION);
|
|
58
|
-
const localKey = await fallbackLocalStorage.get(KEY_STORAGE_KEY);
|
|
59
|
-
if (localChain && localKey && keyType === ECDSA_KEY_LABEL) {
|
|
60
|
-
console.log("Discovered an identity stored in localstorage. Migrating to IndexedDB");
|
|
61
|
-
await storage.set(KEY_STORAGE_DELEGATION, localChain);
|
|
62
|
-
await storage.set(KEY_STORAGE_KEY, localKey);
|
|
63
|
-
maybeIdentityStorage = localChain;
|
|
64
|
-
await fallbackLocalStorage.remove(KEY_STORAGE_DELEGATION);
|
|
65
|
-
await fallbackLocalStorage.remove(KEY_STORAGE_KEY);
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error(`error while attempting to recover localstorage: ${error}`);
|
|
12
|
+
const ECDSA_KEY_LABEL = 'ECDSA';
|
|
13
|
+
const ED25519_KEY_LABEL = 'Ed25519';
|
|
14
|
+
// localStorage key used to cache the delegation expiration so that
|
|
15
|
+
// isAuthenticated() can answer synchronously without hitting IndexedDB.
|
|
16
|
+
const KEY_STORAGE_EXPIRATION = 'ic-delegation_expiration';
|
|
17
|
+
export const OPENID_PROVIDER_URLS = {
|
|
18
|
+
google: 'https://accounts.google.com',
|
|
19
|
+
apple: 'https://appleid.apple.com',
|
|
20
|
+
microsoft: 'https://login.microsoftonline.com/{tid}/v2.0',
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_OPENID_SCOPE_KEYS = ['name', 'email', 'verified_email'];
|
|
23
|
+
/**
|
|
24
|
+
* Manages authentication and identity for Internet Computer web apps.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const authClient = new AuthClient();
|
|
28
|
+
*
|
|
29
|
+
* const identity = authClient.isAuthenticated()
|
|
30
|
+
* ? await authClient.getIdentity()
|
|
31
|
+
* : await authClient.signIn();
|
|
32
|
+
*/
|
|
33
|
+
export class AuthClient {
|
|
34
|
+
#identity = new AnonymousIdentity();
|
|
35
|
+
#chain = null;
|
|
36
|
+
#storage;
|
|
37
|
+
#signer;
|
|
38
|
+
#options;
|
|
39
|
+
#initPromise = null;
|
|
40
|
+
idleManager;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.#options = options;
|
|
43
|
+
this.#storage = options.storage ?? new IdbStorage();
|
|
44
|
+
const identityProviderUrl = new URL(options.identityProvider?.toString() || IDENTITY_PROVIDER_DEFAULT);
|
|
45
|
+
if (options.openIdProvider) {
|
|
46
|
+
identityProviderUrl.searchParams.set('openid', OPENID_PROVIDER_URLS[options.openIdProvider]);
|
|
69
47
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
48
|
+
const transport = new PostMessageTransport({
|
|
49
|
+
url: identityProviderUrl.toString(),
|
|
50
|
+
windowOpenerFeatures: options.windowOpenerFeatures,
|
|
51
|
+
});
|
|
52
|
+
this.#signer = new Signer({
|
|
53
|
+
transport,
|
|
54
|
+
derivationOrigin: options.derivationOrigin?.toString(),
|
|
55
|
+
});
|
|
56
|
+
this.#registerDefaultIdleCallback();
|
|
57
|
+
// Eagerly start restoring a previous session from storage.
|
|
58
|
+
// The result is awaited in getIdentity() before returning.
|
|
59
|
+
this.#init();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns the current identity, restoring a previous session if available.
|
|
63
|
+
*/
|
|
64
|
+
async getIdentity() {
|
|
65
|
+
await this.#init();
|
|
66
|
+
return this.#identity;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Checks whether the user has an active, non-expired session.
|
|
70
|
+
*/
|
|
71
|
+
isAuthenticated() {
|
|
72
|
+
// Uses a cached expiration in localStorage to avoid an async IndexedDB read.
|
|
73
|
+
const expiration = getExpirationFlag();
|
|
74
|
+
if (expiration === null)
|
|
75
|
+
return false;
|
|
76
|
+
const nowNs = BigInt(Date.now()) * BigInt(1_000_000);
|
|
77
|
+
return nowNs < expiration;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Opens the identity provider, requests a delegation, and returns the authenticated identity.
|
|
81
|
+
*
|
|
82
|
+
* @param options - Sign-in options.
|
|
83
|
+
* @param options.maxTimeToLive - Maximum lifetime of the delegation in nanoseconds.
|
|
84
|
+
* @param options.targets - Restrict the delegation to specific canisters.
|
|
85
|
+
* @returns The authenticated identity.
|
|
86
|
+
* @throws When authentication fails.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* try {
|
|
90
|
+
* const identity = await authClient.signIn();
|
|
91
|
+
* } catch (error) {
|
|
92
|
+
* console.error('Sign-in failed:', error);
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
async signIn(options) {
|
|
96
|
+
await this.#signer.openChannel();
|
|
97
|
+
const maxTimeToLive = options?.maxTimeToLive ?? DEFAULT_MAX_TIME_TO_LIVE;
|
|
98
|
+
// Fresh key per sign-in so each session has its own cryptographic identity.
|
|
99
|
+
const key = this.#options.identity ?? (await generateKey(this.#options.keyType ?? ECDSA_KEY_LABEL));
|
|
100
|
+
const delegationChain = await this.#signer.requestDelegation({
|
|
101
|
+
publicKey: key.getPublicKey(),
|
|
102
|
+
targets: options?.targets,
|
|
103
|
+
maxTimeToLive,
|
|
104
|
+
});
|
|
105
|
+
this.#chain = delegationChain;
|
|
106
|
+
// PartialIdentity only has the public key — no signing capability.
|
|
107
|
+
if ('toDer' in key) {
|
|
108
|
+
this.#identity = PartialDelegationIdentity.fromDelegation(key, this.#chain);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.#identity = DelegationIdentity.fromDelegation(key, this.#chain);
|
|
112
|
+
}
|
|
113
|
+
const idleOptions = this.#options?.idleOptions;
|
|
114
|
+
if (!this.idleManager && !idleOptions?.disableIdle) {
|
|
115
|
+
this.idleManager = IdleManager.create(idleOptions);
|
|
116
|
+
this.#registerDefaultIdleCallback();
|
|
83
117
|
}
|
|
84
|
-
|
|
118
|
+
// Persist so the session survives page reloads.
|
|
119
|
+
await persistChain(this.#storage, this.#chain);
|
|
120
|
+
await persistKey(this.#storage, key);
|
|
121
|
+
return this.#identity;
|
|
85
122
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Requests signed identity attributes from the identity provider.
|
|
125
|
+
*
|
|
126
|
+
* @param params - Request parameters.
|
|
127
|
+
* @param params.keys - Attribute keys to request (e.g. `['email', 'name']`).
|
|
128
|
+
* @param params.nonce - 32-byte nonce issued by the RP canister.
|
|
129
|
+
* @returns Signed attribute data and signature.
|
|
130
|
+
* @throws When the identity provider returns an error or an invalid response.
|
|
131
|
+
*/
|
|
132
|
+
async requestAttributes(params) {
|
|
133
|
+
const nonceBytes = params.nonce;
|
|
134
|
+
const response = await this.#signer.sendRequest({
|
|
135
|
+
jsonrpc: '2.0',
|
|
136
|
+
method: 'ii-icrc3-attributes',
|
|
137
|
+
params: { keys: params.keys, nonce: toBase64(nonceBytes) },
|
|
138
|
+
});
|
|
139
|
+
if ('error' in response) {
|
|
140
|
+
throw new Error(response.error.message);
|
|
95
141
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
const result = response.result;
|
|
143
|
+
if (typeof result?.data !== 'string' || typeof result?.signature !== 'string') {
|
|
144
|
+
throw new Error('Invalid response: missing data or signature');
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return {
|
|
148
|
+
data: fromBase64(result.data),
|
|
149
|
+
signature: fromBase64(result.signature),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (cause) {
|
|
153
|
+
throw new Error('Invalid response: data or signature is not valid base64', { cause });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Clears the stored session and resets the client to an anonymous state.
|
|
158
|
+
*
|
|
159
|
+
* @param options - Logout options.
|
|
160
|
+
* @param options.returnTo - URL to navigate to after logout.
|
|
161
|
+
*/
|
|
162
|
+
async logout(options = {}) {
|
|
163
|
+
await deleteStorage(this.#storage);
|
|
164
|
+
this.#identity = new AnonymousIdentity();
|
|
165
|
+
this.#chain = null;
|
|
166
|
+
if (options.returnTo) {
|
|
167
|
+
try {
|
|
168
|
+
window.history.pushState({}, '', options.returnTo);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
window.location.href = options.returnTo;
|
|
108
172
|
}
|
|
109
|
-
}
|
|
110
173
|
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.error(e);
|
|
113
|
-
await _deleteStorage(storage);
|
|
114
|
-
key = null;
|
|
115
|
-
}
|
|
116
174
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
// Memoized — only runs #hydrate once, returns the same promise on repeat calls.
|
|
176
|
+
#init() {
|
|
177
|
+
if (!this.#initPromise) {
|
|
178
|
+
this.#initPromise = this.#hydrate();
|
|
179
|
+
}
|
|
180
|
+
return this.#initPromise;
|
|
122
181
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
182
|
+
// Attempts to restore a previous session (key + delegation chain) from
|
|
183
|
+
// storage. If found and still valid, sets #identity and #chain so the
|
|
184
|
+
// client is ready to use without a new signIn().
|
|
185
|
+
async #hydrate() {
|
|
186
|
+
const key = this.#options.identity ??
|
|
187
|
+
(await restoreKey(this.#storage, this.#options.keyType ?? ECDSA_KEY_LABEL));
|
|
188
|
+
if (!key)
|
|
189
|
+
return;
|
|
190
|
+
const chain = await restoreChain(this.#storage);
|
|
191
|
+
if (!chain)
|
|
192
|
+
return;
|
|
193
|
+
this.#chain = chain;
|
|
194
|
+
if ('toDer' in key) {
|
|
195
|
+
this.#identity = PartialDelegationIdentity.fromDelegation(key, chain);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
this.#identity = DelegationIdentity.fromDelegation(key, chain);
|
|
199
|
+
}
|
|
200
|
+
if (!this.#options.idleOptions?.disableIdle && !this.idleManager) {
|
|
201
|
+
this.idleManager = IdleManager.create(this.#options.idleOptions);
|
|
202
|
+
this.#registerDefaultIdleCallback();
|
|
131
203
|
}
|
|
132
|
-
key = await ECDSAKeyIdentity.generate();
|
|
133
|
-
}
|
|
134
|
-
await persistKey(storage, key);
|
|
135
204
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
205
|
+
#registerDefaultIdleCallback() {
|
|
206
|
+
const idleOptions = this.#options?.idleOptions;
|
|
207
|
+
if (!idleOptions?.onIdle && !idleOptions?.disableDefaultIdleCallback) {
|
|
208
|
+
this.idleManager?.registerCallback(() => {
|
|
209
|
+
this.logout();
|
|
210
|
+
location.reload();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
145
213
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
),
|
|
155
|
-
signature: signedDelegation.signature
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
const delegationChain = DelegationChain.fromDelegations(
|
|
159
|
-
delegations,
|
|
160
|
-
message.userPublicKey
|
|
161
|
-
);
|
|
162
|
-
const key = this._key;
|
|
163
|
-
if (!key) {
|
|
164
|
-
return;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Encodes a Uint8Array to a base64 string.
|
|
217
|
+
* @param bytes - The bytes to encode.
|
|
218
|
+
*/
|
|
219
|
+
function toBase64(bytes) {
|
|
220
|
+
if ('toBase64' in bytes && typeof bytes.toBase64 === 'function') {
|
|
221
|
+
return bytes.toBase64();
|
|
165
222
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else {
|
|
170
|
-
this._identity = DelegationIdentity.fromDelegation(key, this._chain);
|
|
223
|
+
let binary = '';
|
|
224
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
225
|
+
binary += String.fromCharCode(bytes[i]);
|
|
171
226
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
return globalThis.btoa(binary);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Decodes a base64 string to a Uint8Array.
|
|
231
|
+
* @param str - The base64-encoded string.
|
|
232
|
+
*/
|
|
233
|
+
function fromBase64(str) {
|
|
234
|
+
if ('fromBase64' in Uint8Array && typeof Uint8Array.fromBase64 === 'function') {
|
|
235
|
+
return Uint8Array.fromBase64(str);
|
|
236
|
+
}
|
|
237
|
+
const binary = globalThis.atob(str);
|
|
238
|
+
const bytes = new Uint8Array(binary.length);
|
|
239
|
+
for (let i = 0; i < binary.length; i++) {
|
|
240
|
+
bytes[i] = binary.charCodeAt(i);
|
|
241
|
+
}
|
|
242
|
+
return bytes;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Generates a new session key.
|
|
246
|
+
* @param keyType - The key algorithm to use.
|
|
247
|
+
*/
|
|
248
|
+
async function generateKey(keyType) {
|
|
249
|
+
if (keyType === ED25519_KEY_LABEL) {
|
|
250
|
+
return Ed25519KeyIdentity.generate();
|
|
251
|
+
}
|
|
252
|
+
return await ECDSAKeyIdentity.generate();
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Saves a session key to storage.
|
|
256
|
+
* @param storage - The storage backend.
|
|
257
|
+
* @param key - The key to persist.
|
|
258
|
+
*/
|
|
259
|
+
async function persistKey(storage, key) {
|
|
260
|
+
await storage.set(KEY_STORAGE_KEY, serializeKey(key));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Loads a session key from storage. Falls back to migrating a legacy
|
|
264
|
+
* key from localStorage if nothing is found in the primary store.
|
|
265
|
+
* @param storage - The storage backend.
|
|
266
|
+
* @param keyType - The expected key algorithm (determines deserialization).
|
|
267
|
+
*/
|
|
268
|
+
async function restoreKey(storage, keyType) {
|
|
269
|
+
let stored = await storage.get(KEY_STORAGE_KEY);
|
|
270
|
+
if (!stored) {
|
|
271
|
+
stored = await migrateFromLocalStorage(storage, keyType);
|
|
272
|
+
}
|
|
273
|
+
if (!stored)
|
|
274
|
+
return null;
|
|
275
|
+
try {
|
|
276
|
+
// CryptoKeyPair (object) → ECDSA, JSON string → Ed25519
|
|
277
|
+
if (typeof stored === 'object') {
|
|
278
|
+
return await ECDSAKeyIdentity.fromKeyPair(stored);
|
|
279
|
+
}
|
|
280
|
+
return Ed25519KeyIdentity.fromJSON(stored);
|
|
177
281
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
282
|
+
catch {
|
|
283
|
+
// The stored value may be corrupt or from an incompatible version.
|
|
284
|
+
// Returning null lets the caller fall through to key generation,
|
|
285
|
+
// which is safer than crashing on startup.
|
|
286
|
+
return null;
|
|
182
287
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
* maxTimeToLive: BigInt (7) * BigInt(24) * BigInt(3_600_000_000_000), // 1 week
|
|
208
|
-
* windowOpenerFeatures: "toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100",
|
|
209
|
-
* onSuccess: () => {
|
|
210
|
-
* console.log('Login Successful!');
|
|
211
|
-
* },
|
|
212
|
-
* onError: (error) => {
|
|
213
|
-
* console.error('Login Failed: ', error);
|
|
214
|
-
* }
|
|
215
|
-
* });
|
|
216
|
-
*/
|
|
217
|
-
async login(options) {
|
|
218
|
-
const loginOptions = mergeLoginOptions(this._createOptions?.loginOptions, options);
|
|
219
|
-
const maxTimeToLive = loginOptions?.maxTimeToLive ?? DEFAULT_MAX_TIME_TO_LIVE;
|
|
220
|
-
const identityProviderUrl = new URL(
|
|
221
|
-
loginOptions?.identityProvider?.toString() || IDENTITY_PROVIDER_DEFAULT
|
|
222
|
-
);
|
|
223
|
-
identityProviderUrl.hash = IDENTITY_PROVIDER_ENDPOINT;
|
|
224
|
-
this._idpWindow?.close();
|
|
225
|
-
this._removeEventListener();
|
|
226
|
-
this._eventHandler = this._getEventHandler(identityProviderUrl, {
|
|
227
|
-
maxTimeToLive,
|
|
228
|
-
...loginOptions
|
|
229
|
-
});
|
|
230
|
-
window.addEventListener("message", this._eventHandler);
|
|
231
|
-
this._idpWindow = window.open(
|
|
232
|
-
identityProviderUrl.toString(),
|
|
233
|
-
"idpWindow",
|
|
234
|
-
loginOptions?.windowOpenerFeatures
|
|
235
|
-
) ?? void 0;
|
|
236
|
-
const checkInterruption = () => {
|
|
237
|
-
if (this._idpWindow) {
|
|
238
|
-
if (this._idpWindow.closed) {
|
|
239
|
-
this._handleFailure(ERROR_USER_INTERRUPT, loginOptions?.onError);
|
|
240
|
-
} else {
|
|
241
|
-
setTimeout(checkInterruption, INTERRUPT_CHECK_INTERVAL);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Converts a key into a format suitable for storage.
|
|
291
|
+
* @param key - The key to serialize.
|
|
292
|
+
*/
|
|
293
|
+
function serializeKey(key) {
|
|
294
|
+
if (key instanceof ECDSAKeyIdentity)
|
|
295
|
+
return key.getKeyPair();
|
|
296
|
+
if (key instanceof Ed25519KeyIdentity)
|
|
297
|
+
return JSON.stringify(key.toJSON());
|
|
298
|
+
throw new Error('Unsupported key type');
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Saves the delegation chain and caches its earliest expiration
|
|
302
|
+
* in localStorage so {@link AuthClient.isAuthenticated} can check it synchronously.
|
|
303
|
+
* @param storage - The storage backend.
|
|
304
|
+
* @param chain - The delegation chain to persist.
|
|
305
|
+
*/
|
|
306
|
+
async function persistChain(storage, chain) {
|
|
307
|
+
await storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(chain.toJSON()));
|
|
308
|
+
let earliest = null;
|
|
309
|
+
for (const { delegation } of chain.delegations) {
|
|
310
|
+
if (earliest === null || delegation.expiration < earliest) {
|
|
311
|
+
earliest = delegation.expiration;
|
|
242
312
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
...options?.customValues
|
|
263
|
-
};
|
|
264
|
-
this._idpWindow?.postMessage(request, identityProviderUrl.origin);
|
|
265
|
-
break;
|
|
313
|
+
}
|
|
314
|
+
if (earliest !== null) {
|
|
315
|
+
localStorage.setItem(KEY_STORAGE_EXPIRATION, earliest.toString());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Loads the delegation chain from storage. Returns `null` and wipes
|
|
320
|
+
* storage if the chain is expired or corrupted.
|
|
321
|
+
* @param storage - The storage backend.
|
|
322
|
+
*/
|
|
323
|
+
async function restoreChain(storage) {
|
|
324
|
+
try {
|
|
325
|
+
const raw = await storage.get(KEY_STORAGE_DELEGATION);
|
|
326
|
+
if (!raw || typeof raw !== 'string')
|
|
327
|
+
return null;
|
|
328
|
+
const chain = DelegationChain.fromJSON(raw);
|
|
329
|
+
if (!isDelegationValid(chain)) {
|
|
330
|
+
await deleteStorage(storage);
|
|
331
|
+
return null;
|
|
266
332
|
}
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
await this._handleSuccess(message, options?.onSuccess);
|
|
270
|
-
} catch (err) {
|
|
271
|
-
this._handleFailure(err.message, options?.onError);
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
case "authorize-client-failure":
|
|
275
|
-
this._handleFailure(message.text, options?.onError);
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
_handleFailure(errorMessage, onError) {
|
|
281
|
-
this._idpWindow?.close();
|
|
282
|
-
onError?.(errorMessage);
|
|
283
|
-
this._removeEventListener();
|
|
284
|
-
delete this._idpWindow;
|
|
285
|
-
}
|
|
286
|
-
_removeEventListener() {
|
|
287
|
-
if (this._eventHandler) {
|
|
288
|
-
window.removeEventListener("message", this._eventHandler);
|
|
333
|
+
return chain;
|
|
289
334
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
this._identity = new AnonymousIdentity();
|
|
295
|
-
this._chain = null;
|
|
296
|
-
if (options.returnTo) {
|
|
297
|
-
try {
|
|
298
|
-
window.history.pushState({}, "", options.returnTo);
|
|
299
|
-
} catch {
|
|
300
|
-
window.location.href = options.returnTo;
|
|
301
|
-
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
console.error(e);
|
|
337
|
+
await deleteStorage(storage);
|
|
338
|
+
return null;
|
|
302
339
|
}
|
|
303
|
-
}
|
|
304
340
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Clears all session data from storage.
|
|
343
|
+
* @param storage - The storage backend.
|
|
344
|
+
*/
|
|
345
|
+
async function deleteStorage(storage) {
|
|
346
|
+
await storage.remove(KEY_STORAGE_KEY);
|
|
347
|
+
await storage.remove(KEY_STORAGE_DELEGATION);
|
|
348
|
+
await storage.remove(KEY_VECTOR);
|
|
349
|
+
localStorage.removeItem(KEY_STORAGE_EXPIRATION);
|
|
309
350
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
...otherLoginOptions?.customValues
|
|
317
|
-
} : void 0;
|
|
318
|
-
return {
|
|
319
|
-
...loginOptions,
|
|
320
|
-
...otherLoginOptions,
|
|
321
|
-
customValues
|
|
322
|
-
};
|
|
351
|
+
/** Reads the cached delegation expiration from localStorage (nanoseconds). */
|
|
352
|
+
function getExpirationFlag() {
|
|
353
|
+
const value = localStorage.getItem(KEY_STORAGE_EXPIRATION);
|
|
354
|
+
if (value === null)
|
|
355
|
+
return null;
|
|
356
|
+
return BigInt(value);
|
|
323
357
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
358
|
+
/**
|
|
359
|
+
* One-time migration: moves a legacy session stored in localStorage
|
|
360
|
+
* into the primary storage, then cleans up the old entries.
|
|
361
|
+
* @param storage - The target storage backend.
|
|
362
|
+
* @param keyType - The expected key algorithm (only ECDSA keys are migrated).
|
|
363
|
+
*/
|
|
364
|
+
async function migrateFromLocalStorage(storage, keyType) {
|
|
365
|
+
try {
|
|
366
|
+
const fallback = new LocalStorage();
|
|
367
|
+
const localChain = await fallback.get(KEY_STORAGE_DELEGATION);
|
|
368
|
+
const localKey = await fallback.get(KEY_STORAGE_KEY);
|
|
369
|
+
if (!localChain || !localKey || keyType !== ECDSA_KEY_LABEL)
|
|
370
|
+
return null;
|
|
371
|
+
console.log('Discovered an identity stored in localstorage. Migrating to IndexedDB');
|
|
372
|
+
await storage.set(KEY_STORAGE_DELEGATION, localChain);
|
|
373
|
+
await storage.set(KEY_STORAGE_KEY, localKey);
|
|
374
|
+
await fallback.remove(KEY_STORAGE_DELEGATION);
|
|
375
|
+
await fallback.remove(KEY_STORAGE_KEY);
|
|
376
|
+
return localKey;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
console.error(`error while attempting to recover localstorage: ${error}`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
332
382
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Scopes attribute keys to an OpenID provider.
|
|
385
|
+
*
|
|
386
|
+
* When using one-click sign-in, attributes can be scoped to the same provider
|
|
387
|
+
* so the user grants access in a single step without an additional prompt.
|
|
388
|
+
*
|
|
389
|
+
* @param params.openIdProvider - The OpenID provider the keys should be scoped to.
|
|
390
|
+
* @param params.keys - The attribute keys to scope. Defaults to `['name', 'email', 'verified_email']`.
|
|
391
|
+
* @returns The scoped attribute keys as `openid:<provider-url>:<key>`.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* scopedKeys({ openIdProvider: 'google', keys: ['email'] });
|
|
395
|
+
* // ['openid:https://accounts.google.com:email']
|
|
396
|
+
*/
|
|
397
|
+
export function scopedKeys(params) {
|
|
398
|
+
const provider = OPENID_PROVIDER_URLS[params.openIdProvider];
|
|
399
|
+
const keys = params.keys ?? DEFAULT_OPENID_SCOPE_KEYS;
|
|
400
|
+
return keys.map((key) => `openid:${provider}:${key}`);
|
|
336
401
|
}
|
|
337
|
-
|
|
338
|
-
AuthClient,
|
|
339
|
-
ERROR_USER_INTERRUPT
|
|
340
|
-
};
|
|
341
|
-
//# sourceMappingURL=auth-client.js.map
|
|
402
|
+
//# sourceMappingURL=auth-client.js.map
|