@icp-sdk/auth 5.0.0-beta.2 → 6.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.
@@ -1,341 +1,336 @@
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
+ 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
+ /**
23
+ * Manages authentication and identity for Internet Computer web apps.
24
+ *
25
+ * @example
26
+ * const authClient = new AuthClient();
27
+ *
28
+ * if (authClient.isAuthenticated()) {
29
+ * const identity = await authClient.getIdentity();
30
+ * }
31
+ *
32
+ * await authClient.login({
33
+ * onSuccess: () => console.log('Logged in!'),
34
+ * });
35
+ */
36
+ export class AuthClient {
37
+ #identity = new AnonymousIdentity();
38
+ #chain = null;
39
+ #storage;
40
+ #signer;
41
+ #options;
42
+ #initPromise = null;
43
+ idleManager;
44
+ constructor(options = {}) {
45
+ this.#options = options;
46
+ this.#storage = options.storage ?? new IdbStorage();
47
+ const identityProviderUrl = new URL(options.identityProvider?.toString() || IDENTITY_PROVIDER_DEFAULT);
48
+ if (options.openIdProvider) {
49
+ identityProviderUrl.searchParams.set('openid', OPENID_PROVIDER_URLS[options.openIdProvider]);
69
50
  }
70
- }
71
- if (maybeIdentityStorage) {
51
+ const transport = new PostMessageTransport({
52
+ url: identityProviderUrl.toString(),
53
+ windowOpenerFeatures: options.windowOpenerFeatures,
54
+ });
55
+ this.#signer = new Signer({
56
+ transport,
57
+ derivationOrigin: options.derivationOrigin?.toString(),
58
+ });
59
+ this.#registerDefaultIdleCallback();
60
+ // Eagerly start restoring a previous session from storage.
61
+ // The result is awaited in getIdentity() before returning.
62
+ this.#init();
63
+ }
64
+ /**
65
+ * Returns the current identity, restoring a previous session if available.
66
+ */
67
+ async getIdentity() {
68
+ await this.#init();
69
+ return this.#identity;
70
+ }
71
+ /**
72
+ * Checks whether the user has an active, non-expired session.
73
+ */
74
+ isAuthenticated() {
75
+ // Uses a cached expiration in localStorage to avoid an async IndexedDB read.
76
+ const expiration = getExpirationFlag();
77
+ if (expiration === null)
78
+ return false;
79
+ const nowNs = BigInt(Date.now()) * BigInt(1_000_000);
80
+ return nowNs < expiration;
81
+ }
82
+ /**
83
+ * Opens the identity provider and requests a delegation.
84
+ *
85
+ * @param options - Login options.
86
+ * @param options.maxTimeToLive - Maximum lifetime of the delegation in nanoseconds.
87
+ * @param options.targets - Restrict the delegation to specific canisters.
88
+ * @param options.onSuccess - Called after a successful login.
89
+ * @param options.onError - Called when login fails. When provided the error is not re-thrown.
90
+ * @throws When authentication fails and no `onError` callback is provided.
91
+ *
92
+ * @example
93
+ * await authClient.login({
94
+ * onSuccess: () => console.log('Logged in!'),
95
+ * onError: (err) => console.error(err),
96
+ * });
97
+ */
98
+ async login(options) {
72
99
  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);
100
+ await this.#signer.openChannel();
101
+ const maxTimeToLive = options?.maxTimeToLive ?? DEFAULT_MAX_TIME_TO_LIVE;
102
+ // Fresh key per login so each session has its own cryptographic identity.
103
+ const key = this.#options.identity ?? (await generateKey(this.#options.keyType ?? ECDSA_KEY_LABEL));
104
+ const delegationChain = await this.#signer.requestDelegation({
105
+ publicKey: key.getPublicKey(),
106
+ targets: options?.targets,
107
+ maxTimeToLive,
108
+ });
109
+ this.#chain = delegationChain;
110
+ // PartialIdentity only has the public key — no signing capability.
111
+ if ('toDer' in key) {
112
+ this.#identity = PartialDelegationIdentity.fromDelegation(key, this.#chain);
78
113
  }
