accounts 0.16.2 → 0.17.1

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 (73) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -4
  3. package/dist/cli/Provider.d.ts +1 -1
  4. package/dist/cli/Provider.d.ts.map +1 -1
  5. package/dist/cli/Provider.js +3 -3
  6. package/dist/cli/Provider.js.map +1 -1
  7. package/dist/cli/adapter.d.ts +13 -8
  8. package/dist/cli/adapter.d.ts.map +1 -1
  9. package/dist/cli/adapter.js +92 -303
  10. package/dist/cli/adapter.js.map +1 -1
  11. package/dist/core/Schema.d.ts +8 -0
  12. package/dist/core/Schema.d.ts.map +1 -1
  13. package/dist/core/adapters/deviceCode/deviceCode.d.ts +59 -0
  14. package/dist/core/adapters/deviceCode/deviceCode.d.ts.map +1 -0
  15. package/dist/core/adapters/deviceCode/deviceCode.js +93 -0
  16. package/dist/core/adapters/deviceCode/deviceCode.js.map +1 -0
  17. package/dist/core/adapters/deviceCode/index.d.ts +3 -0
  18. package/dist/core/adapters/deviceCode/index.d.ts.map +1 -0
  19. package/dist/core/adapters/deviceCode/index.js +3 -0
  20. package/dist/core/adapters/deviceCode/index.js.map +1 -0
  21. package/dist/core/adapters/deviceCode/tempoWallet.d.ts +9 -0
  22. package/dist/core/adapters/deviceCode/tempoWallet.d.ts.map +1 -0
  23. package/dist/core/adapters/deviceCode/tempoWallet.js +11 -0
  24. package/dist/core/adapters/deviceCode/tempoWallet.js.map +1 -0
  25. package/dist/core/adapters/internal/fromRequest.d.ts +10 -0
  26. package/dist/core/adapters/internal/fromRequest.d.ts.map +1 -1
  27. package/dist/core/adapters/internal/fromRequest.js +4 -2
  28. package/dist/core/adapters/internal/fromRequest.js.map +1 -1
  29. package/dist/core/zod/rpc.d.ts +5 -0
  30. package/dist/core/zod/rpc.d.ts.map +1 -1
  31. package/dist/core/zod/rpc.js +1 -0
  32. package/dist/core/zod/rpc.js.map +1 -1
  33. package/dist/server/CliAuth.d.ts +69 -1
  34. package/dist/server/CliAuth.d.ts.map +1 -1
  35. package/dist/server/CliAuth.js +6 -1
  36. package/dist/server/CliAuth.js.map +1 -1
  37. package/dist/server/Handler.d.ts +2 -1
  38. package/dist/server/Handler.d.ts.map +1 -1
  39. package/dist/server/Handler.js +2 -1
  40. package/dist/server/Handler.js.map +1 -1
  41. package/dist/server/index.d.ts +1 -0
  42. package/dist/server/index.d.ts.map +1 -1
  43. package/dist/server/index.js +1 -0
  44. package/dist/server/index.js.map +1 -1
  45. package/dist/server/internal/handlers/codeAuth.d.ts +1 -0
  46. package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -1
  47. package/dist/server/internal/handlers/codeAuth.js +1 -0
  48. package/dist/server/internal/handlers/codeAuth.js.map +1 -1
  49. package/dist/server/internal/handlers/deviceCode.d.ts +60 -0
  50. package/dist/server/internal/handlers/deviceCode.d.ts.map +1 -0
  51. package/dist/server/internal/handlers/deviceCode.js +218 -0
  52. package/dist/server/internal/handlers/deviceCode.js.map +1 -0
  53. package/package.json +7 -1
  54. package/src/cli/Provider.localnet.test.ts +414 -470
  55. package/src/cli/Provider.test-d.ts +4 -3
  56. package/src/cli/Provider.ts +6 -6
  57. package/src/cli/adapter.ts +111 -380
  58. package/src/core/Provider.test.ts +48 -0
  59. package/src/core/adapters/deviceCode/deviceCode.localnet.test.ts +226 -0
  60. package/src/core/adapters/deviceCode/deviceCode.test-d.ts +19 -0
  61. package/src/core/adapters/deviceCode/deviceCode.ts +157 -0
  62. package/src/core/adapters/deviceCode/index.ts +2 -0
  63. package/src/core/adapters/deviceCode/tempoWallet.ts +17 -0
  64. package/src/core/adapters/internal/fromRequest.ts +115 -88
  65. package/src/core/zod/rpc.ts +1 -0
  66. package/src/server/CliAuth.test.ts +29 -18
  67. package/src/server/CliAuth.ts +10 -2
  68. package/src/server/Handler.test-d.ts +19 -33
  69. package/src/server/Handler.ts +2 -1
  70. package/src/server/index.ts +1 -0
  71. package/src/server/internal/handlers/codeAuth.ts +1 -0
  72. package/src/server/internal/handlers/deviceCode.test.ts +140 -0
  73. package/src/server/internal/handlers/deviceCode.ts +323 -0
@@ -1,16 +1,18 @@
1
1
  import { mkdtemp } from 'node:fs/promises'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
- import { Address, Hex, PublicKey } from 'ox'
4
+ import type { Address, Hex } from 'ox'
5
+ import { Hex as ox_Hex } from 'ox'
5
6
  import { KeyAuthorization } from 'ox/tempo'
6
- import { type Address as ViemAddress, parseUnits } from 'viem'
7
+ import { type Address as ViemAddress, hashMessage, parseUnits } from 'viem'
7
8
  import { Actions, Addresses } from 'viem/tempo'
8
- import { describe, expect, test } from 'vp/test'
9
- import * as z from 'zod/mini'
9
+ import { describe, expect, test, vi } from 'vp/test'
10
+ import type * as z from 'zod/mini'
10
11
 
11
12
  import { accounts, chain, getClient } from '../../test/config.js'
12
- import { createServer } from '../../test/utils.js'
13
- import * as CliAuth from '../server/CliAuth.js'
13
+ import { createDeviceCodeHost, submitVerify } from '../../test/deviceCode.js'
14
+ import { createJsonStorage, createServer } from '../../test/utils.js'
15
+ import type * as Rpc from '../core/zod/rpc.js'
14
16
  import * as Handler from '../server/Handler.js'
15
17
  import * as Provider from './Provider.js'
16
18
  import * as Storage from './storage.js'
@@ -21,43 +23,12 @@ const accessKey_2 = accounts[2]!
21
23
  const expiry = Math.floor(Date.now() / 1000) + 3_600
