accounts 0.6.7 → 0.7.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/ExecutionError.d.ts +25 -0
  3. package/dist/core/ExecutionError.d.ts.map +1 -0
  4. package/dist/core/ExecutionError.js +170 -0
  5. package/dist/core/ExecutionError.js.map +1 -0
  6. package/dist/core/Schema.d.ts +33 -7
  7. package/dist/core/Schema.d.ts.map +1 -1
  8. package/dist/core/zod/rpc.d.ts +14 -1
  9. package/dist/core/zod/rpc.d.ts.map +1 -1
  10. package/dist/core/zod/rpc.js +14 -1
  11. package/dist/core/zod/rpc.js.map +1 -1
  12. package/dist/server/CliAuth.d.ts +110 -43
  13. package/dist/server/CliAuth.d.ts.map +1 -1
  14. package/dist/server/CliAuth.js +243 -155
  15. package/dist/server/CliAuth.js.map +1 -1
  16. package/dist/server/Handler.d.ts +0 -1
  17. package/dist/server/Handler.d.ts.map +1 -1
  18. package/dist/server/Handler.js +0 -1
  19. package/dist/server/Handler.js.map +1 -1
  20. package/dist/server/internal/handlers/relay.d.ts +29 -12
  21. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  22. package/dist/server/internal/handlers/relay.js +180 -125
  23. package/dist/server/internal/handlers/relay.js.map +1 -1
  24. package/dist/server/internal/handlers/sponsorship.d.ts +77 -0
  25. package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -0
  26. package/dist/server/internal/handlers/sponsorship.js +96 -0
  27. package/dist/server/internal/handlers/sponsorship.js.map +1 -0
  28. package/dist/server/internal/handlers/utils.d.ts +3 -1
  29. package/dist/server/internal/handlers/utils.d.ts.map +1 -1
  30. package/dist/server/internal/handlers/utils.js +15 -12
  31. package/dist/server/internal/handlers/utils.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/core/ExecutionError.test.ts +205 -0
  34. package/src/core/ExecutionError.ts +189 -0
  35. package/src/core/Provider.test.ts +4 -2
  36. package/src/core/zod/rpc.ts +18 -1
  37. package/src/server/CliAuth.test-d.ts +6 -0
  38. package/src/server/CliAuth.test.ts +83 -0
  39. package/src/server/CliAuth.ts +331 -208
  40. package/src/server/Handler.ts +0 -1
  41. package/src/server/internal/handlers/relay.test.ts +318 -108
  42. package/src/server/internal/handlers/relay.ts +243 -138
  43. package/src/server/internal/handlers/sponsorship.ts +172 -0
  44. package/src/server/internal/handlers/utils.ts +15 -10
  45. package/dist/server/internal/handlers/feePayer.d.ts +0 -73
  46. package/dist/server/internal/handlers/feePayer.d.ts.map +0 -1
  47. package/dist/server/internal/handlers/feePayer.js +0 -184
  48. package/dist/server/internal/handlers/feePayer.js.map +0 -1
  49. package/src/server/internal/handlers/feePayer.test.ts +0 -336
  50. package/src/server/internal/handlers/feePayer.ts +0 -271
@@ -1,7 +1,6 @@
1
- import { Address as core_Address, Base64, Hex, PublicKey } from 'ox'
1
+ 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
- import type { Address } from 'viem/accounts'
5
4
  import { verifyHash } from 'viem/actions'
6
5
  import { tempo } from 'viem/chains'
7
6
  import * as z from 'zod/mini'
@@ -10,6 +9,11 @@ import * as u from '../core/zod/utils.js'
10
9
  import type { MaybePromise } from '../internal/types.js'
11
10
  import type { Kv } from './Kv.js'
12
11
 
12
+ const limit = z.object({ token: u.address(), limit: u.bigint() })
13
+ const limits = z.readonly(z.array(limit))
14
+ const defaultTtlMs = 10 * 60 * 1_000
15
+ const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
16
+
13
17
  /** Supported access-key types for CLI bootstrap. */
14
18
  export const keyType = z.union([z.literal('secp256k1'), z.literal('p256'), z.literal('webAuthn')])
15
19
 
