accounts 0.8.2 → 0.8.4
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/Adapter.d.ts +7 -0
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Adapter.js.map +1 -1
- package/dist/core/Dialog.d.ts.map +1 -1
- package/dist/core/Dialog.js +1 -1
- package/dist/core/Dialog.js.map +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +8 -0
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +87 -0
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/Schema.js +1 -0
- package/dist/core/Schema.js.map +1 -1
- package/dist/core/TrustedHosts.d.ts +7 -2
- package/dist/core/TrustedHosts.d.ts.map +1 -1
- package/dist/core/TrustedHosts.js +18 -3
- package/dist/core/TrustedHosts.js.map +1 -1
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +3 -0
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +57 -0
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +24 -0
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/react/Remote.js +1 -1
- package/dist/react/Remote.js.map +1 -1
- package/dist/server/CliAuth.d.ts +5 -3
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +19 -19
- package/dist/server/CliAuth.js.map +1 -1
- package/package.json +2 -2
- package/src/core/Adapter.ts +13 -0
- package/src/core/Dialog.test-d.ts +1 -0
- package/src/core/Dialog.ts +3 -1
- package/src/core/Provider.test-d.ts +6 -0
- package/src/core/Provider.test.ts +16 -0
- package/src/core/Provider.ts +12 -0
- package/src/core/Schema.test-d.ts +20 -1
- package/src/core/Schema.ts +1 -0
- package/src/core/TrustedHosts.ts +19 -3
- package/src/core/adapters/dialog.ts +4 -0
- package/src/core/zod/request.test.ts +72 -0
- package/src/core/zod/rpc.ts +27 -0
- package/src/react/Remote.ts +1 -1
- package/src/server/CliAuth.test.ts +106 -13
- package/src/server/CliAuth.ts +26 -23
|
@@ -47,6 +47,12 @@ describe('request', () => {
|
|
|
47
47
|
expectTypeOf<Result<'wallet_disconnect'>>().toEqualTypeOf<undefined>()
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('wallet_swap', () => {
|
|
51
|
+
expectTypeOf<Result<'wallet_swap'>>().toMatchTypeOf<{
|
|
52
|
+
receipt: { transactionHash: `0x${string}` }
|
|
53
|
+
}>()
|
|
54
|
+
})
|
|
55
|
+
|
|
50
56
|
test('wallet_switchEthereumChain', () => {
|
|
51
57
|
expectTypeOf<Result<'wallet_switchEthereumChain'>>().toEqualTypeOf<undefined>()
|
|
52
58
|
})
|
|
@@ -664,6 +664,22 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
664
664
|
})
|
|
665
665
|
})
|
|
666
666
|
|
|
667
|
+
describe('wallet_swap', () => {
|
|
668
|
+
test('error: throws UnsupportedMethodError when adapter has no swap action', async () => {
|
|
669
|
+
const provider = Provider.create({ adapter: adapter(), chains: [chain] })
|
|
670
|
+
await connect(provider)
|
|
671
|
+
|
|
672
|
+
await expect(
|
|
673
|
+
provider.request({
|
|
674
|
+
method: 'wallet_swap',
|
|
675
|
+
params: [{ amount: '1', token: Addresses.pathUsd, type: 'sell' }],
|
|
676
|
+
}),
|
|
677
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
678
|
+
`[Provider.UnsupportedMethodError: \`swap\` not supported by adapter.]`,
|
|
679
|
+
)
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
|
|
667
683
|
describe('wallet_getCapabilities', () => {
|
|
668
684
|
test('default: returns atomic supported for all chains', async () => {
|
|
669
685
|
const provider = Provider.create({ adapter: adapter() })
|
package/src/core/Provider.ts
CHANGED
|
@@ -638,6 +638,18 @@ export function create(options: create.Options = {}): create.ReturnType {
|
|
|
638
638
|
)) satisfies Rpc.wallet_send.Encoded['returns']
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
case 'wallet_swap': {
|
|
642
|
+
assertConnected()
|
|
643
|
+
if (!actions.swap)
|
|
644
|
+
throw new ox_Provider.UnsupportedMethodError({
|
|
645
|
+
message: '`swap` not supported by adapter.',
|
|
646
|
+
})
|
|
647
|
+
return (await actions.swap(
|
|
648
|
+
(request._decoded.params?.[0] ?? {}) as Adapter.swap.Parameters,
|
|
649
|
+
request,
|
|
650
|
+
)) satisfies Rpc.wallet_swap.Encoded['returns']
|
|
651
|
+
}
|
|
652
|
+
|
|
641
653
|
case 'wallet_switchEthereumChain': {
|
|
642
654
|
const { chainId } = request._decoded.params[0]
|
|
643
655
|
if (!chains.some((c) => c.id === chainId))
|
|
@@ -137,6 +137,24 @@ describe('Encoded', () => {
|
|
|
137
137
|
}>()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
+
test('wallet_swap', () => {
|
|
141
|
+
expectTypeOf<Rpc.wallet_swap.Encoded>().toMatchTypeOf<{
|
|
142
|
+
method: 'wallet_swap'
|
|
143
|
+
params:
|
|
144
|
+
| readonly [
|
|
145
|
+
{
|
|
146
|
+
amount?: string | undefined
|
|
147
|
+
pairToken?: Hex | undefined
|
|
148
|
+
slippage?: number | undefined
|
|
149
|
+
token?: Hex | undefined
|
|
150
|
+
type?: 'buy' | 'sell' | undefined
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
| undefined
|
|
154
|
+
returns: { receipt: { transactionHash: Hex } }
|
|
155
|
+
}>()
|
|
156
|
+
})
|
|
157
|
+
|
|
140
158
|
test('wallet_switchEthereumChain', () => {
|
|
141
159
|
expectTypeOf<Rpc.wallet_switchEthereumChain.Encoded>().toEqualTypeOf<{
|
|
142
160
|
method: 'wallet_switchEthereumChain'
|
|
@@ -175,7 +193,7 @@ describe('Ox', () => {
|
|
|
175
193
|
describe('Viem', () => {
|
|
176
194
|
test('is a tuple of all provider methods', () => {
|
|
177
195
|
expectTypeOf<Schema.Viem[0]['Method']>().toEqualTypeOf<'eth_accounts'>()
|
|
178
|
-
expectTypeOf<Schema.Viem[
|
|
196
|
+
expectTypeOf<Schema.Viem[20]['Method']>().toEqualTypeOf<'wallet_switchEthereumChain'>()
|
|
179
197
|
})
|
|
180
198
|
})
|
|
181
199
|
|
|
@@ -203,6 +221,7 @@ describe('Request', () => {
|
|
|
203
221
|
| 'wallet_getBalances'
|
|
204
222
|
| 'wallet_revokeAccessKey'
|
|
205
223
|
| 'wallet_send'
|
|
224
|
+
| 'wallet_swap'
|
|
206
225
|
>()
|
|
207
226
|
})
|
|
208
227
|
|
package/src/core/Schema.ts
CHANGED
package/src/core/TrustedHosts.ts
CHANGED
|
@@ -10,15 +10,31 @@ import _hosts from '../trusted-hosts.json' with { type: 'json' }
|
|
|
10
10
|
export const hosts: Record<string, readonly string[]> = _hosts
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Returns `true` if `hostname` matches any pattern in `trustedHosts
|
|
13
|
+
* Returns `true` if `hostname` matches any pattern in `trustedHosts`,
|
|
14
|
+
* or (when `source` is provided) if `hostname` shares the same
|
|
15
|
+
* registrable domain ("eTLD+1") as `source`.
|
|
16
|
+
*
|
|
14
17
|
* Patterns starting with `*.` match any subdomain suffix
|
|
15
18
|
* (e.g. `*.workers.dev` matches `foo.workers.dev`).
|
|
16
19
|
*/
|
|
17
|
-
export function match(trustedHosts: readonly string[], hostname: string) {
|
|
18
|
-
if (hostname
|
|
20
|
+
export function match(trustedHosts: readonly string[], hostname: string, source?: string) {
|
|
21
|
+
if (source && sameRegistrableDomain(hostname, source)) return true
|
|
19
22
|
return trustedHosts.some((pattern) => {
|
|
20
23
|
if (pattern.startsWith('*.'))
|
|
21
24
|
return hostname.endsWith(pattern.slice(1)) && hostname.length > pattern.length - 1
|
|
22
25
|
return pattern === hostname
|
|
23
26
|
})
|
|
24
27
|
}
|
|
28
|
+
|
|
29
|
+
/** Returns `true` if `a` and `b` share the same registrable domain ("eTLD+1"). */
|
|
30
|
+
export function sameRegistrableDomain(a: string, b: string) {
|
|
31
|
+
return registrableDomain(a) === registrableDomain(b)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns the registrable domain ("eTLD+1") for a hostname. */
|
|
35
|
+
function registrableDomain(host: string) {
|
|
36
|
+
const hostname = host.split(':')[0]!.toLowerCase()
|
|
37
|
+
const labels = hostname.split('.')
|
|
38
|
+
if (labels.length <= 2) return hostname
|
|
39
|
+
return labels.slice(-2).join('.')
|
|
40
|
+
}
|
|
@@ -403,6 +403,10 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
403
403
|
})
|
|
404
404
|
},
|
|
405
405
|
|
|
406
|
+
async swap(_params, request) {
|
|
407
|
+
return await provider.request(request)
|
|
408
|
+
},
|
|
409
|
+
|
|
406
410
|
async disconnect() {
|
|
407
411
|
store.setState({ accessKeys: [], accounts: [], activeAccount: 0 })
|
|
408
412
|
},
|
|
@@ -84,6 +84,35 @@ describe('validate', () => {
|
|
|
84
84
|
`)
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
+
test('default: validates wallet_swap with sell amount', () => {
|
|
88
|
+
const result = RpcRequest.validate(Schema.Request, {
|
|
89
|
+
method: 'wallet_swap',
|
|
90
|
+
params: [
|
|
91
|
+
{
|
|
92
|
+
amount: '1.5',
|
|
93
|
+
pairToken: '0x0000000000000000000000000000000000000002',
|
|
94
|
+
slippage: 0.01,
|
|
95
|
+
token: '0x0000000000000000000000000000000000000001',
|
|
96
|
+
type: 'sell',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
})
|
|
100
|
+
expect(result._decoded).toMatchInlineSnapshot(`
|
|
101
|
+
{
|
|
102
|
+
"method": "wallet_swap",
|
|
103
|
+
"params": [
|
|
104
|
+
{
|
|
105
|
+
"amount": "1.5",
|
|
106
|
+
"pairToken": "0x0000000000000000000000000000000000000002",
|
|
107
|
+
"slippage": 0.01,
|
|
108
|
+
"token": "0x0000000000000000000000000000000000000001",
|
|
109
|
+
"type": "sell",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
`)
|
|
114
|
+
})
|
|
115
|
+
|
|
87
116
|
test('behavior: preserves original request properties', () => {
|
|
88
117
|
const result = RpcRequest.validate(Schema.Request, {
|
|
89
118
|
method: 'eth_accounts',
|
|
@@ -118,4 +147,47 @@ describe('validate', () => {
|
|
|
118
147
|
`[ProviderRpcError: Invalid params: params.0.chainId: Expected hex value, params.0.chainId: Invalid input]`,
|
|
119
148
|
)
|
|
120
149
|
})
|
|
150
|
+
|
|
151
|
+
test('error: rejects wallet_swap with invalid type', () => {
|
|
152
|
+
expect(() =>
|
|
153
|
+
RpcRequest.validate(Schema.Request, {
|
|
154
|
+
method: 'wallet_swap',
|
|
155
|
+
params: [
|
|
156
|
+
{
|
|
157
|
+
type: 'hold',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
162
|
+
`[ProviderRpcError: Invalid params: params.0.type: Invalid input, params.0.type: Invalid input]`,
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('error: rejects wallet_swap with out-of-range slippage', () => {
|
|
167
|
+
expect(() =>
|
|
168
|
+
RpcRequest.validate(Schema.Request, {
|
|
169
|
+
method: 'wallet_swap',
|
|
170
|
+
params: [
|
|
171
|
+
{
|
|
172
|
+
slippage: 1.1,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}),
|
|
176
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
177
|
+
`[ProviderRpcError: Invalid params: params.0.slippage: Invalid input]`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(() =>
|
|
181
|
+
RpcRequest.validate(Schema.Request, {
|
|
182
|
+
method: 'wallet_swap',
|
|
183
|
+
params: [
|
|
184
|
+
{
|
|
185
|
+
slippage: -0.1,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
190
|
+
`[ProviderRpcError: Invalid params: params.0.slippage: Invalid input]`,
|
|
191
|
+
)
|
|
192
|
+
})
|
|
121
193
|
})
|
package/src/core/zod/rpc.ts
CHANGED
|
@@ -559,6 +559,33 @@ export namespace wallet_send {
|
|
|
559
559
|
export type Decoded = Schema.Decoded<typeof schema>
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
const swapParameters = z.object({
|
|
563
|
+
/** Human-readable amount to pre-fill (e.g. "1.5"). */
|
|
564
|
+
amount: z.optional(z.string()),
|
|
565
|
+
/** Other side of the swap pair. For buys, this is the token to sell. For sells, this is the token to buy. */
|
|
566
|
+
pairToken: z.optional(u.address()),
|
|
567
|
+
/** Maximum allowed slippage as a decimal fraction (for example `0.05` for 5%). */
|
|
568
|
+
slippage: z.optional(z.number().check(z.minimum(0), z.maximum(1))),
|
|
569
|
+
/** Token to buy or sell. Omit to let the user choose. */
|
|
570
|
+
token: z.optional(u.address()),
|
|
571
|
+
/** Whether the amount is an exact buy amount (`swapExactAmountOut`) or sell amount (`swapExactAmountIn`). */
|
|
572
|
+
type: z.optional(z.union([z.literal('buy'), z.literal('sell')])),
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
/** Opens the wallet swap flow with optional pre-filled swap intent fields. */
|
|
576
|
+
export namespace wallet_swap {
|
|
577
|
+
export const schema = Schema.defineItem({
|
|
578
|
+
method: z.literal('wallet_swap'),
|
|
579
|
+
params: z.optional(z.readonly(z.tuple([swapParameters]))),
|
|
580
|
+
returns: z.object({
|
|
581
|
+
/** Receipt of the submitted swap. */
|
|
582
|
+
receipt,
|
|
583
|
+
}),
|
|
584
|
+
})
|
|
585
|
+
export type Encoded = Schema.Encoded<typeof schema>
|
|
586
|
+
export type Decoded = Schema.Decoded<typeof schema>
|
|
587
|
+
}
|
|
588
|
+
|
|
562
589
|
export namespace wallet_switchEthereumChain {
|
|
563
590
|
export const schema = Schema.defineItem({
|
|
564
591
|
method: z.literal('wallet_switchEthereumChain'),
|
package/src/react/Remote.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function useEnsureVisibility(
|
|
|
18
18
|
if (!origin) return false
|
|
19
19
|
try {
|
|
20
20
|
const hostname = new URL(origin).hostname.replace(/^www\./, '')
|
|
21
|
-
return TrustedHosts.match(remote.trustedHosts, hostname)
|
|
21
|
+
return TrustedHosts.match(remote.trustedHosts, hostname, window.location.hostname)
|
|
22
22
|
} catch {
|
|
23
23
|
return false
|
|
24
24
|
}
|
|
@@ -343,6 +343,7 @@ describe('createDeviceCode', () => {
|
|
|
343
343
|
const store = CliAuth.Store.memory()
|
|
344
344
|
const now = () => 1_000
|
|
345
345
|
const defaultExpiry = 4_600
|
|
346
|
+
let validatedChainId: bigint | undefined
|
|
346
347
|
const { request } = await createRequest('device-code-verifier', {
|
|
347
348
|
accessKey: secpAccessKey,
|
|
348
349
|
expiry: undefined,
|
|
@@ -354,7 +355,8 @@ describe('createDeviceCode', () => {
|
|
|
354
355
|
chainId: chain.id,
|
|
355
356
|
now,
|
|
356
357
|
policy: {
|
|
357
|
-
validate({ expiry, limits }) {
|
|
358
|
+
validate({ chainId, expiry, limits }) {
|
|
359
|
+
validatedChainId = chainId
|
|
358
360
|
return {
|
|
359
361
|
expiry: expiry ?? defaultExpiry,
|
|
360
362
|
...(limits ? { limits } : {}),
|
|
@@ -368,17 +370,20 @@ describe('createDeviceCode', () => {
|
|
|
368
370
|
})
|
|
369
371
|
const entry = await store.get(result.code)
|
|
370
372
|
|
|
371
|
-
expect(entry).toMatchInlineSnapshot(`
|
|
373
|
+
expect({ entry, validatedChainId }).toMatchInlineSnapshot(`
|
|
372
374
|
{
|
|
373
|
-
"
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
375
|
+
"entry": {
|
|
376
|
+
"chainId": 1337n,
|
|
377
|
+
"code": "ABCDEFGH",
|
|
378
|
+
"codeChallenge": "NUwjc1h8PuXcsvSOG44Rp4bMayBXnOkriHEJ19CaSQM",
|
|
379
|
+
"createdAt": 1000,
|
|
380
|
+
"expiresAt": 31000,
|
|
381
|
+
"expiry": 4600,
|
|
382
|
+
"keyType": "secp256k1",
|
|
383
|
+
"pubKey": "${secpAccessKey.publicKey}",
|
|
384
|
+
"status": "pending",
|
|
385
|
+
},
|
|
386
|
+
"validatedChainId": 1337n,
|
|
382
387
|
}
|
|
383
388
|
`)
|
|
384
389
|
})
|
|
@@ -734,6 +739,94 @@ describe('authorize', () => {
|
|
|
734
739
|
expect(polled.status).toMatchInlineSnapshot(`"authorized"`)
|
|
735
740
|
})
|
|
736
741
|
|
|
742
|
+
test('behavior: accepts user-approved expiry and limit changes', async () => {
|
|
743
|
+
const store = CliAuth.Store.memory()
|
|
744
|
+
const { codeVerifier, request } = await createRequest()
|
|
745
|
+
const { code } = await CliAuth.createDeviceCode({
|
|
746
|
+
chainId: chain.id,
|
|
747
|
+
request,
|
|
748
|
+
store,
|
|
749
|
+
})
|
|
750
|
+
const approvedLimits = [
|
|
751
|
+
{
|
|
752
|
+
limit: 2_000n,
|
|
753
|
+
token: limits[0]!.token,
|
|
754
|
+
},
|
|
755
|
+
] as const
|
|
756
|
+
|
|
757
|
+
const authorized = await CliAuth.authorize({
|
|
758
|
+
chainId: chain.id,
|
|
759
|
+
request: await authorize(code, { expiry: expiry + 60 * 60 * 24 * 6, limits: approvedLimits }),
|
|
760
|
+
store,
|
|
761
|
+
})
|
|
762
|
+
const polled = await CliAuth.poll({
|
|
763
|
+
code,
|
|
764
|
+
request: {
|
|
765
|
+
codeVerifier: codeVerifier,
|
|
766
|
+
},
|
|
767
|
+
store,
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
if (polled.status !== 'authorized') throw new Error('Expected device code to be authorized.')
|
|
771
|
+
|
|
772
|
+
expect(authorized).toMatchInlineSnapshot(`
|
|
773
|
+
{
|
|
774
|
+
"status": "authorized",
|
|
775
|
+
}
|
|
776
|
+
`)
|
|
777
|
+
expect({ expiry: polled.keyAuthorization.expiry, limits: polled.keyAuthorization.limits })
|
|
778
|
+
.toMatchInlineSnapshot(`
|
|
779
|
+
{
|
|
780
|
+
"expiry": ${expiry + 60 * 60 * 24 * 6},
|
|
781
|
+
"limits": [
|
|
782
|
+
{
|
|
783
|
+
"limit": 2000n,
|
|
784
|
+
"token": "0x20c0000000000000000000000000000000000001",
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
}
|
|
788
|
+
`)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
test('behavior: rejects unsigned expiry and limit changes', async () => {
|
|
792
|
+
const store = CliAuth.Store.memory()
|
|
793
|
+
const { request } = await createRequest()
|
|
794
|
+
const { code } = await CliAuth.createDeviceCode({
|
|
795
|
+
chainId: chain.id,
|
|
796
|
+
request,
|
|
797
|
+
store,
|
|
798
|
+
})
|
|
799
|
+
const authorized = await authorize(code)
|
|
800
|
+
|
|
801
|
+
await expect(
|
|
802
|
+
CliAuth.authorize({
|
|
803
|
+
chainId: chain.id,
|
|
804
|
+
request: {
|
|
805
|
+
...authorized,
|
|
806
|
+
keyAuthorization: {
|
|
807
|
+
...authorized.keyAuthorization,
|
|
808
|
+
expiry: expiry + 1,
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
store,
|
|
812
|
+
}),
|
|
813
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Key authorization signature is invalid.]`)
|
|
814
|
+
|
|
815
|
+
await expect(
|
|
816
|
+
CliAuth.authorize({
|
|
817
|
+
chainId: chain.id,
|
|
818
|
+
request: {
|
|
819
|
+
...authorized,
|
|
820
|
+
keyAuthorization: {
|
|
821
|
+
...authorized.keyAuthorization,
|
|
822
|
+
limits: [{ limit: limits[0]!.limit + 1n, token: limits[0]!.token }],
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
store,
|
|
826
|
+
}),
|
|
827
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Key authorization signature is invalid.]`)
|
|
828
|
+
})
|
|
829
|
+
|
|
737
830
|
test('behavior: rejects a mismatched key authorization', async () => {
|
|
738
831
|
const store = CliAuth.Store.memory()
|
|
739
832
|
const { request } = await createRequest()
|
|
@@ -746,11 +839,11 @@ describe('authorize', () => {
|
|
|
746
839
|
await expect(
|
|
747
840
|
CliAuth.authorize({
|
|
748
841
|
chainId: chain.id,
|
|
749
|
-
request: await authorize(code, {
|
|
842
|
+
request: await authorize(code, { accessKeyAddress: secpAccessKey.address }),
|
|
750
843
|
store,
|
|
751
844
|
}),
|
|
752
845
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
753
|
-
`[Error: Key authorization
|
|
846
|
+
`[Error: Key authorization key does not match the device-code request.]`,
|
|
754
847
|
)
|
|
755
848
|
})
|
|
756
849
|
|
package/src/server/CliAuth.ts
CHANGED
|
@@ -170,7 +170,7 @@ export type Store = {
|
|
|
170
170
|
|
|
171
171
|
/** Host validation and sanitization for requested CLI auth defaults. */
|
|
172
172
|
export type Policy = {
|
|
173
|
-
/** Validates and optionally rewrites requested
|
|
173
|
+
/** Validates and optionally rewrites requested defaults before the entry is stored. */
|
|
174
174
|
validate: (options: Policy.validate.Options) => MaybePromise<Policy.validate.ReturnType>
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -208,6 +208,8 @@ export declare namespace Policy {
|
|
|
208
208
|
export type Options = {
|
|
209
209
|
/** Requested root account restriction. */
|
|
210
210
|
account?: Address.Address | undefined
|
|
211
|
+
/** Requested chain ID. */
|
|
212
|
+
chainId: bigint
|
|
211
213
|
/** Requested access-key expiry timestamp. Omit to let the server choose one. */
|
|
212
214
|
expiry?: number | undefined
|
|
213
215
|
/** Requested key type. */
|
|
@@ -219,9 +221,9 @@ export declare namespace Policy {
|
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
export type ReturnType = {
|
|
222
|
-
/**
|
|
224
|
+
/** Suggested access-key expiry timestamp. */
|
|
223
225
|
expiry: number
|
|
224
|
-
/**
|
|
226
|
+
/** Suggested spending limits. */
|
|
225
227
|
limits?: readonly { token: Address.Address; limit: bigint }[] | undefined
|
|
226
228
|
}
|
|
227
229
|
}
|
|
@@ -417,24 +419,38 @@ export function from(options: from.Options = {}): CliAuth {
|
|
|
417
419
|
throw new Error('Key authorization key type does not match the device-code request.')
|
|
418
420
|
if (actual.chainId !== expected.chainId)
|
|
419
421
|
throw new Error('Key authorization chain does not match the device-code request.')
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
|
|
423
|
+
const signed = TempoKeyAuthorization.from({
|
|
424
|
+
address: actual.address,
|
|
425
|
+
chainId: actual.chainId,
|
|
426
|
+
expiry: actual.expiry,
|
|
427
|
+
...(actual.limits ? { limits: actual.limits } : {}),
|
|
428
|
+
type: actual.keyType,
|
|
429
|
+
})
|
|
424
430
|
|
|
425
431
|
const valid = await verifyHash((options.client ?? cache.get(current.chainId)) as never, {
|
|
426
432
|
address: options.request.accountAddress,
|
|
427
|
-
hash: TempoKeyAuthorization.getSignPayload(
|
|
433
|
+
hash: TempoKeyAuthorization.getSignPayload(signed),
|
|
428
434
|
signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
|
|
429
435
|
magic: actual.signature.type === 'webAuthn',
|
|
430
436
|
}),
|
|
431
437
|
})
|
|
432
438
|
if (!valid) throw new Error('Key authorization signature is invalid.')
|
|
433
439
|
|
|
440
|
+
const signedKeyAuthorization = {
|
|
441
|
+
address: options.request.keyAuthorization.address,
|
|
442
|
+
chainId: options.request.keyAuthorization.chainId,
|
|
443
|
+
expiry: actual.expiry,
|
|
444
|
+
keyId: options.request.keyAuthorization.keyId,
|
|
445
|
+
keyType: options.request.keyAuthorization.keyType,
|
|
446
|
+
...(actual.limits ? { limits: actual.limits } : {}),
|
|
447
|
+
signature: options.request.keyAuthorization.signature,
|
|
448
|
+
} satisfies z.output<typeof keyAuthorization>
|
|
449
|
+
|
|
434
450
|
const authorized = await store.authorize({
|
|
435
451
|
accountAddress: options.request.accountAddress,
|
|
436
452
|
code,
|
|
437
|
-
keyAuthorization:
|
|
453
|
+
keyAuthorization: signedKeyAuthorization,
|
|
438
454
|
})
|
|
439
455
|
if (!authorized) throw new Error('Unable to authorize device code.')
|
|
440
456
|
|
|
@@ -446,6 +462,7 @@ export function from(options: from.Options = {}): CliAuth {
|
|
|
446
462
|
const keyType = options.request.keyType ?? 'secp256k1'
|
|
447
463
|
const approved = await policy.validate({
|
|
448
464
|
...(account ? { account } : {}),
|
|
465
|
+
chainId: typeof nextChainId === 'bigint' ? nextChainId : BigInt(nextChainId),
|
|
449
466
|
expiry: options.request.expiry,
|
|
450
467
|
keyType,
|
|
451
468
|
...(options.request.limits ? { limits: options.request.limits } : {}),
|
|
@@ -808,20 +825,6 @@ function normalizeKeyAuthorization(value: z.output<typeof keyAuthorization>) {
|
|
|
808
825
|
}
|
|
809
826
|
}
|
|
810
827
|
|
|
811
|
-
/** @internal */
|
|
812
|
-
function sameLimits(
|
|
813
|
-
a: Policy.validate.ReturnType['limits'],
|
|
814
|
-
b: Policy.validate.ReturnType['limits'],
|
|
815
|
-
) {
|
|
816
|
-
if (!a && !b) return true
|
|
817
|
-
if (!a || !b || a.length !== b.length) return false
|
|
818
|
-
return a.every((limit, i) => {
|
|
819
|
-
const other = b[i]
|
|
820
|
-
if (!other) return false
|
|
821
|
-
return limit.token.toLowerCase() === other.token.toLowerCase() && limit.limit === other.limit
|
|
822
|
-
})
|
|
823
|
-
}
|
|
824
|
-
|
|
825
828
|
/** @internal */
|
|
826
829
|
async function verifyCodeChallenge(codeVerifier: string, codeChallenge: string) {
|
|
827
830
|
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
|