@0xsequence/wallet-wdk 3.0.2 → 3.0.4

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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/CHANGELOG.md +24 -0
  5. package/dist/sequence/handlers/authcode.d.ts.map +1 -1
  6. package/dist/sequence/handlers/authcode.js +6 -0
  7. package/dist/sequence/handlers/identity.d.ts +1 -0
  8. package/dist/sequence/handlers/identity.d.ts.map +1 -1
  9. package/dist/sequence/handlers/identity.js +3 -0
  10. package/dist/sequence/handlers/idtoken.d.ts +33 -0
  11. package/dist/sequence/handlers/idtoken.d.ts.map +1 -0
  12. package/dist/sequence/handlers/idtoken.js +113 -0
  13. package/dist/sequence/handlers/index.d.ts +1 -0
  14. package/dist/sequence/handlers/index.d.ts.map +1 -1
  15. package/dist/sequence/handlers/index.js +1 -0
  16. package/dist/sequence/manager.d.ts +23 -14
  17. package/dist/sequence/manager.d.ts.map +1 -1
  18. package/dist/sequence/manager.js +28 -4
  19. package/dist/sequence/sessions.d.ts.map +1 -1
  20. package/dist/sequence/sessions.js +5 -1
  21. package/dist/sequence/signers.d.ts.map +1 -1
  22. package/dist/sequence/signers.js +4 -0
  23. package/dist/sequence/types/signer.d.ts +1 -1
  24. package/dist/sequence/types/signer.js +1 -1
  25. package/dist/sequence/types/wallet.d.ts +1 -1
  26. package/dist/sequence/wallets.d.ts +7 -1
  27. package/dist/sequence/wallets.d.ts.map +1 -1
  28. package/dist/sequence/wallets.js +73 -7
  29. package/package.json +6 -6
  30. package/src/sequence/handlers/authcode.ts +6 -0
  31. package/src/sequence/handlers/identity.ts +4 -0
  32. package/src/sequence/handlers/idtoken.ts +146 -0
  33. package/src/sequence/handlers/index.ts +1 -0
  34. package/src/sequence/manager.ts +110 -43
  35. package/src/sequence/sessions.ts +7 -1
  36. package/src/sequence/signers.ts +5 -0
  37. package/src/sequence/types/signer.ts +1 -1
  38. package/src/sequence/types/wallet.ts +1 -1
  39. package/src/sequence/wallets.ts +98 -9
  40. package/test/authcode-pkce.test.ts +1 -1
  41. package/test/authcode.test.ts +2 -2
  42. package/test/identity-auth-dbs.test.ts +122 -2
  43. package/test/identity-signer.test.ts +1 -1
  44. package/test/idtoken.test.ts +343 -0
  45. package/test/sessions-idtoken.test.ts +98 -0
  46. package/test/signers-kindof.test.ts +22 -0
  47. package/test/wallets.test.ts +202 -1
