accounts 0.7.2 → 0.8.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +19 -20
  3. package/dist/cli/Provider.d.ts +1 -1
  4. package/dist/cli/Provider.d.ts.map +1 -1
  5. package/dist/cli/Provider.js +4 -1
  6. package/dist/cli/Provider.js.map +1 -1
  7. package/dist/cli/keyring.js +1 -1
  8. package/dist/cli/keyring.js.map +1 -1
  9. package/dist/core/Account.d.ts +2 -0
  10. package/dist/core/Account.d.ts.map +1 -1
  11. package/dist/core/Account.js.map +1 -1
  12. package/dist/core/Adapter.d.ts +9 -1
  13. package/dist/core/Adapter.d.ts.map +1 -1
  14. package/dist/core/Dialog.d.ts +16 -1
  15. package/dist/core/Dialog.d.ts.map +1 -1
  16. package/dist/core/Dialog.js +40 -3
  17. package/dist/core/Dialog.js.map +1 -1
  18. package/dist/core/Messenger.d.ts +15 -0
  19. package/dist/core/Messenger.d.ts.map +1 -1
  20. package/dist/core/Messenger.js.map +1 -1
  21. package/dist/core/Provider.d.ts +2 -0
  22. package/dist/core/Provider.d.ts.map +1 -1
  23. package/dist/core/Provider.js +24 -6
  24. package/dist/core/Provider.js.map +1 -1
  25. package/dist/core/Remote.d.ts +7 -1
  26. package/dist/core/Remote.d.ts.map +1 -1
  27. package/dist/core/Remote.js +18 -2
  28. package/dist/core/Remote.js.map +1 -1
  29. package/dist/core/Schema.d.ts +17 -3
  30. package/dist/core/Schema.d.ts.map +1 -1
  31. package/dist/core/Store.d.ts +2 -0
  32. package/dist/core/Store.d.ts.map +1 -1
  33. package/dist/core/Store.js +12 -7
  34. package/dist/core/Store.js.map +1 -1
  35. package/dist/core/TrustedHosts.d.ts.map +1 -1
  36. package/dist/core/TrustedHosts.js +2 -0
  37. package/dist/core/TrustedHosts.js.map +1 -1
  38. package/dist/core/WebAuthnCeremony.d.ts +8 -0
  39. package/dist/core/WebAuthnCeremony.d.ts.map +1 -1
  40. package/dist/core/WebAuthnCeremony.js.map +1 -1
  41. package/dist/core/adapters/dialog.d.ts +3 -1
  42. package/dist/core/adapters/dialog.d.ts.map +1 -1
  43. package/dist/core/adapters/dialog.js +8 -5
  44. package/dist/core/adapters/dialog.js.map +1 -1
  45. package/dist/core/adapters/local.js +4 -4
  46. package/dist/core/adapters/local.js.map +1 -1
  47. package/dist/core/adapters/webAuthn.d.ts.map +1 -1
  48. package/dist/core/adapters/webAuthn.js +7 -2
  49. package/dist/core/adapters/webAuthn.js.map +1 -1
  50. package/dist/core/zod/rpc.d.ts +17 -7
  51. package/dist/core/zod/rpc.d.ts.map +1 -1
  52. package/dist/core/zod/rpc.js +5 -1
  53. package/dist/core/zod/rpc.js.map +1 -1
  54. package/dist/react/Remote.d.ts +2 -0
  55. package/dist/react/Remote.d.ts.map +1 -1
  56. package/dist/react/Remote.js +69 -0
  57. package/dist/react/Remote.js.map +1 -1
  58. package/dist/react-native/Provider.d.ts.map +1 -1
  59. package/dist/react-native/Provider.js +4 -1
  60. package/dist/react-native/Provider.js.map +1 -1
  61. package/dist/react-native/adapter.d.ts +1 -1
  62. package/dist/react-native/adapter.d.ts.map +1 -1
  63. package/dist/react-native/adapter.js +2 -0
  64. package/dist/react-native/adapter.js.map +1 -1
  65. package/dist/server/CliAuth.d.ts +82 -11
  66. package/dist/server/CliAuth.d.ts.map +1 -1
  67. package/dist/server/CliAuth.js +82 -11
  68. package/dist/server/CliAuth.js.map +1 -1
  69. package/dist/server/internal/handlers/codeAuth.d.ts +2 -2
  70. package/dist/server/internal/handlers/codeAuth.js +2 -2
  71. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  72. package/dist/server/internal/handlers/relay.js +182 -88
  73. package/dist/server/internal/handlers/relay.js.map +1 -1
  74. package/dist/server/internal/handlers/utils.d.ts +2 -2
  75. package/dist/server/internal/handlers/utils.d.ts.map +1 -1
  76. package/dist/server/internal/handlers/utils.js +7 -2
  77. package/dist/server/internal/handlers/utils.js.map +1 -1
  78. package/dist/server/internal/handlers/webAuthn.d.ts +2 -0
  79. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
  80. package/dist/server/internal/handlers/webAuthn.js +20 -9
  81. package/dist/server/internal/handlers/webAuthn.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cli/Provider.test.ts +3 -1
  84. package/src/cli/Provider.ts +4 -2
  85. package/src/cli/keyring.ts +1 -1
  86. package/src/core/Account.ts +2 -0
  87. package/src/core/Adapter.ts +9 -1
  88. package/src/core/Dialog.browser.test.ts +3 -3
  89. package/src/core/Dialog.ts +51 -4
  90. package/src/core/Messenger.ts +12 -0
  91. package/src/core/Provider.ts +46 -18
  92. package/src/core/Remote.ts +26 -4
  93. package/src/core/Store.ts +12 -4
  94. package/src/core/TrustedHosts.ts +1 -0
  95. package/src/core/WebAuthnCeremony.ts +8 -0
  96. package/src/core/adapters/dialog.ts +10 -5
  97. package/src/core/adapters/local.ts +4 -4
  98. package/src/core/adapters/webAuthn.ts +7 -2
  99. package/src/core/zod/rpc.ts +5 -1
  100. package/src/react/Remote.ts +76 -0
  101. package/src/react-native/Provider.ts +9 -1
  102. package/src/react-native/adapter.ts +3 -1
  103. package/src/server/CliAuth.ts +82 -11
  104. package/src/server/internal/handlers/codeAuth.ts +2 -2
  105. package/src/server/internal/handlers/relay.test.ts +351 -1
  106. package/src/server/internal/handlers/relay.ts +207 -93
  107. package/src/server/internal/handlers/utils.ts +8 -2
  108. package/src/server/internal/handlers/webAuthn.ts +24 -12
