ox 1.2.0 → 1.3.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.
Files changed (187) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/core/AbiFunction.d.ts +1 -1
  3. package/dist/core/AbiFunction.d.ts.map +1 -1
  4. package/dist/core/AbiFunction.js +6 -2
  5. package/dist/core/AbiFunction.js.map +1 -1
  6. package/dist/core/AbiParameters.d.ts +1 -1
  7. package/dist/core/AbiParameters.d.ts.map +1 -1
  8. package/dist/core/AbiParameters.js +1 -13
  9. package/dist/core/AbiParameters.js.map +1 -1
  10. package/dist/core/AesGcm.d.ts +36 -1
  11. package/dist/core/AesGcm.d.ts.map +1 -1
  12. package/dist/core/AesGcm.js +44 -0
  13. package/dist/core/AesGcm.js.map +1 -1
  14. package/dist/core/Ed25519.d.ts +46 -1
  15. package/dist/core/Ed25519.d.ts.map +1 -1
  16. package/dist/core/Ed25519.js +43 -0
  17. package/dist/core/Ed25519.js.map +1 -1
  18. package/dist/core/Engine.d.ts +1 -1
  19. package/dist/core/Engine.d.ts.map +1 -1
  20. package/dist/core/Engine.js.map +1 -1
  21. package/dist/core/Hash.d.ts +164 -1
  22. package/dist/core/Hash.d.ts.map +1 -1
  23. package/dist/core/Hash.js +172 -0
  24. package/dist/core/Hash.js.map +1 -1
  25. package/dist/core/Hex.d.ts.map +1 -1
  26. package/dist/core/Hex.js +1 -4
  27. package/dist/core/Hex.js.map +1 -1
  28. package/dist/core/Rlp.d.ts +32 -0
  29. package/dist/core/Rlp.d.ts.map +1 -1
  30. package/dist/core/Rlp.js +146 -0
  31. package/dist/core/Rlp.js.map +1 -1
  32. package/dist/core/Secp256k1.d.ts +46 -1
  33. package/dist/core/Secp256k1.d.ts.map +1 -1
  34. package/dist/core/Secp256k1.js +48 -0
  35. package/dist/core/Secp256k1.js.map +1 -1
  36. package/dist/core/WebAuthn.d.ts +224 -0
  37. package/dist/core/WebAuthn.d.ts.map +1 -0
  38. package/dist/core/WebAuthn.js +343 -0
  39. package/dist/core/WebAuthn.js.map +1 -0
  40. package/dist/core/WebAuthnP256.d.ts +46 -50
  41. package/dist/core/WebAuthnP256.d.ts.map +1 -1
  42. package/dist/core/WebAuthnP256.js +44 -50
  43. package/dist/core/WebAuthnP256.js.map +1 -1
  44. package/dist/core/internal/abiParameters.d.ts +7 -87
  45. package/dist/core/internal/abiParameters.d.ts.map +1 -1
  46. package/dist/core/internal/abiParameters.js +339 -194
  47. package/dist/core/internal/abiParameters.js.map +1 -1
  48. package/dist/core/internal/engine.d.ts +35 -2
  49. package/dist/core/internal/engine.d.ts.map +1 -1
  50. package/dist/core/internal/engine.js +12 -1
  51. package/dist/core/internal/engine.js.map +1 -1
  52. package/dist/core/internal/hash.d.ts +10 -0
  53. package/dist/core/internal/hash.d.ts.map +1 -1
  54. package/dist/core/internal/hash.js +26 -1
  55. package/dist/core/internal/hash.js.map +1 -1
  56. package/dist/index.d.ts +23 -14
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +23 -14
  59. package/dist/index.js.map +1 -1
  60. package/dist/node/Engine.d.ts +1 -1
  61. package/dist/node/Engine.d.ts.map +1 -1
  62. package/dist/node/Engine.js.map +1 -1
  63. package/dist/node/Hash.d.ts +6 -4
  64. package/dist/node/Hash.d.ts.map +1 -1
  65. package/dist/node/Hash.js +35 -3
  66. package/dist/node/Hash.js.map +1 -1
  67. package/dist/tempo/AuthorizationTempo.d.ts +6 -6
  68. package/dist/tempo/AuthorizationTempo.js +6 -6
  69. package/dist/tempo/KeyAuthorization.d.ts +3 -8
  70. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  71. package/dist/tempo/KeyAuthorization.js +3 -8
  72. package/dist/tempo/KeyAuthorization.js.map +1 -1
  73. package/dist/tempo/SignatureEnvelope.d.ts +8 -8
  74. package/dist/tempo/SignatureEnvelope.js +8 -8
  75. package/dist/wasm/Engine.d.ts +1 -1
  76. package/dist/wasm/Engine.d.ts.map +1 -1
  77. package/dist/wasm/Engine.js.map +1 -1
  78. package/dist/wasm/Hash.d.ts +1 -1
  79. package/dist/wasm/Hash.d.ts.map +1 -1
  80. package/dist/wasm/Hash.js +5 -0
  81. package/dist/wasm/Hash.js.map +1 -1
  82. package/dist/wasm/internal/blake3.d.ts +10 -1
  83. package/dist/wasm/internal/blake3.d.ts.map +1 -1
  84. package/dist/wasm/internal/blake3.js +12 -1
  85. package/dist/wasm/internal/blake3.js.map +1 -1
  86. package/dist/wasm/internal/blake3.wasm.d.ts +3 -1
  87. package/dist/wasm/internal/blake3.wasm.d.ts.map +1 -1
  88. package/dist/wasm/internal/blake3.wasm.js +3 -1
  89. package/dist/wasm/internal/blake3.wasm.js.map +1 -1
  90. package/dist/wasm/internal/hash.d.ts +9 -0
  91. package/dist/wasm/internal/hash.d.ts.map +1 -1
  92. package/dist/wasm/internal/hash.js +13 -1
  93. package/dist/wasm/internal/hash.js.map +1 -1
  94. package/dist/wasm/internal/hashState.d.ts +25 -0
  95. package/dist/wasm/internal/hashState.d.ts.map +1 -0
  96. package/dist/wasm/internal/hashState.js +105 -0
  97. package/dist/wasm/internal/hashState.js.map +1 -0
  98. package/dist/wasm/internal/hashes.d.ts +12 -1
  99. package/dist/wasm/internal/hashes.d.ts.map +1 -1
  100. package/dist/wasm/internal/hashes.js +18 -1
  101. package/dist/wasm/internal/hashes.js.map +1 -1
  102. package/dist/wasm/internal/hashes.wasm.d.ts +11 -3
  103. package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -1
  104. package/dist/wasm/internal/hashes.wasm.js +11 -3
  105. package/dist/wasm/internal/hashes.wasm.js.map +1 -1
  106. package/dist/webauthn/Authentication.d.ts +2 -0
  107. package/dist/webauthn/Authentication.d.ts.map +1 -1
  108. package/dist/webauthn/Authentication.js +2 -1
  109. package/dist/webauthn/Authentication.js.map +1 -1
  110. package/dist/webauthn/Registration.d.ts.map +1 -1
  111. package/dist/webauthn/Registration.js +2 -1
  112. package/dist/webauthn/Registration.js.map +1 -1
  113. package/dist/webauthn/Types.d.ts +27 -6
  114. package/dist/webauthn/Types.d.ts.map +1 -1
  115. package/dist/webauthn/internal/utils.d.ts.map +1 -1
  116. package/dist/webauthn/internal/utils.js +35 -9
  117. package/dist/webauthn/internal/utils.js.map +1 -1
  118. package/dist/zod/TypedData.d.ts +2 -2
  119. package/package.json +9 -1
  120. package/src/benchmarks/Ox.bench.ts +6 -0
  121. package/src/benchmarks/OxNode.bench.ts +8 -0
  122. package/src/benchmarks/OxV0.bench.ts +4 -0
  123. package/src/benchmarks/OxWasm.bench.ts +22 -0
  124. package/src/core/AbiFunction.ts +5 -5
  125. package/src/core/AbiParameters.ts +8 -20
  126. package/src/core/AesGcm.ts +86 -1
  127. package/src/core/Ed25519.ts +87 -1
  128. package/src/core/Engine.ts +1 -0
  129. package/src/core/Hash.ts +262 -1
  130. package/src/core/Hex.ts +1 -4
  131. package/src/core/Rlp.ts +172 -5
  132. package/src/core/Secp256k1.ts +92 -1
  133. package/src/core/WebAuthn.ts +579 -0
  134. package/src/core/WebAuthnP256.ts +47 -50
  135. package/src/core/_test/AbiFunction.test.ts +9 -0
  136. package/src/core/_test/AbiParameters.differential.fuzz.ts +156 -0
  137. package/src/core/_test/AbiParameters.encode.test.ts +22 -0
  138. package/src/core/_test/AbiParameters.fuzz.ts +4 -0
  139. package/src/core/_test/AesGcm.test.ts +76 -0
  140. package/src/core/_test/Ed25519.test-d.ts +13 -1
  141. package/src/core/_test/Ed25519.test.ts +120 -1
  142. package/src/core/_test/Engine.test-d.ts +9 -0
  143. package/src/core/_test/Engine.test.ts +1 -1
  144. package/src/core/_test/Hash.test-d.ts +23 -0
  145. package/src/core/_test/Hash.test.ts +179 -1
  146. package/src/core/_test/Hex.test.ts +2 -0
  147. package/src/core/_test/Rlp.test-d.ts +18 -0
  148. package/src/core/_test/Rlp.test.ts +109 -0
  149. package/src/core/_test/Secp256k1.test-d.ts +14 -0
  150. package/src/core/_test/Secp256k1.test.ts +91 -1
  151. package/src/core/_test/WebAuthn.test-d.ts +142 -0
  152. package/src/core/_test/WebAuthn.test.ts +766 -0
  153. package/src/core/_test/index.test.ts +1 -0
  154. package/src/core/internal/abiParameters.ts +466 -326
  155. package/src/core/internal/engine.ts +47 -2
  156. package/src/core/internal/hash.ts +53 -1
  157. package/src/index.ts +24 -14
  158. package/src/node/Engine.ts +1 -0
  159. package/src/node/Hash.ts +43 -6
  160. package/src/node/_test/Engine.test-d.ts +1 -0
  161. package/src/node/_test/Engine.test.ts +2 -0
  162. package/src/node/_test/Hash.test-d.ts +10 -0
  163. package/src/node/_test/Hash.test.ts +46 -0
  164. package/src/tempo/AuthorizationTempo.ts +6 -6
  165. package/src/tempo/KeyAuthorization.ts +3 -8
  166. package/src/tempo/SignatureEnvelope.ts +8 -8
  167. package/src/version.ts +1 -1
  168. package/src/wasm/Engine.ts +1 -0
  169. package/src/wasm/Hash.ts +10 -0
  170. package/src/wasm/_test/Engine.test-d.ts +1 -0
  171. package/src/wasm/_test/Engine.test.ts +5 -0
  172. package/src/wasm/_test/Hash.browser.test.ts +28 -0
  173. package/src/wasm/_test/Hash.test-d.ts +16 -0
  174. package/src/wasm/_test/Hash.test.ts +250 -1
  175. package/src/wasm/internal/blake3.ts +23 -2
  176. package/src/wasm/internal/blake3.wasm.ts +4 -1
  177. package/src/wasm/internal/hash.ts +27 -1
  178. package/src/wasm/internal/hashState.ts +154 -0
  179. package/src/wasm/internal/hashes.ts +56 -15
  180. package/src/wasm/internal/hashes.wasm.ts +15 -3
  181. package/src/webauthn/Authentication.ts +4 -0
  182. package/src/webauthn/Registration.ts +2 -0
  183. package/src/webauthn/Types.ts +32 -2
  184. package/src/webauthn/_test/Authentication.test.ts +123 -1
  185. package/src/webauthn/_test/Registration.test.ts +26 -6
  186. package/src/webauthn/_test/WebAuthn.browser.test.ts +28 -0
  187. package/src/webauthn/internal/utils.ts +41 -8
