accounts 0.6.1 → 0.6.2

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 (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/Schema.d.ts +12 -12
  3. package/dist/core/adapters/dialog.d.ts.map +1 -1
  4. package/dist/core/adapters/dialog.js +3 -1
  5. package/dist/core/adapters/dialog.js.map +1 -1
  6. package/dist/core/zod/rpc.d.ts +9 -9
  7. package/dist/core/zod/rpc.js +1 -1
  8. package/dist/core/zod/rpc.js.map +1 -1
  9. package/dist/server/CliAuth.d.ts +11 -11
  10. package/dist/server/CliAuth.js +1 -1
  11. package/dist/server/CliAuth.js.map +1 -1
  12. package/dist/server/Handler.d.ts +4 -252
  13. package/dist/server/Handler.d.ts.map +1 -1
  14. package/dist/server/Handler.js +4 -573
  15. package/dist/server/Handler.js.map +1 -1
  16. package/dist/server/internal/handlers/codeAuth.d.ts +41 -0
  17. package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -0
  18. package/dist/server/internal/handlers/codeAuth.js +104 -0
  19. package/dist/server/internal/handlers/codeAuth.js.map +1 -0
  20. package/dist/server/internal/handlers/feePayer.d.ts +73 -0
  21. package/dist/server/internal/handlers/feePayer.d.ts.map +1 -0
  22. package/dist/server/internal/handlers/feePayer.js +184 -0
  23. package/dist/server/internal/handlers/feePayer.js.map +1 -0
  24. package/dist/server/internal/handlers/relay.d.ts +148 -0
  25. package/dist/server/internal/handlers/relay.d.ts.map +1 -0
  26. package/dist/server/internal/handlers/relay.js +600 -0
  27. package/dist/server/internal/handlers/relay.js.map +1 -0
  28. package/dist/server/internal/handlers/utils.d.ts +12 -0
  29. package/dist/server/internal/handlers/utils.d.ts.map +1 -0
  30. package/dist/server/internal/handlers/utils.js +80 -0
  31. package/dist/server/internal/handlers/utils.js.map +1 -0
  32. package/dist/server/internal/handlers/webAuthn.d.ts +57 -0
  33. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -0
  34. package/dist/server/internal/handlers/webAuthn.js +143 -0
  35. package/dist/server/internal/handlers/webAuthn.js.map +1 -0
  36. package/package.json +2 -2
  37. package/src/core/Provider.connect.browser.test.ts +23 -2
  38. package/src/core/adapters/dialog.ts +6 -1
  39. package/src/core/zod/rpc.ts +1 -1
  40. package/src/server/CliAuth.ts +1 -1
  41. package/src/server/Handler.test.ts +3 -418
  42. package/src/server/Handler.ts +5 -766
  43. package/src/server/internal/handlers/codeAuth.ts +148 -0
  44. package/src/server/internal/handlers/feePayer.test.ts +335 -0
  45. package/src/server/internal/handlers/feePayer.ts +271 -0
  46. package/src/server/internal/handlers/relay.test.ts +767 -0
  47. package/src/server/internal/handlers/relay.ts +817 -0
  48. package/src/server/internal/handlers/utils.ts +96 -0
  49. package/src/server/internal/handlers/webAuthn.test.ts +170 -0
  50. package/src/server/internal/handlers/webAuthn.ts +213 -0
@@ -1,22 +1,11 @@
1
1
  import { Hono } from 'hono'
2
- import { Base64, Bytes, Hex, RpcRequest, RpcResponse, Signature } from 'ox'
3
- import { Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
4
- import { Credential } from 'ox/webauthn'
5
- import { type Chain, type Client, createClient, http, type Transport } from 'viem'
6
- import type { LocalAccount } from 'viem/accounts'
7
- import { signTransaction } from 'viem/actions'
8
- import { tempo, tempoModerato } from 'viem/chains'
9
- import { Transaction } from 'viem/tempo'
10
- import {
11
- Authentication,
12
- Registration,
13
- type Registration as Registration_Types,
14
- } from 'webauthx/server'
15
- import * as z from 'zod/mini'
16
2
 
17
- import * as CliAuth from './CliAuth.js'
18
3
  import * as RequestListener from './internal/requestListener.js'
19
- import * as Kv from './Kv.js'
4
+
5
+ export { codeAuth } from './internal/handlers/codeAuth.js'
6
+ export { feePayer } from './internal/handlers/feePayer.js'
7
+ export { relay } from './internal/handlers/relay.js'
8
+ export { webAuthn } from './internal/handlers/webAuthn.js'
20
9
 
21
10
  export type Handler = Hono & {
22
11
  listener: (req: any, res: any) => void
@@ -104,762 +93,12 @@ export declare namespace from {
104
93
  }
105
94
  }
106
95
 
107
- /**
108
- * Instantiates a fee payer service request handler that can be used to
109
- * sponsor the fee for user transactions.
110
- *
111
- * @example
112
- * ### Cloudflare Worker
113
- *
114
- * ```ts
115
- * import { privateKeyToAccount } from 'viem/accounts'
116
- * import { Handler } from 'accounts/server'
117
- *
118
- * export default {
119
- * fetch(request) {
120
- * return Handler.feePayer({
121
- * account: privateKeyToAccount('0x...'),
122
- * }).fetch(request)
123
- * }
124
- * }
125
- * ```
126
- *
127
- * @example
128
- * ### Next.js
129
- *
130
- * ```ts
131
- * import { privateKeyToAccount } from 'viem/accounts'
132
- * import { Handler } from 'accounts/server'
133
- *
134
- * const handler = Handler.feePayer({
135
- * account: privateKeyToAccount('0x...'),
136
- * })
137
- *
138
- * export GET = handler.fetch
139
- * export POST = handler.fetch
140
- * ```
141
- *
142
- * @example
143
- * ### Hono
144
- *
145
- * ```ts
146
- * import { privateKeyToAccount } from 'viem/accounts'
147
- * import { Handler } from 'accounts/server'
148
- *
149
- * const handler = Handler.feePayer({
150
- * account: privateKeyToAccount('0x...'),
151
- * })
152
- *
153
- * const app = new Hono()
154
- * app.all('*', handler)
155
- *
156
- * export default app
157
- * ```
158
- *
159
- * @example
160
- * ### Node.js
161
- *
162
- * ```ts
163
- * import { privateKeyToAccount } from 'viem/accounts'
164
- * import { Handler } from 'accounts/server'
165
- *
166
- * const handler = Handler.feePayer({
167
- * account: privateKeyToAccount('0x...'),
168
- * })
169
- *
170
- * const server = createServer(handler.listener)
171
- * server.listen(3000)
172
- * ```
173
- *
174
- * @example
175
- * ### Bun
176
- *
177
- * ```ts
178
- * import { privateKeyToAccount } from 'viem/accounts'
179
- * import { Handler } from 'accounts/server'
180
- *
181
- * const handler = Handler.feePayer({
182
- * account: privateKeyToAccount('0x...'),
183
- * })
184
- *
185
- * Bun.serve(handler)
186
- * ```
187
- *
188
- * @example
189
- * ### Deno
190
- *
191
- * ```ts
192
- * import { privateKeyToAccount } from 'viem/accounts'
193
- * import { Handler } from 'accounts/server'
194
- *
195
- * const handler = Handler.feePayer({
196
- * account: privateKeyToAccount('0x...'),
197
- * })
198
- *
199
- * Deno.serve(handler)
200
- * ```
201
- *
202
- * @example
203
- * ### Express
204
- *
205
- * ```ts
206
- * import { privateKeyToAccount } from 'viem/accounts'
207
- * import { Handler } from 'accounts/server'
208
- *
209
- * const handler = Handler.feePayer({
210
- * account: privateKeyToAccount('0x...'),
211
- * })
212
- *
213
- * const app = express()
214
- * app.use(handler.listener)
215
- * app.listen(3000)
216
- * ```
217
- *
218
- * @example
219
- * ### Custom chains & transports
220
- *
221
- * ```ts
222
- * import { http } from 'viem'
223
- * import { privateKeyToAccount } from 'viem/accounts'
224
- * import { tempo, tempoModerato } from 'viem/chains'
225
- * import { Handler } from 'accounts/server'
226
- *
227
- * const handler = Handler.feePayer({
228
- * account: privateKeyToAccount('0x...'),
229
- * chains: [tempo, tempoModerato],
230
- * transports: {
231
- * [tempo.id]: http('https://rpc.tempo.xyz'),
232
- * [tempoModerato.id]: http('https://rpc.testnet.tempo.xyz'),
233
- * },
234
- * })
235
- * ```
236
- *
237
- * @param options - Options.
238
- * @returns Request handler.
239
- */
240
- export function feePayer(options: feePayer.Options) {
241
- const {
242
- account,
243
- chains = [tempo, tempoModerato],
244
- name,
245
- onRequest,
246
- path = '/',
247
- transports = {},
248
- url,
249
- ...rest
250
- } = options
251
-
252
- const clients = new Map<number, Client>()
253
- for (const chain of chains) {
254
- const transport = transports[chain.id] ?? http()
255
- clients.set(chain.id, createClient({ chain, transport }))
256
- }
257
-
258
- function getClient(chainId?: number): Client {
259
- if (chainId) {
260
- const client = clients.get(chainId)
261
- if (!client) throw new Error(`Chain ${chainId} not configured`)
262
- return client
263
- }
264
- return clients.get(chains[0]!.id)!
265
- }
266
-
267
- const sponsor = {
268
- address: account.address,
269
- ...(name ? { name } : {}),
270
- ...(url ? { url } : {}),
271
- }
272
-
273
- const router = from(rest)
274
-
275
- router.post(path, async (c) => {
276
- const request = RpcRequest.from((await c.req.raw.json()) as any)
277
-
278
- try {
279
- await onRequest?.(request)
280
-
281
- const method = request.method as string
282
- if (
283
- method !== 'eth_fillTransaction' &&
284
- method !== 'eth_signRawTransaction' &&
285
- method !== 'eth_sendRawTransaction' &&
286
- method !== 'eth_sendRawTransactionSync'
287
- )
288
- return Response.json(
289
- RpcResponse.from(
290
- {
291
- error: new RpcResponse.MethodNotSupportedError({
292
- message: `Method not supported: ${request.method}`,
293
- }),
294
- },
295
- { request },
296
- ),
297
- )
298
-
299
- if (method === 'eth_fillTransaction') {
300
- const [parameters] = z
301
- .readonly(z.tuple([z.record(z.string(), z.unknown())]))
302
- .parse(request.params) as [Record<string, unknown>]
303
- const chainId = resolveChainId(parameters.chainId)
304
- const client = getClient(chainId)
305
- const transaction = await (async () => {
306
- if (isPreparedFeePayerTransaction(parameters))
307
- return normalizeTempoTransaction(parameters)
308
-
309
- const fillRequest = formatFillTransactionRequest(client, {
310
- ...normalizeFillTransactionRequest(parameters),
311
- ...(typeof chainId !== 'undefined' ? { chainId } : {}),
312
- feePayer: true,
313
- })
314
- const result = (await client.request({
315
- method: 'eth_fillTransaction' as never,
316
- params: [fillRequest],
317
- })) as { tx?: Record<string, unknown> | undefined }
318
- return normalizeTempoTransaction(result.tx)
319
- })()
320
-
321
- const from =
322
- (transaction.from as `0x${string}` | undefined) ??
323
- (typeof parameters.from === 'string' ? (parameters.from as `0x${string}`) : undefined)
324
- const { signature: _, ...withoutSenderSig } = transaction as Record<string, unknown>
325
- const prepared = { ...withoutSenderSig, from }
326
-
327
- if (!prepared.from)
328
- throw new RpcResponse.InvalidParamsError({
329
- message: 'Transaction sender must be provided before fee payer signing.',
330
- })
331
- if (!account.sign) throw new Error('Fee payer account cannot sign transactions.')
332
-
333
- const feePayerSignature = Signature.from(
334
- await account.sign({
335
- hash: TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from(prepared as never), {
336
- sender: prepared.from,
337
- }),
338
- }),
339
- )
340
-
341
- return rpcResult(request, {
342
- sponsor,
343
- tx: core_Transaction.toRpc({
344
- ...prepared,
345
- feePayerSignature,
346
- } as core_Transaction.Transaction),
347
- })
348
- }
349
-
350
- const serialized = request.params?.[0] as `0x76${string}` | undefined
351
-
352
- if (!serialized?.startsWith('0x76') && !serialized?.startsWith('0x78'))
353
- throw new RpcResponse.InvalidParamsError({
354
- message: 'Only Tempo (0x76/0x78) transactions are supported.',
355
- })
356
-
357
- const transaction = Transaction.deserialize(serialized)
358
-
359
- if (!transaction.signature || !transaction.from)
360
- throw new RpcResponse.InvalidParamsError({
361
- message: 'Transaction must be signed by the sender before fee payer signing.',
362
- })
363
-
364
- const client = getClient(transaction.chainId)
365
- const serializedTransaction = toSerializedTransaction(
366
- await signTransaction(client, {
367
- ...transaction,
368
- account,
369
- feePayer: account,
370
- } as never),
371
- )
372
-
373
- if (method === 'eth_signRawTransaction')
374
- return Response.json(RpcResponse.from({ result: serializedTransaction }, { request }))
375
-
376
- const result = await client.request({
377
- method: method as never,
378
- params: [serializedTransaction],
379
- })
380
-
381
- return Response.json(RpcResponse.from({ result }, { request }))
382
- } catch (error) {
383
- return rpcError(request, error)
384
- }
385
- })
386
-
387
- return router
388
- }
389
-
390
- export declare namespace feePayer {
391
- export type Options = from.Options & {
392
- /** Account to use as the fee payer. */
393
- account: LocalAccount
394
- /**
395
- * Supported chains. The handler resolves the client based on the
396
- * `chainId` in the incoming transaction.
397
- * @default [tempo, tempoModerato]
398
- */
399
- chains?: readonly [Chain, ...Chain[]] | undefined
400
- /** Function to call before handling the request. */
401
- onRequest?: (request: RpcRequest.RpcRequest) => Promise<void>
402
- /** Path to use for the handler. */
403
- path?: string | undefined
404
- /** Sponsor display name returned from `eth_fillTransaction`. */
405
- name?: string | undefined
406
- /** Transports keyed by chain ID. Defaults to `http()` for each chain. */
407
- transports?: Record<number, Transport> | undefined
408
- /** Sponsor URL returned from `eth_fillTransaction`. */
409
- url?: string | undefined
410
- }
411
- }
412
-
413
- /**
414
- * Instantiates a generic device-code handler for access-key bootstrap.
415
- *
416
- * Exposes 4 endpoints:
417
- * - `GET /auth/pkce/pending/:code`
418
- * - `POST /auth/pkce/code`
419
- * - `POST /auth/pkce/poll/:code`
420
- * - `POST /auth/pkce`
421
- *
422
- * @param options - Options.
423
- * @returns Request handler.
424
- */
425
- export function codeAuth(options: codeAuth.Options = {}): Handler {
426
- const {
427
- chains = [tempo, tempoModerato],
428
- now,
429
- path = '/auth/pkce',
430
- policy,
431
- random,
432
- store = CliAuth.Store.memory(),
433
- transports = {},
434
- ttlMs,
435
- ...rest
436
- } = options
437
-
438
- const clients = new Map<number, Client>()
439
- for (const chain of chains) {
440
- const transport = transports[chain.id] ?? http()
441
- clients.set(chain.id, createClient({ chain, transport }))
442
- }
443
-
444
- function getClient(chainId?: bigint | number): Client {
445
- if (typeof chainId !== 'undefined') {
446
- const id = Number(chainId)
447
- const client = clients.get(id)
448
- if (!client) throw new Error(`Chain ${id} not configured`)
449
- return client
450
- }
451
- return clients.get(chains[0]!.id)!
452
- }
453
-
454
- const router = from(rest)
455
-
456
- router.get(`${path}/pending/:code`, async (c) => {
457
- try {
458
- const code = c.req.param('code')
459
- const result = await CliAuth.pending({
460
- code,
461
- ...(now ? { now } : {}),
462
- store,
463
- })
464
-
465
- return Response.json(z.encode(CliAuth.pendingResponse, result))
466
- } catch (error) {
467
- const status = error instanceof CliAuth.PendingError ? error.status : 400
468
- return Response.json({ error: (error as Error).message }, { status })
469
- }
470
- })
471
-
472
- router.post(`${path}/code`, async (c) => {
473
- try {
474
- const request = z.decode(CliAuth.createRequest, await c.req.raw.json())
475
- const chainId = request.chainId ?? chains[0]!.id
476
- getClient(chainId)
477
- const result = await CliAuth.createDeviceCode({
478
- chainId,
479
- ...(now ? { now } : {}),
480
- ...(policy ? { policy } : {}),
481
- ...(random ? { random } : {}),
482
- request,
483
- store,
484
- ...(typeof ttlMs !== 'undefined' ? { ttlMs } : {}),
485
- })
486
-
487
- return Response.json(z.encode(CliAuth.createResponse, result))
488
- } catch (error) {
489
- return Response.json({ error: (error as Error).message }, { status: 400 })
490
- }
491
- })
492
-
493
- router.post(`${path}/poll/:code`, async (c) => {
494
- try {
495
- const request = z.decode(CliAuth.pollRequest, await c.req.raw.json())
496
- const code = c.req.param('code')
497
- const result = await CliAuth.poll({
498
- code,
499
- ...(now ? { now } : {}),
500
- request,
501
- store,
502
- })
503
-
504
- return Response.json(z.encode(CliAuth.pollResponse, result))
505
- } catch (error) {
506
- return Response.json({ error: (error as Error).message }, { status: 400 })
507
- }
508
- })
509
-
510
- router.post(path, async (c) => {
511
- try {
512
- const request = z.decode(CliAuth.authorizeRequest, await c.req.raw.json())
513
- const result = await CliAuth.authorize({
514
- client: getClient(request.keyAuthorization.chainId),
515
- ...(now ? { now } : {}),
516
- request,
517
- store,
518
- })
519
-
520
- return Response.json(z.encode(CliAuth.authorizeResponse, result))
521
- } catch (error) {
522
- return Response.json({ error: (error as Error).message }, { status: 400 })
523
- }
524
- })
525
-
526
- return router
527
- }
528
-
529
- export declare namespace codeAuth {
530
- export type Options = from.Options & {
531
- /**
532
- * Supported chains. The handler resolves the client based on chain IDs carried
533
- * by device-code requests and key authorizations.
534
- * @default [tempo, tempoModerato]
535
- */
536
- chains?: readonly [Chain, ...Chain[]] | undefined
537
- /** Time source used for TTL evaluation. */
538
- now?: (() => number) | undefined
539
- /** Path prefix for the code auth endpoints. @default "/auth/pkce" */
540
- path?: string | undefined
541
- /** Policy used to validate and default requested CLI auth fields. */
542
- policy?: CliAuth.Policy | undefined
543
- /** Random byte generator used for device-code allocation. */
544
- random?: ((size: number) => Uint8Array) | undefined
545
- /** Device-code store. */
546
- store?: CliAuth.Store | undefined
547
- /** Transports keyed by chain ID. Defaults to `http()` for each chain. */
548
- transports?: Record<number, Transport> | undefined
549
- /** Pending entry TTL in milliseconds. @default 600000 */
550
- ttlMs?: number | undefined
551
- }
552
- }
553
-
554
- /**
555
- * Instantiates a WebAuthn ceremony handler that manages registration and
556
- * authentication flows server-side.
557
- *
558
- * Exposes 4 POST endpoints following the webauthx convention:
559
- * - `POST /register/options` — generate credential creation options
560
- * - `POST /register` — verify registration and store credential
561
- * - `POST /login/options` — generate credential request options
562
- * - `POST /login` — verify authentication
563
- *
564
- * @example
565
- * ```ts
566
- * import { Handler, Kv } from 'accounts/server'
567
- *
568
- * const handler = Handler.webAuthn({
569
- * kv: Kv.memory(),
570
- * origin: 'https://example.com',
571
- * rpId: 'example.com',
572
- * })
573
- *
574
- * export default handler
575
- * ```
576
- *
577
- * @param options - Options.
578
- * @returns Request handler.
579
- */
580
- export function webAuthn(options: webAuthn.Options): Handler {
581
- const { challengeTtl = 300, kv, onAuthenticate, onRegister, path = '', rpId, ...rest } = options
582
- const origin = options.origin as string | string[]
583
-
584
- const router = from(rest)
585
-
586
- router.post(`${path}/register/options`, async (c) => {
587
- try {
588
- const body = await c.req.raw.json()
589
- const { excludeCredentialIds, name, userId } = body as {
590
- excludeCredentialIds?: string[]
591
- name: string
592
- userId?: string
593
- }
594
-
595
- const { challenge, options } = Registration.getOptions({
596
- excludeCredentialIds,
597
- name,
598
- rp: { id: rpId, name: rpId },
599
- ...(userId ? { user: { id: new TextEncoder().encode(userId), name } } : undefined),
600
- })
601
-
602
- await kv.set(`challenge:${challenge}`, Date.now())
603
-
604
- return Response.json({ options })
605
- } catch (error) {
606
- return Response.json({ error: (error as Error).message }, { status: 400 })
607
- }
608
- })
609
-
610
- router.post(`${path}/register`, async (c) => {
611
- try {
612
- const credential = (await c.req.raw.json()) as Registration_Types.Credential
613
- const deserialized = Credential.deserialize(credential)
614
-
615
- const clientData = JSON.parse(
616
- Bytes.toString(new Uint8Array(deserialized.clientDataJSON)),
617
- ) as { challenge: string }
618
- const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
619
- const stored = await kv.get<number>(`challenge:${challenge}`)
620
- if (!stored || Date.now() - stored > challengeTtl * 1_000)
621
- throw new Error('Missing or expired challenge')
622
- await kv.delete(`challenge:${challenge}`)
623
-
624
- const result = Registration.verify(credential, {
625
- challenge,
626
- origin,
627
- rpId,
628
- })
629
-
630
- const { publicKey } = result.credential
631
- const credentialId = credential.id
632
-
633
- await kv.set(`credential:${credentialId}`, { publicKey })
634
-
635
- const json = { credentialId, publicKey }
636
- const hook = await onRegister?.({ credentialId, publicKey, request: c.req.raw })
637
- return mergeResponse(json, hook)
638
- } catch (error) {
639
- return Response.json({ error: (error as Error).message }, { status: 400 })
640
- }
641
- })
642
-
643
- router.post(`${path}/login/options`, async (c) => {
644
- try {
645
- const body = await c.req.raw.json()
646
- const {
647
- allowCredentialIds,
648
- challenge: requestChallenge,
649
- credentialId,
650
- mediation,
651
- } = body as {
652
- allowCredentialIds?: string[]
653
- challenge?: Hex.Hex
654
- credentialId?: string
655
- mediation?: string
656
- }
657
-
658
- const { challenge, options: authOptions } = Authentication.getOptions({
659
- challenge: requestChallenge,
660
- credentialId: allowCredentialIds ?? credentialId,
661
- rpId,
662
- })
663
- const options = mediation ? { ...authOptions, mediation } : authOptions
664
-
665
- await kv.set(`challenge:${challenge}`, Date.now())
666
-
667
- return Response.json({ options })
668
- } catch (error) {
669
- return Response.json({ error: (error as Error).message }, { status: 400 })
670
- }
671
- })
672
-
673
- router.post(`${path}/login`, async (c) => {
674
- try {
675
- const response = (await c.req.raw.json()) as Authentication.Response
676
-
677
- const clientData = JSON.parse(response.metadata.clientDataJSON) as {
678
- challenge: string
679
- }
680
- const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
681
- const stored = await kv.get<number>(`challenge:${challenge}`)
682
- if (!stored || Date.now() - stored > challengeTtl * 1_000)
683
- throw new Error('Missing or expired challenge')
684
- await kv.delete(`challenge:${challenge}`)
685
-
686
- const credentialData = await kv.get<{ publicKey: string }>(`credential:${response.id}`)
687
- if (!credentialData) throw new Error('Unknown credential')
688
-
689
- const valid = Authentication.verify(response, {
690
- challenge,
691
- origin,
692
- publicKey: credentialData.publicKey as `0x${string}`,
693
- rpId,
694
- })
695
- if (!valid) throw new Error('Authentication failed')
696
-
697
- const rawResponse = response.raw?.response as unknown as Record<string, string> | undefined
698
- const userHandle = rawResponse?.userHandle
699
-
700
- const json = {
701
- credentialId: response.id,
702
- publicKey: credentialData.publicKey,
703
- ...(userHandle && userHandle.length > 0 ? { userId: userHandle } : undefined),
704
- }
705
- const hook = await onAuthenticate?.({ ...json, request: c.req.raw })
706
- return mergeResponse(json, hook)
707
- } catch (error) {
708
- return Response.json({ error: (error as Error).message }, { status: 400 })
709
- }
710
- })
711
-
712
- return router
713
- }
714
-
715
- export declare namespace webAuthn {
716
- type Options = from.Options & {
717
- /** Maximum age of a challenge in seconds before it expires. @default 300 */
718
- challengeTtl?: number | undefined
719
- /** Key-value store for challenges and credentials. */
720
- kv: Kv.Kv
721
- /** Called after a successful registration. The returned response is merged onto the default JSON response. */
722
- onRegister?: (parameters: {
723
- credentialId: string
724
- publicKey: string
725
- request: Request
726
- }) => Response | Promise<Response> | void | Promise<void>
727
- /** Called after a successful authentication. The returned response is merged onto the default JSON response. */
728
- onAuthenticate?: (parameters: {
729
- credentialId: string
730
- publicKey: string
731
- userId?: string | undefined
732
- request: Request
733
- }) => Response | Promise<Response> | void | Promise<void>
734
- /** Expected origin(s) (e.g. `"https://example.com"` or `["https://a.com", "https://b.com"]`). */
735
- origin: string | readonly string[]
736
- /** Path prefix for the WebAuthn endpoints (e.g. `"/webauthn"`). @default "" */
737
- path?: string | undefined
738
- /** Relying Party ID (e.g. `"example.com"`). */
739
- rpId: string
740
- }
741
- }
742
-
743
- /** @internal */
744
- function resolveChainId(value: unknown) {
745
- if (typeof value === 'number') return value
746
- if (typeof value === 'bigint') return Number(value)
747
- if (typeof value === 'string' && Hex.validate(value)) return Hex.toNumber(value)
748
- return undefined
749
- }
750
-
751
- /** @internal */
752
- function isPreparedFeePayerTransaction(value: Record<string, unknown>) {
753
- return (
754
- typeof value.from === 'string' &&
755
- typeof resolveChainId(value.chainId) === 'number' &&
756
- typeof value.gas !== 'undefined' &&
757
- typeof value.nonce !== 'undefined' &&
758
- (typeof value.maxFeePerGas !== 'undefined' || typeof value.gasPrice !== 'undefined')
759
- )
760
- }
761
-
762
- /** @internal */
763
- function formatFillTransactionRequest(client: Client, value: Record<string, unknown>) {
764
- const format = client.chain?.formatters?.transactionRequest?.format
765
- if (!format) return value
766
- return format({ ...value } as never, 'fillTransaction') as Record<string, unknown>
767
- }
768
-
769
- /** @internal */
770
- function normalizeFillTransactionRequest(value: Record<string, unknown>) {
771
- if (typeof value.to !== 'undefined' || typeof value.data !== 'undefined') return value
772
- if (!Array.isArray(value.calls) || value.calls.length !== 1) return value
773
- const [call] = value.calls as Array<Record<string, unknown>>
774
- const { calls: _, ...rest } = value
775
- return {
776
- ...rest,
777
- ...(typeof call?.data !== 'undefined' ? { data: call.data } : {}),
778
- ...(typeof call?.to !== 'undefined' ? { to: call.to } : {}),
779
- ...(typeof call?.value !== 'undefined' ? { value: normalizeFillValue(call.value) } : {}),
780
- }
781
- }
782
-
783
- /** @internal */
784
- function normalizeFillValue(value: unknown) {
785
- if (typeof value !== 'string' || !value.startsWith('0x')) return value
786
- return BigInt(value === '0x' ? '0x0' : value)
787
- }
788
-
789
- /** @internal */
790
- function normalizeTempoTransaction(value: Record<string, unknown> | undefined) {
791
- if (!value) throw new Error('Expected `tx` in eth_fillTransaction response.')
792
- return core_Transaction.fromRpc({ type: '0x76', ...value } as core_Transaction.Rpc)!
793
- }
794
-
795
- /** @internal */
796
- function rpcError(request: RpcRequest.RpcRequest, error: unknown) {
797
- if (error instanceof RpcResponse.InvalidParamsError)
798
- return Response.json(RpcResponse.from({ error }, { request }))
799
-
800
- if (error instanceof RpcResponse.MethodNotSupportedError)
801
- return Response.json(RpcResponse.from({ error }, { request }))
802
-
803
- if ((error as { name?: string | undefined }).name === 'ZodError')
804
- return Response.json(
805
- RpcResponse.from(
806
- {
807
- error: new RpcResponse.InvalidParamsError({
808
- message: (error as Error).message,
809
- }),
810
- },
811
- { request },
812
- ),
813
- )
814
-
815
- return Response.json(
816
- RpcResponse.from(
817
- {
818
- error: new RpcResponse.InternalError({
819
- message: (error as Error).message,
820
- }),
821
- },
822
- { request },
823
- ),
824
- )
825
- }
826
-
827
- /** @internal */
828
- function rpcResult(request: RpcRequest.RpcRequest, result: unknown) {
829
- return Response.json(RpcResponse.from({ result }, { request }))
830
- }
831
-
832
- /** @internal */
833
- function toSerializedTransaction(value: unknown) {
834
- if (typeof value === 'string') return value
835
- if (value && typeof value === 'object' && 'raw' in value && typeof value.raw === 'string')
836
- return value.raw
837
- throw new Error('Expected a serialized transaction result.')
838
- }
839
-
840
- /** @internal */
841
- async function mergeResponse(
842
- json: Record<string, unknown>,
843
- hook?: Response | void,
844
- ): Promise<Response> {
845
- if (!hook) return Response.json(json)
846
- const extra = (await hook.json().catch(() => ({}))) as Record<string, unknown>
847
- const headers = new Headers(hook.headers)
848
- headers.set('content-type', 'application/json')
849
- return new Response(JSON.stringify({ ...json, ...extra }), {
850
- headers,
851
- status: hook.status,
852
- })
853
- }
854
-
855
- /** @internal */
856
96
  function normalizeHeaders(headers?: Headers | Record<string, string>): Headers {
857
97
  if (!headers) return new Headers()
858
98
  if (headers instanceof Headers) return headers
859
99
  return new Headers(headers)
860
100
  }
861
101
 
862
- /** @internal */
863
102
  function corsToHeaders(cors?: boolean | from.Cors): Headers {
864
103
  if (cors === false) return new Headers()
865
104