@@ -130,107 +130,156 @@ export function relay(options: relay.Options = {}): Handler {
130
130
  typeof parameters.from === 'string' ? (parameters.from as Address) : undefined
131
131
  const requestFeeToken =
132
132
  typeof parameters.feeToken === 'string' ? (parameters.feeToken as Address) : undefined
133
- const requestsSponsorship = !!feePayerOptions && parameters.feePayer !== false
134
-
135
- // 1. Resolve fee token.
136
- const feeToken = features.feeTokenResolution
137
- ? await resolveFeeToken(client, {
138
- account: from,
139
- feeToken: requestFeeToken,
140
- tokens: resolveTokens(chainId),
141
- })
142
- : requestFeeToken
143
-
144
- // 2. Fill transaction via RPC node (with AMM resolution on InsufficientBalance).
145
- const normalized = Utils.normalizeFillTransactionRequest(parameters)
146
- const transaction = {
133
+ const externalFeePayerUrl =
134
+ typeof parameters.feePayer === 'string' ? parameters.feePayer : undefined
135
+ const requestsSponsorship =
136
+ (!!feePayerOptions || !!externalFeePayerUrl) && parameters.feePayer !== false
137
+
138
+ const { feePayer: _feePayer, ...normalized } =
139
+ Utils.normalizeFillTransactionRequest(parameters)
140
+ const baseTx = {
147
141
  ...normalized,
148
142
  ...(typeof chainId !== 'undefined' ? { chainId } : {}),
149
- ...(requestsSponsorship ? { feePayer: true } : {}),
150
- ...(feeToken ? { feeToken } : {}),
143
+ ...(requestFeeToken ? { feeToken: requestFeeToken } : {}),
151
144
  }
152
- let filled = await (async () => {
153
- if (requestsSponsorship && Sponsorship.isPreparedTransaction(transaction))
154
- return { transaction: Utils.normalizeTempoTransaction(transaction) }
155
- return await fill(client, { autoSwap, feeToken, transaction })
156
- })()
157
145
 
158
- // 3. Check if the fee payer approves this transaction.
159
- const sponsored =
160
- requestsSponsorship && feePayerOptions
161
- ? await Sponsorship.shouldSponsor({
162
- sender: from,
163
- transaction: filled.transaction,
164
- validate: feePayerOptions.validate,
146
+ let filled: Awaited<ReturnType<typeof fill>>
147
+ let sponsored = false
148
+ let feeToken = requestFeeToken
149
+
150
+ // Lazily resolve a swap source token when autoSwap needs one.
151
+ const resolveFeeTokenForSwap = from
152
+ ? (insufficientToken: Address) =>
153
+ resolveFeeToken(client, {
154
+ account: from,
155
+ feeToken: undefined,
156
+ tokens: (resolveTokens(chainId) ?? []).filter(
157
+ (t) => t.toLowerCase() !== insufficientToken.toLowerCase(),
158
+ ),
165
159
  })
166
- : false
167
-
168
- // Re-fill without feePayer when sponsorship is rejected so the
169
- // gas estimate and nonce are correct for a self-paid transaction.
170
- if (requestsSponsorship && !sponsored) {
171
- const { feePayer: _, ...tx } = transaction
172
- filled = await fill(client, { autoSwap, feeToken, transaction: tx })
160
+ : undefined
161
+
162
+ // When the app provides its own fee payer URL, route the fill
163
+ // through that service so it can sign the transaction.
164
+ const fillClient = externalFeePayerUrl
165
+ ? createClient({
166
+ chain: client.chain,
167
+ batch: { multicall: { deployless: true } },
168
+ transport: http(externalFeePayerUrl),
169
+ })
170
+ : client
171
+
172
+ if (requestsSponsorship && !feePayerOptions?.validate) {
173
+ // Path A: sponsorship guaranteed (no validate) — skip fee token
174
+ // resolution, fill once with feePayer, then parallelize the rest.
175
+ const transaction = { ...baseTx, feePayer: true }
176
+ if (Sponsorship.isPreparedTransaction(transaction)) {
177
+ filled = {
178
+ transaction: Utils.normalizeTempoTransaction(transaction),
179
+ sponsor: undefined,
180
+ }
181
+ } else {
182
+ filled = await fill(fillClient, {
183
+ autoSwap,
184
+ feeToken,
185
+ resolveFeeToken: resolveFeeTokenForSwap,
186
+ transaction,
187
+ })
188
+ }
189
+ sponsored = true
190
+ } else if (requestsSponsorship && feePayerOptions?.validate) {
191
+ // Path B: sponsorship possible but may be rejected — fill both
192
+ // variants in parallel, then pick the right one.
193
+ const sponsoredTx = { ...baseTx, feePayer: true }
194
+
195
+ if (Sponsorship.isPreparedTransaction(sponsoredTx)) {
196
+ // Already prepared — skip fills, just validate sponsorship.
197
+ const prepared = {
198
+ transaction: Utils.normalizeTempoTransaction(sponsoredTx),
199
+ sponsor: undefined,
200
+ }
201
+ sponsored = await Sponsorship.shouldSponsor({
202
+ sender: from,
203
+ transaction: prepared.transaction,
204
+ validate: feePayerOptions!.validate,
205
+ })
206
+ filled = prepared
207
+ } else {
208
+ const fillOptions = {
209
+ autoSwap,
210
+ feeToken,
211
+ resolveFeeToken: resolveFeeTokenForSwap,
212
+ }
213
+ const [sponsoredFill, unsponsoredFill] = await Promise.all([
214
+ fill(fillClient, { ...fillOptions, transaction: sponsoredTx }),
215
+ fill(client, { ...fillOptions, transaction: baseTx }),
216
+ ])
217
+ sponsored = await Sponsorship.shouldSponsor({
218
+ sender: from,
219
+ transaction: sponsoredFill.transaction,
220
+ validate: feePayerOptions!.validate,
221
+ })
222
+ filled = sponsored ? sponsoredFill : unsponsoredFill
223
+ }
224
+ } else {
225
+ // Path C: no sponsorship configured — resolve fee token, fill once.
226
+ feeToken = features.feeTokenResolution
227
+ ? await resolveFeeToken(client, {
228
+ account: from,
229
+ feeToken: requestFeeToken,
230
+ tokens: resolveTokens(chainId),
231
+ })
232
+ : requestFeeToken
233
+ const transaction = { ...baseTx, ...(feeToken ? { feeToken } : {}) }
234
+ filled = await fill(client, {
235
+ autoSwap,
236
+ feeToken,
237
+ resolveFeeToken: resolveFeeTokenForSwap,
238
+ transaction,
239
+ })
173
240
  }
174
241
 
175
242
  const transaction_filled = filled.transaction
176
243
  const swap = 'swap' in filled ? filled.swap : undefined
244
+ if (!feeToken)
245
+ feeToken =
246
+ (transaction_filled.feeToken as Address | undefined) ?? resolveTokens(chainId)?.[0]
177
247
 
178
- // 4. Simulate and compute balance diffs + fee.
179
- const { balanceDiffs, fee } = features.simulate
180
- ? await simulateAndParseDiffs(client, {
181
- account: from,
182
- calls: extractCalls(transaction_filled),
183
- swap,
184
- feeToken: transaction_filled.feeToken,
185
- gas: transaction_filled.gas,
186
- maxFeePerGas: transaction_filled.maxFeePerGas,
187
- })
188
- : { balanceDiffs: undefined, fee: undefined }
189
-
190
- // 5. Sign as fee payer (if sponsored and not already signed).
248
+ // Parallelize: simulate, fee payer signing, and autoSwap metadata.
191
249
  const alreadySigned =
192
250
  'feePayerSignature' in transaction_filled &&
193
251
  transaction_filled.feePayerSignature != null
194
- const transaction_final = await (async () => {
195
- if (!sponsored || !feePayerOptions || alreadySigned) return transaction_filled
196
- return await Sponsorship.sign({
197
- account: feePayerOptions.account,
198
- sender: from,
199
- transaction: transaction_filled,
200
- })
201
- })()
202
252
 
203
- const sponsor =
204
- sponsored && feePayerOptions ? Sponsorship.getSponsor(feePayerOptions) : undefined
205
-
206
- // 6. Resolve autoSwap metadata (when AMM path was taken).
207
- const autoSwap_ = await (async () => {
208
- if (!autoSwap || !swap) return undefined
209
- const [inMeta, outMeta] = await Promise.all([
210
- resolveTokenMetadata(client, swap.tokenIn).catch(() => undefined),
211
- resolveTokenMetadata(client, swap.tokenOut).catch(() => undefined),
212
- ])
213
- if (!inMeta || !outMeta) return undefined
214
- return {
215
- calls: swap.calls.map((c) => ({ to: c.to, data: c.data })),
216
- slippage: autoSwap!.slippage,
217
- maxIn: {
218
- token: swap.tokenIn,
219
- value: Hex.fromNumber(swap.maxAmountIn) as `0x${string}`,
220
- formatted: formatUnits(swap.maxAmountIn, inMeta.decimals),
221
- decimals: inMeta.decimals,
222
- symbol: inMeta.symbol,
223
- name: inMeta.name,
224
- },
225
- minOut: {
226
- token: swap.tokenOut,
227
- value: Hex.fromNumber(swap.amountOut) as `0x${string}`,
228
- formatted: formatUnits(swap.amountOut, outMeta.decimals),
229
- decimals: outMeta.decimals,
230
- symbol: outMeta.symbol,
231
- name: outMeta.name,
232
- },
233
- }
253
+ const [{ balanceDiffs, fee }, transaction_final, autoSwap_] = await Promise.all([
254
+ // Simulate and compute balance diffs + fee.
255
+ features.simulate
256
+ ? simulateAndParseDiffs(client, {
257
+ account: from,
258
+ calls: extractCalls(transaction_filled),
259
+ swap,
260
+ feeToken,
261
+ gas: transaction_filled.gas,
262
+ maxFeePerGas: transaction_filled.maxFeePerGas,
263
+ })
264
+ : { balanceDiffs: undefined, fee: undefined },
265
+ // Sign as fee payer (if sponsored and not already signed).
266
+ sponsored && feePayerOptions && !alreadySigned
267
+ ? Sponsorship.sign({
268
+ account: feePayerOptions.account,
269
+ sender: from,
270
+ transaction: transaction_filled,
271
+ })
272
+ : Promise.resolve(transaction_filled),
273
+ // Resolve autoSwap metadata (when AMM path was taken).
274
+ resolveAutoSwapMetadata(client, { autoSwap, swap }),
275
+ ])
276
+
277
+ const sponsor = (() => {
278
+ if (!sponsored) return undefined
279
+ // App-provided fee payer: relay back the sponsor from the upstream response.
280
+ if (externalFeePayerUrl) return filled.sponsor
281
+ if (feePayerOptions) return Sponsorship.getSponsor(feePayerOptions)
282
+ return filled.sponsor
234
283
  })()
235
284
 
236
285
  return RpcResponse.from(
@@ -501,6 +550,7 @@ async function fill(
501
550
  options: {
502
551
  autoSwap?: { slippage: number } | undefined
503
552
  feeToken?: Address | undefined
553
+ resolveFeeToken?: ((insufficientToken: Address) => Promise<Address | undefined>) | undefined
504
554
  transaction: Record<string, unknown>
505
555
  },
506
556
  ) {
@@ -511,14 +561,24 @@ async function fill(
511
561
  value.type === '0x76' ? value : Utils.formatFillTransactionRequest(client, value)
512
562
 
513
563
  try {
564
+ const formatted = format(request)
514
565
  const result = await client.request({
515
566
  method: 'eth_fillTransaction',
516
- params: [format(request) as never],
567
+ params: [formatted as never],
517
568
  })
518
- return { transaction: Utils.normalizeTempoTransaction(result.tx) }
569
+ // FIXME: node estimates gas with secp256k1 dummy sig + null feePayerSignature.
570
+ // Actual tx has larger keychain/webAuthn sigs + real fee payer sig, costing
571
+ // more intrinsic gas. Mirror the bump from viem's tempo chainConfig.
572
+ // @ts-expect-error
573
+ if (result.tx.gas && result.tx.feePayer)
574
+ result.tx.gas = Hex.fromNumber(BigInt(result.tx.gas) + 20_000n)
575
+ const sponsor = (result as Record<string, any>).capabilities?.sponsor as
576
+ | { address: Address; name?: string; url?: string }
577
+ | undefined
578
+ return { transaction: Utils.normalizeTempoTransaction(result.tx), sponsor }
519
579
  } catch (error) {
520
580
  if (!(error instanceof Error)) throw error
521
- if (!feeToken || !autoSwap) throw error
581
+ if (!autoSwap) throw error
522
582
 
523
583
  const revert = ExecutionError.parse(error)
524
584
  if (revert?.errorName !== 'InsufficientBalance') throw error
@@ -526,13 +586,19 @@ async function fill(
526
586
  const [available, required, token] = revert.args
527
587
  if (typeof available === 'undefined' || typeof required === 'undefined' || !token) throw error
528
588
 
529
- if (token.toLowerCase() === feeToken.toLowerCase()) throw error
589
+ // Resolve a source token for the swap: use the provided feeToken,
590
+ // or fall back to resolveFeeToken() to find one the sender holds.
591
+ const sourceToken =
592
+ feeToken && feeToken.toLowerCase() !== token.toLowerCase()
593
+ ? feeToken
594
+ : await options.resolveFeeToken?.(token as Address)
595
+ if (!sourceToken || sourceToken.toLowerCase() === token.toLowerCase()) throw error
530
596
 
531
597
  const deficit = required - available
532
598
  const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n
533
599
 
534
600
  const originalCalls = (request.calls as Call[] | undefined) ?? []
535
- const swapCalls = buildSwapCalls(feeToken, token, deficit, maxAmountIn)
601
+ const swapCalls = buildSwapCalls(sourceToken, token, deficit, maxAmountIn)
536
602
 
537
603
  const result = await client.request({
538
604
  method: 'eth_fillTransaction',
@@ -543,11 +609,15 @@ async function fill(
543
609
  }) as never,
544
610
  ],
545
611
  })
612
+ const sponsor = (result as Record<string, any>).capabilities?.sponsor as
613
+ | { address: Address; name?: string; url?: string }
614
+ | undefined
546
615
  return {
547
616
  transaction: Utils.normalizeTempoTransaction(result.tx),
617
+ sponsor,
548
618
  swap: {
549
619
  calls: swapCalls,
550
- tokenIn: feeToken,
620
+ tokenIn: sourceToken,
551
621
  tokenOut: token,
552
622
  amountOut: deficit,
553
623
  maxAmountIn,
@@ -851,6 +921,50 @@ async function resolveTokenMetadata(
851
921
  }
852
922
  }
853
923
 
924
+ async function resolveAutoSwapMetadata(
925
+ client: Client,
926
+ options: {
927
+ autoSwap?: { slippage: number } | undefined
928
+ swap?:
929
+ | {
930
+ calls: readonly { to: Address; data: `0x${string}` }[]
931
+ tokenIn: Address
932
+ tokenOut: Address
933
+ amountOut: bigint
934
+ maxAmountIn: bigint
935
+ }
936
+ | undefined
937
+ },
938
+ ) {
939
+ const { autoSwap, swap } = options
940
+ if (!autoSwap || !swap) return undefined
941
+ const [inMeta, outMeta] = await Promise.all([
942
+ resolveTokenMetadata(client, swap.tokenIn).catch(() => undefined),
943
+ resolveTokenMetadata(client, swap.tokenOut).catch(() => undefined),
944
+ ])
945
+ if (!inMeta || !outMeta) return undefined
946
+ return {
947
+ calls: swap.calls.map((c) => ({ to: c.to, data: c.data })),
948
+ slippage: autoSwap.slippage,
949
+ maxIn: {
950
+ token: swap.tokenIn,
951
+ value: Hex.fromNumber(swap.maxAmountIn) as `0x${string}`,
952
+ formatted: formatUnits(swap.maxAmountIn, inMeta.decimals),
953
+ decimals: inMeta.decimals,
954
+ symbol: inMeta.symbol,
955
+ name: inMeta.name,
956
+ },
957
+ minOut: {
958
+ token: swap.tokenOut,
959
+ value: Hex.fromNumber(swap.amountOut) as `0x${string}`,
960
+ formatted: formatUnits(swap.amountOut, outMeta.decimals),
961
+ decimals: outMeta.decimals,
962
+ symbol: outMeta.symbol,
963
+ name: outMeta.name,
964
+ },
965
+ }
966
+ }
967
+
854
968
  async function computeFee(
855
969
  client: Client,
856
970
  options: {
@@ -6,7 +6,11 @@ import * as z from 'zod/mini'
6
6
  export function resolveChainId(value: unknown) {
7
7
  if (typeof value === 'number') return value
8
8
  if (typeof value === 'bigint') return Number(value)
9
- if (typeof value === 'string' && Hex.validate(value)) return Hex.toNumber(value)
9
+ if (typeof value === 'string') {
10
+ if (Hex.validate(value)) return Hex.toNumber(value)
11
+ const n = Number(value)
12
+ if (Number.isFinite(n)) return n
13
+ }
10
14
  return undefined
11
15
  }
12
16
 
@@ -16,7 +20,9 @@ export function formatFillTransactionRequest(client: Client, value: Record<strin
16
20
  return format({ ...value } as never, 'fillTransaction') as Record<string, unknown>
17
21
  }
18
22
 
19
- export function normalizeFillTransactionRequest(tx: Record<string, unknown>) {
23
+ export function normalizeFillTransactionRequest(
24
+ tx: Record<string, unknown>,
25
+ ): Record<string, unknown> & { calls: unknown[] } {
20
26
  const { to, data, value, ...rest } = tx
21
27
  if (Array.isArray(tx.calls) && tx.calls.length > 0)
22
28
  return {
@@ -57,7 +57,7 @@ export function webAuthn(options: webAuthn.Options): Handler {
57
57
  ...(userId ? { user: { id: new TextEncoder().encode(userId), name } } : undefined),
58
58
  })
59
59
 
60
- await kv.set(`challenge:${challenge}`, Date.now())
60
+ await kv.set(`challenge:${challenge}`, { created: Date.now(), name })
61
61
 
62
62
  return Response.json({ options })
63
63
  } catch (error) {
@@ -74,10 +74,9 @@ export function webAuthn(options: webAuthn.Options): Handler {
74
74
  Bytes.toString(new Uint8Array(deserialized.clientDataJSON)),
75
75
  ) as { challenge: string }
76
76
  const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
77
- const stored = await kv.get<number>(`challenge:${challenge}`)
78
- if (!stored || Date.now() - stored > challengeTtl * 1_000)
77
+ const stored = await kv.get<{ created: number; name: string }>(`challenge:${challenge}`)
78
+ if (!stored || Date.now() - stored.created > challengeTtl * 1_000)
79
79
  throw new Error('Missing or expired challenge')
80
- await kv.delete(`challenge:${challenge}`)
81
80
 
82
81
  const result = Registration.verify(credential, {
83
82
  challenge,
@@ -88,10 +87,17 @@ export function webAuthn(options: webAuthn.Options): Handler {
88
87
  const { publicKey } = result.credential
89
88
  const credentialId = credential.id
90
89
 
91
- await kv.set(`credential:${credentialId}`, { publicKey })
92
-
93
90
  const json = { credentialId, publicKey }
94
- const hook = await onRegister?.({ credentialId, publicKey, request: c.req.raw })
91
+ const [, hook] = await Promise.all([
92
+ kv.set(`credential:${credentialId}`, { publicKey }),
93
+ onRegister?.({
94
+ credentialId,
95
+ name: stored.name,
96
+ publicKey,
97
+ request: c.req.raw,
98
+ }),
99
+ kv.delete(`challenge:${challenge}`),
100
+ ])
95
101
  return mergeResponse(json, hook)
96
102
  } catch (error) {
97
103
  return Response.json({ error: (error as Error).message }, { status: 400 })
@@ -136,12 +142,13 @@ export function webAuthn(options: webAuthn.Options): Handler {
136
142
  challenge: string
137
143
  }
138
144
  const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
139
- const stored = await kv.get<number>(`challenge:${challenge}`)
145
+
146
+ const [stored, credentialData] = await Promise.all([
147
+ kv.get<number>(`challenge:${challenge}`),
148
+ kv.get<{ publicKey: string }>(`credential:${response.id}`),
149
+ ])
140
150
  if (!stored || Date.now() - stored > challengeTtl * 1_000)
141
151
  throw new Error('Missing or expired challenge')
142
- await kv.delete(`challenge:${challenge}`)
143
-
144
- const credentialData = await kv.get<{ publicKey: string }>(`credential:${response.id}`)
145
152
  if (!credentialData) throw new Error('Unknown credential')
146
153
 
147
154
  const valid = Authentication.verify(response, {
@@ -160,7 +167,10 @@ export function webAuthn(options: webAuthn.Options): Handler {
160
167
  publicKey: credentialData.publicKey,
161
168
  ...(userHandle && userHandle.length > 0 ? { userId: userHandle } : undefined),
162
169
  }
163
- const hook = await onAuthenticate?.({ ...json, request: c.req.raw })
170
+ const [hook] = await Promise.all([
171
+ onAuthenticate?.({ ...json, request: c.req.raw }),
172
+ kv.delete(`challenge:${challenge}`),
173
+ ])
164
174
  return mergeResponse(json, hook)
165
175
  } catch (error) {
166
176
  return Response.json({ error: (error as Error).message }, { status: 400 })
@@ -179,6 +189,8 @@ export declare namespace webAuthn {
179
189
  /** Called after a successful registration. The returned response is merged onto the default JSON response. */
180
190
  onRegister?: (parameters: {
181
191
  credentialId: string
192
+ /** The name provided during `/register/options` (e.g. user email). */
193
+ name: string
182
194
  publicKey: string
183
195
  request: Request
184
196
  }) => Response | Promise<Response> | void | Promise<void>