@ic-reactor/core 3.3.1 → 3.4.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 +60 -0
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +209 -54
- package/dist/client.js.map +1 -1
- package/dist/identity-attributes.d.ts +14 -0
- package/dist/identity-attributes.d.ts.map +1 -0
- package/dist/identity-attributes.js +138 -0
- package/dist/identity-attributes.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/client.d.ts +68 -2
- package/dist/types/client.d.ts.map +1 -1
- package/package.json +11 -11
- package/src/client.ts +283 -59
- package/src/identity-attributes.ts +220 -0
- package/src/index.ts +1 -0
- package/src/types/client.ts +82 -2
package/src/client.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Identity } from "@icp-sdk/core/agent"
|
|
2
|
-
import type { AuthClient, AuthClientLoginOptions } from "@icp-sdk/auth/client"
|
|
3
2
|
import type {
|
|
4
3
|
ClientManagerParameters,
|
|
5
4
|
AgentState,
|
|
6
5
|
AuthState,
|
|
6
|
+
AuthClientSignInOptions,
|
|
7
|
+
AuthClientLike,
|
|
8
|
+
ClientManagerAuthClientOptions,
|
|
9
|
+
ClientManagerSignInOptions,
|
|
10
|
+
IdentityAttributeResult,
|
|
11
|
+
RequestIdentityAttributesParameters,
|
|
12
|
+
RequestOpenIdIdentityAttributesParameters,
|
|
7
13
|
} from "./types/client"
|
|
8
14
|
import type { Principal } from "@icp-sdk/core/principal"
|
|
9
15
|
import type { QueryClient } from "@tanstack/react-query"
|
|
@@ -20,6 +26,16 @@ import {
|
|
|
20
26
|
getProcessEnvNetwork,
|
|
21
27
|
isDev,
|
|
22
28
|
} from "./utils/helper"
|
|
29
|
+
import {
|
|
30
|
+
decodeIdentityAttributeValues,
|
|
31
|
+
IDENTITY_ATTRIBUTES_BETA_PROVIDER,
|
|
32
|
+
normalizeSignedIdentityAttributes,
|
|
33
|
+
resolveIdentityAttributeKeys,
|
|
34
|
+
} from "./identity-attributes"
|
|
35
|
+
|
|
36
|
+
type AuthClientConstructor = {
|
|
37
|
+
new (options?: ClientManagerAuthClientOptions): AuthClientLike
|
|
38
|
+
}
|
|
23
39
|
|
|
24
40
|
/**
|
|
25
41
|
* ClientManager is a central class for managing the Internet Computer (IC) agent and authentication state.
|
|
@@ -43,7 +59,7 @@ import {
|
|
|
43
59
|
*/
|
|
44
60
|
export class ClientManager {
|
|
45
61
|
#agent: HttpAgent
|
|
46
|
-
#authClient?:
|
|
62
|
+
#authClient?: AuthClientLike
|
|
47
63
|
#identitySubscribers: Array<(identity: Identity) => void> = []
|
|
48
64
|
#agentStateSubscribers: Array<(state: AgentState) => void> = []
|
|
49
65
|
#authStateSubscribers: Array<(state: AuthState) => void> = []
|
|
@@ -64,6 +80,8 @@ export class ClientManager {
|
|
|
64
80
|
|
|
65
81
|
private initPromise?: Promise<void>
|
|
66
82
|
private authPromise?: Promise<Identity | undefined>
|
|
83
|
+
private authStateRevision = 0
|
|
84
|
+
private authClientWasProvided = false
|
|
67
85
|
private port: number
|
|
68
86
|
private internetIdentityId?: string
|
|
69
87
|
|
|
@@ -164,15 +182,11 @@ export class ClientManager {
|
|
|
164
182
|
})
|
|
165
183
|
|
|
166
184
|
if (authClient) {
|
|
185
|
+
this.authClientWasProvided = true
|
|
167
186
|
this.#authClient = authClient
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
identity,
|
|
172
|
-
isAuthenticated: !identity.getPrincipal().isAnonymous(),
|
|
173
|
-
isAuthenticating: false,
|
|
174
|
-
error: undefined,
|
|
175
|
-
}
|
|
187
|
+
this.syncAuthStateFromClient(this.authStateRevision).catch((error) => {
|
|
188
|
+
this.updateAuthState({ error: error as Error, isAuthenticating: false })
|
|
189
|
+
})
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
|
|
@@ -274,25 +288,18 @@ export class ClientManager {
|
|
|
274
288
|
this.updateAuthState({ isAuthenticating: true })
|
|
275
289
|
try {
|
|
276
290
|
if (!this.#authClient) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
return null
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
if (!authModule) {
|
|
283
|
-
this.authModuleMissing = true
|
|
291
|
+
const authClient = await this.initializeAuthClient()
|
|
292
|
+
if (!authClient) {
|
|
284
293
|
this.updateAuthState({ isAuthenticating: false })
|
|
285
294
|
return undefined
|
|
286
295
|
}
|
|
287
|
-
|
|
288
|
-
const { AuthClient } = authModule
|
|
289
|
-
this.#authClient = await AuthClient.create()
|
|
290
296
|
}
|
|
291
|
-
const identity = this.#authClient
|
|
297
|
+
const identity = await this.#authClient!.getIdentity()
|
|
298
|
+
const isAuthenticated = await this.#authClient!.isAuthenticated()
|
|
292
299
|
this.updateAgent(identity)
|
|
293
300
|
this.updateAuthState({
|
|
294
301
|
identity,
|
|
295
|
-
isAuthenticated
|
|
302
|
+
isAuthenticated,
|
|
296
303
|
isAuthenticating: false,
|
|
297
304
|
})
|
|
298
305
|
return identity
|
|
@@ -314,13 +321,29 @@ export class ClientManager {
|
|
|
314
321
|
* @param loginOptions - Options for the login flow, including identity provider and callbacks.
|
|
315
322
|
* @throws An error if the authentication module is not installed.
|
|
316
323
|
*/
|
|
317
|
-
public login = async (loginOptions?:
|
|
324
|
+
public login = async (loginOptions?: ClientManagerSignInOptions) => {
|
|
325
|
+
let didCompleteSignIn = false
|
|
326
|
+
|
|
318
327
|
try {
|
|
319
328
|
// Ensure agent is initialized before login
|
|
320
329
|
if (!this.agentState.isInitialized) {
|
|
321
330
|
await this.initializeAgent()
|
|
322
331
|
}
|
|
323
332
|
|
|
333
|
+
const identityProvider =
|
|
334
|
+
loginOptions?.identityProvider || this.getDefaultIdentityProvider()
|
|
335
|
+
const authClientOptions = getAuthClientOptions({
|
|
336
|
+
...loginOptions,
|
|
337
|
+
identityProvider,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
!this.#authClient ||
|
|
342
|
+
this.shouldRecreateAuthClient(authClientOptions)
|
|
343
|
+
) {
|
|
344
|
+
await this.initializeAuthClient(authClientOptions)
|
|
345
|
+
}
|
|
346
|
+
|
|
324
347
|
if (!this.#authClient) {
|
|
325
348
|
await this.authenticate()
|
|
326
349
|
}
|
|
@@ -333,38 +356,34 @@ export class ClientManager {
|
|
|
333
356
|
|
|
334
357
|
this.updateAuthState({ isAuthenticating: true, error: undefined })
|
|
335
358
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await this.#authClient.login({
|
|
341
|
-
...loginOptions,
|
|
342
|
-
identityProvider,
|
|
343
|
-
onSuccess: () => {
|
|
344
|
-
const identity = this.#authClient!.getIdentity()
|
|
345
|
-
if (identity) {
|
|
346
|
-
this.updateAgent(identity)
|
|
347
|
-
this.updateAuthState({
|
|
348
|
-
identity,
|
|
349
|
-
isAuthenticated: true,
|
|
350
|
-
isAuthenticating: false,
|
|
351
|
-
})
|
|
352
|
-
}
|
|
353
|
-
;(loginOptions?.onSuccess as any)?.()
|
|
354
|
-
},
|
|
355
|
-
onError: (error) => {
|
|
356
|
-
this.updateAuthState({
|
|
357
|
-
error: new Error(error),
|
|
358
|
-
isAuthenticating: false,
|
|
359
|
-
})
|
|
360
|
-
loginOptions?.onError?.(error)
|
|
361
|
-
},
|
|
362
|
-
})
|
|
363
|
-
} catch (error) {
|
|
359
|
+
const identity = await this.#authClient.signIn(
|
|
360
|
+
getSignInOptions(loginOptions)
|
|
361
|
+
)
|
|
362
|
+
this.updateAgent(identity)
|
|
364
363
|
this.updateAuthState({
|
|
365
|
-
|
|
364
|
+
identity,
|
|
365
|
+
isAuthenticated: true,
|
|
366
366
|
isAuthenticating: false,
|
|
367
367
|
})
|
|
368
|
+
didCompleteSignIn = true
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await (
|
|
372
|
+
loginOptions?.onSuccess as (() => void | Promise<void>) | undefined
|
|
373
|
+
)?.()
|
|
374
|
+
} catch (callbackError) {
|
|
375
|
+
this.updateAuthState({ error: callbackError as Error })
|
|
376
|
+
await loginOptions?.onError?.((callbackError as Error).message)
|
|
377
|
+
throw callbackError
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (!didCompleteSignIn) {
|
|
381
|
+
await loginOptions?.onError?.((error as Error).message)
|
|
382
|
+
this.updateAuthState({
|
|
383
|
+
error: error as Error,
|
|
384
|
+
isAuthenticating: false,
|
|
385
|
+
})
|
|
386
|
+
}
|
|
368
387
|
throw error
|
|
369
388
|
}
|
|
370
389
|
}
|
|
@@ -374,25 +393,126 @@ export class ClientManager {
|
|
|
374
393
|
*
|
|
375
394
|
* @throws An error if the authentication module is not installed.
|
|
376
395
|
*/
|
|
377
|
-
public logout = async () => {
|
|
396
|
+
public logout = async (options?: { returnTo?: string }) => {
|
|
378
397
|
if (!this.#authClient) {
|
|
379
398
|
throw new Error(
|
|
380
399
|
"Authentication module is missing or failed to initialize. To use logout, please install the auth package: npm install @icp-sdk/auth"
|
|
381
400
|
)
|
|
382
401
|
}
|
|
383
402
|
this.updateAuthState({ isAuthenticating: true, error: undefined })
|
|
384
|
-
await this.#authClient.logout()
|
|
385
|
-
const identity = this.#authClient.getIdentity()
|
|
386
|
-
|
|
387
|
-
|
|
403
|
+
await this.#authClient.logout(options)
|
|
404
|
+
const identity = await this.#authClient.getIdentity()
|
|
405
|
+
this.updateAgent(identity)
|
|
406
|
+
this.updateAuthState({
|
|
407
|
+
identity,
|
|
408
|
+
isAuthenticated: false,
|
|
409
|
+
isAuthenticating: false,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public requestIdentityAttributes = async ({
|
|
414
|
+
keys,
|
|
415
|
+
nonce,
|
|
416
|
+
identityProvider = IDENTITY_ATTRIBUTES_BETA_PROVIDER,
|
|
417
|
+
openIdProvider,
|
|
418
|
+
windowOpenerFeatures,
|
|
419
|
+
signIn = true,
|
|
420
|
+
maxTimeToLive,
|
|
421
|
+
targets,
|
|
422
|
+
}: RequestIdentityAttributesParameters): Promise<IdentityAttributeResult> => {
|
|
423
|
+
if (!this.agentState.isInitialized) {
|
|
424
|
+
await this.initializeAgent()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const authClientOptions = getAuthClientOptions({
|
|
428
|
+
identityProvider,
|
|
429
|
+
windowOpenerFeatures,
|
|
430
|
+
openIdProvider,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
if (!this.#authClient || this.shouldRecreateAuthClient(authClientOptions)) {
|
|
434
|
+
await this.initializeAuthClient(authClientOptions)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!this.#authClient) {
|
|
438
|
+
await this.authenticate()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!this.#authClient) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
"Authentication module is missing or failed to initialize. To request identity attributes, please install @icp-sdk/auth v6 or provide a compatible authClient."
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
this.updateAuthState({ isAuthenticating: true, error: undefined })
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const requestPromise = this.#authClient.requestAttributes({
|
|
451
|
+
keys,
|
|
452
|
+
nonce,
|
|
453
|
+
})
|
|
454
|
+
const identityPromise = signIn
|
|
455
|
+
? this.#authClient.signIn({
|
|
456
|
+
maxTimeToLive,
|
|
457
|
+
targets,
|
|
458
|
+
})
|
|
459
|
+
: Promise.resolve(this.#authClient.getIdentity())
|
|
460
|
+
|
|
461
|
+
const [signedAttributes, identity] = await Promise.all([
|
|
462
|
+
requestPromise,
|
|
463
|
+
identityPromise,
|
|
464
|
+
])
|
|
465
|
+
|
|
466
|
+
const finalIdentity = identity ?? (await this.#authClient.getIdentity())
|
|
467
|
+
const isAuthenticated = await this.#authClient.isAuthenticated()
|
|
468
|
+
this.updateAgent(finalIdentity)
|
|
388
469
|
this.updateAuthState({
|
|
389
|
-
identity,
|
|
390
|
-
isAuthenticated
|
|
470
|
+
identity: finalIdentity,
|
|
471
|
+
isAuthenticated,
|
|
391
472
|
isAuthenticating: false,
|
|
392
473
|
})
|
|
474
|
+
|
|
475
|
+
const normalizedSignedAttributes =
|
|
476
|
+
normalizeSignedIdentityAttributes(signedAttributes)
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
principal: finalIdentity.getPrincipal().toText(),
|
|
480
|
+
requestedKeys: keys,
|
|
481
|
+
signedAttributes: normalizedSignedAttributes,
|
|
482
|
+
decodedAttributes: decodeIdentityAttributeValues(
|
|
483
|
+
normalizedSignedAttributes.data,
|
|
484
|
+
keys
|
|
485
|
+
),
|
|
486
|
+
completedAt: new Date().toISOString(),
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
this.updateAuthState({ error: error as Error, isAuthenticating: false })
|
|
490
|
+
throw error
|
|
393
491
|
}
|
|
394
492
|
}
|
|
395
493
|
|
|
494
|
+
public requestOpenIdIdentityAttributes = async ({
|
|
495
|
+
nonce,
|
|
496
|
+
openIdProvider,
|
|
497
|
+
keys,
|
|
498
|
+
identityProvider = IDENTITY_ATTRIBUTES_BETA_PROVIDER,
|
|
499
|
+
windowOpenerFeatures,
|
|
500
|
+
signIn,
|
|
501
|
+
maxTimeToLive,
|
|
502
|
+
targets,
|
|
503
|
+
}: RequestOpenIdIdentityAttributesParameters): Promise<IdentityAttributeResult> => {
|
|
504
|
+
return this.requestIdentityAttributes({
|
|
505
|
+
keys: await resolveIdentityAttributeKeys({ openIdProvider, keys }),
|
|
506
|
+
nonce,
|
|
507
|
+
identityProvider,
|
|
508
|
+
openIdProvider,
|
|
509
|
+
windowOpenerFeatures,
|
|
510
|
+
signIn,
|
|
511
|
+
maxTimeToLive,
|
|
512
|
+
targets,
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
396
516
|
/**
|
|
397
517
|
* The underlying HttpAgent managed by this class.
|
|
398
518
|
*/
|
|
@@ -407,6 +527,62 @@ export class ClientManager {
|
|
|
407
527
|
return this.#authClient
|
|
408
528
|
}
|
|
409
529
|
|
|
530
|
+
private async initializeAuthClient(
|
|
531
|
+
options?: ClientManagerAuthClientOptions
|
|
532
|
+
): Promise<AuthClientLike | undefined> {
|
|
533
|
+
const authModule = await import("@icp-sdk/auth/client").catch(() => {
|
|
534
|
+
this.authModuleMissing = true
|
|
535
|
+
return null
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
if (!authModule) {
|
|
539
|
+
this.authModuleMissing = true
|
|
540
|
+
return undefined
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.#authClient = this.createAuthClient(authModule, options)
|
|
544
|
+
return this.#authClient
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private createAuthClient(
|
|
548
|
+
authModule: unknown,
|
|
549
|
+
options?: ClientManagerAuthClientOptions
|
|
550
|
+
): AuthClientLike {
|
|
551
|
+
const AuthClient = (authModule as { AuthClient?: AuthClientConstructor })
|
|
552
|
+
.AuthClient
|
|
553
|
+
|
|
554
|
+
if (!AuthClient) {
|
|
555
|
+
throw new Error("@icp-sdk/auth/client did not export AuthClient")
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return new AuthClient(options)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private shouldRecreateAuthClient(
|
|
562
|
+
options?: ClientManagerAuthClientOptions
|
|
563
|
+
): boolean {
|
|
564
|
+
return !this.authClientWasProvided && hasAuthClientOptions(options)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private async syncAuthStateFromClient(revision = this.authStateRevision) {
|
|
568
|
+
if (!this.#authClient) {
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const identity = await this.#authClient.getIdentity()
|
|
573
|
+
const isAuthenticated = await this.#authClient.isAuthenticated()
|
|
574
|
+
if (revision !== this.authStateRevision) {
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
this.updateAgent(identity)
|
|
578
|
+
this.updateAuthState({
|
|
579
|
+
identity,
|
|
580
|
+
isAuthenticated,
|
|
581
|
+
isAuthenticating: false,
|
|
582
|
+
error: undefined,
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
410
586
|
/**
|
|
411
587
|
* The host URL of the current IC agent.
|
|
412
588
|
*/
|
|
@@ -585,7 +761,55 @@ export class ClientManager {
|
|
|
585
761
|
|
|
586
762
|
private updateAuthState(newState: Partial<AuthState>) {
|
|
587
763
|
if (isDev()) console.debug("[ic-reactor] Updating Auth State:", newState)
|
|
764
|
+
this.authStateRevision += 1
|
|
588
765
|
this.authState = { ...this.authState, ...newState }
|
|
589
766
|
this.notifyAuthStateSubscribers(this.authState)
|
|
590
767
|
}
|
|
591
768
|
}
|
|
769
|
+
|
|
770
|
+
function getAuthClientOptions(
|
|
771
|
+
options?: ClientManagerAuthClientOptions
|
|
772
|
+
): ClientManagerAuthClientOptions | undefined {
|
|
773
|
+
if (!options) {
|
|
774
|
+
return undefined
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
identityProvider: options.identityProvider,
|
|
779
|
+
windowOpenerFeatures: options.windowOpenerFeatures,
|
|
780
|
+
openIdProvider: getAuthClientOpenIdProvider(options.openIdProvider),
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function getAuthClientOpenIdProvider(
|
|
785
|
+
openIdProvider?: ClientManagerAuthClientOptions["openIdProvider"]
|
|
786
|
+
): ClientManagerAuthClientOptions["openIdProvider"] | undefined {
|
|
787
|
+
return openIdProvider === "google" ||
|
|
788
|
+
openIdProvider === "apple" ||
|
|
789
|
+
openIdProvider === "microsoft"
|
|
790
|
+
? openIdProvider
|
|
791
|
+
: undefined
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function hasAuthClientOptions(
|
|
795
|
+
options?: ClientManagerAuthClientOptions
|
|
796
|
+
): boolean {
|
|
797
|
+
return Boolean(
|
|
798
|
+
options?.identityProvider ||
|
|
799
|
+
options?.windowOpenerFeatures ||
|
|
800
|
+
options?.openIdProvider
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function getSignInOptions(
|
|
805
|
+
options?: ClientManagerSignInOptions
|
|
806
|
+
): AuthClientSignInOptions | undefined {
|
|
807
|
+
if (!options) {
|
|
808
|
+
return undefined
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
maxTimeToLive: options.maxTimeToLive,
|
|
813
|
+
targets: options.targets,
|
|
814
|
+
}
|
|
815
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
IdentityAttributeOpenIdProvider,
|
|
5
|
+
IdentityAttributeValues,
|
|
6
|
+
SignedIdentityAttributes,
|
|
7
|
+
} from "./types/client"
|
|
8
|
+
|
|
9
|
+
export const IDENTITY_ATTRIBUTES_BETA_PROVIDER = "https://beta.id.ai/authorize"
|
|
10
|
+
|
|
11
|
+
const OPEN_ID_PROVIDER_URLS = {
|
|
12
|
+
apple: "https://appleid.apple.com",
|
|
13
|
+
google: "https://accounts.google.com",
|
|
14
|
+
microsoft: "https://login.microsoftonline.com/{tid}/v2.0",
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export function identityAttributeKeys({
|
|
18
|
+
openIdProvider,
|
|
19
|
+
keys,
|
|
20
|
+
}: {
|
|
21
|
+
openIdProvider: IdentityAttributeOpenIdProvider
|
|
22
|
+
keys: string[]
|
|
23
|
+
}): string[] {
|
|
24
|
+
const provider = normalizeOpenIdProvider(openIdProvider)
|
|
25
|
+
return keys.map((key) => `openid:${provider}:${key}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeOpenIdProvider(
|
|
29
|
+
openIdProvider: IdentityAttributeOpenIdProvider
|
|
30
|
+
): string {
|
|
31
|
+
return (
|
|
32
|
+
OPEN_ID_PROVIDER_URLS[
|
|
33
|
+
openIdProvider as keyof typeof OPEN_ID_PROVIDER_URLS
|
|
34
|
+
] ?? openIdProvider
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolveIdentityAttributeKeys({
|
|
39
|
+
openIdProvider,
|
|
40
|
+
keys,
|
|
41
|
+
}: {
|
|
42
|
+
openIdProvider: IdentityAttributeOpenIdProvider
|
|
43
|
+
keys: string[]
|
|
44
|
+
}): Promise<string[]> {
|
|
45
|
+
return identityAttributeKeys({ openIdProvider, keys })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function decodeIdentityAttributeValues(
|
|
49
|
+
data: Uint8Array,
|
|
50
|
+
requestedKeys: string[]
|
|
51
|
+
): IdentityAttributeValues {
|
|
52
|
+
const requestedKeyMap = requestedKeys.reduce<Record<string, string>>(
|
|
53
|
+
(acc, key) => {
|
|
54
|
+
acc[key] = identityAttributeDisplayKey(key)
|
|
55
|
+
acc[identityAttributeDisplayKey(key)] = identityAttributeDisplayKey(key)
|
|
56
|
+
return acc
|
|
57
|
+
},
|
|
58
|
+
{}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const decodedValues = decodeCandidAttributeValues(data, requestedKeyMap)
|
|
62
|
+
if (Object.keys(decodedValues).length > 0) {
|
|
63
|
+
return decodedValues
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return extractPrintableAttributeValues(data, requestedKeys)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function decodeCandidAttributeValues(
|
|
70
|
+
data: Uint8Array,
|
|
71
|
+
requestedKeyMap: Record<string, string>
|
|
72
|
+
): IdentityAttributeValues {
|
|
73
|
+
const textPairs = IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))
|
|
74
|
+
const recordPairs = IDL.Vec(
|
|
75
|
+
IDL.Record({
|
|
76
|
+
key: IDL.Text,
|
|
77
|
+
value: IDL.Text,
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const candidates = [
|
|
82
|
+
[textPairs],
|
|
83
|
+
[recordPairs],
|
|
84
|
+
[IDL.Record({ attributes: textPairs })],
|
|
85
|
+
[IDL.Record({ values: textPairs })],
|
|
86
|
+
[IDL.Record({ attributes: recordPairs })],
|
|
87
|
+
[IDL.Record({ values: recordPairs })],
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
try {
|
|
92
|
+
const decoded = IDL.decode(candidate, data)
|
|
93
|
+
const values = collectDecodedAttributeValues(decoded, requestedKeyMap)
|
|
94
|
+
if (Object.keys(values).length > 0) {
|
|
95
|
+
return values
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Try the next known beta payload shape, then fall back to text extraction.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collectDecodedAttributeValues(
|
|
106
|
+
value: unknown,
|
|
107
|
+
requestedKeyMap: Record<string, string>,
|
|
108
|
+
activeKey?: string
|
|
109
|
+
): IdentityAttributeValues {
|
|
110
|
+
const values: IdentityAttributeValues = {}
|
|
111
|
+
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
const displayKey = requestedKeyMap[value]
|
|
114
|
+
if (displayKey) {
|
|
115
|
+
return values
|
|
116
|
+
}
|
|
117
|
+
if (activeKey && isPrintableTextValue(value)) {
|
|
118
|
+
values[activeKey] = value
|
|
119
|
+
}
|
|
120
|
+
return values
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
if (value.length === 2 && typeof value[0] === "string") {
|
|
125
|
+
const displayKey = requestedKeyMap[value[0]]
|
|
126
|
+
if (
|
|
127
|
+
displayKey &&
|
|
128
|
+
typeof value[1] === "string" &&
|
|
129
|
+
isPrintableTextValue(value[1])
|
|
130
|
+
) {
|
|
131
|
+
values[displayKey] = value[1]
|
|
132
|
+
return values
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const item of value) {
|
|
137
|
+
Object.assign(
|
|
138
|
+
values,
|
|
139
|
+
collectDecodedAttributeValues(item, requestedKeyMap, activeKey)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return values
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (value && typeof value === "object") {
|
|
146
|
+
const record = value as Record<string, unknown>
|
|
147
|
+
if (typeof record.key === "string" && typeof record.value === "string") {
|
|
148
|
+
const displayKey = requestedKeyMap[record.key]
|
|
149
|
+
if (displayKey && isPrintableTextValue(record.value)) {
|
|
150
|
+
values[displayKey] = record.value
|
|
151
|
+
return values
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
156
|
+
const displayKey = requestedKeyMap[key] ?? activeKey
|
|
157
|
+
if (
|
|
158
|
+
displayKey &&
|
|
159
|
+
typeof nested === "string" &&
|
|
160
|
+
isPrintableTextValue(nested)
|
|
161
|
+
) {
|
|
162
|
+
values[displayKey] = nested
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
Object.assign(
|
|
166
|
+
values,
|
|
167
|
+
collectDecodedAttributeValues(nested, requestedKeyMap, displayKey)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return values
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractPrintableAttributeValues(
|
|
176
|
+
data: Uint8Array,
|
|
177
|
+
requestedKeys: string[]
|
|
178
|
+
): IdentityAttributeValues {
|
|
179
|
+
const text = new TextDecoder().decode(data)
|
|
180
|
+
const values: IdentityAttributeValues = {}
|
|
181
|
+
|
|
182
|
+
for (const requestedKey of requestedKeys) {
|
|
183
|
+
const start = text.indexOf(requestedKey)
|
|
184
|
+
if (start === -1) {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tail = text.slice(start + requestedKey.length)
|
|
189
|
+
const printableRuns = tail.match(/[\x20-\x7E]{2,512}/g) ?? []
|
|
190
|
+
const value = printableRuns.find(
|
|
191
|
+
(candidate) =>
|
|
192
|
+
isPrintableTextValue(candidate) &&
|
|
193
|
+
candidate !== requestedKey &&
|
|
194
|
+
!candidate.startsWith("openid:")
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if (value) {
|
|
198
|
+
values[identityAttributeDisplayKey(requestedKey)] = value.trim()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return values
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function identityAttributeDisplayKey(key: string): string {
|
|
206
|
+
return key.split(":").pop() || key
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isPrintableTextValue(value: string): boolean {
|
|
210
|
+
return value.length > 0 && value.length <= 512 && /^[\x20-\x7E]+$/.test(value)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function normalizeSignedIdentityAttributes(
|
|
214
|
+
attributes: SignedIdentityAttributes
|
|
215
|
+
): SignedIdentityAttributes {
|
|
216
|
+
return {
|
|
217
|
+
data: new Uint8Array(attributes.data),
|
|
218
|
+
signature: new Uint8Array(attributes.signature),
|
|
219
|
+
}
|
|
220
|
+
}
|