@@ -0,0 +1,343 @@
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 normalize apple-id-token handlers to login-apple', () => {
105
+ const handler = new IdTokenHandler(
106
+ 'apple-id-token',
107
+ 'https://appleid.apple.com',
108
+ 'test-apple-client-id',
109
+ mockNitroInstrument,
110
+ mockSignatures,
111
+ mockAuthKeys,
112
+ )
113
+
114
+ expect(handler.signupKind).toBe('apple-id-token')
115
+ expect(handler.issuer).toBe('https://appleid.apple.com')
116
+ expect(handler.audience).toBe('test-apple-client-id')
117
+ expect(handler.kind).toBe(Kinds.LoginApple)
118
+ })
119
+
120
+ it('Should initialize without a registered UI callback', () => {
121
+ expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
122
+ })
123
+ })
124
+
125
+ describe('UI Registration', () => {
126
+ it('Should register ID token UI callback', () => {
127
+ const unregister = idTokenHandler.registerUI(mockPromptIdToken)
128
+
129
+ expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
130
+ expect(typeof unregister).toBe('function')
131
+ })
132
+
133
+ it('Should unregister UI callback when returned function is called', () => {
134
+ const unregister = idTokenHandler.registerUI(mockPromptIdToken)
135
+ expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
136
+
137
+ unregister()
138
+
139
+ expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
140
+ })
141
+
142
+ it('Should unregister UI callback directly', () => {
143
+ idTokenHandler.registerUI(mockPromptIdToken)
144
+ expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
145
+
146
+ idTokenHandler.unregisterUI()
147
+
148
+ expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
149
+ })
150
+
151
+ it('Should allow multiple registrations by overwriting the previous callback', () => {
152
+ const secondCallback = vi.fn()
153
+
154
+ idTokenHandler.registerUI(mockPromptIdToken)
155
+ expect(idTokenHandler['onPromptIdToken']).toBe(mockPromptIdToken)
156
+
157
+ idTokenHandler.registerUI(secondCallback)
158
+
159
+ expect(idTokenHandler['onPromptIdToken']).toBe(secondCallback)
160
+ })
161
+ })
162
+
163
+ describe('completeAuth()', () => {
164
+ it('Should complete auth using an OIDC ID token challenge', async () => {
165
+ const idToken = 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.'
166
+
167
+ const [signer, metadata] = await idTokenHandler.completeAuth(idToken)
168
+
169
+ expect(idTokenHandler['nitroCommitVerifier']).toHaveBeenCalledWith(
170
+ expect.objectContaining({
171
+ issuer: 'https://accounts.google.com',
172
+ audience: 'test-google-client-id',
173
+ idToken,
174
+ }),
175
+ )
176
+ expect(idTokenHandler['nitroCompleteAuth']).toHaveBeenCalledWith(
177
+ expect.objectContaining({
178
+ issuer: 'https://accounts.google.com',
179
+ audience: 'test-google-client-id',
180
+ idToken,
181
+ }),
182
+ )
183
+ expect(signer).toBe(mockIdentitySigner)
184
+ expect(metadata).toEqual({ email: 'user@example.com' })
185
+ })
186
+ })
187
+
188
+ describe('getSigner()', () => {
189
+ it('Should throw when UI is not registered', async () => {
190
+ await expect(idTokenHandler.getSigner()).rejects.toThrow('id-token-handler-ui-not-registered')
191
+ })
192
+
193
+ it('Should acquire a signer by prompting for a fresh ID token', async () => {
194
+ const idToken = 'header.payload.signature'
195
+ const completeAuthSpy = vi
196
+ .spyOn(idTokenHandler, 'completeAuth')
197
+ .mockResolvedValue([mockIdentitySigner, { email: 'user@example.com' }])
198
+
199
+ mockPromptIdToken.mockImplementation(async (kind, respond) => {
200
+ expect(kind).toBe('google-id-token')
201
+ await respond(idToken)
202
+ })
203
+
204
+ idTokenHandler.registerUI(mockPromptIdToken)
205
+
206
+ const result = await idTokenHandler.getSigner()
207
+
208
+ expect(result).toEqual({
209
+ signer: mockIdentitySigner,
210
+ email: 'user@example.com',
211
+ })
212
+ expect(completeAuthSpy).toHaveBeenCalledWith(idToken)
213
+ expect(mockPromptIdToken).toHaveBeenCalledWith('google-id-token', expect.any(Function))
214
+ })
215
+
216
+ it('Should surface authentication failures from completeAuth', async () => {
217
+ const error = new Error('Authentication failed')
218
+ vi.spyOn(idTokenHandler, 'completeAuth').mockRejectedValue(error)
219
+
220
+ mockPromptIdToken.mockImplementation(async (_kind, respond) => {
221
+ await respond('header.payload.signature')
222
+ })
223
+
224
+ idTokenHandler.registerUI(mockPromptIdToken)
225
+
226
+ await expect(idTokenHandler.getSigner()).rejects.toThrow('Authentication failed')
227
+ })
228
+
229
+ it('Should surface UI callback errors', async () => {
230
+ mockPromptIdToken.mockRejectedValue(new Error('UI callback failed'))
231
+ idTokenHandler.registerUI(mockPromptIdToken)
232
+
233
+ await expect(idTokenHandler.getSigner()).rejects.toThrow('UI callback failed')
234
+ })
235
+ })
236
+
237
+ describe('status()', () => {
238
+ it('Should return ready status when an auth key signer is available', async () => {
239
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(mockIdentitySigner)
240
+
241
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
242
+
243
+ expect(status.status).toBe('ready')
244
+ expect(status.handler).toBe(idTokenHandler)
245
+ })
246
+
247
+ it('Should sign the request when ready handle is invoked', async () => {
248
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(mockIdentitySigner)
249
+ const signSpy = vi.spyOn(idTokenHandler as any, 'sign').mockResolvedValue(undefined)
250
+
251
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
252
+ const handled = await (status as any).handle()
253
+
254
+ expect(handled).toBe(true)
255
+ expect(signSpy).toHaveBeenCalledWith(mockIdentitySigner, testRequest)
256
+ })
257
+
258
+ it('Should return unavailable when no auth key signer exists and UI is not registered', async () => {
259
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
260
+
261
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
262
+
263
+ expect(status).toMatchObject({
264
+ address: testWallet,
265
+ handler: idTokenHandler,
266
+ status: 'unavailable',
267
+ reason: 'ui-not-registered',
268
+ })
269
+ })
270
+
271
+ it('Should return actionable when no auth key signer exists and UI is registered', async () => {
272
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
273
+ idTokenHandler.registerUI(mockPromptIdToken)
274
+
275
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
276
+
277
+ expect(status.status).toBe('actionable')
278
+ expect(status.address).toBe(testWallet)
279
+ expect(status.handler).toBe(idTokenHandler)
280
+ expect((status as any).message).toBe('request-id-token')
281
+ expect(typeof (status as any).handle).toBe('function')
282
+ })
283
+
284
+ it('Should reacquire the signer when actionable handle is invoked', async () => {
285
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
286
+ const completeAuthSpy = vi
287
+ .spyOn(idTokenHandler, 'completeAuth')
288
+ .mockResolvedValue([mockIdentitySigner, { email: 'user@example.com' }])
289
+
290
+ mockPromptIdToken.mockImplementation(async (_kind, respond) => {
291
+ await respond('header.payload.signature')
292
+ })
293
+
294
+ idTokenHandler.registerUI(mockPromptIdToken)
295
+
296
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
297
+ const handled = await (status as any).handle()
298
+
299
+ expect(handled).toBe(true)
300
+ expect(completeAuthSpy).toHaveBeenCalledWith('header.payload.signature')
301
+ })
302
+
303
+ it('Should return false when actionable handle authentication fails', async () => {
304
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
305
+ vi.spyOn(idTokenHandler, 'completeAuth').mockRejectedValue(new Error('Authentication failed'))
306
+
307
+ mockPromptIdToken.mockImplementation(async (_kind, respond) => {
308
+ await respond('header.payload.signature')
309
+ })
310
+
311
+ idTokenHandler.registerUI(mockPromptIdToken)
312
+
313
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
314
+ const handled = await (status as any).handle()
315
+
316
+ expect(handled).toBe(false)
317
+ })
318
+
319
+ it('Should return false when actionable handle authenticates the wrong signer', async () => {
320
+ vi.spyOn(idTokenHandler as any, 'getAuthKeySigner').mockResolvedValue(undefined)
321
+ const wrongSigner = '0x9999999999999999999999999999999999999999' as Address.Address
322
+ vi.spyOn(idTokenHandler, 'completeAuth').mockResolvedValue([
323
+ {
324
+ ...mockIdentitySigner,
325
+ address: wrongSigner,
326
+ } as unknown as IdentitySigner,
327
+ { email: 'other-user@example.com' },
328
+ ])
329
+
330
+ mockPromptIdToken.mockImplementation(async (_kind, respond) => {
331
+ await respond('header.payload.signature')
332
+ })
333
+
334
+ idTokenHandler.registerUI(mockPromptIdToken)
335
+
336
+ const status = await idTokenHandler.status(testWallet, undefined, testRequest)
337
+ const handled = await (status as any).handle()
338
+
339
+ expect(handled).toBe(false)
340
+ expect(mockAuthKeys.delBySigner).toHaveBeenCalledWith(wrongSigner)
341
+ })
342
+ })
343
+ })
@@ -0,0 +1,98 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { Hash, Hex, Mnemonic, Secp256k1, Address as OxAddress } from 'ox'
3
+ import { Payload } from '@0xsequence/wallet-primitives'
4
+ import { newManager } from './constants.js'
5
+ import { Manager } from '../src/sequence/index.js'
6
+ import { Kinds } from '../src/sequence/types/signer.js'
7
+
8
+ describe('Sessions ID token attestation', () => {
9
+ let manager: Manager | undefined
10
+
11
+ afterEach(async () => {
12
+ await manager?.stop()
13
+ })
14
+
15
+ it('Should include issuer and audience hashes for google-id-token implicit session authorization', async () => {
16
+ manager = newManager({
17
+ identity: {
18
+ google: {
19
+ enabled: true,
20
+ clientId: 'test-google-client-id',
21
+ authMethod: 'id-token',
22
+ },
23
+ },
24
+ })
25
+
26
+ const wallet = await manager.wallets.signUp({
27
+ mnemonic: Mnemonic.random(Mnemonic.english),
28
+ kind: 'mnemonic',
29
+ noGuard: true,
30
+ })
31
+ expect(wallet).toBeDefined()
32
+
33
+ const signersModule = (manager as any).shared.modules.signers
34
+ vi.spyOn(signersModule, 'kindOf').mockResolvedValue(Kinds.LoginGoogle)
35
+
36
+ const sessionAddress = OxAddress.fromPublicKey(Secp256k1.getPublicKey({ privateKey: Secp256k1.randomPrivateKey() }))
37
+ const requestId = await manager.sessions.prepareAuthorizeImplicitSession(wallet!, sessionAddress, {
38
+ target: 'https://example.com',
39
+ applicationData: '0x1234',
40
+ })
41
+
42
+ const request = await manager.signatures.get(requestId)
43
+ expect(request.action).toBe('session-implicit-authorize')
44
+ expect(Payload.isSessionImplicitAuthorize(request.envelope.payload)).toBe(true)
45
+
46
+ if (!Payload.isSessionImplicitAuthorize(request.envelope.payload)) {
47
+ throw new Error('Expected session implicit authorize payload')
48
+ }
49
+
50
+ const attestation = request.envelope.payload.attestation
51
+ expect(Hex.fromBytes(attestation.issuerHash)).toBe(Hash.keccak256(Hex.fromString('https://accounts.google.com')))
52
+ expect(Hex.fromBytes(attestation.audienceHash)).toBe(Hash.keccak256(Hex.fromString('test-google-client-id')))
53
+ expect(Hex.fromBytes(attestation.applicationData)).toBe('0x1234')
54
+ expect(Hex.fromBytes(attestation.identityType)).toBe('0x00000002')
55
+ })
56
+
57
+ it('Should include issuer and audience hashes for apple implicit session authorization', async () => {
58
+ manager = newManager({
59
+ identity: {
60
+ apple: {
61
+ enabled: true,
62
+ clientId: 'test-apple-client-id',
63
+ authMethod: 'id-token',
64
+ },
65
+ },
66
+ })
67
+
68
+ const wallet = await manager.wallets.signUp({
69
+ mnemonic: Mnemonic.random(Mnemonic.english),
70
+ kind: 'mnemonic',
71
+ noGuard: true,
72
+ })
73
+ expect(wallet).toBeDefined()
74
+
75
+ const signersModule = (manager as any).shared.modules.signers
76
+ vi.spyOn(signersModule, 'kindOf').mockResolvedValue(Kinds.LoginApple)
77
+
78
+ const sessionAddress = OxAddress.fromPublicKey(Secp256k1.getPublicKey({ privateKey: Secp256k1.randomPrivateKey() }))
79
+ const requestId = await manager.sessions.prepareAuthorizeImplicitSession(wallet!, sessionAddress, {
80
+ target: 'https://example.com',
81
+ applicationData: '0x1234',
82
+ })
83
+
84
+ const request = await manager.signatures.get(requestId)
85
+ expect(request.action).toBe('session-implicit-authorize')
86
+ expect(Payload.isSessionImplicitAuthorize(request.envelope.payload)).toBe(true)
87
+
88
+ if (!Payload.isSessionImplicitAuthorize(request.envelope.payload)) {
89
+ throw new Error('Expected session implicit authorize payload')
90
+ }
91
+
92
+ const attestation = request.envelope.payload.attestation
93
+ expect(Hex.fromBytes(attestation.issuerHash)).toBe(Hash.keccak256(Hex.fromString('https://appleid.apple.com')))
94
+ expect(Hex.fromBytes(attestation.audienceHash)).toBe(Hash.keccak256(Hex.fromString('test-apple-client-id')))
95
+ expect(Hex.fromBytes(attestation.applicationData)).toBe('0x1234')
96
+ expect(Hex.fromBytes(attestation.identityType)).toBe('0x00000002')
97
+ })
98
+ })
@@ -37,4 +37,26 @@ describe('Signers.kindOf', () => {
37
37
  await signers.kindOf(wallet, '0x2222222222222222222222222222222222222222')
38
38
  expect(getWitnessFor).toHaveBeenCalledTimes(1)
39
39
  })
