@optimystic/quereus-plugin-crypto 0.13.5 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ ## 0.14.0 — BREAKING: `digest` API rework
4
+
5
+ ### What changed
6
+
7
+ The exported `digest()` function signature changed from:
8
+
9
+ ```ts
10
+ // OLD (≤ 0.13.x)
11
+ digest(data: string | Uint8Array, algorithm?, inputEncoding?, outputEncoding?)
12
+ ```
13
+
14
+ to:
15
+
16
+ ```ts
17
+ // NEW (≥ 0.14.0)
18
+ digest(fields: readonly DigestField[], algorithm?, encoding?)
19
+ ```
20
+
21
+ Key differences:
22
+
23
+ | | Old | New |
24
+ |---|---|---|
25
+ | First argument | A single scalar value (`string` \| `Uint8Array`) | An **array** of values |
26
+ | `inputEncoding` | 3rd positional arg | **Removed** (the new API frames values by type, no string-decoding step) |
27
+ | Output encoding | 4th positional arg | 2nd `encoding` arg (shifted left by one) |
28
+ | Algorithm | 2nd positional arg | 2nd `algorithm` arg (unchanged position) |
29
+ | Result | Bare hash of the decoded bytes | **Framed** injective digest — `digest(['hello'])` ≠ `sha256("hello")` |
30
+ | Algorithm + encoding | Per-call | **Bound at plugin load time** for the SQL function |
31
+
32
+ ### Migration: JS/TypeScript callers
33
+
34
+ ```ts
35
+ // OLD
36
+ const hashBytes = digest(payload, 'sha256', 'utf8', 'bytes') as Uint8Array;
37
+ const payloadDigest = digest(payload, 'sha256', 'utf8', 'base64url') as string;
38
+
39
+ // NEW — wrap the value in an array, drop inputEncoding
40
+ const hashBytes = digest([payload], 'sha256', 'bytes') as Uint8Array;
41
+ const payloadDigest = digest([payload], 'sha256', 'base64url') as string;
42
+ ```
43
+
44
+ > **The result value changes.** The new digest is *framed* (version byte + type tag +
45
+ > length-prefixed payload per field), so `digest(['hello'])` is **not** the same bytes
46
+ > as `sha256(utf8("hello"))`. If you need to match an externally-computed bare hash,
47
+ > see the open question below.
48
+
49
+ ### Migration: SQL callers
50
+
51
+ The SQL `digest(field1, field2, ...)` function is **variadic over data fields** — every
52
+ argument is a field to hash, not a config option — so the signature is unchanged from the
53
+ SQL perspective.
54
+
55
+ However, if you were passing extra positional arguments to mimic `algo`/`inputEncoding`/`outputEncoding`
56
+ (e.g. `digest(data, 'sha256', 'utf8', 'bytes')`), those are now treated as **additional
57
+ data fields** and hashed into the result silently rather than interpreted as config. There
58
+ is **no error** on the SQL path for this; it just hashes more fields. Check any SQL call
59
+ sites that pass more than pure data arguments.
60
+
61
+ Algorithm and encoding are now set via the plugin config at load time (see the
62
+ [Digest configuration](README.md#digest-configuration) section of the README).
63
+
64
+ ### Why no compatibility shim?
65
+
66
+ The old and new calling conventions cannot be cleanly disambiguated: the new first argument
67
+ is always an array; the old's was always a scalar. Adding a scalar → old-API detection
68
+ shim would silently re-enable the broken `inputEncoding` positional footgun and make the
69
+ result value unpredictable. The clean break stays; instead, old-style JS calls now throw
70
+ a clear, actionable error message naming this migration note.
71
+
72
+ ### Error message for old-style calls
73
+
74
+ If you pass a non-array as the first argument to `digest()` in JS/TypeScript, you will now
75
+ see:
76
+
77
+ ```
78
+ digest(fields, algorithm?, encoding?): 'fields' must be an array of values.
79
+ The digest API changed in v0.14: it is now variadic/injective over fields,
80
+ the per-call inputEncoding was removed, and algorithm + output encoding are bound at plugin load time.
81
+ Migrate digest(value, algo, inputEncoding, outputEncoding) → digest([value], algo, outputEncoding) —
82
+ note the result is now a *framed* digest, not a bare hash of the bytes.
83
+ ```
84
+
85
+ ### Open question: bare hash helper
86
+
87
+ The new `digest` has no function that returns a bare (un-framed) hash of a single value's
88
+ bytes in a chosen encoding — what the old `digest(x, algo, inEnc, outEnc)` did. If you
89
+ need bare-hash semantics (e.g. to match a hash stored before v0.14, or computed by an
90
+ external system), there is currently no drop-in. File a separate issue/ticket if a
91
+ `hash(data, algorithm, inputEncoding, outputEncoding)` helper is needed.
package/README.md CHANGED
@@ -5,6 +5,8 @@ Quereus plugin providing cryptographic functions for SQL queries with base64url
5
5
  ## Features
6
6
 
7
7
  - **Hash Functions**: SHA-256, SHA-512, BLAKE3 hashing with base64url output
8
+ - **Content Identifiers (CIDv1)**: Self-describing, interoperable IPFS/IPLD content addresses
9
+ - **Selective Disclosure**: Salted-leaf set commitment — commit to a whole attribute set with one value, later reveal only a chosen subset with proof of authenticity
8
10
  - **Hash Modulo**: Fixed-size hash values (16-bit, 32-bit, etc.) for sharding and partitioning
9
11
  - **Random Bytes**: Generate cryptographically secure random bytes (default: 256 bits)
10
12
  - **Signature Functions**: secp256k1, P-256, Ed25519 signing with base64url encoding
@@ -28,6 +30,13 @@ import { loadPlugin } from '@quereus/quereus/util/plugin-loader.js';
28
30
 
29
31
  const db = new Database();
30
32
  await loadPlugin('npm:@optimystic/quereus-plugin-crypto', db);
33
+
34
+ // Or configure the digest algorithm / output encoding once, at load time
35
+ // (see "Digest configuration" below):
36
+ await loadPlugin('npm:@optimystic/quereus-plugin-crypto', db, {
37
+ algorithm: 'sha256', // 'sha256' (default) | 'sha512' | 'blake3'
38
+ encoding: 'base64url', // 'base64url' (default) | 'base64' | 'hex'
39
+ });
31
40
  ```
32
41
 
33
42
  ### Direct Import (for JavaScript/TypeScript code)
@@ -45,8 +54,8 @@ import {
45
54
  getPublicKey
46
55
  } from '@optimystic/quereus-plugin-crypto';
47
56
 
48
- // Hash data (base64url by default)
49
- const hash = digest('hello world', 'sha256', 'utf8', 'base64url');
57
+ // Hash an ordered tuple of fields (injective distinct tuples never collide)
58
+ const hash = digest(['hello world', 42, null], 'sha256', 'base64url');
50
59
 
51
60
  // Get a 16-bit hash for sharding
52
61
  const shard = hashMod('user@example.com', 16, 'sha256', 'utf8');
@@ -68,24 +77,164 @@ const isValid = verify(hash, signature, publicKey, 'secp256k1', 'base64url', 'ba
68
77
 
69
78
  All SQL functions use **base64url encoding by default** for inputs and outputs. This is URL-safe and SQL-friendly.
70
79
 
71
- ### digest(data, algorithm?, inputEncoding?, outputEncoding?)
80
+ ### digest(field1, field2, ..., fieldN)
81
+
82
+ Hash an **ordered tuple of fields** into a single digest. `digest` is variadic over
83
+ *data* — every argument is a field, not a configuration option. The algorithm and
84
+ output encoding are chosen once, at load time (see [Digest configuration](#digest-configuration)),
85
+ so they never have to be passed per call.
86
+
87
+ ```sql
88
+ -- Hash a single value
89
+ SELECT digest(Id) as hash;
90
+
91
+ -- Hash a tuple of columns — distinct tuples never collide,
92
+ -- NULL is distinguished from '', and 123 from '123'
93
+ SELECT digest(Tid, Name, ImageRef, NumberRequiredTSAs) as commitment;
94
+ ```
95
+
96
+ **Why variadic + injective?** Hashing several fields by joining them
97
+ (`a || '|' || b`) or by `String()`-concatenation is not injective: `('a|b','c')`
98
+ collides with `('a','b|c')`, `NULL` collides with `''`, and `123` collides with
99
+ `'123'`. `digest` instead applies a canonical, length-prefixed, type-tagged framing
100
+ to each field, so distinct tuples always produce distinct digests. This matters when
101
+ the digest is signed or persisted as a commitment.
102
+
103
+ The framing is also why `digest(x)` is **not** a bare `sha256(x)` — it is a framed
104
+ digest of a one-element tuple. For sharding a single value, use `hash_mod`.
105
+
106
+ ### cid(data, codec?, hash?, base?) / cid_v1(digest, hash, codec?, base?) / cid_decode(cid)
72
107
 
73
- Hash data using SHA-256 (default), SHA-512, or BLAKE3.
108
+ `digest` returns a **bare** hash — raw digest bytes in some text encoding, with no
109
+ record of which base, content type, or hash algorithm produced it. `cid` instead
110
+ produces a **self-describing CIDv1**, the same interoperable content address an
111
+ IPFS/IPLD store computes for the same bytes:
112
+
113
+ ```
114
+ CIDv1 = multibase( version ‖ multicodec(content-type) ‖ multihash )
115
+ multihash = hashFnCode ‖ digestLength ‖ digestBytes
116
+ ```
117
+
118
+ Because the multibase, content codec, and hash code all travel *inside* the value,
119
+ a consumer can validate it without out-of-band knowledge, and an algorithm
120
+ migration (e.g. sha2-256 → another hash) is unambiguous rather than a silent
121
+ reinterpretation. The framing comes entirely from the audited
122
+ [`multiformats`](https://github.com/multiformats/js-multiformats) library.
74
123
 
75
124
  ```sql
76
- -- Hash base64url data (default)
77
- SELECT digest('aGVsbG8gd29ybGQ') as hash;
125
+ -- Hash a blob and frame it as the canonical raw/sha2-256/base32 CID
126
+ -- (== the CID IPFS shows for the same bytes)
127
+ SELECT cid(SomeBlob) AS Cid;
78
128
 
79
- -- Hash UTF-8 text
80
- SELECT digest('hello world', 'sha256', 'utf8', 'base64url') as hash;
129
+ -- Pick content codec / hash / base
130
+ SELECT cid(SomeBlob, 'dag-cbor', 'sha2-512', 'base58btc') AS Cid;
81
131
 
82
- -- Hash with SHA-512
83
- SELECT digest('data', 'sha512', 'utf8', 'base64url') as sha512_hash;
132
+ -- A self-describing content address over a field tuple: digest() canonically
133
+ -- frames + hashes the fields; cid_v1() wraps that exact digest as a CIDv1
134
+ -- (no double-hash). The asserted hash must match digest()'s configured algorithm.
135
+ SELECT cid_v1(digest(ColA, ColB, ColC), 'sha2-256') AS Cid;
84
136
 
85
- -- Hash with BLAKE3, output as hex
86
- SELECT digest('data', 'blake3', 'utf8', 'hex') as blake3_hex;
137
+ -- Validate / inspect a stored CID (returns JSON: { version, codec, hashCode, digest })
138
+ SELECT cid_decode(Cid) ->> 'codec' AS codec FROM T;
87
139
  ```
88
140
 
141
+ - **`cid(data, codec?, hash?, base?)`** — hash `data` then frame. `data` is a BLOB,
142
+ or a base64url-encoded TEXT digest/blob (the plugin's canonical text encoding).
143
+ Defaults: `codec='raw'`, `hash='sha2-256'`, `base='base32'`.
144
+ - **`cid_v1(digest, hash, codec?, base?)`** — wrap an **already-computed** digest
145
+ without re-hashing. `hash` is required and asserts which algorithm produced the
146
+ digest; the digest length is checked against it (sha2-256/blake3 = 32 bytes,
147
+ sha2-512 = 64) and a mismatch is rejected.
148
+ - **`cid_decode(cid) → JSON`** — parse a CID back to `{ version, codec, hashCode,
149
+ digest }` (digest as base64url). Throws cleanly on malformed input.
150
+
151
+ Selectable values: `codec` ∈ `raw`, `dag-cbor`; `hash` ∈ `sha2-256`, `sha2-512`,
152
+ `blake3`; `base` ∈ `base32` (default), `base58btc`, `base64url`, `base16`.
153
+
154
+ **Why `base32` by default?** A CID's whole purpose is to match what an external
155
+ content-addressed store computes, and IPFS renders CIDv1 canonically in base32 (the
156
+ `b…` prefix). This is deliberately different from the plugin's `digest`/`random_bytes`
157
+ default of base64url: base64url is compact for *internal* values that live in memory,
158
+ on the wire, or in JSON, whereas base32 is case-insensitive and DNS/URL/filename-safe
159
+ where interoperable addresses are copied and read. Two audiences, two defaults — pass
160
+ `'base64url'` explicitly if you want the compact form.
161
+
162
+ ### set_commit(leaves_json) / set_verify(root, disclosed_json, hidden_json)
163
+
164
+ **Per-attribute selective disclosure.** An authority commits to a registrant's whole
165
+ attribute set as a single root (which it signs / persists), then later reveals only a
166
+ chosen *subset* to a recipient — with a proof the revealed values are genuinely the
167
+ committed ones — **without** leaking the withheld attribute values. A flat
168
+ `digest(whole set)` can't do this (verifying one field needs the whole pre-image, so
169
+ it's all-or-nothing); `set_commit` supports *partial opening*.
170
+
171
+ Each attribute becomes a salted leaf, and the root is the digest of all leaf digests in
172
+ canonical (sort-by-leaf-digest-bytes) order:
173
+
174
+ ```
175
+ leafDigest = digest([SD_LEAF_DOMAIN_V1, name, value, salt]) -- raw digest bytes
176
+ root = digest([SD_SET_DOMAIN_V1, sortedLeaf_0, sortedLeaf_1, ...])
177
+ ```
178
+
179
+ This is the same flat salted-hash shape the IETF SD-JWT standard
180
+ (`draft-ietf-oauth-selective-disclosure-jwt`) settled on rather than a Merkle tree —
181
+ cited as conceptual precedent only; the framing here is Optimystic's own `digest`
182
+ `encodeFields` (not SD-JWT wire-compatible). The `name` is hashed into the leaf so a
183
+ `(value, salt)` proof can't be replayed under another attribute; the `salt` is per-leaf
184
+ and **mandatory** (low-entropy values like a DOB or a boolean are brute-forceable from a
185
+ bare hash, and independent salts defeat cross-registrant correlation).
186
+
187
+ ```sql
188
+ -- Commit to a JSON array of [name, value, salt] leaves -> single root.
189
+ -- Pair with cid() for the self-describing persisted/signed column representation:
190
+ SELECT cid(set_commit(SelectiveDetails)) AS SelectiveCid;
191
+
192
+ -- A schema CHECK makes a forged root impossible to store: the root is recomputed from
193
+ -- the stored attribute triples, so SelectiveCid must equal the genuine commitment.
194
+ CREATE TABLE Registrant (
195
+ ...,
196
+ SelectiveDetails TEXT, -- JSON array of [name, value, salt] triples
197
+ SelectiveCid TEXT,
198
+ CHECK (SelectiveCid = cid(set_commit(SelectiveDetails)))
199
+ );
200
+
201
+ -- A recipient verifies a disclosure: the disclosed [name, value, salt] triples plus
202
+ -- the opaque leaf digests of the withheld leaves reconstruct the entire root.
203
+ SELECT set_verify(root, disclosed_json, hidden_json) AS ok;
204
+ ```
205
+
206
+ - **`set_commit(leaves_json) → TEXT`** — `leaves_json` is a JSON array; each element is a
207
+ leaf `[name, value, salt]` *or* `{ "name", "value", "salt" }`. Values follow `digest`'s
208
+ rules as parsed from JSON (INTEGER vs REAL by JS value, TEXT, BOOL, null, nested
209
+ object/array). Each leaf must carry all three fields — a missing `value` (object form) or
210
+ a `[name, value]` array (under three elements) throws; pass `value: null` for a null-valued
211
+ attribute. The salt is base64url TEXT (e.g. from `random_bytes`). **THROWS** on a duplicate
212
+ name, a missing value, a missing/empty salt, or unparseable/non-array JSON (invalid states
213
+ made impossible). `replicable` — the root is signed and persisted, same bar as `digest`.
214
+ - **`set_verify(root, disclosed_json, hidden_json) → BOOLEAN`** — `disclosed_json` is the
215
+ opened triples (same leaf shape), `hidden_json` a JSON array of the withheld leaves'
216
+ opaque base64url digests. Reconstructs the **entire** root and compares — so the holder
217
+ cannot add, drop, or swap a leaf. Returns `false` on any mismatch or malformed input
218
+ (forgiving, like `verify`).
219
+
220
+ > **Disclosure generation is JS-only** (`setDisclose`, below) — there's no SQL
221
+ > `set_disclose`, because building a disclosure requires the full attribute set including
222
+ > the secret salts, which lives engine-side, not in a query.
223
+
224
+ **BLOB-valued attributes via SQL:** JSON has no blob type, so a blob attribute passed
225
+ through `set_commit` is committed as its base64url TEXT. Callers needing a *true* BLOB
226
+ value (committed as a BLOB field, `TAG_BLOB`) must use the JS `setCommit` with a
227
+ `Uint8Array` value.
228
+
229
+ **Privacy note:** a fixed commitment disclosed to two audiences exposes the same hidden
230
+ digests and field count to both, so they can correlate that it's the same record.
231
+ Re-randomizing per disclosure would require fresh salts → a new root → a new signature
232
+ (out of scope here).
233
+
234
+ **Framing coupling:** leaf and root reuse `digest`'s `encodeFields`, so a future digest
235
+ framing-version bump changes `set_commit` output too — intentional (one canonical
236
+ framing), but it means the pinned set-commitment vectors move with the digest vectors.
237
+
89
238
  ### hash_mod(data, bits, algorithm?, inputEncoding?)
90
239
 
91
240
  Hash data and return modulo 2^bits for fixed-size hash values.
@@ -121,6 +270,16 @@ SELECT random_bytes(64) as random_id;
121
270
 
122
271
  ### sign(data, privateKey, curve?, inputEncoding?, keyEncoding?, outputEncoding?)
123
272
 
273
+ **Security note (replication):** passing a private key to `sign()` as a literal or
274
+ bound parameter is **safe** with respect to Optimystic replication. The Quereus engine
275
+ rebuilds the replicated statement from evaluated column values — not from source SQL —
276
+ so the key argument is evaluated away and never reaches the record. Peers re-execute
277
+ `INSERT ... VALUES (<signature>)`, never the original `sign(..., key)` call. The one
278
+ thing to avoid is storing a raw private key **as a column value** in an
279
+ optimystic-backed table, since any persisted column value is replicated. Sign or derive
280
+ and store only the public result (signature, public key, commitment). See
281
+ `docs/transactions.md` § "Secrets and the replicated statement record" for full detail.
282
+
124
283
  Sign data using secp256k1 (default), P-256, or Ed25519.
125
284
 
126
285
  ```sql
@@ -152,6 +311,40 @@ SELECT verify('hello', 'c2lnbmF0dXJl', 'cHVibGljS2V5', 'secp256k1', 'utf8') as i
152
311
  SELECT verify('data', 'c2lnbmF0dXJl', 'cHVibGljS2V5', 'p256', 'utf8') as is_valid;
153
312
  ```
154
313
 
314
+ ## Migration (v0.14 breaking change)
315
+
316
+ The `digest()` JS/TypeScript API changed significantly in v0.14. See [CHANGELOG.md](CHANGELOG.md) for the
317
+ full migration guide, including old → new call-site examples, what changed in the SQL function, and
318
+ the open bare-hash-helper question.
319
+
320
+ ## Digest configuration
321
+
322
+ Because `digest` is variadic over data, its **algorithm** and **output encoding** are
323
+ not call arguments — they are bound once when the plugin is loaded, via the plugin
324
+ config object:
325
+
326
+ ```ts
327
+ import { registerPlugin } from '@quereus/quereus';
328
+ import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
329
+
330
+ await registerPlugin(db, cryptoPlugin, {
331
+ algorithm: 'sha256', // 'sha256' (default) | 'sha512' | 'blake3'
332
+ encoding: 'base64url', // 'base64url' (default) | 'base64' | 'hex'
333
+ });
334
+ ```
335
+
336
+ An unknown `algorithm` or a non-text `encoding` throws at registration (fail fast),
337
+ and the algorithm/encoding are resolved once so the per-call path does no branching.
338
+
339
+ **Why load-time and not per-connection?** The SQL `digest` is registered as
340
+ `replicable` — its output must be bit-identical across peers, platforms, and app
341
+ versions, because these digests are signed and persisted as commitments. That holds
342
+ only if the configuration is fixed for every peer. *Mutable* per-connection
343
+ configuration (e.g. a runtime `SET`/PRAGMA) would let two peers disagree and silently
344
+ break signature validation, so it is intentionally not offered for this function. If a
345
+ single database genuinely needs two digest configurations, register the plugin twice
346
+ (or expose named variants) rather than flipping mutable session state.
347
+
155
348
  ## Supported Algorithms
156
349
 
157
350
  ### Hash Algorithms
@@ -216,7 +409,7 @@ const privateKey = generatePrivateKey('secp256k1', 'base64url');
216
409
  const publicKey = getPublicKey(privateKey);
217
410
 
218
411
  const message = 'Hello, World!';
219
- const hash = digest(message, 'sha256', 'utf8', 'base64url');
412
+ const hash = digest([message], 'sha256', 'base64url') as string;
220
413
  const signature = sign(hash, privateKey);
221
414
  const isValid = verify(hash, signature, publicKey);
222
415
 
@@ -229,9 +422,59 @@ console.log('Random nonce:', nonce);
229
422
 
230
423
  ## JavaScript API Reference
231
424
 
232
- ### digest(data, algorithm?, inputEncoding?, outputEncoding?)
233
- Hash data using SHA-256, SHA-512, or BLAKE3.
234
- - **Returns**: Hash as string (or Uint8Array if encoding is 'bytes')
425
+ ### digest(fields, algorithm?, encoding?)
426
+ Injective digest over an ordered tuple of fields. `fields` is an array of values
427
+ (any SQL value type). `algorithm` defaults to `'sha256'`, `encoding` to `'base64url'`.
428
+ - **Returns**: Hash as string (or Uint8Array if encoding is `'bytes'`)
429
+ - **Related**: `encodeFields(fields)` returns the canonical pre-hash byte framing;
430
+ `digestFields(fields, hasher, encode)` / `resolveHasher` / `resolveOutputEncoder`
431
+ are the building blocks the SQL function composes (resolve once, no per-call branching).
432
+
433
+ ### cid(data, codec?, hash?, base?)
434
+ Hash `data` (a `Uint8Array`) and frame it as a self-describing CIDv1 string. Defaults:
435
+ `codec='raw'`, `hash='sha2-256'`, `base='base32'`. Byte-identical to the CID an IPFS/IPLD
436
+ store computes for the same bytes.
437
+ - **Returns**: CIDv1 string
438
+
439
+ ### cidV1(digest, hash, codec?, base?)
440
+ Frame an **already-computed** `digest` (a `Uint8Array`) as a CIDv1 without re-hashing.
441
+ `hash` asserts which algorithm produced the digest; the digest length is validated
442
+ against it. Use to turn a `digest(...)` result into a CID: `cidV1(digest(fields, 'sha256', 'bytes'), 'sha2-256')`.
443
+ - **Returns**: CIDv1 string
444
+
445
+ ### cidDecode(cid)
446
+ Parse a CID string into `{ version, codec, hashCode, digest }` for validation/migration.
447
+ Recognized codec/hash codes are returned as names, otherwise as numbers; `digest` is a
448
+ `Uint8Array`. Throws on malformed input.
449
+ - **Returns**: `{ version: number, codec: Multicodec | number, hashCode: MultihashCode | number, digest: Uint8Array }`
450
+
451
+ ### setCommit(leaves, hasher?, encode?) / setDisclose(leaves, revealNames, hasher?) / setVerify(root, disclosure, hasher?, encode?)
452
+ Salted-leaf set commitment for selective disclosure. `leaves` is an array of
453
+ `{ name, value, salt }` (`salt` a base64url string or `Uint8Array`).
454
+ - **`setCommit`** → the root (`string`, or `Uint8Array` with a bytes encoder). Throws on a
455
+ duplicate name or a missing/empty salt; the empty set is well-defined, not an error.
456
+ - **`setDisclose`** → `{ disclosed, hidden }`: the revealed `{ name, value, salt }` triples
457
+ plus the opaque base64url leaf digests of the withheld leaves (withheld values/salts never
458
+ appear). This is the engine-side generator with no SQL equivalent.
459
+ - **`setVerify`** → `boolean`: reconstructs the entire root from `disclosure` and compares to
460
+ `root`. `false` on mismatch or malformed input. `encode` is how the signed root is rendered
461
+ (default base64url); a `Uint8Array` root is compared by raw bytes.
462
+ - **`leafDigest(leaf, hasher)`** → raw leaf digest bytes (the low-level building block).
463
+
464
+ ```typescript
465
+ import { setCommit, setDisclose, setVerify, randomBytes } from '@optimystic/quereus-plugin-crypto';
466
+
467
+ const leaves = [
468
+ { name: 'name', value: 'Alice', salt: randomBytes(256) as string },
469
+ { name: 'over18', value: true, salt: randomBytes(256) as string },
470
+ { name: 'zip', value: '90210', salt: randomBytes(256) as string },
471
+ ];
472
+ const root = setCommit(leaves); // sign / persist this (often as cid(root))
473
+
474
+ // Recipient gets only `over18`, with proof it belongs to the committed set:
475
+ const disclosure = setDisclose(leaves, ['over18']);
476
+ const ok = setVerify(root, disclosure); // true — withheld values never left the engine
477
+ ```
235
478
 
236
479
  ### hashMod(data, bits, algorithm?, inputEncoding?)
237
480
  Hash data and return modulo 2^bits for fixed-size hash values.