accounts 0.16.0 → 0.16.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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/Adapter.d.ts +2 -2
  3. package/dist/core/Adapter.d.ts.map +1 -1
  4. package/dist/core/Provider.d.ts.map +1 -1
  5. package/dist/core/Provider.js +6 -1
  6. package/dist/core/Provider.js.map +1 -1
  7. package/dist/core/Schema.d.ts +2 -2
  8. package/dist/core/WebAuthnCeremony.d.ts +2 -4
  9. package/dist/core/WebAuthnCeremony.d.ts.map +1 -1
  10. package/dist/core/WebAuthnCeremony.js +8 -4
  11. package/dist/core/WebAuthnCeremony.js.map +1 -1
  12. package/dist/core/adapters/webAuthn.js +1 -1
  13. package/dist/core/adapters/webAuthn.js.map +1 -1
  14. package/dist/core/zod/rpc.d.ts +4 -4
  15. package/dist/core/zod/rpc.js +2 -2
  16. package/dist/core/zod/rpc.js.map +1 -1
  17. package/dist/server/internal/handlers/utils.d.ts.map +1 -1
  18. package/dist/server/internal/handlers/utils.js +10 -0
  19. package/dist/server/internal/handlers/utils.js.map +1 -1
  20. package/dist/server/internal/handlers/webAuthn.d.ts +7 -1
  21. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
  22. package/dist/server/internal/handlers/webAuthn.js +21 -12
  23. package/dist/server/internal/handlers/webAuthn.js.map +1 -1
  24. package/package.json +3 -3
  25. package/src/core/Adapter.ts +2 -2
  26. package/src/core/Provider.browser.test.ts +30 -0
  27. package/src/core/Provider.test.ts +32 -0
  28. package/src/core/Provider.ts +6 -1
  29. package/src/core/Schema.test-d.ts +1 -1
  30. package/src/core/WebAuthnCeremony.test.ts +21 -0
  31. package/src/core/WebAuthnCeremony.ts +11 -8
  32. package/src/core/adapters/webAuthn.ts +1 -1
  33. package/src/core/zod/rpc.test.ts +19 -0
  34. package/src/core/zod/rpc.ts +2 -2
  35. package/src/server/internal/handlers/utils.test.ts +53 -0
  36. package/src/server/internal/handlers/utils.ts +16 -0
  37. package/src/server/internal/handlers/webAuthn.test.ts +76 -0
  38. package/src/server/internal/handlers/webAuthn.ts +28 -14
@@ -710,7 +710,7 @@ export namespace wallet_connect {
710
710
  }),