40
+
41
+ it('normalizes legacy Google PKCE signer kind to the canonical Google signer kind', async () => {
42
+ const getWitnessFor = vi.fn().mockResolvedValue({
43
+ payload: {
44
+ type: 'message',
45
+ message: '0x' + Buffer.from(JSON.stringify({ signerKind: 'login-google-pkce' }), 'utf8').toString('hex'),
46
+ },
47
+ })
48
+
49
+ const manager = newManager({
50
+ stateProvider: {
51
+ getWitnessFor,
52
+ getWitnessForSapient: vi.fn(),
53
+ } as any,
54
+ })
55
+
56
+ const signers = (manager as any).shared.modules.signers
57
+ const wallet = '0x1111111111111111111111111111111111111111'
58
+ const signer = '0x2222222222222222222222222222222222222222'
59
+
60
+ await expect(signers.kindOf(wallet, signer)).resolves.toBe(Kinds.LoginGoogle)
61
+ })
40
62
  })
@@ -1,8 +1,13 @@
1
- import { afterEach, describe, expect, it } from 'vitest'
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
  import { Manager, SignerActionable, SignerReady } from '../src/sequence/index.js'
3
3
  import { Mnemonic, Address } from 'ox'
4
4
  import { newManager } from './constants.js'
5
5
  import { Config, Constants, Network } from '@0xsequence/wallet-primitives'
