@icp-sdk/auth 5.0.0 → 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.
- package/README.md +25 -13
- package/dist/esm/client/auth-client.d.ts +86 -116
- package/dist/esm/client/auth-client.js +310 -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 +11 -9
- 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 +354 -472
- package/src/client/db.ts +1 -1
- package/src/client/idle-manager.ts +43 -25
- package/src/client/index.ts +4 -4
- package/src/client/storage.ts +1 -1
|
@@ -1,341 +1,336 @@
|
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
338
|
-
AuthClient,
|
|
339
|
-
ERROR_USER_INTERRUPT
|
|
340
|
-
};
|
|
341
|
-
//# sourceMappingURL=auth-client.js.map
|
|
336
|
+
//# sourceMappingURL=auth-client.js.map
|