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.
Files changed (168) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/dist/core/AccessKey.d.ts +55 -0
  5. package/dist/core/AccessKey.d.ts.map +1 -0
  6. package/dist/core/AccessKey.js +69 -0
  7. package/dist/core/AccessKey.js.map +1 -0
  8. package/dist/core/Account.d.ts +91 -0
  9. package/dist/core/Account.d.ts.map +1 -0
  10. package/dist/core/Account.js +64 -0
  11. package/dist/core/Account.js.map +1 -0
  12. package/dist/core/Adapter.d.ts +187 -0
  13. package/dist/core/Adapter.d.ts.map +1 -0
  14. package/dist/core/Adapter.js +7 -0
  15. package/dist/core/Adapter.js.map +1 -0
  16. package/dist/core/Ceremony.d.ts +109 -0
  17. package/dist/core/Ceremony.d.ts.map +1 -0
  18. package/dist/core/Ceremony.js +104 -0
  19. package/dist/core/Ceremony.js.map +1 -0
  20. package/dist/core/Client.d.ts +16 -0
  21. package/dist/core/Client.d.ts.map +1 -0
  22. package/dist/core/Client.js +18 -0
  23. package/dist/core/Client.js.map +1 -0
  24. package/dist/core/Dialog.d.ts +52 -0
  25. package/dist/core/Dialog.d.ts.map +1 -0
  26. package/dist/core/Dialog.js +342 -0
  27. package/dist/core/Dialog.js.map +1 -0
  28. package/dist/core/Expiry.d.ts +15 -0
  29. package/dist/core/Expiry.d.ts.map +1 -0
  30. package/dist/core/Expiry.js +29 -0
  31. package/dist/core/Expiry.js.map +1 -0
  32. package/dist/core/Messenger.d.ts +86 -0
  33. package/dist/core/Messenger.d.ts.map +1 -0
  34. package/dist/core/Messenger.js +127 -0
  35. package/dist/core/Messenger.js.map +1 -0
  36. package/dist/core/Provider.d.ts +69 -0
  37. package/dist/core/Provider.d.ts.map +1 -0
  38. package/dist/core/Provider.js +401 -0
  39. package/dist/core/Provider.js.map +1 -0
  40. package/dist/core/Remote.d.ts +114 -0
  41. package/dist/core/Remote.d.ts.map +1 -0
  42. package/dist/core/Remote.js +116 -0
  43. package/dist/core/Remote.js.map +1 -0
  44. package/dist/core/Schema.d.ts +805 -0
  45. package/dist/core/Schema.d.ts.map +1 -0
  46. package/dist/core/Schema.js +43 -0
  47. package/dist/core/Schema.js.map +1 -0
  48. package/dist/core/Storage.d.ts +42 -0
  49. package/dist/core/Storage.d.ts.map +1 -0
  50. package/dist/core/Storage.js +173 -0
  51. package/dist/core/Storage.js.map +1 -0
  52. package/dist/core/Store.d.ts +58 -0
  53. package/dist/core/Store.d.ts.map +1 -0
  54. package/dist/core/Store.js +58 -0
  55. package/dist/core/Store.js.map +1 -0
  56. package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
  57. package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
  58. package/dist/core/adapters/dangerous_secp256k1.js +39 -0
  59. package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
  60. package/dist/core/adapters/dialog.d.ts +31 -0
  61. package/dist/core/adapters/dialog.d.ts.map +1 -0
  62. package/dist/core/adapters/dialog.js +306 -0
  63. package/dist/core/adapters/dialog.js.map +1 -0
  64. package/dist/core/adapters/local.d.ts +33 -0
  65. package/dist/core/adapters/local.d.ts.map +1 -0
  66. package/dist/core/adapters/local.js +227 -0
  67. package/dist/core/adapters/local.js.map +1 -0
  68. package/dist/core/adapters/webAuthn.d.ts +36 -0
  69. package/dist/core/adapters/webAuthn.d.ts.map +1 -0
  70. package/dist/core/adapters/webAuthn.js +93 -0
  71. package/dist/core/adapters/webAuthn.js.map +1 -0
  72. package/dist/core/internal/withDedupe.d.ts +12 -0
  73. package/dist/core/internal/withDedupe.d.ts.map +1 -0
  74. package/dist/core/internal/withDedupe.js +12 -0
  75. package/dist/core/internal/withDedupe.js.map +1 -0
  76. package/dist/core/zod/request.d.ts +31 -0
  77. package/dist/core/zod/request.d.ts.map +1 -0
  78. package/dist/core/zod/request.js +41 -0
  79. package/dist/core/zod/request.js.map +1 -0
  80. package/dist/core/zod/rpc.d.ts +603 -0
  81. package/dist/core/zod/rpc.d.ts.map +1 -0
  82. package/dist/core/zod/rpc.js +293 -0
  83. package/dist/core/zod/rpc.js.map +1 -0
  84. package/dist/core/zod/utils.d.ts +18 -0
  85. package/dist/core/zod/utils.d.ts.map +1 -0
  86. package/dist/core/zod/utils.js +21 -0
  87. package/dist/core/zod/utils.js.map +1 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +15 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/internal/types.d.ts +284 -0
  93. package/dist/internal/types.d.ts.map +1 -0
  94. package/dist/internal/types.js +2 -0
  95. package/dist/internal/types.js.map +1 -0
  96. package/dist/server/Handler.d.ts +257 -0
  97. package/dist/server/Handler.d.ts.map +1 -0
  98. package/dist/server/Handler.js +433 -0
  99. package/dist/server/Handler.js.map +1 -0
  100. package/dist/server/Kv.d.ts +16 -0
  101. package/dist/server/Kv.d.ts.map +1 -0
  102. package/dist/server/Kv.js +30 -0
  103. package/dist/server/Kv.js.map +1 -0
  104. package/dist/server/index.d.ts +3 -0
  105. package/dist/server/index.d.ts.map +1 -0
  106. package/dist/server/index.js +3 -0
  107. package/dist/server/index.js.map +1 -0
  108. package/dist/server/internal/requestListener.d.ts +124 -0
  109. package/dist/server/internal/requestListener.d.ts.map +1 -0
  110. package/dist/server/internal/requestListener.js +173 -0
  111. package/dist/server/internal/requestListener.js.map +1 -0
  112. package/dist/wagmi/Connector.d.ts +93 -0
  113. package/dist/wagmi/Connector.d.ts.map +1 -0
  114. package/dist/wagmi/Connector.js +238 -0
  115. package/dist/wagmi/Connector.js.map +1 -0
  116. package/dist/wagmi/index.d.ts +3 -0
  117. package/dist/wagmi/index.d.ts.map +1 -0
  118. package/dist/wagmi/index.js +3 -0
  119. package/dist/wagmi/index.js.map +1 -0
  120. package/package.json +56 -2
  121. package/src/core/AccessKey.test.ts +257 -0
  122. package/src/core/AccessKey.ts +123 -0
  123. package/src/core/Account.test.ts +309 -0
  124. package/src/core/Account.ts +152 -0
  125. package/src/core/Adapter.ts +238 -0
  126. package/src/core/Ceremony.browser.test.ts +239 -0
  127. package/src/core/Ceremony.test.ts +151 -0
  128. package/src/core/Ceremony.ts +203 -0
  129. package/src/core/Client.ts +36 -0
  130. package/src/core/Dialog.browser.test.ts +309 -0
  131. package/src/core/Dialog.test-d.ts +19 -0
  132. package/src/core/Dialog.ts +442 -0
  133. package/src/core/Expiry.ts +34 -0
  134. package/src/core/Messenger.ts +206 -0
  135. package/src/core/Provider.browser.test.ts +774 -0
  136. package/src/core/Provider.connect.browser.test.ts +415 -0
  137. package/src/core/Provider.test-d.ts +53 -0
  138. package/src/core/Provider.test.ts +1566 -0
  139. package/src/core/Provider.ts +559 -0
  140. package/src/core/Remote.ts +262 -0
  141. package/src/core/Schema.test-d.ts +211 -0
  142. package/src/core/Schema.ts +143 -0
  143. package/src/core/Storage.ts +213 -0
  144. package/src/core/Store.test.ts +287 -0
  145. package/src/core/Store.ts +129 -0
  146. package/src/core/adapters/dangerous_secp256k1.ts +53 -0
  147. package/src/core/adapters/dialog.ts +379 -0
  148. package/src/core/adapters/local.test.ts +97 -0
  149. package/src/core/adapters/local.ts +277 -0
  150. package/src/core/adapters/webAuthn.ts +129 -0
  151. package/src/core/internal/withDedupe.test.ts +116 -0
  152. package/src/core/internal/withDedupe.ts +20 -0
  153. package/src/core/mppx.test.ts +83 -0
  154. package/src/core/zod/request.test.ts +121 -0
  155. package/src/core/zod/request.ts +70 -0
  156. package/src/core/zod/rpc.ts +374 -0
  157. package/src/core/zod/utils.test.ts +69 -0
  158. package/src/core/zod/utils.ts +40 -0
  159. package/src/index.ts +14 -0
  160. package/src/internal/types.ts +378 -0
  161. package/src/server/Handler.test.ts +1014 -0
  162. package/src/server/Handler.ts +605 -0
  163. package/src/server/Kv.ts +46 -0
  164. package/src/server/index.ts +2 -0
  165. package/src/server/internal/requestListener.ts +273 -0
  166. package/src/tsconfig.json +9 -0
  167. package/src/wagmi/Connector.ts +287 -0
  168. package/src/wagmi/index.ts +2 -0
