accounts 0.4.0 → 0.4.2

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 (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +38 -7
  3. package/dist/cli/Provider.d.ts +12 -0
  4. package/dist/cli/Provider.d.ts.map +1 -0
  5. package/dist/cli/Provider.js +19 -0
  6. package/dist/cli/Provider.js.map +1 -0
  7. package/dist/cli/adapter.d.ts +24 -0
  8. package/dist/cli/adapter.d.ts.map +1 -0
  9. package/dist/cli/adapter.js +173 -0
  10. package/dist/cli/adapter.js.map +1 -0
  11. package/dist/cli/index.d.ts +3 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +3 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/core/Dialog.d.ts.map +1 -1
  16. package/dist/core/Dialog.js +25 -1
  17. package/dist/core/Dialog.js.map +1 -1
  18. package/dist/core/IntersectionObserver.d.ts +3 -0
  19. package/dist/core/IntersectionObserver.d.ts.map +1 -0
  20. package/dist/core/IntersectionObserver.js +6 -0
  21. package/dist/core/IntersectionObserver.js.map +1 -0
  22. package/dist/core/Messenger.d.ts +14 -3
  23. package/dist/core/Messenger.d.ts.map +1 -1
  24. package/dist/core/Messenger.js +4 -4
  25. package/dist/core/Messenger.js.map +1 -1
  26. package/dist/core/Remote.d.ts +6 -3
  27. package/dist/core/Remote.d.ts.map +1 -1
  28. package/dist/core/Remote.js +3 -6
  29. package/dist/core/Remote.js.map +1 -1
  30. package/dist/core/adapters/local.d.ts.map +1 -1
  31. package/dist/core/adapters/local.js +2 -2
  32. package/dist/core/adapters/local.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/react/Remote.d.ts +21 -0
  38. package/dist/react/Remote.d.ts.map +1 -0
  39. package/dist/react/Remote.js +51 -0
  40. package/dist/react/Remote.js.map +1 -0
  41. package/dist/react/index.d.ts +2 -0
  42. package/dist/react/index.d.ts.map +1 -0
  43. package/dist/react/index.js +2 -0
  44. package/dist/react/index.js.map +1 -0
  45. package/dist/server/CliAuth.d.ts +553 -0
  46. package/dist/server/CliAuth.d.ts.map +1 -0
  47. package/dist/server/CliAuth.js +446 -0
  48. package/dist/server/CliAuth.js.map +1 -0
  49. package/dist/server/Handler.d.ts +36 -2
  50. package/dist/server/Handler.d.ts.map +1 -1
  51. package/dist/server/Handler.js +84 -0
  52. package/dist/server/Handler.js.map +1 -1
  53. package/dist/server/index.d.ts +1 -0
  54. package/dist/server/index.d.ts.map +1 -1
  55. package/dist/server/index.js +1 -0
  56. package/dist/server/index.js.map +1 -1
  57. package/package.json +16 -54
  58. package/src/cli/Provider.test-d.ts +28 -0
  59. package/src/cli/Provider.test.ts +235 -0
  60. package/src/cli/Provider.ts +26 -0
  61. package/src/cli/adapter.ts +229 -0
  62. package/src/cli/index.ts +2 -0
  63. package/src/core/Dialog.ts +31 -1
  64. package/src/core/IntersectionObserver.ts +6 -0
  65. package/src/core/Messenger.ts +18 -8
  66. package/src/core/Provider.test.ts +12 -2
  67. package/src/core/Remote.ts +9 -10
  68. package/src/core/adapters/local.ts +7 -2
  69. package/src/index.ts +1 -0
  70. package/src/react/Remote.ts +94 -0
  71. package/src/react/index.ts +1 -0
  72. package/src/server/CliAuth.test-d.ts +56 -0
  73. package/src/server/CliAuth.test.ts +800 -0
  74. package/src/server/CliAuth.ts +634 -0
  75. package/src/server/Handler.ts +123 -1
  76. package/src/server/index.ts +1 -0
@@ -0,0 +1,634 @@
1
+ import { Address as core_Address, Base64, Hex, PublicKey } from 'ox'
2
+ import { KeyAuthorization as TempoKeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
+ import { createClient, http, type Chain, type Client, type Transport } from 'viem'
4
+ import type { Address } from 'viem/accounts'
5
+ import { verifyHash } from 'viem/actions'
6
+ import { tempo } from 'viem/chains'
7
+ import * as z from 'zod/mini'
8
+
9
+ import * as u from '../core/zod/utils.js'
10
+ import type { MaybePromise } from '../internal/types.js'
11
+ import type { Kv } from './Kv.js'
12
+
13
+ /** Supported access-key types for CLI bootstrap. */
14
+ export const keyType = z.union([z.literal('secp256k1'), z.literal('p256'), z.literal('webAuthn')])
15
+
16
+ /** Signed key authorization returned by the device-code flow. */
17
+ export const keyAuthorization = z.object({
18
+ address: u.address(),
19
+ chainId: u.bigint(),
20
+ expiry: z.nullish(u.number()),
21
+ keyId: u.address(),
22
+ keyType,
23
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
24
+ signature: z.custom<SignatureEnvelope.SignatureEnvelopeRpc>(),
25
+ })
26
+
27
+ /** Request body for `POST /auth/pkce/code`. */
28
+ export const createRequest = z.object({
29
+ account: z.optional(u.address()),
30
+ codeChallenge: z.string(),
31
+ expiry: z.optional(z.number()),
32
+ keyType: z.optional(keyType),
33
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
34
+ pubKey: u.hex(),
35
+ })
36
+
37
+ /** Response body for `POST /cli-auth/device-code`. */
38
+ export const createResponse = z.object({
39
+ code: z.string(),
40
+ })
41
+
42
+ /** Request body for `POST /auth/pkce/poll/:code`. */
43
+ export const pollRequest = z.object({
44
+ codeVerifier: z.string(),
45
+ })
46
+
47
+ /** Response body for `POST /auth/pkce/poll/:code`. */
48
+ export const pollResponse = u.oneOf([
49
+ z.object({
50
+ status: z.literal('pending'),
51
+ }),
52
+ z.object({
53
+ status: z.literal('authorized'),
54
+ accountAddress: u.address(),
55
+ keyAuthorization: keyAuthorization,
56
+ }),
57
+ z.object({
58
+ status: z.literal('expired'),
59
+ }),
60
+ ])
61
+
62
+ /** Response body for `GET /auth/pkce/pending/:code`. */
63
+ export const pendingResponse = z.object({
64
+ accessKeyAddress: u.address(),
65
+ account: z.optional(u.address()),
66
+ chainId: u.bigint(),
67
+ code: z.string(),
68
+ expiry: z.number(),
69
+ keyType,
70
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
71
+ pubKey: u.hex(),
72
+ status: z.literal('pending'),
73
+ })
74
+
75
+ /** Request body for `POST /auth/pkce`. */
76
+ export const authorizeRequest = z.object({
77
+ accountAddress: u.address(),
78
+ code: z.string(),
79
+ keyAuthorization: keyAuthorization,
80
+ })
81
+
82
+ /** Response body for `POST /cli-auth/authorize`. */
83
+ export const authorizeResponse = z.object({
84
+ status: z.literal('authorized'),
85
+ })
86
+
87
+ /** Stored device-code entry schema. */
88
+ export const entry = u.oneOf([
89
+ z.object({
90
+ account: z.optional(u.address()),
91
+ chainId: u.bigint(),
92
+ code: z.string(),
93
+ codeChallenge: z.string(),
94
+ createdAt: z.number(),
95
+ expiresAt: z.number(),
96
+ expiry: z.number(),
97
+ keyType,
98
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
99
+ pubKey: u.hex(),
100
+ status: z.literal('pending'),
101
+ }),
102
+ z.object({
103
+ account: z.optional(u.address()),
104
+ accountAddress: u.address(),
105
+ authorizedAt: z.number(),
106
+ chainId: u.bigint(),
107
+ code: z.string(),
108
+ codeChallenge: z.string(),
109
+ createdAt: z.number(),
110
+ expiresAt: z.number(),
111
+ expiry: z.number(),
112
+ keyAuthorization,
113
+ keyType,
114
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
115
+ pubKey: u.hex(),
116
+ status: z.literal('authorized'),
117
+ }),
118
+ z.object({
119
+ account: z.optional(u.address()),
120
+ accountAddress: u.address(),
121
+ authorizedAt: z.number(),
122
+ chainId: u.bigint(),
123
+ code: z.string(),
124
+ codeChallenge: z.string(),
125
+ consumedAt: z.number(),
126
+ createdAt: z.number(),
127
+ expiresAt: z.number(),
128
+ expiry: z.number(),
129
+ keyAuthorization,
130
+ keyType,
131
+ limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
132
+ pubKey: u.hex(),
133
+ status: z.literal('consumed'),
134
+ }),
135
+ ])
136
+
137
+ /** Stored device-code entry. */
138
+ export type Entry = z.output<typeof entry>
139
+
140
+ /** Device-code storage contract. */
141
+ export type Store = {
142
+ /** Saves a new pending device-code entry. */
143
+ create: (entry: Entry.Pending) => MaybePromise<void>
144
+ /** Loads a device-code entry by verification code. */
145
+ get: (code: string) => MaybePromise<Entry | undefined>
146
+ /** Marks a pending device-code as authorized. */
147
+ authorize: (options: Store.authorize.Options) => MaybePromise<Entry.Authorized | undefined>
148
+ /** Consumes an authorized device-code exactly once. */
149
+ consume: (code: string) => MaybePromise<Entry.Authorized | undefined>
150
+ /** Deletes a device-code entry. */
151
+ delete: (code: string) => MaybePromise<void>
152
+ }
153
+
154
+ export declare namespace Entry {
155
+ /** Pending device-code entry. */
156
+ export type Pending = Extract<z.output<typeof entry>, { status: 'pending' }>
157
+ /** Authorized device-code entry. */
158
+ export type Authorized = Extract<z.output<typeof entry>, { status: 'authorized' }>
159
+ /** Consumed device-code entry. */
160
+ export type Consumed = Extract<z.output<typeof entry>, { status: 'consumed' }>
161
+ }
162
+
163
+ export declare namespace Store {
164
+ export namespace authorize {
165
+ export type Options = {
166
+ /** Root account that approved the access key. */
167
+ accountAddress: Address
168
+ /** Signed key authorization. */
169
+ keyAuthorization: z.output<typeof keyAuthorization>
170
+ /** Verification code to authorize. */
171
+ code: string
172
+ }
173
+ }
174
+
175
+ export namespace kv {
176
+ export type Options = {
177
+ /** Prefix used for KV keys. @default "cli-auth" */
178
+ key?: string | undefined
179
+ }
180
+ }
181
+ }
182
+
183
+ /** Error thrown when pending device-code lookup cannot return a pending request. */
184
+ export class PendingError extends Error {
185
+ status: 400 | 404
186
+
187
+ constructor(message: string, status: 400 | 404) {
188
+ super(message)
189
+ this.name = 'PendingError'
190
+ this.status = status
191
+ }
192
+ }
193
+
194
+ /** Host validation and sanitization for requested CLI auth defaults. */
195
+ export type Policy = {
196
+ /** Validates and optionally rewrites requested policy before the entry is stored. */
197
+ validate: (options: Policy.validate.Options) => MaybePromise<Policy.validate.ReturnType>
198
+ }
199
+
200
+ export declare namespace Policy {
201
+ export namespace validate {
202
+ export type Options = {
203
+ /** Requested root account restriction. */
204
+ account?: Address | undefined
205
+ /** Requested access-key expiry timestamp. Omit to let the server choose one. */
206
+ expiry?: number | undefined
207
+ /** Requested key type. */
208
+ keyType: z.output<typeof keyType>
209
+ /** Requested spending limits. */
210
+ limits?: readonly { token: Address; limit: bigint }[] | undefined
211
+ /** Requested access-key public key. */
212
+ pubKey: Hex.Hex
213
+ }
214
+
215
+ export type ReturnType = {
216
+ /** Approved access-key expiry timestamp. */
217
+ expiry: number
218
+ /** Approved spending limits. */
219
+ limits?: readonly { token: Address; limit: bigint }[] | undefined
220
+ }
221
+ }
222
+ }
223
+
224
+ /** Built-in device-code store helpers. */
225
+ export const Store = {
226
+ /**
227
+ * Creates an in-memory device-code store.
228
+ *
229
+ * Useful for tests and single-process servers.
230
+ */
231
+ memory(): Store {
232
+ const entries = new Map<string, Entry>()
233
+
234
+ return {
235
+ async authorize(options) {
236
+ const current = entries.get(options.code)
237
+ if (!current || current.status !== 'pending') return undefined
238
+ const next = {
239
+ ...current,
240
+ accountAddress: options.accountAddress,
241
+ authorizedAt: Date.now(),
242
+ keyAuthorization: options.keyAuthorization,
243
+ status: 'authorized',
244
+ } satisfies Entry.Authorized
245
+ entries.set(options.code, next)
246
+ return next
247
+ },
248
+ async consume(code) {
249
+ const current = entries.get(code)
250
+ if (!current || current.status !== 'authorized') return undefined
251
+ entries.set(code, {
252
+ ...current,
253
+ consumedAt: Date.now(),
254
+ status: 'consumed',
255
+ } satisfies Entry.Consumed)
256
+ return current
257
+ },
258
+ async create(entry_) {
259
+ entries.set(entry_.code, entry_)
260
+ },
261
+ async delete(code) {
262
+ entries.delete(code)
263
+ },
264
+ async get(code) {
265
+ return entries.get(code)
266
+ },
267
+ }
268
+ },
269
+ /**
270
+ * Creates a key-value backed device-code store.
271
+ *
272
+ * Stored values are encoded through the shared entry schema so they remain
273
+ * JSON-safe across KV implementations.
274
+ */
275
+ kv(kv: Kv, options: Store.kv.Options = {}): Store {
276
+ const key = options.key ?? 'cli-auth'
277
+
278
+ function toKey(code: string) {
279
+ return `${key}:${code}`
280
+ }
281
+
282
+ return {
283
+ async authorize(options) {
284
+ const current = await this.get(options.code)
285
+ if (!current || current.status !== 'pending') return undefined
286
+ const next = {
287
+ ...current,
288
+ accountAddress: options.accountAddress,
289
+ authorizedAt: Date.now(),
290
+ keyAuthorization: options.keyAuthorization,
291
+ status: 'authorized',
292
+ } satisfies Entry.Authorized
293
+ await kv.set(toKey(options.code), z.encode(entry, next))
294
+ return next
295
+ },
296
+ async consume(code) {
297
+ const current = await this.get(code)
298
+ if (!current || current.status !== 'authorized') return undefined
299
+ await kv.set(
300
+ toKey(code),
301
+ z.encode(entry, {
302
+ ...current,
303
+ consumedAt: Date.now(),
304
+ status: 'consumed',
305
+ } satisfies Entry.Consumed),
306
+ )
307
+ return current
308
+ },
309
+ async create(entry_) {
310
+ await kv.set(toKey(entry_.code), z.encode(entry, entry_))
311
+ },
312
+ async delete(code) {
313
+ await kv.delete(toKey(code))
314
+ },
315
+ async get(code) {
316
+ const value = await kv.get<z.input<typeof entry>>(toKey(code))
317
+ if (!value) return undefined
318
+ return z.decode(entry, value)
319
+ },
320
+ }
321
+ },
322
+ }
323
+
324
+ /** Built-in policy helpers. */
325
+ export const Policy = {
326
+ /** Creates an allow-all policy with a default 24-hour expiry when omitted. */
327
+ allow(): Policy {
328
+ return {
329
+ validate({ expiry, limits }) {
330
+ return {
331
+ expiry: expiry ?? Math.floor(Date.now() / 1000) + 60 * 60 * 24,
332
+ ...(limits ? { limits } : {}),
333
+ }
334
+ },
335
+ }
336
+ },
337
+ /** Returns the provided policy unchanged. */
338
+ from(policy: Policy): Policy {
339
+ return policy
340
+ },
341
+ }
342
+
343
+ /** Creates and stores a new device code. */
344
+ export async function createDeviceCode(
345
+ options: createDeviceCode.Options,
346
+ ): Promise<createDeviceCode.ReturnType> {
347
+ const {
348
+ chainId = BigInt(tempo.id),
349
+ now = Date.now,
350
+ policy = Policy.allow(),
351
+ random = randomBytes,
352
+ request,
353
+ store = Store.memory(),
354
+ ttlMs = 10 * 60 * 1_000,
355
+ } = options
356
+ const { account, codeChallenge, pubKey } = request
357
+ const keyType = request.keyType ?? 'secp256k1'
358
+ const approved = await policy.validate({
359
+ ...(account ? { account } : {}),
360
+ expiry: request.expiry,
361
+ keyType,
362
+ ...(request.limits ? { limits: request.limits } : {}),
363
+ pubKey,
364
+ })
365
+
366
+ let code: string | undefined
367
+ for (let i = 0; i < 10; i++) {
368
+ const candidate = createCode(random)
369
+ if (await store.get(candidate)) continue
370
+ code = candidate
371
+ break
372
+ }
373
+ if (!code) throw new Error('Unable to allocate device code.')
374
+
375
+ const createdAt = now()
376
+
377
+ await store.create({
378
+ ...(account ? { account } : {}),
379
+ chainId: typeof chainId === 'bigint' ? chainId : BigInt(chainId),
380
+ code,
381
+ codeChallenge,
382
+ createdAt,
383
+ expiresAt: createdAt + ttlMs,
384
+ expiry: approved.expiry,
385
+ keyType,
386
+ ...(approved.limits ? { limits: approved.limits } : {}),
387
+ pubKey,
388
+ status: 'pending',
389
+ })
390
+
391
+ return { code }
392
+ }
393
+
394
+ export declare namespace createDeviceCode {
395
+ export type Options = {
396
+ /** Chain ID embedded into the expected key authorization. @default tempo.id */
397
+ chainId?: bigint | number | undefined
398
+ /** Time source used for TTL evaluation. */
399
+ now?: (() => number) | undefined
400
+ /** Policy used to validate requested expiry and limits. */
401
+ policy?: Policy | undefined
402
+ /** Random byte generator used for verification code allocation. */
403
+ random?: ((size: number) => Uint8Array) | undefined
404
+ /** Incoming device-code creation request. */
405
+ request: z.output<typeof createRequest>
406
+ /** Device-code store. */
407
+ store?: Store | undefined
408
+ /** Pending entry TTL in milliseconds. @default 600000 */
409
+ ttlMs?: number | undefined
410
+ }
411
+
412
+ export type ReturnType = z.output<typeof createResponse>
413
+ }
414
+
415
+ /** Looks up a pending device code for browser approval UIs. */
416
+ export async function pending(options: pending.Options): Promise<pending.ReturnType> {
417
+ const { code, now = Date.now, store = Store.memory() } = options
418
+ const normalized = normalizeCode(code)
419
+ const current = await store.get(normalized)
420
+ if (!current) throw new PendingError('Unknown device code.', 404)
421
+ if (isExpired(current, now)) {
422
+ await store.delete(normalized)
423
+ throw new PendingError('Expired device code.', 404)
424
+ }
425
+ if (current.status !== 'pending') throw new PendingError('Device code already completed.', 400)
426
+
427
+ return {
428
+ accessKeyAddress: core_Address.fromPublicKey(PublicKey.from(current.pubKey)),
429
+ ...(current.account ? { account: current.account } : {}),
430
+ chainId: current.chainId,
431
+ code: current.code,
432
+ expiry: current.expiry,
433
+ keyType: current.keyType,
434
+ ...(current.limits ? { limits: current.limits } : {}),
435
+ pubKey: current.pubKey,
436
+ status: 'pending',
437
+ }
438
+ }
439
+
440
+ export declare namespace pending {
441
+ export type Options = {
442
+ /** Verification code from the route path. */
443
+ code: string
444
+ /** Time source used for TTL evaluation. */
445
+ now?: (() => number) | undefined
446
+ /** Device-code store. */
447
+ store?: Store | undefined
448
+ }
449
+
450
+ export type ReturnType = z.output<typeof pendingResponse>
451
+ }
452
+
453
+ /** Polls a device code with PKCE verification. */
454
+ export async function poll(options: poll.Options): Promise<poll.ReturnType> {
455
+ const { code, now = Date.now, request, store = Store.memory() } = options
456
+ const normalized = normalizeCode(code)
457
+ const current = await store.get(normalized)
458
+ if (!current) return { status: 'expired' }
459
+ if (isExpired(current, now)) {
460
+ await store.delete(normalized)
461
+ return { status: 'expired' }
462
+ }
463
+ if (!(await verifyCodeChallenge(request.codeVerifier, current.codeChallenge)))
464
+ throw new Error('Invalid code verifier.')
465
+ if (current.status === 'pending') return { status: 'pending' }
466
+ if (current.status === 'consumed') {
467
+ await store.delete(normalized)
468
+ return { status: 'expired' }
469
+ }
470
+ const authorized = await store.consume(normalized)
471
+ if (!authorized) return { status: 'expired' }
472
+ return {
473
+ accountAddress: authorized.accountAddress,
474
+ keyAuthorization: authorized.keyAuthorization,
475
+ status: 'authorized',
476
+ }
477
+ }
478
+
479
+ export declare namespace poll {
480
+ export type Options = {
481
+ /** Verification code from the route path. */
482
+ code: string
483
+ /** Time source used for TTL evaluation. */
484
+ now?: (() => number) | undefined
485
+ /** Poll request body. */
486
+ request: z.output<typeof pollRequest>
487
+ /** Device-code store. */
488
+ store?: Store | undefined
489
+ }
490
+
491
+ export type ReturnType = z.output<typeof pollResponse>
492
+ }
493
+
494
+ /** Authorizes a pending device code after validating the signed key authorization. */
495
+ export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
496
+ const {
497
+ chainId,
498
+ client = getClient({ chainId }),
499
+ now = Date.now,
500
+ request,
501
+ store = Store.memory(),
502
+ } = options
503
+ const code = normalizeCode(request.code)
504
+ const current = await store.get(code)
505
+ if (!current) throw new Error('Unknown device code.')
506
+ if (isExpired(current, now)) {
507
+ await store.delete(code)
508
+ throw new Error('Expired device code.')
509
+ }
510
+ if (current.status !== 'pending') throw new Error('Device code already completed.')
511
+ if (current.account && current.account.toLowerCase() !== request.accountAddress.toLowerCase())
512
+ throw new Error('Account does not match requested account.')
513
+
514
+ const expected = expectedKeyAuthorization(current)
515
+ const actual = normalizeKeyAuthorization(request.keyAuthorization)
516
+
517
+ if (actual.keyId.toLowerCase() !== expected.address.toLowerCase())
518
+ throw new Error('Key authorization key does not match the device-code request.')
519
+ if (actual.address.toLowerCase() !== expected.address.toLowerCase())
520
+ throw new Error('Key authorization address does not match the device-code request.')
521
+ if (actual.keyType !== expected.type)
522
+ throw new Error('Key authorization key type does not match the device-code request.')
523
+ if (actual.chainId !== expected.chainId)
524
+ throw new Error('Key authorization chain does not match the device-code request.')
525
+ if ((actual.expiry ?? undefined) !== (expected.expiry ?? undefined))
526
+ throw new Error('Key authorization expiry does not match the device-code request.')
527
+ if (!sameLimits(actual.limits, expected.limits))
528
+ throw new Error('Key authorization limits do not match the device-code request.')
529
+
530
+ const valid = await verifyHash(client as never, {
531
+ address: request.accountAddress,
532
+ hash: TempoKeyAuthorization.getSignPayload(expected),
533
+ signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
534
+ magic: actual.signature.type === 'webAuthn',
535
+ }),
536
+ })
537
+ if (!valid) throw new Error('Key authorization signature is invalid.')
538
+
539
+ const authorized = await store.authorize({
540
+ accountAddress: request.accountAddress,
541
+ code,
542
+ keyAuthorization: request.keyAuthorization,
543
+ })
544
+ if (!authorized) throw new Error('Unable to authorize device code.')
545
+
546
+ return { status: 'authorized' }
547
+ }
548
+
549
+ export declare namespace authorize {
550
+ export type Options = {
551
+ /** Chain ID embedded into the expected key authorization. Defaults to the client chain or tempo.id. */
552
+ chainId?: bigint | number | undefined
553
+ /** Client used to verify the signed key authorization. */
554
+ client?: Client<Transport, Chain | undefined> | undefined
555
+ /** Time source used for TTL evaluation. */
556
+ now?: (() => number) | undefined
557
+ /** Authorize request body. */
558
+ request: z.output<typeof authorizeRequest>
559
+ /** Device-code store. */
560
+ store?: Store | undefined
561
+ }
562
+
563
+ export type ReturnType = z.output<typeof authorizeResponse>
564
+ }
565
+
566
+ const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
567
+
568
+ function createCode(random: (size: number) => Uint8Array) {
569
+ const bytes = random(8)
570
+ let code = ''
571
+ for (const byte of bytes) code += alphabet[byte % alphabet.length]
572
+ return code
573
+ }
574
+
575
+ function normalizeCode(code: string) {
576
+ return code.replaceAll('-', '').toUpperCase()
577
+ }
578
+
579
+ function expectedKeyAuthorization(entry: Entry.Pending) {
580
+ return TempoKeyAuthorization.from({
581
+ address: core_Address.fromPublicKey(PublicKey.from(entry.pubKey)),
582
+ chainId: entry.chainId,
583
+ expiry: entry.expiry,
584
+ ...(entry.limits ? { limits: entry.limits } : {}),
585
+ type: entry.keyType,
586
+ })
587
+ }
588
+
589
+ function getClient(options: { chainId?: bigint | number | undefined } = {}) {
590
+ const chainId = options.chainId
591
+ return createClient({
592
+ chain: chainId
593
+ ? {
594
+ ...tempo,
595
+ id: typeof chainId === 'bigint' ? Number(chainId) : chainId,
596
+ }
597
+ : tempo,
598
+ transport: http(),
599
+ })
600
+ }
601
+
602
+ function isExpired(entry: Entry, now: () => number) {
603
+ return now() > entry.expiresAt
604
+ }
605
+
606
+ function normalizeKeyAuthorization(value: z.output<typeof keyAuthorization>) {
607
+ return {
608
+ ...value,
609
+ expiry: value.expiry ?? undefined,
610
+ limits: value.limits ?? undefined,
611
+ }
612
+ }
613
+
614
+ function randomBytes(size: number) {
615
+ return crypto.getRandomValues(new Uint8Array(size))
616
+ }
617
+
618
+ function sameLimits(
619
+ a: readonly { token: Address; limit: bigint }[] | undefined,
620
+ b: readonly { token: Address; limit: bigint }[] | undefined,
621
+ ) {
622
+ if (!a && !b) return true
623
+ if (!a || !b || a.length !== b.length) return false
624
+ return a.every((limit, i) => {
625
+ const other = b[i]
626
+ if (!other) return false
627
+ return limit.token.toLowerCase() === other.token.toLowerCase() && limit.limit === other.limit
628
+ })
629
+ }
630
+
631
+ async function verifyCodeChallenge(codeVerifier: string, codeChallenge: string) {
632
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
633
+ return Base64.fromBytes(new Uint8Array(hash), { pad: false, url: true }) === codeChallenge
634
+ }