@0xsequence/wallet-wdk 3.0.2 → 3.0.3
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/sequence/handlers/authcode.d.ts.map +1 -1
- package/dist/sequence/handlers/authcode.js +6 -0
- package/dist/sequence/handlers/identity.d.ts +1 -0
- package/dist/sequence/handlers/identity.d.ts.map +1 -1
- package/dist/sequence/handlers/identity.js +3 -0
- package/dist/sequence/handlers/idtoken.d.ts +33 -0
- package/dist/sequence/handlers/idtoken.d.ts.map +1 -0
- package/dist/sequence/handlers/idtoken.js +110 -0
- package/dist/sequence/handlers/index.d.ts +1 -0
- package/dist/sequence/handlers/index.d.ts.map +1 -1
- package/dist/sequence/handlers/index.js +1 -0
- package/dist/sequence/manager.d.ts +20 -14
- package/dist/sequence/manager.d.ts.map +1 -1
- package/dist/sequence/manager.js +21 -3
- package/dist/sequence/sessions.d.ts.map +1 -1
- package/dist/sequence/sessions.js +5 -1
- package/dist/sequence/signers.d.ts.map +1 -1
- package/dist/sequence/signers.js +4 -0
- package/dist/sequence/types/signer.d.ts +1 -1
- package/dist/sequence/types/signer.js +1 -1
- package/dist/sequence/types/wallet.d.ts +1 -1
- package/dist/sequence/wallets.d.ts +7 -1
- package/dist/sequence/wallets.d.ts.map +1 -1
- package/dist/sequence/wallets.js +69 -7
- package/package.json +6 -6
- package/src/sequence/handlers/authcode.ts +6 -0
- package/src/sequence/handlers/identity.ts +4 -0
- package/src/sequence/handlers/idtoken.ts +140 -0
- package/src/sequence/handlers/index.ts +1 -0
- package/src/sequence/manager.ts +78 -29
- package/src/sequence/sessions.ts +7 -1
- package/src/sequence/signers.ts +5 -0
- package/src/sequence/types/signer.ts +1 -1
- package/src/sequence/types/wallet.ts +1 -1
- package/src/sequence/wallets.ts +88 -9
- package/test/authcode-pkce.test.ts +1 -1
- package/test/authcode.test.ts +2 -2
- package/test/identity-auth-dbs.test.ts +86 -2
- package/test/identity-signer.test.ts +1 -1
- package/test/idtoken.test.ts +327 -0
- package/test/sessions-idtoken.test.ts +97 -0
- package/test/signers-kindof.test.ts +22 -0
- package/test/wallets.test.ts +141 -1
package/src/sequence/wallets.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Config, Constants, Payload } from '@0xsequence/wallet-primitives'
|
|
|
3
3
|
import { Address, Hex, Provider, RpcTransport } from 'ox'
|
|
4
4
|
import { AuthCommitment } from '../dbs/auth-commitments.js'
|
|
5
5
|
import { AuthCodeHandler } from './handlers/authcode.js'
|
|
6
|
+
import { IdTokenHandler } from './handlers/idtoken.js'
|
|
6
7
|
import { MnemonicHandler } from './handlers/mnemonic.js'
|
|
7
8
|
import { OtpHandler } from './handlers/otp.js'
|
|
8
9
|
import { Shared } from './manager.js'
|
|
@@ -13,6 +14,37 @@ import { Wallet, WalletSelectionUiHandler } from './types/wallet.js'
|
|
|
13
14
|
import { PasskeysHandler } from './handlers/passkeys.js'
|
|
14
15
|
import type { PasskeySigner } from './passkeys-provider.js'
|
|
15
16
|
|
|
17
|
+
function getSignupHandlerKey(kind: SignupArgs['kind'] | StartSignUpWithRedirectArgs['kind'] | AuthCommitment['kind']) {
|
|
18
|
+
if (kind === 'google-pkce') {
|
|
19
|
+
return Kinds.LoginGoogle
|
|
20
|
+
}
|
|
21
|
+
if (kind.startsWith('custom-')) {
|
|
22
|
+
return kind
|
|
23
|
+
}
|
|
24
|
+
return 'login-' + kind
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSignerKindForSignup(kind: SignupArgs['kind'] | AuthCommitment['kind']) {
|
|
28
|
+
if (kind === 'google-id-token' || kind === 'google-pkce') {
|
|
29
|
+
return Kinds.LoginGoogle
|
|
30
|
+
}
|
|
31
|
+
if (kind.startsWith('custom-')) {
|
|
32
|
+
return kind
|
|
33
|
+
}
|
|
34
|
+
return ('login-' + kind) as string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getIdTokenSignupHandler(shared: Shared, kind: typeof Kinds.LoginGoogle | `custom-${string}`): IdTokenHandler {
|
|
38
|
+
const handler = shared.handlers.get(kind)
|
|
39
|
+
if (!handler) {
|
|
40
|
+
throw new Error('handler-not-registered')
|
|
41
|
+
}
|
|
42
|
+
if (!(handler instanceof IdTokenHandler)) {
|
|
43
|
+
throw new Error('handler-does-not-support-id-token')
|
|
44
|
+
}
|
|
45
|
+
return handler
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
export type StartSignUpWithRedirectArgs = {
|
|
17
49
|
kind: 'google-pkce' | 'apple' | `custom-${string}`
|
|
18
50
|
target: string
|
|
@@ -49,6 +81,11 @@ export type EmailOtpSignupArgs = CommonSignupArgs & {
|
|
|
49
81
|
email: string
|
|
50
82
|
}
|
|
51
83
|
|
|
84
|
+
export type IdTokenSignupArgs = CommonSignupArgs & {
|
|
85
|
+
kind: 'google-id-token' | `custom-${string}`
|
|
86
|
+
idToken: string
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
export type CompleteRedirectArgs = CommonSignupArgs & {
|
|
53
90
|
state: string
|
|
54
91
|
code: string
|
|
@@ -62,7 +99,12 @@ export type AuthCodeSignupArgs = CommonSignupArgs & {
|
|
|
62
99
|
isRedirect: boolean
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
export type SignupArgs =
|
|
102
|
+
export type SignupArgs =
|
|
103
|
+
| PasskeySignupArgs
|
|
104
|
+
| MnemonicSignupArgs
|
|
105
|
+
| EmailOtpSignupArgs
|
|
106
|
+
| IdTokenSignupArgs
|
|
107
|
+
| AuthCodeSignupArgs
|
|
66
108
|
|
|
67
109
|
export type LoginToWalletArgs = {
|
|
68
110
|
wallet: Address.Address
|
|
@@ -180,6 +222,7 @@ export interface WalletsInterface {
|
|
|
180
222
|
* - `kind: 'mnemonic'`: Uses a mnemonic phrase as the login credential.
|
|
181
223
|
* - `kind: 'passkey'`: Prompts the user to create a WebAuthn passkey.
|
|
182
224
|
* - `kind: 'email-otp'`: Initiates an OTP flow to the user's email.
|
|
225
|
+
* - `kind: 'google-id-token'`: Completes an OIDC ID token flow when Google is configured with `authMethod: 'id-token'`.
|
|
183
226
|
* - `kind: 'google-pkce' | 'apple'`: Completes an OAuth redirect flow.
|
|
184
227
|
* Common options like `noGuard` or `noRecovery` can customize the wallet's security features.
|
|
185
228
|
* @returns A promise that resolves to the address of the newly created wallet, or `undefined` if the sign-up was aborted.
|
|
@@ -361,7 +404,11 @@ export function isLoginToPasskeyArgs(args: LoginArgs): args is LoginToPasskeyArg
|
|
|
361
404
|
}
|
|
362
405
|
|
|
363
406
|
export function isAuthCodeArgs(args: SignupArgs): args is AuthCodeSignupArgs {
|
|
364
|
-
return '
|
|
407
|
+
return 'code' in args && 'commitment' in args
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function isIdTokenArgs(args: SignupArgs): args is IdTokenSignupArgs {
|
|
411
|
+
return 'idToken' in args
|
|
365
412
|
}
|
|
366
413
|
|
|
367
414
|
function buildCappedTree(members: { address: Address.Address; imageHash?: Hex.Hex }[]): Config.Topology {
|
|
@@ -674,9 +721,24 @@ export class Wallets implements WalletsInterface {
|
|
|
674
721
|
}
|
|
675
722
|
}
|
|
676
723
|
|
|
724
|
+
case 'google-id-token': {
|
|
725
|
+
const handler = getIdTokenSignupHandler(this.shared, Kinds.LoginGoogle)
|
|
726
|
+
const [signer, metadata] = await handler.completeAuth(args.idToken)
|
|
727
|
+
const loginEmail = metadata.email
|
|
728
|
+
this.shared.modules.logger.log('Created new id token signer:', signer.address)
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
signer,
|
|
732
|
+
extra: {
|
|
733
|
+
signerKind: Kinds.LoginGoogle,
|
|
734
|
+
},
|
|
735
|
+
loginEmail,
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
677
739
|
case 'google-pkce':
|
|
678
740
|
case 'apple': {
|
|
679
|
-
const handler = this.shared.handlers.get(
|
|
741
|
+
const handler = this.shared.handlers.get(getSignupHandlerKey(args.kind)) as AuthCodeHandler
|
|
680
742
|
if (!handler) {
|
|
681
743
|
throw new Error('handler-not-registered')
|
|
682
744
|
}
|
|
@@ -688,7 +750,7 @@ export class Wallets implements WalletsInterface {
|
|
|
688
750
|
return {
|
|
689
751
|
signer,
|
|
690
752
|
extra: {
|
|
691
|
-
signerKind:
|
|
753
|
+
signerKind: getSignerKindForSignup(args.kind),
|
|
692
754
|
},
|
|
693
755
|
loginEmail,
|
|
694
756
|
}
|
|
@@ -696,7 +758,18 @@ export class Wallets implements WalletsInterface {
|
|
|
696
758
|
}
|
|
697
759
|
|
|
698
760
|
if (args.kind.startsWith('custom-')) {
|
|
699
|
-
|
|
761
|
+
if (isIdTokenArgs(args)) {
|
|
762
|
+
const handler = getIdTokenSignupHandler(this.shared, args.kind)
|
|
763
|
+
const [signer, metadata] = await handler.completeAuth(args.idToken)
|
|
764
|
+
return {
|
|
765
|
+
signer,
|
|
766
|
+
extra: {
|
|
767
|
+
signerKind: args.kind,
|
|
768
|
+
},
|
|
769
|
+
loginEmail: metadata.email,
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
700
773
|
const handler = this.shared.handlers.get(args.kind) as AuthCodeHandler
|
|
701
774
|
if (!handler) {
|
|
702
775
|
throw new Error('handler-not-registered')
|
|
@@ -716,11 +789,14 @@ export class Wallets implements WalletsInterface {
|
|
|
716
789
|
}
|
|
717
790
|
|
|
718
791
|
async startSignUpWithRedirect(args: StartSignUpWithRedirectArgs) {
|
|
719
|
-
const kind = args.kind
|
|
720
|
-
const handler = this.shared.handlers.get(kind)
|
|
792
|
+
const kind = getSignupHandlerKey(args.kind)
|
|
793
|
+
const handler = this.shared.handlers.get(kind)
|
|
721
794
|
if (!handler) {
|
|
722
795
|
throw new Error('handler-not-registered')
|
|
723
796
|
}
|
|
797
|
+
if (!(handler instanceof AuthCodeHandler)) {
|
|
798
|
+
throw new Error('handler-does-not-support-redirect')
|
|
799
|
+
}
|
|
724
800
|
return handler.commitAuth(args.target, true)
|
|
725
801
|
}
|
|
726
802
|
|
|
@@ -742,11 +818,14 @@ export class Wallets implements WalletsInterface {
|
|
|
742
818
|
use4337: args.use4337,
|
|
743
819
|
})
|
|
744
820
|
} else {
|
|
745
|
-
const
|
|
746
|
-
const handler = this.shared.handlers.get(
|
|
821
|
+
const handlerKind = getSignupHandlerKey(commitment.kind)
|
|
822
|
+
const handler = this.shared.handlers.get(handlerKind)
|
|
747
823
|
if (!handler) {
|
|
748
824
|
throw new Error('handler-not-registered')
|
|
749
825
|
}
|
|
826
|
+
if (!(handler instanceof AuthCodeHandler)) {
|
|
827
|
+
throw new Error('handler-does-not-support-redirect')
|
|
828
|
+
}
|
|
750
829
|
|
|
751
830
|
await handler.completeAuth(commitment, args.code)
|
|
752
831
|
}
|
|
@@ -326,7 +326,7 @@ describe('AuthCodePkceHandler', () => {
|
|
|
326
326
|
|
|
327
327
|
describe('Integration and Edge Cases', () => {
|
|
328
328
|
it('Should have correct kind property', () => {
|
|
329
|
-
expect(handler.kind).toBe('login-google
|
|
329
|
+
expect(handler.kind).toBe('login-google')
|
|
330
330
|
})
|
|
331
331
|
|
|
332
332
|
it('Should handle redirect URI configuration', () => {
|
package/test/authcode.test.ts
CHANGED
|
@@ -188,7 +188,7 @@ describe('AuthCodeHandler', () => {
|
|
|
188
188
|
// === KIND GETTER ===
|
|
189
189
|
|
|
190
190
|
describe('kind getter', () => {
|
|
191
|
-
it('Should return login-google
|
|
191
|
+
it('Should return login-google for Google PKCE handler', () => {
|
|
192
192
|
const googleHandler = new AuthCodeHandler(
|
|
193
193
|
'google-pkce',
|
|
194
194
|
'https://accounts.google.com',
|
|
@@ -200,7 +200,7 @@ describe('AuthCodeHandler', () => {
|
|
|
200
200
|
mockAuthKeys,
|
|
201
201
|
)
|
|
202
202
|
|
|
203
|
-
expect(googleHandler.kind).toBe('login-google
|
|
203
|
+
expect(googleHandler.kind).toBe('login-google')
|
|
204
204
|
})
|
|
205
205
|
|
|
206
206
|
it('Should return login-apple for Apple handler', () => {
|
|
@@ -351,9 +351,48 @@ describe('Identity Authentication Databases', () => {
|
|
|
351
351
|
},
|
|
352
352
|
})
|
|
353
353
|
|
|
354
|
-
// Verify that Google
|
|
354
|
+
// Verify that Google is registered under the canonical signer kind while
|
|
355
|
+
// still using the PKCE flow by default.
|
|
355
356
|
const handlers = (manager as any).shared.handlers
|
|
356
|
-
expect(handlers.has('login-google
|
|
357
|
+
expect(handlers.has('login-google')).toBe(true)
|
|
358
|
+
expect(handlers.has('login-google-pkce')).toBe(false)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('Should register the Google ID token handler when configured explicitly', async () => {
|
|
362
|
+
manager = new Manager({
|
|
363
|
+
stateProvider: new State.Local.Provider(new State.Local.IndexedDbStore(`manager-google-idtoken-${Date.now()}`)),
|
|
364
|
+
networks: [
|
|
365
|
+
{
|
|
366
|
+
name: 'Test Network',
|
|
367
|
+
type: Network.NetworkType.MAINNET,
|
|
368
|
+
rpcUrl: LOCAL_RPC_URL,
|
|
369
|
+
chainId: Network.ChainId.ARBITRUM,
|
|
370
|
+
blockExplorer: { url: 'https://arbiscan.io' },
|
|
371
|
+
nativeCurrency: {
|
|
372
|
+
name: 'Ether',
|
|
373
|
+
symbol: 'ETH',
|
|
374
|
+
decimals: 18,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
relayers: [],
|
|
379
|
+
authCommitmentsDb,
|
|
380
|
+
authKeysDb,
|
|
381
|
+
identity: {
|
|
382
|
+
url: 'https://dev-identity.sequence-dev.app',
|
|
383
|
+
fetch: window.fetch,
|
|
384
|
+
google: {
|
|
385
|
+
enabled: true,
|
|
386
|
+
clientId: 'test-google-client-id',
|
|
387
|
+
authMethod: 'id-token',
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const handlers = (manager as any).shared.handlers
|
|
393
|
+
expect(handlers.has('login-google-id-token')).toBe(false)
|
|
394
|
+
expect(handlers.has('login-google')).toBe(true)
|
|
395
|
+
expect(handlers.has('login-google-pkce')).toBe(false)
|
|
357
396
|
})
|
|
358
397
|
|
|
359
398
|
it('Should use auth databases when email authentication is enabled', async () => {
|
|
@@ -424,5 +463,50 @@ describe('Identity Authentication Databases', () => {
|
|
|
424
463
|
const handlers = (manager as any).shared.handlers
|
|
425
464
|
expect(handlers.has('login-apple')).toBe(true)
|
|
426
465
|
})
|
|
466
|
+
|
|
467
|
+
it('Should register custom ID token providers without enabling redirect flow for them', async () => {
|
|
468
|
+
manager = new Manager({
|
|
469
|
+
stateProvider: new State.Local.Provider(new State.Local.IndexedDbStore(`manager-custom-idtoken-${Date.now()}`)),
|
|
470
|
+
networks: [
|
|
471
|
+
{
|
|
472
|
+
name: 'Test Network',
|
|
473
|
+
type: Network.NetworkType.MAINNET,
|
|
474
|
+
rpcUrl: LOCAL_RPC_URL,
|
|
475
|
+
chainId: Network.ChainId.ARBITRUM,
|
|
476
|
+
blockExplorer: { url: 'https://arbiscan.io' },
|
|
477
|
+
nativeCurrency: {
|
|
478
|
+
name: 'Ether',
|
|
479
|
+
symbol: 'ETH',
|
|
480
|
+
decimals: 18,
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
relayers: [],
|
|
485
|
+
authCommitmentsDb,
|
|
486
|
+
authKeysDb,
|
|
487
|
+
identity: {
|
|
488
|
+
url: 'https://dev-identity.sequence-dev.app',
|
|
489
|
+
fetch: window.fetch,
|
|
490
|
+
customProviders: [
|
|
491
|
+
{
|
|
492
|
+
kind: 'custom-google-native',
|
|
493
|
+
authMethod: 'id-token',
|
|
494
|
+
issuer: 'https://accounts.google.com',
|
|
495
|
+
clientId: 'test-google-client-id',
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
const handlers = (manager as any).shared.handlers
|
|
502
|
+
expect(handlers.has('custom-google-native')).toBe(true)
|
|
503
|
+
await expect(
|
|
504
|
+
manager.wallets.startSignUpWithRedirect({
|
|
505
|
+
kind: 'custom-google-native',
|
|
506
|
+
target: '/home',
|
|
507
|
+
metadata: {},
|
|
508
|
+
}),
|
|
509
|
+
).rejects.toThrow('handler-does-not-support-redirect')
|
|
510
|
+
})
|
|
427
511
|
})
|
|
428
512
|
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'
|
|
2
|
+
import { Address, Hex } from 'ox'
|
|
3
|
+
import { Network, Payload } from '@0xsequence/wallet-primitives'
|
|
4
|
+
import { IdentityInstrument, IdentityType } from '@0xsequence/identity-instrument'
|
|
5
|
+
import { IdTokenHandler, PromptIdTokenHandler } from '../src/sequence/handlers/idtoken.js'
|
|
6
|
+
import { Signatures } from '../src/sequence/signatures.js'
|
|
7
|
+
import * as Db from '../src/dbs/index.js'
|
|
8
|
+
import { IdentitySigner } from '../src/identity/signer.js'
|
|
9
|
+
import { BaseSignatureRequest } from '../src/sequence/types/signature-request.js'
|
|
10
|
+
import { Kinds } from '../src/sequence/types/signer.js'
|
|
11
|
+
|
|
12
|
+
describe('IdTokenHandler', () => {
|
|
13
|
+
let idTokenHandler: IdTokenHandler
|
|
14
|
+
let mockNitroInstrument: IdentityInstrument
|
|
15
|
+
let mockSignatures: Signatures
|
|
16
|
+
let mockAuthKeys: Db.AuthKeys
|
|
17
|
+
let mockIdentitySigner: IdentitySigner
|
|
18
|
+
let testWallet: Address.Address
|
|
19
|
+
let testRequest: BaseSignatureRequest
|
|
20
|
+
let mockPromptIdToken: Mock<PromptIdTokenHandler>
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks()
|
|
24
|
+
|
|
25
|
+
testWallet = '0x1234567890123456789012345678901234567890' as Address.Address
|
|
26
|
+
|
|
27
|
+
mockNitroInstrument = {
|
|
28
|
+
commitVerifier: vi.fn(),
|
|
29
|
+
completeAuth: vi.fn(),
|
|
30
|
+
} as unknown as IdentityInstrument
|
|
31
|
+
|
|
32
|
+
mockSignatures = {
|
|
33
|
+
addSignature: vi.fn(),
|
|
34
|
+
} as unknown as Signatures
|
|
35
|
+
|
|
36
|
+
mockAuthKeys = {
|
|
37
|
+
set: vi.fn(),
|
|
38
|
+
get: vi.fn(),
|
|
39
|
+
del: vi.fn(),
|
|
40
|
+
delBySigner: vi.fn(),
|
|
41
|
+
getBySigner: vi.fn(),
|
|
42
|
+
addListener: vi.fn(),
|
|
43
|
+
} as unknown as Db.AuthKeys
|
|
44
|
+
|
|
45
|
+
mockIdentitySigner = {
|
|
46
|
+
address: testWallet,
|
|
47
|
+
sign: vi.fn(),
|
|
48
|
+
} as unknown as IdentitySigner
|
|
49
|
+
|
|
50
|
+
testRequest = {
|
|
51
|
+
id: 'test-request-id',
|
|
52
|
+
envelope: {
|
|
53
|
+
wallet: testWallet,
|
|
54
|
+
chainId: Network.ChainId.ARBITRUM,
|
|
55
|
+
payload: Payload.fromMessage(Hex.fromString('Test message')),
|
|
56
|
+
},
|
|
57
|
+
} as BaseSignatureRequest
|
|
58
|
+
|
|
59
|
+
mockPromptIdToken = vi.fn()
|
|
60
|
+
|
|
61
|
+
idTokenHandler = new IdTokenHandler(
|
|
62
|
+
'google-id-token',
|
|
63
|
+
'https://accounts.google.com',
|
|
64
|
+
'test-google-client-id',
|
|
65
|
+
mockNitroInstrument,
|
|
66
|
+
mockSignatures,
|
|
67
|
+
mockAuthKeys,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
vi.spyOn(idTokenHandler as any, 'nitroCommitVerifier').mockResolvedValue({
|
|
71
|
+
verifier: 'unused-verifier',
|
|
72
|
+
loginHint: '',
|
|
73
|
+
challenge: '',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
vi.spyOn(idTokenHandler as any, 'nitroCompleteAuth').mockResolvedValue({
|
|
77
|
+
signer: mockIdentitySigner,
|
|
78
|
+
email: 'user@example.com',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
vi.resetAllMocks()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('Constructor', () => {
|
|
87
|
+
it('Should create IdTokenHandler with correct properties', () => {
|
|
88
|
+
const handler = new IdTokenHandler(
|
|
89
|
+
'google-id-token',
|
|
90
|
+
'https://accounts.google.com',
|
|
91
|
+
'test-google-client-id',
|
|
92
|
+
mockNitroInstrument,
|
|
93
|
+
mockSignatures,
|
|
94
|
+
mockAuthKeys,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(handler.signupKind).toBe('google-id-token')
|
|
98
|
+
expect(handler.issuer).toBe('https://accounts.google.com')
|
|
99
|
+
expect(handler.audience).toBe('test-google-client-id')
|
|
100
|
+
expect(handler.identityType).toBe(IdentityType.OIDC)
|
|
101
|
+
expect(handler.kind).toBe(Kinds.LoginGoogle)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('Should initialize without a registered UI callback', () => {
|
|
105
|
+
expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('UI Registration', () => {
|
|
110
|
+
it('Should register ID token UI callback', () => {
|
|
111
|
+
const unregister = idTokenHandler.registerUI(mockPromptIdToken)
|
|
112
|
+
|
|
113
|
+
expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
|
|
114
|
+
expect(typeof unregister).toBe('function')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('Should unregister UI callback when returned function is called', () => {
|
|
118
|
+
const unregister = idTokenHandler.registerUI(mockPromptIdToken)
|
|
119
|
+
expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
|
|
120
|
+
|
|
121
|
+
unregister()
|
|
122
|
+
|
|
123
|
+
expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('Should unregister UI callback directly', () => {
|
|
127
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
128
|
+
expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
|
|
129
|
+
|
|
130
|
+
idTokenHandler.unregisterUI()
|
|
131
|
+
|
|
132
|
+
expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('Should allow multiple registrations by overwriting the previous callback', () => {
|
|
136
|
+
const secondCallback = vi.fn()
|
|
137
|
+
|
|
138
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
139
|
+
expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
|
|
140
|
+
|
|
141
|
+
idTokenHandler.registerUI(secondCallback)
|
|
142
|
+
|
|
143
|
+
expect(idTokenHandler['onPromptIdToken']).toBe(secondCallback)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('completeAuth()', () => {
|
|
148
|
+
it('Should complete auth using an OIDC ID token challenge', async () => {
|
|
149
|
+
const idToken = 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.'
|
|
150
|
+
|
|
151
|
+
const [signer, metadata] = await idTokenHandler.completeAuth(idToken)
|
|
152
|
+
|
|
153
|
+
expect(idTokenHandler['nitroCommitVerifier']).toHaveBeenCalledWith(
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
issuer: 'https://accounts.google.com',
|
|
156
|
+
audience: 'test-google-client-id',
|
|
157
|
+
idToken,
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
expect(idTokenHandler['nitroCompleteAuth']).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
issuer: 'https://accounts.google.com',
|
|
163
|
+
audience: 'test-google-client-id',
|
|
164
|
+
idToken,
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
expect(signer).toBe(mockIdentitySigner)
|
|
168
|
+
expect(metadata).toEqual({ email: 'user@example.com' })
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('getSigner()', () => {
|
|
173
|
+
it('Should throw when UI is not registered', async () => {
|
|
174
|
+
await expect(idTokenHandler.getSigner()).rejects.toThrow('id-token-handler-ui-not-registered')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('Should acquire a signer by prompting for a fresh ID token', async () => {
|
|
178
|
+
const idToken = 'header.payload.signature'
|
|
179
|
+
const completeAuthSpy = vi
|
|
180
|
+
.spyOn(idTokenHandler, 'completeAuth')
|
|
181
|
+
.mockResolvedValue([mockIdentitySigner, { email: 'user@example.com' }])
|
|
182
|
+
|
|
183
|
+
mockPromptIdToken.mockImplementation(async (kind, respond) => {
|
|
184
|
+
expect(kind).toBe('google-id-token')
|
|
185
|
+
await respond(idToken)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
189
|
+
|
|
190
|
+
const result = await idTokenHandler.getSigner()
|
|
191
|
+
|
|
192
|
+
expect(result).toEqual({
|
|
193
|
+
signer: mockIdentitySigner,
|
|
194
|
+
email: 'user@example.com',
|
|
195
|
+
})
|
|
196
|
+
expect(completeAuthSpy).toHaveBeenCalledWith(idToken)
|
|
197
|
+
expect(mockPromptIdToken).toHaveBeenCalledWith('google-id-token', expect.any(Function))
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('Should surface authentication failures from completeAuth', async () => {
|
|
201
|
+
const error = new Error('Authentication failed')
|
|
202
|
+
vi.spyOn(idTokenHandler, 'completeAuth').mockRejectedValue(error)
|
|
203
|
+
|
|
204
|
+
mockPromptIdToken.mockImplementation(async (_kind, respond) => {
|
|
205
|
+
await respond('header.payload.signature')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
209
|
+
|
|
210
|
+
await expect(idTokenHandler.getSigner()).rejects.toThrow('Authentication failed')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('Should surface UI callback errors', async () => {
|
|
214
|
+
mockPromptIdToken.mockRejectedValue(new Error('UI callback failed'))
|
|
215
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
216
|
+
|
|
217
|
+
await expect(idTokenHandler.getSigner()).rejects.toThrow('UI callback failed')
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('status()', () => {
|
|
222
|
+
it('Should return ready status when an auth key signer is available', async () => {
|
|
223
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(mockIdentitySigner)
|
|
224
|
+
|
|
225
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
226
|
+
|
|
227
|
+
expect(status.status).toBe('ready')
|
|
228
|
+
expect(status.handler).toBe(idTokenHandler)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('Should sign the request when ready handle is invoked', async () => {
|
|
232
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(mockIdentitySigner)
|
|
233
|
+
const signSpy = vi.spyOn(idTokenHandler as any, 'sign').mockResolvedValue(undefined)
|
|
234
|
+
|
|
235
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
236
|
+
const handled = await (status as any).handle()
|
|
237
|
+
|
|
238
|
+
expect(handled).toBe(true)
|
|
239
|
+
expect(signSpy).toHaveBeenCalledWith(mockIdentitySigner, testRequest)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('Should return unavailable when no auth key signer exists and UI is not registered', async () => {
|
|
243
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
|
|
244
|
+
|
|
245
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
246
|
+
|
|
247
|
+
expect(status).toMatchObject({
|
|
248
|
+
address: testWallet,
|
|
249
|
+
handler: idTokenHandler,
|
|
250
|
+
status: 'unavailable',
|
|
251
|
+
reason: 'ui-not-registered',
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('Should return actionable when no auth key signer exists and UI is registered', async () => {
|
|
256
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
|
|
257
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
258
|
+
|
|
259
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
260
|
+
|
|
261
|
+
expect(status.status).toBe('actionable')
|
|
262
|
+
expect(status.address).toBe(testWallet)
|
|
263
|
+
expect(status.handler).toBe(idTokenHandler)
|
|
264
|
+
expect((status as any).message).toBe('request-id-token')
|
|
265
|
+
expect(typeof (status as any).handle).toBe('function')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('Should reacquire the signer when actionable handle is invoked', async () => {
|
|
269
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
|
|
270
|
+
const completeAuthSpy = vi
|
|
271
|
+
.spyOn(idTokenHandler, 'completeAuth')
|
|
272
|
+
.mockResolvedValue([mockIdentitySigner, { email: 'user@example.com' }])
|
|
273
|
+
|
|
274
|
+
mockPromptIdToken.mockImplementation(async (_kind, respond) => {
|
|
275
|
+
await respond('header.payload.signature')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
279
|
+
|
|
280
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
281
|
+
const handled = await (status as any).handle()
|
|
282
|
+
|
|
283
|
+
expect(handled).toBe(true)
|
|
284
|
+
expect(completeAuthSpy).toHaveBeenCalledWith('header.payload.signature')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('Should return false when actionable handle authentication fails', async () => {
|
|
288
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
|
|
289
|
+
vi.spyOn(idTokenHandler, 'completeAuth').mockRejectedValue(new Error('Authentication failed'))
|
|
290
|
+
|
|
291
|
+
mockPromptIdToken.mockImplementation(async (_kind, respond) => {
|
|
292
|
+
await respond('header.payload.signature')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
296
|
+
|
|
297
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
298
|
+
const handled = await (status as any).handle()
|
|
299
|
+
|
|
300
|
+
expect(handled).toBe(false)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('Should return false when actionable handle authenticates the wrong signer', async () => {
|
|
304
|
+
vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
|
|
305
|
+
const wrongSigner = '0x9999999999999999999999999999999999999999' as Address.Address
|
|
306
|
+
vi.spyOn(idTokenHandler, 'completeAuth').mockResolvedValue([
|
|
307
|
+
{
|
|
308
|
+
...mockIdentitySigner,
|
|
309
|
+
address: wrongSigner,
|
|
310
|
+
} as unknown as IdentitySigner,
|
|
311
|
+
{ email: 'other-user@example.com' },
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
mockPromptIdToken.mockImplementation(async (_kind, respond) => {
|
|
315
|
+
await respond('header.payload.signature')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
idTokenHandler.registerUI(mockPromptIdToken)
|
|
319
|
+
|
|
320
|
+
const status = await idTokenHandler.status(testWallet, undefined, testRequest)
|
|
321
|
+
const handled = await (status as any).handle()
|
|
322
|
+
|
|
323
|
+
expect(handled).toBe(false)
|
|
324
|
+
expect(mockAuthKeys.delBySigner).toHaveBeenCalledWith(wrongSigner)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
})
|