22
24
  const expiry_2 = expiry + 60
23
25
 
24
- async function authorize(
25
- code: string,
26
- options: {
27
- accessKey?: typeof accessKey | undefined
28
- expiry?: number | undefined
29
- } = {},
30
- ) {
31
- const { accessKey: key = accessKey, expiry: expiry_ = expiry } = options
32
-
33
- const signed = await root.signKeyAuthorization(
34
- {
35
- accessKeyAddress: key.address,
36
- keyType: key.keyType,
37
- },
38
- {
39
- chainId: BigInt(chain.id),
40
- expiry: expiry_,
41
- },
42
- )
43
- const keyAuthorization = KeyAuthorization.toRpc(signed)
44
-
45
- return z.encode(CliAuth.authorizeRequest, {
46
- accountAddress: root.address,
47
- code,
48
- keyAuthorization: z.decode(CliAuth.keyAuthorization, {
49
- ...keyAuthorization,
50
- address: keyAuthorization.keyId,
51
- }),
52
- })
53
- }
54
-
55
26
  function connectRequest(
56
27
  options: {
57
28
  accessKey?: typeof accessKey | undefined
58
29
  expiry?: number | undefined
59
30
  method?: 'login' | 'register' | undefined
60
- showDeposit?: z.output<typeof CliAuth.createRequest>['showDeposit'] | undefined
31
+ showDeposit?: z.output<typeof Rpc.wallet_connect.showDeposit> | undefined
61
32
  } = {},
62
33
  ) {
63
34
  const { accessKey: key = accessKey, expiry: expiry_ = expiry, method, showDeposit } = options
@@ -80,104 +51,6 @@ function connectRequest(
80
51
  } as const
81
52
  }
82
53
 
83
- function createHandler() {
84
- let random = 0
85
-
86
- return Handler.codeAuth({
87
- path: '/cli-auth',
88
- chains: [chain],
89
- policy: {
90
- update({ limits }) {
91
- return { limits }
92
- },
93
- validate({ expiry: requestedExpiry, limits }) {
94
- return {
95
- expiry: requestedExpiry ?? expiry,
96
- ...(limits ? { limits } : {}),
97
- }
98
- },
99
- },
100
- random: () => {
101
- const out = new Uint8Array(Array.from({ length: 8 }, (_, i) => random + i))
102
- random += 8
103
- return out
104
- },
105
- })
106
- }
107
-
108
- async function authorizePending(serverUrl: string, code: string) {
109
- const response = await fetch(`${serverUrl}/cli-auth/pending/${code}`)
110
- const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
111
- if (pending.action === 'updateAccessKey') throw new Error('Expected authorization request.')
112
- const signed = await root.signKeyAuthorization(
113
- {
114
- accessKeyAddress: Address.fromPublicKey(PublicKey.from(pending.pubKey)),
115
- keyType: pending.keyType,
116
- },
117
- {
118
- chainId: pending.chainId,
119
- expiry: pending.expiry,
120
- ...(pending.limits ? { limits: pending.limits } : {}),
121
- },
122
- )
123
- const keyAuthorization = KeyAuthorization.toRpc(signed)
124
-
125
- return z.encode(CliAuth.authorizeRequest, {
126
- accountAddress: root.address,
127
- code,
128
- keyAuthorization: z.decode(CliAuth.keyAuthorization, {
129
- ...keyAuthorization,
130
- address: keyAuthorization.keyId,
131
- }),
132
- })
133
- }
134
-
135
- async function completePending(serverUrl: string, code: string) {
136
- const response = await fetch(`${serverUrl}/cli-auth/pending/${code}`)
137
- const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
138
- if (pending.action !== 'updateAccessKey') return authorizePending(serverUrl, code)
139
-
140
- if (pending.keyAuthorization) {
141
- const signed = await root.signKeyAuthorization(
142
- {
143
- accessKeyAddress: pending.accessKeyAddress,
144
- keyType: pending.keyAuthorization.keyType,
145
- },
146
- {
147
- chainId: pending.chainId,
148
- expiry: pending.keyAuthorization.expiry,
149
- limits: pending.limits,
150
- },
151
- )
152
- const replacement = KeyAuthorization.toRpc(signed)
153
- return z.encode(CliAuth.authorizeRequest, {
154
- action: 'updateAccessKey',
155
- accountAddress: root.address,
156
- chainId: pending.chainId,
157
- code,
158
- keyAuthorization: z.decode(CliAuth.keyAuthorization, {
159
- ...replacement,
160
- address: replacement.keyId,
161
- }),
162
- })
163
- }
164
-
165
- const client = getClient({ account: root })
166
- for (const item of pending.limits)
167
- await Actions.accessKey.updateLimitSync(client, {
168
- accessKey: pending.accessKeyAddress,
169
- limit: item.limit,
170
- token: item.token,
171
- })
172
-
173
- return z.encode(CliAuth.authorizeRequest, {
174
- action: 'updateAccessKey',
175
- accountAddress: root.address,
176
- chainId: pending.chainId,
177
- code,
178
- })
179
- }
180
-
181
54
  async function createStoragePath() {
182
55
  return join(await mkdtemp(join(tmpdir(), 'accounts-cli-')), 'store.json')
183
56
  }
@@ -214,25 +87,30 @@ const transferCall = Actions.token.transfer.call({
214
87
  amount: parseUnits('1', 6),
215
88
  })
216
89
 
