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.
- package/CHANGELOG.md +15 -0
- package/dist/core/Adapter.d.ts +2 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +6 -1
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +2 -2
- package/dist/core/WebAuthnCeremony.d.ts +2 -4
- package/dist/core/WebAuthnCeremony.d.ts.map +1 -1
- package/dist/core/WebAuthnCeremony.js +8 -4
- package/dist/core/WebAuthnCeremony.js.map +1 -1
- package/dist/core/adapters/webAuthn.js +1 -1
- package/dist/core/adapters/webAuthn.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +4 -4
- package/dist/core/zod/rpc.js +2 -2
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/internal/handlers/utils.d.ts.map +1 -1
- package/dist/server/internal/handlers/utils.js +10 -0
- package/dist/server/internal/handlers/utils.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +7 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +21 -12
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +3 -3
- package/src/core/Adapter.ts +2 -2
- package/src/core/Provider.browser.test.ts +30 -0
- package/src/core/Provider.test.ts +32 -0
- package/src/core/Provider.ts +6 -1
- package/src/core/Schema.test-d.ts +1 -1
- package/src/core/WebAuthnCeremony.test.ts +21 -0
- package/src/core/WebAuthnCeremony.ts +11 -8
- package/src/core/adapters/webAuthn.ts +1 -1
- package/src/core/zod/rpc.test.ts +19 -0
- package/src/core/zod/rpc.ts +2 -2
- package/src/server/internal/handlers/utils.test.ts +53 -0
- package/src/server/internal/handlers/utils.ts +16 -0
- package/src/server/internal/handlers/webAuthn.test.ts +76 -0
- package/src/server/internal/handlers/webAuthn.ts +28 -14
package/src/core/zod/rpc.ts
CHANGED
|
@@ -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
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|