79
- } else if (typeof maybeIdentityStorage === "string") {
80
- key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage);
81
- }
82
- } catch {
114
+ else {
115
+ this.#identity = DelegationIdentity.fromDelegation(key, this.#chain);
116
+ }
117
+ const idleOptions = this.#options?.idleOptions;
118
+ if (!this.idleManager && !idleOptions?.disableIdle) {
119
+ this.idleManager = IdleManager.create(idleOptions);
120
+ this.#registerDefaultIdleCallback();
121
+ }
122
+ // Persist so the session survives page reloads.
123
+ await persistChain(this.#storage, this.#chain);
124
+ await persistKey(this.#storage, key);
125
+ await options?.onSuccess?.();
83
126
  }
84
- }
85
- }
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
- );
127
+ catch (error) {
128
+ // If an onError callback is provided, delegate error handling to the caller.
129
+ // Otherwise, re-throw so the error can be caught with try/catch or .catch().
130
+ if (options?.onError) {
131
+ await options.onError(error instanceof Error ? error.message : String(error));
132
+ }
133
+ else {
134
+ throw error;
135
+ }
95
136
  }
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);
137
+ }
138
+ /**
139
+ * Clears the stored session and resets the client to an anonymous state.
140
+ *
141
+ * @param options - Logout options.
142
+ * @param options.returnTo - URL to navigate to after logout.
143
+ */
144
+ async logout(options = {}) {
145
+ await deleteStorage(this.#storage);
146
+ this.#identity = new AnonymousIdentity();
147
+ this.#chain = null;
148
+ if (options.returnTo) {
149
+ try {
150
+ window.history.pushState({}, '', options.returnTo);
151
+ }
152
+ catch {
153
+ window.location.href = options.returnTo;
108
154
  }
109
- }
110
155
  }
111
- } catch (e) {
112
- console.error(e);
113
- await _deleteStorage(storage);
114
- key = null;
115
- }
116
156
  }
117
- let idleManager;
118
- if (options.idleOptions?.disableIdle) {
119
- idleManager = void 0;
120
- } else if (chain || options.identity) {
121
- idleManager = IdleManager.create(options.idleOptions);
157
+ // Memoized — only runs #hydrate once, returns the same promise on repeat calls.
158
+ #init() {
159
+ if (!this.#initPromise) {
160
+ this.#initPromise = this.#hydrate();
161
+ }
162
+ return this.#initPromise;
122
163
  }
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
- );
164
+ // Attempts to restore a previous session (key + delegation chain) from
165
+ // storage. If found and still valid, sets #identity and #chain so the
166
+ // client is ready to use without a new login().
167
+ async #hydrate() {
168
+ const key = this.#options.identity ??
169
+ (await restoreKey(this.#storage, this.#options.keyType ?? ECDSA_KEY_LABEL));
170
+ if (!key)
171
+ return;
172
+ const chain = await restoreChain(this.#storage);
173
+ if (!chain)
174
+ return;
175
+ this.#chain = chain;
176
+ if ('toDer' in key) {
177
+ this.#identity = PartialDelegationIdentity.fromDelegation(key, chain);
178
+ }
179
+ else {
180
+ this.#identity = DelegationIdentity.fromDelegation(key, chain);
181
+ }
182
+ if (!this.#options.idleOptions?.disableIdle && !this.idleManager) {
183
+ this.idleManager = IdleManager.create(this.#options.idleOptions);
184
+ this.#registerDefaultIdleCallback();
131
185
  }
132
- key = await ECDSAKeyIdentity.generate();
133
- }
134
- await persistKey(storage, key);
135
186
  }
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
- });
187
+ #registerDefaultIdleCallback() {
188
+ const idleOptions = this.#options?.idleOptions;
189
+ if (!idleOptions?.onIdle && !idleOptions?.disableDefaultIdleCallback) {
190
+ this.idleManager?.registerCallback(() => {
191
+ this.logout();
192
+ location.reload();
193
+ });
194
+ }
145
195
  }
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;
196
+ }
197
+ /**
198
+ * Generates a new session key.
199
+ * @param keyType - The key algorithm to use.
200
+ */
201
+ async function generateKey(keyType) {
202
+ if (keyType === ED25519_KEY_LABEL) {
203
+ return Ed25519KeyIdentity.generate();
165
204
  }
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);
205
+ return await ECDSAKeyIdentity.generate();
206
+ }
207
+ /**
208
+ * Saves a session key to storage.
209
+ * @param storage - The storage backend.
210
+ * @param key - The key to persist.
211
+ */
212
+ async function persistKey(storage, key) {
213
+ await storage.set(KEY_STORAGE_KEY, serializeKey(key));
214
+ }
215
+ /**
216
+ * Loads a session key from storage. Falls back to migrating a legacy
217
+ * key from localStorage if nothing is found in the primary store.
218
+ * @param storage - The storage backend.
219
+ * @param keyType - The expected key algorithm (determines deserialization).
220
+ */
221
+ async function restoreKey(storage, keyType) {
222
+ let stored = await storage.get(KEY_STORAGE_KEY);
223
+ if (!stored) {
224
+ stored = await migrateFromLocalStorage(storage, keyType);
171
225
  }
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();
226
+ if (!stored)
227
+ return null;
228
+ try {
229
+ // CryptoKeyPair (object) → ECDSA, JSON string → Ed25519
230
+ if (typeof stored === 'object') {
231
+ return await ECDSAKeyIdentity.fromKeyPair(stored);
232
+ }
233
+ return Ed25519KeyIdentity.fromJSON(stored);
177
234
  }
178
- this._removeEventListener();
179
- delete this._idpWindow;
180
- if (this._chain) {
181
- await this._storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(this._chain.toJSON()));
235
+ catch {
236
+ // The stored value may be corrupt or from an incompatible version.
237
+ // Returning null lets the caller fall through to key generation,
238
+ // which is safer than crashing on startup.
239
+ return null;
182
240
  }
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);
242
- }
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;
241
+ }
242
+ /**
243
+ * Converts a key into a format suitable for storage.
244
+ * @param key - The key to serialize.
245
+ */
246
+ function serializeKey(key) {
247
+ if (key instanceof ECDSAKeyIdentity)
248
+ return key.getKeyPair();
249
+ if (key instanceof Ed25519KeyIdentity)
250
+ return JSON.stringify(key.toJSON());
251
+ throw new Error('Unsupported key type');
252
+ }
253
+ /**
254
+ * Saves the delegation chain and caches its earliest expiration
255
+ * in localStorage so {@link AuthClient.isAuthenticated} can check it synchronously.
256
+ * @param storage - The storage backend.
257
+ * @param chain - The delegation chain to persist.
258
+ */
259
+ async function persistChain(storage, chain) {
260
+ await storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(chain.toJSON()));
261
+ let earliest = null;
262
+ for (const { delegation } of chain.delegations) {
263
+ if (earliest === null || delegation.expiration < earliest) {
264
+ earliest = delegation.expiration;
266
265
  }
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);
289
266
  }
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
- }
267
+ if (earliest !== null) {
268
+ localStorage.setItem(KEY_STORAGE_EXPIRATION, earliest.toString());
302
269
  }