90
+ function connectCapabilities(host: ReturnType<typeof createDeviceCodeHost>, index: number) {
91
+ const params = host.requests()[index]!.params as readonly {
92
+ capabilities?:
93
+ | NonNullable<Rpc.wallet_connect.Decoded['params']>[number]['capabilities']
94
+ | undefined
95
+ }[]
96
+ return params[0]?.capabilities
97
+ }
98
+
217
99
  describe('Provider.create', () => {
218
- test('default: bootstraps wallet_connect through the device code flow', async () => {
219
- const handler = createHandler()
220
- const server = await createServer(handler.listener)
100
+ test('default: bootstraps wallet_connect through the device-code flow', async () => {
101
+ const host = createDeviceCodeHost()
102
+ const server = await createServer(host.listener)
221
103
  const opened: string[] = []
222
104
 
223
105
  try {
224
106
  const provider = Provider.create({
225
107
  chains: [chain],
226
- open: async (url) => {
108
+ open: async (url, prompt) => {
227
109
  opened.push(url)
228
- const code = new URL(url).searchParams.get('code')!
229
- await fetch(`${server.url}/cli-auth`, {
230
- body: JSON.stringify(await authorize(code)),
231
- headers: { 'content-type': 'application/json' },
232
- method: 'POST',
233
- })
110
+ await submitVerify(prompt)
234
111
  },
235
- host: `${server.url}/cli-auth`,
112
+ host: `${server.url}/auth/device`,
113
+ storage: createJsonStorage(),
236
114
  })
237
115
 
238
116
  const result = await provider.request(connectRequest())
@@ -246,22 +124,25 @@ describe('Provider.create', () => {
246
124
  }
247
125
  : undefined
248
126
 
127
+ expect(opened).toHaveLength(1)
128
+ expect(opened[0]).toMatch(
129
+ new RegExp(`^${server.url}/auth/device/verify\\?user_code=[A-Z]{4}-[A-Z]{4}$`),
130
+ )
249
131
  expect({
250
132
  account: {
251
133
  address: account.address,
252
134
  capabilities: keyAuthorization ? { keyAuthorization } : {},
253
135
  },
254
- opened: opened.map((url) => url.replace(server.url, 'http://service')),
255
136
  }).toMatchInlineSnapshot(`
256
137
  {
257
138
  "account": {
258
139
  "address": "${root.address}",
259
140
  "capabilities": {
260
141
  "keyAuthorization": {
261
- "address": "${accessKey.address}",
262
- "chainId": "${Hex.fromNumber(chain.id)}",
263
- "expiry": "${Hex.fromNumber(expiry)}",
264
- "keyId": "${accessKey.address}",
142
+ "address": "${accessKey.address.toLowerCase()}",
143
+ "chainId": "${ox_Hex.fromNumber(chain.id)}",
144
+ "expiry": "${ox_Hex.fromNumber(expiry)}",
145
+ "keyId": "${accessKey.address.toLowerCase()}",
265
146
  "keyType": "secp256k1",
266
147
  "signature": {
267
148
  "type": "secp256k1",
@@ -269,36 +150,132 @@ describe('Provider.create', () => {
269
150
  },
270
151
  },
271
152
  },
272
- "opened": [
273
- "http://service/cli-auth?code=ABCDEFGH",
153
+ }
154
+ `)
155
+ } finally {
156
+ await server.closeAsync()
157
+ }
158
+ })
159
+
160
+ test('behavior: preserves wallet_connect authorization capabilities', async () => {
161
+ const host = createDeviceCodeHost()
162
+ const server = await createServer(host.listener)
163
+ let listener: Parameters<typeof createServer>[0] | undefined
164
+ const authServer = await createServer((request, response) => {
165
+ if (!listener) {
166
+ const auth = Handler.auth({ origin: authServer.url })
167
+ listener = Handler.compose([auth], { path: '/auth' }).listener
168
+ }
169
+ return listener(request, response)
170
+ })
171
+
172
+ try {
173
+ const provider = Provider.create({
174
+ chains: [chain],
175
+ open: async (_url, prompt) => {
176
+ await submitVerify(prompt)
177
+ },
178
+ host: `${server.url}/auth/device`,
179
+ storage: createJsonStorage(),
180
+ })
181
+
182
+ const result = await provider.request({
183
+ method: 'wallet_connect',
184
+ params: [
185
+ {
186
+ capabilities: {
187
+ auth: {
188
+ resources: [
189
+ 'urn:nanocodex:memory:read',
190
+ 'urn:nanocodex:connector:github:repo:read',
191
+ ],
192
+ returnToken: true,
193
+ url: `${authServer.url}/auth`,
194
+ },
195
+ authorizeAccessKey: {
196
+ expiry,
197
+ keyType: accessKey.keyType,
198
+ publicKey: accessKey.publicKey,
199
+ scopes: [
200
+ {
201
+ address: Addresses.pathUsd,
202
+ selector: 'transfer(address,uint256)',
203
+ },
204
+ ],
205
+ },
206
+ identity: { email: true },
207
+ method: 'login',
208
+ },
209
+ },
210
+ ],
211
+ })
212
+
213
+ const capabilities = connectCapabilities(host, 0)
214
+ const connected = result.accounts[0]!.capabilities
215
+ const message = connected.personalSign?.message
216
+ expect({
217
+ auth: capabilities?.auth,
218
+ authToken: connected.auth?.token ? 'present' : 'missing',
219
+ identity: capabilities?.identity,
220
+ personalSign: {
221
+ keyAuthorization: connected.personalSign?.keyAuthorization ? 'present' : 'missing',
222
+ message: message ? 'present' : 'missing',
223
+ signature: connected.signature ? 'present' : 'missing',
224
+ witness:
225
+ message && connected.personalSign?.keyAuthorization
226
+ ? KeyAuthorization.deserialize(connected.personalSign.keyAuthorization).witness ===
227
+ hashMessage(message)
228
+ : false,
229
+ },
230
+ scopes: capabilities?.authorizeAccessKey?.scopes,
231
+ }).toMatchInlineSnapshot(`
232
+ {
233
+ "auth": {
234
+ "challenge": "${authServer.url}/auth/challenge",
235
+ "logout": "${authServer.url}/auth/logout",
236
+ "resources": [
237
+ "urn:nanocodex:memory:read",
238
+ "urn:nanocodex:connector:github:repo:read",
239
+ ],
240
+ "returnToken": true,
241
+ "verify": "${authServer.url}/auth",
242
+ },
243
+ "authToken": "present",
244
+ "identity": {
245
+ "email": true,
246
+ },
247
+ "personalSign": {
248
+ "keyAuthorization": "present",
249
+ "message": "present",
250
+ "signature": "present",
251
+ "witness": true,
252
+ },
253
+ "scopes": [
254
+ {
255
+ "address": "${Addresses.pathUsd}",
256
+ "selector": "transfer(address,uint256)",
257
+ },
274
258
  ],
275
259
  }
276
260
  `)
277
261
  } finally {
262
+ await authServer.closeAsync()
278
263
  await server.closeAsync()
279
264
  }
280
265
  })
281
266
 
282
- test('behavior: forwards showDeposit through registration device code requests', async () => {
283
- const handler = createHandler()
284
- const server = await createServer(handler.listener)
285
- const pendingShowDeposit: z.output<typeof CliAuth.pendingResponse>['showDeposit'][] = []
267
+ test('behavior: forwards showDeposit through registration device-code requests', async () => {
268
+ const host = createDeviceCodeHost()
269
+ const server = await createServer(host.listener)
286
270
 
287
271
  try {
288
272
  const provider = Provider.create({
289
273
  chains: [chain],
290
- open: async (url) => {
291
- const code = new URL(url).searchParams.get('code')!
292
- const response = await fetch(`${server.url}/cli-auth/pending/${code}`)
293
- const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
294
- pendingShowDeposit.push(pending.showDeposit)
295
- await fetch(`${server.url}/cli-auth`, {
296
- body: JSON.stringify(await authorizePending(server.url, code)),
297
- headers: { 'content-type': 'application/json' },
298
- method: 'POST',
299
- })
274
+ open: async (_url, prompt) => {
275
+ await submitVerify(prompt)
300
276
  },
301
- host: `${server.url}/cli-auth`,
277
+ host: `${server.url}/auth/device`,
278
+ storage: createJsonStorage(),
302
279
  })
303
280
 
304
281
  await provider.request(
@@ -313,82 +290,56 @@ describe('Provider.create', () => {
313
290
  }),
314
291
  )
315
292
 
316
- expect(pendingShowDeposit).toMatchInlineSnapshot(`
317
- [
318
- {
319
- "amount": "50",
320
- "displayName": "DoorDash",
321
- "on": "register",
322
- "token": "USDC",
323
- },
324
- ]
293
+ expect(connectCapabilities(host, 0)?.showDeposit).toMatchInlineSnapshot(`
294
+ {
295
+ "amount": "50",
296
+ "displayName": "DoorDash",
297
+ "on": "register",
298
+ "token": "USDC",
299
+ }
325
300
  `)
326
301
  } finally {
327
302
  await server.closeAsync()
328
303
  }
329
304
  })
330
305
 
331
- test('behavior: forwards showDeposit through login device code requests', async () => {
332
- const handler = createHandler()
333
- const server = await createServer(handler.listener)
334
- const pendingShowDeposit: z.output<typeof CliAuth.pendingResponse>['showDeposit'][] = []
306
+ test('behavior: forwards showDeposit through login device-code requests', async () => {
307
+ const host = createDeviceCodeHost()
308
+ const server = await createServer(host.listener)
335
309
 
336
310
  try {
337
311
  const provider = Provider.create({
338
312
  chains: [chain],
339
- open: async (url) => {
340
- const code = new URL(url).searchParams.get('code')!
341
- const response = await fetch(`${server.url}/cli-auth/pending/${code}`)
342
- const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
343
- pendingShowDeposit.push(pending.showDeposit)
344
- await fetch(`${server.url}/cli-auth`, {
345
- body: JSON.stringify(await authorizePending(server.url, code)),
346
- headers: { 'content-type': 'application/json' },
347
- method: 'POST',
348
- })
313
+ open: async (_url, prompt) => {
314
+ await submitVerify(prompt)
349
315
  },
350
- host: `${server.url}/cli-auth`,
316
+ host: `${server.url}/auth/device`,
317
+ storage: createJsonStorage(),
351
318
  })
352
319
 
353
- await provider.request(
354
- connectRequest({
355
- method: 'login',
356
- showDeposit: true,
357
- }),
358
- )
320
+ await provider.request(connectRequest({ method: 'login', showDeposit: true }))
359
321
 
360
- expect(pendingShowDeposit).toMatchInlineSnapshot(`
361
- [
362
- true,
363
- ]
364
- `)
322
+ expect(connectCapabilities(host, 0)?.showDeposit).toBe(true)
365
323
  } finally {
366
324
  await server.closeAsync()
367
325
  }
368
326
  })
369
327
 
370
- test('behavior: forwards showDeposit through wallet_authorizeAccessKey device code requests', async () => {
371
- const handler = createHandler()
372
- const server = await createServer(handler.listener)
373
- const pendingShowDeposit: z.output<typeof CliAuth.pendingResponse>['showDeposit'][] = []
328
+ test('behavior: forwards showDeposit through wallet_authorizeAccessKey device-code requests', async () => {
329
+ const host = createDeviceCodeHost()
330
+ const server = await createServer(host.listener)
374
331
 
375
332
  try {
376
333
  const provider = Provider.create({
377
334
  chains: [chain],
378
- open: async (url) => {
379
- const code = new URL(url).searchParams.get('code')!
380
- const response = await fetch(`${server.url}/cli-auth/pending/${code}`)
381
- const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
382
- pendingShowDeposit.push(pending.showDeposit)
383
- await fetch(`${server.url}/cli-auth`, {
384
- body: JSON.stringify(await authorizePending(server.url, code)),
385
- headers: { 'content-type': 'application/json' },
386
- method: 'POST',
387
- })
335
+ open: async (_url, prompt) => {
336
+ await submitVerify(prompt)
388
337
  },
389
- host: `${server.url}/cli-auth`,
338
+ host: `${server.url}/auth/device`,
339
+ storage: createJsonStorage(),
390
340
  })
391
341
 
342
+ await provider.request(connectRequest())
392
343
  await provider.request({
393
344
  method: 'wallet_authorizeAccessKey',
394
345
  params: [
@@ -401,19 +352,17 @@ describe('Provider.create', () => {
401
352
  ],
402
353
  })
403
354
 
404
- expect(pendingShowDeposit).toMatchInlineSnapshot(`
405
- [
406
- true,
407
- ]
408
- `)
355
+ const params = host.requests()[1]!.params as readonly { showDeposit?: unknown }[]
356
+ expect(host.requests()[1]!.method).toBe('wallet_authorizeAccessKey')
357
+ expect(params[0]?.showDeposit).toBe(true)
409
358
  } finally {
410
359
  await server.closeAsync()
411
360
  }
412
361
  })
413
362
 
414
363
  test('behavior: browser-open failures surface the URL and code', async () => {
415
- const handler = createHandler()
416
- const server = await createServer(handler.listener)
364
+ const host = createDeviceCodeHost()
365
+ const server = await createServer(host.listener)
417
366
 
418
367
  try {
419
368
  const provider = Provider.create({
@@ -421,154 +370,117 @@ describe('Provider.create', () => {
421
370
  open() {
422
371
  throw new Error('browser unavailable')
423
372
  },
424
- host: `${server.url}/cli-auth`,
373
+ host: `${server.url}/auth/device`,
374
+ storage: createJsonStorage(),
425
375
  })
426
376
 
427
- await expect(
428
- provider
429
- .request(connectRequest())
430
- .catch((error: { code: number; message: string; name: string }) => {
431
- return {
432
- code: error.code,
433
- message: error.message.replace(server.url, 'http://service'),
434
- name: error.name,
435
- }
436
- }),
437
- ).resolves.toMatchInlineSnapshot(`
438
- {
439
- "code": -32603,
440
- "message": "Failed to open browser for device code ABCD-EFGH. Open http://service/cli-auth?code=ABCDEFGH manually.",
441
- "name": "RpcResponse.InternalError",
442
- }
443
- `)
377
+ const error = await provider.request(connectRequest()).then(
378
+ () => undefined,
379
+ (error: Error) => error,
380
+ )
381
+ expect(error?.message).toMatch(
382
+ /^Failed to surface device code [A-Z]{4}-[A-Z]{4}\. Open .*\/auth\/device\/verify\?user_code=[A-Z]{4}-[A-Z]{4} manually\.$/,
383
+ )
444
384
  } finally {
445
385
  await server.closeAsync()
446
386
  }
447
387
  })
448
388
 
389
+ test('behavior: prints the user code when the complete verification URI is absent', async () => {
390
+ const host = createDeviceCodeHost({ omitVerificationUriFull: true })
391
+ const server = await createServer(host.listener)
392
+ const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
393
+
394
+ try {
395
+ const provider = Provider.create({
396
+ chains: [chain],
397
+ open: async (url, prompt) => {
398
+ expect(url).toBe(prompt.verificationUri)
399
+ await submitVerify(prompt)
400
+ },
401
+ host: `${server.url}/auth/device`,
402
+ storage: createJsonStorage(),
403
+ })
404
+
405
+ await provider.request(connectRequest())
406
+
407
+ expect(write).toHaveBeenCalledWith(
408
+ expect.stringMatching(/^Enter code [A-Z]{4}-[A-Z]{4} at http/),
409
+ )
410
+ } finally {
411
+ write.mockRestore()
412
+ await server.closeAsync()
413
+ }
414
+ })
415
+
449
416
  test('behavior: times out while waiting for authorization', async () => {
450
- const handler = createHandler()
451
- const server = await createServer(handler.listener)
417
+ const host = createDeviceCodeHost({ pollingInterval: 10 })
418
+ const server = await createServer(host.listener)
452
419
 
453
420
  try {
454
421
  const provider = Provider.create({
455
422
  chains: [chain],
456
423
  open() {},
457
- pollIntervalMs: 1,
458
- host: `${server.url}/cli-auth`,
459
- timeoutMs: 10,
424
+ host: `${server.url}/auth/device`,
425
+ storage: createJsonStorage(),
426
+ timeout: 100,
460
427
  })
461
428
 
462
- await expect(
463
- provider
464
- .request(connectRequest())
465
- .catch((error: { code: number; message: string; name: string }) => {
466
- return {
467
- code: error.code,
468
- message: error.message.replace(server.url, 'http://service'),
469
- name: error.name,
470
- }
471
- }),
472
- ).resolves.toMatchInlineSnapshot(`
473
- {
474
- "code": -32603,
475
- "message": "Timed out waiting for device code ABCD-EFGH. Continue at http://service/cli-auth?code=ABCDEFGH.",
476
- "name": "RpcResponse.InternalError",
477
- }
478
- `)
429
+ const error = await provider.request(connectRequest()).then(
430
+ () => undefined,
431
+ (error: Error) => error,
432
+ )
433
+ expect(error?.message).toMatch(
434
+ /^Timed out waiting for device code [A-Z]{4}-[A-Z]{4}\. Continue at /,
435
+ )
479
436
  } finally {
480
437
  await server.closeAsync()
481
438
  }
482
439
  })
483
440
 
484
- test('behavior: authorizes an access key while disconnected when publicKey is provided', async () => {
485
- const handler = createHandler()
486
- const server = await createServer(handler.listener)
487
- const opened: string[] = []
441
+ test('behavior: wallet_authorizeAccessKey requires a connected account', async () => {
442
+ const host = createDeviceCodeHost()
443
+ const server = await createServer(host.listener)
488
444
 
489
445
  try {
490
446
  const provider = Provider.create({
491
447
  chains: [chain],
492
- open: async (url) => {
493
- opened.push(url)
494
- const code = new URL(url).searchParams.get('code')!
495
- try {
496
- await fetch(`${server.url}/cli-auth`, {
497
- body: JSON.stringify(
498
- await authorize(code, { accessKey: accessKey_2, expiry: expiry_2 }),
499
- ),
500
- headers: { 'content-type': 'application/json' },
501
- method: 'POST',
502
- })
503
- } catch {}
448
+ open() {
449
+ throw new Error('Unexpected browser open.')
504
450
  },
505
- host: `${server.url}/cli-auth`,
451
+ host: `${server.url}/auth/device`,
452
+ storage: createJsonStorage(),
506
453
  })
507
454
 
508
- const result = await provider.request({
509
- method: 'wallet_authorizeAccessKey',
510
- params: [
511
- { expiry: expiry_2, keyType: accessKey_2.keyType, publicKey: accessKey_2.publicKey },
512
- ],
513
- })
514
- const keyAuthorization = {
515
- ...result.keyAuthorization,
516
- signature: {
517
- type: result.keyAuthorization.signature.type,
518
- },
519
- }
520
-
521
- expect({
522
- keyAuthorization,
523
- rootAddress: result.rootAddress,
524
- opened: opened.map((url) => url.replace(server.url, 'http://service')),
525
- }).toMatchInlineSnapshot(`
526
- {
527
- "keyAuthorization": {
528
- "address": "${accessKey_2.address}",
529
- "chainId": "${Hex.fromNumber(chain.id)}",
530
- "expiry": "${Hex.fromNumber(expiry_2)}",
531
- "keyId": "${accessKey_2.address}",
532
- "keyType": "secp256k1",
533
- "signature": {
534
- "type": "secp256k1",
535
- },
536
- },
537
- "opened": [
538
- "http://service/cli-auth?code=ABCDEFGH",
455
+ await expect(
456
+ provider.request({
457
+ method: 'wallet_authorizeAccessKey',
458
+ params: [
459
+ { expiry: expiry_2, keyType: accessKey_2.keyType, publicKey: accessKey_2.publicKey },
539
460
  ],
540
- "rootAddress": "${root.address}",
541
- }
542
- `)
461
+ }),
462
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
463
+ `[Provider.DisconnectedError: No active account.]`,
464
+ )
543
465
  } finally {
544
466
  await server.closeAsync()
545
467
  }
546
468
  })
547
469
 
548
470
  test('behavior: authorizes an access key for the active account', async () => {
549
- const handler = createHandler()
550
- const server = await createServer(handler.listener)
471
+ const host = createDeviceCodeHost()
472
+ const server = await createServer(host.listener)
551
473
  const opened: string[] = []
552
474
 
553
475
  try {
554
- const approvals = [
555
- (code: string) => authorize(code),
556
- (code: string) => authorize(code, { accessKey: accessKey_2, expiry: expiry_2 }),
557
- ]
558
476
  const provider = Provider.create({
559
477
  chains: [chain],
560
- open: async (url) => {
478
+ open: async (url, prompt) => {
561
479
  opened.push(url)
562
- const approve = approvals.shift()
563
- if (!approve) throw new Error('Unexpected device code approval request.')
564
- const code = new URL(url).searchParams.get('code')!
565
- await fetch(`${server.url}/cli-auth`, {
566
- body: JSON.stringify(await approve(code)),
567
- headers: { 'content-type': 'application/json' },
568
- method: 'POST',
569
- })
480
+ await submitVerify(prompt)
570
481
  },
571
- host: `${server.url}/cli-auth`,
482
+ host: `${server.url}/auth/device`,
483
+ storage: createJsonStorage(),
572
484
  })
573
485
 
574
486
  await provider.request(connectRequest())
@@ -586,26 +498,22 @@ describe('Provider.create', () => {
586
498
  },
587
499
  }
588
500
 
501
+ expect(opened).toHaveLength(2)
589
502
  expect({
590
503
  keyAuthorization,
591
504
  rootAddress: result.rootAddress,
592
- opened: opened.map((url) => url.replace(server.url, 'http://service')),
593
505
  }).toMatchInlineSnapshot(`
594
506
  {
595
507
  "keyAuthorization": {
596
- "address": "${accessKey_2.address}",
597
- "chainId": "${Hex.fromNumber(chain.id)}",
598
- "expiry": "${Hex.fromNumber(expiry_2)}",
599
- "keyId": "${accessKey_2.address}",
508
+ "address": "${accessKey_2.address.toLowerCase()}",
509
+ "chainId": "${ox_Hex.fromNumber(chain.id)}",
510
+ "expiry": "${ox_Hex.fromNumber(expiry_2)}",
511
+ "keyId": "${accessKey_2.address.toLowerCase()}",
600
512
  "keyType": "secp256k1",
601
513
  "signature": {
602
514
  "type": "secp256k1",
603
515
  },
604
516
  },
605
- "opened": [
606
- "http://service/cli-auth?code=ABCDEFGH",
607
- "http://service/cli-auth?code=JKLMNPQR",
608
- ],
609
517
  "rootAddress": "${root.address}",
610
518
  }
611
519
  `)
@@ -615,21 +523,17 @@ describe('Provider.create', () => {
615
523
  })
616
524
 
617
525
  test('behavior: rejects unsupported revokeAccessKey after bootstrap', async () => {
618
- const handler = createHandler()
619
- const server = await createServer(handler.listener)
526
+ const host = createDeviceCodeHost()
527
+ const server = await createServer(host.listener)
620
528
 
621
529
  try {
622
530
  const provider = Provider.create({
623
531
  chains: [chain],
624
- open: async (url) => {
625
- const code = new URL(url).searchParams.get('code')!
626
- await fetch(`${server.url}/cli-auth`, {
627
- body: JSON.stringify(await authorize(code)),
628
- headers: { 'content-type': 'application/json' },
629
- method: 'POST',
630
- })
532
+ open: async (_url, prompt) => {
533
+ await submitVerify(prompt)
631
534
  },
632
- host: `${server.url}/cli-auth`,
535
+ host: `${server.url}/auth/device`,
536
+ storage: createJsonStorage(),
633
537
  })
634
538
 
635
539
  await provider.request(connectRequest())
@@ -639,32 +543,25 @@ describe('Provider.create', () => {
639
543
  method: 'wallet_revokeAccessKey',
640
544
  params: [{ accessKeyAddress: accessKey.address, address: root.address }],
641
545
  }),
642
- ).rejects.toThrowErrorMatchingInlineSnapshot(
643
- `[Provider.UnsupportedMethodError: \`wallet_revokeAccessKey\` not supported by CLI adapter.]`,
644
- )
546
+ ).rejects.toThrow('`wallet_revokeAccessKey` not supported by device-code adapter.')
645
547
  } finally {
646
548
  await server.closeAsync()
647
549
  }
648
550
  })
649
551
 
650
552
  test('behavior: generates, persists, and reuses a managed key during wallet_connect', async () => {
651
- const handler = createHandler()
652
- const server = await createServer(handler.listener)
553
+ const host = createDeviceCodeHost()
554
+ const server = await createServer(host.listener)
653
555
  const storagePath = await createStoragePath()
654
556
  const storage = Storage.filesystem({ path: storagePath })
655
557
 
656
558
  try {
657
559
  const provider = Provider.create({
658
560
  chains: [chain],
659
- open: async (url) => {
660
- const code = new URL(url).searchParams.get('code')!
661
- await fetch(`${server.url}/cli-auth`, {
662
- body: JSON.stringify(await authorizePending(server.url, code)),
663
- headers: { 'content-type': 'application/json' },
664
- method: 'POST',
665
- })
561
+ open: async (_url, prompt) => {
562
+ await submitVerify(prompt)
666
563
  },
667
- host: `${server.url}/cli-auth`,
564
+ host: `${server.url}/auth/device`,
668
565
  storage,
669
566
  })
670
567
 
@@ -685,7 +582,7 @@ describe('Provider.create', () => {
685
582
  open() {
686
583
  throw new Error('Unexpected browser open.')
687
584
  },
688
- host: `${server.url}/cli-auth`,
585
+ host: `${server.url}/auth/device`,
689
586
  storage: storage_2,
690
587
  })
691
588
  const receipt_2 = await provider_2.request({
@@ -716,27 +613,73 @@ describe('Provider.create', () => {
716
613
  }
717
614
  })
718
615
 
616
+ test('behavior: signs messages locally with the managed access key', async () => {
617
+ const host = createDeviceCodeHost()
618
+ const server = await createServer(host.listener)
619
+
620
+ try {
621
+ const provider = Provider.create({
622
+ chains: [chain],
623
+ open: async (_url, prompt) => {
624
+ await submitVerify(prompt)
625
+ },
626
+ host: `${server.url}/auth/device`,
627
+ storage: createJsonStorage(),
628
+ })
629
+ const connected = await provider.request({
630
+ method: 'wallet_connect',
631
+ params: [{ capabilities: { authorizeAccessKey: { expiry: expiry_2 } } }],
632
+ })
633
+ const address = connected.accounts[0]!.address
634
+
635
+ const personal = await provider.request({
636
+ method: 'personal_sign',
637
+ params: ['0x68656c6c6f', address],
638
+ })
639
+ const typedData = await provider.request({
640
+ method: 'eth_signTypedData_v4',
641
+ params: [
642
+ address,
643
+ JSON.stringify({
644
+ domain: {},
645
+ message: { contents: 'hello' },
646
+ primaryType: 'Message',
647
+ types: {
648
+ EIP712Domain: [],
649
+ Message: [{ name: 'contents', type: 'string' }],
650
+ },
651
+ }),
652
+ ],
653
+ })
654
+
655
+ expect(personal).toMatch(/^0x/)
656
+ expect(typedData).toMatch(/^0x/)
657
+ expect(host.requests()).toHaveLength(1)
658
+ } finally {
659
+ await server.closeAsync()
660
+ }
661
+ })
662
+
719
663
  test('behavior: generates a managed key for wallet_authorizeAccessKey without publicKey', async () => {
720
- const handler = createHandler()
721
- const server = await createServer(handler.listener)
664
+ const host = createDeviceCodeHost()
665
+ const server = await createServer(host.listener)
722
666
  const storagePath = await createStoragePath()
723
667
  const storage = Storage.filesystem({ path: storagePath })
724
668
 
725
669
  try {
726
670
  const provider = Provider.create({
727
671
  chains: [chain],
728
- open: async (url) => {
729
- const code = new URL(url).searchParams.get('code')!
730
- await fetch(`${server.url}/cli-auth`, {
731
- body: JSON.stringify(await authorizePending(server.url, code)),
732
- headers: { 'content-type': 'application/json' },
733
- method: 'POST',
734
- })
672
+ open: async (_url, prompt) => {
673
+ await submitVerify(prompt)
735
674
  },
736
- host: `${server.url}/cli-auth`,
675
+ host: `${server.url}/auth/device`,
737
676
  storage,
738
677
  })
739
678
 
679
+ const connected = await provider.request({
680
+ method: 'wallet_connect',
681
+ params: [{ capabilities: {} }],
682
+ })
740
683
  const result = await provider.request({
741
684
  method: 'wallet_authorizeAccessKey',
742
685
  params: [{ expiry: expiry_2 }],
@@ -749,6 +692,7 @@ describe('Provider.create', () => {
749
692
  })
750
693
  const [entry] = await readAccessKeys(storage)
751
694
 
695
+ expect(connected.accounts[0]!.address).toBe(root.address)
752
696
  expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
753
697
  expect({
754
698
  access: entry!.access,
@@ -764,97 +708,53 @@ describe('Provider.create', () => {
764
708
  }
765
709
  })
766
710
 
767
- test('behavior: regenerates a managed key when the requested key type changes', async () => {
768
- const handler = createHandler()
769
- const server = await createServer(handler.listener)
711
+ test('behavior: wallet_updateAccessKey replaces an unpublished key authorization', async () => {
712
+ const host = createDeviceCodeHost()
713
+ const server = await createServer(host.listener)
770
714
  const storagePath = await createStoragePath()
771
715
  const storage = Storage.filesystem({ path: storagePath })
772
716
 
773
717
  try {
774
718
  const provider = Provider.create({
775
719
  chains: [chain],
776
- open: async (url) => {
777
- const code = new URL(url).searchParams.get('code')!
778
- await fetch(`${server.url}/cli-auth`, {
779
- body: JSON.stringify(await authorizePending(server.url, code)),
780
- headers: { 'content-type': 'application/json' },
781
- method: 'POST',
782
- })
720
+ open: async (_url, prompt) => {
721
+ await submitVerify(prompt)
783
722
  },
784
- host: `${server.url}/cli-auth`,
723
+ host: `${server.url}/auth/device`,
785
724
  storage,
786
725
  })
787
726
 
788
- // secp256k1 is requested explicitly; p256 is the default — a genuine
789
- // type change that regenerates the managed key.
790
- const first = await provider.request({
791
- method: 'wallet_authorizeAccessKey',
792
- params: [{ expiry, keyType: 'secp256k1' }],
793
- })
794
- const second = await provider.request({
795
- method: 'wallet_authorizeAccessKey',
796
- params: [{ expiry: expiry_2, keyType: 'p256' }],
797
- })
798
- await fund(second.rootAddress)
799
-
800
- const receipt = await provider.request({
801
- method: 'eth_sendTransactionSync',
802
- params: [{ calls: [transferCall] }],
727
+ const result = await provider.request({
728
+ method: 'wallet_connect',
729
+ params: [
730
+ {
731
+ capabilities: {
732
+ authorizeAccessKey: {
733
+ expiry: expiry_2,
734
+ limits: [{ limit: ox_Hex.fromNumber(1n), token: Addresses.pathUsd }],
735
+ },
736
+ },
737
+ },
738
+ ],
803
739
  })
804
- const keys = await readAccessKeys(storage)
805
-
806
- expect(first.keyAuthorization.keyType).toBe('secp256k1')
807
- expect(second.keyAuthorization.keyType).toBe('p256')
808
- expect(first.keyAuthorization.keyId).not.toBe(second.keyAuthorization.keyId)
809
- expect(new Set(keys.map((key) => key.keyType))).toEqual(new Set(['secp256k1', 'p256']))
810
- expect(receipt.status).toBe('0x1')
811
- } finally {
812
- await server.closeAsync()
813
- }
814
- })
815
-
816
- test('wallet_updateAccessKey: replaces an unpublished key authorization', async () => {
817
- const handler = createHandler()
818
- const server = await createServer(handler.listener)
819
- const storagePath = await createStoragePath()
820
- const storage = Storage.filesystem({ path: storagePath })
740
+ const account = result.accounts[0]!
741
+ const accessKeyAddress = account.capabilities.keyAuthorization!.keyId
821
742
 
822
- try {
823
- const provider = Provider.create({
824
- chains: [chain],
825
- open: async (url) => {
826
- const code = new URL(url).searchParams.get('code')!
827
- const body = await completePending(server.url, code)
828
- const response = await fetch(`${server.url}/cli-auth`, {
829
- body: JSON.stringify(body),
830
- headers: { 'content-type': 'application/json' },
831
- method: 'POST',
832
- })
833
- if (!response.ok) throw new Error(await response.text())
834
- },
835
- host: `${server.url}/cli-auth`,
836
- storage,
837
- })
838
743
  const updated = parseUnits('9', 6)
839
- const { keyAuthorization, rootAddress } = await provider.request({
840
- method: 'wallet_authorizeAccessKey',
841
- params: [{ expiry, limits: [{ limit: Hex.fromNumber(1), token: Addresses.pathUsd }] }],
842
- })
843
-
844
744
  await provider.request({
845
745
  method: 'wallet_updateAccessKey',
846
746
  params: [
847
747
  {
848
- address: rootAddress,
849
- accessKeyAddress: keyAuthorization.address!,
850
- limits: [{ limit: Hex.fromNumber(updated), token: Addresses.pathUsd }],
748
+ address: account.address,
749
+ accessKeyAddress,
750
+ limits: [{ limit: ox_Hex.fromNumber(updated), token: Addresses.pathUsd }],
851
751
  },
852
752
  ],
853
753
  })
854
754
 
855
755
  const [stored] = provider.store.accessKeys.list({
856
- account: rootAddress,
857
- accessKey: keyAuthorization.address!,
756
+ accessKey: accessKeyAddress,
757
+ account: account.address,
858
758
  chainId: chain.id,
859
759
  })
860
760
  expect(stored?.keyAuthorization?.limits).toEqual([
@@ -862,8 +762,8 @@ describe('Provider.create', () => {
862
762
  ])
863
763
  expect(
864
764
  await provider.store.accessKeys.getStatus({
865
- account: rootAddress,
866
- accessKey: keyAuthorization.address!,
765
+ accessKey: accessKeyAddress,
766
+ account: account.address,
867
767
  chainId: chain.id,
868
768
  client: getClient(),
869
769
  }),
@@ -873,53 +773,49 @@ describe('Provider.create', () => {
873
773
  }
874
774
  })
875
775
 
876
- test('wallet_updateAccessKey: updates published spending limits through browser approval', async () => {
877
- const handler = createHandler()
878
- const server = await createServer(handler.listener)
776
+ test('behavior: wallet_updateAccessKey updates published spending limits through approval', async () => {
777
+ const host = createDeviceCodeHost()
778
+ const server = await createServer(host.listener)
879
779
  const storagePath = await createStoragePath()
880
780
  const storage = Storage.filesystem({ path: storagePath })
881
781
 
882
782
  try {
883
783
  const provider = Provider.create({
884
784
  chains: [chain],
885
- open: async (url) => {
886
- const code = new URL(url).searchParams.get('code')!
887
- const body = await completePending(server.url, code)
888
- const response = await fetch(`${server.url}/cli-auth`, {
889
- body: JSON.stringify(body),
890
- headers: { 'content-type': 'application/json' },
891
- method: 'POST',
892
- })
893
- if (!response.ok) throw new Error(await response.text())
785
+ open: async (_url, prompt) => {
786
+ await submitVerify(prompt)
894
787
  },
895
- host: `${server.url}/cli-auth`,
788
+ host: `${server.url}/auth/device`,
896
789
  storage,
897
790
  })
898
- const updated = parseUnits('9', 6)
899
- const { keyAuthorization, rootAddress } = await provider.request({
900
- method: 'wallet_authorizeAccessKey',
901
- params: [{ expiry }],
791
+
792
+ const result = await provider.request({
793
+ method: 'wallet_connect',
794
+ params: [{ capabilities: { authorizeAccessKey: { expiry: expiry_2 } } }],
902
795
  })
903
- await fund(rootAddress)
796
+ const account = result.accounts[0]!
797
+ const accessKeyAddress = account.capabilities.keyAuthorization!.keyId
798
+ await fund(account.address)
904
799
  await provider.request({
905
800
  method: 'eth_sendTransactionSync',
906
801
  params: [{ calls: [transferCall] }],
907
802
  })
908
803
 
804
+ const updated = parseUnits('9', 6)
909
805
  await provider.request({
910
806
  method: 'wallet_updateAccessKey',
911
807
  params: [
912
808
  {
913
- address: rootAddress,
914
- accessKeyAddress: keyAuthorization.address!,
915
- limits: [{ limit: Hex.fromNumber(updated), token: Addresses.pathUsd }],
809
+ address: account.address,
810
+ accessKeyAddress,
811
+ limits: [{ limit: ox_Hex.fromNumber(updated), token: Addresses.pathUsd }],
916
812
  },
917
813
  ],
918
814
  })
919
815
 
920
816
  const { remaining } = await Actions.accessKey.getRemainingLimit(getClient(), {
921
- account: rootAddress,
922
- accessKey: keyAuthorization.address!,
817
+ account: account.address,
818
+ accessKey: accessKeyAddress,
923
819
  token: Addresses.pathUsd,
924
820
  })
925
821
  expect(remaining).toBe(updated)
@@ -927,4 +823,52 @@ describe('Provider.create', () => {
927
823
  await server.closeAsync()
928
824
  }
929
825
  })
826
+
827
+ test('behavior: regenerates a managed key when the requested key type changes', async () => {
828
+ const host = createDeviceCodeHost()
829
+ const server = await createServer(host.listener)
830
+ const storagePath = await createStoragePath()
831
+ const storage = Storage.filesystem({ path: storagePath })
832
+
833
+ try {
834
+ const provider = Provider.create({
835
+ chains: [chain],
836
+ open: async (_url, prompt) => {
837
+ await submitVerify(prompt)
838
+ },
839
+ host: `${server.url}/auth/device`,
840
+ storage,
841
+ })
842
+
843
+ await provider.request({
844
+ method: 'wallet_connect',
845
+ params: [{ capabilities: {} }],
846
+ })
847
+ // secp256k1 is requested explicitly; p256 is the default — a genuine
848
+ // type change that regenerates the managed key.
849
+ const first = await provider.request({
850
+ method: 'wallet_authorizeAccessKey',
851
+ params: [{ expiry, keyType: 'secp256k1' }],
852
+ })
853
+ const second = await provider.request({
854
+ method: 'wallet_authorizeAccessKey',
855
+ params: [{ expiry: expiry_2, keyType: 'p256' }],
856
+ })
857
+ await fund(second.rootAddress)
858
+
859
+ const receipt = await provider.request({
860
+ method: 'eth_sendTransactionSync',
861
+ params: [{ calls: [transferCall] }],
862
+ })
863
+ const keys = await readAccessKeys(storage)
864
+
865
+ expect(first.keyAuthorization.keyType).toBe('secp256k1')
866
+ expect(second.keyAuthorization.keyType).toBe('p256')
867
+ expect(first.keyAuthorization.keyId).not.toBe(second.keyAuthorization.keyId)
868
+ expect(new Set(keys.map((key) => key.keyType))).toEqual(new Set(['secp256k1', 'p256']))
869
+ expect(receipt.status).toBe('0x1')
870
+ } finally {
871
+ await server.closeAsync()
872
+ }
873
+ })
930
874
  })