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