303
- }
304
270
  }
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);
271
+ /**
272
+ * Loads the delegation chain from storage. Returns `null` and wipes
273
+ * storage if the chain is expired or corrupted.
274
+ * @param storage - The storage backend.
275
+ */
276
+ async function restoreChain(storage) {
277
+ try {
278
+ const raw = await storage.get(KEY_STORAGE_DELEGATION);
279
+ if (!raw || typeof raw !== 'string')
280
+ return null;
281
+ const chain = DelegationChain.fromJSON(raw);
282
+ if (!isDelegationValid(chain)) {
283
+ await deleteStorage(storage);
284
+ return null;
285
+ }
286
+ return chain;
287
+ }
288
+ catch (e) {
289
+ console.error(e);
290
+ await deleteStorage(storage);
291
+ return null;
292
+ }
309
293
  }
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
- };
294
+ /**
295
+ * Clears all session data from storage.
296
+ * @param storage - The storage backend.
297
+ */
298
+ async function deleteStorage(storage) {
299
+ await storage.remove(KEY_STORAGE_KEY);
300
+ await storage.remove(KEY_STORAGE_DELEGATION);
301
+ await storage.remove(KEY_VECTOR);
302
+ localStorage.removeItem(KEY_STORAGE_EXPIRATION);
323
303
  }
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");
304
+ /** Reads the cached delegation expiration from localStorage (nanoseconds). */
305
+ function getExpirationFlag() {
306
+ const value = localStorage.getItem(KEY_STORAGE_EXPIRATION);
307
+ if (value === null)
308
+ return null;
309
+ return BigInt(value);
332
310
  }
333
- async function persistKey(storage, key) {
334
- const serialized = toStoredKey(key);
335
- await storage.set(KEY_STORAGE_KEY, serialized);
311
+ /**
312
+ * One-time migration: moves a legacy session stored in localStorage
313
+ * into the primary storage, then cleans up the old entries.
314
+ * @param storage - The target storage backend.
315
+ * @param keyType - The expected key algorithm (only ECDSA keys are migrated).
316
+ */
317
+ async function migrateFromLocalStorage(storage, keyType) {
318
+ try {
319
+ const fallback = new LocalStorage();
320
+ const localChain = await fallback.get(KEY_STORAGE_DELEGATION);
321
+ const localKey = await fallback.get(KEY_STORAGE_KEY);
322
+ if (!localChain || !localKey || keyType !== ECDSA_KEY_LABEL)
323
+ return null;
324
+ console.log('Discovered an identity stored in localstorage. Migrating to IndexedDB');
325
+ await storage.set(KEY_STORAGE_DELEGATION, localChain);
326
+ await storage.set(KEY_STORAGE_KEY, localKey);
327
+ await fallback.remove(KEY_STORAGE_DELEGATION);
328
+ await fallback.remove(KEY_STORAGE_KEY);
329
+ return localKey;
330
+ }
331
+ catch (error) {
332
+ console.error(`error while attempting to recover localstorage: ${error}`);
333
+ return null;
334
+ }
336
335
  }
337
- export {
338
- AuthClient,
339
- ERROR_USER_INTERRUPT
340
- };
341
- //# sourceMappingURL=auth-client.js.map
336
+ //# sourceMappingURL=auth-client.js.map