@@ -0,0 +1,766 @@
1
+ import { runInNewContext } from 'node:vm'
2
+ import { Bytes, Hex, WebAuthn } from 'ox'
3
+ import { afterAll, beforeAll, describe, expect, test, vi } from 'vp/test'
4
+ import type * as Types from '../../webauthn/Types.js'
5
+
6
+ const challenge =
7
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
8
+ const credentialId = 'AQID'
9
+ const input = Bytes.fromHex(
10
+ '0x202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f',
11
+ )
12
+ const publicKey = Uint8Array.from([
13
+ 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61,
14
+ 3, 1, 7, 3, 66, 0, 4, 171, 137, 20, 0, 20, 15, 196, 248, 233, 65, 206, 15,
15
+ 249, 14, 65, 157, 233, 71, 10, 202, 202, 97, 59, 189, 113, 122, 71, 117, 67,
16
+ 80, 49, 167, 216, 132, 49, 142, 145, 159, 211, 179, 229, 166, 49, 216, 102,
17
+ 216, 163, 128, 180, 64, 99, 231, 15, 12, 56, 30, 225, 110, 6, 82, 247, 249,
18
+ 117, 84,
19
+ ])
20
+
21
+ beforeAll(() => {
22
+ vi.stubGlobal('window', {
23
+ document: {
24
+ title: 'Example',
25
+ },
26
+ location: {
27
+ hostname: 'example.com',
28
+ },
29
+ })
30
+ })
31
+
32
+ afterAll(() => {
33
+ vi.restoreAllMocks()
34
+ })
35
+
36
+ describe('createCredential', () => {
37
+ test('default', async () => {
38
+ let options: Types.CredentialCreationOptions | undefined
39
+ const output = Uint8Array.from({ length: 32 }, (_, index) => index + 64)
40
+
41
+ const result = await WebAuthn.createCredential({
42
+ authenticatorSelection: {
43
+ authenticatorAttachment: 'platform',
44
+ residentKey: 'required',
45
+ },
46
+ challenge,
47
+ createFn(options_) {
48
+ options = options_
49
+ return Promise.resolve(registrationCredential({ output }))
50
+ },
51
+ extensions: {
52
+ credProps: true,
53
+ },
54
+ name: 'Example',
55
+ prf: true,
56
+ rp: {
57
+ id: 'example.com',
58
+ name: 'Example',
59
+ },
60
+ })
61
+
62
+ expect({
63
+ credentialId: result.id,
64
+ prf: Hex.fromBytes(result.prf),
65
+ }).toMatchInlineSnapshot(`
66
+ {
67
+ "credentialId": "AQID",
68
+ "prf": "0x404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
69
+ }
70
+ `)
71
+ expect(options?.publicKey?.authenticatorSelection).toMatchInlineSnapshot(`
72
+ {
73
+ "authenticatorAttachment": "platform",
74
+ "residentKey": "required",
75
+ "userVerification": "required",
76
+ }
77
+ `)
78
+ expect(options?.publicKey?.extensions).toMatchInlineSnapshot(`
79
+ {
80
+ "credProps": true,
81
+ "prf": {
82
+ "eval": {
83
+ "first": Uint8Array [
84
+ 111,
85
+ 120,
86
+ 46,
87
+ 119,
88
+ 101,
89
+ 98,
90
+ 97,
91
+ 117,
92
+ 116,
93
+ 104,
94
+ 110,
95
+ 46,
96
+ 112,
97
+ 114,
98
+ 102,
99
+ 46,
100
+ 118,
101
+ 49,
102
+ ],
103
+ },
104
+ },
105
+ }
106
+ `)
107
+ expect(options?.publicKey?.challenge).toMatchInlineSnapshot(`
108
+ Uint8Array [
109
+ 0,
110
+ 1,
111
+ 2,
112
+ 3,
113
+ 4,
114
+ 5,
115
+ 6,
116
+ 7,
117
+ 8,
118
+ 9,
119
+ 10,
120
+ 11,
121
+ 12,
122
+ 13,
123
+ 14,
124
+ 15,
125
+ 16,
126
+ 17,
127
+ 18,
128
+ 19,
129
+ 20,
130
+ 21,
131
+ 22,
132
+ 23,
133
+ 24,
134
+ 25,
135
+ 26,
136
+ 27,
137
+ 28,
138
+ 29,
139
+ 30,
140
+ 31,
141
+ ]
142
+ `)
143
+ expect(result.prf).not.toBe(output)
144
+ })
145
+
146
+ test('behavior: custom input', async () => {
147
+ let options: Types.CredentialCreationOptions | undefined
148
+ const customInput = input.slice()
149
+
150
+ await WebAuthn.createCredential({
151
+ createFn(options_) {
152
+ options = options_
153
+ return Promise.resolve(
154
+ registrationCredential({
155
+ output: new Uint8Array(32),
156
+ }),
157
+ )
158
+ },
159
+ name: 'Example',
160
+ prf: {
161
+ input: customInput,
162
+ },
163
+ })
164
+
165
+ const first = options?.publicKey?.extensions?.prf?.eval?.first
166
+ expect(first).toEqual(input)
167
+ expect(first).not.toBe(customInput)
168
+ expect(options?.publicKey?.authenticatorSelection).toMatchObject({
169
+ requireResidentKey: true,
170
+ residentKey: 'required',
171
+ userVerification: 'required',
172
+ })
173
+ })
174
+
175
+ test('behavior: without PRF', async () => {
176
+ let options: Types.CredentialCreationOptions | undefined
177
+ const credential = await WebAuthn.createCredential({
178
+ createFn(options_) {
179
+ options = options_
180
+ return Promise.resolve(registrationCredential({}))
181
+ },
182
+ name: 'Example',
183
+ })
184
+
185
+ expect(credential.id).toMatchInlineSnapshot(`"AQID"`)
186
+ expect(options?.publicKey?.extensions).toBeUndefined()
187
+ })
188
+
189
+ test('behavior: follow-up assertion', async () => {
190
+ let creationOptions: Types.CredentialCreationOptions | undefined
191
+ let options: Types.CredentialRequestOptions | undefined
192
+ const output = Uint8Array.from({ length: 32 }, (_, index) => index + 96)
193
+
194
+ const result = await WebAuthn.createCredential({
195
+ challenge,
196
+ createFn(options_) {
197
+ creationOptions = options_
198
+ return Promise.resolve(
199
+ registrationCredential({
200
+ enabled: true,
201
+ }),
202
+ )
203
+ },
204
+ getFn(options_) {
205
+ options = options_
206
+ return Promise.resolve(authenticationCredential({ output }))
207
+ },
208
+ name: 'Example',
209
+ prf: {
210
+ input,
211
+ },
212
+ rp: {
213
+ id: 'wallet.example.com',
214
+ name: 'Example',
215
+ },
216
+ timeout: 30_000,
217
+ })
218
+
219
+ expect(Hex.fromBytes(result.prf)).toMatchInlineSnapshot(
220
+ `"0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f"`,
221
+ )
222
+ expect(options?.publicKey).toMatchObject({
223
+ allowCredentials: [
224
+ {
225
+ id: Uint8Array.from([1, 2, 3]),
226
+ type: 'public-key',
227
+ },
228
+ ],
229
+ extensions: {
230
+ prf: {
231
+ eval: {
232
+ first: input,
233
+ },
234
+ },
235
+ },
236
+ rpId: 'wallet.example.com',
237
+ timeout: 30_000,
238
+ userVerification: 'required',
239
+ })
240
+ expect(creationOptions?.publicKey?.timeout).toBe(30_000)
241
+ expect(options?.publicKey?.challenge).toHaveLength(32)
242
+ })
243
+
244
+ test('behavior: plain array output', async () => {
245
+ const output = Array.from({ length: 32 }, (_, index) => index)
246
+ const result = await WebAuthn.createCredential({
247
+ createFn() {
248
+ return Promise.resolve(registrationCredential({ output }))
249
+ },
250
+ name: 'Example',
251
+ prf: true,
252
+ })
253
+
254
+ expect(Hex.fromBytes(result.prf)).toMatchInlineSnapshot(
255
+ `"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"`,
256
+ )
257
+ })
258
+
259
+ test('behavior: cross-realm ArrayBuffer output', async () => {
260
+ const output = runInNewContext('new ArrayBuffer(32)') as ArrayBuffer
261
+ const result = await WebAuthn.createCredential({
262
+ createFn() {
263
+ return Promise.resolve(registrationCredential({ output }))
264
+ },
265
+ name: 'Example',
266
+ prf: true,
267
+ })
268
+
269
+ expect(output).not.toBeInstanceOf(ArrayBuffer)
270
+ expect(result.prf).toEqual(new Uint8Array(32))
271
+ })
272
+
273
+ test('error: unsupported credential is retained', async () => {
274
+ const promise = WebAuthn.createCredential({
275
+ createFn() {
276
+ return Promise.resolve(
277
+ registrationCredential({
278
+ enabled: false,
279
+ }),
280
+ )
281
+ },
282
+ name: 'Example',
283
+ prf: true,
284
+ })
285
+
286
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
287
+ `[WebAuthn.PrfNotSupportedError: The created credential does not support PRF evaluation.]`,
288
+ )
289
+ await expect(promise).rejects.toMatchObject({
290
+ credential: {
291
+ id: credentialId,
292
+ },
293
+ })
294
+ })
295
+
296
+ test('error: failed follow-up retains credential', async () => {
297
+ const promise = WebAuthn.createCredential({
298
+ createFn() {
299
+ return Promise.resolve(
300
+ registrationCredential({
301
+ enabled: true,
302
+ }),
303
+ )
304
+ },
305
+ getFn() {
306
+ return Promise.resolve(null)
307
+ },
308
+ name: 'Example',
309
+ prf: true,
310
+ })
311
+
312
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`
313
+ [WebAuthn.PrfEvaluationFailedError: Created a credential, but failed to evaluate its PRF.
314
+
315
+ Details: Failed to request credential.]
316
+ `)
317
+ await expect(promise).rejects.toMatchObject({
318
+ credential: {
319
+ id: credentialId,
320
+ },
321
+ })
322
+ })
323
+
324
+ test('error: follow-up credential mismatch', async () => {
325
+ const promise = WebAuthn.createCredential({
326
+ createFn() {
327
+ return Promise.resolve(
328
+ registrationCredential({
329
+ enabled: true,
330
+ }),
331
+ )
332
+ },
333
+ getFn() {
334
+ return Promise.resolve(
335
+ authenticationCredential({
336
+ id: 'different',
337
+ output: new Uint8Array(32),
338
+ }),
339
+ )
340
+ },
341
+ name: 'Example',
342
+ prf: true,
343
+ })
344
+
345
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`
346
+ [WebAuthn.PrfEvaluationFailedError: Created a credential, but failed to evaluate its PRF.
347
+
348
+ Details: Expected credential "AQID", but received "different".]
349
+ `)
350
+ await expect(promise).rejects.toMatchObject({
351
+ credential: {
352
+ id: credentialId,
353
+ },
354
+ })
355
+ })
356
+
357
+ test('error: invalid output size', async () => {
358
+ const promise = WebAuthn.createCredential({
359
+ createFn() {
360
+ return Promise.resolve(
361
+ registrationCredential({
362
+ output: new Uint8Array(31),
363
+ }),
364
+ )
365
+ },
366
+ name: 'Example',
367
+ prf: true,
368
+ })
369
+
370
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
371
+ `[WebAuthn.InvalidPrfOutputError: Expected a 32-byte PRF output, but received 31 bytes.]`,
372
+ )
373
+ await expect(promise).rejects.toMatchObject({
374
+ credential: {
375
+ id: credentialId,
376
+ },
377
+ })
378
+ })
379
+
380
+ test('error: extension result failure retains credential', async () => {
381
+ const promise = WebAuthn.createCredential({
382
+ createFn() {
383
+ return Promise.resolve(
384
+ registrationCredential({
385
+ error: new Error('extension result unavailable'),
386
+ }),
387
+ )
388
+ },
389
+ name: 'Example',
390
+ prf: true,
391
+ })
392
+
393
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`
394
+ [WebAuthn.PrfEvaluationFailedError: Created a credential, but failed to evaluate its PRF.
395
+
396
+ Details: extension result unavailable]
397
+ `)
398
+ await expect(promise).rejects.toMatchObject({
399
+ credential: {
400
+ id: credentialId,
401
+ },
402
+ })
403
+ })
404
+
405
+ test('error: invalid plain array output', async () => {
406
+ const output = Array.from({ length: 32 }, () => 0)
407
+ output[31] = 256
408
+
409
+ await expect(() =>
410
+ WebAuthn.createCredential({
411
+ createFn() {
412
+ return Promise.resolve(registrationCredential({ output }))
413
+ },
414
+ name: 'Example',
415
+ prf: true,
416
+ }),
417
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
418
+ `[WebAuthn.InvalidPrfOutputError: Expected a 32-byte PRF output.]`,
419
+ )
420
+ })
421
+
422
+ test('error: managed extension', async () => {
423
+ await expect(() =>
424
+ WebAuthn.createCredential({
425
+ extensions: {
426
+ prf: {
427
+ eval: {
428
+ first: input,
429
+ },
430
+ },
431
+ } as never,
432
+ name: 'Example',
433
+ prf: true,
434
+ }),
435
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
436
+ `[WebAuthn.InvalidExtensionError: The \`prf\` extension is managed by this function.]`,
437
+ )
438
+ })
439
+
440
+ test('error: raw credential options', async () => {
441
+ await expect(() =>
442
+ WebAuthn.createCredential({
443
+ name: 'Example',
444
+ prf: true,
445
+ publicKey: {},
446
+ } as never),
447
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
448
+ `[WebAuthn.InvalidOptionsError: The \`publicKey\` option cannot be combined with managed PRF evaluation.]`,
449
+ )
450
+ })
451
+ })
452
+
453
+ describe('getCredential', () => {
454
+ test('default', async () => {
455
+ let options: Types.CredentialRequestOptions | undefined
456
+ const output = Uint8Array.from({ length: 32 }, (_, index) => index + 128)
457
+
458
+ const result = await WebAuthn.getCredential({
459
+ challenge,
460
+ credentialId,
461
+ extensions: {
462
+ appid: 'https://example.com',
463
+ },
464
+ getFn(options_) {
465
+ options = options_
466
+ return Promise.resolve(authenticationCredential({ output }))
467
+ },
468
+ prf: true,
469
+ rpId: 'example.com',
470
+ })
471
+
472
+ expect({
473
+ id: result.id,
474
+ prf: Hex.fromBytes(result.prf),
475
+ }).toMatchInlineSnapshot(`
476
+ {
477
+ "id": "AQID",
478
+ "prf": "0x808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
479
+ }
480
+ `)
481
+ expect(options?.publicKey?.extensions).toMatchInlineSnapshot(`
482
+ {
483
+ "appid": "https://example.com",
484
+ "prf": {
485
+ "eval": {
486
+ "first": Uint8Array [
487
+ 111,
488
+ 120,
489
+ 46,
490
+ 119,
491
+ 101,
492
+ 98,
493
+ 97,
494
+ 117,
495
+ 116,
496
+ 104,
497
+ 110,
498
+ 46,
499
+ 112,
500
+ 114,
501
+ 102,
502
+ 46,
503
+ 118,
504
+ 49,
505
+ ],
506
+ },
507
+ },
508
+ }
509
+ `)
510
+ expect(options?.publicKey?.userVerification).toMatchInlineSnapshot(
511
+ `"required"`,
512
+ )
513
+ expect(result.prf).not.toBe(output)
514
+ })
515
+
516
+ test('behavior: custom input', async () => {
517
+ let options: Types.CredentialRequestOptions | undefined
518
+ const customInput = input.slice()
519
+
520
+ await WebAuthn.getCredential({
521
+ getFn(options_) {
522
+ options = options_
523
+ return Promise.resolve(
524
+ authenticationCredential({
525
+ output: new Uint8Array(32),
526
+ }),
527
+ )
528
+ },
529
+ prf: {
530
+ input: customInput,
531
+ },
532
+ })
533
+
534
+ const first = options?.publicKey?.extensions?.prf?.eval?.first
535
+ expect(first).toEqual(input)
536
+ expect(first).not.toBe(customInput)
537
+ })
538
+
539
+ test('behavior: does not parse the assertion signature', async () => {
540
+ const result = await WebAuthn.getCredential({
541
+ getFn() {
542
+ return Promise.resolve({
543
+ ...authenticationCredential({
544
+ output: new Uint8Array(32),
545
+ }),
546
+ response: {},
547
+ } as Types.PublicKeyCredential)
548
+ },
549
+ prf: true,
550
+ })
551
+
552
+ expect(result.id).toMatchInlineSnapshot(`"AQID"`)
553
+ })
554
+
555
+ test('error: failed credential request', async () => {
556
+ await expect(() =>
557
+ WebAuthn.getCredential({
558
+ getFn() {
559
+ return Promise.resolve(null)
560
+ },
561
+ prf: true,
562
+ }),
563
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
564
+ `[WebAuthn.GetCredentialFailedError: Failed to request credential.]`,
565
+ )
566
+ })
567
+
568
+ test('error: unavailable output retains response', async () => {
569
+ const promise = WebAuthn.getCredential({
570
+ getFn() {
571
+ return Promise.resolve(
572
+ authenticationCredential({
573
+ enabled: true,
574
+ }),
575
+ )
576
+ },
577
+ prf: true,
578
+ })
579
+
580
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
581
+ `[WebAuthn.PrfUnavailableError: The credential did not return a PRF output.]`,
582
+ )
583
+ await expect(promise).rejects.toMatchObject({
584
+ response: {
585
+ id: credentialId,
586
+ },
587
+ })
588
+ })
589
+
590
+ test('error: invalid output size', async () => {
591
+ const promise = WebAuthn.getCredential({
592
+ getFn() {
593
+ return Promise.resolve(
594
+ authenticationCredential({
595
+ output: new Uint8Array(33),
596
+ }),
597
+ )
598
+ },
599
+ prf: true,
600
+ })
601
+
602
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
603
+ `[WebAuthn.InvalidPrfOutputError: Expected a 32-byte PRF output, but received 33 bytes.]`,
604
+ )
605
+ await expect(promise).rejects.toMatchObject({
606
+ response: {
607
+ id: credentialId,
608
+ },
609
+ })
610
+ })
611
+
612
+ test('error: extension result failure retains response', async () => {
613
+ const promise = WebAuthn.getCredential({
614
+ getFn() {
615
+ return Promise.resolve(
616
+ authenticationCredential({
617
+ error: new Error('extension result unavailable'),
618
+ }),
619
+ )
620
+ },
621
+ prf: true,
622
+ })
623
+
624
+ await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`
625
+ [WebAuthn.PrfEvaluationFailedError: Failed to evaluate the credential PRF.
626
+
627
+ Details: extension result unavailable]
628
+ `)
629
+ await expect(promise).rejects.toMatchObject({
630
+ response: {
631
+ id: credentialId,
632
+ },
633
+ })
634
+ })
635
+
636
+ test('error: managed extension', async () => {
637
+ await expect(() =>
638
+ WebAuthn.getCredential({
639
+ extensions: {
640
+ prf: {
641
+ eval: {
642
+ first: input,
643
+ },
644
+ },
645
+ } as never,
646
+ prf: true,
647
+ }),
648
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
649
+ `[WebAuthn.InvalidExtensionError: The \`prf\` extension is managed by this function.]`,
650
+ )
651
+ })
652
+
653
+ test('error: raw credential options', async () => {
654
+ await expect(() =>
655
+ WebAuthn.getCredential({
656
+ prf: true,
657
+ publicKey: {},
658
+ } as never),
659
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
660
+ `[WebAuthn.InvalidOptionsError: The \`publicKey\` option cannot be combined with managed PRF evaluation.]`,
661
+ )
662
+ })
663
+ })
664
+
665
+ function authenticationCredential({
666
+ enabled,
667
+ error,
668
+ id = credentialId,
669
+ output,
670
+ }: {
671
+ enabled?: boolean | undefined
672
+ error?: Error | undefined
673
+ id?: string | undefined
674
+ output?: ArrayLike<number> | Types.BufferSource | undefined
675
+ }) {
676
+ const authenticatorData = new Uint8Array(37)
677
+ const clientDataJSON = Bytes.fromString(
678
+ '{"type":"webauthn.get","challenge":"AA","origin":"https://example.com"}',
679
+ )
680
+ const signature = Uint8Array.from([48, 6, 2, 1, 1, 2, 1, 1])
681
+ return {
682
+ authenticatorAttachment: null,
683
+ getClientExtensionResults() {
684
+ if (error) throw error
685
+ if (output)
686
+ return {
687
+ prf: {
688
+ enabled,
689
+ results: {
690
+ first: output,
691
+ },
692
+ },
693
+ }
694
+ return {
695
+ prf: {
696
+ enabled,
697
+ },
698
+ }
699
+ },
700
+ id,
701
+ rawId: new ArrayBuffer(0),
702
+ response: {
703
+ authenticatorData: toArrayBuffer(authenticatorData),
704
+ clientDataJSON: toArrayBuffer(clientDataJSON),
705
+ signature: toArrayBuffer(signature),
706
+ },
707
+ type: 'public-key',
708
+ } as Types.PublicKeyCredential
709
+ }
710
+
711
+ function registrationCredential({
712
+ enabled,
713
+ error,
714
+ output,
715
+ }: {
716
+ enabled?: boolean | undefined
717
+ error?: Error | undefined
718
+ output?: ArrayLike<number> | Types.BufferSource | undefined
719
+ }) {
720
+ return {
721
+ authenticatorAttachment: null,
722
+ getClientExtensionResults() {
723
+ if (error) throw error
724
+ if (output)
725
+ return {
726
+ prf: {
727
+ enabled,
728
+ results: {
729
+ first: output,
730
+ },
731
+ },
732
+ }
733
+ return {
734
+ prf: {
735
+ enabled,
736
+ },
737
+ }
738
+ },
739
+ id: credentialId,
740
+ rawId: new ArrayBuffer(0),
741
+ response: {
742
+ attestationObject: new ArrayBuffer(0),
743
+ clientDataJSON: new ArrayBuffer(0),
744
+ getAuthenticatorData() {
745
+ return new ArrayBuffer(0)
746
+ },
747
+ getPublicKey() {
748
+ return toArrayBuffer(publicKey)
749
+ },
750
+ getPublicKeyAlgorithm() {
751
+ return -7
752
+ },
753
+ getTransports() {
754
+ return []
755
+ },
756
+ },
757
+ type: 'public-key',
758
+ } as Types.PublicKeyCredential
759
+ }
760
+
761
+ function toArrayBuffer(bytes: Uint8Array) {
762
+ return bytes.buffer.slice(
763
+ bytes.byteOffset,
764
+ bytes.byteOffset + bytes.byteLength,
765
+ ) as ArrayBuffer
766
+ }