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.
- package/CHANGELOG.md +16 -0
- package/dist/core/ExecutionError.d.ts +25 -0
- package/dist/core/ExecutionError.d.ts.map +1 -0
- package/dist/core/ExecutionError.js +170 -0
- package/dist/core/ExecutionError.js.map +1 -0
- package/dist/core/Schema.d.ts +33 -7
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/zod/rpc.d.ts +14 -1
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +14 -1
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/CliAuth.d.ts +110 -43
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +243 -155
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Handler.d.ts +0 -1
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +0 -1
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +29 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +180 -125
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/sponsorship.d.ts +77 -0
- package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -0
- package/dist/server/internal/handlers/sponsorship.js +96 -0
- package/dist/server/internal/handlers/sponsorship.js.map +1 -0
- package/dist/server/internal/handlers/utils.d.ts +3 -1
- package/dist/server/internal/handlers/utils.d.ts.map +1 -1
- package/dist/server/internal/handlers/utils.js +15 -12
- package/dist/server/internal/handlers/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ExecutionError.test.ts +205 -0
- package/src/core/ExecutionError.ts +189 -0
- package/src/core/Provider.test.ts +4 -2
- package/src/core/zod/rpc.ts +18 -1
- package/src/server/CliAuth.test-d.ts +6 -0
- package/src/server/CliAuth.test.ts +83 -0
- package/src/server/CliAuth.ts +331 -208
- package/src/server/Handler.ts +0 -1
- package/src/server/internal/handlers/relay.test.ts +318 -108
- package/src/server/internal/handlers/relay.ts +243 -138
- package/src/server/internal/handlers/sponsorship.ts +172 -0
- package/src/server/internal/handlers/utils.ts +15 -10
- package/dist/server/internal/handlers/feePayer.d.ts +0 -73
- package/dist/server/internal/handlers/feePayer.d.ts.map +0 -1
- package/dist/server/internal/handlers/feePayer.js +0 -184
- package/dist/server/internal/handlers/feePayer.js.map +0 -1
- package/src/server/internal/handlers/feePayer.test.ts +0 -336
- package/src/server/internal/handlers/feePayer.ts +0 -271
package/src/server/CliAuth.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Address
|
|
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(
|
|
27
|
+
limits: z.optional(limits),
|
|
24
28
|
signature: z.custom<SignatureEnvelope.SignatureEnvelopeRpc>(),
|
|
25
29
|
})
|
|
26
30
|
|
|
27
|
-
/**
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
/**
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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 =
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
527
|
+
export declare namespace from {
|
|
528
|
+
/** Shared CLI auth helper configuration. */
|
|
396
529
|
export type Options = {
|
|
397
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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,
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
499
|
-
client
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
return crypto.getRandomValues(new Uint8Array(size))
|
|
617
|
-
}
|
|
618
|
-
|
|
740
|
+
/** @internal */
|
|
619
741
|
function sameLimits(
|
|
620
|
-
a:
|
|
621
|
-
b:
|
|
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
|