accounts 0.7.2 → 0.8.1

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 (109) hide show
  1. package/CHANGELOG.md +29 -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 +183 -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.test.ts +1 -1
  102. package/src/react-native/Provider.ts +9 -1
  103. package/src/react-native/adapter.ts +3 -1
  104. package/src/server/CliAuth.ts +82 -11
  105. package/src/server/internal/handlers/codeAuth.ts +2 -2
  106. package/src/server/internal/handlers/relay.test.ts +351 -1
  107. package/src/server/internal/handlers/relay.ts +208 -93
  108. package/src/server/internal/handlers/utils.ts +8 -2
  109. 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,25 @@ 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
+ // Skip if another relay already bumped (indicated by feePayerSignature).
573
+ // @ts-expect-error
574
+ if (result.tx.gas && request.feePayer && !result.tx.feePayerSignature)
575
+ result.tx.gas = Hex.fromNumber(BigInt(result.tx.gas) + 20_000n)
576
+ const sponsor = (result as Record<string, any>).capabilities?.sponsor as
577
+ | { address: Address; name?: string; url?: string }
578
+ | undefined
579
+ return { transaction: Utils.normalizeTempoTransaction(result.tx), sponsor }
519
580
  } catch (error) {
520
581
  if (!(error instanceof Error)) throw error
521
- if (!feeToken || !autoSwap) throw error
582
+ if (!autoSwap) throw error
522
583
 
523
584
  const revert = ExecutionError.parse(error)
524
585
  if (revert?.errorName !== 'InsufficientBalance') throw error
@@ -526,13 +587,19 @@ async function fill(
526
587
  const [available, required, token] = revert.args
527
588
  if (typeof available === 'undefined' || typeof required === 'undefined' || !token) throw error
528
589
 
529
- if (token.toLowerCase() === feeToken.toLowerCase()) throw error
590
+ // Resolve a source token for the swap: use the provided feeToken,
591
+ // or fall back to resolveFeeToken() to find one the sender holds.
592
+ const sourceToken =
593
+ feeToken && feeToken.toLowerCase() !== token.toLowerCase()
594
+ ? feeToken
595
+ : await options.resolveFeeToken?.(token as Address)
596
+ if (!sourceToken || sourceToken.toLowerCase() === token.toLowerCase()) throw error
530
597
 
531
598
  const deficit = required - available
532
599
  const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n
533
600
 
534
601
  const originalCalls = (request.calls as Call[] | undefined) ?? []
535
- const swapCalls = buildSwapCalls(feeToken, token, deficit, maxAmountIn)
602
+ const swapCalls = buildSwapCalls(sourceToken, token, deficit, maxAmountIn)
536
603
 
537
604
  const result = await client.request({
538
605
  method: 'eth_fillTransaction',
@@ -543,11 +610,15 @@ async function fill(
543
610
  }) as never,
544
611
  ],
545
612
  })
613
+ const sponsor = (result as Record<string, any>).capabilities?.sponsor as
614
+ | { address: Address; name?: string; url?: string }
615
+ | undefined
546
616
  return {
547
617
  transaction: Utils.normalizeTempoTransaction(result.tx),
618
+ sponsor,
548
619
  swap: {
549
620
  calls: swapCalls,
550
- tokenIn: feeToken,
621
+ tokenIn: sourceToken,
551
622
  tokenOut: token,
552
623
  amountOut: deficit,
553
624
  maxAmountIn,
@@ -851,6 +922,50 @@ async function resolveTokenMetadata(
851
922
  }
852
923
  }
853
924
 
925
+ async function resolveAutoSwapMetadata(
926
+ client: Client,
927
+ options: {
928
+ autoSwap?: { slippage: number } | undefined
929
+ swap?:
930
+ | {
931
+ calls: readonly { to: Address; data: `0x${string}` }[]
932
+ tokenIn: Address
933
+ tokenOut: Address
934
+ amountOut: bigint
935
+ maxAmountIn: bigint
936
+ }
937
+ | undefined
938
+ },
939
+ ) {
940
+ const { autoSwap, swap } = options
941
+ if (!autoSwap || !swap) return undefined
942
+ const [inMeta, outMeta] = await Promise.all([
943
+ resolveTokenMetadata(client, swap.tokenIn).catch(() => undefined),
944
+ resolveTokenMetadata(client, swap.tokenOut).catch(() => undefined),
945
+ ])
946
+ if (!inMeta || !outMeta) return undefined
947
+ return {
948
+ calls: swap.calls.map((c) => ({ to: c.to, data: c.data })),
949
+ slippage: autoSwap.slippage,
950
+ maxIn: {
951
+ token: swap.tokenIn,
952
+ value: Hex.fromNumber(swap.maxAmountIn) as `0x${string}`,
953
+ formatted: formatUnits(swap.maxAmountIn, inMeta.decimals),
954
+ decimals: inMeta.decimals,
955
+ symbol: inMeta.symbol,
956
+ name: inMeta.name,
957
+ },
958
+ minOut: {
959
+ token: swap.tokenOut,
960
+ value: Hex.fromNumber(swap.amountOut) as `0x${string}`,
961
+ formatted: formatUnits(swap.amountOut, outMeta.decimals),
962
+ decimals: outMeta.decimals,
963
+ symbol: outMeta.symbol,
964
+ name: outMeta.name,
965
+ },
966
+ }
967
+ }
968
+
854
969
  async function computeFee(
855
970
  client: Client,
856
971
  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>