6
+ import { AuthCodePkceHandler } from '../src/sequence/handlers/authcode-pkce.js'
7
+ import { IdTokenHandler } from '../src/sequence/handlers/idtoken.js'
8
+ import { IdentitySigner } from '../src/identity/signer.js'
9
+ import { MnemonicHandler } from '../src/sequence/handlers/mnemonic.js'
10
+ import { Kinds } from '../src/sequence/types/signer.js'
6
11
 
7
12
  describe('Wallets', () => {
8
13
  let manager: Manager | undefined
@@ -24,6 +29,202 @@ describe('Wallets', () => {
24
29
  await expect(manager.wallets.has(wallet!)).resolves.toBeTruthy()
25
30
  })
26
31
 
32
+ it('Should create a new wallet using google-id-token when Google ID token auth is enabled', async () => {
33
+ manager = newManager({
34
+ identity: {
35
+ google: {
36
+ enabled: true,
37
+ clientId: 'test-google-client-id',
38
+ authMethod: 'id-token',
39
+ },
40
+ },
41
+ })
42
+
43
+ const handler = (manager as any).shared.handlers.get(Kinds.LoginGoogle) as IdTokenHandler
44
+ const loginMnemonic = Mnemonic.random(Mnemonic.english)
45
+ const loginSigner = MnemonicHandler.toSigner(loginMnemonic)
46
+ if (!loginSigner) {
47
+ throw new Error('Failed to create login signer for test')
48
+ }
49
+
50
+ const completeAuthSpy = vi
51
+ .spyOn(handler, 'completeAuth')
52
+ .mockResolvedValue([loginSigner as unknown as IdentitySigner, { email: 'google-user@example.com' }])
53
+
54
+ const wallet = await manager.wallets.signUp({
55
+ kind: 'google-id-token',
56
+ idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
57
+ noGuard: true,
58
+ })
59
+
60
+ expect(wallet).toBeDefined()
61
+ expect(completeAuthSpy).toHaveBeenCalledWith('eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.')
62
+ await expect(manager.wallets.has(wallet!)).resolves.toBeTruthy()
63
+
64
+ const walletEntry = await manager.wallets.get(wallet!)
65
+ expect(walletEntry).toBeDefined()
66
+ expect(walletEntry!.loginType).toBe(Kinds.LoginGoogle)
67
+ expect(walletEntry!.loginEmail).toBe('google-user@example.com')
68
+
69
+ const configuration = await manager.wallets.getConfiguration(wallet!)
70
+ expect(configuration.login).toHaveLength(1)
71
+ expect(configuration.login[0]!.kind).toBe(Kinds.LoginGoogle)
72
+ })
73
+
74
+ it('Should create a new wallet using apple-id-token when Apple ID token auth is enabled', async () => {
75
+ manager = newManager({
76
+ identity: {
77
+ apple: {
78
+ enabled: true,
79
+ clientId: 'test-apple-client-id',
80
+ authMethod: 'id-token',
81
+ },
82
+ },
83
+ })
84
+
85
+ const handler = (manager as any).shared.handlers.get(Kinds.LoginApple) as IdTokenHandler
86
+ const loginMnemonic = Mnemonic.random(Mnemonic.english)
87
+ const loginSigner = MnemonicHandler.toSigner(loginMnemonic)
88
+ if (!loginSigner) {
89
+ throw new Error('Failed to create login signer for test')
90
+ }
91
+
92
+ const completeAuthSpy = vi
93
+ .spyOn(handler, 'completeAuth')
94
+ .mockResolvedValue([loginSigner as unknown as IdentitySigner, { email: 'apple-user@example.com' }])
95
+
96
+ const wallet = await manager.wallets.signUp({
97
+ kind: 'apple-id-token',
98
+ idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
99
+ noGuard: true,
100
+ })
101
+
102
+ expect(wallet).toBeDefined()
103
+ expect(completeAuthSpy).toHaveBeenCalledWith('eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.')
104
+ await expect(manager.wallets.has(wallet!)).resolves.toBeTruthy()
105
+
106
+ const walletEntry = await manager.wallets.get(wallet!)
107
+ expect(walletEntry).toBeDefined()
108
+ expect(walletEntry!.loginType).toBe(Kinds.LoginApple)
109
+ expect(walletEntry!.loginEmail).toBe('apple-user@example.com')
110
+
111
+ const configuration = await manager.wallets.getConfiguration(wallet!)
112
+ expect(configuration.login).toHaveLength(1)
113
+ expect(configuration.login[0]!.kind).toBe(Kinds.LoginApple)
114
+ })
115
+
116
+ it('Should register and unregister Google ID token UI callbacks through the manager', async () => {
117
+ manager = newManager({
118
+ identity: {
119
+ google: {
120
+ enabled: true,
121
+ clientId: 'test-google-client-id',
122
+ authMethod: 'id-token',
123
+ },
124
+ },
125
+ })
126
+
127
+ const handler = (manager as any).shared.handlers.get(Kinds.LoginGoogle) as IdTokenHandler
128
+ const promptIdToken = vi.fn()
129
+
130
+ const unregister = manager.registerIdTokenUI(promptIdToken)
131
+
132
+ expect(handler['onPromptIdToken']).toBe(promptIdToken)
133
+
134
+ unregister()
135
+
136
+ expect(handler['onPromptIdToken']).toBeUndefined()
137
+ })
138
+
139
+ it('Should keep Google PKCE redirect flow as the default when authMethod is not specified', async () => {
140
+ manager = newManager({
141
+ identity: {
142
+ google: {
143
+ enabled: true,
144
+ clientId: 'test-google-client-id',
145
+ },
146
+ },
147
+ })
148
+
149
+ const handler = (manager as any).shared.handlers.get(Kinds.LoginGoogle) as AuthCodePkceHandler
150
+ expect(handler).toBeInstanceOf(AuthCodePkceHandler)
151
+
152
+ const commitAuthSpy = vi
153
+ .spyOn(handler, 'commitAuth')
154
+ .mockResolvedValue('https://accounts.google.com/o/oauth2/v2/auth?state=test-state')
155
+
156
+ const url = await manager.wallets.startSignUpWithRedirect({
157
+ kind: 'google-pkce',
158
+ target: '/auth/return',
159
+ metadata: {},
160
+ })
161
+
162
+ expect(url).toBe('https://accounts.google.com/o/oauth2/v2/auth?state=test-state')
163
+ expect(commitAuthSpy).toHaveBeenCalledWith('/auth/return', true)
164
+ })
165
+
166
+ it('Should reject google-id-token signup when Google is configured for redirect auth', async () => {
167
+ manager = newManager({
168
+ identity: {
169
+ google: {
170
+ enabled: true,
171
+ clientId: 'test-google-client-id',
172
+ },
173
+ },
174
+ })
175
+
176
+ await expect(
177
+ manager.wallets.signUp({
178
+ kind: 'google-id-token',
179
+ idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
180
+ noGuard: true,
181
+ }),
182
+ ).rejects.toThrow('handler-does-not-support-id-token')
183
+ })
184
+
185
+ it('Should reject apple-id-token signup when Apple is configured for redirect auth', async () => {
186
+ manager = newManager({
187
+ identity: {
188
+ apple: {
189
+ enabled: true,
190
+ clientId: 'test-apple-client-id',
191
+ },
192
+ },
193
+ })
194
+
195
+ await expect(
196
+ manager.wallets.signUp({
197
+ kind: 'apple-id-token',
198
+ idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
199
+ noGuard: true,
200
+ }),
201
+ ).rejects.toThrow('handler-does-not-support-id-token')
202
+ })
203
+
204
+ it('Should reject custom ID token signup when the provider uses redirect auth', async () => {
205
+ manager = newManager({
206
+ identity: {
207
+ customProviders: [
208
+ {
209
+ kind: 'custom-oidc',
210
+ authMethod: 'authcode',
211
+ issuer: 'https://issuer.example.com',
212
+ oauthUrl: 'https://issuer.example.com/oauth/authorize',
213
+ clientId: 'test-custom-client-id',
214
+ },
215
+ ],
216
+ },
217
+ })
218
+
219
+ await expect(
220
+ manager.wallets.signUp({
221
+ kind: 'custom-oidc',
222
+ idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
223
+ noGuard: true,
224
+ }),
225
+ ).rejects.toThrow('handler-does-not-support-id-token')
226
+ })
227
+
27
228
  it('Should get a specific wallet by address', async () => {
28
229
  manager = newManager()
29
230
  const mnemonic = Mnemonic.random(Mnemonic.english)