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
@@ -136,5 +136,14 @@ describe('Engine', () => {
136
136
  expectTypeOf<NonNullable<Engine.Hash['keccak256']>>().toEqualTypeOf<
137
137
  (input: Uint8Array) => Uint8Array
138
138
  >()
139
+ expectTypeOf<NonNullable<Engine.Hash['createKeccak256']>>().toEqualTypeOf<
140
+ () => Engine.HashState
141
+ >()
142
+ expectTypeOf<Engine.HashState>().toEqualTypeOf<{
143
+ clone(): Engine.HashState
144
+ destroy(): void
145
+ digestInto(output: Uint8Array): void
146
+ update(input: Uint8Array): void
147
+ }>()
139
148
  })
140
149
  })
@@ -266,7 +266,7 @@ describe('set', () => {
266
266
  ).toThrowErrorMatchingInlineSnapshot(`
267
267
  [Engine.UnknownPrimitiveError: \`keccak_256\` is not a valid primitive for the \`Hash\` slot.
268
268
 
269
- Valid primitives: blake3, hmacSha256, keccak256, ripemd160, sha256]
269
+ Valid primitives: blake3, createBlake3, createHmacSha256, createKeccak256, createRipemd160, createSha256, hmacSha256, keccak256, ripemd160, sha256]
270
270
  `)
271
271
  })
272
272
 
@@ -16,3 +16,26 @@ describe('blake3', () => {
16
16
  ).toEqualTypeOf<Hex.Hex>()
17
17
  })
18
18
  })