@@ -0,0 +1,1014 @@
1
+ import { Elysia } from 'elysia'
2
+ import express from 'express'
3
+ import { Hono } from 'hono'
4
+ import type { RpcRequest } from 'ox'
5
+ import { sendTransactionSync } from 'viem/actions'
6
+ import { withFeePayer } from 'viem/tempo'
7
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
8
+
9
+ import { accounts, chain, getClient, http } from '../../test/config.js'
10
+ import { createServer, type Server } from '../../test/utils.js'
11
+ import * as Ceremony from '../core/Ceremony.js'
12
+ import * as Handler from './Handler.js'
13
+ import * as Kv from './Kv.js'
14
+
15
+ describe('from', () => {
16
+ describe('cors', () => {
17
+ test('default: adds CORS headers', async () => {
18
+ const handler = Handler.from()
19
+ handler.get('/test', () => new Response('test'))
20
+
21
+ const response = await handler.fetch(new Request('http://localhost/test'))
22
+
23
+ expect(response.status).toBe(200)
24
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
25
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe(
26
+ 'GET, POST, PUT, DELETE, OPTIONS',
27
+ )
28
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type')
29
+ })
30
+
31
+ test('behavior: cors = false disables CORS headers', async () => {
32
+ const handler = Handler.from({ cors: false })
33
+ handler.get('/test', () => new Response('test'))
34
+
35
+ const response = await handler.fetch(new Request('http://localhost/test'))
36
+
37
+ expect(response.status).toBe(200)
38
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull()
39
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBeNull()
40
+ })
41
+
42
+ test('behavior: custom cors config', async () => {
43
+ const handler = Handler.from({
44
+ cors: {
45
+ origin: 'https://example.com',
46
+ methods: 'GET, POST',
47
+ headers: 'Content-Type, Authorization',
48
+ credentials: true,
49
+ maxAge: 86400,
50
+ },
51
+ })
52
+ handler.get('/test', () => new Response('test'))
53
+
54
+ const response = await handler.fetch(new Request('http://localhost/test'))
55
+
56
+ expect(response.status).toBe(200)
57
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com')
58
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST')
59
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe(
60
+ 'Content-Type, Authorization',
61
+ )
62
+ expect(response.headers.get('Access-Control-Allow-Credentials')).toBe('true')
63
+ expect(response.headers.get('Access-Control-Max-Age')).toBe('86400')
64
+ })
65
+
66
+ test('behavior: cors with array of origins', async () => {
67
+ const handler = Handler.from({
68
+ cors: {
69
+ origin: ['https://example.com', 'https://other.com'],
70
+ },
71
+ })
72
+ handler.get('/test', () => new Response('test'))
73
+
74
+ const response = await handler.fetch(new Request('http://localhost/test'))
75
+
76
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe(
77
+ 'https://example.com, https://other.com',
78
+ )
79
+ })
80
+
81
+ test('behavior: OPTIONS preflight with default CORS', async () => {
82
+ const handler = Handler.from()
83
+ handler.get('/test', () => new Response('test'))
84
+
85
+ const response = await handler.fetch(
86
+ new Request('http://localhost/test', { method: 'OPTIONS' }),
87
+ )
88
+
89
+ expect(response.status).toBe(200)
90
+ expect(await response.text()).toBe('')
91
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
92
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe(
93
+ 'GET, POST, PUT, DELETE, OPTIONS',
94
+ )
95
+ })
96
+
97
+ test('behavior: custom headers override CORS headers', async () => {
98
+ const handler = Handler.from({
99
+ cors: { origin: 'https://default.com' },
100
+ headers: { 'Access-Control-Allow-Origin': 'https://override.com' },
101
+ })
102
+ handler.get('/test', () => new Response('test'))
103
+
104
+ const response = await handler.fetch(new Request('http://localhost/test'))
105
+
106
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://override.com')
107
+ })
108
+ })
109
+ })
110
+
111
+ describe('compose', () => {
112
+ test('default', async () => {
113
+ const handler1 = Handler.from()
114
+ handler1.get('/test', () => new Response('test'))
115
+ const handler2 = Handler.from()
116
+ handler2.get('/test2', () => new Response('test2'))
117
+
118
+ const handler = Handler.compose([handler1, handler2])
119
+ expect(handler).toBeDefined()
120
+
121
+ {
122
+ const response = await handler.fetch(new Request('http://localhost/test'))
123
+ expect(response.status).toBe(200)
124
+ expect(await response.text()).toBe('test')
125
+ }
126
+
127
+ {
128
+ const response = await handler.fetch(new Request('http://localhost/test2'))
129
+ expect(response.status).toBe(200)
130
+ expect(await response.text()).toBe('test2')
131
+ }
132
+ })
133
+
134
+ test('behavior: path', async () => {
135
+ const handler1 = Handler.from()
136
+ handler1.get('/test', () => new Response('test'))
137
+ const handler2 = Handler.from()
138
+ handler2.get('/test2', () => new Response('test2'))
139
+
140
+ const handler = Handler.compose([handler1, handler2], {
141
+ path: '/api',
142
+ })
143
+ expect(handler).toBeDefined()
144
+
145
+ {
146
+ const response = await handler.fetch(new Request('http://localhost/api/test'))
147
+ expect(response.status).toBe(200)
148
+ expect(await response.text()).toBe('test')
149
+ }
150
+
151
+ {
152
+ const response = await handler.fetch(new Request('http://localhost/api/test2'))
153
+ expect(response.status).toBe(200)
154
+ expect(await response.text()).toBe('test2')
155
+ }
156
+ })
157
+
158
+ test('behavior: headers', async () => {
159
+ const handler1 = Handler.from()
160
+ handler1.get('/test', () => new Response('test'))
161
+ const handler2 = Handler.from()
162
+ handler2.get('/test2', () => new Response('test2'))
163
+
164
+ const headers = new Headers({
165
+ 'Access-Control-Allow-Origin': '*',
166
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
167
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
168
+ })
169
+
170
+ const handler = Handler.compose([handler1, handler2], {
171
+ headers,
172
+ })
173
+
174
+ {
175
+ const response = await handler.fetch(new Request('http://localhost/test'))
176
+ expect(response.status).toBe(200)
177
+ expect(await response.text()).toBe('test')
178
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
179
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
180
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe(
181
+ 'Content-Type, Authorization',
182
+ )
183
+ }
184
+
185
+ {
186
+ const response = await handler.fetch(new Request('http://localhost/test2'))
187
+ expect(response.status).toBe(200)
188
+ expect(await response.text()).toBe('test2')
189
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
190
+ }
191
+ })
192
+
193
+ test('behavior: headers + path', async () => {
194
+ const handler1 = Handler.from()
195
+ handler1.get('/test', () => new Response('test'))
196
+ const handler2 = Handler.from()
197
+ handler2.get('/test2', () => new Response('test2'))
198
+
199
+ const headers = new Headers({
200
+ 'Access-Control-Allow-Origin': '*',
201
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
202
+ })
203
+
204
+ const handler = Handler.compose([handler1, handler2], {
205
+ headers,
206
+ path: '/api',
207
+ })
208
+
209
+ {
210
+ const response = await handler.fetch(new Request('http://localhost/api/test'))
211
+ expect(response.status).toBe(200)
212
+ expect(await response.text()).toBe('test')
213
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
214
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
215
+ }
216
+
217
+ {
218
+ const response = await handler.fetch(new Request('http://localhost/api/test2'))
219
+ expect(response.status).toBe(200)
220
+ expect(await response.text()).toBe('test2')
221
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
222
+ }
223
+ })
224
+
225
+ test('behavior: headers + OPTIONS', async () => {
226
+ const handler1 = Handler.from()
227
+ handler1.get('/test', () => new Response('test'))
228
+
229
+ const headers = new Headers({
230
+ 'Access-Control-Allow-Origin': '*',
231
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
232
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
233
+ 'Access-Control-Max-Age': '86400',
234
+ })
235
+
236
+ const handler = Handler.compose([handler1], {
237
+ headers,
238
+ })
239
+
240
+ const response = await handler.fetch(
241
+ new Request('http://localhost/test', {
242
+ method: 'OPTIONS',
243
+ }),
244
+ )
245
+
246
+ expect(response.status).toBe(200)
247
+ expect(await response.text()).toBe('')
248
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
249
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
250
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type, Authorization')
251
+ expect(response.headers.get('Access-Control-Max-Age')).toBe('86400')
252
+ })
253
+
254
+ test('behavior: headers + 404', async () => {
255
+ const handler1 = Handler.from()
256
+ handler1.get('/test', () => new Response('test'))
257
+
258
+ const headers = new Headers({
259
+ 'Access-Control-Allow-Origin': '*',
260
+ })
261
+
262
+ const handler = Handler.compose([handler1], {
263
+ headers,
264
+ })
265
+
266
+ const response = await handler.fetch(new Request('http://localhost/nonexistent'))
267
+
268
+ expect(response.status).toBe(404)
269
+ expect(await response.text()).toBe('Not Found')
270
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
271
+ })
272
+
273
+ test('behavior: headers propagation from child handlers', async () => {
274
+ const handler1 = Handler.from()
275
+ handler1.get('/test', () => {
276
+ const response = new Response('test')
277
+ response.headers.set('X-Custom-Header', 'custom-value')
278
+ return response
279
+ })
280
+
281
+ const headers = new Headers({
282
+ 'Access-Control-Allow-Origin': '*',
283
+ })
284
+
285
+ const handler = Handler.compose([handler1], {
286
+ headers,
287
+ })
288
+
289
+ const response = await handler.fetch(new Request('http://localhost/test'))
290
+
291
+ expect(response.status).toBe(200)
292
+ expect(await response.text()).toBe('test')
293
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
294
+ expect(response.headers.get('X-Custom-Header')).toBe('custom-value')
295
+ })
296
+
297
+ test('behavior: headers with child handler headers', async () => {
298
+ const childHeaders = new Headers({
299
+ 'X-Child-Header': 'child-value',
300
+ })
301
+ const handler1 = Handler.from({ headers: childHeaders })
302
+ handler1.get('/test', () => new Response('test'))
303
+
304
+ const parentHeaders = new Headers({
305
+ 'Access-Control-Allow-Origin': '*',
306
+ 'X-Parent-Header': 'parent-value',
307
+ })
308
+
309
+ const handler = Handler.compose([handler1], {
310
+ headers: parentHeaders,
311
+ })
312
+
313
+ const response = await handler.fetch(new Request('http://localhost/test'))
314
+
315
+ expect(response.status).toBe(200)
316
+ expect(await response.text()).toBe('test')
317
+ // Both parent and child headers should be present
318
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
319
+ expect(response.headers.get('X-Parent-Header')).toBe('parent-value')
320
+ expect(response.headers.get('X-Child-Header')).toBe('child-value')
321
+ })
322
+
323
+ test('behavior: headers as object', async () => {
324
+ const handler1 = Handler.from()
325
+ handler1.get('/test', () => new Response('test'))
326
+
327
+ const handler = Handler.compose([handler1], {
328
+ headers: {
329
+ 'Access-Control-Allow-Origin': '*',
330
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
331
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
332
+ },
333
+ })
334
+
335
+ const response = await handler.fetch(new Request('http://localhost/test'))
336
+ expect(response.status).toBe(200)
337
+ expect(await response.text()).toBe('test')
338
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
339
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
340
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type, Authorization')
341
+ })
342
+
343
+ describe('integration', () => {
344
+ const handler1 = Handler.from()
345
+ handler1.get('/foo', () => new Response('foo'))
346
+ handler1.post('/bar', () => new Response('bar'))
347
+
348
+ const handler2 = Handler.from()
349
+ handler2.get('/baz', () => new Response('baz'))
350
+ handler2.post('/qux', () => new Response('qux'))
351
+
352
+ const handler = Handler.compose([handler1, handler2], {
353
+ path: '/api',
354
+ })
355
+
356
+ test('hono', async () => {
357
+ const app = new Hono()
358
+ app.use((c) => handler.fetch(c.req.raw))
359
+
360
+ {
361
+ const response = await app.request('/api/foo')
362
+ expect(await response.text()).toBe('foo')
363
+ }
364
+
365
+ {
366
+ const response = await app.request('/api/bar', {
367
+ method: 'POST',
368
+ })
369
+ expect(await response.text()).toBe('bar')
370
+ }
371
+
372
+ {
373
+ const response = await app.request('/api/baz', {
374
+ method: 'GET',
375
+ })
376
+ expect(await response.text()).toBe('baz')
377
+ }
378
+
379
+ {
380
+ const response = await app.request('/api/qux', {
381
+ method: 'POST',
382
+ })
383
+ expect(await response.text()).toBe('qux')
384
+ }
385
+ })
386
+
387
+ test('elysia', async () => {
388
+ const app = new Elysia().all('*', ({ request }) => handler.fetch(request))
389
+
390
+ {
391
+ const response = await app.handle(new Request('http://localhost/api/foo'))
392
+ expect(await response.text()).toBe('foo')
393
+ }
394
+
395
+ {
396
+ const response = await app.handle(
397
+ new Request('http://localhost/api/bar', {
398
+ method: 'POST',
399
+ }),
400
+ )
401
+ expect(await response.text()).toBe('bar')
402
+ }
403
+
404
+ {
405
+ const response = await app.handle(
406
+ new Request('http://localhost/api/baz', {
407
+ method: 'GET',
408
+ }),
409
+ )
410
+ expect(await response.text()).toBe('baz')
411
+ }
412
+
413
+ {
414
+ const response = await app.handle(
415
+ new Request('http://localhost/api/qux', {
416
+ method: 'POST',
417
+ }),
418
+ )
419
+ expect(await response.text()).toBe('qux')
420
+ }
421
+ })
422
+
423
+ test('node.js', async () => {
424
+ const server = await createServer(handler.listener)
425
+
426
+ {
427
+ const response = await fetch(`${server.url}/api/foo`)
428
+ expect(await response.text()).toBe('foo')
429
+ }
430
+
431
+ {
432
+ const response = await fetch(`${server.url}/api/bar`, {
433
+ method: 'POST',
434
+ })
435
+ expect(await response.text()).toBe('bar')
436
+ }
437
+
438
+ {
439
+ const response = await fetch(`${server.url}/api/baz`, {
440
+ method: 'GET',
441
+ })
442
+ expect(await response.text()).toBe('baz')
443
+ }
444
+
445
+ {
446
+ const response = await fetch(`${server.url}/api/qux`, {
447
+ method: 'POST',
448
+ })
449
+ expect(await response.text()).toBe('qux')
450
+ }
451
+
452
+ await server.closeAsync()
453
+ })
454
+
455
+ test('express', async () => {
456
+ const app = express()
457
+ app.use(handler.listener)
458
+
459
+ const server = await createServer(app)
460
+
461
+ {
462
+ const response = await fetch(`${server.url}/api/foo`)
463
+ expect(await response.text()).toBe('foo')
464
+ }
465
+
466
+ {
467
+ const response = await fetch(`${server.url}/api/bar`, {
468
+ method: 'POST',
469
+ })
470
+ expect(await response.text()).toBe('bar')
471
+ }
472
+
473
+ {
474
+ const response = await fetch(`${server.url}/api/baz`, {
475
+ method: 'GET',
476
+ })
477
+ expect(await response.text()).toBe('baz')
478
+ }
479
+
480
+ {
481
+ const response = await fetch(`${server.url}/api/qux`, {
482
+ method: 'POST',
483
+ })
484
+ expect(await response.text()).toBe('qux')
485
+ }
486
+
487
+ await server.closeAsync()
488
+ })
489
+ })
490
+ })
491
+
492
+ describe('from', () => {
493
+ test('default', () => {
494
+ const handler = Handler.from()
495
+ expect(handler).toBeDefined()
496
+ })
497
+
498
+ test('.fetch', async () => {
499
+ const handler = Handler.from()
500
+ handler.get('/test', () => new Response('test'))
501
+
502
+ const response = await handler.fetch(new Request('http://localhost/test'))
503
+ expect(response.status).toBe(200)
504
+ expect(await response.text()).toBe('test')
505
+ })
506
+
507
+ test('.listener', async () => {
508
+ const handler = Handler.from()
509
+ handler.get('/test', () => Response.json({ message: 'hello from listener' }))
510
+
511
+ const server = await createServer(handler.listener)
512
+
513
+ // Make a request to the server
514
+ const response = await fetch(`${server.url}/test`)
515
+ expect(response.status).toBe(200)
516
+
517
+ const data = await response.json()
518
+ expect(data).toEqual({ message: 'hello from listener' })
519
+ })
520
+
521
+ test('behavior: headers', async () => {
522
+ const headers = new Headers({
523
+ 'Access-Control-Allow-Origin': '*',
524
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
525
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
526
+ })
527
+
528
+ const handler = Handler.from({ headers })
529
+ handler.get('/test', () => new Response('test'))
530
+
531
+ const response = await handler.fetch(new Request('http://localhost/test'))
532
+ expect(response.status).toBe(200)
533
+ expect(await response.text()).toBe('test')
534
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
535
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
536
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type, Authorization')
537
+ })
538
+
539
+ test('behavior: headers + OPTIONS', async () => {
540
+ const headers = new Headers({
541
+ 'Access-Control-Allow-Origin': '*',
542
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
543
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
544
+ 'Access-Control-Max-Age': '86400',
545
+ })
546
+
547
+ const handler = Handler.from({ headers })
548
+ handler.get('/test', () => new Response('test'))
549
+
550
+ const response = await handler.fetch(
551
+ new Request('http://localhost/test', {
552
+ method: 'OPTIONS',
553
+ }),
554
+ )
555
+
556
+ expect(response.status).toBe(200)
557
+ expect(await response.text()).toBe('')
558
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
559
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
560
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type, Authorization')
561
+ expect(response.headers.get('Access-Control-Max-Age')).toBe('86400')
562
+ })
563
+
564
+ test('behavior: headers + 404', async () => {
565
+ const headers = new Headers({
566
+ 'Access-Control-Allow-Origin': '*',
567
+ })
568
+
569
+ const handler = Handler.from({ headers })
570
+ handler.get('/test', () => new Response('test'))
571
+
572
+ const response = await handler.fetch(new Request('http://localhost/nonexistent'))
573
+
574
+ expect(response.status).toBe(404)
575
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
576
+ })
577
+
578
+ test('behavior: headers propagation from routes', async () => {
579
+ const headers = new Headers({
580
+ 'Access-Control-Allow-Origin': '*',
581
+ })
582
+
583
+ const handler = Handler.from({ headers })
584
+ handler.get('/test', () => {
585
+ const response = new Response('test')
586
+ response.headers.set('X-Custom-Header', 'custom-value')
587
+ response.headers.set('Content-Type', 'text/plain')
588
+ return response
589
+ })
590
+
591
+ const response = await handler.fetch(new Request('http://localhost/test'))
592
+
593
+ expect(response.status).toBe(200)
594
+ expect(await response.text()).toBe('test')
595
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
596
+ expect(response.headers.get('X-Custom-Header')).toBe('custom-value')
597
+ expect(response.headers.get('Content-Type')).toBe('text/plain')
598
+ })
599
+
600
+ test('behavior: headers as object', async () => {
601
+ const handler = Handler.from({
602
+ headers: {
603
+ 'Access-Control-Allow-Origin': '*',
604
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
605
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
606
+ },
607
+ })
608
+ handler.get('/test', () => new Response('test'))
609
+
610
+ const response = await handler.fetch(new Request('http://localhost/test'))
611
+ expect(response.status).toBe(200)
612
+ expect(await response.text()).toBe('test')
613
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
614
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('POST, OPTIONS')
615
+ expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type, Authorization')
616
+ })
617
+
618
+ describe('integration', () => {
619
+ const handler = Handler.from()
620
+ handler.get('/foo', () => new Response('foo'))
621
+ handler.post('/bar', () => new Response('bar'))
622
+
623
+ test('hono', async () => {
624
+ const app = new Hono()
625
+ app.use((c) => handler.fetch(c.req.raw))
626
+
627
+ {
628
+ const response = await app.request('/foo')
629
+ expect(await response.text()).toBe('foo')
630
+ }
631
+
632
+ {
633
+ const response = await app.request('/bar', {
634
+ method: 'POST',
635
+ })
636
+ expect(await response.text()).toBe('bar')
637
+ }
638
+ })
639
+
640
+ test('elysia', async () => {
641
+ const app = new Elysia().all('*', ({ request }) => handler.fetch(request))
642
+
643
+ {
644
+ const response = await app.handle(new Request('http://localhost/foo'))
645
+ expect(await response.text()).toBe('foo')
646
+ }
647
+
648
+ {
649
+ const response = await app.handle(
650
+ new Request('http://localhost/bar', {
651
+ method: 'POST',
652
+ }),
653
+ )
654
+ expect(await response.text()).toBe('bar')
655
+ }
656
+ })
657
+
658
+ test('node.js', async () => {
659
+ const server = await createServer(handler.listener)
660
+
661
+ {
662
+ const response = await fetch(`${server.url}/foo`)
663
+ expect(await response.text()).toBe('foo')
664
+ }
665
+
666
+ {
667
+ const response = await fetch(`${server.url}/bar`, {
668
+ method: 'POST',
669
+ })
670
+ expect(await response.text()).toBe('bar')
671
+ }
672
+
673
+ await server.closeAsync()
674
+ })
675
+
676
+ test('express', async () => {
677
+ const app = express()
678
+ app.use(handler.listener)
679
+
680
+ const server = await createServer(app)
681
+
682
+ {
683
+ const response = await fetch(`${server.url}/foo`)
684
+ expect(await response.text()).toBe('foo')
685
+ }
686
+
687
+ {
688
+ const response = await fetch(`${server.url}/bar`, {
689
+ method: 'POST',
690
+ })
691
+ expect(await response.text()).toBe('bar')
692
+ }
693
+
694
+ await server.closeAsync()
695
+ })
696
+ })
697
+ })
698
+
699
+ describe('feePayer', () => {
700
+ const userAccount = accounts[9]!
701
+ const feePayerAccount = accounts[0]!
702
+
703
+ let server: Server
704
+ let requests: RpcRequest.RpcRequest[] = []
705
+
706
+ beforeAll(async () => {
707
+ server = await createServer(
708
+ Handler.feePayer({
709
+ account: feePayerAccount,
710
+ chains: [chain],
711
+ transports: { [chain.id]: http() },
712
+ onRequest: async (request) => {
713
+ requests.push(request)
714
+ },
715
+ }).listener,
716
+ )
717
+ })
718
+
719
+ afterAll(() => {
720
+ server.close()
721
+ process.on('SIGINT', () => {
722
+ server.close()
723
+ process.exit(0)
724
+ })
725
+ process.on('SIGTERM', () => {
726
+ server.close()
727
+ process.exit(0)
728
+ })
729
+ })
730
+
731
+ afterEach(() => {
732
+ requests = []
733
+ })
734
+
735
+ describe('POST /', () => {
736
+ test('behavior: eth_signRawTransaction', async () => {
737
+ const client = getClient({
738
+ account: userAccount,
739
+ transport: withFeePayer(http(), http(server.url)),
740
+ })
741
+
742
+ const receipt = await sendTransactionSync(client, {
743
+ feePayer: true,
744
+ to: '0x0000000000000000000000000000000000000000',
745
+ })
746
+
747
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
748
+
749
+ expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
750
+ [
751
+ "eth_signRawTransaction",
752
+ ]
753
+ `)
754
+ })
755
+
756
+ test('behavior: eth_sendRawTransaction', async () => {
757
+ const client = getClient({
758
+ account: userAccount,
759
+ transport: withFeePayer(http(), http(server.url), {
760
+ policy: 'sign-and-broadcast',
761
+ }),
762
+ })
763
+
764
+ const receipt = await sendTransactionSync(client, {
765
+ feePayer: true,
766
+ to: '0x0000000000000000000000000000000000000000',
767
+ })
768
+
769
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
770
+
771
+ expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
772
+ [
773
+ "eth_sendRawTransactionSync",
774
+ ]
775
+ `)
776
+ })
777
+
778
+ test('behavior: eth_sendRawTransactionSync', async () => {
779
+ const client = getClient({
780
+ account: userAccount,
781
+ transport: withFeePayer(http(), http(server.url), {
782
+ policy: 'sign-and-broadcast',
783
+ }),
784
+ })
785
+
786
+ const receipt = await sendTransactionSync(client, {
787
+ feePayer: true,
788
+ to: '0x0000000000000000000000000000000000000000',
789
+ })
790
+
791
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
792
+
793
+ expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
794
+ [
795
+ "eth_sendRawTransactionSync",
796
+ ]
797
+ `)
798
+ })
799
+
800
+ test('behavior: unsupported method', async () => {
801
+ await expect(
802
+ fetch(server.url, {
803
+ method: 'POST',
804
+ body: JSON.stringify({
805
+ jsonrpc: '2.0',
806
+ id: 1,
807
+ method: 'eth_chainId',
808
+ }),
809
+ }).then((response) => response.json()),
810
+ ).resolves.toMatchInlineSnapshot(`
811
+ {
812
+ "error": {
813
+ "code": -32004,
814
+ "name": "RpcResponse.MethodNotSupportedError",
815
+ "stack": "",
816
+ },
817
+ "id": 1,
818
+ "jsonrpc": "2.0",
819
+ }
820
+ `)
821
+ })
822
+
823
+ test('behavior: internal error', async () => {
824
+ const response = await fetch(server.url, {
825
+ method: 'POST',
826
+ body: JSON.stringify({
827
+ jsonrpc: '2.0',
828
+ id: 1,
829
+ method: 'eth_signRawTransaction',
830
+ params: ['0xinvalid'],
831
+ }),
832
+ })
833
+
834
+ const data = await response.json()
835
+ expect(data).toMatchInlineSnapshot(`
836
+ {
837
+ "error": {
838
+ "code": -32603,
839
+ "name": "RpcResponse.InternalError",
840
+ "stack": "",
841
+ },
842
+ "id": 1,
843
+ "jsonrpc": "2.0",
844
+ }
845
+ `)
846
+ })
847
+ })
848
+ })
849
+
850
+ describe('webauthn', () => {
851
+ let server: Server
852
+ let ceremony: Ceremony.Ceremony
853
+
854
+ beforeAll(async () => {
855
+ server = await createServer(
856
+ Handler.webauthn({
857
+ kv: Kv.memory(),
858
+ origin: 'http://localhost',
859
+ rpId: 'localhost',
860
+ }).listener,
861
+ )
862
+ ceremony = Ceremony.server({ url: server.url })
863
+ })
864
+
865
+ afterAll(async () => {
866
+ await server.closeAsync()
867
+ })
868
+
869
+ describe('POST /register/options', () => {
870
+ test('default: returns registration options', async () => {
871
+ const { options } = await ceremony.getRegistrationOptions({ name: 'Test' })
872
+ expect(options.publicKey).toBeDefined()
873
+ expect(options.publicKey!.rp.id).toMatchInlineSnapshot(`"localhost"`)
874
+ expect(options.publicKey!.rp.name).toMatchInlineSnapshot(`"localhost"`)
875
+ expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
876
+ })
877
+
878
+ test('behavior: each call generates a unique challenge', async () => {
879
+ const { options: a } = await ceremony.getRegistrationOptions({ name: 'Test' })
880
+ const { options: b } = await ceremony.getRegistrationOptions({ name: 'Test' })
881
+ expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
882
+ })
883
+ })
884
+
885
+ describe('POST /login/options', () => {
886
+ test('default: returns authentication options', async () => {
887
+ const { options } = await ceremony.getAuthenticationOptions()
888
+ expect(options.publicKey).toBeDefined()
889
+ expect(options.publicKey!.rpId).toMatchInlineSnapshot(`"localhost"`)
890
+ expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
891
+ })
892
+
893
+ test('behavior: each call generates a unique challenge', async () => {
894
+ const { options: a } = await ceremony.getAuthenticationOptions()
895
+ const { options: b } = await ceremony.getAuthenticationOptions()
896
+ expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
897
+ })
898
+ })
899
+
900
+ describe('POST /register', () => {
901
+ test('error: invalid credential → 400', async () => {
902
+ const response = await fetch(`${server.url}/register`, {
903
+ method: 'POST',
904
+ headers: { 'Content-Type': 'application/json' },
905
+ body: JSON.stringify({ id: 'fake', clientDataJSON: 'bad', attestationObject: 'bad' }),
906
+ })
907
+ expect(response.status).toBe(400)
908
+ const body = await response.json()
909
+ expect(body.error).toBeTypeOf('string')
910
+ })
911
+ })
912
+
913
+ describe('POST /login', () => {
914
+ test('error: unknown credential → 400', async () => {
915
+ const response = await fetch(`${server.url}/login`, {
916
+ method: 'POST',
917
+ headers: { 'Content-Type': 'application/json' },
918
+ body: JSON.stringify({
919
+ id: 'unknown',
920
+ metadata: { authenticatorData: '0x00', clientDataJSON: '{"challenge":"0xdead"}' },
921
+ raw: {
922
+ id: 'unknown',
923
+ type: 'public-key',
924
+ authenticatorAttachment: null,
925
+ rawId: 'unknown',
926
+ response: { clientDataJSON: 'e30' },
927
+ },
928
+ signature: '0x00',
929
+ }),
930
+ })
931
+ expect(response.status).toBe(400)
932
+ const body = await response.json()
933
+ expect(body.error).toMatchInlineSnapshot(`"Missing or expired challenge"`)
934
+ })
935
+ })
936
+
937
+ describe('challenge replay', () => {
938
+ test('behavior: challenge consumed after register/options → re-fetching is required', async () => {
939
+ // Get options twice — each should have a unique challenge stored in KV
940
+ const { options: a } = await ceremony.getRegistrationOptions({ name: 'Replay' })
941
+ const { options: b } = await ceremony.getRegistrationOptions({ name: 'Replay' })
942
+ expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
943
+ })
944
+
945
+ test('behavior: challenge consumed after login/options → re-fetching is required', async () => {
946
+ const { options: a } = await ceremony.getAuthenticationOptions()
947
+ const { options: b } = await ceremony.getAuthenticationOptions()
948
+ expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
949
+ })
950
+ })
951
+
952
+ describe('hooks', () => {
953
+ test('behavior: onRegister error does not call hook', async () => {
954
+ let called = false
955
+ const hookServer = await createServer(
956
+ Handler.webauthn({
957
+ kv: Kv.memory(),
958
+ origin: 'http://localhost',
959
+ rpId: 'localhost',
960
+ onRegister() {
961
+ called = true
962
+ return Response.json({ extra: true })
963
+ },
964
+ }).listener,
965
+ )
966
+
967
+ const response = await fetch(`${hookServer.url}/register`, {
968
+ method: 'POST',
969
+ headers: { 'Content-Type': 'application/json' },
970
+ body: JSON.stringify({ id: 'fake', clientDataJSON: 'bad', attestationObject: 'bad' }),
971
+ })
972
+ expect(response.status).toBe(400)
973
+ expect(called).toBe(false)
974
+
975
+ await hookServer.closeAsync()
976
+ })
977
+
978
+ test('behavior: onAuthenticate error does not call hook', async () => {
979
+ let called = false
980
+ const hookServer = await createServer(
981
+ Handler.webauthn({
982
+ kv: Kv.memory(),
983
+ origin: 'http://localhost',
984
+ rpId: 'localhost',
985
+ onAuthenticate() {
986
+ called = true
987
+ return Response.json({ extra: true })
988
+ },
989
+ }).listener,
990
+ )
991
+
992
+ const response = await fetch(`${hookServer.url}/login`, {
993
+ method: 'POST',
994
+ headers: { 'Content-Type': 'application/json' },
995
+ body: JSON.stringify({
996
+ id: 'unknown',
997
+ metadata: { authenticatorData: '0x00', clientDataJSON: '{"challenge":"0xdead"}' },
998
+ raw: {
999
+ id: 'unknown',
1000
+ type: 'public-key',
1001
+ authenticatorAttachment: null,
1002
+ rawId: 'unknown',
1003
+ response: { clientDataJSON: 'e30' },
1004
+ },
1005
+ signature: '0x00',
1006
+ }),
1007
+ })
1008
+ expect(response.status).toBe(400)
1009
+ expect(called).toBe(false)
1010
+
1011
+ await hookServer.closeAsync()
1012
+ })
1013
+ })
1014
+ })