@@ -20,18 +24,18 @@ export const keyAuthorization = z.object({
20
24
  expiry: z.union([u.number(), z.null(), z.undefined()]),
21
25
  keyId: u.address(),
22
26
  keyType,
23
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
27
+ limits: z.optional(limits),
24
28
  signature: z.custom<SignatureEnvelope.SignatureEnvelopeRpc>(),
25
29
  })
26
30
 
27
- /** Request body for `POST /auth/pkce/code`. */
31
+ /** CLI auth device-code creation request body. */
28
32
  export const createRequest = z.object({
29
33
  account: z.optional(u.address()),
30
34
  chainId: z.optional(u.bigint()),
31
35
  codeChallenge: z.string(),
32
36
  expiry: z.optional(z.number()),
33
37
  keyType: z.optional(keyType),
34
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
38
+ limits: z.optional(limits),
35
39
  pubKey: u.hex(),
36
40
  })
37
41
 
@@ -68,7 +72,7 @@ export const pendingResponse = z.object({
68
72
  code: z.string(),
69
73
  expiry: z.number(),
70
74
  keyType,
71
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
75
+ limits: z.optional(limits),
72
76
  pubKey: u.hex(),
73
77
  status: z.literal('pending'),
74
78
  })
@@ -96,7 +100,7 @@ export const entry = u.oneOf([
96
100
  expiresAt: z.number(),
97
101
  expiry: z.number(),
98
102
  keyType,
99
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
103
+ limits: z.optional(limits),
100
104
  pubKey: u.hex(),
101
105
  status: z.literal('pending'),
102
106
  }),
@@ -112,7 +116,7 @@ export const entry = u.oneOf([
112
116
  expiry: z.number(),
113
117
  keyAuthorization,
114
118
  keyType,
115
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
119
+ limits: z.optional(limits),
116
120
  pubKey: u.hex(),
117
121
  status: z.literal('authorized'),
118
122
  }),
@@ -129,12 +133,24 @@ export const entry = u.oneOf([
129
133
  expiry: z.number(),
130
134
  keyAuthorization,
131
135
  keyType,
132
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
136
+ limits: z.optional(limits),
133
137
  pubKey: u.hex(),
134
138
  status: z.literal('consumed'),
135
139
  }),
136
140
  ])
137
141
 
142
+ /** Shared CLI auth helper with pre-bound defaults and cached clients. */
143
+ export type CliAuth = {
144
+ /** Creates and stores a new device code. */
145
+ createDeviceCode: (options: createDeviceCode.Parameters) => Promise<createDeviceCode.ReturnType>
146
+ /** Looks up a pending device code for browser approval UIs. */
147
+ pending: (options: pending.Parameters) => Promise<pending.ReturnType>
148
+ /** Polls a device code with PKCE verification. */
149
+ poll: (options: poll.Parameters) => Promise<poll.ReturnType>
150
+ /** Authorizes a pending device code after validating the signed key authorization. */
151
+ authorize: (options: authorize.Parameters) => Promise<authorize.ReturnType>
152
+ }
153
+
138
154
  /** Stored device-code entry. */
139
155
  export type Entry = z.output<typeof entry>
140
156
 
@@ -152,6 +168,12 @@ export type Store = {
152
168
  delete: (code: string) => MaybePromise<void>
153
169
  }
154
170
 
