accounts 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,11 @@ import { Address, Base64, Bytes, Hex, PublicKey } from 'ox'
2
2
  import { KeyAuthorization as TempoKeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
3
  import { createClient, http, type Chain, type Client, type Transport } from 'viem'
4
4
  import { verifyHash } from 'viem/actions'
5
+ import { Actions } from 'viem/tempo'
5
6
  import { tempo } from 'viem/tempo/chains'
6
7
  import * as z from 'zod/mini'
7
8
 
9
+ import * as AccessKey from '../core/AccessKey.js'
8
10
  import * as u from '../core/zod/utils.js'
9
11
  import type { MaybePromise } from '../internal/types.js'
10
12
  import type { Kv } from './Kv.js'
@@ -26,10 +28,10 @@ const showDeposit = z.optional(
26
28
  const defaultTtlMs = 10 * 60 * 1_000
27
29
  const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
28
30
 
29
- /** Supported access-key types for CLI bootstrap. */
31
+ /** Supported access key types for CLI bootstrap. */
30
32
  export const keyType = z.union([z.literal('secp256k1'), z.literal('p256'), z.literal('webAuthn')])
31
33
 
32
- /** Signed key authorization returned by the device-code flow. */
34
+ /** Signed key authorization returned by the device code flow. */
33
35
  export const keyAuthorization = z.object({
34
36
  address: u.address(),
35
37
  chainId: u.bigint(),
@@ -40,8 +42,8 @@ export const keyAuthorization = z.object({
40
42
  signature: z.custom<SignatureEnvelope.SignatureEnvelopeRpc>(),
41
43
  })
42
44
 
43
- /** CLI auth device-code creation request body. */
44
- export const createRequest = z.object({
45
+ const authorizeAccessKeyCreateRequest = z.object({
46
+ action: z.optional(z.literal('authorizeAccessKey')),
45
47
  account: z.optional(u.address()),
46
48
  chainId: z.optional(u.bigint()),
47
49
  codeChallenge: z.string(),
@@ -52,6 +54,22 @@ export const createRequest = z.object({
52
54
  showDeposit,
53
55
  })
54
56
 
57
+ const updateAccessKeyCreateRequest = z.object({
58
+ action: z.literal('updateAccessKey'),
59
+ accessKeyAddress: u.address(),
60
+ account: u.address(),
61
+ chainId: z.optional(u.bigint()),
62
+ codeChallenge: z.string(),
63
+ keyAuthorization: z.optional(keyAuthorization),
64
+ limits: z.readonly(z.array(limit).check(z.minLength(1), z.maxLength(maxLimits))),
65
+ })
66
+
67
+ /** CLI auth device code creation request body. */
68
+ export const createRequest = u.oneOf([
69
+ authorizeAccessKeyCreateRequest,
70
+ updateAccessKeyCreateRequest,
71
+ ])
72
+
55
73
  /** Response body for `POST /cli-auth/device-code`. */
56
74
  export const createResponse = z.object({
57
75
  code: z.string(),
@@ -72,40 +90,69 @@ export const pollResponse = u.oneOf([
72
90
  accountAddress: u.address(),
73
91
  keyAuthorization: keyAuthorization,
74
92
  }),
93
+ z.object({
94
+ action: z.literal('updateAccessKey'),
95
+ keyAuthorization: z.optional(keyAuthorization),
96
+ status: z.literal('authorized'),
97
+ }),
75
98
  z.object({
76
99
  status: z.literal('expired'),
77
100
  }),
78
101
  ])
79
102
 
80
103
  /** Response body for `GET /auth/pkce/pending/:code`. */
81
- export const pendingResponse = z.object({
82
- accessKeyAddress: u.address(),
83
- account: z.optional(u.address()),
84
- chainId: u.bigint(),
85
- code: z.string(),
86
- expiry: z.number(),
87
- keyType,
88
- limits: z.optional(limits),
89
- pubKey: u.hex(),
90
- showDeposit,
91
- status: z.literal('pending'),
92
- })
104
+ export const pendingResponse = u.oneOf([
105
+ z.object({
106
+ action: z.optional(z.literal('authorizeAccessKey')),
107
+ accessKeyAddress: u.address(),
108
+ account: z.optional(u.address()),
109
+ chainId: u.bigint(),
110
+ code: z.string(),
111
+ expiry: z.number(),
112
+ keyType,
113
+ limits: z.optional(limits),
114
+ pubKey: u.hex(),
115
+ showDeposit,
116
+ status: z.literal('pending'),
117
+ }),
118
+ z.object({
119
+ action: z.literal('updateAccessKey'),
120
+ accessKeyAddress: u.address(),
121
+ account: u.address(),
122
+ chainId: u.bigint(),
123
+ code: z.string(),
124
+ keyAuthorization: z.optional(keyAuthorization),
125
+ limits: z.readonly(z.array(limit).check(z.minLength(1), z.maxLength(maxLimits))),
126
+ status: z.literal('pending'),
127
+ }),
128
+ ])
93
129
 
94
130
  /** Request body for `POST /auth/pkce`. */
95
- export const authorizeRequest = z.object({
96
- accountAddress: u.address(),
97
- code: z.string(),
98
- keyAuthorization: keyAuthorization,
99
- })
131
+ export const authorizeRequest = u.oneOf([
132
+ z.object({
133
+ action: z.optional(z.literal('authorizeAccessKey')),
134
+ accountAddress: u.address(),
135
+ code: z.string(),
136
+ keyAuthorization: keyAuthorization,
137
+ }),
138
+ z.object({
139
+ action: z.literal('updateAccessKey'),
140
+ accountAddress: u.address(),
141
+ chainId: u.bigint(),
142
+ code: z.string(),
143
+ keyAuthorization: z.optional(keyAuthorization),
144
+ }),
145
+ ])
100
146
 
101
147
  /** Response body for `POST /cli-auth/authorize`. */
102
148
  export const authorizeResponse = z.object({
103
149
  status: z.literal('authorized'),
104
150
  })
105
151
 
106
- /** Stored device-code entry schema. */
152
+ /** Stored device code entry schema. */
107
153
  export const entry = u.oneOf([
108
154
  z.object({
155
+ action: z.optional(z.literal('authorizeAccessKey')),
109
156
  account: z.optional(u.address()),
110
157
  chainId: u.bigint(),
111
158
  code: z.string(),
@@ -120,6 +167,7 @@ export const entry = u.oneOf([
120
167
  status: z.literal('pending'),
121
168
  }),
122
169
  z.object({
170
+ action: z.optional(z.literal('authorizeAccessKey')),
123
171
  account: z.optional(u.address()),
124
172
  accountAddress: u.address(),
125
173
  authorizedAt: z.number(),
@@ -137,6 +185,7 @@ export const entry = u.oneOf([
137
185
  status: z.literal('authorized'),
138
186
  }),
139
187
  z.object({
188
+ action: z.optional(z.literal('authorizeAccessKey')),
140
189
  account: z.optional(u.address()),
141
190
  accountAddress: u.address(),
142
191
  authorizedAt: z.number(),
@@ -154,6 +203,50 @@ export const entry = u.oneOf([
154
203
  showDeposit,
155
204
  status: z.literal('consumed'),
156
205
  }),
206
+ z.object({
207
+ action: z.literal('updateAccessKey'),
208
+ accessKeyAddress: u.address(),
209
+ account: u.address(),
210
+ chainId: u.bigint(),
211
+ code: z.string(),
212
+ codeChallenge: z.string(),
213
+ createdAt: z.number(),
214
+ expiresAt: z.number(),
215
+ keyAuthorization: z.optional(keyAuthorization),
216
+ limits: z.readonly(z.array(limit).check(z.minLength(1), z.maxLength(maxLimits))),
217
+ status: z.literal('pending'),
218
+ }),
219
+ z.object({
220
+ action: z.literal('updateAccessKey'),
221
+ accessKeyAddress: u.address(),
222
+ account: u.address(),
223
+ accountAddress: u.address(),
224
+ authorizedAt: z.number(),
225
+ chainId: u.bigint(),
226
+ code: z.string(),
227
+ codeChallenge: z.string(),
228
+ createdAt: z.number(),
229
+ expiresAt: z.number(),
230
+ keyAuthorization: z.optional(keyAuthorization),
231
+ limits: z.readonly(z.array(limit).check(z.minLength(1), z.maxLength(maxLimits))),
232
+ status: z.literal('authorized'),
233
+ }),
234
+ z.object({
235
+ action: z.literal('updateAccessKey'),
236
+ accessKeyAddress: u.address(),
237
+ account: u.address(),
238
+ accountAddress: u.address(),
239
+ authorizedAt: z.number(),
240
+ chainId: u.bigint(),
241
+ code: z.string(),
242
+ codeChallenge: z.string(),
243
+ consumedAt: z.number(),
244
+ createdAt: z.number(),
245
+ expiresAt: z.number(),
246
+ keyAuthorization: z.optional(keyAuthorization),
247
+ limits: z.readonly(z.array(limit).check(z.minLength(1), z.maxLength(maxLimits))),
248
+ status: z.literal('consumed'),
249
+ }),
157
250
  ])
158
251
 
159
252
  /** Shared CLI auth helper with pre-bound defaults and cached clients. */
@@ -168,20 +261,22 @@ export type CliAuth = {
168
261
  authorize: (options: authorize.Parameters) => Promise<authorize.ReturnType>
169
262
  }
170
263
 
171
- /** Stored device-code entry. */
264
+ /** Stored device code entry. */
172
265
  export type Entry = z.output<typeof entry>
173
266
 
174
- /** Device-code storage contract. */
267
+ /** Device code storage contract. */
175
268
  export type Store = {
176
- /** Saves a new pending device-code entry. */
269
+ /** Saves a new pending device code entry. */
177
270
  create: (entry: Entry.Pending) => MaybePromise<void>
178
- /** Loads a device-code entry by verification code. */
271
+ /** Loads a device code entry by verification code. */
179
272
  get: (code: string) => MaybePromise<Entry | undefined>
180
- /** Marks a pending device-code as authorized. */
273
+ /** Marks a pending device code as authorized. */
181
274
  authorize: (options: Store.authorize.Options) => MaybePromise<Entry.Authorized | undefined>
182
- /** Consumes an authorized device-code exactly once. */
275
+ /** Marks a pending access key update as complete. */
276
+ update: (options: Store.update.Options) => MaybePromise<Entry.Authorized | undefined>
277
+ /** Consumes an authorized device code exactly once. */
183
278
  consume: (code: string) => MaybePromise<Entry.Authorized | undefined>
184
- /** Deletes a device-code entry. */
279
+ /** Deletes a device code entry. */
185
280
  delete: (code: string) => MaybePromise<void>
186
281
  }
187
282
 
@@ -189,6 +284,8 @@ export type Store = {
189
284
  export type Policy = {
190
285
  /** Validates and optionally rewrites requested defaults before the entry is stored. */
191
286
  validate: (options: Policy.validate.Options) => MaybePromise<Policy.validate.ReturnType>
287
+ /** Validates and optionally rewrites requested access key limit updates. */
288
+ update?: ((options: Policy.update.Options) => MaybePromise<Policy.update.ReturnType>) | undefined
192
289
  }
193
290
 
194
291
  /** Request rate limiter used by CLI auth handlers. */
@@ -198,11 +295,11 @@ export type RateLimit = {
198
295
  }
199
296
 
200
297
  export declare namespace Entry {
201
- /** Pending device-code entry. */
298
+ /** Pending device code entry. */
202
299
  export type Pending = Extract<z.output<typeof entry>, { status: 'pending' }>
203
- /** Authorized device-code entry. */
300
+ /** Authorized device code entry. */
204
301
  export type Authorized = Extract<z.output<typeof entry>, { status: 'authorized' }>
205
- /** Consumed device-code entry. */
302
+ /** Consumed device code entry. */
206
303
  export type Consumed = Extract<z.output<typeof entry>, { status: 'consumed' }>
207
304
  }
208
305
 
@@ -218,6 +315,17 @@ export declare namespace Store {
218
315
  }
219
316
  }
220
317
 
318
+ export namespace update {
319
+ export type Options = {
320
+ /** Root account that updated the access key. */
321
+ accountAddress: Address.Address
322
+ /** Replacement authorization for an access key that is still pending publication. */
323
+ keyAuthorization?: z.output<typeof keyAuthorization> | undefined
324
+ /** Verification code to complete. */
325
+ code: string
326
+ }
327
+ }
328
+
221
329
  export namespace kv {
222
330
  export type Options = {
223
331
  /** Prefix used for KV keys. @default "cli-auth" */
@@ -233,23 +341,41 @@ export declare namespace Policy {
233
341
  account?: Address.Address | undefined
234
342
  /** Requested chain ID. */
235
343
  chainId: bigint
236
- /** Requested access-key expiry timestamp. Omit to let the server choose one. */
344
+ /** Requested access key expiry timestamp. Omit to let the server choose one. */
237
345
  expiry?: number | undefined
238
346
  /** Requested key type. */
239
347
  keyType: z.output<typeof keyType>
240
348
  /** Requested spending limits. */
241
349
  limits?: readonly { token: Address.Address; limit: bigint }[] | undefined
242
- /** Requested access-key public key. */
350
+ /** Requested access key public key. */
243
351
  pubKey: Hex.Hex
244
352
  }
245
353
 
246
354
  export type ReturnType = {
247
- /** Suggested access-key expiry timestamp. */
355
+ /** Suggested access key expiry timestamp. */
248
356
  expiry: number
249
357
  /** Suggested spending limits. */
250
358
  limits?: readonly { token: Address.Address; limit: bigint }[] | undefined
251
359
  }
252
360
  }
361
+
362
+ export namespace update {
363
+ export type Options = {
364
+ /** Access key to update. */
365
+ accessKeyAddress: Address.Address
366
+ /** Root account that owns the access key. */
367
+ account: Address.Address
368
+ /** Requested chain ID. */
369
+ chainId: bigint
370
+ /** Requested spending limits. */
371
+ limits: readonly { token: Address.Address; limit: bigint }[]
372
+ }
373
+
374
+ export type ReturnType = {
375
+ /** Approved spending limits. */
376
+ limits: readonly { token: Address.Address; limit: bigint }[]
377
+ }
378
+ }
253
379
  }
254
380
 
255
381
  export declare namespace RateLimit {
@@ -289,7 +415,7 @@ export declare namespace RateLimit {
289
415
  }
290
416
  }
291
417
 
292
- /** Error thrown when pending device-code lookup cannot return a pending request. */
418
+ /** Error thrown when pending device code lookup cannot return a pending request. */
293
419
  export class PendingError extends Error {
294
420
  /** HTTP status returned by handler surfaces. */
295
421
  status: 400 | 404
@@ -301,10 +427,10 @@ export class PendingError extends Error {
301
427
  }
302
428
  }
303
429
 
304
- /** Built-in device-code store helpers. */
430
+ /** Built-in device code store helpers. */
305
431
  export const Store = {
306
432
  /**
307
- * Creates an in-memory device-code store.
433
+ * Creates an in-memory device code store.
308
434
  *
309
435
  * Useful for tests and single-process servers.
310
436
  */
@@ -314,7 +440,8 @@ export const Store = {
314
440
  return {
315
441
  async authorize(options) {
316
442
  const current = entries.get(options.code)
317
- if (!current || current.status !== 'pending') return undefined
443
+ if (!current || current.status !== 'pending' || current.action === 'updateAccessKey')
444
+ return undefined
318
445
  const next = {
319
446
  ...current,
320
447
  accountAddress: options.accountAddress,
@@ -344,10 +471,24 @@ export const Store = {
344
471
  async get(code) {
345
472
  return entries.get(code)
346
473
  },
474
+ async update(options) {
475
+ const current = entries.get(options.code)
476
+ if (!current || current.status !== 'pending' || current.action !== 'updateAccessKey')
477
+ return undefined
478
+ const next = {
479
+ ...current,
480
+ accountAddress: options.accountAddress,
481
+ authorizedAt: Date.now(),
482
+ ...(options.keyAuthorization ? { keyAuthorization: options.keyAuthorization } : {}),
483
+ status: 'authorized',
484
+ } satisfies Entry.Authorized
485
+ entries.set(options.code, next)
486
+ return next
487
+ },
347
488
  }
348
489
  },
349
490
  /**
350
- * Creates a key-value backed device-code store.
491
+ * Creates a key-value backed device code store.
351
492
  *
352
493
  * Stored values are encoded through the shared entry schema so they remain
353
494
  * JSON-safe across KV implementations.
@@ -362,7 +503,8 @@ export const Store = {
362
503
  return {
363
504
  async authorize(options) {
364
505
  const current = await this.get(options.code)
365
- if (!current || current.status !== 'pending') return undefined
506
+ if (!current || current.status !== 'pending' || current.action === 'updateAccessKey')
507
+ return undefined
366
508
  const next = {
367
509
  ...current,
368
510
  accountAddress: options.accountAddress,
@@ -397,6 +539,20 @@ export const Store = {
397
539
  if (!value) return undefined
398
540
  return z.decode(entry, value)
399
541
  },
542
+ async update(options) {
543
+ const current = await this.get(options.code)
544
+ if (!current || current.status !== 'pending' || current.action !== 'updateAccessKey')
545
+ return undefined
546
+ const next = {
547
+ ...current,
548
+ accountAddress: options.accountAddress,
549
+ authorizedAt: Date.now(),
550
+ ...(options.keyAuthorization ? { keyAuthorization: options.keyAuthorization } : {}),
551
+ status: 'authorized',
552
+ } satisfies Entry.Authorized
553
+ await kv.set(toKey(options.code), z.encode(entry, next))
554
+ return next
555
+ },
400
556
  }
401
557
  },
402
558
  }
@@ -406,6 +562,9 @@ export const Policy = {
406
562
  /** Creates an allow-all policy with a default 24-hour expiry when omitted. */
407
563
  allow(): Policy {
408
564
  return {
565
+ update({ limits }) {
566
+ return { limits }
567
+ },
409
568
  validate({ expiry, limits }) {
410
569
  return {
411
570
  expiry: expiry ?? Math.floor(Date.now() / 1000) + 60 * 60 * 24,
@@ -507,17 +666,87 @@ export function from(options: from.Options = {}): CliAuth {
507
666
  )
508
667
  throw new Error('Account does not match requested account.')
509
668
 
669
+ if (current.action === 'updateAccessKey') {
670
+ if (options.request.action !== 'updateAccessKey')
671
+ throw new Error('Device code action does not match the completion request.')
672
+ if (options.request.chainId !== current.chainId)
673
+ throw new Error('Access key update chain does not match the device code request.')
674
+
675
+ const client = options.client ?? cache.get(current.chainId)
676
+ let replacement: z.output<typeof keyAuthorization> | undefined
677
+ if (current.keyAuthorization) {
678
+ const metadata = await Actions.accessKey
679
+ .getMetadata(client, {
680
+ account: options.request.accountAddress,
681
+ accessKey: current.accessKeyAddress,
682
+ })
683
+ .catch((error) => {
684
+ if (AccessKey.isUnavailableError(error)) return undefined
685
+ throw error
686
+ })
687
+ if (metadata?.address.toLowerCase() === current.accessKeyAddress.toLowerCase())
688
+ throw new Error('Access key was published while the update was pending.')
689
+ if (!options.request.keyAuthorization)
690
+ throw new Error('Pending access key update requires a replacement key authorization.')
691
+ const previous = normalizeKeyAuthorization(current.keyAuthorization)
692
+ const next = await verifyKeyAuthorizationSignature({
693
+ account: options.request.accountAddress,
694
+ client,
695
+ keyAuthorization: options.request.keyAuthorization,
696
+ })
697
+ if (
698
+ next.address.toLowerCase() !== current.accessKeyAddress.toLowerCase() ||
699
+ next.keyId.toLowerCase() !== current.accessKeyAddress.toLowerCase()
700
+ )
701
+ throw new Error('Replacement key authorization does not match the access key request.')
702
+ if (next.chainId !== current.chainId)
703
+ throw new Error(
704
+ 'Replacement key authorization chain does not match the device code request.',
705
+ )
706
+ if (next.keyType !== previous.keyType || next.expiry !== previous.expiry)
707
+ throw new Error('Replacement key authorization changed immutable access key fields.')
708
+ if (!sameLimits(next.limits, current.limits))
709
+ throw new Error(
710
+ 'Replacement key authorization limits do not match the device code request.',
711
+ )
712
+ replacement = toStoredKeyAuthorization(options.request.keyAuthorization, next)
713
+ } else {
714
+ if (options.request.keyAuthorization)
715
+ throw new Error('Published access key update cannot return a key authorization.')
716
+ for (const limit of current.limits) {
717
+ const result = await Actions.accessKey.getRemainingLimit(client, {
718
+ account: options.request.accountAddress,
719
+ accessKey: current.accessKeyAddress,
720
+ token: limit.token,
721
+ })
722
+ if (result.remaining !== limit.limit)
723
+ throw new Error('Access key spending limits do not match the device code request.')
724
+ }
725
+ }
726
+
727
+ const authorized = await store.update({
728
+ accountAddress: options.request.accountAddress,
729
+ code,
730
+ ...(replacement ? { keyAuthorization: replacement } : {}),
731
+ })
732
+ if (!authorized) throw new Error('Unable to complete access key update.')
733
+ return { status: 'authorized' }
734
+ }
735
+
736
+ if (options.request.action === 'updateAccessKey')
737
+ throw new Error('Device code action does not match the completion request.')
738
+
510
739
  const expected = expectedKeyAuthorization(current)
511
740
  const actual = normalizeKeyAuthorization(options.request.keyAuthorization)
512
741
 
513
742
  if (actual.keyId.toLowerCase() !== expected.address.toLowerCase())
514
- throw new Error('Key authorization key does not match the device-code request.')
743
+ throw new Error('Key authorization key does not match the device code request.')
515
744
  if (actual.address.toLowerCase() !== expected.address.toLowerCase())
516
- throw new Error('Key authorization address does not match the device-code request.')
745
+ throw new Error('Key authorization address does not match the device code request.')
517
746
  if (actual.keyType !== expected.type)
518
- throw new Error('Key authorization key type does not match the device-code request.')
747
+ throw new Error('Key authorization key type does not match the device code request.')
519
748
  if (actual.chainId !== expected.chainId)
520
- throw new Error('Key authorization chain does not match the device-code request.')
749
+ throw new Error('Key authorization chain does not match the device code request.')
521
750
 
522
751
  const signed = TempoKeyAuthorization.from({
523
752
  address: actual.address,
@@ -558,17 +787,7 @@ export function from(options: from.Options = {}): CliAuth {
558
787
  },
559
788
  async createDeviceCode(options) {
560
789
  const nextChainId = options.request.chainId ?? chainId ?? cache.defaultChainId
561
- const { account, codeChallenge, pubKey } = options.request
562
- const keyType = options.request.keyType ?? 'secp256k1'
563
- PublicKey.assert(PublicKey.from(pubKey))
564
- const approved = await policy.validate({
565
- ...(account ? { account } : {}),
566
- chainId: typeof nextChainId === 'bigint' ? nextChainId : BigInt(nextChainId),
567
- expiry: options.request.expiry,
568
- keyType,
569
- ...(options.request.limits ? { limits: options.request.limits } : {}),
570
- pubKey,
571
- })
790
+ const chainId_resolved = typeof nextChainId === 'bigint' ? nextChainId : BigInt(nextChainId)
572
791
 
573
792
  let code: string | undefined
574
793
  for (let i = 0; i < 10; i++) {
@@ -581,9 +800,72 @@ export function from(options: from.Options = {}): CliAuth {
581
800
 
582
801
  const createdAt = now()
583
802
 
803
+ if (options.request.action === 'updateAccessKey') {
804
+ if (!policy.update) throw new Error('Access key updates are not supported by this policy.')
805
+ const approved = await policy.update({
806
+ accessKeyAddress: options.request.accessKeyAddress,
807
+ account: options.request.account,
808
+ chainId: chainId_resolved,
809
+ limits: options.request.limits,
810
+ })
811
+ const tokens = new Set<string>()
812
+ for (const limit of approved.limits) {
813
+ const token = limit.token.toLowerCase()
814
+ if (tokens.has(token)) throw new Error('Access key update limits must use unique tokens.')
815
+ tokens.add(token)
816
+ }
817
+ let pendingKeyAuthorization: z.output<typeof keyAuthorization> | undefined
818
+ if (options.request.keyAuthorization) {
819
+ const actual = await verifyKeyAuthorizationSignature({
820
+ account: options.request.account,
821
+ client: options.client ?? cache.get(chainId_resolved),
822
+ keyAuthorization: options.request.keyAuthorization,
823
+ })
824
+ if (
825
+ actual.address.toLowerCase() !== options.request.accessKeyAddress.toLowerCase() ||
826
+ actual.keyId.toLowerCase() !== options.request.accessKeyAddress.toLowerCase()
827
+ )
828
+ throw new Error('Pending key authorization does not match the access key request.')
829
+ if (actual.chainId !== chainId_resolved)
830
+ throw new Error(
831
+ 'Pending key authorization chain does not match the device code request.',
832
+ )
833
+ pendingKeyAuthorization = toStoredKeyAuthorization(
834
+ options.request.keyAuthorization,
835
+ actual,
836
+ )
837
+ }
838
+ await store.create({
839
+ action: 'updateAccessKey',
840
+ accessKeyAddress: options.request.accessKeyAddress,
841
+ account: options.request.account,
842
+ chainId: chainId_resolved,
843
+ code,
844
+ codeChallenge: options.request.codeChallenge,
845
+ createdAt,
846
+ expiresAt: createdAt + ttlMs,
847
+ ...(pendingKeyAuthorization ? { keyAuthorization: pendingKeyAuthorization } : {}),
848
+ limits: approved.limits,
849
+ status: 'pending',
850
+ })
851
+ return { code }
852
+ }
853
+
854
+ const { account, codeChallenge, pubKey } = options.request
855
+ const keyType = options.request.keyType ?? 'secp256k1'
856
+ PublicKey.assert(PublicKey.from(pubKey))
857
+ const approved = await policy.validate({
858
+ ...(account ? { account } : {}),
859
+ chainId: chainId_resolved,
860
+ expiry: options.request.expiry,
861
+ keyType,
862
+ ...(options.request.limits ? { limits: options.request.limits } : {}),
863
+ pubKey,
864
+ })
865
+
584
866
  await store.create({
585
867
  ...(account ? { account } : {}),
586
- chainId: typeof nextChainId === 'bigint' ? nextChainId : BigInt(nextChainId),
868
+ chainId: chainId_resolved,
587
869
  code,
588
870
  codeChallenge,
589
871
  createdAt,
@@ -611,6 +893,18 @@ export function from(options: from.Options = {}): CliAuth {
611
893
  if (current.status !== 'pending')
612
894
  throw new PendingError('Device code already completed.', 400)
613
895
 
896
+ if (current.action === 'updateAccessKey')
897
+ return {
898
+ action: current.action,
899
+ accessKeyAddress: current.accessKeyAddress,
900
+ account: current.account,
901
+ chainId: current.chainId,
902
+ code: current.code,
903
+ ...(current.keyAuthorization ? { keyAuthorization: current.keyAuthorization } : {}),
904
+ limits: current.limits,
905
+ status: current.status,
906
+ }
907
+
614
908
  return {
615
909
  accessKeyAddress: Address.fromPublicKey(PublicKey.from(current.pubKey)),
616
910
  ...(current.account ? { account: current.account } : {}),
@@ -641,6 +935,12 @@ export function from(options: from.Options = {}): CliAuth {
641
935
  }
642
936
  const authorized = await store.consume(normalized)
643
937
  if (!authorized) return { status: 'expired' }
938
+ if (authorized.action === 'updateAccessKey')
939
+ return {
940
+ action: authorized.action,
941
+ ...(authorized.keyAuthorization ? { keyAuthorization: authorized.keyAuthorization } : {}),
942
+ status: 'authorized',
943
+ }
644
944
  return {
645
945
  accountAddress: authorized.accountAddress,
646
946
  keyAuthorization: authorized.keyAuthorization,
@@ -670,7 +970,7 @@ export declare namespace from {
670
970
  policy?: Policy | undefined
671
971
  /** Random byte generator used for verification code allocation. */
672
972
  random?: ((size: number) => Uint8Array) | undefined
673
- /** Device-code store. */
973
+ /** Device code store. */
674
974
  store?: Store | undefined
675
975
  /** Pending entry TTL in milliseconds. @default 600000 */
676
976
  ttlMs?: number | undefined
@@ -705,21 +1005,26 @@ export declare namespace from {
705
1005
  export async function createDeviceCode(
706
1006
  options: createDeviceCode.Options,
707
1007
  ): Promise<createDeviceCode.ReturnType> {
708
- const { request, ...rest } = options
709
- return from(rest).createDeviceCode({ request })
1008
+ const { client, request, ...rest } = options
1009
+ return from(rest).createDeviceCode({
1010
+ ...(client ? { client } : {}),
1011
+ request,
1012
+ })
710
1013
  }
711
1014
 
712
1015
  export declare namespace createDeviceCode {
713
1016
  /** Parameters for creating a new device code. */
714
1017
  export type Parameters = {
715
- /** Incoming device-code creation request. */
1018
+ /** Client used to verify a pending key authorization. */
1019
+ client?: Client<Transport, Chain | undefined> | undefined
1020
+ /** Incoming device code creation request. */
716
1021
  request: z.output<typeof createRequest>
717
1022
  }
718
1023
 
719
- /** Shared CLI auth defaults plus create-device-code parameters. */
1024
+ /** Shared CLI auth defaults plus create device code parameters. */
720
1025
  export type Options = from.Options & Parameters
721
1026
 
722
- /** Created device-code response body. */
1027
+ /** Created device code response body. */
723
1028
  export type ReturnType = z.output<typeof createResponse>
724
1029
  }
725
1030
 
@@ -727,7 +1032,7 @@ export declare namespace createDeviceCode {
727
1032
  * Looks up a pending device code for browser approval UIs.
728
1033
  *
729
1034
  * @param {pending.Options} options - Shared defaults plus the pending lookup parameters.
730
- * @returns {Promise<pending.ReturnType>} Pending device-code payload.
1035
+ * @returns {Promise<pending.ReturnType>} Pending device code payload.
731
1036
  *
732
1037
  * @example
733
1038
  * ```ts
@@ -760,7 +1065,7 @@ export declare namespace pending {
760
1065
  /** Shared CLI auth defaults plus pending lookup parameters. */
761
1066
  export type Options = from.Options & Parameters
762
1067
 
763
- /** Pending device-code response body. */
1068
+ /** Pending device code response body. */
764
1069
  export type ReturnType = z.output<typeof pendingResponse>
765
1070
  }
766
1071
 
@@ -907,7 +1212,7 @@ function normalizeCode(code: string) {
907
1212
  }
908
1213
 
909
1214
  /** @internal */
910
- function expectedKeyAuthorization(entry: Entry.Pending) {
1215
+ function expectedKeyAuthorization(entry: Exclude<Entry.Pending, { action: 'updateAccessKey' }>) {
911
1216
  return TempoKeyAuthorization.from({
912
1217
  address: Address.fromPublicKey(PublicKey.from(entry.pubKey)),
913
1218
  chainId: entry.chainId,
@@ -931,6 +1236,60 @@ function normalizeKeyAuthorization(value: z.output<typeof keyAuthorization>) {
931
1236
  }
932
1237
  }
933
1238
 
1239
+ /** @internal */
1240
+ async function verifyKeyAuthorizationSignature(options: {
1241
+ account: Address.Address
1242
+ client: Client<Transport, Chain | undefined>
1243
+ keyAuthorization: z.output<typeof keyAuthorization>
1244
+ }) {
1245
+ const actual = normalizeKeyAuthorization(options.keyAuthorization)
1246
+ const unsigned = TempoKeyAuthorization.from({
1247
+ address: actual.address,
1248
+ chainId: actual.chainId,
1249
+ expiry: actual.expiry,
1250
+ ...(actual.limits ? { limits: actual.limits } : {}),
1251
+ type: actual.keyType,
1252
+ })
1253
+ const valid = await verifyHash(options.client, {
1254
+ address: options.account,
1255
+ hash: TempoKeyAuthorization.getSignPayload(unsigned),
1256
+ signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
1257
+ magic: actual.signature.type === 'webAuthn',
1258
+ }),
1259
+ })
1260
+ if (!valid) throw new Error('Key authorization signature is invalid.')
1261
+ return actual
1262
+ }
1263
+
1264
+ /** @internal */
1265
+ function toStoredKeyAuthorization(
1266
+ value: z.output<typeof keyAuthorization>,
1267
+ normalized = normalizeKeyAuthorization(value),
1268
+ ) {
1269
+ return {
1270
+ address: value.address,
1271
+ chainId: value.chainId,
1272
+ expiry: normalized.expiry,
1273
+ keyId: value.keyId,
1274
+ keyType: value.keyType,
1275
+ ...(normalized.limits ? { limits: normalized.limits } : {}),
1276
+ signature: value.signature,
1277
+ } satisfies z.output<typeof keyAuthorization>
1278
+ }
1279
+
1280
+ /** @internal */
1281
+ function sameLimits(
1282
+ actual: readonly { token: Address.Address; limit: bigint }[] | undefined,
1283
+ expected: readonly { token: Address.Address; limit: bigint }[],
1284
+ ) {
1285
+ if (!actual || actual.length !== expected.length) return false
1286
+ const normalize = (items: readonly { token: Address.Address; limit: bigint }[]) =>
1287
+ items.map((item) => `${item.token.toLowerCase()}:${item.limit}`).sort()
1288
+ const actualNormalized = normalize(actual)
1289
+ const expectedNormalized = normalize(expected)
1290
+ return actualNormalized.every((value, index) => value === expectedNormalized[index])
1291
+ }
1292
+
934
1293
  /** @internal */
935
1294
  async function verifyCodeChallenge(codeVerifier: string, codeChallenge: string) {
936
1295
  const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))