accounts 0.12.2 → 0.13.0
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 +23 -0
- package/README.md +6 -5
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +53 -67
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +89 -106
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +226 -210
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +0 -9
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +1 -13
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +10 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts +4 -5
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +66 -76
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +7 -7
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +114 -124
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +92 -105
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/privy.d.ts +164 -0
- package/dist/core/adapters/privy.d.ts.map +1 -0
- package/dist/core/adapters/privy.js +574 -0
- package/dist/core/adapters/privy.js.map +1 -0
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +136 -133
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
- package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
- package/dist/core/internal/AccessKeyTransaction.js +155 -0
- package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +9 -4
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +9 -4
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +61 -72
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/Kv.d.ts +19 -9
- package/dist/server/Kv.d.ts.map +1 -1
- package/dist/server/Kv.js +32 -9
- package/dist/server/Kv.js.map +1 -1
- package/dist/server/internal/handlers/auth.d.ts.map +1 -1
- package/dist/server/internal/handlers/auth.js +2 -0
- package/dist/server/internal/handlers/auth.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +18 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +115 -2
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +16 -9
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/adapter.ts +69 -74
- package/src/core/AccessKey.test.ts +525 -559
- package/src/core/AccessKey.ts +323 -327
- package/src/core/Account.test.ts +5 -50
- package/src/core/Account.ts +1 -19
- package/src/core/Adapter.ts +6 -2
- package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +97 -31
- package/src/core/Provider.ts +110 -110
- package/src/core/Schema.test-d.ts +18 -0
- package/src/core/adapters/dialog.test.ts +137 -13
- package/src/core/adapters/dialog.ts +109 -121
- package/src/core/adapters/local.ts +95 -119
- package/src/core/adapters/privy.test-d.ts +128 -0
- package/src/core/adapters/privy.test.ts +634 -0
- package/src/core/adapters/privy.ts +805 -0
- package/src/core/adapters/turnkey.test.ts +117 -13
- package/src/core/adapters/turnkey.ts +132 -149
- package/src/core/internal/AccessKeyTransaction.ts +243 -0
- package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
- package/src/core/zod/request.test.ts +36 -0
- package/src/core/zod/rpc.ts +17 -10
- package/src/index.ts +1 -1
- package/src/react-native/adapter.ts +77 -84
- package/src/server/Kv.test.ts +51 -0
- package/src/server/Kv.ts +43 -9
- package/src/server/internal/handlers/auth.test.ts +10 -5
- package/src/server/internal/handlers/auth.ts +3 -0
- package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +3 -0
- package/src/server/internal/handlers/relay.ts +138 -13
- package/src/server/internal/handlers/session.test.ts +1 -1
- package/src/server/internal/handlers/webAuthn.test.ts +20 -0
- package/src/server/internal/handlers/webAuthn.ts +24 -12
- /package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
package/src/server/Kv.test.ts
CHANGED
|
@@ -47,6 +47,36 @@ describe('memory', () => {
|
|
|
47
47
|
expect(await kv.get('a')).toMatchInlineSnapshot(`"v2"`)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('create: writes only when key is absent', async () => {
|
|
51
|
+
const kv = Kv.memory()
|
|
52
|
+
|
|
53
|
+
expect(await kv.create!('a', 'v1')).toMatchInlineSnapshot(`true`)
|
|
54
|
+
expect(await kv.create!('a', 'v2')).toMatchInlineSnapshot(`false`)
|
|
55
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('create: replaces expired entries', async () => {
|
|
59
|
+
let now = 1_000_000
|
|
60
|
+
const kv = Kv.memory({ now: () => now })
|
|
61
|
+
|
|
62
|
+
await kv.set('a', 'v1', { ttl: 1 })
|
|
63
|
+
now += 2_000
|
|
64
|
+
expect(await kv.create!('a', 'v2')).toMatchInlineSnapshot(`true`)
|
|
65
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v2"`)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('create: concurrent callers — only one writes', async () => {
|
|
69
|
+
const kv = Kv.memory()
|
|
70
|
+
|
|
71
|
+
const results = await Promise.all([kv.create!('a', 'v1'), kv.create!('a', 'v2')])
|
|
72
|
+
expect(results.filter(Boolean)).toMatchInlineSnapshot(`
|
|
73
|
+
[
|
|
74
|
+
true,
|
|
75
|
+
]
|
|
76
|
+
`)
|
|
77
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
78
|
+
})
|
|
79
|
+
|
|
50
80
|
test('take: returns the value and removes the entry', async () => {
|
|
51
81
|
const kv = Kv.memory()
|
|
52
82
|
|
|
@@ -163,6 +193,18 @@ describe('durableObject + NonceStorage', () => {
|
|
|
163
193
|
`)
|
|
164
194
|
})
|
|
165
195
|
|
|
196
|
+
test('create: concurrent callers — only one wins', async () => {
|
|
197
|
+
const kv = Kv.durableObject(fakeDurableObject())
|
|
198
|
+
|
|
199
|
+
const results = await Promise.all([kv.create!('a', 'v1'), kv.create!('a', 'v2')])
|
|
200
|
+
expect(results.filter(Boolean)).toMatchInlineSnapshot(`
|
|
201
|
+
[
|
|
202
|
+
true,
|
|
203
|
+
]
|
|
204
|
+
`)
|
|
205
|
+
expect(await kv.get('a')).toMatchInlineSnapshot(`"v1"`)
|
|
206
|
+
})
|
|
207
|
+
|
|
166
208
|
test('take: missing key returns undefined', async () => {
|
|
167
209
|
const kv = Kv.durableObject(fakeDurableObject())
|
|
168
210
|
expect(await kv.take!('missing')).toMatchInlineSnapshot(`undefined`)
|
|
@@ -226,6 +268,15 @@ describe('cloudflare', () => {
|
|
|
226
268
|
expect(kv.take).toBeUndefined()
|
|
227
269
|
})
|
|
228
270
|
|
|
271
|
+
test('create: NOT implemented (CF KV is not linearizable)', () => {
|
|
272
|
+
const kv = Kv.cloudflare({
|
|
273
|
+
get: async () => null,
|
|
274
|
+
put: async () => {},
|
|
275
|
+
delete: async () => {},
|
|
276
|
+
})
|
|
277
|
+
expect(kv.create).toBeUndefined()
|
|
278
|
+
})
|
|
279
|
+
|
|
229
280
|
test('ttl: passes expirationTtl seconds to underlying put', async () => {
|
|
230
281
|
const puts: { key: string; value: string; options: unknown }[] = []
|
|
231
282
|
const fakeKv = {
|
package/src/server/Kv.ts
CHANGED
|
@@ -16,6 +16,16 @@ export type Kv = {
|
|
|
16
16
|
set: (key: string, value: unknown, options?: set.Options | undefined) => Promise<void>
|
|
17
17
|
/** Delete a value by key. */
|
|
18
18
|
delete: (key: string) => Promise<void>
|
|
19
|
+
/**
|
|
20
|
+
* Atomic create-if-absent. Returns `true` when the value was written,
|
|
21
|
+
* `false` when a non-expired value already exists.
|
|
22
|
+
*
|
|
23
|
+
* Optional. Backends with a linearizable create primitive should
|
|
24
|
+
* implement this so consumers can atomically reject duplicates.
|
|
25
|
+
* Consumers may fall back to `get` then `set` when strict atomicity is
|
|
26
|
+
* not required.
|
|
27
|
+
*/
|
|
28
|
+
create?: (key: string, value: unknown, options?: set.Options | undefined) => Promise<boolean>
|
|
19
29
|
/**
|
|
20
30
|
* Atomic read-and-delete. Returns the value if present, `undefined` if
|
|
21
31
|
* missing or expired. Across concurrent callers, exactly one observer
|
|
@@ -51,11 +61,10 @@ export function from<kv extends Kv>(kv: kv): kv {
|
|
|
51
61
|
* Cloudflare KV's minimum TTL is 60 seconds; the platform enforces its own
|
|
52
62
|
* minimum independent of what's passed here.
|
|
53
63
|
*
|
|
54
|
-
* **Not safe for one-time-consume semantics.**
|
|
55
|
-
* consistent across data centers
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* for the SIWE challenge nonce store.
|
|
64
|
+
* **Not safe for one-time-consume or unique-key semantics.**
|
|
65
|
+
* Cloudflare KV is eventually consistent across data centers, so `take`
|
|
66
|
+
* and `create` are intentionally NOT implemented. Use a Durable Object
|
|
67
|
+
* (or another linearizable backend) for the SIWE challenge nonce store.
|
|
59
68
|
*/
|
|
60
69
|
export function cloudflare(kv: cloudflare.Parameters): Kv {
|
|
61
70
|
return from({
|
|
@@ -84,10 +93,11 @@ export declare namespace cloudflare {
|
|
|
84
93
|
|
|
85
94
|
/**
|
|
86
95
|
* Adapt a Cloudflare Durable Object namespace into a `Kv` with atomic
|
|
87
|
-
* `take`. Unlike `Kv.cloudflare`, a Durable Object's
|
|
88
|
-
* single-actor and linearizable
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* `take` and `create`. Unlike `Kv.cloudflare`, a Durable Object's
|
|
97
|
+
* storage is single-actor and linearizable, so `take` (read+delete) and
|
|
98
|
+
* `create` (create-if-absent) are guaranteed atomic across concurrent
|
|
99
|
+
* callers. This makes it the recommended backend for SIWE challenge
|
|
100
|
+
* nonce storage on Cloudflare Workers.
|
|
91
101
|
*
|
|
92
102
|
* Pair with `Kv.NonceStorage` (or your own DO class implementing the
|
|
93
103
|
* same fetch protocol).
|
|
@@ -147,6 +157,12 @@ export function durableObject(
|
|
|
147
157
|
async set(key, value, options) {
|
|
148
158
|
await rpc('set', key, { value, ttl: options?.ttl })
|
|
149
159
|
},
|
|
160
|
+
async create(key, value, options) {
|
|
161
|
+
const { created } = (await rpc('create', key, { value, ttl: options?.ttl })) as {
|
|
162
|
+
created: boolean
|
|
163
|
+
}
|
|
164
|
+
return created
|
|
165
|
+
},
|
|
150
166
|
async delete(key) {
|
|
151
167
|
await rpc('delete', key)
|
|
152
168
|
},
|
|
@@ -228,6 +244,17 @@ export class NonceStorage {
|
|
|
228
244
|
await this.state.storage.put(key, entry)
|
|
229
245
|
return Response.json({})
|
|
230
246
|
}
|
|
247
|
+
if (op === 'create') {
|
|
248
|
+
const current = await this.state.storage.get<NonceStorage.Entry>(key)
|
|
249
|
+
if (current && !isExpired(current)) return Response.json({ created: false })
|
|
250
|
+
|
|
251
|
+
const body = (await request.json()) as { value: unknown; ttl?: number }
|
|
252
|
+
const entry: NonceStorage.Entry = body.ttl
|
|
253
|
+
? { value: body.value, expiresAt: Date.now() + body.ttl * 1000 }
|
|
254
|
+
: { value: body.value }
|
|
255
|
+
await this.state.storage.put(key, entry)
|
|
256
|
+
return Response.json({ created: true })
|
|
257
|
+
}
|
|
231
258
|
if (op === 'delete') {
|
|
232
259
|
await this.state.storage.delete(key)
|
|
233
260
|
return Response.json({})
|
|
@@ -280,6 +307,13 @@ export function memory(options: memory.Options = {}): Kv {
|
|
|
280
307
|
const expiresAt = options?.ttl ? now() + options.ttl * 1000 : undefined
|
|
281
308
|
store.set(key, expiresAt !== undefined ? { value, expiresAt } : { value })
|
|
282
309
|
},
|
|
310
|
+
async create(key, value, options) {
|
|
311
|
+
const entry = store.get(key)
|
|
312
|
+
if (entry && !isExpired(entry)) return false
|
|
313
|
+
const expiresAt = options?.ttl ? now() + options.ttl * 1000 : undefined
|
|
314
|
+
store.set(key, expiresAt !== undefined ? { value, expiresAt } : { value })
|
|
315
|
+
return true
|
|
316
|
+
},
|
|
283
317
|
// Atomic in-process: the synchronous `Map.get` + `Map.delete` runs
|
|
284
318
|
// in a single microtask, so concurrent `take(key)` callers (within
|
|
285
319
|
// the same Node/Bun/Worker process) cannot both observe the value.
|
|
@@ -15,6 +15,12 @@ const otherAccount = privateKeyToAccount(
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
describe('challenge', () => {
|
|
18
|
+
test('error: requires pinned origin or domain', () => {
|
|
19
|
+
expect(() => auth()).toThrowErrorMatchingInlineSnapshot(
|
|
20
|
+
`[Error: \`auth()\` requires \`origin\` or \`domain\` to pin SIWE domain binding.]`,
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
18
24
|
test('returns challenge message with chainId, nonce, zero-address placeholder', async () => {
|
|
19
25
|
const { app } = setup()
|
|
20
26
|
|
|
@@ -689,7 +695,7 @@ describe('getSession', () => {
|
|
|
689
695
|
|
|
690
696
|
describe('store: atomic `take` preferred, non-atomic fallback', () => {
|
|
691
697
|
test('Kv.memory() (has `take`) is accepted', () => {
|
|
692
|
-
expect(() => auth({ store: Kv.memory() })).not.toThrow()
|
|
698
|
+
expect(() => auth({ domain: 'wallet.example', store: Kv.memory() })).not.toThrow()
|
|
693
699
|
})
|
|
694
700
|
|
|
695
701
|
test('store without `take` falls back to non-atomic get + delete', async () => {
|
|
@@ -726,8 +732,7 @@ describe('store: atomic `take` preferred, non-atomic fallback', () => {
|
|
|
726
732
|
|
|
727
733
|
describe('origin / trustProxy', () => {
|
|
728
734
|
test('default: ignores `x-forwarded-host` and `x-forwarded-proto`', async () => {
|
|
729
|
-
|
|
730
|
-
const handler = auth()
|
|
735
|
+
const handler = auth({ domain: 'real.example' })
|
|
731
736
|
const app = new Hono()
|
|
732
737
|
app.route('/', handler)
|
|
733
738
|
|
|
@@ -747,7 +752,7 @@ describe('origin / trustProxy', () => {
|
|
|
747
752
|
})
|
|
748
753
|
|
|
749
754
|
test('trustProxy: true → honors `x-forwarded-host` and `x-forwarded-proto`', async () => {
|
|
750
|
-
const handler = auth({ trustProxy: true })
|
|
755
|
+
const handler = auth({ domain: 'app.example', trustProxy: true })
|
|
751
756
|
const app = new Hono()
|
|
752
757
|
app.route('/', handler)
|
|
753
758
|
|
|
@@ -805,7 +810,7 @@ describe('origin / trustProxy', () => {
|
|
|
805
810
|
configurable: true,
|
|
806
811
|
})
|
|
807
812
|
try {
|
|
808
|
-
const handler = auth()
|
|
813
|
+
const handler = auth({ domain: 'app.example' })
|
|
809
814
|
const app = new Hono()
|
|
810
815
|
app.route('/', handler)
|
|
811
816
|
|
|
@@ -138,6 +138,9 @@ export function auth(options: auth.Options = {}): auth.ReturnType {
|
|
|
138
138
|
...rest
|
|
139
139
|
} = options
|
|
140
140
|
|
|
141
|
+
if (!origin_option && !domain)
|
|
142
|
+
throw new Error('`auth()` requires `origin` or `domain` to pin SIWE domain binding.')
|
|
143
|
+
|
|
141
144
|
async function take(key: string): Promise<ChallengePayload | undefined> {
|
|
142
145
|
if (store.take) return store.take<ChallengePayload>(key)
|
|
143
146
|
const value = await store.get<ChallengePayload>(key)
|
|
@@ -414,6 +414,7 @@ describe('behavior: with app-provided feePayer URL', () => {
|
|
|
414
414
|
walletServer = await createServer(
|
|
415
415
|
relay({
|
|
416
416
|
chains: [chain],
|
|
417
|
+
internal_allowUnsafeUrls: true,
|
|
417
418
|
transports: { [chain.id]: http() },
|
|
418
419
|
}).listener,
|
|
419
420
|
)
|
|
@@ -504,6 +505,7 @@ describe('behavior: with app-provided feePayer URL + autoSwap', () => {
|
|
|
504
505
|
relay({
|
|
505
506
|
chains: [chain],
|
|
506
507
|
features: 'all',
|
|
508
|
+
internal_allowUnsafeUrls: true,
|
|
507
509
|
transports: { [chain.id]: http() },
|
|
508
510
|
}).listener,
|
|
509
511
|
)
|
|
@@ -625,6 +627,7 @@ describe('behavior: app-provided feePayer URL bypasses wallet validate', () => {
|
|
|
625
627
|
relay({
|
|
626
628
|
chains: [chain],
|
|
627
629
|
features: 'all',
|
|
630
|
+
internal_allowUnsafeUrls: true,
|
|
628
631
|
transports: { [chain.id]: http() },
|
|
629
632
|
feePayer: {
|
|
630
633
|
account: feePayerAccount,
|
|
@@ -74,6 +74,7 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
74
74
|
const {
|
|
75
75
|
cacheTtl = defaultCacheTtl,
|
|
76
76
|
chains = [tempo, tempoModerato, tempoDevnet],
|
|
77
|
+
internal_allowUnsafeUrls = false,
|
|
77
78
|
kv = Kv.memory(),
|
|
78
79
|
onRequest,
|
|
79
80
|
path = '/',
|
|
@@ -150,7 +151,11 @@ export function relay(options: relay.Options = {}): Handler {
|
|
|
150
151
|
const requestFeeToken =
|
|
151
152
|
typeof parameters.feeToken === 'string' ? (parameters.feeToken as Address) : undefined
|
|
152
153
|
const externalFeePayerUrl =
|
|
153
|
-
typeof parameters.feePayer === 'string'
|
|
154
|
+
typeof parameters.feePayer === 'string'
|
|
155
|
+
? normalizeExternalFeePayerUrl(parameters.feePayer, {
|
|
156
|
+
allowUnsafe: internal_allowUnsafeUrls,
|
|
157
|
+
})
|
|
158
|
+
: undefined
|
|
154
159
|
const requestsSponsorship =
|
|
155
160
|
(!!feePayerOptions || !!externalFeePayerUrl) && parameters.feePayer !== false
|
|
156
161
|
// Default to `true`. Dapps that don't render diffs can pass
|
|
@@ -639,6 +644,20 @@ export namespace relay {
|
|
|
639
644
|
url?: string | undefined
|
|
640
645
|
}
|
|
641
646
|
| undefined
|
|
647
|
+
/**
|
|
648
|
+
* Relay features.
|
|
649
|
+
*
|
|
650
|
+
* - `'all'` — enables fee token resolution, auto-swap,
|
|
651
|
+
* fee payer, and simulation (balance diffs + fee breakdown).
|
|
652
|
+
* - `undefined` (default) — only fee payers.
|
|
653
|
+
*/
|
|
654
|
+
features?: 'all' | undefined
|
|
655
|
+
/**
|
|
656
|
+
* Allows app-provided external fee payer URLs to use non-HTTPS protocols
|
|
657
|
+
* or local/private hosts. Intended only for trusted development setups.
|
|
658
|
+
* @default false
|
|
659
|
+
*/
|
|
660
|
+
internal_allowUnsafeUrls?: boolean | undefined
|
|
642
661
|
/**
|
|
643
662
|
* Kv store used to cache `resolveTokens` results across requests.
|
|
644
663
|
* Provide `Kv.cloudflare(env.KV)` for cross-instance caching, or omit
|
|
@@ -646,6 +665,10 @@ export namespace relay {
|
|
|
646
665
|
* @default Kv.memory()
|
|
647
666
|
*/
|
|
648
667
|
kv?: Kv.Kv | undefined
|
|
668
|
+
/** Function to call before handling the request. */
|
|
669
|
+
onRequest?: ((request: RpcRequest.RpcRequest) => Promise<void>) | undefined
|
|
670
|
+
/** Path to use for the handler. @default "/" */
|
|
671
|
+
path?: string | undefined
|
|
649
672
|
/**
|
|
650
673
|
* Resolves the list of known tokens for a chain. The relay checks
|
|
651
674
|
* `balanceOf` for each token and picks the one with the highest balance
|
|
@@ -655,18 +678,6 @@ export namespace relay {
|
|
|
655
678
|
resolveTokens?:
|
|
656
679
|
| ((chainId: number) => readonly Tokenlist.Token[] | Promise<readonly Tokenlist.Token[]>)
|
|
657
680
|
| undefined
|
|
658
|
-
/**
|
|
659
|
-
* Relay features.
|
|
660
|
-
*
|
|
661
|
-
* - `'all'` — enables fee token resolution, auto-swap,
|
|
662
|
-
* fee payer, and simulation (balance diffs + fee breakdown).
|
|
663
|
-
* - `undefined` (default) — only fee payers.
|
|
664
|
-
*/
|
|
665
|
-
features?: 'all' | undefined
|
|
666
|
-
/** Function to call before handling the request. */
|
|
667
|
-
onRequest?: ((request: RpcRequest.RpcRequest) => Promise<void>) | undefined
|
|
668
|
-
/** Path to use for the handler. @default "/" */
|
|
669
|
-
path?: string | undefined
|
|
670
681
|
/** Transports keyed by chain ID. Defaults to `http()` for each chain. */
|
|
671
682
|
transports?: Record<number, Transport> | undefined
|
|
672
683
|
}
|
|
@@ -1406,6 +1417,120 @@ function mergeCallsFromRequest(
|
|
|
1406
1417
|
return merged
|
|
1407
1418
|
}
|
|
1408
1419
|
|
|
1420
|
+
/** Normalizes external fee-payer relay URLs before forwarding fill payloads. */
|
|
1421
|
+
function normalizeExternalFeePayerUrl(
|
|
1422
|
+
value: string,
|
|
1423
|
+
options: normalizeExternalFeePayerUrl.Options = {},
|
|
1424
|
+
): string {
|
|
1425
|
+
let url: URL
|
|
1426
|
+
try {
|
|
1427
|
+
url = new URL(value)
|
|
1428
|
+
} catch {
|
|
1429
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL.' })
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const allowUnsafe = options.allowUnsafe === true
|
|
1433
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
1434
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' })
|
|
1435
|
+
if (!allowUnsafe && url.protocol !== 'https:')
|
|
1436
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' })
|
|
1437
|
+
if (!allowUnsafe && isUnsafeHostname(url.hostname))
|
|
1438
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL host.' })
|
|
1439
|
+
|
|
1440
|
+
url.username = ''
|
|
1441
|
+
url.password = ''
|
|
1442
|
+
url.hash = ''
|
|
1443
|
+
return url.href
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
declare namespace normalizeExternalFeePayerUrl {
|
|
1447
|
+
type Options = {
|
|
1448
|
+
/** Whether to allow non-HTTPS or local/private external fee payer URLs. */
|
|
1449
|
+
allowUnsafe?: boolean | undefined
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function isUnsafeHostname(hostname: string): boolean {
|
|
1454
|
+
const host = normalizeHostname(hostname)
|
|
1455
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true
|
|
1456
|
+
const ipv4 = parseIpv4(host)
|
|
1457
|
+
if (ipv4) return isUnsafeIpv4(ipv4)
|
|
1458
|
+
if (!host.includes(':')) return false
|
|
1459
|
+
const ipv6 = parseIpv6(host)
|
|
1460
|
+
if (!ipv6) return true
|
|
1461
|
+
return isUnsafeIpv6(ipv6)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function normalizeHostname(hostname: string): string {
|
|
1465
|
+
return hostname
|
|
1466
|
+
.toLowerCase()
|
|
1467
|
+
.replace(/^\[|\]$/g, '')
|
|
1468
|
+
.split('%')[0]!
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function parseIpv4(value: string): [number, number, number, number] | undefined {
|
|
1472
|
+
const parts = value.split('.')
|
|
1473
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) return undefined
|
|
1474
|
+
const numbers = parts.map((part) => Number(part))
|
|
1475
|
+
if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined
|
|
1476
|
+
return numbers as [number, number, number, number]
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function isUnsafeIpv4(parts: [number, number, number, number]): boolean {
|
|
1480
|
+
const [a, b] = parts
|
|
1481
|
+
if (a === 0 || a === 10 || a === 127) return true
|
|
1482
|
+
if (a === 100 && b >= 64 && b <= 127) return true
|
|
1483
|
+
if (a === 169 && b === 254) return true
|
|
1484
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
1485
|
+
if (a === 192 && b === 168) return true
|
|
1486
|
+
if (a >= 224) return true
|
|
1487
|
+
return false
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function parseIpv6(
|
|
1491
|
+
value: string,
|
|
1492
|
+
): [number, number, number, number, number, number, number, number] | undefined {
|
|
1493
|
+
const halves = value.split('::')
|
|
1494
|
+
if (halves.length > 2) return undefined
|
|
1495
|
+
|
|
1496
|
+
const left = halves[0] ? halves[0].split(':') : []
|
|
1497
|
+
const right = halves[1] ? halves[1].split(':') : []
|
|
1498
|
+
const missing = halves.length === 2 ? 8 - left.length - right.length : 0
|
|
1499
|
+
if (missing < 0) return undefined
|
|
1500
|
+
|
|
1501
|
+
const parts = [...left, ...Array(missing).fill('0'), ...right]
|
|
1502
|
+
if (parts.length !== 8) return undefined
|
|
1503
|
+
|
|
1504
|
+
const numbers = parts.map((part) =>
|
|
1505
|
+
/^[\da-f]{1,4}$/i.test(part) ? Number.parseInt(part, 16) : NaN,
|
|
1506
|
+
)
|
|
1507
|
+
if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff)) return undefined
|
|
1508
|
+
return numbers as [number, number, number, number, number, number, number, number]
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function isUnsafeIpv6(parts: [number, number, number, number, number, number, number, number]) {
|
|
1512
|
+
const [a] = parts
|
|
1513
|
+
if (parts.every((part) => part === 0)) return true
|
|
1514
|
+
if (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1) return true
|
|
1515
|
+
if ((a & 0xfe00) === 0xfc00) return true
|
|
1516
|
+
if ((a & 0xffc0) === 0xfe80) return true
|
|
1517
|
+
if ((a & 0xffc0) === 0xfec0) return true
|
|
1518
|
+
if ((a & 0xff00) === 0xff00) return true
|
|
1519
|
+
|
|
1520
|
+
const ipv4 = ipv4FromIpv6(parts)
|
|
1521
|
+
if (ipv4) return isUnsafeIpv4(ipv4)
|
|
1522
|
+
return false
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function ipv4FromIpv6(
|
|
1526
|
+
parts: [number, number, number, number, number, number, number, number],
|
|
1527
|
+
): [number, number, number, number] | undefined {
|
|
1528
|
+
const mapped = parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff
|
|
1529
|
+
const compatible = parts.slice(0, 6).every((part) => part === 0)
|
|
1530
|
+
if (!mapped && !compatible) return undefined
|
|
1531
|
+
return [parts[6]! >> 8, parts[6]! & 0xff, parts[7]! >> 8, parts[7]! & 0xff]
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1409
1534
|
/**
|
|
1410
1535
|
* Reconstructs a `swap` shape (matching the inner autoSwap branch's return
|
|
1411
1536
|
* value) from an upstream relay's `capabilities.autoSwap`. Used so a wallet
|
|
@@ -143,7 +143,7 @@ describe('tokenFromRequest (Node.js headers)', () => {
|
|
|
143
143
|
|
|
144
144
|
describe('getSession with http.IncomingMessage', () => {
|
|
145
145
|
const store = Kv.memory()
|
|
146
|
-
const handler = auth({ store, cookie: false })
|
|
146
|
+
const handler = auth({ store, cookie: false, domain: 'localhost' })
|
|
147
147
|
|
|
148
148
|
let authServer: Http.Server
|
|
149
149
|
let authUrl: string
|
|
@@ -67,6 +67,26 @@ describe('POST /register', () => {
|
|
|
67
67
|
})
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
describe('kv', () => {
|
|
71
|
+
test('store without atomic create is accepted', () => {
|
|
72
|
+
const kv: Kv.Kv = {
|
|
73
|
+
async get() {
|
|
74
|
+
return undefined
|
|
75
|
+
},
|
|
76
|
+
async set() {},
|
|
77
|
+
async delete() {},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(() =>
|
|
81
|
+
webAuthn({
|
|
82
|
+
kv,
|
|
83
|
+
origin: 'http://localhost',
|
|
84
|
+
rpId: 'localhost',
|
|
85
|
+
}),
|
|
86
|
+
).not.toThrow()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
70
90
|
describe('POST /login', () => {
|
|
71
91
|
test('error: unknown credential → 400', async () => {
|
|
72
92
|
const response = await fetch(`${server.url}/login`, {
|
|
@@ -20,6 +20,17 @@ const defaults = {
|
|
|
20
20
|
|
|
21
21
|
const sessionKey = (token: string) => `session:${token}`
|
|
22
22
|
|
|
23
|
+
async function createCredential(
|
|
24
|
+
kv: Kv.Kv,
|
|
25
|
+
key: string,
|
|
26
|
+
value: { publicKey: string; userId?: string | undefined },
|
|
27
|
+
) {
|
|
28
|
+
if (kv.create) return await kv.create(key, value)
|
|
29
|
+
if (await kv.get(key)) return false
|
|
30
|
+
await kv.set(key, value)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/**
|
|
24
35
|
* Session payload persisted in the session store and surfaced via
|
|
25
36
|
* `getSession`. Mirrors the shape of the WebAuthn login response so
|
|
@@ -149,9 +160,13 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
|
|
|
149
160
|
const userId = stored.userId
|
|
150
161
|
? Base64.fromBytes(Bytes.fromString(stored.userId), { pad: false, url: true })
|
|
151
162
|
: undefined
|
|
163
|
+
const created = await createCredential(kv, `credential:${credentialId}`, {
|
|
164
|
+
publicKey,
|
|
165
|
+
...(userId ? { userId } : {}),
|
|
166
|
+
})
|
|
167
|
+
if (!created) throw new Error('Credential already exists')
|
|
152
168
|
|
|
153
|
-
const [
|
|
154
|
-
kv.set(`credential:${credentialId}`, { publicKey, ...(userId ? { userId } : {}) }),
|
|
169
|
+
const [hook] = await Promise.all([
|
|
155
170
|
onRegister?.({
|
|
156
171
|
credentialId,
|
|
157
172
|
name: stored.name,
|
|
@@ -263,17 +278,9 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
|
|
|
263
278
|
})
|
|
264
279
|
if (!valid) throw new Error('Authentication failed')
|
|
265
280
|
|
|
266
|
-
const rawResponse = response.raw?.response as unknown as Record<string, string> | undefined
|
|
267
|
-
const userHandle = rawResponse?.userHandle
|
|
268
|
-
|
|
269
281
|
const credentialId = response.id
|
|
270
282
|
const publicKey = credentialData.publicKey
|
|
271
|
-
|
|
272
|
-
// (base64url-encoded user id). Fall back to the base64-encoded
|
|
273
|
-
// userId we stashed during register, so callers see the same
|
|
274
|
-
// identifier shape across register and login.
|
|
275
|
-
const userId =
|
|
276
|
-
userHandle && userHandle.length > 0 ? userHandle : (credentialData.userId ?? undefined)
|
|
283
|
+
const userId = credentialData.userId
|
|
277
284
|
|
|
278
285
|
// Hook for side effects (user provisioning, analytics, allow/deny).
|
|
279
286
|
// The legacy contract — return a `Response` to merge fields onto
|
|
@@ -400,7 +407,12 @@ export declare namespace webAuthn {
|
|
|
400
407
|
cookie?: boolean | undefined
|
|
401
408
|
/** Cookie name for the session token. @default "accounts_webauthn" */
|
|
402
409
|
cookieName?: string | undefined
|
|
403
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Key-value store for challenges, credentials, and sessions. When
|
|
412
|
+
* `create` is available, credential registration uses it to reject
|
|
413
|
+
* duplicates atomically. Otherwise, registration falls back to
|
|
414
|
+
* best-effort `get` then `set` storage.
|
|
415
|
+
*/
|
|
404
416
|
kv: Kv.Kv
|
|
405
417
|
/** Called after a successful registration. The returned response is merged onto the default JSON response. */
|
|
406
418
|
onRegister?: (parameters: {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|