19
+
20
+ describe('incremental', () => {
21
+ test('factories', () => {
22
+ expectTypeOf(Hash.createBlake3()).toEqualTypeOf<Hash.Hasher>()
23
+ expectTypeOf(Hash.createHmac256('0x')).toEqualTypeOf<Hash.Hasher>()
24
+ expectTypeOf(Hash.createKeccak256()).toEqualTypeOf<Hash.Hasher>()
25
+ expectTypeOf(Hash.createRipemd160()).toEqualTypeOf<Hash.Hasher>()
26
+ expectTypeOf(Hash.createSha256()).toEqualTypeOf<Hash.Hasher>()
27
+ })
28
+
29
+ test('Hasher', () => {
30
+ const hash = Hash.createSha256()
31
+
32
+ expectTypeOf(hash.update('0x')).toEqualTypeOf<Hash.Hasher>()
33
+ expectTypeOf(hash.update(new Uint8Array())).toEqualTypeOf<Hash.Hasher>()
34
+ expectTypeOf(hash.clone()).toEqualTypeOf<Hash.Hasher>()
35
+ expectTypeOf(hash.digest()).toEqualTypeOf<Hex.Hex>()
36
+ expectTypeOf(hash.digest({ as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
37
+ expectTypeOf(hash.digest({ as: 'Bytes' })).toEqualTypeOf<Bytes.Bytes>()
38
+ expectTypeOf(hash.digestInto(new Uint8Array())).toEqualTypeOf<void>()
39
+ expectTypeOf(hash.destroy()).toEqualTypeOf<void>()
40
+ })
41
+ })
@@ -1,6 +1,151 @@
1
- import { Engine, Hash } from 'ox'
1
+ import { type Bytes, Engine, Hash, type Hex } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
 
4
+ type Factory = {
5
+ create(): Hash.Hasher
6
+ digest(value: Hex.Hex | Bytes.Bytes): Bytes.Bytes
7
+ digestSize: number
8
+ name: string
9
+ }
10
+
11
+ const factories = [
12
+ {
13
+ create: () => Hash.createBlake3(),
14
+ digest: (value) => Hash.blake3(value, { as: 'Bytes' }),
15
+ digestSize: 32,
16
+ name: 'createBlake3',
17
+ },
18
+ {
19
+ create: () => Hash.createHmac256('0x6b6579'),
20
+ digest: (value) => Hash.hmac256('0x6b6579', value, { as: 'Bytes' }),
21
+ digestSize: 32,
22
+ name: 'createHmac256',
23
+ },
24
+ {
25
+ create: () => Hash.createKeccak256(),
26
+ digest: (value) => Hash.keccak256(value, { as: 'Bytes' }),
27
+ digestSize: 32,
28
+ name: 'createKeccak256',
29
+ },
30
+ {
31
+ create: () => Hash.createRipemd160(),
32
+ digest: (value) => Hash.ripemd160(value, { as: 'Bytes' }),
33
+ digestSize: 20,
34
+ name: 'createRipemd160',
35
+ },
36
+ {
37
+ create: () => Hash.createSha256(),
38
+ digest: (value) => Hash.sha256(value, { as: 'Bytes' }),
39
+ digestSize: 32,
40
+ name: 'createSha256',
41
+ },
42
+ ] as const satisfies readonly Factory[]
43
+
44
+ describe('incremental', () => {
45
+ test.each(factories)(
46
+ '$name: chunked input matches the one-shot function',
47
+ ({ create, digest }) => {
48
+ const hash = create()
49
+ hash.update('0xdead').update(Uint8Array.of(0xbe, 0xef))
50
+ expect(hash.digest({ as: 'Bytes' })).toEqual(digest('0xdeadbeef'))
51
+ },
52
+ )
53
+
54
+ test.each(factories)(
55
+ '$name: empty input matches the one-shot function',
56
+ ({ create, digest }) => {
57
+ expect(create().digest({ as: 'Bytes' })).toEqual(digest('0x'))
58
+ },
59
+ )
60
+
61
+ test.each(factories)(
62
+ '$name: clone branches from the same prefix',
63
+ ({ create, digest }) => {
64
+ const first = create().update('0xdead')
65
+ const second = first.clone()
66
+
67
+ first.update('0xbeef')
68
+ second.update('0xcafe')
69
+
70
+ expect(first.digest({ as: 'Bytes' })).toEqual(digest('0xdeadbeef'))
71
+ expect(second.digest({ as: 'Bytes' })).toEqual(digest('0xdeadcafe'))
72
+ },
73
+ )
74
+
75
+ test.each(factories)(
76
+ '$name: digestInto validates bounds without consuming the state',
77
+ ({ create, digest, digestSize }) => {
78
+ const hash = create().update('0xdeadbeef')
79
+
80
+ expect(() =>
81
+ hash.digestInto(new Uint8Array(digestSize - 1)),
82
+ ).toThrowError(Hash.InvalidDigestSizeError)
83
+
84
+ const output = new Uint8Array(digestSize + 2).fill(0xff)
85
+ hash.digestInto(output)
86
+
87
+ expect(output.slice(0, digestSize)).toEqual(digest('0xdeadbeef'))
88
+ expect(output.slice(digestSize)).toEqual(Uint8Array.of(0xff, 0xff))
89
+ expect(() => hash.update('0x')).toThrowError(Hash.HasherDestroyedError)
90
+ },
91
+ )
92
+
93
+ test.each(factories)(
94
+ '$name: digest consumes the state and destroy is idempotent',
95
+ ({ create }) => {
96
+ const hash = create()
97
+ hash.digest()
98
+
99
+ expect(() => hash.clone()).toThrowError(Hash.HasherDestroyedError)
100
+ expect(() => hash.digest()).toThrowError(Hash.HasherDestroyedError)
101
+ expect(() => hash.digestInto(new Uint8Array(32))).toThrowError(
102
+ Hash.HasherDestroyedError,
103
+ )
104
+ expect(() => hash.update('0x')).toThrowError(Hash.HasherDestroyedError)
105
+ expect(() => {
106
+ hash.destroy()
107
+ hash.destroy()
108
+ }).not.toThrow()
109
+ },
110
+ )
111
+
112
+ test.each(factories)(
113
+ '$name: destroy consumes the state and is idempotent',
114
+ ({ create }) => {
115
+ const hash = create()
116
+ hash.destroy()
117
+ hash.destroy()
118
+
119
+ expect(() => hash.clone()).toThrowError(Hash.HasherDestroyedError)
120
+ expect(() => hash.digest()).toThrowError(Hash.HasherDestroyedError)
121
+ expect(() => hash.update('0x')).toThrowError(Hash.HasherDestroyedError)
122
+ },
123
+ )
124
+
125
+ test('behavior: independent states can be interleaved', () => {
126
+ const first = Hash.createSha256().update('0xaa')
127
+ const second = Hash.createSha256().update('0xbb')
128
+
129
+ first.update('0xcc')
130
+ second.update('0xdd')
131
+
132
+ expect(first.digest()).toEqual(Hash.sha256('0xaacc'))
133
+ expect(second.digest()).toEqual(Hash.sha256('0xbbdd'))
134
+ })
135
+
136
+ test('behavior: a state keeps the provider captured at creation', () => {
137
+ Engine.set({ Hash: { createSha256: () => sentinelState(1) } })
138
+ const first = Hash.createSha256()
139
+
140
+ Engine.set({ Hash: { createSha256: () => sentinelState(2) } })
141
+
142
+ expect(first.digest({ as: 'Bytes' })[0]).toMatchInlineSnapshot('1')
143
+ expect(
144
+ Hash.createSha256().digest({ as: 'Bytes' })[0],
145
+ ).toMatchInlineSnapshot('2')
146
+ })
147
+ })
148
+
4
149
  describe('blake3', () => {
5
150
  test('default', () => {
6
151
  expect(Hash.blake3('0x')).toMatchInlineSnapshot(
@@ -625,15 +770,48 @@ describe('validate', () => {
625
770
  })
626
771
  })
627
772
 
773
+ function sentinelState(byte: number): Engine.HashState {
774
+ let active = true
775
+
776
+ const assertActive = () => {
777
+ if (!active) throw new Error('destroyed')
778
+ }
779
+
780
+ return {
781
+ clone: () => {
782
+ assertActive()
783
+ return sentinelState(byte)
784
+ },
785
+ destroy: () => {
786
+ active = false
787
+ },
788
+ digestInto: (output) => {
789
+ assertActive()
790
+ active = false
791
+ output.fill(byte, 0, 32)
792
+ },
793
+ update: () => {
794
+ assertActive()
795
+ },
796
+ }
797
+ }
798
+
628
799
  test('exports', () => {
629
800
  expect(Object.keys(Hash)).toMatchInlineSnapshot(`
630
801
  [
802
+ "createBlake3",
803
+ "createHmac256",
804
+ "createKeccak256",
805
+ "createRipemd160",
806
+ "createSha256",
631
807
  "blake3",
632
808
  "keccak256",
633
809
  "hmac256",
634
810
  "ripemd160",
635
811
  "sha256",
636
812
  "validate",
813
+ "HasherDestroyedError",
814
+ "InvalidDigestSizeError",
637
815
  ]
638
816
  `)
639
817
  })
@@ -30,6 +30,8 @@ describe('assert', () => {
30
30
 
31
31
  describe('concat', () => {
32
32
  test('default', () => {
33
+ expect(Hex.concat()).toMatchInlineSnapshot(`"0x"`)
34
+ expect(Hex.concat('0x1234')).toMatchInlineSnapshot(`"0x1234"`)
33
35
  expect(Hex.concat('0x0', '0x1')).toBe('0x01')
34
36
  expect(Hex.concat('0x1', '0x69', '0x420')).toBe('0x169420')
35
37
  expect(Hex.concat('0x00000001', '0x00000069', '0x00000420')).toBe(
@@ -2,6 +2,24 @@ import { Bytes, type Hex, Rlp } from 'ox'
2
2
  import { describe, expectTypeOf, test } from 'vp/test'
3
3
  import type { RecursiveArray } from '../internal/types.js'
4
4
 
5
+ describe('Rlp.encodeTo', () => {
6
+ test('write', () => {
7
+ expectTypeOf(
8
+ Rlp.encodeTo('0x', (value) => {
9
+ expectTypeOf(value).toEqualTypeOf<Bytes.Bytes>()
10
+ }),
11
+ ).toEqualTypeOf<void>()
12
+ })
13
+
14
+ test('rejects asynchronous callbacks', () => {
15
+ Rlp.encodeTo(
16
+ '0x',
17
+ // @ts-expect-error RLP write callbacks are synchronous.
18
+ async () => {},
19
+ )
20
+ })
21
+ })
22
+
5
23
  describe('Rlp.to', () => {
6
24
  test('default', () => {
7
25
  expectTypeOf(Rlp.toHex('0x')).toEqualTypeOf<RecursiveArray<Hex.Hex>>()
@@ -10,6 +10,7 @@ test('exports', () => {
10
10
  "decodeRlpCursor",
11
11
  "readLength",
12
12
  "readList",
13
+ "encodeTo",
13
14
  "from",
14
15
  "fromBytes",
15
16
  "fromHex",
@@ -32,6 +33,114 @@ const generateList = (length: number) => {
32
33
  return bytes
33
34
  }
34
35
 
36
+ function encodeToBytes(value: Parameters<typeof Rlp.encodeTo>[0]) {
37
+ const chunks: Uint8Array[] = []
38
+ Rlp.encodeTo(value, (chunk) => {
39
+ chunks.push(chunk)
40
+ })
41
+ return Bytes.concat(...chunks)
42
+ }
43
+
44
+ describe('Rlp.encodeTo', () => {
45
+ test('matches `from` for nested Hex values', () => {
46
+ const values = [
47
+ '0x',
48
+ '0x00',
49
+ '0x7f',
50
+ '0x80',
51
+ '0x1',
52
+ '0xabc',
53
+ ['0x', '0x80', ['0x1', `0x${'ab'.repeat(56)}`], `0x${'cd'.repeat(256)}`],
54
+ ] as const
55
+
56
+ for (const value of values)
57
+ expect(encodeToBytes(value)).toEqual(Rlp.from(value, { as: 'Bytes' }))
58
+ })
59
+
60
+ test('matches `from` for nested byte values', () => {
61
+ const values = [
62
+ new Uint8Array(),
63
+ Uint8Array.of(0),
64
+ Uint8Array.of(0x7f),
65
+ Uint8Array.of(0x80),
66
+ [
67
+ new Uint8Array(),
68
+ Uint8Array.of(0x80),
69
+ [generateBytes(56), generateBytes(256)],
70
+ ],
71
+ ] as const
72
+
73
+ for (const value of values)
74
+ expect(encodeToBytes(value)).toEqual(Rlp.from(value, { as: 'Bytes' }))
75
+ })
76
+
77
+ test('does not mutate emitted chunks', () => {
78
+ const chunks: Uint8Array[] = []
79
+ const values: Hex.Hex[] = []
80
+
81
+ Rlp.encodeTo(['0x01', `0x${'ab'.repeat(16_386)}`, '0x80'], (chunk) => {
82
+ chunks.push(chunk)
83
+ values.push(Hex.fromBytes(chunk))
84
+ })
85
+
86
+ expect(chunks.map((chunk) => Hex.fromBytes(chunk))).toEqual(values)
87
+ })
88
+
89
+ test('passes byte leaves without copying', () => {
90
+ const leaf = generateBytes(64)
91
+ let found = false
92
+
93
+ Rlp.encodeTo(leaf, (chunk) => {
94
+ if (chunk === leaf) found = true
95
+ })
96
+
97
+ expect(found).toMatchInlineSnapshot(`true`)
98
+ })
99
+
100
+ test('validates every Hex leaf before writing', () => {
101
+ let writes = 0
102
+
103
+ expect(() =>
104
+ Rlp.encodeTo(['0x01', ['0x02', '0xzz' as Hex.Hex]], () => {
105
+ writes++
106
+ }),
107
+ ).toThrowErrorMatchingInlineSnapshot(
108
+ `[BaseError: Invalid hex string \`0xzz\`.]`,
109
+ )
110
+ expect(writes).toMatchInlineSnapshot(`0`)
111
+ })
112
+
113
+ test('propagates callback errors after partial output', () => {
114
+ const chunks: Uint8Array[] = []
115
+
116
+ expect(() =>
117
+ Rlp.encodeTo([Uint8Array.of(1), Uint8Array.of(2)], (chunk) => {
118
+ chunks.push(chunk)
119
+ if (chunks.length === 2) throw new Error('write failed')
120
+ }),
121
+ ).toThrowErrorMatchingInlineSnapshot(`[Error: write failed]`)
122
+ expect(chunks.map((chunk) => Hex.fromBytes(chunk))).toMatchInlineSnapshot(`
123
+ [
124
+ "0xc2",
125
+ "0x01",
126
+ ]
127
+ `)
128
+ })
129
+
130
+ test('snapshots list structure before writing', () => {
131
+ const value: Hex.Hex[] = ['0x01']
132
+ const expected = Rlp.from(value, { as: 'Bytes' })
133
+ const chunks: Uint8Array[] = []
134
+
135
+ Rlp.encodeTo(value, (chunk) => {
136
+ chunks.push(chunk)
137
+ if (chunks.length === 1) value.push('0x02')
138
+ })
139
+
140
+ expect(Bytes.concat(...chunks)).toEqual(expected)
141
+ })
142
+ })
143
+
35
144
  describe('Rlp.to', () => {
36
145
  test('no bytes', () => {
37
146
  const rlpHex = '0x'
@@ -0,0 +1,14 @@
1
+ import { type Bytes, type Hex, Secp256k1 } from 'ox'
2
+ import { expectTypeOf, test } from 'vp/test'
3
+
4
+ test('fromPrf', () => {
5
+ const prf =
6
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
7
+
8
+ expectTypeOf(Secp256k1.fromPrf(prf)).toEqualTypeOf<Hex.Hex>()
9
+ expectTypeOf(Secp256k1.fromPrf(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
10
+ expectTypeOf(Secp256k1.fromPrf(prf, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
11
+ expectTypeOf(
12
+ Secp256k1.fromPrf(prf, { as: 'Bytes' }),
13
+ ).toEqualTypeOf<Bytes.Bytes>()
14
+ })
@@ -1,4 +1,4 @@
1
- import { Address, Bytes, Hex, PublicKey, Secp256k1 } from 'ox'
1
+ import { Address, Bytes, Engine, Hex, PublicKey, Secp256k1 } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
  import { accounts } from '../../../test/constants/accounts.js'
4
4
 
@@ -123,6 +123,94 @@ describe('createKeyPair', () => {
123
123
  })
124
124
  })
125
125
 
126
+ describe('fromPrf', () => {
127
+ test.each([
128
+ {
129
+ privateKey:
130
+ '0xd23e6d2bbfdc109c1d4706bc3a78a32356682a4e6ef09c3bbeabf5460235de36',
131
+ prf: '0x0000000000000000000000000000000000000000000000000000000000000000',
132
+ },
133
+ {
134
+ privateKey:
135
+ '0xcf0eab798d92f274ee2b9b6f3397186cdd3a08b09c77b7313fb1abf529611bc5',
136
+ prf: '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
137
+ },
138
+ ] as const)('vector: $prf', ({ privateKey, prf }) => {
139
+ expect(Secp256k1.fromPrf(prf)).toBe(privateKey)
140
+ })
141
+
142
+ test('value: Bytes', () => {
143
+ const prf = Bytes.fromHex(
144
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
145
+ )
146
+ expect(Secp256k1.fromPrf(prf)).toMatchInlineSnapshot(
147
+ `"0xcf0eab798d92f274ee2b9b6f3397186cdd3a08b09c77b7313fb1abf529611bc5"`,
148
+ )
149
+ })
150
+
151
+ test('options: as', () => {
152
+ const privateKey = Secp256k1.fromPrf(
153
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
154
+ { as: 'Bytes' },
155
+ )
156
+ expect(privateKey).toEqual(
157
+ Bytes.fromHex(
158
+ '0xcf0eab798d92f274ee2b9b6f3397186cdd3a08b09c77b7313fb1abf529611bc5',
159
+ ),
160
+ )
161
+ })
162
+
163
+ test('behavior: output is a valid secp256k1 private key', () => {
164
+ const privateKey = Secp256k1.fromPrf(Bytes.random(32), { as: 'Bytes' })
165
+ expect(Secp256k1.noble.utils.isValidSecretKey(privateKey)).toBe(true)
166
+ })
167
+
168
+ test('behavior: skips invalid scalar candidates', () => {
169
+ const messages: Hex.Hex[] = []
170
+ const privateKey = Engine.with(
171
+ {
172
+ Hash: {
173
+ hmacSha256: (_key, message) => {
174
+ messages.push(Hex.fromBytes(message))
175
+ if (messages.length === 1) return new Uint8Array(32)
176
+ return Bytes.fromHex(
177
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
178
+ )
179
+ },
180
+ },
181
+ },
182
+ () =>
183
+ Secp256k1.fromPrf(
184
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
185
+ ),
186
+ )
187
+
188
+ expect(privateKey).toBe(
189
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
190
+ )
191
+ expect(messages).toMatchInlineSnapshot(`
192
+ [
193
+ "0x6f782e736563703235366b312e66726f6d5072662e763100000000",
194
+ "0x6f782e736563703235366b312e66726f6d5072662e763100000001",
195
+ ]
196
+ `)
197
+ })
198
+
199
+ test('error: PRF output is too short', () => {
200
+ expect(() => Secp256k1.fromPrf(new Uint8Array(31)))
201
+ .toThrowErrorMatchingInlineSnapshot(`
202
+ [Secp256k1.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 31 bytes.]
203
+ `)
204
+ })
205
+
206
+ test('error: PRF output is too long', () => {
207
+ expect(() => Secp256k1.fromPrf(new Uint8Array(33)))
208
+ .toThrowErrorMatchingInlineSnapshot(`
209
+ [Secp256k1.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 33 bytes.]
210
+ `)
211
+ })
212
+ })
213
+
126
214
  describe('getSharedSecret', () => {
127
215
  test('default', () => {
128
216
  const privateKeyA = accounts[0].privateKey
@@ -483,6 +571,7 @@ test('exports', () => {
483
571
  [
484
572
  "noble",
485
573
  "createKeyPair",
574
+ "fromPrf",
486
575
  "getPublicKey",
487
576
  "getSharedSecret",
488
577
  "randomPrivateKey",
@@ -490,6 +579,7 @@ test('exports', () => {
490
579
  "recoverPublicKey",
491
580
  "sign",
492
581
  "verify",
582
+ "InvalidPrfSizeError",
493
583
  ]
494
584
  `)
495
585
  })
@@ -0,0 +1,142 @@
1
+ import {
2
+ type Bytes,
3
+ type Hex,
4
+ Secp256k1,
5
+ WebAuthn,
6
+ type WebAuthnP256,
7
+ } from 'ox'
8
+ import { describe, expectTypeOf, test } from 'vp/test'
9
+
10
+ declare const input: Bytes.Bytes
11
+
12
+ describe('createCredential', () => {
13
+ test('behavior: returns required PRF output when requested', () => {
14
+ const infer = async () => {
15
+ const credential = await WebAuthn.createCredential({
16
+ name: 'Example',
17
+ prf: true,
18
+ })
19
+
20
+ expectTypeOf(credential.prf).toEqualTypeOf<Bytes.Bytes>()
21
+ expectTypeOf(Secp256k1.fromPrf(credential.prf)).toEqualTypeOf<Hex.Hex>()
22
+ }
23
+ expectTypeOf(infer).toBeFunction()
24
+ })
25
+
26
+ test('behavior: preserves ordinary credential return type', () => {
27
+ const infer = async () => {
28
+ const credential = await WebAuthn.createCredential({
29
+ name: 'Example',
30
+ })
31
+
32
+ expectTypeOf(credential).toEqualTypeOf<WebAuthnP256.P256Credential>()
33
+ // @ts-expect-error Ordinary credential creation has no PRF output.
34
+ void credential.prf
35
+ }
36
+ expectTypeOf(infer).toBeFunction()
37
+ })
38
+
39
+ test('behavior: accepts custom input', () => {
40
+ const infer = async () => {
41
+ const credential = await WebAuthn.createCredential({
42
+ name: 'Example',
43
+ prf: {
44
+ input,
45
+ },
46
+ })
47
+
48
+ expectTypeOf(credential.prf).toEqualTypeOf<Bytes.Bytes>()
49
+ }
50
+ expectTypeOf(infer).toBeFunction()
51
+ })
52
+
53
+ test('error: managed options', () => {
54
+ const managedExtension = () =>
55
+ // @ts-expect-error The PRF extension is managed by createCredential.
56
+ WebAuthn.createCredential({
57
+ extensions: {
58
+ prf: {
59
+ eval: {
60
+ first: input,
61
+ },
62
+ },
63
+ },
64
+ name: 'Example',
65
+ prf: true,
66
+ })
67
+
68
+ const userVerification = () =>
69
+ WebAuthn.createCredential({
70
+ authenticatorSelection: {
71
+ // @ts-expect-error User verification is always required.
72
+ userVerification: 'preferred',
73
+ },
74
+ name: 'Example',
75
+ prf: true,
76
+ })
77
+
78
+ const rawOptions = {
79
+ name: 'Example',
80
+ prf: true,
81
+ publicKey: {},
82
+ } as const
83
+ const publicKey = () =>
84
+ // @ts-expect-error Raw credential options cannot be combined with PRF.
85
+ WebAuthn.createCredential(rawOptions)
86
+
87
+ expectTypeOf(managedExtension).toBeFunction()
88
+ expectTypeOf(publicKey).toBeFunction()
89
+ expectTypeOf(userVerification).toBeFunction()
90
+ })
91
+ })
92
+
93
+ describe('getCredential', () => {
94
+ test('behavior: returns required PRF output', () => {
95
+ const infer = async () => {
96
+ const credential = await WebAuthn.getCredential({
97
+ prf: true,
98
+ })
99
+
100
+ expectTypeOf(credential.prf).toEqualTypeOf<Bytes.Bytes>()
101
+ expectTypeOf(credential.id).toEqualTypeOf<string>()
102
+ expectTypeOf(credential.raw).toEqualTypeOf<WebAuthn.PublicKeyCredential>()
103
+ // @ts-expect-error PRF requests do not parse the assertion signature.
104
+ void credential.signature
105
+ }
106
+ expectTypeOf(infer).toBeFunction()
107
+ })
108
+
109
+ test('error: managed options', () => {
110
+ const managedExtension = () =>
111
+ WebAuthn.getCredential({
112
+ extensions: {
113
+ // @ts-expect-error The PRF extension is managed by getCredential.
114
+ prf: {
115
+ eval: {
116
+ first: input,
117
+ },
118
+ },
119
+ },
120
+ prf: true,
121
+ })
122
+
123
+ const userVerification = () =>
124
+ WebAuthn.getCredential({
125
+ prf: true,
126
+ // @ts-expect-error User verification is always required.
127
+ userVerification: 'preferred',
128
+ })
129
+
130
+ const rawOptions = {
131
+ prf: true,
132
+ publicKey: {},
133
+ } as const
134
+ const publicKey = () =>
135
+ // @ts-expect-error Raw credential options cannot be combined with PRF.
136
+ WebAuthn.getCredential(rawOptions)
137
+
138
+ expectTypeOf(managedExtension).toBeFunction()
139
+ expectTypeOf(publicKey).toBeFunction()
140
+ expectTypeOf(userVerification).toBeFunction()
141
+ })
142
+ })