@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,12 +1,5 @@
|
|
|
1
|
+
import { AnonymousIdentity, type Identity, type SignIdentity } from '@icp-sdk/core/agent';
|
|
1
2
|
import {
|
|
2
|
-
AnonymousIdentity,
|
|
3
|
-
type DerEncodedPublicKey,
|
|
4
|
-
type Identity,
|
|
5
|
-
type Signature,
|
|
6
|
-
type SignIdentity,
|
|
7
|
-
} from '@icp-sdk/core/agent';
|
|
8
|
-
import {
|
|
9
|
-
Delegation,
|
|
10
3
|
DelegationChain,
|
|
11
4
|
DelegationIdentity,
|
|
12
5
|
ECDSAKeyIdentity,
|
|
@@ -16,7 +9,9 @@ import {
|
|
|
16
9
|
type PartialIdentity,
|
|
17
10
|
} from '@icp-sdk/core/identity';
|
|
18
11
|
import type { Principal } from '@icp-sdk/core/principal';
|
|
19
|
-
import {
|
|
12
|
+
import { Signer } from '@icp-sdk/signer';
|
|
13
|
+
import { PostMessageTransport } from '@icp-sdk/signer/web';
|
|
14
|
+
import { IdleManager, type IdleManagerOptions } from './idle-manager.js';
|
|
20
15
|
import {
|
|
21
16
|
type AuthClientStorage,
|
|
22
17
|
IdbStorage,
|
|
@@ -25,592 +20,556 @@ import {
|
|
|
25
20
|
KEY_VECTOR,
|
|
26
21
|
LocalStorage,
|
|
27
22
|
type StoredKey,
|
|
28
|
-
} from './storage.
|
|
23
|
+
} from './storage.js';
|
|
29
24
|
|
|
30
25
|
const NANOSECONDS_PER_SECOND = BigInt(1_000_000_000);
|
|
31
26
|
const SECONDS_PER_HOUR = BigInt(3_600);
|
|
32
27
|
const NANOSECONDS_PER_HOUR = NANOSECONDS_PER_SECOND * SECONDS_PER_HOUR;
|
|
33
28
|
|
|
34
|
-
const IDENTITY_PROVIDER_DEFAULT = 'https://
|
|
35
|
-
const IDENTITY_PROVIDER_ENDPOINT = '#authorize';
|
|
36
|
-
|
|
29
|
+
const IDENTITY_PROVIDER_DEFAULT = 'https://id.ai/authorize';
|
|
37
30
|
const DEFAULT_MAX_TIME_TO_LIVE = BigInt(8) * NANOSECONDS_PER_HOUR;
|
|
38
31
|
|
|
39
32
|
const ECDSA_KEY_LABEL = 'ECDSA';
|
|
40
33
|
const ED25519_KEY_LABEL = 'Ed25519';
|
|
41
34
|
type BaseKeyType = typeof ECDSA_KEY_LABEL | typeof ED25519_KEY_LABEL;
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
// localStorage key used to cache the delegation expiration so that
|
|
37
|
+
// isAuthenticated() can answer synchronously without hitting IndexedDB.
|
|
38
|
+
const KEY_STORAGE_EXPIRATION = 'ic-delegation_expiration';
|
|
39
|
+
|
|
40
|
+
export type OpenIdProvider = 'google' | 'apple' | 'microsoft';
|
|
41
|
+
|
|
42
|
+
export const OPENID_PROVIDER_URLS = {
|
|
43
|
+
google: 'https://accounts.google.com',
|
|
44
|
+
apple: 'https://appleid.apple.com',
|
|
45
|
+
microsoft: 'https://login.microsoftonline.com/{tid}/v2.0',
|
|
46
|
+
} as const satisfies Record<OpenIdProvider, string>;
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
const DEFAULT_OPENID_SCOPE_KEYS = ['name', 'email', 'verified_email'] as const;
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
51
|
+
* Options for creating an {@link AuthClient}.
|
|
49
52
|
*/
|
|
50
53
|
export interface AuthClientCreateOptions {
|
|
51
54
|
/**
|
|
52
|
-
* An
|
|
55
|
+
* An identity to authenticate via delegation.
|
|
53
56
|
*/
|
|
54
57
|
identity?: SignIdentity | PartialIdentity;
|
|
58
|
+
|
|
55
59
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @
|
|
60
|
+
* Persistent storage backend. Defaults to IndexedDB.
|
|
61
|
+
* @default IdbStorage
|
|
58
62
|
*/
|
|
59
63
|
storage?: AuthClientStorage;
|
|
60
64
|
|
|
61
65
|
/**
|
|
62
|
-
* Type to
|
|
66
|
+
* Type of session key to generate on each sign-in.
|
|
63
67
|
*
|
|
64
|
-
*
|
|
65
|
-
* you should use `Ed25519` as the key type, as it can serialize to a string.
|
|
68
|
+
* Use `'Ed25519'` when your storage provider does not support `CryptoKey`.
|
|
66
69
|
* @default 'ECDSA'
|
|
67
70
|
*/
|
|
68
71
|
keyType?: BaseKeyType;
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
|
-
*
|
|
74
|
+
* Idle timeout configuration.
|
|
72
75
|
* @default after 10 minutes, invalidates the identity
|
|
73
76
|
*/
|
|
74
77
|
idleOptions?: IdleOptions;
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
loginOptions?: AuthClientLoginOptions;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface IdleOptions extends IdleManagerOptions {
|
|
83
|
-
/**
|
|
84
|
-
* Disables idle functionality for {@link IdleManager}
|
|
85
|
-
* @default false
|
|
86
|
-
*/
|
|
87
|
-
disableIdle?: boolean;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Disables default idle behavior - call logout & reload window
|
|
91
|
-
* @default false
|
|
92
|
-
*/
|
|
93
|
-
disableDefaultIdleCallback?: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export type OnSuccessFunc =
|
|
97
|
-
| (() => void | Promise<void>)
|
|
98
|
-
| ((message: InternetIdentityAuthResponseSuccess) => void | Promise<void>);
|
|
99
|
-
|
|
100
|
-
export type OnErrorFunc = (error?: string) => void | Promise<void>;
|
|
101
|
-
|
|
102
|
-
export interface AuthClientLoginOptions {
|
|
103
|
-
/**
|
|
104
|
-
* Identity provider
|
|
105
|
-
* @default "https://identity.internetcomputer.org"
|
|
80
|
+
* Identity provider URL.
|
|
81
|
+
* @default "https://id.ai/authorize"
|
|
106
82
|
*/
|
|
107
83
|
identityProvider?: string | URL;
|
|
84
|
+
|
|
108
85
|
/**
|
|
109
|
-
*
|
|
110
|
-
* @default BigInt(8) hours * BigInt(3_600_000_000_000) nanoseconds
|
|
111
|
-
*/
|
|
112
|
-
maxTimeToLive?: bigint;
|
|
113
|
-
/**
|
|
114
|
-
* 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).
|
|
115
|
-
*/
|
|
116
|
-
allowPinAuthentication?: boolean;
|
|
117
|
-
/**
|
|
118
|
-
* Origin for Identity Provider to use while generating the delegated identity. For II, the derivation origin must authorize this origin by setting a record at `<derivation-origin>/.well-known/ii-alternative-origins`.
|
|
86
|
+
* Derivation origin for the identity provider.
|
|
119
87
|
* @see https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc
|
|
120
88
|
*/
|
|
121
89
|
derivationOrigin?: string | URL;
|
|
90
|
+
|
|
122
91
|
/**
|
|
123
|
-
*
|
|
92
|
+
* Window features string for the authentication popup.
|
|
124
93
|
* @example "toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100"
|
|
125
94
|
*/
|
|
126
95
|
windowOpenerFeatures?: string;
|
|
96
|
+
|
|
127
97
|
/**
|
|
128
|
-
*
|
|
98
|
+
* OpenID provider for one-click sign-in. When set, the identity provider
|
|
99
|
+
* URL includes an `openid` search param so the user authenticates via
|
|
100
|
+
* the chosen provider (e.g. Google) instead of seeing Internet Identity directly.
|
|
129
101
|
*/
|
|
130
|
-
|
|
102
|
+
openIdProvider?: OpenIdProvider;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface IdleOptions extends IdleManagerOptions {
|
|
131
106
|
/**
|
|
132
|
-
*
|
|
107
|
+
* Disables idle functionality entirely.
|
|
108
|
+
* @default false
|
|
133
109
|
*/
|
|
134
|
-
|
|
110
|
+
disableIdle?: boolean;
|
|
111
|
+
|
|
135
112
|
/**
|
|
136
|
-
*
|
|
113
|
+
* Disables the default idle callback (logout & reload).
|
|
114
|
+
* @default false
|
|
137
115
|
*/
|
|
138
|
-
|
|
116
|
+
disableDefaultIdleCallback?: boolean;
|
|
139
117
|
}
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Options for {@link AuthClient.signIn}.
|
|
121
|
+
*/
|
|
122
|
+
export interface AuthClientSignInOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Maximum lifetime of the delegation in nanoseconds.
|
|
125
|
+
* @default 8 hours
|
|
126
|
+
*/
|
|
144
127
|
maxTimeToLive?: bigint;
|
|
145
|
-
allowPinAuthentication?: boolean;
|
|
146
|
-
derivationOrigin?: string;
|
|
147
|
-
}
|
|
148
128
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
pubkey: Uint8Array;
|
|
154
|
-
expiration: bigint;
|
|
155
|
-
targets?: Principal[];
|
|
156
|
-
};
|
|
157
|
-
signature: Uint8Array;
|
|
158
|
-
}[];
|
|
159
|
-
userPublicKey: Uint8Array;
|
|
160
|
-
authnMethod: 'passkey' | 'pin' | 'recovery';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
interface AuthReadyMessage {
|
|
164
|
-
kind: 'authorize-ready';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
interface AuthResponseSuccess {
|
|
168
|
-
kind: 'authorize-client-success';
|
|
169
|
-
delegations: {
|
|
170
|
-
delegation: {
|
|
171
|
-
pubkey: Uint8Array;
|
|
172
|
-
expiration: bigint;
|
|
173
|
-
targets?: Principal[];
|
|
174
|
-
};
|
|
175
|
-
signature: Uint8Array;
|
|
176
|
-
}[];
|
|
177
|
-
userPublicKey: Uint8Array;
|
|
178
|
-
authnMethod: 'passkey' | 'pin' | 'recovery';
|
|
129
|
+
/**
|
|
130
|
+
* Restrict the delegation to specific canisters.
|
|
131
|
+
*/
|
|
132
|
+
targets?: Principal[];
|
|
179
133
|
}
|
|
180
134
|
|
|
181
|
-
interface
|
|
182
|
-
|
|
183
|
-
|
|
135
|
+
export interface SignedAttributes {
|
|
136
|
+
data: Uint8Array;
|
|
137
|
+
signature: Uint8Array;
|
|
184
138
|
}
|
|
185
139
|
|
|
186
|
-
type IdentityServiceResponseMessage = AuthReadyMessage | AuthResponse;
|
|
187
|
-
type AuthResponse = AuthResponseSuccess | AuthResponseFailure;
|
|
188
|
-
|
|
189
140
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
141
|
+
* Manages authentication and identity for Internet Computer web apps.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const authClient = new AuthClient();
|
|
145
|
+
*
|
|
146
|
+
* const identity = authClient.isAuthenticated()
|
|
147
|
+
* ? await authClient.getIdentity()
|
|
148
|
+
* : await authClient.signIn();
|
|
192
149
|
*/
|
|
193
150
|
export class AuthClient {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
* 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.
|
|
206
|
-
* @example
|
|
207
|
-
* const authClient = await AuthClient.create({
|
|
208
|
-
* idleOptions: {
|
|
209
|
-
* disableIdle: true
|
|
210
|
-
* }
|
|
211
|
-
* })
|
|
212
|
-
*/
|
|
213
|
-
public static async create(options: AuthClientCreateOptions = {}): Promise<AuthClient> {
|
|
214
|
-
const storage = options.storage ?? new IdbStorage();
|
|
215
|
-
const keyType = options.keyType ?? ECDSA_KEY_LABEL;
|
|
151
|
+
#identity: Identity | PartialIdentity = new AnonymousIdentity();
|
|
152
|
+
#chain: DelegationChain | null = null;
|
|
153
|
+
#storage: AuthClientStorage;
|
|
154
|
+
#signer: Signer;
|
|
155
|
+
#options: AuthClientCreateOptions;
|
|
156
|
+
#initPromise: Promise<void> | null = null;
|
|
157
|
+
idleManager: IdleManager | undefined;
|
|
158
|
+
|
|
159
|
+
constructor(options: AuthClientCreateOptions = {}) {
|
|
160
|
+
this.#options = options;
|
|
161
|
+
this.#storage = options.storage ?? new IdbStorage();
|
|
216
162
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (!maybeIdentityStorage) {
|
|
223
|
-
// Attempt to migrate from localstorage
|
|
224
|
-
try {
|
|
225
|
-
const fallbackLocalStorage = new LocalStorage();
|
|
226
|
-
const localChain = await fallbackLocalStorage.get(KEY_STORAGE_DELEGATION);
|
|
227
|
-
const localKey = await fallbackLocalStorage.get(KEY_STORAGE_KEY);
|
|
228
|
-
// not relevant for Ed25519
|
|
229
|
-
if (localChain && localKey && keyType === ECDSA_KEY_LABEL) {
|
|
230
|
-
console.log('Discovered an identity stored in localstorage. Migrating to IndexedDB');
|
|
231
|
-
await storage.set(KEY_STORAGE_DELEGATION, localChain);
|
|
232
|
-
await storage.set(KEY_STORAGE_KEY, localKey);
|
|
233
|
-
|
|
234
|
-
maybeIdentityStorage = localChain;
|
|
235
|
-
// clean up
|
|
236
|
-
await fallbackLocalStorage.remove(KEY_STORAGE_DELEGATION);
|
|
237
|
-
await fallbackLocalStorage.remove(KEY_STORAGE_KEY);
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error(`error while attempting to recover localstorage: ${error}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (maybeIdentityStorage) {
|
|
244
|
-
try {
|
|
245
|
-
if (typeof maybeIdentityStorage === 'object') {
|
|
246
|
-
if (keyType === ED25519_KEY_LABEL && typeof maybeIdentityStorage === 'string') {
|
|
247
|
-
key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage);
|
|
248
|
-
} else {
|
|
249
|
-
key = await ECDSAKeyIdentity.fromKeyPair(maybeIdentityStorage);
|
|
250
|
-
}
|
|
251
|
-
} else if (typeof maybeIdentityStorage === 'string') {
|
|
252
|
-
// This is a legacy identity, which is a serialized Ed25519KeyIdentity.
|
|
253
|
-
key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage);
|
|
254
|
-
}
|
|
255
|
-
} catch {
|
|
256
|
-
// Ignore this, this means that the localStorage value isn't a valid Ed25519KeyIdentity or ECDSAKeyIdentity
|
|
257
|
-
// serialization.
|
|
258
|
-
}
|
|
259
|
-
}
|
|
163
|
+
const identityProviderUrl = new URL(
|
|
164
|
+
options.identityProvider?.toString() || IDENTITY_PROVIDER_DEFAULT,
|
|
165
|
+
);
|
|
166
|
+
if (options.openIdProvider) {
|
|
167
|
+
identityProviderUrl.searchParams.set('openid', OPENID_PROVIDER_URLS[options.openIdProvider]);
|
|
260
168
|
}
|
|
261
169
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const chainStorage = await storage.get(KEY_STORAGE_DELEGATION);
|
|
267
|
-
if (typeof chainStorage === 'object' && chainStorage !== null) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
'Delegation chain is incorrectly stored. A delegation chain should be stored as a string.',
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (options.identity) {
|
|
274
|
-
identity = options.identity;
|
|
275
|
-
} else if (chainStorage) {
|
|
276
|
-
chain = DelegationChain.fromJSON(chainStorage);
|
|
277
|
-
|
|
278
|
-
// Verify that the delegation isn't expired.
|
|
279
|
-
if (!isDelegationValid(chain)) {
|
|
280
|
-
await _deleteStorage(storage);
|
|
281
|
-
key = null;
|
|
282
|
-
} else {
|
|
283
|
-
// If the key is a public key, then we create a PartialDelegationIdentity.
|
|
284
|
-
if ('toDer' in key) {
|
|
285
|
-
identity = PartialDelegationIdentity.fromDelegation(key, chain);
|
|
286
|
-
// otherwise, we create a DelegationIdentity.
|
|
287
|
-
} else {
|
|
288
|
-
identity = DelegationIdentity.fromDelegation(key, chain);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
} catch (e) {
|
|
293
|
-
console.error(e);
|
|
294
|
-
// If there was a problem loading the chain, delete the key.
|
|
295
|
-
await _deleteStorage(storage);
|
|
296
|
-
key = null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
let idleManager: IdleManager | undefined;
|
|
300
|
-
if (options.idleOptions?.disableIdle) {
|
|
301
|
-
idleManager = undefined;
|
|
302
|
-
}
|
|
303
|
-
// if there is a delegation chain or provided identity, setup idleManager
|
|
304
|
-
else if (chain || options.identity) {
|
|
305
|
-
idleManager = IdleManager.create(options.idleOptions);
|
|
306
|
-
}
|
|
170
|
+
const transport = new PostMessageTransport({
|
|
171
|
+
url: identityProviderUrl.toString(),
|
|
172
|
+
windowOpenerFeatures: options.windowOpenerFeatures,
|
|
173
|
+
});
|
|
307
174
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.warn(
|
|
315
|
-
`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`,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
key = await ECDSAKeyIdentity.generate();
|
|
319
|
-
}
|
|
320
|
-
await persistKey(storage, key);
|
|
321
|
-
}
|
|
175
|
+
this.#signer = new Signer({
|
|
176
|
+
transport,
|
|
177
|
+
derivationOrigin: options.derivationOrigin?.toString(),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.#registerDefaultIdleCallback();
|
|
322
181
|
|
|
323
|
-
|
|
182
|
+
// Eagerly start restoring a previous session from storage.
|
|
183
|
+
// The result is awaited in getIdentity() before returning.
|
|
184
|
+
this.#init();
|
|
324
185
|
}
|
|
325
186
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
private _createOptions: AuthClientCreateOptions | undefined,
|
|
333
|
-
// A handle on the IdP window.
|
|
334
|
-
private _idpWindow?: Window,
|
|
335
|
-
// The event handler for processing events from the IdP.
|
|
336
|
-
private _eventHandler?: (event: MessageEvent) => void,
|
|
337
|
-
) {
|
|
338
|
-
this._registerDefaultIdleCallback();
|
|
187
|
+
/**
|
|
188
|
+
* Returns the current identity, restoring a previous session if available.
|
|
189
|
+
*/
|
|
190
|
+
async getIdentity(): Promise<Identity> {
|
|
191
|
+
await this.#init();
|
|
192
|
+
return this.#identity;
|
|
339
193
|
}
|
|
340
194
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
location.reload();
|
|
351
|
-
});
|
|
352
|
-
}
|
|
195
|
+
/**
|
|
196
|
+
* Checks whether the user has an active, non-expired session.
|
|
197
|
+
*/
|
|
198
|
+
isAuthenticated(): boolean {
|
|
199
|
+
// Uses a cached expiration in localStorage to avoid an async IndexedDB read.
|
|
200
|
+
const expiration = getExpirationFlag();
|
|
201
|
+
if (expiration === null) return false;
|
|
202
|
+
const nowNs = BigInt(Date.now()) * BigInt(1_000_000);
|
|
203
|
+
return nowNs < expiration;
|
|
353
204
|
}
|
|
354
205
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Opens the identity provider, requests a delegation, and returns the authenticated identity.
|
|
208
|
+
*
|
|
209
|
+
* @param options - Sign-in options.
|
|
210
|
+
* @param options.maxTimeToLive - Maximum lifetime of the delegation in nanoseconds.
|
|
211
|
+
* @param options.targets - Restrict the delegation to specific canisters.
|
|
212
|
+
* @returns The authenticated identity.
|
|
213
|
+
* @throws When authentication fails.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* try {
|
|
217
|
+
* const identity = await authClient.signIn();
|
|
218
|
+
* } catch (error) {
|
|
219
|
+
* console.error('Sign-in failed:', error);
|
|
220
|
+
* }
|
|
221
|
+
*/
|
|
222
|
+
async signIn(options?: AuthClientSignInOptions): Promise<Identity> {
|
|
223
|
+
await this.#signer.openChannel();
|
|
369
224
|
|
|
370
|
-
const
|
|
371
|
-
delegations,
|
|
372
|
-
message.userPublicKey as DerEncodedPublicKey,
|
|
373
|
-
);
|
|
225
|
+
const maxTimeToLive = options?.maxTimeToLive ?? DEFAULT_MAX_TIME_TO_LIVE;
|
|
374
226
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
227
|
+
// Fresh key per sign-in so each session has its own cryptographic identity.
|
|
228
|
+
const key =
|
|
229
|
+
this.#options.identity ?? (await generateKey(this.#options.keyType ?? ECDSA_KEY_LABEL));
|
|
230
|
+
|
|
231
|
+
const delegationChain = await this.#signer.requestDelegation({
|
|
232
|
+
publicKey: key.getPublicKey(),
|
|
233
|
+
targets: options?.targets,
|
|
234
|
+
maxTimeToLive,
|
|
235
|
+
});
|
|
379
236
|
|
|
380
|
-
this
|
|
237
|
+
this.#chain = delegationChain;
|
|
381
238
|
|
|
239
|
+
// PartialIdentity only has the public key — no signing capability.
|
|
382
240
|
if ('toDer' in key) {
|
|
383
|
-
this
|
|
241
|
+
this.#identity = PartialDelegationIdentity.fromDelegation(key, this.#chain);
|
|
384
242
|
} else {
|
|
385
|
-
this
|
|
243
|
+
this.#identity = DelegationIdentity.fromDelegation(key, this.#chain);
|
|
386
244
|
}
|
|
387
245
|
|
|
388
|
-
this
|
|
389
|
-
const idleOptions = this._createOptions?.idleOptions;
|
|
390
|
-
// create the idle manager on a successful login if we haven't disabled it
|
|
391
|
-
// and it doesn't already exist.
|
|
246
|
+
const idleOptions = this.#options?.idleOptions;
|
|
392
247
|
if (!this.idleManager && !idleOptions?.disableIdle) {
|
|
393
248
|
this.idleManager = IdleManager.create(idleOptions);
|
|
394
|
-
this
|
|
249
|
+
this.#registerDefaultIdleCallback();
|
|
395
250
|
}
|
|
396
251
|
|
|
397
|
-
|
|
398
|
-
|
|
252
|
+
// Persist so the session survives page reloads.
|
|
253
|
+
await persistChain(this.#storage, this.#chain);
|
|
254
|
+
await persistKey(this.#storage, key);
|
|
255
|
+
|
|
256
|
+
return this.#identity;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Requests signed identity attributes from the identity provider.
|
|
261
|
+
*
|
|
262
|
+
* @param params - Request parameters.
|
|
263
|
+
* @param params.keys - Attribute keys to request (e.g. `['email', 'name']`).
|
|
264
|
+
* @param params.nonce - 32-byte nonce issued by the RP canister.
|
|
265
|
+
* @returns Signed attribute data and signature.
|
|
266
|
+
* @throws When the identity provider returns an error or an invalid response.
|
|
267
|
+
*/
|
|
268
|
+
async requestAttributes(params: {
|
|
269
|
+
keys: string[];
|
|
270
|
+
nonce: Uint8Array;
|
|
271
|
+
}): Promise<SignedAttributes> {
|
|
272
|
+
const nonceBytes = params.nonce;
|
|
273
|
+
|
|
274
|
+
const response = await this.#signer.sendRequest({
|
|
275
|
+
jsonrpc: '2.0',
|
|
276
|
+
method: 'ii-icrc3-attributes',
|
|
277
|
+
params: { keys: params.keys, nonce: toBase64(nonceBytes) },
|
|
278
|
+
});
|
|
399
279
|
|
|
400
|
-
if (
|
|
401
|
-
|
|
280
|
+
if ('error' in response) {
|
|
281
|
+
throw new Error(response.error.message);
|
|
402
282
|
}
|
|
403
283
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
284
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
285
|
+
if (typeof result?.data !== 'string' || typeof result?.signature !== 'string') {
|
|
286
|
+
throw new Error('Invalid response: missing data or signature');
|
|
287
|
+
}
|
|
408
288
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
289
|
+
try {
|
|
290
|
+
return {
|
|
291
|
+
data: fromBase64(result.data),
|
|
292
|
+
signature: fromBase64(result.signature),
|
|
293
|
+
};
|
|
294
|
+
} catch (cause) {
|
|
295
|
+
throw new Error('Invalid response: data or signature is not valid base64', { cause });
|
|
296
|
+
}
|
|
412
297
|
}
|
|
413
298
|
|
|
414
|
-
|
|
415
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Clears the stored session and resets the client to an anonymous state.
|
|
301
|
+
*
|
|
302
|
+
* @param options - Logout options.
|
|
303
|
+
* @param options.returnTo - URL to navigate to after logout.
|
|
304
|
+
*/
|
|
305
|
+
async logout(options: { returnTo?: string } = {}): Promise<void> {
|
|
306
|
+
await deleteStorage(this.#storage);
|
|
307
|
+
|
|
308
|
+
this.#identity = new AnonymousIdentity();
|
|
309
|
+
this.#chain = null;
|
|
310
|
+
|
|
311
|
+
if (options.returnTo) {
|
|
312
|
+
try {
|
|
313
|
+
window.history.pushState({}, '', options.returnTo);
|
|
314
|
+
} catch {
|
|
315
|
+
window.location.href = options.returnTo;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
416
318
|
}
|
|
417
319
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this
|
|
422
|
-
|
|
423
|
-
|
|
320
|
+
// Memoized — only runs #hydrate once, returns the same promise on repeat calls.
|
|
321
|
+
#init(): Promise<void> {
|
|
322
|
+
if (!this.#initPromise) {
|
|
323
|
+
this.#initPromise = this.#hydrate();
|
|
324
|
+
}
|
|
325
|
+
return this.#initPromise;
|
|
424
326
|
}
|
|
425
327
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
* @param options.onSuccess Callback once login has completed
|
|
435
|
-
* @param options.onError Callback in case authentication fails
|
|
436
|
-
* @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.
|
|
437
|
-
* @example
|
|
438
|
-
* const authClient = await AuthClient.create();
|
|
439
|
-
* authClient.login({
|
|
440
|
-
* identityProvider: 'http://<canisterID>.127.0.0.1:8000',
|
|
441
|
-
* maxTimeToLive: BigInt (7) * BigInt(24) * BigInt(3_600_000_000_000), // 1 week
|
|
442
|
-
* windowOpenerFeatures: "toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100",
|
|
443
|
-
* onSuccess: () => {
|
|
444
|
-
* console.log('Login Successful!');
|
|
445
|
-
* },
|
|
446
|
-
* onError: (error) => {
|
|
447
|
-
* console.error('Login Failed: ', error);
|
|
448
|
-
* }
|
|
449
|
-
* });
|
|
450
|
-
*/
|
|
451
|
-
public async login(options?: AuthClientLoginOptions): Promise<void> {
|
|
452
|
-
// Merge the passed options with the options set during creation
|
|
453
|
-
const loginOptions = mergeLoginOptions(this._createOptions?.loginOptions, options);
|
|
328
|
+
// Attempts to restore a previous session (key + delegation chain) from
|
|
329
|
+
// storage. If found and still valid, sets #identity and #chain so the
|
|
330
|
+
// client is ready to use without a new signIn().
|
|
331
|
+
async #hydrate(): Promise<void> {
|
|
332
|
+
const key =
|
|
333
|
+
this.#options.identity ??
|
|
334
|
+
(await restoreKey(this.#storage, this.#options.keyType ?? ECDSA_KEY_LABEL));
|
|
335
|
+
if (!key) return;
|
|
454
336
|
|
|
455
|
-
|
|
456
|
-
|
|
337
|
+
const chain = await restoreChain(this.#storage);
|
|
338
|
+
if (!chain) return;
|
|
457
339
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
340
|
+
this.#chain = chain;
|
|
341
|
+
if ('toDer' in key) {
|
|
342
|
+
this.#identity = PartialDelegationIdentity.fromDelegation(key, chain);
|
|
343
|
+
} else {
|
|
344
|
+
this.#identity = DelegationIdentity.fromDelegation(key, chain);
|
|
345
|
+
}
|
|
464
346
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
347
|
+
if (!this.#options.idleOptions?.disableIdle && !this.idleManager) {
|
|
348
|
+
this.idleManager = IdleManager.create(this.#options.idleOptions);
|
|
349
|
+
this.#registerDefaultIdleCallback();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
469
352
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this._idpWindow =
|
|
479
|
-
window.open(
|
|
480
|
-
identityProviderUrl.toString(),
|
|
481
|
-
'idpWindow',
|
|
482
|
-
loginOptions?.windowOpenerFeatures,
|
|
483
|
-
) ?? undefined;
|
|
484
|
-
|
|
485
|
-
// Check if the _idpWindow is closed by user.
|
|
486
|
-
const checkInterruption = (): void => {
|
|
487
|
-
// The _idpWindow is opened and not yet closed by the client
|
|
488
|
-
if (this._idpWindow) {
|
|
489
|
-
if (this._idpWindow.closed) {
|
|
490
|
-
this._handleFailure(ERROR_USER_INTERRUPT, loginOptions?.onError);
|
|
491
|
-
} else {
|
|
492
|
-
setTimeout(checkInterruption, INTERRUPT_CHECK_INTERVAL);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
checkInterruption();
|
|
353
|
+
#registerDefaultIdleCallback() {
|
|
354
|
+
const idleOptions = this.#options?.idleOptions;
|
|
355
|
+
if (!idleOptions?.onIdle && !idleOptions?.disableDefaultIdleCallback) {
|
|
356
|
+
this.idleManager?.registerCallback(() => {
|
|
357
|
+
this.logout();
|
|
358
|
+
location.reload();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
497
361
|
}
|
|
362
|
+
}
|
|
498
363
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Encodes a Uint8Array to a base64 string.
|
|
366
|
+
* @param bytes - The bytes to encode.
|
|
367
|
+
*/
|
|
368
|
+
function toBase64(bytes: Uint8Array): string {
|
|
369
|
+
if ('toBase64' in bytes && typeof bytes.toBase64 === 'function') {
|
|
370
|
+
return bytes.toBase64();
|
|
371
|
+
}
|
|
372
|
+
let binary = '';
|
|
373
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
374
|
+
binary += String.fromCharCode(bytes[i]);
|
|
375
|
+
}
|
|
376
|
+
return globalThis.btoa(binary);
|
|
377
|
+
}
|
|
505
378
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
...options?.customValues,
|
|
519
|
-
};
|
|
520
|
-
this._idpWindow?.postMessage(request, identityProviderUrl.origin);
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
case 'authorize-client-success':
|
|
524
|
-
// Create the delegation chain and store it.
|
|
525
|
-
try {
|
|
526
|
-
await this._handleSuccess(message, options?.onSuccess);
|
|
527
|
-
} catch (err) {
|
|
528
|
-
this._handleFailure((err as Error).message, options?.onError);
|
|
529
|
-
}
|
|
530
|
-
break;
|
|
531
|
-
case 'authorize-client-failure':
|
|
532
|
-
this._handleFailure(message.text, options?.onError);
|
|
533
|
-
break;
|
|
534
|
-
default:
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
};
|
|
379
|
+
/**
|
|
380
|
+
* Decodes a base64 string to a Uint8Array.
|
|
381
|
+
* @param str - The base64-encoded string.
|
|
382
|
+
*/
|
|
383
|
+
function fromBase64(str: string): Uint8Array {
|
|
384
|
+
if ('fromBase64' in Uint8Array && typeof Uint8Array.fromBase64 === 'function') {
|
|
385
|
+
return Uint8Array.fromBase64(str);
|
|
386
|
+
}
|
|
387
|
+
const binary = globalThis.atob(str);
|
|
388
|
+
const bytes = new Uint8Array(binary.length);
|
|
389
|
+
for (let i = 0; i < binary.length; i++) {
|
|
390
|
+
bytes[i] = binary.charCodeAt(i);
|
|
538
391
|
}
|
|
392
|
+
return bytes;
|
|
393
|
+
}
|
|
539
394
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Generates a new session key.
|
|
397
|
+
* @param keyType - The key algorithm to use.
|
|
398
|
+
*/
|
|
399
|
+
async function generateKey(keyType: BaseKeyType): Promise<SignIdentity> {
|
|
400
|
+
if (keyType === ED25519_KEY_LABEL) {
|
|
401
|
+
return Ed25519KeyIdentity.generate();
|
|
545
402
|
}
|
|
403
|
+
return await ECDSAKeyIdentity.generate();
|
|
404
|
+
}
|
|
546
405
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Saves a session key to storage.
|
|
408
|
+
* @param storage - The storage backend.
|
|
409
|
+
* @param key - The key to persist.
|
|
410
|
+
*/
|
|
411
|
+
async function persistKey(
|
|
412
|
+
storage: AuthClientStorage,
|
|
413
|
+
key: SignIdentity | PartialIdentity,
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
await storage.set(KEY_STORAGE_KEY, serializeKey(key));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Loads a session key from storage. Falls back to migrating a legacy
|
|
420
|
+
* key from localStorage if nothing is found in the primary store.
|
|
421
|
+
* @param storage - The storage backend.
|
|
422
|
+
* @param keyType - The expected key algorithm (determines deserialization).
|
|
423
|
+
*/
|
|
424
|
+
async function restoreKey(
|
|
425
|
+
storage: AuthClientStorage,
|
|
426
|
+
keyType: BaseKeyType,
|
|
427
|
+
): Promise<SignIdentity | PartialIdentity | null> {
|
|
428
|
+
let stored = await storage.get(KEY_STORAGE_KEY);
|
|
429
|
+
if (!stored) {
|
|
430
|
+
stored = await migrateFromLocalStorage(storage, keyType);
|
|
431
|
+
}
|
|
432
|
+
if (!stored) return null;
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
// CryptoKeyPair (object) → ECDSA, JSON string → Ed25519
|
|
436
|
+
if (typeof stored === 'object') {
|
|
437
|
+
return await ECDSAKeyIdentity.fromKeyPair(stored);
|
|
550
438
|
}
|
|
551
|
-
|
|
439
|
+
return Ed25519KeyIdentity.fromJSON(stored);
|
|
440
|
+
} catch {
|
|
441
|
+
// The stored value may be corrupt or from an incompatible version.
|
|
442
|
+
// Returning null lets the caller fall through to key generation,
|
|
443
|
+
// which is safer than crashing on startup.
|
|
444
|
+
return null;
|
|
552
445
|
}
|
|
446
|
+
}
|
|
553
447
|
|
|
554
|
-
|
|
555
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Converts a key into a format suitable for storage.
|
|
450
|
+
* @param key - The key to serialize.
|
|
451
|
+
*/
|
|
452
|
+
function serializeKey(key: SignIdentity | PartialIdentity): StoredKey {
|
|
453
|
+
if (key instanceof ECDSAKeyIdentity) return key.getKeyPair();
|
|
454
|
+
if (key instanceof Ed25519KeyIdentity) return JSON.stringify(key.toJSON());
|
|
455
|
+
throw new Error('Unsupported key type');
|
|
456
|
+
}
|
|
556
457
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Saves the delegation chain and caches its earliest expiration
|
|
460
|
+
* in localStorage so {@link AuthClient.isAuthenticated} can check it synchronously.
|
|
461
|
+
* @param storage - The storage backend.
|
|
462
|
+
* @param chain - The delegation chain to persist.
|
|
463
|
+
*/
|
|
464
|
+
async function persistChain(storage: AuthClientStorage, chain: DelegationChain): Promise<void> {
|
|
465
|
+
await storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(chain.toJSON()));
|
|
560
466
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
467
|
+
let earliest: bigint | null = null;
|
|
468
|
+
for (const { delegation } of chain.delegations) {
|
|
469
|
+
if (earliest === null || delegation.expiration < earliest) {
|
|
470
|
+
earliest = delegation.expiration;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (earliest !== null) {
|
|
474
|
+
localStorage.setItem(KEY_STORAGE_EXPIRATION, earliest.toString());
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Loads the delegation chain from storage. Returns `null` and wipes
|
|
480
|
+
* storage if the chain is expired or corrupted.
|
|
481
|
+
* @param storage - The storage backend.
|
|
482
|
+
*/
|
|
483
|
+
async function restoreChain(storage: AuthClientStorage): Promise<DelegationChain | null> {
|
|
484
|
+
try {
|
|
485
|
+
const raw = await storage.get(KEY_STORAGE_DELEGATION);
|
|
486
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
487
|
+
|
|
488
|
+
const chain = DelegationChain.fromJSON(raw);
|
|
489
|
+
if (!isDelegationValid(chain)) {
|
|
490
|
+
await deleteStorage(storage);
|
|
491
|
+
return null;
|
|
567
492
|
}
|
|
493
|
+
return chain;
|
|
494
|
+
} catch (e) {
|
|
495
|
+
console.error(e);
|
|
496
|
+
await deleteStorage(storage);
|
|
497
|
+
return null;
|
|
568
498
|
}
|
|
569
499
|
}
|
|
570
500
|
|
|
571
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Clears all session data from storage.
|
|
503
|
+
* @param storage - The storage backend.
|
|
504
|
+
*/
|
|
505
|
+
async function deleteStorage(storage: AuthClientStorage): Promise<void> {
|
|
572
506
|
await storage.remove(KEY_STORAGE_KEY);
|
|
573
507
|
await storage.remove(KEY_STORAGE_DELEGATION);
|
|
574
508
|
await storage.remove(KEY_VECTOR);
|
|
509
|
+
localStorage.removeItem(KEY_STORAGE_EXPIRATION);
|
|
575
510
|
}
|
|
576
511
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return undefined;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const customValues =
|
|
586
|
-
loginOptions?.customValues || otherLoginOptions?.customValues
|
|
587
|
-
? {
|
|
588
|
-
...loginOptions?.customValues,
|
|
589
|
-
...otherLoginOptions?.customValues,
|
|
590
|
-
}
|
|
591
|
-
: undefined;
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
...loginOptions,
|
|
595
|
-
...otherLoginOptions,
|
|
596
|
-
customValues,
|
|
597
|
-
};
|
|
512
|
+
/** Reads the cached delegation expiration from localStorage (nanoseconds). */
|
|
513
|
+
function getExpirationFlag(): bigint | null {
|
|
514
|
+
const value = localStorage.getItem(KEY_STORAGE_EXPIRATION);
|
|
515
|
+
if (value === null) return null;
|
|
516
|
+
return BigInt(value);
|
|
598
517
|
}
|
|
599
518
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
519
|
+
/**
|
|
520
|
+
* One-time migration: moves a legacy session stored in localStorage
|
|
521
|
+
* into the primary storage, then cleans up the old entries.
|
|
522
|
+
* @param storage - The target storage backend.
|
|
523
|
+
* @param keyType - The expected key algorithm (only ECDSA keys are migrated).
|
|
524
|
+
*/
|
|
525
|
+
async function migrateFromLocalStorage(
|
|
526
|
+
storage: AuthClientStorage,
|
|
527
|
+
keyType: BaseKeyType,
|
|
528
|
+
): Promise<StoredKey | null> {
|
|
529
|
+
try {
|
|
530
|
+
const fallback = new LocalStorage();
|
|
531
|
+
const localChain = await fallback.get(KEY_STORAGE_DELEGATION);
|
|
532
|
+
const localKey = await fallback.get(KEY_STORAGE_KEY);
|
|
533
|
+
|
|
534
|
+
if (!localChain || !localKey || keyType !== ECDSA_KEY_LABEL) return null;
|
|
535
|
+
|
|
536
|
+
console.log('Discovered an identity stored in localstorage. Migrating to IndexedDB');
|
|
537
|
+
await storage.set(KEY_STORAGE_DELEGATION, localChain);
|
|
538
|
+
await storage.set(KEY_STORAGE_KEY, localKey);
|
|
539
|
+
await fallback.remove(KEY_STORAGE_DELEGATION);
|
|
540
|
+
await fallback.remove(KEY_STORAGE_KEY);
|
|
541
|
+
|
|
542
|
+
return localKey;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error(`error while attempting to recover localstorage: ${error}`);
|
|
545
|
+
return null;
|
|
606
546
|
}
|
|
607
|
-
throw new Error('Unsupported key type');
|
|
608
547
|
}
|
|
609
548
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Scopes attribute keys to an OpenID provider.
|
|
551
|
+
*
|
|
552
|
+
* When using one-click sign-in, attributes can be scoped to the same provider
|
|
553
|
+
* so the user grants access in a single step without an additional prompt.
|
|
554
|
+
*
|
|
555
|
+
* @param params.openIdProvider - The OpenID provider the keys should be scoped to.
|
|
556
|
+
* @param params.keys - The attribute keys to scope. Defaults to `['name', 'email', 'verified_email']`.
|
|
557
|
+
* @returns The scoped attribute keys as `openid:<provider-url>:<key>`.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* scopedKeys({ openIdProvider: 'google', keys: ['email'] });
|
|
561
|
+
* // ['openid:https://accounts.google.com:email']
|
|
562
|
+
*/
|
|
563
|
+
export function scopedKeys<
|
|
564
|
+
P extends keyof typeof OPENID_PROVIDER_URLS,
|
|
565
|
+
K extends string = (typeof DEFAULT_OPENID_SCOPE_KEYS)[number],
|
|
566
|
+
>(params: {
|
|
567
|
+
openIdProvider: P;
|
|
568
|
+
keys?: readonly K[];
|
|
569
|
+
}): `openid:${(typeof OPENID_PROVIDER_URLS)[P]}:${K}`[] {
|
|
570
|
+
const provider = OPENID_PROVIDER_URLS[params.openIdProvider];
|
|
571
|
+
const keys = params.keys ?? DEFAULT_OPENID_SCOPE_KEYS;
|
|
572
|
+
return keys.map(
|
|
573
|
+
(key) => `openid:${provider}:${key}` as `openid:${(typeof OPENID_PROVIDER_URLS)[P]}:${K}`,
|
|
574
|
+
);
|
|
616
575
|
}
|