@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.
@@ -1,341 +1,402 @@
1
- import { AnonymousIdentity } from "@icp-sdk/core/agent";
2
- import { Ed25519KeyIdentity, ECDSAKeyIdentity, DelegationChain, isDelegationValid, PartialDelegationIdentity, DelegationIdentity, Delegation } from "@icp-sdk/core/identity";
3
- import { IdleManager } from "./idle-manager.js";
4
- import { IdbStorage, KEY_STORAGE_KEY, LocalStorage, KEY_STORAGE_DELEGATION, KEY_VECTOR } from "./storage.js";
5
- const NANOSECONDS_PER_SECOND = BigInt(1e9);
6
- const SECONDS_PER_HOUR = BigInt(3600);
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 = "https://identity.internetcomputer.org";
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 = "ECDSA";
12
- const ED25519_KEY_LABEL = "Ed25519";
13
- const INTERRUPT_CHECK_INTERVAL = 500;
14
- const ERROR_USER_INTERRUPT = "UserInterrupt";
15
- class AuthClient {
16
- constructor(_identity, _key, _chain, _storage, idleManager, _createOptions, _idpWindow, _eventHandler) {
17
- this._identity = _identity;
18
- this._key = _key;
19
- this._chain = _chain;
20
- this._storage = _storage;
21
- this.idleManager = idleManager;
22
- this._createOptions = _createOptions;
23
- this._idpWindow = _idpWindow;
24
- this._eventHandler = _eventHandler;
25
- this._registerDefaultIdleCallback();
26
- }
27
- /**
28
- * Create an AuthClient to manage authentication and identity
29
- * @param {AuthClientCreateOptions} options - Options for creating an {@link AuthClient}
30
- * @see {@link AuthClientCreateOptions}
31
- * @param options.identity Optional Identity to use as the base
32
- * @see {@link SignIdentity}
33
- * @param options.storage Storage mechanism for delegation credentials
34
- * @see {@link AuthClientStorage}
35
- * @param options.keyType Type of key to use for the base key
36
- * @param {IdleOptions} options.idleOptions Configures an {@link IdleManager}
37
- * @see {@link IdleOptions}
38
- * Default behavior is to clear stored identity and reload the page when a user goes idle, unless you set the disableDefaultIdleCallback flag or pass in a custom idle callback.
39
- * @example
40
- * const authClient = await AuthClient.create({
41
- * idleOptions: {
42
- * disableIdle: true
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
- if (maybeIdentityStorage) {
72
- try {
73
- if (typeof maybeIdentityStorage === "object") {
74
- if (keyType === ED25519_KEY_LABEL && typeof maybeIdentityStorage === "string") {
75
- key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage);
76
- } else {
77
- key = await ECDSAKeyIdentity.fromKeyPair(maybeIdentityStorage);
78
- }
79
- } else if (typeof maybeIdentityStorage === "string") {
80
- key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage);
81
- }
82
- } catch {
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
- let identity = new AnonymousIdentity();
87
- let chain = null;
88
- if (key) {
89
- try {
90
- const chainStorage = await storage.get(KEY_STORAGE_DELEGATION);
91
- if (typeof chainStorage === "object" && chainStorage !== null) {
92
- throw new Error(
93
- "Delegation chain is incorrectly stored. A delegation chain should be stored as a string."
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
- if (options.identity) {
97
- identity = options.identity;
98
- } else if (chainStorage) {
99
- chain = DelegationChain.fromJSON(chainStorage);
100
- if (!isDelegationValid(chain)) {
101
- await _deleteStorage(storage);
102
- key = null;
103
- } else {
104
- if ("toDer" in key) {
105
- identity = PartialDelegationIdentity.fromDelegation(key, chain);
106
- } else {
107
- identity = DelegationIdentity.fromDelegation(key, chain);
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
- let idleManager;
118
- if (options.idleOptions?.disableIdle) {
119
- idleManager = void 0;
120
- } else if (chain || options.identity) {
121
- idleManager = IdleManager.create(options.idleOptions);
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
- if (!key) {
124
- if (keyType === ED25519_KEY_LABEL) {
125
- key = Ed25519KeyIdentity.generate();
126
- } else {
127
- if (options.storage && keyType === ECDSA_KEY_LABEL) {
128
- console.warn(
129
- `You are using a custom storage provider that may not support CryptoKey storage. If you are using a custom storage provider that does not support CryptoKey storage, you should use '${ED25519_KEY_LABEL}' as the key type, as it can serialize to a string`
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
- return new AuthClient(identity, key, chain, storage, idleManager, options);
137
- }
138
- _registerDefaultIdleCallback() {
139
- const idleOptions = this._createOptions?.idleOptions;
140
- if (!idleOptions?.onIdle && !idleOptions?.disableDefaultIdleCallback) {
141
- this.idleManager?.registerCallback(() => {
142
- this.logout();
143
- location.reload();
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
- async _handleSuccess(message, onSuccess) {
148
- const delegations = message.delegations.map((signedDelegation) => {
149
- return {
150
- delegation: new Delegation(
151
- signedDelegation.delegation.pubkey,
152
- signedDelegation.delegation.expiration,
153
- signedDelegation.delegation.targets
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
- this._chain = delegationChain;
167
- if ("toDer" in key) {
168
- this._identity = PartialDelegationIdentity.fromDelegation(key, this._chain);
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
- this._idpWindow?.close();
173
- const idleOptions = this._createOptions?.idleOptions;
174
- if (!this.idleManager && !idleOptions?.disableIdle) {
175
- this.idleManager = IdleManager.create(idleOptions);
176
- this._registerDefaultIdleCallback();
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
- this._removeEventListener();
179
- delete this._idpWindow;
180
- if (this._chain) {
181
- await this._storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(this._chain.toJSON()));
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
- await persistKey(this._storage, this._key);
184
- onSuccess?.(message);
185
- }
186
- getIdentity() {
187
- return this._identity;
188
- }
189
- async isAuthenticated() {
190
- return !this.getIdentity().getPrincipal().isAnonymous() && this._chain !== null && isDelegationValid(this._chain);
191
- }
192
- /**
193
- * AuthClient Login - Opens up a new window to authenticate with Internet Identity
194
- * @param {AuthClientLoginOptions} options - Options for logging in, merged with the options set during creation if any. Note: we only perform a shallow merge for the `customValues` property.
195
- * @param options.identityProvider Identity provider
196
- * @param options.maxTimeToLive Expiration of the authentication in nanoseconds
197
- * @param options.allowPinAuthentication If present, indicates whether or not the Identity Provider should allow the user to authenticate and/or register using a temporary key/PIN identity. Authenticating dapps may want to prevent users from using Temporary keys/PIN identities because Temporary keys/PIN identities are less secure than Passkeys (webauthn credentials) and because Temporary keys/PIN identities generally only live in a browser database (which may get cleared by the browser/OS).
198
- * @param options.derivationOrigin Origin for Identity Provider to use while generating the delegated identity
199
- * @param options.windowOpenerFeatures Configures the opened authentication window
200
- * @param options.onSuccess Callback once login has completed
201
- * @param options.onError Callback in case authentication fails
202
- * @param options.customValues Extra values to be passed in the login request during the authorize-ready phase. Note: we only perform a shallow merge for the `customValues` property.
203
- * @example
204
- * const authClient = await AuthClient.create();
205
- * authClient.login({
206
- * identityProvider: 'http://<canisterID>.127.0.0.1:8000',
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
- checkInterruption();
246
- }
247
- _getEventHandler(identityProviderUrl, options) {
248
- return async (event) => {
249
- if (event.origin !== identityProviderUrl.origin) {
250
- return;
251
- }
252
- const message = event.data;
253
- switch (message.kind) {
254
- case "authorize-ready": {
255
- const request = {
256
- kind: "authorize-client",
257
- sessionPublicKey: new Uint8Array(this._key?.getPublicKey().toDer()),
258
- maxTimeToLive: options?.maxTimeToLive,
259
- allowPinAuthentication: options?.allowPinAuthentication,
260
- derivationOrigin: options?.derivationOrigin?.toString(),
261
- // Pass any custom values to the IDP.
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
- case "authorize-client-success":
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
- this._eventHandler = void 0;
291
- }
292
- async logout(options = {}) {
293
- await _deleteStorage(this._storage);
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
- async function _deleteStorage(storage) {
306
- await storage.remove(KEY_STORAGE_KEY);
307
- await storage.remove(KEY_STORAGE_DELEGATION);
308
- await storage.remove(KEY_VECTOR);
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
- function mergeLoginOptions(loginOptions, otherLoginOptions) {
311
- if (!loginOptions && !otherLoginOptions) {
312
- return void 0;
313
- }
314
- const customValues = loginOptions?.customValues || otherLoginOptions?.customValues ? {
315
- ...loginOptions?.customValues,
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
- function toStoredKey(key) {
325
- if (key instanceof ECDSAKeyIdentity) {
326
- return key.getKeyPair();
327
- }
328
- if (key instanceof Ed25519KeyIdentity) {
329
- return JSON.stringify(key.toJSON());
330
- }
331
- throw new Error("Unsupported key type");
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
- async function persistKey(storage, key) {
334
- const serialized = toStoredKey(key);
335
- await storage.set(KEY_STORAGE_KEY, serialized);
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
- export {
338
- AuthClient,
339
- ERROR_USER_INTERRUPT
340
- };
341
- //# sourceMappingURL=auth-client.js.map
402
+ //# sourceMappingURL=auth-client.js.map