accounts 0.12.2 → 0.14.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 +35 -0
- package/README.md +6 -5
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +76 -69
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +89 -106
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +230 -210
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +0 -9
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +1 -13
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +24 -4
- package/dist/core/Adapter.d.ts.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.map +1 -1
- package/dist/core/Provider.d.ts +6 -7
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +73 -77
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Remote.d.ts +2 -0
- package/dist/core/Remote.d.ts.map +1 -1
- package/dist/core/Remote.js +3 -1
- package/dist/core/Remote.js.map +1 -1
- package/dist/core/Schema.d.ts +17 -7
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +126 -131
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +92 -105
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/privy.d.ts +164 -0
- package/dist/core/adapters/privy.d.ts.map +1 -0
- package/dist/core/adapters/privy.js +578 -0
- package/dist/core/adapters/privy.js.map +1 -0
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +136 -133
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
- package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
- package/dist/core/internal/AccessKeyTransaction.js +155 -0
- package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +71 -4
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +33 -4
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +86 -75
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/CliAuth.d.ts +55 -0
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +18 -1
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Kv.d.ts +19 -9
- package/dist/server/Kv.d.ts.map +1 -1
- package/dist/server/Kv.js +32 -9
- package/dist/server/Kv.js.map +1 -1
- package/dist/server/internal/handlers/auth.d.ts.map +1 -1
- package/dist/server/internal/handlers/auth.js +3 -1
- package/dist/server/internal/handlers/auth.js.map +1 -1
- package/dist/server/internal/handlers/codeAuth.js +1 -1
- package/dist/server/internal/handlers/codeAuth.js.map +1 -1
- package/dist/server/internal/handlers/exchange.js +1 -1
- package/dist/server/internal/handlers/exchange.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +18 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +116 -3
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +16 -9
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +49 -1
- package/src/cli/adapter.ts +95 -76
- package/src/core/AccessKey.test.ts +563 -560
- package/src/core/AccessKey.ts +327 -327
- package/src/core/Account.test.ts +6 -51
- package/src/core/Account.ts +1 -19
- package/src/core/Adapter.ts +22 -4
- package/src/core/Client.test.ts +1 -1
- package/src/core/Client.ts +1 -1
- package/src/core/Provider.connect.browser.test.ts +1 -1
- package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +98 -32
- package/src/core/Provider.ts +118 -112
- package/src/core/Remote.test.ts +28 -0
- package/src/core/Remote.ts +4 -1
- package/src/core/Schema.test-d.ts +41 -0
- package/src/core/adapters/dialog.test.ts +335 -15
- package/src/core/adapters/dialog.ts +120 -127
- package/src/core/adapters/local.test.ts +1 -1
- package/src/core/adapters/local.ts +95 -119
- package/src/core/adapters/privy.test-d.ts +128 -0
- package/src/core/adapters/privy.test.ts +648 -0
- package/src/core/adapters/privy.ts +811 -0
- package/src/core/adapters/turnkey.test.ts +117 -13
- package/src/core/adapters/turnkey.ts +132 -149
- package/src/core/internal/AccessKeyTransaction.ts +243 -0
- package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
- package/src/core/zod/request.test.ts +36 -0
- package/src/core/zod/rpc.test.ts +139 -0
- package/src/core/zod/rpc.ts +44 -10
- package/src/index.ts +1 -1
- package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +65 -0
- package/src/react-native/adapter.ts +107 -87
- package/src/server/CliAuth.test-d.ts +23 -0
- package/src/server/CliAuth.test.ts +47 -0
- package/src/server/CliAuth.ts +20 -1
- package/src/server/Handler.test-d.ts +1 -1
- package/src/server/Kv.test.ts +51 -0
- package/src/server/Kv.ts +43 -9
- package/src/server/internal/handlers/auth.test.ts +10 -5
- package/src/server/internal/handlers/auth.ts +4 -1
- package/src/server/internal/handlers/codeAuth.ts +1 -1
- package/src/server/internal/handlers/exchange.ts +1 -1
- package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +4 -1
- package/src/server/internal/handlers/relay.ts +139 -14
- package/src/server/internal/handlers/session.test.ts +1 -1
- package/src/server/internal/handlers/webAuthn.test.ts +20 -0
- package/src/server/internal/handlers/webAuthn.ts +24 -12
- /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
RpcResponse,
|
|
9
9
|
} from 'ox'
|
|
10
10
|
import { KeyAuthorization } from 'ox/tempo'
|
|
11
|
-
import { prepareTransactionRequest } from 'viem/actions'
|
|
12
11
|
import { Actions, Account as TempoAccount, Secp256k1 } from 'viem/tempo'
|
|
13
12
|
|
|
14
13
|
import * as AccessKey from '../core/AccessKey.js'
|
|
15
14
|
import * as Adapter from '../core/Adapter.js'
|
|
15
|
+
import * as AccessKeyTransaction from '../core/internal/AccessKeyTransaction.js'
|
|
16
16
|
import type * as Storage from '../core/Storage.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -58,19 +58,19 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
58
58
|
const keyAuthorization = deserialized as KeyAuthorization.Signed
|
|
59
59
|
|
|
60
60
|
if (keyAuthorization.address.toLowerCase() === keyAddress.toLowerCase())
|
|
61
|
-
AccessKey.
|
|
62
|
-
address,
|
|
63
|
-
keyAuthorization,
|
|
61
|
+
AccessKey.add({
|
|
62
|
+
account: address,
|
|
63
|
+
authorization: keyAuthorization,
|
|
64
64
|
privateKey: entry.key,
|
|
65
65
|
store,
|
|
66
66
|
})
|
|
67
67
|
else
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
})
|
|
68
|
+
AccessKey.remove({
|
|
69
|
+
account: address,
|
|
70
|
+
accessKey: keyAuthorization.address,
|
|
71
|
+
chainId: Number(keyAuthorization.chainId),
|
|
72
|
+
store,
|
|
73
|
+
})
|
|
74
74
|
|
|
75
75
|
return {
|
|
76
76
|
account,
|
|
@@ -127,9 +127,9 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
127
127
|
) {
|
|
128
128
|
if (!managedKey) return
|
|
129
129
|
|
|
130
|
-
AccessKey.
|
|
131
|
-
address,
|
|
132
|
-
keyAuthorization,
|
|
130
|
+
AccessKey.add({
|
|
131
|
+
account: address,
|
|
132
|
+
authorization: keyAuthorization,
|
|
133
133
|
privateKey: managedKey.key,
|
|
134
134
|
store,
|
|
135
135
|
})
|
|
@@ -189,36 +189,47 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
189
189
|
return result.keyAuthorization
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
async function prepareManagedTransaction(
|
|
193
|
+
client: ReturnType<typeof getClient>,
|
|
194
|
+
parameters: AccessKeyTransaction.create.PrepareParameters,
|
|
195
|
+
options: {
|
|
196
|
+
calls?: AccessKeyTransaction.create.Options['calls'] | undefined
|
|
197
|
+
chainId?: number | undefined
|
|
198
|
+
} = {},
|
|
197
199
|
) {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
const state = store.getState()
|
|
201
|
+
const address = parameters.from ?? state.accounts[state.activeAccount]?.address
|
|
202
|
+
if (!address) throw new core_Provider.DisconnectedError({ message: 'No active account.' })
|
|
203
|
+
const managedKey = await loadManagedKey(address)
|
|
204
|
+
if (managedKey && !(await isManagedKeyAuthorized(address, managedKey)))
|
|
205
|
+
await reauthorizeManagedKey(address, managedKey)
|
|
206
|
+
const transaction = await AccessKeyTransaction.create({
|
|
207
|
+
address,
|
|
208
|
+
calls: options.calls,
|
|
209
|
+
chainId: options.chainId ?? state.chainId,
|
|
210
|
+
client,
|
|
211
|
+
store,
|
|
212
|
+
})
|
|
213
|
+
if (!transaction)
|
|
214
|
+
throw new core_Provider.UnauthorizedError({
|
|
215
|
+
message: `Account "${address}" cannot sign with an access key.`,
|
|
216
|
+
})
|
|
217
|
+
return await transaction.prepare(parameters)
|
|
218
|
+
}
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
AccessKey.remove(account, { store })
|
|
211
|
-
throw error
|
|
212
|
-
}
|
|
220
|
+
async function loadManagedAccount(address: Adapter.signPersonalMessage.Parameters['address']) {
|
|
221
|
+
await loadManagedKey(address)
|
|
222
|
+
return getAccount({ address, signable: true })
|
|
213
223
|
}
|
|
214
224
|
|
|
215
225
|
async function authorize(request: {
|
|
216
226
|
account?: Adapter.authorizeAccessKey.ReturnType['rootAddress'] | undefined
|
|
217
227
|
authorizeAccessKey: Adapter.authorizeAccessKey.Parameters | undefined
|
|
218
228
|
method: 'wallet_authorizeAccessKey' | 'wallet_connect'
|
|
229
|
+
showDeposit?: Adapter.createAccount.Parameters['showDeposit'] | undefined
|
|
219
230
|
}) {
|
|
220
231
|
const { host, redirectUri, open = defaultOpen } = options
|
|
221
|
-
const { account, authorizeAccessKey, method } = request
|
|
232
|
+
const { account, authorizeAccessKey, method, showDeposit } = request
|
|
222
233
|
|
|
223
234
|
const managedKey =
|
|
224
235
|
authorizeAccessKey && !authorizeAccessKey.publicKey && !authorizeAccessKey.address
|
|
@@ -251,6 +262,7 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
251
262
|
? { limits: authorizeAccessKey.limits.map((l) => ({ ...l, limit: String(l.limit) })) }
|
|
252
263
|
: {}),
|
|
253
264
|
pubKey: publicKey,
|
|
265
|
+
...(showDeposit !== undefined ? { showDeposit } : {}),
|
|
254
266
|
state,
|
|
255
267
|
})
|
|
256
268
|
|
|
@@ -303,8 +315,29 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
303
315
|
rootAddress: result.accountAddress,
|
|
304
316
|
}
|
|
305
317
|
},
|
|
306
|
-
async createAccount(
|
|
307
|
-
|
|
318
|
+
async createAccount(parameters) {
|
|
319
|
+
if (parameters?.digest)
|
|
320
|
+
throw unsupported(
|
|
321
|
+
'`wallet_connect` digest signing not supported by React Native adapter.',
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const result = await authorize({
|
|
325
|
+
authorizeAccessKey: parameters?.authorizeAccessKey,
|
|
326
|
+
method: 'wallet_connect',
|
|
327
|
+
...(parameters?.showDeposit !== undefined
|
|
328
|
+
? { showDeposit: parameters.showDeposit }
|
|
329
|
+
: {}),
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
accounts: [
|
|
334
|
+
{
|
|
335
|
+
address: result.accountAddress,
|
|
336
|
+
capabilities: {},
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
|
|
340
|
+
}
|
|
308
341
|
},
|
|
309
342
|
async loadAccounts(parameters) {
|
|
310
343
|
if (parameters?.digest)
|
|
@@ -333,74 +366,57 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
333
366
|
async sendTransaction(parameters) {
|
|
334
367
|
const { feePayer, ...rest } = parameters
|
|
335
368
|
const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}),
|
|
369
|
+
const prepared = await prepareManagedTransaction(
|
|
370
|
+
client,
|
|
371
|
+
{
|
|
372
|
+
...rest,
|
|
373
|
+
...(feePayer ? { feePayer: true } : {}),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
calls: parameters.calls as AccessKeyTransaction.create.Options['calls'],
|
|
377
|
+
chainId: parameters.chainId,
|
|
378
|
+
},
|
|
347
379
|
)
|
|
348
|
-
|
|
349
|
-
const result = await client.request({
|
|
350
|
-
method: 'eth_sendRawTransaction' as never,
|
|
351
|
-
params: [signed],
|
|
352
|
-
})
|
|
353
|
-
AccessKey.removePending(account, { store })
|
|
354
|
-
return result
|
|
380
|
+
return await prepared.send()
|
|
355
381
|
},
|
|
356
382
|
async sendTransactionSync(parameters) {
|
|
357
383
|
const { feePayer, ...rest } = parameters
|
|
358
384
|
const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}),
|
|
385
|
+
const prepared = await prepareManagedTransaction(
|
|
386
|
+
client,
|
|
387
|
+
{
|
|
388
|
+
...rest,
|
|
389
|
+
...(feePayer ? { feePayer: true } : {}),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
calls: parameters.calls as AccessKeyTransaction.create.Options['calls'],
|
|
393
|
+
chainId: parameters.chainId,
|
|
394
|
+
},
|
|
370
395
|
)
|
|
371
|
-
|
|
372
|
-
const result = await client.request({
|
|
373
|
-
method: 'eth_sendRawTransactionSync' as never,
|
|
374
|
-
params: [signed],
|
|
375
|
-
})
|
|
376
|
-
AccessKey.removePending(account, { store })
|
|
377
|
-
return result
|
|
396
|
+
return await prepared.sendSync()
|
|
378
397
|
},
|
|
379
398
|
async signPersonalMessage({ address, data }) {
|
|
380
|
-
await
|
|
381
|
-
const account = getAccount({ address, signable: true })
|
|
399
|
+
const account = await loadManagedAccount(address)
|
|
382
400
|
return await account.signMessage({ message: { raw: data } })
|
|
383
401
|
},
|
|
384
402
|
async signTransaction(parameters) {
|
|
385
403
|
const { feePayer, ...rest } = parameters
|
|
386
404
|
const client = getClient(typeof feePayer === 'string' ? { feePayer } : {})
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}),
|
|
405
|
+
const prepared = await prepareManagedTransaction(
|
|
406
|
+
client,
|
|
407
|
+
{
|
|
408
|
+
...rest,
|
|
409
|
+
...(feePayer ? { feePayer: true } : {}),
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
calls: parameters.calls as AccessKeyTransaction.create.Options['calls'],
|
|
413
|
+
chainId: parameters.chainId,
|
|
414
|
+
},
|
|
398
415
|
)
|
|
399
|
-
return await
|
|
416
|
+
return await prepared.sign()
|
|
400
417
|
},
|
|
401
418
|
async signTypedData({ address, data }) {
|
|
402
|
-
await
|
|
403
|
-
const account = getAccount({ address, signable: true })
|
|
419
|
+
const account = await loadManagedAccount(address)
|
|
404
420
|
return await account.signTypedData(JSON.parse(data) as never)
|
|
405
421
|
},
|
|
406
422
|
},
|
|
@@ -490,6 +506,7 @@ function buildAuthUrl(
|
|
|
490
506
|
keyType?: string | undefined
|
|
491
507
|
limits?: readonly { token: string; limit: string }[] | undefined
|
|
492
508
|
pubKey: Hex.Hex
|
|
509
|
+
showDeposit?: Adapter.createAccount.Parameters['showDeposit'] | undefined
|
|
493
510
|
state: string
|
|
494
511
|
},
|
|
495
512
|
): string {
|
|
@@ -501,6 +518,9 @@ function buildAuthUrl(
|
|
|
501
518
|
url.searchParams.set('chainId', String(params.chainId))
|
|
502
519
|
if (typeof params.expiry !== 'undefined') url.searchParams.set('expiry', String(params.expiry))
|
|
503
520
|
if (params.limits) url.searchParams.set('limits', JSON.stringify(params.limits))
|
|
521
|
+
if (params.showDeposit === true) url.searchParams.set('showDeposit', 'true')
|
|
522
|
+
else if (params.showDeposit)
|
|
523
|
+
url.searchParams.set('showDeposit', JSON.stringify(params.showDeposit))
|
|
504
524
|
url.searchParams.set('callback', params.callback)
|
|
505
525
|
url.searchParams.set('state', params.state)
|
|
506
526
|
return url.toString()
|
|
@@ -13,6 +13,14 @@ describe('createRequest', () => {
|
|
|
13
13
|
keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
|
|
14
14
|
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
15
15
|
pubKey: Hex
|
|
16
|
+
showDeposit?:
|
|
17
|
+
| boolean
|
|
18
|
+
| {
|
|
19
|
+
amount?: string | undefined
|
|
20
|
+
displayName?: string | undefined
|
|
21
|
+
token?: string | undefined
|
|
22
|
+
}
|
|
23
|
+
| undefined
|
|
16
24
|
}>()
|
|
17
25
|
})
|
|
18
26
|
|
|
@@ -20,6 +28,13 @@ describe('createRequest', () => {
|
|
|
20
28
|
type Request = z.output<typeof CliAuth.createRequest>
|
|
21
29
|
expectTypeOf<Request>().not.toHaveProperty('scopes')
|
|
22
30
|
})
|
|
31
|
+
|
|
32
|
+
test('showDeposit does not include address or chainId', () => {
|
|
33
|
+
type Request = z.output<typeof CliAuth.createRequest>
|
|
34
|
+
type ShowDeposit = Exclude<Exclude<Request['showDeposit'], boolean | undefined>, undefined>
|
|
35
|
+
expectTypeOf<ShowDeposit>().not.toHaveProperty('address')
|
|
36
|
+
expectTypeOf<ShowDeposit>().not.toHaveProperty('chainId')
|
|
37
|
+
})
|
|
23
38
|
})
|
|
24
39
|
|
|
25
40
|
describe('pollResponse', () => {
|
|
@@ -44,6 +59,14 @@ describe('pendingResponse', () => {
|
|
|
44
59
|
keyType: 'secp256k1' | 'p256' | 'webAuthn'
|
|
45
60
|
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
46
61
|
pubKey: Hex
|
|
62
|
+
showDeposit?:
|
|
63
|
+
| boolean
|
|
64
|
+
| {
|
|
65
|
+
amount?: string | undefined
|
|
66
|
+
displayName?: string | undefined
|
|
67
|
+
token?: string | undefined
|
|
68
|
+
}
|
|
69
|
+
| undefined
|
|
47
70
|
status: 'pending'
|
|
48
71
|
}>()
|
|
49
72
|
})
|
|
@@ -109,6 +109,7 @@ async function createRequest(
|
|
|
109
109
|
expiry?: number | undefined
|
|
110
110
|
keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
|
|
111
111
|
limits?: readonly { token: `0x${string}`; limit: bigint }[] | undefined
|
|
112
|
+
showDeposit?: z.output<typeof CliAuth.createRequest>['showDeposit'] | undefined
|
|
112
113
|
} = {},
|
|
113
114
|
) {
|
|
114
115
|
const key = options.accessKey ?? accessKey
|
|
@@ -128,6 +129,7 @@ async function createRequest(
|
|
|
128
129
|
: { keyType: key.keyType }),
|
|
129
130
|
...('limits' in options ? (options.limits ? { limits: options.limits } : {}) : { limits }),
|
|
130
131
|
pubKey: key.publicKey,
|
|
132
|
+
...(options.showDeposit !== undefined ? { showDeposit: options.showDeposit } : {}),
|
|
131
133
|
} satisfies z.output<typeof CliAuth.createRequest>,
|
|
132
134
|
}
|
|
133
135
|
}
|
|
@@ -671,6 +673,51 @@ describe('pending', () => {
|
|
|
671
673
|
`)
|
|
672
674
|
})
|
|
673
675
|
|
|
676
|
+
test('behavior: preserves showDeposit for browser approval', async () => {
|
|
677
|
+
const store = CliAuth.Store.memory()
|
|
678
|
+
const { request } = await createRequest('device-code-verifier', {
|
|
679
|
+
showDeposit: {
|
|
680
|
+
amount: '50',
|
|
681
|
+
displayName: 'DoorDash',
|
|
682
|
+
token: 'USDC',
|
|
683
|
+
},
|
|
684
|
+
})
|
|
685
|
+
const { code } = await CliAuth.createDeviceCode({
|
|
686
|
+
chainId: chain.id,
|
|
687
|
+
random: () => new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
|
|
688
|
+
request,
|
|
689
|
+
store,
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
const result = await CliAuth.pending({
|
|
693
|
+
code,
|
|
694
|
+
store,
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
expect(result).toMatchInlineSnapshot(`
|
|
698
|
+
{
|
|
699
|
+
"accessKeyAddress": "${accessKey.address.toLowerCase()}",
|
|
700
|
+
"chainId": 1337n,
|
|
701
|
+
"code": "ABCDEFGH",
|
|
702
|
+
"expiry": ${expiry},
|
|
703
|
+
"keyType": "p256",
|
|
704
|
+
"limits": [
|
|
705
|
+
{
|
|
706
|
+
"limit": 1000n,
|
|
707
|
+
"token": "0x20c0000000000000000000000000000000000001",
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
"pubKey": "${accessKey.publicKey}",
|
|
711
|
+
"showDeposit": {
|
|
712
|
+
"amount": "50",
|
|
713
|
+
"displayName": "DoorDash",
|
|
714
|
+
"token": "USDC",
|
|
715
|
+
},
|
|
716
|
+
"status": "pending",
|
|
717
|
+
}
|
|
718
|
+
`)
|
|
719
|
+
})
|
|
720
|
+
|
|
674
721
|
test('behavior: handler returns 404 for an unknown code', async () => {
|
|
675
722
|
const handler = Handler.codeAuth()
|
|
676
723
|
|
package/src/server/CliAuth.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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 { tempo } from 'viem/chains'
|
|
5
|
+
import { tempo } from 'viem/tempo/chains'
|
|
6
6
|
import * as z from 'zod/mini'
|
|
7
7
|
|
|
8
8
|
import * as u from '../core/zod/utils.js'
|
|
@@ -12,6 +12,16 @@ import type { Kv } from './Kv.js'
|
|
|
12
12
|
const maxLimits = 10
|
|
13
13
|
const limit = z.object({ token: u.address(), limit: u.bigint() })
|
|
14
14
|
const limits = z.readonly(z.array(limit).check(z.maxLength(maxLimits)))
|
|
15
|
+
const showDeposit = z.optional(
|
|
16
|
+
z.union([
|
|
17
|
+
z.boolean(),
|
|
18
|
+
z.object({
|
|
19
|
+
amount: z.optional(z.string()),
|
|
20
|
+
displayName: z.optional(z.string()),
|
|
21
|
+
token: z.optional(z.union([u.address(), z.string()])),
|
|
22
|
+
}),
|
|
23
|
+
]),
|
|
24
|
+
)
|
|
15
25
|
const defaultTtlMs = 10 * 60 * 1_000
|
|
16
26
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
17
27
|
|
|
@@ -38,6 +48,7 @@ export const createRequest = z.object({
|
|
|
38
48
|
keyType: z.optional(keyType),
|
|
39
49
|
limits: z.optional(limits),
|
|
40
50
|
pubKey: u.hex(),
|
|
51
|
+
showDeposit,
|
|
41
52
|
})
|
|
42
53
|
|
|
43
54
|
/** Response body for `POST /cli-auth/device-code`. */
|
|
@@ -75,6 +86,7 @@ export const pendingResponse = z.object({
|
|
|
75
86
|
keyType,
|
|
76
87
|
limits: z.optional(limits),
|
|
77
88
|
pubKey: u.hex(),
|
|
89
|
+
showDeposit,
|
|
78
90
|
status: z.literal('pending'),
|
|
79
91
|
})
|
|
80
92
|
|
|
@@ -103,6 +115,7 @@ export const entry = u.oneOf([
|
|
|
103
115
|
keyType,
|
|
104
116
|
limits: z.optional(limits),
|
|
105
117
|
pubKey: u.hex(),
|
|
118
|
+
showDeposit,
|
|
106
119
|
status: z.literal('pending'),
|
|
107
120
|
}),
|
|
108
121
|
z.object({
|
|
@@ -119,6 +132,7 @@ export const entry = u.oneOf([
|
|
|
119
132
|
keyType,
|
|
120
133
|
limits: z.optional(limits),
|
|
121
134
|
pubKey: u.hex(),
|
|
135
|
+
showDeposit,
|
|
122
136
|
status: z.literal('authorized'),
|
|
123
137
|
}),
|
|
124
138
|
z.object({
|
|
@@ -136,6 +150,7 @@ export const entry = u.oneOf([
|
|
|
136
150
|
keyType,
|
|
137
151
|
limits: z.optional(limits),
|
|
138
152
|
pubKey: u.hex(),
|
|
153
|
+
showDeposit,
|
|
139
154
|
status: z.literal('consumed'),
|
|
140
155
|
}),
|
|
141
156
|
])
|
|
@@ -576,6 +591,9 @@ export function from(options: from.Options = {}): CliAuth {
|
|
|
576
591
|
keyType,
|
|
577
592
|
...(approved.limits ? { limits: approved.limits } : {}),
|
|
578
593
|
pubKey,
|
|
594
|
+
...(options.request.showDeposit !== undefined
|
|
595
|
+
? { showDeposit: options.request.showDeposit }
|
|
596
|
+
: {}),
|
|
579
597
|
status: 'pending',
|
|
580
598
|
})
|
|
581
599
|
|
|
@@ -601,6 +619,7 @@ export function from(options: from.Options = {}): CliAuth {
|
|
|
601
619
|
keyType: current.keyType,
|
|
602
620
|
...(current.limits ? { limits: current.limits } : {}),
|
|
603
621
|
pubKey: current.pubKey,
|
|
622
|
+
...(current.showDeposit !== undefined ? { showDeposit: current.showDeposit } : {}),
|
|
604
623
|
status: 'pending',
|
|
605
624
|
}
|
|
606
625
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hc } from 'hono/client'
|
|
2
2
|
import type { ExtractSchema } from 'hono/types'
|
|
3
3
|
import { http, type Chain, type Transport } from 'viem'
|
|
4
|
-
import { tempo, tempoModerato } from 'viem/chains'
|
|
4
|
+
import { tempo, tempoModerato } from 'viem/tempo/chains'
|
|
5
5
|
import { describe, expectTypeOf, test } from 'vp/test'
|
|
6
6
|
|
|
7
7
|
import * as CliAuth from './CliAuth.js'
|
package/src/server/Kv.test.ts
CHANGED
|
@@ -47,6 +47,36 @@ describe('memory', () => {
|
|
|
47
47
|
expect(await kv.get('a')).toMatchInlineSnapshot(`"v2"`)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('create: writes only when key is absent', async () => {
|
|
51
|
+
const kv = Kv.memory()
|
|
52
|
+
|
|
53
|
+
expect(await kv.create!('a', 'v1')).toMatchInlineSnapshot(`true`)
|
|
54
|
+
expect(await kv.create!('a', 'v2')).toMatchInlineSnapshot(`false`)
|
|
55
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('create: replaces expired entries', async () => {
|
|
59
|
+
let now = 1_000_000
|
|
60
|
+
const kv = Kv.memory({ now: () => now })
|
|
61
|
+
|
|
62
|
+
await kv.set('a', 'v1', { ttl: 1 })
|
|
63
|
+
now += 2_000
|
|
64
|
+
expect(await kv.create!('a', 'v2')).toMatchInlineSnapshot(`true`)
|
|
65
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v2"`)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('create: concurrent callers — only one writes', async () => {
|
|
69
|
+
const kv = Kv.memory()
|
|
70
|
+
|
|
71
|
+
const results = await Promise.all([kv.create!('a', 'v1'), kv.create!('a', 'v2')])
|
|
72
|
+
expect(results.filter(Boolean)).toMatchInlineSnapshot(`
|
|
73
|
+
[
|
|
74
|
+
true,
|
|
75
|
+
]
|
|
76
|
+
`)
|
|
77
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
78
|
+
})
|
|
79
|
+
|
|
50
80
|
test('take: returns the value and removes the entry', async () => {
|
|
51
81
|
const kv = Kv.memory()
|
|
52
82
|
|
|
@@ -163,6 +193,18 @@ describe('durableObject + NonceStorage', () => {
|
|
|
163
193
|
`)
|
|
164
194
|
})
|
|
165
195
|
|
|
196
|
+
test('create: concurrent callers — only one wins', async () => {
|
|
197
|
+
const kv = Kv.durableObject(fakeDurableObject())
|
|
198
|
+
|
|
199
|
+
const results = await Promise.all([kv.create!('a', 'v1'), kv.create!('a', 'v2')])
|
|
200
|
+
expect(results.filter(Boolean)).toMatchInlineSnapshot(`
|
|
201
|
+
[
|
|
202
|
+
true,
|
|
203
|
+
]
|
|
204
|
+
`)
|
|
205
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
206
|
+
})
|
|
207
|
+
|
|
166
208
|
test('take: missing key returns undefined', async () => {
|
|
167
209
|
const kv = Kv.durableObject(fakeDurableObject())
|
|
168
210
|
expect(await kv.take!('missing')).toMatchInlineSnapshot(`undefined`)
|
|
@@ -226,6 +268,15 @@ describe('cloudflare', () => {
|
|
|
226
268
|
expect(kv.take).toBeUndefined()
|
|
227
269
|
})
|
|
228
270
|
|
|
271
|
+
test('create: NOT implemented (CF KV is not linearizable)', () => {
|
|
272
|
+
const kv = Kv.cloudflare({
|
|
273
|
+
get: async () => null,
|
|
274
|
+
put: async () => {},
|
|
275
|
+
delete: async () => {},
|
|
276
|
+
})
|
|
277
|
+
expect(kv.create).toBeUndefined()
|
|
278
|
+
})
|
|
279
|
+
|
|
229
280
|
test('ttl: passes expirationTtl seconds to underlying put', async () => {
|
|
230
281
|
const puts: { key: string; value: string; options: unknown }[] = []
|
|
231
282
|
const fakeKv = {
|
package/src/server/Kv.ts
CHANGED
|
@@ -16,6 +16,16 @@ export type Kv = {
|
|
|
16
16
|
set: (key: string, value: unknown, options?: set.Options | undefined) => Promise<void>
|
|
17
17
|
/** Delete a value by key. */
|
|
18
18
|
delete: (key: string) => Promise<void>
|
|
19
|
+
/**
|
|
20
|
+
* Atomic create-if-absent. Returns `true` when the value was written,
|
|
21
|
+
* `false` when a non-expired value already exists.
|
|
22
|
+
*
|
|
23
|
+
* Optional. Backends with a linearizable create primitive should
|
|
24
|
+
* implement this so consumers can atomically reject duplicates.
|
|
25
|
+
* Consumers may fall back to `get` then `set` when strict atomicity is
|
|
26
|
+
* not required.
|
|
27
|
+
*/
|
|
28
|
+
create?: (key: string, value: unknown, options?: set.Options | undefined) => Promise<boolean>
|
|
19
29
|
/**
|
|
20
30
|
* Atomic read-and-delete. Returns the value if present, `undefined` if
|
|
21
31
|
* missing or expired. Across concurrent callers, exactly one observer
|
|
@@ -51,11 +61,10 @@ export function from<kv extends Kv>(kv: kv): kv {
|
|
|
51
61
|
* Cloudflare KV's minimum TTL is 60 seconds; the platform enforces its own
|
|
52
62
|
* minimum independent of what's passed here.
|
|
53
63
|
*
|
|
54
|
-
* **Not safe for one-time-consume semantics.**
|
|
55
|
-
* consistent across data centers
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* for the SIWE challenge nonce store.
|
|
64
|
+
* **Not safe for one-time-consume or unique-key semantics.**
|
|
65
|
+
* Cloudflare KV is eventually consistent across data centers, so `take`
|
|
66
|
+
* and `create` are intentionally NOT implemented. Use a Durable Object
|
|
67
|
+
* (or another linearizable backend) for the SIWE challenge nonce store.
|
|
59
68
|
*/
|
|
60
69
|
export function cloudflare(kv: cloudflare.Parameters): Kv {
|
|
61
70
|
return from({
|
|
@@ -84,10 +93,11 @@ export declare namespace cloudflare {
|
|
|
84
93
|
|
|
85
94
|
/**
|
|
86
95
|
* Adapt a Cloudflare Durable Object namespace into a `Kv` with atomic
|
|
87
|
-
* `take`. Unlike `Kv.cloudflare`, a Durable Object's
|
|
88
|
-
* single-actor and linearizable
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* `take` and `create`. Unlike `Kv.cloudflare`, a Durable Object's
|
|
97
|
+
* storage is single-actor and linearizable, so `take` (read+delete) and
|
|
98
|
+
* `create` (create-if-absent) are guaranteed atomic across concurrent
|
|
99
|
+
* callers. This makes it the recommended backend for SIWE challenge
|
|
100
|
+
* nonce storage on Cloudflare Workers.
|
|
91
101
|
*
|
|
92
102
|
* Pair with `Kv.NonceStorage` (or your own DO class implementing the
|
|
93
103
|
* same fetch protocol).
|
|
@@ -147,6 +157,12 @@ export function durableObject(
|
|
|
147
157
|
async set(key, value, options) {
|
|
148
158
|
await rpc('set', key, { value, ttl: options?.ttl })
|
|
149
159
|
},
|
|
160
|
+
async create(key, value, options) {
|
|
161
|
+
const { created } = (await rpc('create', key, { value, ttl: options?.ttl })) as {
|
|
162
|
+
created: boolean
|
|
163
|
+
}
|
|
164
|
+
return created
|
|
165
|
+
},
|
|
150
166
|
async delete(key) {
|
|
151
167
|
await rpc('delete', key)
|
|
152
168
|
},
|
|
@@ -228,6 +244,17 @@ export class NonceStorage {
|
|
|
228
244
|
await this.state.storage.put(key, entry)
|
|
229
245
|
return Response.json({})
|
|
230
246
|
}
|
|
247
|
+
if (op === 'create') {
|
|
248
|
+
const current = await this.state.storage.get<NonceStorage.Entry>(key)
|
|
249
|
+
if (current && !isExpired(current)) return Response.json({ created: false })
|
|
250
|
+
|
|
251
|
+
const body = (await request.json()) as { value: unknown; ttl?: number }
|
|
252
|
+
const entry: NonceStorage.Entry = body.ttl
|
|
253
|
+
? { value: body.value, expiresAt: Date.now() + body.ttl * 1000 }
|
|
254
|
+
: { value: body.value }
|
|
255
|
+
await this.state.storage.put(key, entry)
|
|
256
|
+
return Response.json({ created: true })
|
|
257
|
+
}
|
|
231
258
|
if (op === 'delete') {
|
|
232
259
|
await this.state.storage.delete(key)
|
|
233
260
|
return Response.json({})
|
|
@@ -280,6 +307,13 @@ export function memory(options: memory.Options = {}): Kv {
|
|
|
280
307
|
const expiresAt = options?.ttl ? now() + options.ttl * 1000 : undefined
|
|
281
308
|
store.set(key, expiresAt !== undefined ? { value, expiresAt } : { value })
|
|
282
309
|
},
|
|
310
|
+
async create(key, value, options) {
|
|
311
|
+
const entry = store.get(key)
|
|
312
|
+
if (entry && !isExpired(entry)) return false
|
|
313
|
+
const expiresAt = options?.ttl ? now() + options.ttl * 1000 : undefined
|
|
314
|
+
store.set(key, expiresAt !== undefined ? { value, expiresAt } : { value })
|
|
315
|
+
return true
|
|
316
|
+
},
|
|
283
317
|
// Atomic in-process: the synchronous `Map.get` + `Map.delete` runs
|
|
284
318
|
// in a single microtask, so concurrent `take(key)` callers (within
|
|
285
319
|
// the same Node/Bun/Worker process) cannot both observe the value.
|