leviathan-crypto 2.1.0 → 3.0.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 (296) hide show
  1. package/CLAUDE.md +86 -443
  2. package/README.md +198 -65
  3. package/dist/aes/aes-cbc.d.ts +40 -0
  4. package/dist/aes/aes-cbc.js +158 -0
  5. package/dist/aes/aes-ctr.d.ts +50 -0
  6. package/dist/aes/aes-ctr.js +141 -0
  7. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  8. package/dist/aes/aes-gcm-siv.js +217 -0
  9. package/dist/aes/aes-gcm.d.ts +61 -0
  10. package/dist/aes/aes-gcm.js +226 -0
  11. package/dist/aes/cipher-suite.d.ts +21 -0
  12. package/dist/aes/cipher-suite.js +179 -0
  13. package/dist/aes/embedded.d.ts +1 -0
  14. package/dist/aes/embedded.js +26 -0
  15. package/dist/aes/generator.d.ts +14 -0
  16. package/dist/aes/generator.js +103 -0
  17. package/dist/aes/index.d.ts +58 -0
  18. package/dist/aes/index.js +125 -0
  19. package/dist/aes/ops.d.ts +60 -0
  20. package/dist/aes/ops.js +164 -0
  21. package/dist/aes/pool-worker.d.ts +1 -0
  22. package/dist/aes/pool-worker.js +92 -0
  23. package/dist/aes/types.d.ts +1 -0
  24. package/dist/aes/types.js +23 -0
  25. package/dist/aes.wasm +0 -0
  26. package/dist/blake3/embedded.d.ts +1 -0
  27. package/dist/blake3/embedded.js +26 -0
  28. package/dist/blake3/index.d.ts +143 -0
  29. package/dist/blake3/index.js +620 -0
  30. package/dist/blake3/types.d.ts +102 -0
  31. package/dist/blake3/types.js +31 -0
  32. package/dist/blake3/validate.d.ts +29 -0
  33. package/dist/blake3/validate.js +80 -0
  34. package/dist/blake3.wasm +0 -0
  35. package/dist/chacha20/cipher-suite.js +47 -25
  36. package/dist/chacha20/generator.d.ts +2 -2
  37. package/dist/chacha20/generator.js +4 -4
  38. package/dist/chacha20/index.d.ts +16 -15
  39. package/dist/chacha20/index.js +52 -46
  40. package/dist/chacha20/ops.d.ts +7 -7
  41. package/dist/chacha20/ops.js +34 -34
  42. package/dist/chacha20/pool-worker.js +5 -3
  43. package/dist/cte-wasm.d.ts +1 -0
  44. package/dist/cte-wasm.js +3 -0
  45. package/dist/curve25519.wasm +0 -0
  46. package/dist/ecdsa/der.d.ts +23 -0
  47. package/dist/ecdsa/der.js +192 -0
  48. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  49. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  50. package/dist/ecdsa/embedded.d.ts +1 -0
  51. package/dist/ecdsa/embedded.js +25 -0
  52. package/dist/ecdsa/index.d.ts +124 -0
  53. package/dist/ecdsa/index.js +366 -0
  54. package/dist/ecdsa/types.d.ts +31 -0
  55. package/dist/ecdsa/types.js +28 -0
  56. package/dist/ecdsa/validate.d.ts +18 -0
  57. package/dist/ecdsa/validate.js +92 -0
  58. package/dist/ed25519/embedded.d.ts +1 -0
  59. package/dist/ed25519/embedded.js +31 -0
  60. package/dist/ed25519/index.d.ts +70 -0
  61. package/dist/ed25519/index.js +308 -0
  62. package/dist/ed25519/types.d.ts +27 -0
  63. package/dist/ed25519/types.js +27 -0
  64. package/dist/ed25519/validate.d.ts +7 -0
  65. package/dist/ed25519/validate.js +77 -0
  66. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  67. package/dist/embedded/aes-pool-worker.js +5 -0
  68. package/dist/embedded/aes.d.ts +1 -0
  69. package/dist/embedded/aes.js +3 -0
  70. package/dist/embedded/blake3.d.ts +1 -0
  71. package/dist/embedded/blake3.js +3 -0
  72. package/dist/embedded/chacha20-pool-worker.d.ts +1 -1
  73. package/dist/embedded/chacha20-pool-worker.js +2 -2
  74. package/dist/embedded/chacha20.d.ts +1 -1
  75. package/dist/embedded/chacha20.js +2 -2
  76. package/dist/embedded/curve25519.d.ts +1 -0
  77. package/dist/embedded/curve25519.js +3 -0
  78. package/dist/embedded/mldsa.d.ts +1 -0
  79. package/dist/embedded/mldsa.js +3 -0
  80. package/dist/embedded/mlkem.d.ts +1 -0
  81. package/dist/embedded/mlkem.js +3 -0
  82. package/dist/embedded/p256.d.ts +1 -0
  83. package/dist/embedded/p256.js +3 -0
  84. package/dist/embedded/serpent-pool-worker.d.ts +1 -1
  85. package/dist/embedded/serpent-pool-worker.js +2 -2
  86. package/dist/embedded/serpent.d.ts +1 -1
  87. package/dist/embedded/serpent.js +2 -2
  88. package/dist/embedded/sha2.d.ts +1 -1
  89. package/dist/embedded/sha2.js +2 -2
  90. package/dist/embedded/sha3.d.ts +1 -1
  91. package/dist/embedded/sha3.js +2 -2
  92. package/dist/embedded/slhdsa.d.ts +1 -0
  93. package/dist/embedded/slhdsa.js +3 -0
  94. package/dist/errors.d.ts +92 -1
  95. package/dist/errors.js +111 -1
  96. package/dist/fortuna.d.ts +5 -5
  97. package/dist/fortuna.js +37 -64
  98. package/dist/index.d.ts +38 -9
  99. package/dist/index.js +63 -19
  100. package/dist/init.d.ts +1 -1
  101. package/dist/init.js +11 -25
  102. package/dist/keccak/embedded.js +1 -1
  103. package/dist/keccak/index.d.ts +2 -0
  104. package/dist/keccak/index.js +4 -2
  105. package/dist/loader.d.ts +1 -24
  106. package/dist/loader.js +13 -16
  107. package/dist/merkle/blake3-tree.d.ts +35 -0
  108. package/dist/merkle/blake3-tree.js +187 -0
  109. package/dist/merkle/checkpoint.d.ts +58 -0
  110. package/dist/merkle/checkpoint.js +217 -0
  111. package/dist/merkle/index.d.ts +19 -0
  112. package/dist/merkle/index.js +37 -0
  113. package/dist/merkle/merkle-log.d.ts +130 -0
  114. package/dist/merkle/merkle-log.js +207 -0
  115. package/dist/merkle/merkle-verifier.d.ts +126 -0
  116. package/dist/merkle/merkle-verifier.js +296 -0
  117. package/dist/merkle/proof.d.ts +70 -0
  118. package/dist/merkle/proof.js +300 -0
  119. package/dist/merkle/sha256-tree.d.ts +33 -0
  120. package/dist/merkle/sha256-tree.js +145 -0
  121. package/dist/merkle/signed-log.d.ts +156 -0
  122. package/dist/merkle/signed-log.js +356 -0
  123. package/dist/merkle/signed-note.d.ts +309 -0
  124. package/dist/merkle/signed-note.js +648 -0
  125. package/dist/merkle/sth.d.ts +31 -0
  126. package/dist/merkle/sth.js +31 -0
  127. package/dist/merkle/storage.d.ts +40 -0
  128. package/dist/merkle/storage.js +71 -0
  129. package/dist/merkle/tree.d.ts +68 -0
  130. package/dist/merkle/tree.js +94 -0
  131. package/dist/mldsa/embedded.d.ts +1 -0
  132. package/dist/{kyber → mldsa}/embedded.js +5 -5
  133. package/dist/mldsa/expand.d.ts +53 -0
  134. package/dist/mldsa/expand.js +188 -0
  135. package/dist/mldsa/format.d.ts +16 -0
  136. package/dist/mldsa/format.js +68 -0
  137. package/dist/mldsa/hashvariant.d.ts +32 -0
  138. package/dist/mldsa/hashvariant.js +248 -0
  139. package/dist/mldsa/index.d.ts +142 -0
  140. package/dist/mldsa/index.js +463 -0
  141. package/dist/mldsa/keygen.d.ts +16 -0
  142. package/dist/mldsa/keygen.js +232 -0
  143. package/dist/mldsa/params.d.ts +21 -0
  144. package/dist/mldsa/params.js +55 -0
  145. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  146. package/dist/mldsa/sha3-helpers.js +124 -0
  147. package/dist/mldsa/sign.d.ts +36 -0
  148. package/dist/mldsa/sign.js +380 -0
  149. package/dist/mldsa/types.d.ts +91 -0
  150. package/dist/mldsa/types.js +25 -0
  151. package/dist/mldsa/validate.d.ts +55 -0
  152. package/dist/mldsa/validate.js +125 -0
  153. package/dist/mldsa/verify.d.ts +29 -0
  154. package/dist/mldsa/verify.js +269 -0
  155. package/dist/mldsa.wasm +0 -0
  156. package/dist/mlkem/embedded.d.ts +1 -0
  157. package/dist/mlkem/embedded.js +27 -0
  158. package/dist/mlkem/indcpa.d.ts +49 -0
  159. package/dist/{kyber → mlkem}/indcpa.js +44 -44
  160. package/dist/mlkem/index.d.ts +37 -0
  161. package/dist/{kyber → mlkem}/index.js +24 -34
  162. package/dist/mlkem/kem.d.ts +21 -0
  163. package/dist/{kyber → mlkem}/kem.js +44 -64
  164. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  165. package/dist/{kyber → mlkem}/params.js +2 -2
  166. package/dist/mlkem/suite.d.ts +12 -0
  167. package/dist/{kyber → mlkem}/suite.js +17 -12
  168. package/dist/{kyber → mlkem}/types.d.ts +3 -3
  169. package/dist/{kyber → mlkem}/types.js +1 -1
  170. package/dist/{kyber → mlkem}/validate.d.ts +7 -7
  171. package/dist/{kyber → mlkem}/validate.js +7 -7
  172. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  173. package/dist/p256.wasm +0 -0
  174. package/dist/ratchet/index.d.ts +2 -0
  175. package/dist/ratchet/index.js +1 -0
  176. package/dist/ratchet/kdf-chain.js +3 -3
  177. package/dist/ratchet/ratchet-keypair.js +2 -2
  178. package/dist/ratchet/root-kdf.js +7 -7
  179. package/dist/ratchet/skipped-key-store.js +4 -4
  180. package/dist/ratchet/types.d.ts +1 -1
  181. package/dist/serpent/cipher-suite.js +20 -17
  182. package/dist/serpent/generator.d.ts +1 -1
  183. package/dist/serpent/generator.js +2 -2
  184. package/dist/serpent/index.d.ts +8 -7
  185. package/dist/serpent/index.js +18 -27
  186. package/dist/serpent/pool-worker.js +7 -5
  187. package/dist/serpent/serpent-cbc.d.ts +4 -4
  188. package/dist/serpent/serpent-cbc.js +11 -8
  189. package/dist/serpent/shared-ops.d.ts +3 -23
  190. package/dist/serpent/shared-ops.js +50 -85
  191. package/dist/serpent.wasm +0 -0
  192. package/dist/sha2/hkdf.js +5 -5
  193. package/dist/sha2/index.d.ts +21 -1
  194. package/dist/sha2/index.js +65 -10
  195. package/dist/sha2/types.d.ts +41 -2
  196. package/dist/sha2.wasm +0 -0
  197. package/dist/sha3/index.d.ts +72 -3
  198. package/dist/sha3/index.js +240 -14
  199. package/dist/sha3/kmac.d.ts +121 -0
  200. package/dist/sha3/kmac.js +800 -0
  201. package/dist/sha3.wasm +0 -0
  202. package/dist/shared/pkcs7.d.ts +22 -0
  203. package/dist/shared/pkcs7.js +84 -0
  204. package/dist/sign/ctx.d.ts +41 -0
  205. package/dist/sign/ctx.js +102 -0
  206. package/dist/sign/envelope.d.ts +45 -0
  207. package/dist/sign/envelope.js +152 -0
  208. package/dist/sign/hasher.d.ts +9 -0
  209. package/dist/sign/hasher.js +132 -0
  210. package/dist/sign/index.d.ts +11 -0
  211. package/dist/sign/index.js +34 -0
  212. package/dist/sign/sign-stream.d.ts +25 -0
  213. package/dist/sign/sign-stream.js +112 -0
  214. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  215. package/dist/sign/suites/ecdsa-p256.js +120 -0
  216. package/dist/sign/suites/ed25519.d.ts +3 -0
  217. package/dist/sign/suites/ed25519.js +165 -0
  218. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  219. package/dist/sign/suites/hybrid-classical.js +526 -0
  220. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  221. package/dist/sign/suites/hybrid-pq.js +234 -0
  222. package/dist/sign/suites/mldsa.d.ts +7 -0
  223. package/dist/sign/suites/mldsa.js +161 -0
  224. package/dist/sign/suites/slhdsa.d.ts +7 -0
  225. package/dist/sign/suites/slhdsa.js +176 -0
  226. package/dist/sign/types.d.ts +106 -0
  227. package/dist/sign/types.js +28 -0
  228. package/dist/sign/verify-stream.d.ts +30 -0
  229. package/dist/sign/verify-stream.js +227 -0
  230. package/dist/slhdsa/embedded.d.ts +1 -0
  231. package/dist/slhdsa/embedded.js +26 -0
  232. package/dist/slhdsa/index.d.ts +149 -0
  233. package/dist/slhdsa/index.js +493 -0
  234. package/dist/slhdsa/params.d.ts +26 -0
  235. package/dist/slhdsa/params.js +70 -0
  236. package/dist/slhdsa/prehash.d.ts +68 -0
  237. package/dist/slhdsa/prehash.js +307 -0
  238. package/dist/slhdsa/sign.d.ts +39 -0
  239. package/dist/slhdsa/sign.js +116 -0
  240. package/dist/slhdsa/types.d.ts +129 -0
  241. package/dist/slhdsa/types.js +27 -0
  242. package/dist/slhdsa/validate.d.ts +60 -0
  243. package/dist/slhdsa/validate.js +127 -0
  244. package/dist/slhdsa/verify.d.ts +32 -0
  245. package/dist/slhdsa/verify.js +107 -0
  246. package/dist/slhdsa.wasm +0 -0
  247. package/dist/stream/header.js +3 -3
  248. package/dist/stream/index.d.ts +1 -0
  249. package/dist/stream/index.js +1 -0
  250. package/dist/stream/open-stream.js +31 -10
  251. package/dist/stream/seal-stream-pool.d.ts +1 -0
  252. package/dist/stream/seal-stream-pool.js +63 -26
  253. package/dist/stream/seal-stream.d.ts +1 -1
  254. package/dist/stream/seal-stream.js +20 -9
  255. package/dist/stream/seal.js +6 -6
  256. package/dist/stream/types.d.ts +3 -1
  257. package/dist/stream/types.js +1 -1
  258. package/dist/types.d.ts +1 -1
  259. package/dist/types.js +1 -1
  260. package/dist/utils.d.ts +3 -3
  261. package/dist/utils.js +46 -54
  262. package/dist/wasm-source.d.ts +7 -7
  263. package/dist/wasm-source.js +1 -1
  264. package/dist/x25519/embedded.d.ts +1 -0
  265. package/dist/x25519/embedded.js +31 -0
  266. package/dist/x25519/index.d.ts +43 -0
  267. package/dist/x25519/index.js +159 -0
  268. package/dist/x25519/types.d.ts +25 -0
  269. package/dist/x25519/types.js +27 -0
  270. package/dist/x25519/validate.d.ts +2 -0
  271. package/dist/x25519/validate.js +39 -0
  272. package/package.json +70 -26
  273. package/SECURITY.md +0 -163
  274. package/dist/ct-wasm.d.ts +0 -1
  275. package/dist/ct-wasm.js +0 -3
  276. package/dist/docs/aead.md +0 -363
  277. package/dist/docs/architecture.md +0 -1011
  278. package/dist/docs/argon2id.md +0 -305
  279. package/dist/docs/chacha20.md +0 -781
  280. package/dist/docs/exports.md +0 -277
  281. package/dist/docs/fortuna.md +0 -530
  282. package/dist/docs/init.md +0 -301
  283. package/dist/docs/loader.md +0 -256
  284. package/dist/docs/serpent.md +0 -617
  285. package/dist/docs/sha2.md +0 -671
  286. package/dist/docs/sha3.md +0 -612
  287. package/dist/docs/types.md +0 -416
  288. package/dist/docs/utils.md +0 -457
  289. package/dist/embedded/kyber.d.ts +0 -1
  290. package/dist/embedded/kyber.js +0 -3
  291. package/dist/kyber/embedded.d.ts +0 -1
  292. package/dist/kyber/indcpa.d.ts +0 -49
  293. package/dist/kyber/index.d.ts +0 -38
  294. package/dist/kyber/kem.d.ts +0 -21
  295. package/dist/kyber/suite.d.ts +0 -12
  296. /package/dist/{ct.wasm → cte.wasm} +0 -0
