accounts 0.3.0 → 0.4.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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/core/AccessKey.d.ts +55 -0
- package/dist/core/AccessKey.d.ts.map +1 -0
- package/dist/core/AccessKey.js +69 -0
- package/dist/core/AccessKey.js.map +1 -0
- package/dist/core/Account.d.ts +91 -0
- package/dist/core/Account.d.ts.map +1 -0
- package/dist/core/Account.js +64 -0
- package/dist/core/Account.js.map +1 -0
- package/dist/core/Adapter.d.ts +187 -0
- package/dist/core/Adapter.d.ts.map +1 -0
- package/dist/core/Adapter.js +7 -0
- package/dist/core/Adapter.js.map +1 -0
- package/dist/core/Ceremony.d.ts +109 -0
- package/dist/core/Ceremony.d.ts.map +1 -0
- package/dist/core/Ceremony.js +104 -0
- package/dist/core/Ceremony.js.map +1 -0
- package/dist/core/Client.d.ts +16 -0
- package/dist/core/Client.d.ts.map +1 -0
- package/dist/core/Client.js +18 -0
- package/dist/core/Client.js.map +1 -0
- package/dist/core/Dialog.d.ts +52 -0
- package/dist/core/Dialog.d.ts.map +1 -0
- package/dist/core/Dialog.js +342 -0
- package/dist/core/Dialog.js.map +1 -0
- package/dist/core/Expiry.d.ts +15 -0
- package/dist/core/Expiry.d.ts.map +1 -0
- package/dist/core/Expiry.js +29 -0
- package/dist/core/Expiry.js.map +1 -0
- package/dist/core/Messenger.d.ts +86 -0
- package/dist/core/Messenger.d.ts.map +1 -0
- package/dist/core/Messenger.js +127 -0
- package/dist/core/Messenger.js.map +1 -0
- package/dist/core/Provider.d.ts +69 -0
- package/dist/core/Provider.d.ts.map +1 -0
- package/dist/core/Provider.js +401 -0
- package/dist/core/Provider.js.map +1 -0
- package/dist/core/Remote.d.ts +114 -0
- package/dist/core/Remote.d.ts.map +1 -0
- package/dist/core/Remote.js +116 -0
- package/dist/core/Remote.js.map +1 -0
- package/dist/core/Schema.d.ts +805 -0
- package/dist/core/Schema.d.ts.map +1 -0
- package/dist/core/Schema.js +43 -0
- package/dist/core/Schema.js.map +1 -0
- package/dist/core/Storage.d.ts +42 -0
- package/dist/core/Storage.d.ts.map +1 -0
- package/dist/core/Storage.js +173 -0
- package/dist/core/Storage.js.map +1 -0
- package/dist/core/Store.d.ts +58 -0
- package/dist/core/Store.d.ts.map +1 -0
- package/dist/core/Store.js +58 -0
- package/dist/core/Store.js.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.js +39 -0
- package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
- package/dist/core/adapters/dialog.d.ts +31 -0
- package/dist/core/adapters/dialog.d.ts.map +1 -0
- package/dist/core/adapters/dialog.js +306 -0
- package/dist/core/adapters/dialog.js.map +1 -0
- package/dist/core/adapters/local.d.ts +33 -0
- package/dist/core/adapters/local.d.ts.map +1 -0
- package/dist/core/adapters/local.js +227 -0
- package/dist/core/adapters/local.js.map +1 -0
- package/dist/core/adapters/webAuthn.d.ts +36 -0
- package/dist/core/adapters/webAuthn.d.ts.map +1 -0
- package/dist/core/adapters/webAuthn.js +93 -0
- package/dist/core/adapters/webAuthn.js.map +1 -0
- package/dist/core/internal/withDedupe.d.ts +12 -0
- package/dist/core/internal/withDedupe.d.ts.map +1 -0
- package/dist/core/internal/withDedupe.js +12 -0
- package/dist/core/internal/withDedupe.js.map +1 -0
- package/dist/core/zod/request.d.ts +31 -0
- package/dist/core/zod/request.d.ts.map +1 -0
- package/dist/core/zod/request.js +41 -0
- package/dist/core/zod/request.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +603 -0
- package/dist/core/zod/rpc.d.ts.map +1 -0
- package/dist/core/zod/rpc.js +293 -0
- package/dist/core/zod/rpc.js.map +1 -0
- package/dist/core/zod/utils.d.ts +18 -0
- package/dist/core/zod/utils.d.ts.map +1 -0
- package/dist/core/zod/utils.js +21 -0
- package/dist/core/zod/utils.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/types.d.ts +284 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/server/Handler.d.ts +257 -0
- package/dist/server/Handler.d.ts.map +1 -0
- package/dist/server/Handler.js +433 -0
- package/dist/server/Handler.js.map +1 -0
- package/dist/server/Kv.d.ts +16 -0
- package/dist/server/Kv.d.ts.map +1 -0
- package/dist/server/Kv.js +30 -0
- package/dist/server/Kv.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/internal/requestListener.d.ts +124 -0
- package/dist/server/internal/requestListener.d.ts.map +1 -0
- package/dist/server/internal/requestListener.js +173 -0
- package/dist/server/internal/requestListener.js.map +1 -0
- package/dist/wagmi/Connector.d.ts +93 -0
- package/dist/wagmi/Connector.d.ts.map +1 -0
- package/dist/wagmi/Connector.js +238 -0
- package/dist/wagmi/Connector.js.map +1 -0
- package/dist/wagmi/index.d.ts +3 -0
- package/dist/wagmi/index.d.ts.map +1 -0
- package/dist/wagmi/index.js +3 -0
- package/dist/wagmi/index.js.map +1 -0
- package/package.json +56 -2
- package/src/core/AccessKey.test.ts +257 -0
- package/src/core/AccessKey.ts +123 -0
- package/src/core/Account.test.ts +309 -0
- package/src/core/Account.ts +152 -0
- package/src/core/Adapter.ts +238 -0
- package/src/core/Ceremony.browser.test.ts +239 -0
- package/src/core/Ceremony.test.ts +151 -0
- package/src/core/Ceremony.ts +203 -0
- package/src/core/Client.ts +36 -0
- package/src/core/Dialog.browser.test.ts +309 -0
- package/src/core/Dialog.test-d.ts +19 -0
- package/src/core/Dialog.ts +442 -0
- package/src/core/Expiry.ts +34 -0
- package/src/core/Messenger.ts +206 -0
- package/src/core/Provider.browser.test.ts +774 -0
- package/src/core/Provider.connect.browser.test.ts +415 -0
- package/src/core/Provider.test-d.ts +53 -0
- package/src/core/Provider.test.ts +1566 -0
- package/src/core/Provider.ts +559 -0
- package/src/core/Remote.ts +262 -0
- package/src/core/Schema.test-d.ts +211 -0
- package/src/core/Schema.ts +143 -0
- package/src/core/Storage.ts +213 -0
- package/src/core/Store.test.ts +287 -0
- package/src/core/Store.ts +129 -0
- package/src/core/adapters/dangerous_secp256k1.ts +53 -0
- package/src/core/adapters/dialog.ts +379 -0
- package/src/core/adapters/local.test.ts +97 -0
- package/src/core/adapters/local.ts +277 -0
- package/src/core/adapters/webAuthn.ts +129 -0
- package/src/core/internal/withDedupe.test.ts +116 -0
- package/src/core/internal/withDedupe.ts +20 -0
- package/src/core/mppx.test.ts +83 -0
- package/src/core/zod/request.test.ts +121 -0
- package/src/core/zod/request.ts +70 -0
- package/src/core/zod/rpc.ts +374 -0
- package/src/core/zod/utils.test.ts +69 -0
- package/src/core/zod/utils.ts +40 -0
- package/src/index.ts +14 -0
- package/src/internal/types.ts +378 -0
- package/src/server/Handler.test.ts +1014 -0
- package/src/server/Handler.ts +605 -0
- package/src/server/Kv.ts +46 -0
- package/src/server/index.ts +2 -0
- package/src/server/internal/requestListener.ts +273 -0
- package/src/tsconfig.json +9 -0
- package/src/wagmi/Connector.ts +287 -0
- package/src/wagmi/index.ts +2 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRouter,
|
|
3
|
+
type Middleware,
|
|
4
|
+
type Router,
|
|
5
|
+
type RouterOptions,
|
|
6
|
+
} from '@remix-run/fetch-router'
|
|
7
|
+
import { Base64, Bytes, Hex, RpcRequest, RpcResponse } from 'ox'
|
|
8
|
+
import { Credential } from 'ox/webauthn'
|
|
9
|
+
import { type Chain, type Client, createClient, http, type Transport } from 'viem'
|
|
10
|
+
import type { LocalAccount } from 'viem/accounts'
|
|
11
|
+
import { signTransaction } from 'viem/actions'
|
|
12
|
+
import { tempo, tempoModerato } from 'viem/chains'
|
|
13
|
+
import { Transaction } from 'viem/tempo'
|
|
14
|
+
import {
|
|
15
|
+
Authentication,
|
|
16
|
+
Registration,
|
|
17
|
+
type Registration as Registration_Types,
|
|
18
|
+
} from 'webauthx/server'
|
|
19
|
+
|
|
20
|
+
import * as RequestListener from './internal/requestListener.js'
|
|
21
|
+
import type { Kv } from './Kv.js'
|
|
22
|
+
|
|
23
|
+
export type Handler = Omit<Router, 'fetch'> & {
|
|
24
|
+
fetch: (input: string | URL | Request, ...args: any[]) => Promise<Response>
|
|
25
|
+
listener: (req: any, res: any) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function compose(handlers: Handler[], options: compose.Options = {}): Handler {
|
|
29
|
+
const path = options.path ?? '/'
|
|
30
|
+
|
|
31
|
+
return from({
|
|
32
|
+
...options,
|
|
33
|
+
async defaultHandler(context) {
|
|
34
|
+
const url = new URL(context.request.url)
|
|
35
|
+
if (!url.pathname.startsWith(path)) return new Response('Not Found', { status: 404 })
|
|
36
|
+
|
|
37
|
+
url.pathname = url.pathname.replace(path, '')
|
|
38
|
+
for (const handler of handlers) {
|
|
39
|
+
const request = new Request(url, context.request.clone() as RequestInit)
|
|
40
|
+
const response = await handler.fetch(request)
|
|
41
|
+
if (response.status !== 404) return response
|
|
42
|
+
}
|
|
43
|
+
return new Response('Not Found', { status: 404 })
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare namespace compose {
|
|
49
|
+
export type Options = from.Options & {
|
|
50
|
+
/** The path to use for the handler. */
|
|
51
|
+
path?: string | undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Instantiates a new request handler.
|
|
57
|
+
*
|
|
58
|
+
* @param options - constructor options
|
|
59
|
+
* @returns Handler instance
|
|
60
|
+
*/
|
|
61
|
+
export function from(options: from.Options = {}): Handler {
|
|
62
|
+
const corsHeaders = corsToHeaders(options.cors)
|
|
63
|
+
const mergedHeaders = new Headers(corsHeaders)
|
|
64
|
+
for (const [key, value] of normalizeHeaders(options.headers).entries())
|
|
65
|
+
mergedHeaders.set(key, value)
|
|
66
|
+
|
|
67
|
+
const router = createRouter({
|
|
68
|
+
...options,
|
|
69
|
+
middleware: [headers(mergedHeaders), preflight(mergedHeaders)],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...router,
|
|
74
|
+
listener: RequestListener.fromFetchHandler((request) => {
|
|
75
|
+
return router.fetch(request)
|
|
76
|
+
}),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare namespace from {
|
|
81
|
+
export type Options = RouterOptions & {
|
|
82
|
+
/**
|
|
83
|
+
* CORS configuration.
|
|
84
|
+
* - `true` (default): Allow all origins with default methods/headers
|
|
85
|
+
* - `false`: Disable CORS headers
|
|
86
|
+
* - Object: Custom CORS configuration
|
|
87
|
+
*/
|
|
88
|
+
cors?: boolean | Cors | undefined
|
|
89
|
+
/** Headers to add to the response. */
|
|
90
|
+
headers?: Headers | Record<string, string> | undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type Cors = {
|
|
94
|
+
/** Allowed origins. Defaults to `'*'`. */
|
|
95
|
+
origin?: string | string[] | undefined
|
|
96
|
+
/** Allowed methods. Defaults to `'GET, POST, PUT, DELETE, OPTIONS'`. */
|
|
97
|
+
methods?: string | undefined
|
|
98
|
+
/** Allowed headers. Defaults to `'Content-Type'`. */
|
|
99
|
+
headers?: string | undefined
|
|
100
|
+
/** Whether to allow credentials. */
|
|
101
|
+
credentials?: boolean | undefined
|
|
102
|
+
/** Headers to expose to the browser. */
|
|
103
|
+
exposeHeaders?: string | undefined
|
|
104
|
+
/** Max age for preflight cache in seconds. */
|
|
105
|
+
maxAge?: number | undefined
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Instantiates a fee payer service request handler that can be used to
|
|
111
|
+
* sponsor the fee for user transactions.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ### Cloudflare Worker
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
118
|
+
* import { Handler } from 'accounts/server'
|
|
119
|
+
*
|
|
120
|
+
* export default {
|
|
121
|
+
* fetch(request) {
|
|
122
|
+
* return Handler.feePayer({
|
|
123
|
+
* account: privateKeyToAccount('0x...'),
|
|
124
|
+
* }).fetch(request)
|
|
125
|
+
* }
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ### Next.js
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
134
|
+
* import { Handler } from 'accounts/server'
|
|
135
|
+
*
|
|
136
|
+
* const handler = Handler.feePayer({
|
|
137
|
+
* account: privateKeyToAccount('0x...'),
|
|
138
|
+
* })
|
|
139
|
+
*
|
|
140
|
+
* export GET = handler.fetch
|
|
141
|
+
* export POST = handler.fetch
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ### Hono
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
149
|
+
* import { Handler } from 'accounts/server'
|
|
150
|
+
*
|
|
151
|
+
* const handler = Handler.feePayer({
|
|
152
|
+
* account: privateKeyToAccount('0x...'),
|
|
153
|
+
* })
|
|
154
|
+
*
|
|
155
|
+
* const app = new Hono()
|
|
156
|
+
* app.all('*', handler)
|
|
157
|
+
*
|
|
158
|
+
* export default app
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ### Node.js
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
166
|
+
* import { Handler } from 'accounts/server'
|
|
167
|
+
*
|
|
168
|
+
* const handler = Handler.feePayer({
|
|
169
|
+
* account: privateKeyToAccount('0x...'),
|
|
170
|
+
* })
|
|
171
|
+
*
|
|
172
|
+
* const server = createServer(handler.listener)
|
|
173
|
+
* server.listen(3000)
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ### Bun
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
181
|
+
* import { Handler } from 'accounts/server'
|
|
182
|
+
*
|
|
183
|
+
* const handler = Handler.feePayer({
|
|
184
|
+
* account: privateKeyToAccount('0x...'),
|
|
185
|
+
* })
|
|
186
|
+
*
|
|
187
|
+
* Bun.serve(handler)
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ### Deno
|
|
192
|
+
*
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
195
|
+
* import { Handler } from 'accounts/server'
|
|
196
|
+
*
|
|
197
|
+
* const handler = Handler.feePayer({
|
|
198
|
+
* account: privateKeyToAccount('0x...'),
|
|
199
|
+
* })
|
|
200
|
+
*
|
|
201
|
+
* Deno.serve(handler)
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ### Express
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
209
|
+
* import { Handler } from 'accounts/server'
|
|
210
|
+
*
|
|
211
|
+
* const handler = Handler.feePayer({
|
|
212
|
+
* account: privateKeyToAccount('0x...'),
|
|
213
|
+
* })
|
|
214
|
+
*
|
|
215
|
+
* const app = express()
|
|
216
|
+
* app.use(handler.listener)
|
|
217
|
+
* app.listen(3000)
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ### Custom chains & transports
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* import { http } from 'viem'
|
|
225
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
226
|
+
* import { tempo, tempoModerato } from 'viem/chains'
|
|
227
|
+
* import { Handler } from 'accounts/server'
|
|
228
|
+
*
|
|
229
|
+
* const handler = Handler.feePayer({
|
|
230
|
+
* account: privateKeyToAccount('0x...'),
|
|
231
|
+
* chains: [tempo, tempoModerato],
|
|
232
|
+
* transports: {
|
|
233
|
+
* [tempo.id]: http('https://rpc.tempo.xyz'),
|
|
234
|
+
* [tempoModerato.id]: http('https://rpc.moderato.tempo.xyz'),
|
|
235
|
+
* },
|
|
236
|
+
* })
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* @param options - Options.
|
|
240
|
+
* @returns Request handler.
|
|
241
|
+
*/
|
|
242
|
+
export function feePayer(options: feePayer.Options) {
|
|
243
|
+
const {
|
|
244
|
+
account,
|
|
245
|
+
chains = [tempo, tempoModerato],
|
|
246
|
+
onRequest,
|
|
247
|
+
path = '/',
|
|
248
|
+
transports = {},
|
|
249
|
+
} = options
|
|
250
|
+
|
|
251
|
+
const clients = new Map<number, Client>()
|
|
252
|
+
for (const chain of chains) {
|
|
253
|
+
const transport = transports[chain.id] ?? http()
|
|
254
|
+
clients.set(chain.id, createClient({ chain, transport }))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getClient(chainId?: number): Client {
|
|
258
|
+
if (chainId) {
|
|
259
|
+
const client = clients.get(chainId)
|
|
260
|
+
if (!client) throw new Error(`Chain ${chainId} not configured`)
|
|
261
|
+
return client
|
|
262
|
+
}
|
|
263
|
+
return clients.get(chains[0]!.id)!
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const router = from(options)
|
|
267
|
+
|
|
268
|
+
router.post(path, async ({ request: req }) => {
|
|
269
|
+
const request = RpcRequest.from((await req.json()) as any)
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await onRequest?.(request)
|
|
273
|
+
|
|
274
|
+
const method = request.method as string
|
|
275
|
+
if (
|
|
276
|
+
method !== 'eth_signRawTransaction' &&
|
|
277
|
+
method !== 'eth_sendRawTransaction' &&
|
|
278
|
+
method !== 'eth_sendRawTransactionSync'
|
|
279
|
+
)
|
|
280
|
+
return Response.json(
|
|
281
|
+
RpcResponse.from(
|
|
282
|
+
{
|
|
283
|
+
error: new RpcResponse.MethodNotSupportedError({
|
|
284
|
+
message: `Method not supported: ${request.method}`,
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
{ request },
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
const serialized = request.params?.[0] as `0x76${string}`
|
|
292
|
+
|
|
293
|
+
if (!serialized?.startsWith('0x76') && !serialized?.startsWith('0x78'))
|
|
294
|
+
throw new RpcResponse.InvalidParamsError({
|
|
295
|
+
message: 'Only Tempo (0x76/0x78) transactions are supported.',
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const transaction = Transaction.deserialize(serialized) as any
|
|
299
|
+
|
|
300
|
+
if (!transaction.signature || !transaction.from)
|
|
301
|
+
throw new RpcResponse.InvalidParamsError({
|
|
302
|
+
message: 'Transaction must be signed by the sender before fee payer signing.',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const client = getClient(transaction.chainId)
|
|
306
|
+
const serializedTransaction = await signTransaction(client, {
|
|
307
|
+
...transaction,
|
|
308
|
+
account,
|
|
309
|
+
feePayer: account,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
if (method === 'eth_signRawTransaction')
|
|
313
|
+
return Response.json(RpcResponse.from({ result: serializedTransaction }, { request }))
|
|
314
|
+
|
|
315
|
+
const result = await (client as any).request({
|
|
316
|
+
method,
|
|
317
|
+
params: [serializedTransaction],
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return Response.json(RpcResponse.from({ result }, { request }))
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return Response.json(
|
|
323
|
+
RpcResponse.from(
|
|
324
|
+
{
|
|
325
|
+
error: new RpcResponse.InternalError({
|
|
326
|
+
message: (error as Error).message,
|
|
327
|
+
}),
|
|
328
|
+
},
|
|
329
|
+
{ request },
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
return router
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export declare namespace feePayer {
|
|
339
|
+
export type Options = from.Options & {
|
|
340
|
+
/** Account to use as the fee payer. */
|
|
341
|
+
account: LocalAccount
|
|
342
|
+
/**
|
|
343
|
+
* Supported chains. The handler resolves the client based on the
|
|
344
|
+
* `chainId` in the incoming transaction.
|
|
345
|
+
* @default [tempo, tempoModerato]
|
|
346
|
+
*/
|
|
347
|
+
chains?: readonly [Chain, ...Chain[]] | undefined
|
|
348
|
+
/** Function to call before handling the request. */
|
|
349
|
+
onRequest?: (request: RpcRequest.RpcRequest) => Promise<void>
|
|
350
|
+
/** Path to use for the handler. */
|
|
351
|
+
path?: string | undefined
|
|
352
|
+
/** Transports keyed by chain ID. Defaults to `http()` for each chain. */
|
|
353
|
+
transports?: Record<number, Transport> | undefined
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Instantiates a WebAuthn ceremony handler that manages registration and
|
|
359
|
+
* authentication flows server-side.
|
|
360
|
+
*
|
|
361
|
+
* Exposes 4 POST endpoints following the webauthx convention:
|
|
362
|
+
* - `POST /register/options` — generate credential creation options
|
|
363
|
+
* - `POST /register` — verify registration and store credential
|
|
364
|
+
* - `POST /login/options` — generate credential request options
|
|
365
|
+
* - `POST /login` — verify authentication
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* import { Handler, Kv } from 'accounts/server'
|
|
370
|
+
*
|
|
371
|
+
* const handler = Handler.webauthn({
|
|
372
|
+
* kv: Kv.memory(),
|
|
373
|
+
* origin: 'https://example.com',
|
|
374
|
+
* rpId: 'example.com',
|
|
375
|
+
* })
|
|
376
|
+
*
|
|
377
|
+
* export default handler
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
380
|
+
* @param options - Options.
|
|
381
|
+
* @returns Request handler.
|
|
382
|
+
*/
|
|
383
|
+
export function webauthn(options: webauthn.Options): Handler {
|
|
384
|
+
const { challengeTtl = 300, kv, onAuthenticate, onRegister, path = '', rpId, ...rest } = options
|
|
385
|
+
const origin = options.origin as string | string[]
|
|
386
|
+
|
|
387
|
+
const router = from(rest)
|
|
388
|
+
|
|
389
|
+
router.post(`${path}/register/options`, async ({ request: req }) => {
|
|
390
|
+
try {
|
|
391
|
+
const body = await req.json()
|
|
392
|
+
const { excludeCredentialIds, name, userId } = body as {
|
|
393
|
+
excludeCredentialIds?: string[]
|
|
394
|
+
name: string
|
|
395
|
+
userId?: string
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const { challenge, options } = Registration.getOptions({
|
|
399
|
+
excludeCredentialIds,
|
|
400
|
+
name,
|
|
401
|
+
rp: { id: rpId, name: rpId },
|
|
402
|
+
...(userId ? { user: { id: new TextEncoder().encode(userId), name } } : undefined),
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await kv.set(`challenge:${challenge}`, Date.now())
|
|
406
|
+
|
|
407
|
+
return Response.json({ options })
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
router.post(`${path}/register`, async ({ request: req }) => {
|
|
414
|
+
try {
|
|
415
|
+
const credential = (await req.json()) as Registration_Types.Credential
|
|
416
|
+
const deserialized = Credential.deserialize(credential)
|
|
417
|
+
|
|
418
|
+
const clientData = JSON.parse(
|
|
419
|
+
Bytes.toString(new Uint8Array(deserialized.clientDataJSON)),
|
|
420
|
+
) as { challenge: string }
|
|
421
|
+
const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
|
|
422
|
+
const stored = await kv.get<number>(`challenge:${challenge}`)
|
|
423
|
+
if (!stored || Date.now() - stored > challengeTtl * 1_000)
|
|
424
|
+
throw new Error('Missing or expired challenge')
|
|
425
|
+
await kv.delete(`challenge:${challenge}`)
|
|
426
|
+
|
|
427
|
+
const result = Registration.verify(credential, {
|
|
428
|
+
challenge,
|
|
429
|
+
origin,
|
|
430
|
+
rpId,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const { publicKey } = result.credential
|
|
434
|
+
const credentialId = credential.id
|
|
435
|
+
|
|
436
|
+
await kv.set(`credential:${credentialId}`, { publicKey })
|
|
437
|
+
|
|
438
|
+
const json = { credentialId, publicKey }
|
|
439
|
+
const hook = await onRegister?.({ credentialId, publicKey, request: req })
|
|
440
|
+
return mergeResponse(json, hook)
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
router.post(`${path}/login/options`, async ({ request: req }) => {
|
|
447
|
+
try {
|
|
448
|
+
const body = await req.json()
|
|
449
|
+
const {
|
|
450
|
+
allowCredentialIds,
|
|
451
|
+
challenge: requestChallenge,
|
|
452
|
+
credentialId,
|
|
453
|
+
mediation,
|
|
454
|
+
} = body as {
|
|
455
|
+
allowCredentialIds?: string[]
|
|
456
|
+
challenge?: Hex.Hex
|
|
457
|
+
credentialId?: string
|
|
458
|
+
mediation?: string
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { challenge, options: authOptions } = Authentication.getOptions({
|
|
462
|
+
challenge: requestChallenge,
|
|
463
|
+
credentialId: allowCredentialIds ?? credentialId,
|
|
464
|
+
rpId,
|
|
465
|
+
})
|
|
466
|
+
const options = mediation ? { ...authOptions, mediation } : authOptions
|
|
467
|
+
|
|
468
|
+
await kv.set(`challenge:${challenge}`, Date.now())
|
|
469
|
+
|
|
470
|
+
return Response.json({ options })
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
router.post(`${path}/login`, async ({ request: req }) => {
|
|
477
|
+
try {
|
|
478
|
+
const response = (await req.json()) as Authentication.Response
|
|
479
|
+
|
|
480
|
+
const clientData = JSON.parse(response.metadata.clientDataJSON) as {
|
|
481
|
+
challenge: string
|
|
482
|
+
}
|
|
483
|
+
const challenge = Hex.fromBytes(Base64.toBytes(clientData.challenge))
|
|
484
|
+
const stored = await kv.get<number>(`challenge:${challenge}`)
|
|
485
|
+
if (!stored || Date.now() - stored > challengeTtl * 1_000)
|
|
486
|
+
throw new Error('Missing or expired challenge')
|
|
487
|
+
await kv.delete(`challenge:${challenge}`)
|
|
488
|
+
|
|
489
|
+
const credentialData = await kv.get<{ publicKey: string }>(`credential:${response.id}`)
|
|
490
|
+
if (!credentialData) throw new Error('Unknown credential')
|
|
491
|
+
|
|
492
|
+
const valid = Authentication.verify(response, {
|
|
493
|
+
challenge,
|
|
494
|
+
origin,
|
|
495
|
+
publicKey: credentialData.publicKey as `0x${string}`,
|
|
496
|
+
rpId,
|
|
497
|
+
})
|
|
498
|
+
if (!valid) throw new Error('Authentication failed')
|
|
499
|
+
|
|
500
|
+
const rawResponse = response.raw?.response as unknown as Record<string, string> | undefined
|
|
501
|
+
const userHandle = rawResponse?.userHandle
|
|
502
|
+
|
|
503
|
+
const json = {
|
|
504
|
+
credentialId: response.id,
|
|
505
|
+
publicKey: credentialData.publicKey,
|
|
506
|
+
...(userHandle && userHandle.length > 0 ? { userId: userHandle } : undefined),
|
|
507
|
+
}
|
|
508
|
+
const hook = await onAuthenticate?.({ ...json, request: req })
|
|
509
|
+
return mergeResponse(json, hook)
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
return router
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export declare namespace webauthn {
|
|
519
|
+
type Options = from.Options & {
|
|
520
|
+
/** Maximum age of a challenge in seconds before it expires. @default 300 */
|
|
521
|
+
challengeTtl?: number | undefined
|
|
522
|
+
/** Key-value store for challenges and credentials. */
|
|
523
|
+
kv: Kv
|
|
524
|
+
/** Called after a successful registration. The returned response is merged onto the default JSON response. */
|
|
525
|
+
onRegister?: (parameters: {
|
|
526
|
+
credentialId: string
|
|
527
|
+
publicKey: string
|
|
528
|
+
request: Request
|
|
529
|
+
}) => Response | Promise<Response> | void | Promise<void>
|
|
530
|
+
/** Called after a successful authentication. The returned response is merged onto the default JSON response. */
|
|
531
|
+
onAuthenticate?: (parameters: {
|
|
532
|
+
credentialId: string
|
|
533
|
+
publicKey: string
|
|
534
|
+
userId?: string | undefined
|
|
535
|
+
request: Request
|
|
536
|
+
}) => Response | Promise<Response> | void | Promise<void>
|
|
537
|
+
/** Expected origin(s) (e.g. `"https://example.com"` or `["https://a.com", "https://b.com"]`). */
|
|
538
|
+
origin: string | readonly string[]
|
|
539
|
+
/** Path prefix for the WebAuthn endpoints (e.g. `"/webauthn"`). @default "" */
|
|
540
|
+
path?: string | undefined
|
|
541
|
+
/** Relying Party ID (e.g. `"example.com"`). */
|
|
542
|
+
rpId: string
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** @internal */
|
|
547
|
+
async function mergeResponse(
|
|
548
|
+
json: Record<string, unknown>,
|
|
549
|
+
hook?: Response | void,
|
|
550
|
+
): Promise<Response> {
|
|
551
|
+
if (!hook) return Response.json(json)
|
|
552
|
+
const extra = (await hook.json().catch(() => ({}))) as Record<string, unknown>
|
|
553
|
+
const headers = new Headers(hook.headers)
|
|
554
|
+
headers.set('content-type', 'application/json')
|
|
555
|
+
return new Response(JSON.stringify({ ...json, ...extra }), {
|
|
556
|
+
headers,
|
|
557
|
+
status: hook.status,
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** @internal */
|
|
562
|
+
function normalizeHeaders(headers?: Headers | Record<string, string>): Headers {
|
|
563
|
+
if (!headers) return new Headers()
|
|
564
|
+
if (headers instanceof Headers) return headers
|
|
565
|
+
return new Headers(headers)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @internal */
|
|
569
|
+
function corsToHeaders(cors?: boolean | from.Cors): Headers {
|
|
570
|
+
if (cors === false) return new Headers()
|
|
571
|
+
|
|
572
|
+
const config = cors === true || cors === undefined ? {} : cors
|
|
573
|
+
|
|
574
|
+
const headers = new Headers()
|
|
575
|
+
const origin = Array.isArray(config.origin) ? config.origin.join(', ') : (config.origin ?? '*')
|
|
576
|
+
headers.set('Access-Control-Allow-Origin', origin)
|
|
577
|
+
headers.set('Access-Control-Allow-Methods', config.methods ?? 'GET, POST, PUT, DELETE, OPTIONS')
|
|
578
|
+
headers.set('Access-Control-Allow-Headers', config.headers ?? 'Content-Type')
|
|
579
|
+
if (config.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
580
|
+
if (config.exposeHeaders) headers.set('Access-Control-Expose-Headers', config.exposeHeaders)
|
|
581
|
+
if (config.maxAge !== undefined) headers.set('Access-Control-Max-Age', String(config.maxAge))
|
|
582
|
+
|
|
583
|
+
return headers
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** @internal */
|
|
587
|
+
function headers(headers: Headers): Middleware {
|
|
588
|
+
return async (_, next) => {
|
|
589
|
+
const response = await next()
|
|
590
|
+
const responseHeaders = new Headers(response.headers)
|
|
591
|
+
for (const [key, value] of headers.entries()) responseHeaders.set(key, value)
|
|
592
|
+
return new Response(response.body, {
|
|
593
|
+
headers: responseHeaders,
|
|
594
|
+
status: response.status,
|
|
595
|
+
statusText: response.statusText,
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** @internal */
|
|
601
|
+
function preflight(headers: Headers): Middleware {
|
|
602
|
+
return async (context) => {
|
|
603
|
+
if (context.request.method === 'OPTIONS') return new Response(null, { headers })
|
|
604
|
+
}
|
|
605
|
+
}
|
package/src/server/Kv.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Json } from 'ox'
|
|
2
|
+
|
|
3
|
+
export type Kv = {
|
|
4
|
+
get: <value = unknown>(key: string) => Promise<value>
|
|
5
|
+
set: (key: string, value: unknown) => Promise<void>
|
|
6
|
+
delete: (key: string) => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function from<kv extends Kv>(kv: kv): kv {
|
|
10
|
+
return kv
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cloudflare(kv: cloudflare.Parameters): Kv {
|
|
14
|
+
return from({
|
|
15
|
+
delete: kv.delete.bind(kv),
|
|
16
|
+
async get(key) {
|
|
17
|
+
return kv.get(key, 'json')
|
|
18
|
+
},
|
|
19
|
+
async set(key, value) {
|
|
20
|
+
return kv.put(key, Json.stringify(value))
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare namespace cloudflare {
|
|
26
|
+
export type Parameters = {
|
|
27
|
+
get: <value = unknown>(key: string, format: 'json') => Promise<value>
|
|
28
|
+
put: (key: string, value: string) => Promise<void>
|
|
29
|
+
delete: (key: string) => Promise<void>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function memory(): Kv {
|
|
34
|
+
const store = new Map<string, unknown>()
|
|
35
|
+
return from({
|
|
36
|
+
async delete(key) {
|
|
37
|
+
Promise.resolve(store.delete(key))
|
|
38
|
+
},
|
|
39
|
+
async get(key) {
|
|
40
|
+
return store.get(key) as any
|
|
41
|
+
},
|
|
42
|
+
async set(key, value) {
|
|
43
|
+
store.set(key, value)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
}
|