171
+ /** Host validation and sanitization for requested CLI auth defaults. */
172
+ export type Policy = {
173
+ /** Validates and optionally rewrites requested policy before the entry is stored. */
174
+ validate: (options: Policy.validate.Options) => MaybePromise<Policy.validate.ReturnType>
175
+ }
176
+
155
177
  export declare namespace Entry {
156
178
  /** Pending device-code entry. */
157
179
  export type Pending = Extract<z.output<typeof entry>, { status: 'pending' }>
@@ -165,7 +187,7 @@ export declare namespace Store {
165
187
  export namespace authorize {
166
188
  export type Options = {
167
189
  /** Root account that approved the access key. */
168
- accountAddress: Address
190
+ accountAddress: Address.Address
169
191
  /** Signed key authorization. */
170
192
  keyAuthorization: z.output<typeof keyAuthorization>
171
193
  /** Verification code to authorize. */
@@ -181,34 +203,17 @@ export declare namespace Store {
181
203
  }
182
204
  }
183
205
 
184
- /** Error thrown when pending device-code lookup cannot return a pending request. */
185
- export class PendingError extends Error {
186
- status: 400 | 404
187
-
188
- constructor(message: string, status: 400 | 404) {
189
- super(message)
190
- this.name = 'PendingError'
191
- this.status = status
192
- }
193
- }
194
-
195
- /** Host validation and sanitization for requested CLI auth defaults. */
196
- export type Policy = {
197
- /** Validates and optionally rewrites requested policy before the entry is stored. */
198
- validate: (options: Policy.validate.Options) => MaybePromise<Policy.validate.ReturnType>
199
- }
200
-
201
206
  export declare namespace Policy {
202
207
  export namespace validate {
203
208
  export type Options = {
204
209
  /** Requested root account restriction. */
205
- account?: Address | undefined
210
+ account?: Address.Address | undefined
206
211
  /** Requested access-key expiry timestamp. Omit to let the server choose one. */
207
212
  expiry?: number | undefined
208
213
  /** Requested key type. */
209
214
  keyType: z.output<typeof keyType>
210
215
  /** Requested spending limits. */
211
- limits?: readonly { token: Address; limit: bigint }[] | undefined
216
+ limits?: readonly { token: Address.Address; limit: bigint }[] | undefined
212
217
  /** Requested access-key public key. */
213
218
  pubKey: Hex.Hex
214
219
  }
@@ -217,11 +222,23 @@ export declare namespace Policy {
217
222
  /** Approved access-key expiry timestamp. */
218
223
  expiry: number
219
224
  /** Approved spending limits. */
220
- limits?: readonly { token: Address; limit: bigint }[] | undefined
225
+ limits?: readonly { token: Address.Address; limit: bigint }[] | undefined
221
226
  }
222
227
  }
223
228
  }
224
229
 
230
+ /** Error thrown when pending device-code lookup cannot return a pending request. */
231
+ export class PendingError extends Error {
232
+ /** HTTP status returned by handler surfaces. */
233
+ status: 400 | 404
234
+
235
+ constructor(message: string, status: 400 | 404) {
236
+ super(message)
237
+ this.name = 'PendingError'
238
+ this.status = status
239
+ }
240
+ }
241
+
225
242
  /** Built-in device-code store helpers. */
226
243
  export const Store = {
227
244
  /**
@@ -341,231 +358,316 @@ export const Policy = {
341
358
  },
342
359
  }
343
360
 
344
- /** Creates and stores a new device code. */
345
- export async function createDeviceCode(
346
- options: createDeviceCode.Options,
347
- ): Promise<createDeviceCode.ReturnType> {
361
+ /**
362
+ * Instantiates a CLI auth helper with shared defaults and cached clients.
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * import { CliAuth } from 'accounts/server'
367
+ *
368
+ * const cli = CliAuth.from({
369
+ * store: CliAuth.Store.memory(),
370
+ * })
371
+ *
372
+ * const created = await cli.createDeviceCode({ request })
373
+ * ```
374
+ *
375
+ * @param options - Shared CLI auth defaults.
376
+ * @returns CLI auth helper.
377
+ */
378
+ export function from(options: from.Options = {}): CliAuth {
379
+ const cache = createClientCache(options)
348
380
  const {
381
+ chainId,
349
382
  now = Date.now,
350
383
  policy = Policy.allow(),
351
384
  random = randomBytes,
352
- request,
353
385
  store = Store.memory(),
354
- ttlMs = 10 * 60 * 1_000,
386
+ ttlMs = defaultTtlMs,
355
387
  } = options
356
- const chainId = request.chainId ?? options.chainId ?? BigInt(tempo.id)
357
- const { account, codeChallenge, pubKey } = request
358
- const keyType = request.keyType ?? 'secp256k1'
359
- const approved = await policy.validate({
360
- ...(account ? { account } : {}),
361
- expiry: request.expiry,
362
- keyType,
363
- ...(request.limits ? { limits: request.limits } : {}),
364
- pubKey,
365
- })
366
388
 
367
- let code: string | undefined
368
- for (let i = 0; i < 10; i++) {
369
- const candidate = createCode(random)
370
- if (await store.get(candidate)) continue
371
- code = candidate
372
- break
389
+ return {
390
+ async authorize(options) {
391
+ const code = normalizeCode(options.request.code)
392
+ const current = await store.get(code)
393
+ if (!current) throw new Error('Unknown device code.')
394
+ if (isExpired(current, now)) {
395
+ await store.delete(code)
396
+ throw new Error('Expired device code.')
397
+ }
398
+ if (current.status !== 'pending') throw new Error('Device code already completed.')
399
+ if (
400
+ current.account &&
401
+ current.account.toLowerCase() !== options.request.accountAddress.toLowerCase()
402
+ )
403
+ throw new Error('Account does not match requested account.')
404
+
405
+ const expected = expectedKeyAuthorization(current)
406
+ const actual = normalizeKeyAuthorization(options.request.keyAuthorization)
407
+
408
+ if (actual.keyId.toLowerCase() !== expected.address.toLowerCase())
409
+ throw new Error('Key authorization key does not match the device-code request.')
410
+ if (actual.address.toLowerCase() !== expected.address.toLowerCase())
411
+ throw new Error('Key authorization address does not match the device-code request.')
412
+ if (actual.keyType !== expected.type)
413
+ throw new Error('Key authorization key type does not match the device-code request.')
414
+ if (actual.chainId !== expected.chainId)
415
+ throw new Error('Key authorization chain does not match the device-code request.')
416
+ if ((actual.expiry ?? undefined) !== (expected.expiry ?? undefined))
417
+ throw new Error('Key authorization expiry does not match the device-code request.')
418
+ if (!sameLimits(actual.limits, expected.limits))
419
+ throw new Error('Key authorization limits do not match the device-code request.')
420
+
421
+ const valid = await verifyHash((options.client ?? cache.get(current.chainId)) as never, {
422
+ address: options.request.accountAddress,
423
+ hash: TempoKeyAuthorization.getSignPayload(expected),
424
+ signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
425
+ magic: actual.signature.type === 'webAuthn',
426
+ }),
427
+ })
428
+ if (!valid) throw new Error('Key authorization signature is invalid.')
429
+
430
+ const authorized = await store.authorize({
431
+ accountAddress: options.request.accountAddress,
432
+ code,
433
+ keyAuthorization: options.request.keyAuthorization,
434
+ })
435
+ if (!authorized) throw new Error('Unable to authorize device code.')
436
+
437
+ return { status: 'authorized' }
438
+ },
439
+ async createDeviceCode(options) {
440
+ const nextChainId = options.request.chainId ?? chainId ?? cache.defaultChainId
441
+ const { account, codeChallenge, pubKey } = options.request
442
+ const keyType = options.request.keyType ?? 'secp256k1'
443
+ const approved = await policy.validate({
444
+ ...(account ? { account } : {}),
445
+ expiry: options.request.expiry,
446
+ keyType,
447
+ ...(options.request.limits ? { limits: options.request.limits } : {}),
448
+ pubKey,
449
+ })
450
+
451
+ let code: string | undefined
452
+ for (let i = 0; i < 10; i++) {
453
+ const candidate = createCode(random)
454
+ if (await store.get(candidate)) continue
455
+ code = candidate
456
+ break
457
+ }
458
+ if (!code) throw new Error('Unable to allocate device code.')
459
+
460
+ const createdAt = now()
461
+
462
+ await store.create({
463
+ ...(account ? { account } : {}),
464
+ chainId: typeof nextChainId === 'bigint' ? nextChainId : BigInt(nextChainId),
465
+ code,
466
+ codeChallenge,
467
+ createdAt,
468
+ expiresAt: createdAt + ttlMs,
469
+ expiry: approved.expiry,
470
+ keyType,
471
+ ...(approved.limits ? { limits: approved.limits } : {}),
472
+ pubKey,
473
+ status: 'pending',
474
+ })
475
+
476
+ return { code }
477
+ },
478
+ async pending(options) {
479
+ const normalized = normalizeCode(options.code)
480
+ const current = await store.get(normalized)
481
+ if (!current) throw new PendingError('Unknown device code.', 404)
482
+ if (isExpired(current, now)) {
483
+ await store.delete(normalized)
484
+ throw new PendingError('Expired device code.', 404)
485
+ }
486
+ if (current.status !== 'pending')
487
+ throw new PendingError('Device code already completed.', 400)
488
+
489
+ return {
490
+ accessKeyAddress: Address.fromPublicKey(PublicKey.from(current.pubKey)),
491
+ ...(current.account ? { account: current.account } : {}),
492
+ chainId: current.chainId,
493
+ code: current.code,
494
+ expiry: current.expiry,
495
+ keyType: current.keyType,
496
+ ...(current.limits ? { limits: current.limits } : {}),
497
+ pubKey: current.pubKey,
498
+ status: 'pending',
499
+ }
500
+ },
501
+ async poll(options) {
502
+ const normalized = normalizeCode(options.code)
503
+ const current = await store.get(normalized)
504
+ if (!current) return { status: 'expired' }
505
+ if (isExpired(current, now)) {
506
+ await store.delete(normalized)
507
+ return { status: 'expired' }
508
+ }
509
+ if (!(await verifyCodeChallenge(options.request.codeVerifier, current.codeChallenge)))
510
+ throw new Error('Invalid code verifier.')
511
+ if (current.status === 'pending') return { status: 'pending' }
512
+ if (current.status === 'consumed') {
513
+ await store.delete(normalized)
514
+ return { status: 'expired' }
515
+ }
516
+ const authorized = await store.consume(normalized)
517
+ if (!authorized) return { status: 'expired' }
518
+ return {
519
+ accountAddress: authorized.accountAddress,
520
+ keyAuthorization: authorized.keyAuthorization,
521
+ status: 'authorized',
522
+ }
523
+ },
373
524
  }
374
- if (!code) throw new Error('Unable to allocate device code.')
375
-
376
- const createdAt = now()
377
-
378
- await store.create({
379
- ...(account ? { account } : {}),
380
- chainId: typeof chainId === 'bigint' ? chainId : BigInt(chainId),
381
- code,
382
- codeChallenge,
383
- createdAt,
384
- expiresAt: createdAt + ttlMs,
385
- expiry: approved.expiry,
386
- keyType,
387
- ...(approved.limits ? { limits: approved.limits } : {}),
388
- pubKey,
389
- status: 'pending',
390
- })
391
-
392
- return { code }
393
525
  }
394
526
 
395
- export declare namespace createDeviceCode {
527
+ export declare namespace from {
528
+ /** Shared CLI auth helper configuration. */
396
529
  export type Options = {
397
- /** Chain ID embedded into the expected key authorization. @default tempo.id */
530
+ /** Default chain ID embedded into created device codes. @default tempo.id */
398
531
  chainId?: bigint | number | undefined
532
+ /**
533
+ * Preconfigured chains used to build and cache viem clients.
534
+ *
535
+ * Unknown chain IDs are cached lazily using a tempo-shaped chain object so
536
+ * standalone helpers can still verify signatures without a full chain list.
537
+ *
538
+ * @default [tempo]
539
+ */
540
+ chains?: readonly [Chain, ...Chain[]] | undefined
399
541
  /** Time source used for TTL evaluation. */
400
542
  now?: (() => number) | undefined
401
543
  /** Policy used to validate requested expiry and limits. */
402
544
  policy?: Policy | undefined
403
545
  /** Random byte generator used for verification code allocation. */
404
546
  random?: ((size: number) => Uint8Array) | undefined
405
- /** Incoming device-code creation request. */
406
- request: z.output<typeof createRequest>
407
547
  /** Device-code store. */
408
548
  store?: Store | undefined
409
549
  /** Pending entry TTL in milliseconds. @default 600000 */
410
550
  ttlMs?: number | undefined
551
+ /** Transports keyed by chain ID. Defaults to `http()` for each chain. */
552
+ transports?: Record<number, Transport> | undefined
411
553
  }
554
+ }
412
555
 
413
- export type ReturnType = z.output<typeof createResponse>
556
+ /**
557
+ * Creates and stores a new device code.
558
+ *
559
+ * @param options - Shared defaults plus the incoming request.
560
+ * @returns Created device code.
561
+ */
562
+ export async function createDeviceCode(
563
+ options: createDeviceCode.Options,
564
+ ): Promise<createDeviceCode.ReturnType> {
565
+ const { request, ...rest } = options
566
+ return from(rest).createDeviceCode({ request })
414
567
  }
415
568
 
416
- /** Looks up a pending device code for browser approval UIs. */
417
- export async function pending(options: pending.Options): Promise<pending.ReturnType> {
418
- const { code, now = Date.now, store = Store.memory() } = options
419
- const normalized = normalizeCode(code)
420
- const current = await store.get(normalized)
421
- if (!current) throw new PendingError('Unknown device code.', 404)
422
- if (isExpired(current, now)) {
423
- await store.delete(normalized)
424
- throw new PendingError('Expired device code.', 404)
569
+ export declare namespace createDeviceCode {
570
+ /** Parameters for creating a new device code. */
571
+ export type Parameters = {
572
+ /** Incoming device-code creation request. */
573
+ request: z.output<typeof createRequest>
425
574
  }
426
- if (current.status !== 'pending') throw new PendingError('Device code already completed.', 400)
427
575
 
428
- return {
429
- accessKeyAddress: core_Address.fromPublicKey(PublicKey.from(current.pubKey)),
430
- ...(current.account ? { account: current.account } : {}),
431
- chainId: current.chainId,
432
- code: current.code,
433
- expiry: current.expiry,
434
- keyType: current.keyType,
435
- ...(current.limits ? { limits: current.limits } : {}),
436
- pubKey: current.pubKey,
437
- status: 'pending',
438
- }
576
+ /** Shared CLI auth defaults plus create-device-code parameters. */
577
+ export type Options = from.Options & Parameters
578
+
579
+ /** Created device-code response body. */
580
+ export type ReturnType = z.output<typeof createResponse>
581
+ }
582
+
583
+ /**
584
+ * Looks up a pending device code for browser approval UIs.
585
+ *
586
+ * @param options - Shared defaults plus the pending lookup parameters.
587
+ * @returns Pending device-code payload.
588
+ */
589
+ export async function pending(options: pending.Options): Promise<pending.ReturnType> {
590
+ const { code, ...rest } = options
591
+ return from(rest).pending({ code })
439
592
  }
440
593
 
441
594
  export declare namespace pending {
442
- export type Options = {
595
+ /** Parameters for looking up a pending device code. */
596
+ export type Parameters = {
443
597
  /** Verification code from the route path. */
444
598
  code: string
445
- /** Time source used for TTL evaluation. */
446
- now?: (() => number) | undefined
447
- /** Device-code store. */
448
- store?: Store | undefined
449
599
  }
450
600
 
601
+ /** Shared CLI auth defaults plus pending lookup parameters. */
602
+ export type Options = from.Options & Parameters
603
+
604
+ /** Pending device-code response body. */
451
605
  export type ReturnType = z.output<typeof pendingResponse>
452
606
  }
453
607
 
454
- /** Polls a device code with PKCE verification. */
608
+ /**
609
+ * Polls a device code with PKCE verification.
610
+ *
611
+ * @param options - Shared defaults plus the poll parameters.
612
+ * @returns Pending, authorized, or expired poll response.
613
+ */
455
614
  export async function poll(options: poll.Options): Promise<poll.ReturnType> {
456
- const { code, now = Date.now, request, store = Store.memory() } = options
457
- const normalized = normalizeCode(code)
458
- const current = await store.get(normalized)
459
- if (!current) return { status: 'expired' }
460
- if (isExpired(current, now)) {
461
- await store.delete(normalized)
462
- return { status: 'expired' }
463
- }
464
- if (!(await verifyCodeChallenge(request.codeVerifier, current.codeChallenge)))
465
- throw new Error('Invalid code verifier.')
466
- if (current.status === 'pending') return { status: 'pending' }
467
- if (current.status === 'consumed') {
468
- await store.delete(normalized)
469
- return { status: 'expired' }
470
- }
471
- const authorized = await store.consume(normalized)
472
- if (!authorized) return { status: 'expired' }
473
- return {
474
- accountAddress: authorized.accountAddress,
475
- keyAuthorization: authorized.keyAuthorization,
476
- status: 'authorized',
477
- }
615
+ const { code, request, ...rest } = options
616
+ return from(rest).poll({ code, request })
478
617
  }
479
618
 
480
619
  export declare namespace poll {
481
- export type Options = {
620
+ /** Parameters for polling a device code. */
621
+ export type Parameters = {
482
622
  /** Verification code from the route path. */
483
623
  code: string
484
- /** Time source used for TTL evaluation. */
485
- now?: (() => number) | undefined
486
624
  /** Poll request body. */
487
625
  request: z.output<typeof pollRequest>
488
- /** Device-code store. */
489
- store?: Store | undefined
490
626
  }
491
627
 
628
+ /** Shared CLI auth defaults plus poll parameters. */
629
+ export type Options = from.Options & Parameters
630
+
631
+ /** Poll response body. */
492
632
  export type ReturnType = z.output<typeof pollResponse>
493
633
  }
494
634
 
495
- /** Authorizes a pending device code after validating the signed key authorization. */
635
+ /**
636
+ * Authorizes a pending device code after validating the signed key authorization.
637
+ *
638
+ * @param options - Shared defaults plus the authorization request.
639
+ * @returns Authorized response body.
640
+ */
496
641
  export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
497
- const {
498
- chainId,
499
- client = getClient({ chainId }),
500
- now = Date.now,
642
+ const { client, request, ...rest } = options
643
+ return from(rest).authorize({
644
+ ...(client ? { client } : {}),
501
645
  request,
502
- store = Store.memory(),
503
- } = options
504
- const code = normalizeCode(request.code)
505
- const current = await store.get(code)
506
- if (!current) throw new Error('Unknown device code.')
507
- if (isExpired(current, now)) {
508
- await store.delete(code)
509
- throw new Error('Expired device code.')
510
- }
511
- if (current.status !== 'pending') throw new Error('Device code already completed.')
512
- if (current.account && current.account.toLowerCase() !== request.accountAddress.toLowerCase())
513
- throw new Error('Account does not match requested account.')
514
-
515
- const expected = expectedKeyAuthorization(current)
516
- const actual = normalizeKeyAuthorization(request.keyAuthorization)
517
-
518
- if (actual.keyId.toLowerCase() !== expected.address.toLowerCase())
519
- throw new Error('Key authorization key does not match the device-code request.')
520
- if (actual.address.toLowerCase() !== expected.address.toLowerCase())
521
- throw new Error('Key authorization address does not match the device-code request.')
522
- if (actual.keyType !== expected.type)
523
- throw new Error('Key authorization key type does not match the device-code request.')
524
- if (actual.chainId !== expected.chainId)
525
- throw new Error('Key authorization chain does not match the device-code request.')
526
- if ((actual.expiry ?? undefined) !== (expected.expiry ?? undefined))
527
- throw new Error('Key authorization expiry does not match the device-code request.')
528
- if (!sameLimits(actual.limits, expected.limits))
529
- throw new Error('Key authorization limits do not match the device-code request.')
530
-
531
- const valid = await verifyHash(client as never, {
532
- address: request.accountAddress,
533
- hash: TempoKeyAuthorization.getSignPayload(expected),
534
- signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
535
- magic: actual.signature.type === 'webAuthn',
536
- }),
537
646
  })
538
- if (!valid) throw new Error('Key authorization signature is invalid.')
539
-
540
- const authorized = await store.authorize({
541
- accountAddress: request.accountAddress,
542
- code,
543
- keyAuthorization: request.keyAuthorization,
544
- })
545
- if (!authorized) throw new Error('Unable to authorize device code.')
546
-
547
- return { status: 'authorized' }
548
647
  }
549
648
 
550
649
  export declare namespace authorize {
551
- export type Options = {
552
- /** Chain ID embedded into the expected key authorization. Defaults to the client chain or tempo.id. */
553
- chainId?: bigint | number | undefined
650
+ /** Parameters for authorizing a pending device code. */
651
+ export type Parameters = {
554
652
  /** Client used to verify the signed key authorization. */
555
653
  client?: Client<Transport, Chain | undefined> | undefined
556
- /** Time source used for TTL evaluation. */
557
- now?: (() => number) | undefined
558
654
  /** Authorize request body. */
559
655
  request: z.output<typeof authorizeRequest>
560
- /** Device-code store. */
561
- store?: Store | undefined
562
656
  }
563
657
 
658
+ /** Shared CLI auth defaults plus authorization parameters. */
659
+ export type Options = from.Options & Parameters
660
+
661
+ /** Authorization response body. */
564
662
  export type ReturnType = z.output<typeof authorizeResponse>
565
663
  }
566
664
 
567
- const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
665
+ /** @internal */
666
+ function randomBytes(size: number) {
667
+ return Bytes.random(size)
668
+ }
568
669
 
670
+ /** @internal */
569
671
  function createCode(random: (size: number) => Uint8Array) {
570
672
  const bytes = random(8)
571
673
  let code = ''
@@ -573,13 +675,47 @@ function createCode(random: (size: number) => Uint8Array) {
573
675
  return code
574
676
  }
575
677
 
678
+ /** @internal */
679
+ function createClientCache(options: from.Options = {}) {
680
+ const chains = options.chains ?? [tempo]
681
+ const transports = options.transports ?? {}
682
+ const clients = new Map<number, Client<Transport, Chain | undefined>>()
683
+
684
+ for (const chain of chains) {
685
+ const transport = transports[chain.id] ?? http()
686
+ clients.set(chain.id, createClient({ chain, transport }))
687
+ }
688
+
689
+ const defaultChainId = options.chainId ?? chains[0]!.id
690
+
691
+ return {
692
+ defaultChainId,
693
+ get(chainId: bigint | number = defaultChainId) {
694
+ const id = typeof chainId === 'bigint' ? Number(chainId) : chainId
695
+ const current = clients.get(id)
696
+ if (current) return current
697
+ const client = createClient({
698
+ chain: {
699
+ ...tempo,
700
+ id,
701
+ },
702
+ transport: transports[id] ?? http(),
703
+ })
704
+ clients.set(id, client)
705
+ return client
706
+ },
707
+ }
708
+ }
709
+
710
+ /** @internal */
576
711
  function normalizeCode(code: string) {
577
712
  return code.replaceAll('-', '').toUpperCase()
578
713
  }
579
714
 
715
+ /** @internal */
580
716
  function expectedKeyAuthorization(entry: Entry.Pending) {
581
717
  return TempoKeyAuthorization.from({
582
- address: core_Address.fromPublicKey(PublicKey.from(entry.pubKey)),
718
+ address: Address.fromPublicKey(PublicKey.from(entry.pubKey)),
583
719
  chainId: entry.chainId,
584
720
  expiry: entry.expiry,
585
721
  ...(entry.limits ? { limits: entry.limits } : {}),
@@ -587,23 +723,12 @@ function expectedKeyAuthorization(entry: Entry.Pending) {
587
723
  })
588
724
  }
589
725
 
590
- function getClient(options: { chainId?: bigint | number | undefined } = {}) {
591
- const chainId = options.chainId
592
- return createClient({
593
- chain: chainId
594
- ? {
595
- ...tempo,
596
- id: typeof chainId === 'bigint' ? Number(chainId) : chainId,
597
- }
598
- : tempo,
599
- transport: http(),
600
- })
601
- }
602
-
726
+ /** @internal */
603
727
  function isExpired(entry: Entry, now: () => number) {
604
728
  return now() > entry.expiresAt
605
729
  }
606
730
 
731
+ /** @internal */
607
732
  function normalizeKeyAuthorization(value: z.output<typeof keyAuthorization>) {
608
733
  return {
609
734
  ...value,
@@ -612,13 +737,10 @@ function normalizeKeyAuthorization(value: z.output<typeof keyAuthorization>) {
612
737
  }
613
738
  }
614
739
 
615
- function randomBytes(size: number) {
616
- return crypto.getRandomValues(new Uint8Array(size))
617
- }
618
-
740
+ /** @internal */
619
741
  function sameLimits(
620
- a: readonly { token: Address; limit: bigint }[] | undefined,
621
- b: readonly { token: Address; limit: bigint }[] | undefined,
742
+ a: Policy.validate.ReturnType['limits'],
743
+ b: Policy.validate.ReturnType['limits'],
622
744
  ) {
623
745
  if (!a && !b) return true
624
746
  if (!a || !b || a.length !== b.length) return false
@@ -629,6 +751,7 @@ function sameLimits(
629
751
  })
630
752
  }
631
753
 
754
+ /** @internal */
632
755
  async function verifyCodeChallenge(codeVerifier: string, codeChallenge: string) {
633
756
  const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
634
757
  return Base64.fromBytes(new Uint8Array(hash), { pad: false, url: true }) === codeChallenge