package/dist/loader.js CHANGED
@@ -13,12 +13,13 @@ function toArrayBuffer(bytes) {
13
13
  * Decode a gzip+base64 embedded WASM string to raw bytes.
14
14
  * Guards against missing DecompressionStream (Node <18, non-browser runtimes).
15
15
  * Exported for pool worker launchers that decode blobs before spawning threads.
16
+ * @internal
16
17
  */
17
18
  export async function decodeWasm(b64) {
18
19
  if (typeof DecompressionStream === 'undefined')
19
- throw new Error('leviathan-crypto: DecompressionStream not available '
20
+ throw new Error('leviathan-crypto: DecompressionStream not available, '
20
21
  + 'use a URL, ArrayBuffer, or WebAssembly.Module source in this runtime');
21
- // _b64 throws RangeError on invalid base64 no nullish check required.
22
+ // _b64 throws RangeError on invalid base64, no nullish check required.
22
23
  const compressed = _b64(b64);
23
24
  const ds = new DecompressionStream('gzip');
24
25
  const writer = ds.writable.getWriter();
@@ -39,10 +40,7 @@ export async function decodeWasm(b64) {
39
40
  }
40
41
  return out;
41
42
  }
42
- // Max thenable nesting depth. A caller can pass `Promise<Response>` or even
43
- // `Promise<Promise<Response>>` (e.g. deferred fetch wrapped in another async
44
- // layer), but arbitrary `Promise<Promise<Promise<...>>>` chains would indicate
45
- // a caller bug — cap at 3 levels and throw a clear error beyond that.
43
+ // Cap thenable-source nesting at 3 to prevent runaway recursion.
46
44
  const MAX_THENABLE_DEPTH = 3;
47
45
  /**
48
46
  * Compile a WASM source to a Module without instantiating.
@@ -51,13 +49,14 @@ const MAX_THENABLE_DEPTH = 3;
51
49
  * Thenable sources (Promise<Response>, Promise<ArrayBuffer>, etc.) are
52
50
  * resolved and then re-dispatched by the runtime type of the resolved value.
53
51
  * Depth is capped at `MAX_THENABLE_DEPTH` to prevent runaway recursion.
52
+ * @internal
54
53
  */
55
- export async function compileWasm(source, _depth = 0) {
56
- if (_depth > MAX_THENABLE_DEPTH)
54
+ export async function compileWasm(source, depth = 0) {
55
+ if (depth > MAX_THENABLE_DEPTH)
57
56
  throw new TypeError(`leviathan-crypto: thenable nesting too deep (max ${MAX_THENABLE_DEPTH})`);
58
57
  if (typeof source === 'string') {
59
58
  if (source.length === 0)
60
- throw new TypeError('leviathan-crypto: invalid WasmSource empty string');
59
+ throw new TypeError('leviathan-crypto: invalid WasmSource, empty string');
61
60
  return WebAssembly.compile(toArrayBuffer(await decodeWasm(source)));
62
61
  }
63
62
  if (source instanceof URL)
@@ -72,22 +71,20 @@ export async function compileWasm(source, _depth = 0) {
72
71
  return WebAssembly.compileStreaming(source);
73
72
  if (source != null && typeof source.then === 'function') {
74
73
  const resolved = await source;
75
- return compileWasm(resolved, _depth + 1);
74
+ return compileWasm(resolved, depth + 1);
76
75
  }
77
- throw new TypeError(`leviathan-crypto: invalid WasmSource got ${source === null ? 'null' : typeof source}`);
76
+ throw new TypeError(`leviathan-crypto: invalid WasmSource, got ${source === null ? 'null' : typeof source}`);
78
77
  }
79
78
  /**
80
79
  * Load a WASM module from any accepted source type.
81
- * The loading strategy is inferred from the argument type no mode string.
80
+ * The loading strategy is inferred from the argument type, no mode string.
82
81
  *
83
82
  * Throws `TypeError` for null, numeric, or unrecognised inputs, or if a
84
83
  * thenable source nests deeper than `MAX_THENABLE_DEPTH`.
84
+ * @internal
85
85
  */
86
86
  export async function loadWasm(source) {
87
- // All leviathan-crypto WASM modules export their own memory and import
88
- // nothing from the host. If a future module needs imports, they would be
89
- // computed and passed here.
90
- // compileWasm already handles thenable resolution + depth capping.
87
+ // All modules export their own memory; no host imports today.
91
88
  const mod = await compileWasm(source);
92
89
  return WebAssembly.instantiate(mod);
93
90
  }
@@ -0,0 +1,35 @@
1
+ import type { Hasher, MerkleTree } from './tree.js';
2
+ import type { MerkleStorage } from './storage.js';
3
+ /**
4
+ * BLAKE3-native `Hasher` (BLAKE3 §2.3, §2.4, §2.5).
5
+ *
6
+ * Empty-tree value is `BLAKE3()`; leaves are `BLAKE3(leaf)`; internal
7
+ * nodes are the §2.5 parent compress over the two child CVs with the
8
+ * BLAKE3 IV as the starting CV and `modeFlags = 0`, `isRoot = 0`.
9
+ *
10
+ * Stateless and reentrant: each method acquires the blake3 module
11
+ * fresh, runs the operation, and releases. No `dispose()` is needed.
12
+ */
13
+ export declare const Blake3Hasher: Hasher;
14
+ /**
15
+ * Stateful BLAKE3 Merkle log. Same surface and storage discipline as
16
+ * `Sha256Tree`: leaf hashes and every perfect aligned internal subtree
17
+ * hash live in the injected `MerkleStorage`; partial right-edge subtrees
18
+ * are recomputed on demand.
19
+ *
20
+ * `append` is the only mutator and is the leaf-hash factory; consumers
21
+ * feed leaf bytes, not pre-computed leaf hashes.
22
+ */
23
+ export declare class Blake3Tree implements MerkleTree {
24
+ readonly hasher: Hasher;
25
+ private readonly storage;
26
+ constructor(storage: MerkleStorage);
27
+ size(): number;
28
+ rootHash(): Uint8Array;
29
+ append(leafBytes: Uint8Array): {
30
+ leafIndex: number;
31
+ leafHash: Uint8Array;
32
+ };
33
+ getInclusionProof(leafIndex: number, treeSize?: number): Uint8Array[];
34
+ getConsistencyProof(oldSize: number, newSize: number): Uint8Array[];
35
+ }
@@ -0,0 +1,187 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/merkle/blake3-tree.ts
23
+ //
24
+ // BLAKE3 Hasher / MerkleTree. Domain separation comes from BLAKE3's own
25
+ // §2.4 CHUNK_START / CHUNK_END / ROOT and §2.5 PARENT flags, not from
26
+ // RFC 6962-style 0x00 / 0x01 prefix bytes (those would be redundant and
27
+ // discard `compress4` parallelism at the internal-node layer).
28
+ //
29
+ // Composition:
30
+ // hashEmpty() = BLAKE3() §2.5
31
+ // hashLeaf(leaf) = BLAKE3(leaf) §2.4
32
+ // hashInternal(left, right) = _testParentCV(left, right, IV, 0, 0) §2.5
33
+ //
34
+ // Parent compress runs with modeFlags = 0 and isRoot = 0 at every level.
35
+ // The root flag is the SignedLog layer's concern; the tree's top hash
36
+ // exits as a plain CV, keeping `hashInternal` symmetric.
37
+ import { BLAKE3 } from '../blake3/index.js';
38
+ import { getInstance } from '../init.js';
39
+ import { buildConsistencyProof, buildInclusionProof, subtreeHash, } from './proof.js';
40
+ function getBlake3Exports() {
41
+ return getInstance('blake3').exports;
42
+ }
43
+ // ── Scratch layout for `_testParentCV` ──────────────────────────────────────
44
+ //
45
+ // Second-page offsets (past BUFFER_END = 26328 from
46
+ // `src/asm/blake3/buffers.ts`) are untouched by the §2.4 chunk pipeline
47
+ // and §2.5 tree-assembly queues; safe for caller-supplied scratch.
48
+ const PARENT_LEFT_OFF = 65536;
49
+ const PARENT_RIGHT_OFF = PARENT_LEFT_OFF + 32;
50
+ const PARENT_START_OFF = PARENT_RIGHT_OFF + 32;
51
+ const PARENT_OUT_OFF = PARENT_START_OFF + 32;
52
+ const PARENT_SCRATCH_LEN = 128;
53
+ // BLAKE3 §2.2 Table 1: the BLAKE3 IV equals the FIPS 180-4 SHA-256 IV,
54
+ // packed as eight u32 little-endian words. The IV is the starting CV
55
+ // for default-mode parent compresses (BLAKE3 §2.5, Tree Mode).
56
+ const BLAKE3_IV_BYTES = (() => {
57
+ const iv32 = new Uint32Array([
58
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
59
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
60
+ ]);
61
+ return new Uint8Array(iv32.buffer);
62
+ })();
63
+ const BLAKE3_OUTPUT = 32;
64
+ const BLAKE3_WASM_MODULES = Object.freeze(['blake3']);
65
+ // ── Blake3Hasher const ──────────────────────────────────────────────────────
66
+ /**
67
+ * BLAKE3-native `Hasher` (BLAKE3 §2.3, §2.4, §2.5).
68
+ *
69
+ * Empty-tree value is `BLAKE3()`; leaves are `BLAKE3(leaf)`; internal
70
+ * nodes are the §2.5 parent compress over the two child CVs with the
71
+ * BLAKE3 IV as the starting CV and `modeFlags = 0`, `isRoot = 0`.
72
+ *
73
+ * Stateless and reentrant: each method acquires the blake3 module
74
+ * fresh, runs the operation, and releases. No `dispose()` is needed.
75
+ */
76
+ export const Blake3Hasher = Object.freeze({
77
+ name: 'blake3',
78
+ outputSize: BLAKE3_OUTPUT,
79
+ wasmModules: BLAKE3_WASM_MODULES,
80
+ hashEmpty() {
81
+ // BLAKE3 §2.5, Tree Mode: the natural tree-mode root for an
82
+ // empty input is `BLAKE3()` itself. The chunk machine handles
83
+ // the single empty chunk (CHUNK_START | CHUNK_END | ROOT)
84
+ // internally and returns the 32-byte XOF prefix.
85
+ const h = new BLAKE3();
86
+ try {
87
+ return h.hash(new Uint8Array(0));
88
+ }
89
+ finally {
90
+ h.dispose();
91
+ }
92
+ },
93
+ hashLeaf(leaf) {
94
+ // BLAKE3 §2.4, Chunks: the chunk pipeline applies CHUNK_START,
95
+ // CHUNK_END, and ROOT flags internally. The caller sees a plain
96
+ // 32-byte hash; leaf-vs-internal domain separation is BLAKE3's
97
+ // job through these flag bytes, not the caller's job through
98
+ // prefix bytes.
99
+ const h = new BLAKE3();
100
+ try {
101
+ return h.hash(leaf);
102
+ }
103
+ finally {
104
+ h.dispose();
105
+ }
106
+ },
107
+ hashInternal(left, right) {
108
+ if (left.length !== BLAKE3_OUTPUT)
109
+ throw new RangeError(`Blake3Hasher.hashInternal: left must be ${BLAKE3_OUTPUT} bytes, got ${left.length}`);
110
+ if (right.length !== BLAKE3_OUTPUT)
111
+ throw new RangeError(`Blake3Hasher.hashInternal: right must be ${BLAKE3_OUTPUT} bytes, got ${right.length}`);
112
+ // BLAKE3 §2.5, Tree Mode: parent compress over (left || right)
113
+ // with IV as the starting CV and PARENT as the only flag bit
114
+ // (modeFlags = 0 selects default mode; isRoot = 0 keeps the
115
+ // node generic so callers can stack identical compresses up
116
+ // the tree).
117
+ const x = getBlake3Exports();
118
+ const mem = new Uint8Array(x.memory.buffer);
119
+ mem.set(left, PARENT_LEFT_OFF);
120
+ mem.set(right, PARENT_RIGHT_OFF);
121
+ mem.set(BLAKE3_IV_BYTES, PARENT_START_OFF);
122
+ try {
123
+ x._testParentCV(PARENT_LEFT_OFF, PARENT_RIGHT_OFF, PARENT_START_OFF, 0, 0, PARENT_OUT_OFF);
124
+ return mem.slice(PARENT_OUT_OFF, PARENT_OUT_OFF + BLAKE3_OUTPUT);
125
+ }
126
+ finally {
127
+ mem.fill(0, PARENT_LEFT_OFF, PARENT_LEFT_OFF + PARENT_SCRATCH_LEN);
128
+ x.wipeBuffers();
129
+ }
130
+ },
131
+ });
132
+ // ── Blake3Tree class ────────────────────────────────────────────────────────
133
+ /**
134
+ * Stateful BLAKE3 Merkle log. Same surface and storage discipline as
135
+ * `Sha256Tree`: leaf hashes and every perfect aligned internal subtree
136
+ * hash live in the injected `MerkleStorage`; partial right-edge subtrees
137
+ * are recomputed on demand.
138
+ *
139
+ * `append` is the only mutator and is the leaf-hash factory; consumers
140
+ * feed leaf bytes, not pre-computed leaf hashes.
141
+ */
142
+ export class Blake3Tree {
143
+ hasher = Blake3Hasher;
144
+ storage;
145
+ constructor(storage) {
146
+ this.storage = storage;
147
+ }
148
+ size() {
149
+ return this.storage.size();
150
+ }
151
+ rootHash() {
152
+ const n = this.storage.size();
153
+ if (n === 0)
154
+ return this.hasher.hashEmpty();
155
+ const getNode = (level, index) => this.storage.getNode(level, index);
156
+ return subtreeHash(this.hasher, 0, n, getNode);
157
+ }
158
+ append(leafBytes) {
159
+ const leafIndex = this.storage.size();
160
+ const leafHash = this.hasher.hashLeaf(leafBytes);
161
+ this.storage.appendLeaf(leafIndex, leafHash);
162
+ let level = 0;
163
+ let idx = leafIndex;
164
+ while ((idx & 1) === 1) {
165
+ const left = this.storage.getNode(level, idx - 1);
166
+ const right = this.storage.getNode(level, idx);
167
+ const parent = this.hasher.hashInternal(left, right);
168
+ this.storage.putNode(level + 1, idx >>> 1, parent);
169
+ idx = idx >>> 1;
170
+ level++;
171
+ }
172
+ return { leafIndex, leafHash };
173
+ }
174
+ getInclusionProof(leafIndex, treeSize) {
175
+ const ts = treeSize ?? this.storage.size();
176
+ if (!Number.isInteger(ts) || ts < 1 || ts > this.storage.size())
177
+ throw new RangeError(`Blake3Tree.getInclusionProof: treeSize ${ts} out of range [1, ${this.storage.size()}]`);
178
+ const getNode = (level, index) => this.storage.getNode(level, index);
179
+ return buildInclusionProof({ hasher: this.hasher, leafIndex, treeSize: ts, getNode });
180
+ }
181
+ getConsistencyProof(oldSize, newSize) {
182
+ if (!Number.isInteger(newSize) || newSize < 0 || newSize > this.storage.size())
183
+ throw new RangeError(`Blake3Tree.getConsistencyProof: newSize ${newSize} out of range [0, ${this.storage.size()}]`);
184
+ const getNode = (level, index) => this.storage.getNode(level, index);
185
+ return buildConsistencyProof({ hasher: this.hasher, oldSize, newSize, getNode });
186
+ }
187
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Decoded form of a c2sp.org/tlog-checkpoint body. The body shape is
3
+ * hash-and-algo-agnostic: `rootHash` is 32 bytes for both the SHA-256 and
4
+ * BLAKE3 trees Phase 7 ships, but the codec only enforces a caller-supplied
5
+ * length in `parseCheckpointBody`. The signed-note envelope that wraps a
6
+ * checkpoint is handled in `signed-note.ts`.
7
+ */
8
+ export interface Checkpoint {
9
+ /**
10
+ * Log identity, non-empty UTF-8 with no Unicode spaces, plus signs, or
11
+ * embedded newlines. Per c2sp.org/tlog-checkpoint §Note text the origin
12
+ * SHOULD be a schemeless URL such as `example.com/log42`, but the codec
13
+ * only enforces the MUST-level structural constraints; broader URL
14
+ * shape policy is a caller concern.
15
+ */
16
+ readonly origin: string;
17
+ /**
18
+ * Number of leaves in the tree at signing time. Must be a non-negative
19
+ * safe integer; ASCII decimal serialization carries no leading zeroes
20
+ * (the literal `0` is the only valid string starting with `0`).
21
+ */
22
+ readonly treeSize: number;
23
+ /**
24
+ * Merkle root hash. 32 bytes for both Sha256Tree and Blake3Tree; the
25
+ * caller-supplied `expectedHashLen` parameter on `parseCheckpointBody`
26
+ * pins the exact length for a given hasher.
27
+ */
28
+ readonly rootHash: Uint8Array;
29
+ }
30
+ /**
31
+ * Serialize a Checkpoint into its canonical body bytes per
32
+ * c2sp.org/tlog-checkpoint §Note text. Layout:
33
+ *
34
+ * utf8(origin) || 0x0A || utf8(decimal(treeSize)) || 0x0A
35
+ * || base64(rootHash) || 0x0A
36
+ *
37
+ * Base64 uses the RFC 4648 §4 standard alphabet with `=` padding (NOT the
38
+ * URL-safe variant from §5 and NOT padding-stripped). The body has no
39
+ * leading or trailing whitespace beyond the final 0x0A; byte stability
40
+ * is the entire purpose of the codec, since the body bytes are what the
41
+ * STH signature is computed over.
42
+ */
43
+ export declare function serializeCheckpointBody(c: Checkpoint): Uint8Array;
44
+ /**
45
+ * Parse a canonical checkpoint body. Inverse of `serializeCheckpointBody`;
46
+ * round-trips byte-for-byte. Rejects extension lines, leading or trailing
47
+ * whitespace beyond the mandatory final 0x0A, non-newline ASCII control
48
+ * characters, malformed base64, and root hashes whose decoded length does
49
+ * not match `expectedHashLen` (default 32, the size for both Sha256Tree
50
+ * and Blake3Tree).
51
+ *
52
+ * The caller pins `expectedHashLen` to its hasher's `outputSize`; a future
53
+ * SignedLog (TASK-4) will bind this to the tree's hasher automatically.
54
+ *
55
+ * Per c2sp.org/tlog-checkpoint §Note text and c2sp.org/signed-note
56
+ * §Format.
57
+ */
58
+ export declare function parseCheckpointBody(bytes: Uint8Array, expectedHashLen?: number): Checkpoint;
@@ -0,0 +1,217 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/merkle/checkpoint.ts
23
+ //
24
+ // Canonical checkpoint body codec per c2sp.org/tlog-checkpoint (Transparency
25
+ // Log Checkpoints) §Note text. Three newline-terminated lines: origin, tree
26
+ // size in ASCII decimal with no leading zeroes, base64-encoded root hash.
27
+ // The body bytes are exactly what the STH signature is computed over, so
28
+ // producers and verifiers MUST serialize byte-for-byte identically.
29
+ //
30
+ // Extension lines are spec-listed as OPTIONAL and NOT RECOMMENDED. The
31
+ // ML-DSA-44 cosignature format defined in c2sp.org/tlog-cosignature does
32
+ // not commit to extension lines, so leviathan emits empty extension sections
33
+ // and the parser rejects any input that contains extension lines.
34
+ import { utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64 } from '../utils.js';
35
+ // ── serialization ───────────────────────────────────────────────────────────
36
+ const LF = 0x0a; // U+000A, the only legal line terminator in the body
37
+ const SPACE = 0x20; // U+0020, illegal anywhere inside origin
38
+ const PLUS = 0x2b; // U+002B, illegal anywhere inside origin
39
+ /**
40
+ * Decimal-encode a non-negative integer per c2sp.org/tlog-checkpoint §Note
41
+ * text: ASCII digits, no leading zeroes, the literal `0` for an empty tree.
42
+ * `Number.toString(10)` is already in this form for non-negative safe
43
+ * integers, the explicit guard exists so a Number that slipped past the
44
+ * upstream call site does not silently produce `"1e+21"` or similar.
45
+ */
46
+ function decimalTreeSize(n) {
47
+ if (!Number.isInteger(n) || n < 0 || n > Number.MAX_SAFE_INTEGER)
48
+ throw new RangeError(`serializeCheckpointBody: treeSize must be a non-negative safe integer, got ${n}`);
49
+ return n.toString(10);
50
+ }
51
+ /**
52
+ * Throw if `origin` violates the c2sp.org/tlog-checkpoint §Note text MUSTs:
53
+ * non-empty, no embedded newlines, no Unicode spaces, no plus characters.
54
+ * The "schemeless URL" advice from the spec is SHOULD-level and not
55
+ * enforced here, broader policy belongs to the application layer.
56
+ */
57
+ function validateOrigin(origin) {
58
+ if (origin.length === 0)
59
+ throw new RangeError('checkpoint: origin must be non-empty');
60
+ // Unicode space classes are wider than ASCII 0x20; the c2sp spec text
61
+ // says "Unicode spaces", so we use \s which covers the same family.
62
+ if (/\s/.test(origin) || origin.includes('+'))
63
+ throw new RangeError('checkpoint: origin must not contain whitespace or plus characters');
64
+ }
65
+ /**
66
+ * Serialize a Checkpoint into its canonical body bytes per
67
+ * c2sp.org/tlog-checkpoint §Note text. Layout:
68
+ *
69
+ * utf8(origin) || 0x0A || utf8(decimal(treeSize)) || 0x0A
70
+ * || base64(rootHash) || 0x0A
71
+ *
72
+ * Base64 uses the RFC 4648 §4 standard alphabet with `=` padding (NOT the
73
+ * URL-safe variant from §5 and NOT padding-stripped). The body has no
74
+ * leading or trailing whitespace beyond the final 0x0A; byte stability
75
+ * is the entire purpose of the codec, since the body bytes are what the
76
+ * STH signature is computed over.
77
+ */
78
+ export function serializeCheckpointBody(c) {
79
+ validateOrigin(c.origin);
80
+ const originBytes = utf8ToBytes(c.origin);
81
+ const sizeBytes = utf8ToBytes(decimalTreeSize(c.treeSize));
82
+ const rootB64 = bytesToBase64(c.rootHash);
83
+ const rootBytes = utf8ToBytes(rootB64);
84
+ const out = new Uint8Array(originBytes.length + 1 + sizeBytes.length + 1 + rootBytes.length + 1);
85
+ let off = 0;
86
+ out.set(originBytes, off);
87
+ off += originBytes.length;
88
+ out[off++] = LF;
89
+ out.set(sizeBytes, off);
90
+ off += sizeBytes.length;
91
+ out[off++] = LF;
92
+ out.set(rootBytes, off);
93
+ off += rootBytes.length;
94
+ out[off] = LF;
95
+ return out;
96
+ }
97
+ // ── parsing ─────────────────────────────────────────────────────────────────
98
+ /**
99
+ * Reject ASCII control characters below U+0020 other than 0x0A. The
100
+ * signed-note spec at c2sp.org/signed-note §Format prohibits these in the
101
+ * envelope; the checkpoint body inherits the same rule because the body
102
+ * is the prefix of a signed-note text region.
103
+ */
104
+ function hasIllegalControls(bytes) {
105
+ for (const b of bytes) {
106
+ if (b < 0x20 && b !== LF)
107
+ return true;
108
+ if (b === 0x7f)
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ /**
114
+ * Validate that a decimal tree-size string carries no leading zeroes per
115
+ * c2sp.org/tlog-checkpoint §Note text. The literal `"0"` is the sole legal
116
+ * string starting with `0`.
117
+ */
118
+ function parseTreeSize(s) {
119
+ if (s.length === 0)
120
+ throw new RangeError('checkpoint: empty tree-size line');
121
+ if (!/^[0-9]+$/.test(s))
122
+ throw new RangeError(`checkpoint: tree size '${s}' is not ASCII decimal`);
123
+ if (s.length > 1 && s.charCodeAt(0) === 0x30 /* '0' */)
124
+ throw new RangeError(`checkpoint: tree size '${s}' has a leading zero`);
125
+ const n = Number(s);
126
+ if (!Number.isInteger(n) || n < 0 || n > Number.MAX_SAFE_INTEGER)
127
+ throw new RangeError(`checkpoint: tree size '${s}' exceeds Number.MAX_SAFE_INTEGER`);
128
+ return n;
129
+ }
130
+ /**
131
+ * Parse a canonical checkpoint body. Inverse of `serializeCheckpointBody`;
132
+ * round-trips byte-for-byte. Rejects extension lines, leading or trailing
133
+ * whitespace beyond the mandatory final 0x0A, non-newline ASCII control
134
+ * characters, malformed base64, and root hashes whose decoded length does
135
+ * not match `expectedHashLen` (default 32, the size for both Sha256Tree
136
+ * and Blake3Tree).
137
+ *
138
+ * The caller pins `expectedHashLen` to its hasher's `outputSize`; a future
139
+ * SignedLog (TASK-4) will bind this to the tree's hasher automatically.
140
+ *
141
+ * Per c2sp.org/tlog-checkpoint §Note text and c2sp.org/signed-note
142
+ * §Format.
143
+ */
144
+ export function parseCheckpointBody(bytes, expectedHashLen = 32) {
145
+ if (!(bytes instanceof Uint8Array))
146
+ throw new TypeError('parseCheckpointBody: input must be a Uint8Array');
147
+ if (bytes.length === 0)
148
+ throw new RangeError('parseCheckpointBody: empty body');
149
+ if (bytes[bytes.length - 1] !== LF)
150
+ throw new RangeError('parseCheckpointBody: body must end with U+000A');
151
+ if (hasIllegalControls(bytes))
152
+ throw new RangeError('parseCheckpointBody: body contains non-newline ASCII control characters');
153
+ // Collect line offsets without TextDecoder gymnastics, so an embedded
154
+ // newline inside the origin can be caught structurally rather than via
155
+ // post-hoc string checks.
156
+ const lineStarts = [0];
157
+ for (let i = 0; i < bytes.length; i++) {
158
+ if (bytes[i] === LF && i + 1 < bytes.length)
159
+ lineStarts.push(i + 1);
160
+ }
161
+ // Three mandatory lines, each newline-terminated, plus the trailing LF
162
+ // at end of body. Extension lines (4th line and beyond) are NOT
163
+ // RECOMMENDED per c2sp.org/tlog-checkpoint §Note text; the ML-DSA-44
164
+ // cosignature format does not sign them, so leviathan rejects them
165
+ // outright to keep the wire format witness-ready end to end.
166
+ if (lineStarts.length !== 3)
167
+ throw new RangeError(`parseCheckpointBody: expected exactly 3 lines, got ${lineStarts.length}`);
168
+ // Slice the three lines without their terminating LF.
169
+ const sliceLine = (idx) => {
170
+ const start = lineStarts[idx];
171
+ const end = idx + 1 < lineStarts.length ? lineStarts[idx + 1] - 1 : bytes.length - 1;
172
+ return bytes.subarray(start, end);
173
+ };
174
+ const originBytes = sliceLine(0);
175
+ const sizeBytes = sliceLine(1);
176
+ const rootB64Bytes = sliceLine(2);
177
+ if (originBytes.length === 0)
178
+ throw new RangeError('parseCheckpointBody: empty origin line');
179
+ let origin;
180
+ try {
181
+ origin = bytesToUtf8(originBytes);
182
+ }
183
+ catch {
184
+ throw new RangeError('parseCheckpointBody: origin is not valid UTF-8');
185
+ }
186
+ validateOrigin(origin);
187
+ // The byte-level scan caught most disallowed characters above; reject
188
+ // stray SP/PLUS bytes that slipped past the UTF-8 validation in case
189
+ // of a future encoding edge case.
190
+ for (const b of originBytes)
191
+ if (b === SPACE || b === PLUS)
192
+ throw new RangeError('parseCheckpointBody: origin contains space or plus');
193
+ const sizeStr = bytesToUtf8(sizeBytes);
194
+ const treeSize = parseTreeSize(sizeStr);
195
+ const rootB64 = bytesToUtf8(rootB64Bytes);
196
+ // RFC 4648 §4 standard alphabet, with padding. `base64ToBytes` accepts
197
+ // the URL-safe variant and the padding-stripped form; reject both
198
+ // explicitly so the codec stays strictly compliant with
199
+ // c2sp.org/tlog-checkpoint §Conventions. A standard padded base64
200
+ // string always has length divisible by 4.
201
+ if (/[-_]/.test(rootB64))
202
+ throw new RangeError('parseCheckpointBody: root hash uses URL-safe base64');
203
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(rootB64))
204
+ throw new RangeError('parseCheckpointBody: root hash is not standard base64');
205
+ if (rootB64.length % 4 !== 0)
206
+ throw new RangeError('parseCheckpointBody: root hash base64 length is not a multiple of 4 (padding missing)');
207
+ let rootHash;
208
+ try {
209
+ rootHash = base64ToBytes(rootB64);
210
+ }
211
+ catch {
212
+ throw new RangeError('parseCheckpointBody: root hash failed base64 decoding');
213
+ }
214
+ if (rootHash.length !== expectedHashLen)
215
+ throw new RangeError(`parseCheckpointBody: root hash length ${rootHash.length} != expected ${expectedHashLen}`);
216
+ return { origin, treeSize, rootHash };
217
+ }
@@ -0,0 +1,19 @@
1
+ export { splitPoint } from './tree.js';
2
+ export type { Hasher, MerkleTree } from './tree.js';
3
+ export { MemoryStorage } from './storage.js';
4
+ export type { MerkleStorage } from './storage.js';
5
+ export { verifyInclusionProof, verifyConsistencyProof, buildInclusionProof, buildConsistencyProof, } from './proof.js';
6
+ export type { VerifyInclusionInput, VerifyConsistencyInput, BuildInclusionInput, BuildConsistencyInput, GetNode, } from './proof.js';
7
+ export { Sha256Hasher, Sha256Tree } from './sha256-tree.js';
8
+ export { Blake3Hasher, Blake3Tree } from './blake3-tree.js';
9
+ export { serializeCheckpointBody, parseCheckpointBody } from './checkpoint.js';
10
+ export type { Checkpoint } from './checkpoint.js';
11
+ export { emitSignedNote, parseSignedNote, deriveKeyId, suiteFormatEnumToAlgoByte, lookupAlgoEntryByFormatEnum, lookupAlgoEntryByByte, buildCosigSignedMessage, buildCosignedMessage, emitCosigSignaturePayload, parseCosigSignaturePayload, ALGO_BYTE_ED25519_NOTE, ALGO_BYTE_ED25519_COSIG, ALGO_BYTE_MLDSA44_COSIG, } from './signed-note.js';
12
+ export type { SignatureLine, SignedNote, AlgoEntry, MessageConstruction, SignaturePayload, CosignedMessageInput, } from './signed-note.js';
13
+ export type { SignedTreeHead } from './sth.js';
14
+ export { SignedLog } from './signed-log.js';
15
+ export type { SignedLogOpts } from './signed-log.js';
16
+ export { MerkleVerifier } from './merkle-verifier.js';
17
+ export type { MerkleVerifierOpts } from './merkle-verifier.js';
18
+ export { MerkleLog } from './merkle-log.js';
19
+ export type { MerkleLogCreateOpts, MerkleLogGenerateOpts } from './merkle-log.js';
@@ -0,0 +1,37 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/merkle/index.ts
23
+ //
24
+ // Public surface for the merkle log primitives. Interfaces, free
25
+ // functions, and the SHA-256 specialisation. Hash-agnostic by design;
26
+ // the BLAKE3 specialisation lives alongside this module and re-exports
27
+ // the same interfaces.
28
+ export { splitPoint } from './tree.js';
29
+ export { MemoryStorage } from './storage.js';
30
+ export { verifyInclusionProof, verifyConsistencyProof, buildInclusionProof, buildConsistencyProof, } from './proof.js';
31
+ export { Sha256Hasher, Sha256Tree } from './sha256-tree.js';
32
+ export { Blake3Hasher, Blake3Tree } from './blake3-tree.js';
33
+ export { serializeCheckpointBody, parseCheckpointBody } from './checkpoint.js';
34
+ export { emitSignedNote, parseSignedNote, deriveKeyId, suiteFormatEnumToAlgoByte, lookupAlgoEntryByFormatEnum, lookupAlgoEntryByByte, buildCosigSignedMessage, buildCosignedMessage, emitCosigSignaturePayload, parseCosigSignaturePayload, ALGO_BYTE_ED25519_NOTE, ALGO_BYTE_ED25519_COSIG, ALGO_BYTE_MLDSA44_COSIG, } from './signed-note.js';
35
+ export { SignedLog } from './signed-log.js';
36
+ export { MerkleVerifier } from './merkle-verifier.js';
37
+ export { MerkleLog } from './merkle-log.js';