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.
- package/CHANGELOG.md +8 -0
- package/dist/core/Schema.d.ts +12 -12
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +3 -1
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +9 -9
- package/dist/core/zod/rpc.js +1 -1
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/CliAuth.d.ts +11 -11
- package/dist/server/CliAuth.js +1 -1
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Handler.d.ts +4 -252
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +4 -573
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/internal/handlers/codeAuth.d.ts +41 -0
- package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -0
- package/dist/server/internal/handlers/codeAuth.js +104 -0
- package/dist/server/internal/handlers/codeAuth.js.map +1 -0
- package/dist/server/internal/handlers/feePayer.d.ts +73 -0
- package/dist/server/internal/handlers/feePayer.d.ts.map +1 -0
- package/dist/server/internal/handlers/feePayer.js +184 -0
- package/dist/server/internal/handlers/feePayer.js.map +1 -0
- package/dist/server/internal/handlers/relay.d.ts +148 -0
- package/dist/server/internal/handlers/relay.d.ts.map +1 -0
- package/dist/server/internal/handlers/relay.js +600 -0
- package/dist/server/internal/handlers/relay.js.map +1 -0
- package/dist/server/internal/handlers/utils.d.ts +12 -0
- package/dist/server/internal/handlers/utils.d.ts.map +1 -0
- package/dist/server/internal/handlers/utils.js +80 -0
- package/dist/server/internal/handlers/utils.js.map +1 -0
- package/dist/server/internal/handlers/webAuthn.d.ts +57 -0
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -0
- package/dist/server/internal/handlers/webAuthn.js +143 -0
- package/dist/server/internal/handlers/webAuthn.js.map +1 -0
- package/package.json +2 -2
- package/src/core/Provider.connect.browser.test.ts +23 -2
- package/src/core/adapters/dialog.ts +6 -1
- package/src/core/zod/rpc.ts +1 -1
- package/src/server/CliAuth.ts +1 -1
- package/src/server/Handler.test.ts +3 -418
- package/src/server/Handler.ts +5 -766
- package/src/server/internal/handlers/codeAuth.ts +148 -0
- package/src/server/internal/handlers/feePayer.test.ts +335 -0
- package/src/server/internal/handlers/feePayer.ts +271 -0
- package/src/server/internal/handlers/relay.test.ts +767 -0
- package/src/server/internal/handlers/relay.ts +817 -0
- package/src/server/internal/handlers/utils.ts +96 -0
- package/src/server/internal/handlers/webAuthn.test.ts +170 -0
- package/src/server/internal/handlers/webAuthn.ts +213 -0
package/src/server/Handler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|