711
711
  z.object({
712
712
  digest: z.optional(u.hex()),
713
- credentialId: z.optional(z.string()),
713
+ credentialId: z.optional(z.union([z.string(), z.array(z.string())])),
714
714
  authorizeAccessKey,
715
715
  auth,
716
716
  identity,
@@ -829,7 +829,7 @@ export namespace wallet_connect_strict {
829
829
  }),
830
830
  z.object({
831
831
  digest: z.optional(u.hex()),
832
- credentialId: z.optional(z.string()),
832
+ credentialId: z.optional(z.union([z.string(), z.array(z.string())])),
833
833
  authorizeAccessKey,
834
834
  auth,
835
835
  identity,
@@ -0,0 +1,53 @@
1
+ import { RpcRequest } from 'ox'
2
+ import { describe, expect, test } from 'vp/test'
3
+
4
+ import * as Utils from './utils.js'
5
+
6
+ const request = RpcRequest.from({ id: 1, method: 'eth_sendRawTransaction', params: ['0x00'] })
7
+
8
+ describe('rpcErrorJson', () => {
9
+ test('maps upstream expired transactions to a transaction-rejected error', () => {
10
+ const error = {
11
+ cause: {
12
+ code: -32603,
13
+ message: 'Revm error: transaction expired',
14
+ },
15
+ }
16
+
17
+ expect(Utils.rpcErrorJson(request, error)).toMatchInlineSnapshot(`
18
+ {
19
+ "error": {
20
+ "code": -32003,
21
+ "data": {
22
+ "code": "transaction_expired",
23
+ },
24
+ "message": "Transaction expired.",
25
+ },
26
+ "id": 1,
27
+ "jsonrpc": "2.0",
28
+ }
29
+ `)
30
+ })
31
+
32
+ test('preserves unrelated upstream internal errors', () => {
33
+ const error = {
34
+ code: -32603,
35
+ data: { reason: 'database unavailable' },
36
+ message: 'Revm error: database unavailable',
37
+ }
38
+
39
+ expect(Utils.rpcErrorJson(request, error)).toMatchInlineSnapshot(`
40
+ {
41
+ "error": {
42
+ "code": -32603,
43
+ "data": {
44
+ "reason": "database unavailable",
45
+ },
46
+ "message": "Revm error: database unavailable",
47
+ },
48
+ "id": 1,
49
+ "jsonrpc": "2.0",
50
+ }
51
+ `)
52
+ })
53
+ })
@@ -97,6 +97,22 @@ export function rpcErrorJson(request: RpcRequest.RpcRequest, error: unknown) {
97
97
  )
98
98
 
99
99
  const inner = resolveError(error)
100
+ if (
101
+ inner.code === RpcResponse.InternalError.code &&
102
+ inner.message !== undefined &&
103
+ /^Revm error: transaction expired\.?$/.test(inner.message)
104
+ )
105
+ return RpcResponse.from(
106
+ {
107
+ error: {
108
+ code: -32003,
109
+ data: { code: 'transaction_expired' },
110
+ message: 'Transaction expired.',
111
+ },
112
+ },
113
+ { request },
114
+ )
115
+
100
116
  const message = inner.message ?? (error as Error).message
101
117
  const code = inner.code ?? -32603
102
118
  const data = inner.data
@@ -1,10 +1,14 @@
1
+ import { vi } from 'vitest'
1
2
  import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
3
+ import { Registration } from 'webauthx/server'
2
4
 
3
5
  import { createServer, type Server } from '../../../../test/utils.js'
4
6
  import * as WebAuthnCeremony from '../../../core/WebAuthnCeremony.js'
5
7
  import * as Kv from '../../Kv.js'
6
8
  import { type SessionPayload, webAuthn } from './webAuthn.js'
7
9
 
10
+ vi.mock('webauthx/server', { spy: true })
11
+
8
12
  let server: Server
9
13
  let ceremony: WebAuthnCeremony.WebAuthnCeremony
10
14
 
@@ -52,6 +56,25 @@ describe('POST /login/options', () => {
52
56
  const { options: b } = await ceremony.getAuthenticationOptions()
53
57
  expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
54
58
  })
59
+
60
+ test('behavior: restricts authentication to multiple credentials', async () => {
61
+ const { options } = await ceremony.getAuthenticationOptions({
62
+ credentialId: ['Y3JlZC0x', 'Y3JlZC0y'],
63
+ })
64
+
65
+ expect(options.publicKey?.allowCredentials).toMatchInlineSnapshot(`
66
+ [
67
+ {
68
+ "id": "Y3JlZC0x",
69
+ "type": "public-key",
70
+ },
71
+ {
72
+ "id": "Y3JlZC0y",
73
+ "type": "public-key",
74
+ },
75
+ ]
76
+ `)
77
+ })
55
78
  })
56
79
 
57
80
  describe('POST /register', () => {
@@ -152,6 +175,59 @@ describe('hooks', () => {
152
175
  await hookServer.closeAsync()
153
176
  })
154
177
 
178
+ test('behavior: onRegister rejection removes the credential', async () => {
179
+ const kv = Kv.memory()
180
+ await kv.set('challenge:0x01', { created: Date.now(), name: 'Test' })
181
+ const publicKey =
182
+ '0x04ab891400140fc4f8e941ce0ff90e419de9470acaca613bbd717a4775435031a7d884318e919fd3b3e5a631d866d8a380b44063e70f0c381ee16e0652f7f97554'
183
+ vi.mocked(Registration.verify).mockReturnValueOnce({
184
+ aaguid: 'fbfc3007-154e-4ecc-8c0b-6e020557d7bd',
185
+ credential: { publicKey },
186
+ } as never)
187
+ const hookServer = await createServer(
188
+ webAuthn({
189
+ kv,
190
+ origin: 'http://localhost',
191
+ rpId: 'localhost',
192
+ onRegister() {
193
+ throw new Error('Authenticator not allowed')
194
+ },
195
+ }).listener,
196
+ )
197
+
198
+ try {
199
+ const clientDataJSON = 'eyJjaGFsbGVuZ2UiOiJBUSJ9'
200
+ const response = await fetch(`${hookServer.url}/register`, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({
204
+ attestationObject: 'AA',
205
+ clientDataJSON,
206
+ id: 'credential',
207
+ publicKey,
208
+ raw: {
209
+ authenticatorAttachment: null,
210
+ id: 'credential',
211
+ rawId: 'Y3JlZGVudGlhbA',
212
+ response: { clientDataJSON },
213
+ type: 'public-key',
214
+ },
215
+ }),
216
+ })
217
+
218
+ expect(response.status).toMatchInlineSnapshot(`400`)
219
+ expect(await response.json()).toMatchInlineSnapshot(`
220
+ {
221
+ "error": "Authenticator not allowed",
222
+ }
223
+ `)
224
+ expect(await kv.get('credential:credential')).toBeUndefined()
225
+ expect(await kv.get('challenge:0x01')).toBeUndefined()
226
+ } finally {
227
+ await hookServer.closeAsync()
228
+ }
229
+ })
230
+
155
231
  test('behavior: onAuthenticate error does not call hook', async () => {
156
232
  let called = false
157
233
  const hookServer = await createServer(
@@ -152,6 +152,7 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
152
152
  rpId,
153
153
  })
154
154
 
155
+ const { aaguid } = result
155
156
  const { publicKey } = result.credential
156
157
  const credentialId = credential.id
157
158
  // Base64url-encode the userId we registered with so it matches
@@ -160,22 +161,29 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
160
161
  const userId = stored.userId
161
162
  ? Base64.fromBytes(Bytes.fromString(stored.userId), { pad: false, url: true })
162
163
  : undefined
163
- const created = await createCredential(kv, `credential:${credentialId}`, {
164
+ const key = `credential:${credentialId}`
165
+ const created = await createCredential(kv, key, {
164
166
  publicKey,
165
167
  ...(userId ? { userId } : {}),
166
168
  })
167
169
  if (!created) throw new Error('Credential already exists')
168
170
 
169
- const [hook] = await Promise.all([
170
- onRegister?.({
171
- credentialId,
172
- name: stored.name,
173
- publicKey,
174
- request: c.req.raw,
175
- ...(userId ? { userId } : {}),
176
- }),
177
- kv.delete(`challenge:${challenge}`),
178
- ])
171
+ const hook = await (async () => {
172
+ try {
173
+ return await onRegister?.({
174
+ ...(aaguid ? { aaguid } : {}),
175
+ credentialId,
176
+ name: stored.name,
177
+ publicKey,
178
+ request: c.req.raw,
179
+ ...(userId ? { userId } : {}),
180
+ })
181
+ } catch (error) {
182
+ await Promise.all([kv.delete(key), kv.delete(`challenge:${challenge}`)])
183
+ throw error
184
+ }
185
+ })()
186
+ await kv.delete(`challenge:${challenge}`)
179
187
 
180
188
  // Successful registration is also a successful authentication for
181
189
  // the freshly-minted credential. Issue a session here so the
@@ -231,13 +239,13 @@ export function webAuthn(options: webAuthn.Options): webAuthn.ReturnType {
231
239
  } = body as {
232
240
  allowCredentialIds?: string[]
233
241
  challenge?: Hex.Hex
234
- credentialId?: string
242
+ credentialId?: string | string[]
235
243
  mediation?: string
236
244
  }
237
245
 
238
246
  const { challenge, options: authOptions } = Authentication.getOptions({
239
247
  challenge: requestChallenge,
240
- credentialId: allowCredentialIds ?? credentialId,
248
+ credentialId: credentialId ?? allowCredentialIds,
241
249
  rpId,
242
250
  })
243
251
  const options = mediation ? { ...authOptions, mediation } : authOptions
@@ -414,8 +422,14 @@ export declare namespace webAuthn {
414
422
  * best-effort `get` then `set` storage.
415
423
  */
416
424
  kv: Kv.Kv
417
- /** Called after a successful registration. The returned response is merged onto the default JSON response. */
425
+ /**
426
+ * Called after a successful registration. The returned response is merged
427
+ * onto the default JSON response. Throwing rejects the request and removes
428
+ * the newly stored credential.
429
+ */
418
430
  onRegister?: (parameters: {
431
+ /** Authenticator model identifier, when the authenticator reports one. */
432
+ aaguid?: string | undefined
419
433
  credentialId: string
420
434
  /** The name provided during `/register/options` (e.g. user email). */
421
435
  name: string