@aleutian/verification 0.0.0 → 0.7.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.
@@ -0,0 +1,3093 @@
1
+ /**
2
+ * Core types for the Aleutian verification SDK.
3
+ *
4
+ * All interfaces use readonly properties for immutability.
5
+ * Field names use camelCase to match TypeScript/JavaScript conventions.
6
+ */
7
+ /**
8
+ * The entry ID range covered by an anchor.
9
+ *
10
+ * Identifies the first and last entries in the chain segment this anchor
11
+ * attests to.
12
+ */
13
+ interface AnchorRange {
14
+ /** v1-v6: the entry_id of the first entry covered by this anchor ("" in v7). */
15
+ readonly startEntryId: string;
16
+ /** v1-v6: the entry_id of the last entry covered by this anchor ("" in v7). */
17
+ readonly endEntryId: string;
18
+ /**
19
+ * v7 only: the `global_seq` of the first entry covered (format-spec §9.1).
20
+ *
21
+ * On the wire a decimal STRING, because it may exceed 2^53: a `number` would
22
+ * round it and canonicalize different bytes. `parseAnchorText` returns a
23
+ * `bigint`; pass a `bigint` (or a safe `number`) yourself.
24
+ */
25
+ readonly startSeq?: bigint | number;
26
+ /** v7 only: the `global_seq` of the last entry covered, inclusive. As `startSeq`. */
27
+ readonly endSeq?: bigint | number;
28
+ }
29
+ /**
30
+ * A signed chain checkpoint.
31
+ *
32
+ * An anchor is a cryptographically signed attestation of a company's
33
+ * audit chain state at a specific point in time. Anchors are generated
34
+ * periodically (typically daily) and can be used to verify chain
35
+ * integrity without network access.
36
+ *
37
+ * The signature is an ML-DSA-65 (FIPS 204) signature over the canonical
38
+ * JSON form of the anchor (all fields except `signature`, alphabetical key order).
39
+ *
40
+ * WARNING (M-5): companyId contains business-identifying information.
41
+ * Do not log anchors in production — wrap with
42
+ * `redactedAnchor(anchor)` from `./redactedView` before passing to any
43
+ * logger or `JSON.stringify`. The interface is a plain object literal,
44
+ * so it does not own a `toJSON` redactor itself.
45
+ */
46
+ interface Anchor {
47
+ /** The anchor format version: 1, 2, 3, 4, 6 or 7 (5 is withdrawn). */
48
+ readonly version: number;
49
+ /** The unique identifier for this anchor. */
50
+ readonly anchorId: string;
51
+ /** The company this anchor belongs to. WARNING: Contains business-identifying information. */
52
+ /**
53
+ * The namespace this anchor is about — whatever the chain describes.
54
+ *
55
+ * WIRE KEY VARIES BY VERSION. v3-v5 spell it `company_id`; v6 spells it
56
+ * `subject`. The key is INSIDE the signed bytes, which is why the rename
57
+ * needed a new version rather than being a refactor — and why the field also
58
+ * MOVES, from 3rd alphabetically to 8th.
59
+ *
60
+ * NOT ERASABLE, and MUST NOT CARRY PERSONAL DATA. Changing it invalidates the
61
+ * signature, and through `chainHash` and `previousAnchorId` it invalidates
62
+ * every anchor that follows. Use a stable pseudonym or an opaque id.
63
+ *
64
+ * Named `companyId` before 2026-09-23.
65
+ */
66
+ readonly subject: string;
67
+ /**
68
+ * Anchor-of-anchors digest binding this anchor to its covered chain.
69
+ *
70
+ * NOT byte-equal to the tip entry's `chain_hash` — it is the output
71
+ * of `computeAnchorChainHash`, which applies a distinct SHA-512
72
+ * domain separator (`"aleutian.anchor.v2:"`) and folds in the
73
+ * previous anchor's hash, the company id, the entry range, and the
74
+ * tip entry's `chain_hash`. Verifiers MUST recompute this value
75
+ * (not byte-compare against the tip) to validate the binding.
76
+ */
77
+ readonly chainHash: string;
78
+ /** The entry ID range covered by this anchor. */
79
+ readonly range: AnchorRange;
80
+ /**
81
+ * The number of entries covered by this anchor.
82
+ *
83
+ * Typed `bigint | number` so int64 values exceeding `Number.MAX_SAFE_INTEGER`
84
+ * (2^53 − 1) can be represented losslessly. Mirrors V3 SAR pattern.
85
+ * Ticket: dsar_js_anchor_int64_widening.
86
+ */
87
+ readonly entryCount: bigint | number;
88
+ /** The base64-encoded ML-DSA-65 signature over the canonical JSON form. */
89
+ readonly signature: string;
90
+ /** Identifies which signing key was used. Corresponds to a key in EMBEDDED_PUBLIC_KEYS. */
91
+ readonly signingKeyId: string;
92
+ /**
93
+ * The timestamp when this anchor was generated, in milliseconds since Unix epoch (UTC).
94
+ *
95
+ * Typed `bigint | number` for int64 boundary safety; realistic millisecond
96
+ * timestamps fit in `number` through year 2286.
97
+ */
98
+ readonly createdAtMs: bigint | number;
99
+ /** The anchor_id of the preceding anchor, or empty string for the first anchor. */
100
+ readonly previousAnchorId: string;
101
+ /**
102
+ * Version=4+: a signed claim that the chain from
103
+ * (previous_anchor.entry_count + 1) through this anchor's
104
+ * entry_count has been verified clean by the anchor-generator at
105
+ * signing time (ui_chainops_18).
106
+ *
107
+ * Version dispatch invariants:
108
+ * - Version <= 3 anchors MUST carry verifiedThrough === 0.
109
+ * - Version === 4 anchors MUST carry verifiedThrough > 0.
110
+ *
111
+ * Cross-language encoding: always present in JSON (no omission) so
112
+ * Go, Python, and JS produce byte-identical canonical bytes.
113
+ *
114
+ * Optional in the TypeScript interface for backwards compatibility
115
+ * with v3 anchor JSON that pre-dates this field; defaults to 0.
116
+ *
117
+ * Typed `bigint | number` so int64 max-boundary values (2^63 − 1)
118
+ * canonicalize losslessly. Ticket: dsar_js_anchor_int64_widening.
119
+ */
120
+ readonly verifiedThrough?: bigint | number;
121
+ /**
122
+ * v7 only: the previous anchor's `chainHash`, or `SEED_ANCHOR_HASH` for the
123
+ * first anchor (format-spec §9.1). Inside the signed bytes, so a v7 anchor
124
+ * is bound with no second file. Absent on v1-v6.
125
+ */
126
+ readonly previousAnchorHash?: string;
127
+ }
128
+ /**
129
+ * An entry from a data export.
130
+ *
131
+ * Contains the fields needed to verify chain integrity for an exported
132
+ * audit entry. This matches the schema of Aleutian's data export format.
133
+ */
134
+ interface ExportedEntry {
135
+ /** The unique identifier for this entry. */
136
+ readonly entryId: string;
137
+ /** Indicates the type of entry (e.g., "request", "tombstone"). */
138
+ readonly entryType: string;
139
+ /** When this entry was created. Format: RFC3339 with microsecond precision. */
140
+ readonly timestamp: string;
141
+ /** The batch run identifier that linked this entry. */
142
+ readonly runId: string;
143
+ /** The 0-indexed position within the batch run. */
144
+ readonly sequenceNum: number;
145
+ /**
146
+ * The global sequence number across all runs.
147
+ *
148
+ * A 64-bit integer on the wire (format-spec §2.1). `parseEntriesText` returns
149
+ * a `number` up to `Number.MAX_SAFE_INTEGER` and a `bigint` beyond it, so the
150
+ * value is never rounded. Pass a `bigint` yourself for anything above 2^53.
151
+ */
152
+ readonly globalSeq: number | bigint;
153
+ /** The SHA-512 hash of the entry content (128 hex chars), or tombstone content hash. */
154
+ readonly contentHash: string;
155
+ /** The chain hash linking this entry to its predecessor. */
156
+ readonly chainHash: string;
157
+ /**
158
+ * v4 only: the entry's salt, 64 lowercase hex (format-spec §2.0). Absent on an
159
+ * erased entry. On a v2 or v3 entry it is `format_field_misuse` (§2.2).
160
+ */
161
+ readonly salt?: string;
162
+ /**
163
+ * v4 only: the leaf hash the chain hash covers, 128 lowercase hex
164
+ * (format-spec §2.0). Kept by erasure. On a v2 or v3 entry it is
165
+ * `format_field_misuse`.
166
+ */
167
+ readonly leafHash?: string;
168
+ /**
169
+ * v4 only: `true` on an erased entry (format-spec §5.0), which then carries
170
+ * no id, type, content hash or salt. A live entry carries no `erased` member:
171
+ * `false` is refused (`invalid_field`).
172
+ */
173
+ readonly erased?: boolean;
174
+ /**
175
+ * The snake_case names of the members the entry carried, as
176
+ * `parseEntriesText` read it. Set on v4 entries only.
177
+ *
178
+ * v4 tells an ABSENT member from an empty one (format-spec §2.0: run fields
179
+ * are "absent, not zero"; an erased entry's id is absent, not empty), which
180
+ * the required string fields of this interface cannot. When this is not set
181
+ * (an entry built in code), an empty `entryId` / `entryType` /
182
+ * `contentHash` / `runId`, a zero `sequenceNum`, and an undefined `salt` /
183
+ * `leafHash` / `erased` count as absent.
184
+ */
185
+ readonly presentMembers?: ReadonlySet<string>;
186
+ /**
187
+ * Which preimage produced `chainHash`: 2, 3 or 4.
188
+ *
189
+ * ZERO OR ABSENT MEANS V2. Entries written before this field existed carry no
190
+ * value for it, so treating its absence as v2 is what keeps every chain in
191
+ * existence verifiable. The two formats cannot be told apart by looking at a
192
+ * digest, so a verifier that guesses is one that reports false breaks.
193
+ */
194
+ readonly formatVersion?: number;
195
+ }
196
+ /**
197
+ * Break types that can be detected during chain verification.
198
+ */
199
+ type BreakType = "hash_mismatch" | "sequence_gap" | "invalid_tombstone" | "anchor_mismatch"
200
+ /**
201
+ * The entry declares a format this build cannot recompute.
202
+ *
203
+ * NOT tamper evidence. Reporting it as a hash mismatch would tell a reader
204
+ * their chain was altered when it was not.
205
+ */
206
+ | "unknown_format"
207
+ /**
208
+ * The entry carries a field its declared format does not bind — a v3 entry
209
+ * with runId or sequenceNum, which invites a reader to treat them as attested
210
+ * when they are free to change.
211
+ */
212
+ | "format_field_misuse"
213
+ /**
214
+ * A field is outside its fixed shape (format-spec §5.5): a `chainHash` or
215
+ * `contentHash` that is not a digest, a negative `globalSeq`, a v2 entry with
216
+ * a negative `sequenceNum` or an empty `runId`, or a malformed predecessor
217
+ * hash. Checked BEFORE recomputing: a hash recomputed over malformed fields
218
+ * can equal the stored one. Named `malformed_field` before format-spec §5.5.
219
+ */
220
+ | "invalid_field"
221
+ /**
222
+ * The stored timestamp is not exactly `YYYY-MM-DDTHH:MM:SS.ffffffZ`
223
+ * (format-spec §4). Another spelling of the same instant is still a break:
224
+ * the hash covers one spelling.
225
+ */
226
+ | "invalid_timestamp";
227
+ /**
228
+ * A break found during offline verification.
229
+ *
230
+ * Contains details about an inconsistency detected during chain
231
+ * verification, including the position, entry ID, and type of break.
232
+ *
233
+ * WARNING (M-4): Contains entryId and hashes that may correlate to PII.
234
+ * Do not log breaks directly — wrap with `redactedOfflineBreak(brk)`
235
+ * from `./redactedView`. Even 8-char hash prefixes can leak audit-
236
+ * chain structure; the opaque view is the only sanctioned logging
237
+ * shape.
238
+ */
239
+ interface OfflineBreak {
240
+ /** The 0-indexed position in the entries list where the break was detected. */
241
+ readonly position: number;
242
+ /** The entryId of the entry where the break was detected. */
243
+ readonly entryId: string;
244
+ /** The expected chain hash (computed from inputs). */
245
+ readonly expectedHash: string;
246
+ /** The actual chain hash found in the entry. */
247
+ readonly actualHash: string;
248
+ /** Categorizes the break. */
249
+ readonly breakType: BreakType;
250
+ /** Additional context about the break. */
251
+ readonly context: Readonly<Record<string, string>>;
252
+ }
253
+ /**
254
+ * Result of offline chain verification.
255
+ *
256
+ * Summarizes the results of verifying a chain export against an anchor.
257
+ *
258
+ * WARNING (M-8): Do not log durationMs in production — it can reveal chain
259
+ * size and business volume. If logging is required, log only the
260
+ * boolean `valid` field; do not pass the result object to a logger.
261
+ */
262
+ interface OfflineVerificationResult {
263
+ /** True if no breaks were found. */
264
+ readonly valid: boolean;
265
+ /**
266
+ * The number of entries walked that a tombstone does not leave exposed:
267
+ * `entriesProcessed - entriesNotVerified`.
268
+ *
269
+ * Entries up to and including the last tombstone are NOT counted here
270
+ * (format-spec §5.5): a tombstone's chain hash is taken as stored, so nothing
271
+ * links the entries before it to the ones after. Equal to `entriesProcessed`
272
+ * when the chain has no tombstone.
273
+ */
274
+ readonly entriesVerified: number;
275
+ /** The number of entries walked (after `maxEntries`), whatever their status. */
276
+ readonly entriesProcessed: number;
277
+ /** The number of tombstone entries encountered. */
278
+ readonly tombstonesFound: number;
279
+ /**
280
+ * The number of v4 entries carrying `erased: true`, malformed or not
281
+ * (format-spec §5.5). An erased v4 entry is still recomputed (§5.0), so it is
282
+ * verified like any other; this only counts them.
283
+ */
284
+ readonly erasedFound: number;
285
+ /**
286
+ * The 0-indexed position of the last tombstone (erased entry) in the entries
287
+ * walked, or -1 when there is none.
288
+ */
289
+ readonly lastTombstone: number;
290
+ /**
291
+ * How many entries this result leaves exposed: every entry up to and
292
+ * including the last tombstone (`lastTombstone + 1`; 0 when there is none).
293
+ *
294
+ * A verifier cannot detect a rewrite of these entries (format-spec §5.5).
295
+ * Never describe them as verified, or as bound by an anchor.
296
+ */
297
+ readonly entriesNotVerified: number;
298
+ /**
299
+ * The outcome of binding `options.anchor`, when one was supplied. Any
300
+ * outcome other than `"bound"` or `"chain_broken"` also appears as an
301
+ * `anchor_mismatch` break.
302
+ */
303
+ readonly anchorOutcome?: AnchorBindOutcome;
304
+ /** Details about any chain breaks found. */
305
+ readonly breaks: readonly OfflineBreak[];
306
+ /** How long verification took in milliseconds. Do not log in production. */
307
+ readonly durationMs: number;
308
+ }
309
+ /**
310
+ * Options for chain verification.
311
+ *
312
+ * Allows customization of chain verification, including setting
313
+ * resource limits to prevent OOM on large chains.
314
+ */
315
+ interface ChainVerifyOptions {
316
+ /**
317
+ * Bind the verified chain to a signed Aleutian anchor.
318
+ *
319
+ * When set, after walking the entries the verifier recomputes the
320
+ * expected anchor chain hash via `computeAnchorChainHash` and
321
+ * compares against `anchor.chainHash`. Mismatch emits an
322
+ * `anchor_mismatch` {@link OfflineBreak}.
323
+ *
324
+ * Omit to skip anchor binding (per-entry chain integrity still checked).
325
+ */
326
+ readonly anchor?: Anchor;
327
+ /**
328
+ * Chain hash of the anchor immediately preceding `anchor` in the
329
+ * anchor-of-anchors chain. Required when `anchor` is set.
330
+ *
331
+ * For genesis anchors (`anchor.previousAnchorId === SEED_ANCHOR_ID`)
332
+ * pass `SEED_ANCHOR_HASH`, or omit — the SDK falls back to
333
+ * `SEED_ANCHOR_HASH` automatically. For mid-chain anchors callers
334
+ * MUST fetch the prior anchor and supply its `chainHash` here;
335
+ * otherwise no trust can be established and verification emits
336
+ * `anchor_mismatch`.
337
+ */
338
+ readonly previousAnchorHash?: string;
339
+ /**
340
+ * The chain hash of the entry immediately before `entries[0]`, to verify a
341
+ * SEGMENT of a chain. Empty or omitted for a whole chain.
342
+ *
343
+ * Must be exactly 128 lowercase hex characters; anything else is an
344
+ * `invalid_field` break at position 0. A segment cannot be bound to an
345
+ * anchor (format-spec §9.8): passing this together with `anchor` throws.
346
+ */
347
+ readonly previousHash?: string;
348
+ /**
349
+ * Deprecated and broken: byte-compares the walked tip chain_hash
350
+ * against a caller-supplied hash. Use `anchor` + `previousAnchorHash`
351
+ * instead.
352
+ *
353
+ * Kept for backwards compatibility with prior 0.x callers; will be
354
+ * removed in the next minor release. Cannot bind to real
355
+ * Aleutian-produced anchors because `anchor.chain_hash` lives in a
356
+ * different SHA-512 domain than the entry's `chain_hash`.
357
+ *
358
+ * @deprecated pass `anchor` + `previousAnchorHash` for real binding.
359
+ */
360
+ readonly expectedLastHash?: string;
361
+ /** Limits entries processed. 0 = unlimited. Use this to prevent OOM on very large exports. */
362
+ readonly maxEntries?: number;
363
+ /** Stops verification early if this many breaks are found. 0 = unlimited. */
364
+ readonly maxBreaks?: number;
365
+ }
366
+ /**
367
+ * The outcome of a keyless bind of an anchor to entries (format-spec §9.7,
368
+ * §9.8). Only `"bound"` is a bind.
369
+ *
370
+ * - `chain_broken`: the entries do not verify, so nothing is bound
371
+ * - `range_start_mismatch`: the first entry given is not `range.startEntryId`
372
+ * - `range_end_mismatch`: the last entry given is not `range.endEntryId`
373
+ * (which is what a chain that has grown since the anchor reports: pass the
374
+ * covered prefix)
375
+ * - `height_mismatch`: the number of entries given is not `entryCount`
376
+ * - `predecessor_mismatch`: `previousAnchorId` is the seed id but the
377
+ * predecessor hash is not the seed hash, or the reverse
378
+ * - `head_mismatch`: the recomputed anchor chain hash is not the anchor's
379
+ */
380
+ type AnchorBindOutcome = "bound" | "chain_broken" | "range_start_mismatch" | "range_end_mismatch" | "height_mismatch" | "head_mismatch" | "predecessor_mismatch"
381
+ /** v7: the caller expected another subject (format-spec §9.7 step 5). */
382
+ | "tenant_mismatch";
383
+ /**
384
+ * Result of a keyless bind of an anchor to entries.
385
+ *
386
+ * KEYLESS: the signature is not checked here. A bind says the anchor's fields
387
+ * describe these entries; `verifyAnchorLocally` says who signed them.
388
+ */
389
+ interface AnchorBindResult {
390
+ /** What the bind found. */
391
+ readonly outcome: AnchorBindOutcome;
392
+ /** True only when `outcome === "bound"`. */
393
+ readonly bound: boolean;
394
+ /** Tombstones among the entries given, in every outcome. */
395
+ readonly tombstones: number;
396
+ /**
397
+ * Entries that the anchor does NOT bind even when it binds: every entry up
398
+ * to and including the last tombstone (format-spec §5.5). 0 when there is no
399
+ * tombstone. The walked value in every outcome. Never describe these entries
400
+ * as verified or as bound; when `bound` is false, no entry is bound at all.
401
+ */
402
+ readonly entriesNotBound: number;
403
+ /** The chain walk over the entries given. */
404
+ readonly chain: OfflineVerificationResult;
405
+ /**
406
+ * v7 only: whether the anchor's subject was compared with one the caller
407
+ * expected (format-spec §9.7). `false` means it was NOT checked: the
408
+ * signature proves who signed the anchor, not that it is about the chain the
409
+ * reader meant.
410
+ */
411
+ readonly subjectChecked?: boolean;
412
+ }
413
+ /**
414
+ * What a caller expects of a v7 anchor it binds (format-spec §9.7). Empty or
415
+ * omitted means "not given".
416
+ */
417
+ interface AnchorBindExpectations {
418
+ /** The subject the anchor must be about, compared byte for byte (`tenant_mismatch`). */
419
+ readonly subject?: string;
420
+ /** The predecessor's chain hash the anchor must name (`predecessor_mismatch`). */
421
+ readonly previousAnchorHash?: string;
422
+ /**
423
+ * The chain hash of the entry before `entries[0]`. Any non-empty value is
424
+ * refused: an anchor is never bound to a segment (format-spec §9.8).
425
+ */
426
+ readonly previousHash?: string;
427
+ }
428
+
429
+ /**
430
+ * Error thrown when anchor signature verification fails.
431
+ */
432
+ declare class AnchorVerificationError extends Error {
433
+ readonly name = "AnchorVerificationError";
434
+ constructor(message: string);
435
+ }
436
+ /**
437
+ * Error thrown when chain verification encounters an unrecoverable error.
438
+ */
439
+ declare class ChainVerificationError extends Error {
440
+ readonly name = "ChainVerificationError";
441
+ constructor(message: string);
442
+ }
443
+ /**
444
+ * Thrown when the text of an entries file is not entries (format-spec §11):
445
+ * not JSON, an empty document, an export bundle, an anchor, or an entry with a
446
+ * repeated, misspelled, missing or wrongly typed member.
447
+ *
448
+ * This is an ERROR, never a broken chain: a file that is not entries says
449
+ * nothing about any chain. Messages name a member, never a value.
450
+ */
451
+ declare class EntriesFormatError extends Error {
452
+ readonly name = "EntriesFormatError";
453
+ constructor(message: string);
454
+ }
455
+ /**
456
+ * Thrown when a ConsentChainEntry fails canonical field validation.
457
+ *
458
+ * Error messages include the field name but NEVER the field value, to prevent
459
+ * log injection from caller-controlled content.
460
+ */
461
+ declare class InvalidConsentEntryError extends Error {
462
+ readonly name = "InvalidConsentEntryError";
463
+ constructor(message: string);
464
+ }
465
+ /**
466
+ * Thrown when consent chain entry verification fails for cryptographic reasons:
467
+ * unknown key ID, wrong signature length, content hash mismatch, or failed
468
+ * ML-DSA-65 verification.
469
+ *
470
+ * Error messages do not include signature bytes or key material.
471
+ */
472
+ declare class ConsentVerificationError extends Error {
473
+ readonly name = "ConsentVerificationError";
474
+ constructor(message: string);
475
+ }
476
+
477
+ /**
478
+ * Domain prefix for chain hash computation.
479
+ * This provides domain separation for the hash function.
480
+ */
481
+ declare const CHAIN_HASH_PREFIX = "aleutian.chain.v2:";
482
+ /**
483
+ * Report whether a value is a SHA-512 digest as the format writes it: exactly
484
+ * 128 lowercase hex characters (format-spec §1). Uppercase is invalid, not
485
+ * equivalent.
486
+ *
487
+ * @param value - The value to check; anything but a string is not a digest
488
+ * @returns True for exactly 128 lowercase hex characters
489
+ */
490
+ declare function isChainDigest(value: unknown): value is string;
491
+ /**
492
+ * Report whether a stored timestamp is exactly the form the chain hash covers:
493
+ * `YYYY-MM-DDTHH:MM:SS.ffffffZ` (format-spec §4), naming a real calendar
494
+ * instant.
495
+ *
496
+ * Any other spelling of the same instant (an offset such as `+05:30`, no
497
+ * fractional digits, three, or more than six, a lowercase `z`) is refused: the
498
+ * hash covers one spelling, and a second one that hashed the same would be a
499
+ * field an editor could change without detection.
500
+ *
501
+ * @param value - The stored timestamp; anything but a string is refused
502
+ * @returns True only for the exact §4 form
503
+ */
504
+ declare function isChainTimestamp(value: unknown): value is string;
505
+ /**
506
+ * Compute the SHA-512 chain hash for an entry.
507
+ *
508
+ * Uses Aleutian's chain hash formula with domain separation:
509
+ * SHA512("aleutian.chain.v2:" + prev + "|" + runID + "|" + seq + "|" + ts + "|" + content)
510
+ *
511
+ * @param previousHash - Previous entry's chain_hash (empty string for first in run)
512
+ * @param runId - Batch run identifier
513
+ * @param sequenceNum - 0-indexed position within batch
514
+ * @param timestamp - Entry timestamp in ISO 8601 format with microsecond precision
515
+ * @param contentHash - SHA-512 hex of content, or tombstone content hash
516
+ * @returns Promise resolving to 128-character lowercase hex SHA-512 hash
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * const hash = await computeChainHash(
521
+ * 'abc123...',
522
+ * 'run_xyz',
523
+ * 42,
524
+ * '2026-01-20T15:30:00.123456Z',
525
+ * 'def456...'
526
+ * );
527
+ * ```
528
+ */
529
+ declare function computeChainHash(previousHash: string, runId: string, sequenceNum: number, timestamp: string, contentHash: string): Promise<string>;
530
+ /**
531
+ * Format a Unix millisecond timestamp as ISO 8601 with microsecond precision.
532
+ *
533
+ * Converts Unix milliseconds to the format: "2006-01-02T15:04:05.000000Z"
534
+ * Since JavaScript Date only has millisecond precision, the last 3 digits
535
+ * of the microseconds are always "000".
536
+ *
537
+ * @param timestampMs - Unix timestamp in milliseconds
538
+ * @returns ISO 8601 string with 6-digit microseconds (last 3 always "000")
539
+ */
540
+ declare function formatTimestampMicro(timestampMs: number): string;
541
+ /**
542
+ * Compute chain hash using Unix milliseconds timestamp.
543
+ *
544
+ * This is a convenience function that converts Unix milliseconds to
545
+ * ISO 8601 format before computing the hash. Use this when you have
546
+ * a numeric timestamp instead of a string.
547
+ *
548
+ * @param previousHash - Previous entry's chain_hash (empty string for first in run)
549
+ * @param runId - Batch run identifier
550
+ * @param sequenceNum - 0-indexed position within batch
551
+ * @param timestampMs - Entry timestamp as Unix milliseconds. Sub-millisecond
552
+ * precision is discarded. Always use `Date.now()` or `time.UnixMilli()` values
553
+ * rather than dividing a nanosecond timestamp, which silently truncates and
554
+ * produces a hash that won't match Go/Python.
555
+ * @param contentHash - SHA-512 hex of content, or tombstone content hash
556
+ * @returns Promise resolving to 128-character lowercase hex SHA-512 hash
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * const hash = await computeChainHashMs(
561
+ * 'abc123...',
562
+ * 'run_xyz',
563
+ * 42,
564
+ * 1706108400123,
565
+ * 'def456...'
566
+ * );
567
+ * ```
568
+ */
569
+ declare function computeChainHashMs(previousHash: string, runId: string, sequenceNum: number, timestampMs: number, contentHash: string): Promise<string>;
570
+
571
+ /**
572
+ * SHA-512 domain separator for the anchor-of-anchors chain.
573
+ * Must be byte-identical across Go/Python/JS SDKs.
574
+ */
575
+ declare const ANCHOR_CHAIN_DOMAIN_V2 = "aleutian.anchor.v2:";
576
+ /**
577
+ * Genesis previousAnchorHash used for the very first anchor in a
578
+ * company's chain. Computed as `SHA-512("aleutian.anchor.seed.v2")`.
579
+ *
580
+ * Byte-identical to the producer constant at
581
+ * `internal/storage/verification_types.go` and the other SDK constants.
582
+ */
583
+ declare const SEED_ANCHOR_HASH: string;
584
+ /**
585
+ * Sentinel `previousAnchorId` value indicating a genesis anchor.
586
+ */
587
+ declare const SEED_ANCHOR_ID = "anchor_00000000-0000-0000-0000-000000000000";
588
+ /**
589
+ * Bind an anchor to its tip entry and prior anchor.
590
+ *
591
+ * Returns a Promise resolving to the hex-encoded SHA-512 digest over a
592
+ * domain-separated, pipe-delimited concatenation of the inputs.
593
+ * Byte-identical to the Go SDK `ComputeAnchorChainHash` and the producer
594
+ * at `internal/chainlinker/anchor_generator.go`.
595
+ *
596
+ * Async because the JS SDK uses WebCrypto (`crypto.subtle.digest`), which
597
+ * is always async — same pattern as `computeChainHash`.
598
+ *
599
+ * @param previousAnchorHash - The prior anchor's `chain_hash`, or
600
+ * {@link SEED_ANCHOR_HASH} for a genesis anchor
601
+ * (`previousAnchorId === SEED_ANCHOR_ID`).
602
+ * @param companyId - The `company_id` field of the anchor being computed.
603
+ * @param startEntryId - `anchor.range.startEntryId`.
604
+ * @param endEntryId - `anchor.range.endEntryId`.
605
+ * @param tipChainHash - The chain_hash of the entry identified by
606
+ * `endEntryId`, as computed by walking the per-entry chain (NOT the
607
+ * stored value if verifying potentially-tampered data — the verifier
608
+ * walks and recomputes).
609
+ * @returns Promise resolving to hex-encoded 128-char SHA-512 digest.
610
+ */
611
+ declare function computeAnchorChainHash(previousAnchorHash: string, companyId: string, startEntryId: string, endEntryId: string, tipChainHash: string): Promise<string>;
612
+ /**
613
+ * SHA-512 domain separator for the v7 anchor chain hash (format-spec §9.4).
614
+ */
615
+ declare const ANCHOR_CHAIN_DOMAIN_V7 = "aleutian.anchor.v7:";
616
+ /**
617
+ * Compute a v7 anchor's chain hash (format-spec §9.4).
618
+ *
619
+ * # Description
620
+ *
621
+ * `SHA-512("aleutian.anchor.v7:" ‖ previous_anchor_hash ‖ "|" ‖ len(subject)
622
+ * ‖ subject ‖ "|" ‖ start_seq ‖ "|" ‖ end_seq ‖ "|" ‖ tip_chain_hash)`, `len`
623
+ * a 4-byte big-endian byte count. The subject is hashed as its UTF-8 bytes,
624
+ * never normalised, and is length-prefixed, so it may contain `|`.
625
+ *
626
+ * Unlike v1-v6, the range is named by sequence: erasing any entry of it (v4,
627
+ * format-spec §5.0) changes nothing this hash covers.
628
+ *
629
+ * # Inputs
630
+ *
631
+ * - previousAnchorHash: the anchor's own `previous_anchor_hash` (the
632
+ * previous anchor's chain hash, or {@link SEED_ANCHOR_HASH}); 128 hex
633
+ * - subject: the anchor's subject, non-empty
634
+ * - startSeq, endSeq: the range, 0 to 2^63 - 1, as bigint, safe number or
635
+ * decimal text
636
+ * - tipChainHash: the chain hash of the entry at `endSeq`; 128 hex
637
+ *
638
+ * # Outputs
639
+ *
640
+ * - Promise<string>: 128 lowercase hex
641
+ * - Error: thrown (as a rejection) when an input is malformed
642
+ *
643
+ * # Example
644
+ *
645
+ * const h = await computeAnchorChainHashV7(SEED_ANCHOR_HASH, "orders", 0n, 2n, tip);
646
+ *
647
+ * # Limitations
648
+ *
649
+ * - Not a bind: compare the result with the anchor's `chainHash`.
650
+ *
651
+ * # Assumptions
652
+ *
653
+ * - None.
654
+ */
655
+ declare function computeAnchorChainHashV7(previousAnchorHash: string, subject: string, startSeq: bigint | number | string, endSeq: bigint | number | string, tipChainHash: string): Promise<string>;
656
+
657
+ /**
658
+ * Tombstone detection and validation for the Aleutian verification SDK.
659
+ *
660
+ * Tombstones are GDPR deletion markers that replace original entries while
661
+ * preserving chain integrity. They have specific format requirements that
662
+ * must be validated during chain verification.
663
+ */
664
+ /**
665
+ * Prefix for tombstone content hashes.
666
+ */
667
+ declare const TOMBSTONE_CONTENT_HASH_PREFIX = "TOMBSTONE:";
668
+ /**
669
+ * Prefix for tombstone entry IDs.
670
+ */
671
+ declare const TOMBSTONE_ENTRY_ID_PREFIX = "tomb_";
672
+ /**
673
+ * Entry type value for tombstones.
674
+ */
675
+ declare const TOMBSTONE_ENTRY_TYPE = "tombstone";
676
+ /**
677
+ * Check if an entry is a tombstone based on entryType and entryId.
678
+ *
679
+ * Both conditions must be true:
680
+ * - entryType === "tombstone"
681
+ * - entryId starts with "tomb_"
682
+ *
683
+ * @param entryType - The entry's type field
684
+ * @param entryId - The entry's ID field
685
+ * @returns True if entry is a tombstone
686
+ *
687
+ * @example
688
+ * ```typescript
689
+ * if (isTombstone(entry.entryType, entry.entryId)) {
690
+ * // Handle tombstone entry
691
+ * }
692
+ * ```
693
+ */
694
+ declare function isTombstone(entryType: string, entryId: string): boolean;
695
+ /**
696
+ * Check if a contentHash uses tombstone format (prefix check only).
697
+ *
698
+ * This is a lightweight check for hot paths. Use validateTombstoneContentHash
699
+ * for full format validation.
700
+ *
701
+ * @param contentHash - The content hash to check
702
+ * @returns True if content hash starts with "TOMBSTONE:"
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * if (isTombstoneContentHash(entry.contentHash)) {
707
+ * // Content was deleted, original data not available
708
+ * }
709
+ * ```
710
+ */
711
+ declare function isTombstoneContentHash(contentHash: string): boolean;
712
+ /**
713
+ * Fully validate tombstone content hash format.
714
+ *
715
+ * Valid format: "TOMBSTONE:" + 64 lowercase hex characters (32 random bytes)
716
+ * Total length: 74 characters (10 prefix + 64 hex)
717
+ *
718
+ * @param contentHash - The content hash to validate
719
+ * @returns True if format is valid
720
+ *
721
+ * @example
722
+ * ```typescript
723
+ * if (isTombstone(entry.entryType, entry.entryId)) {
724
+ * if (!validateTombstoneContentHash(entry.contentHash)) {
725
+ * // Invalid tombstone format - chain integrity issue
726
+ * }
727
+ * }
728
+ * ```
729
+ */
730
+ declare function validateTombstoneContentHash(contentHash: string): boolean;
731
+
732
+ /** ML-DSA-65 raw public key size per FIPS 204. */
733
+ declare const MLDSA_PUB_KEY_SIZE = 1952;
734
+ /**
735
+ * URL for fetching current public keys from Aleutian's servers.
736
+ *
737
+ * This endpoint can be used to:
738
+ * 1. Check for key revocations
739
+ * 2. Fetch newly rotated keys before SDK updates
740
+ * 3. Verify embedded keys match server-side keys
741
+ *
742
+ * For fully offline verification, use the embedded keys instead.
743
+ */
744
+ declare const PUBLIC_KEYS_ENDPOINT = "https://keys.aleutian.ai/.well-known/anchor-signing-keys.json";
745
+ /**
746
+ * Embedded ML-DSA-65 public keys for offline anchor verification.
747
+ *
748
+ * Keys are raw 1952-byte Uint8Arrays, decoded from SPKI DER at load time.
749
+ */
750
+ declare const EMBEDDED_PUBLIC_KEYS: ReadonlyMap<string, Uint8Array>;
751
+ /**
752
+ * Get an embedded public key by ID.
753
+ *
754
+ * @param keyId - The key identifier from anchor.signature.publicKeyId
755
+ * @returns The raw 1952-byte ML-DSA-65 public key bytes
756
+ * @throws Error if key ID is not found in embedded keys
757
+ */
758
+ declare function getPublicKey(keyId: string): Uint8Array;
759
+ /**
760
+ * Check if a key ID is known (exists in embedded keys).
761
+ *
762
+ * @param keyId - The key identifier to check
763
+ * @returns True if the key ID is known
764
+ */
765
+ declare function isKnownKey(keyId: string): boolean;
766
+ /**
767
+ * List all known key IDs.
768
+ *
769
+ * @returns Array of key IDs sorted alphabetically
770
+ */
771
+ declare function listKeyIds(): string[];
772
+ /**
773
+ * SC-1: Validate that no embedded key is an all-zero development placeholder.
774
+ *
775
+ * Production deployments MUST call this function at startup and handle the
776
+ * thrown error (e.g., fail fast before serving traffic). The embedded keys
777
+ * in this SDK release are development placeholders and MUST be replaced with
778
+ * real keys before production use.
779
+ *
780
+ * @throws Error if any embedded key consists entirely of zero bytes
781
+ */
782
+ declare function validateEmbeddedKeys(): void;
783
+
784
+ /**
785
+ * The ML-DSA context a v7 anchor signature is made with (format-spec §9.5).
786
+ * v1-v6 use the empty context.
787
+ */
788
+ declare const ANCHOR_SIGNATURE_CONTEXT_V7 = "aleutian.proof.anchor.v7";
789
+ /**
790
+ * Refuse a malformed anchor (format-spec §9.3 and §9.8).
791
+ *
792
+ * # Description
793
+ *
794
+ * Checks what an anchor must satisfy before its canonical form is built or it
795
+ * is bound to a chain:
796
+ *
797
+ * - `version` is 1, 2, 3, 4, 6 or 7. Version 5 is withdrawn and is refused
798
+ * by name; any other version is refused, never treated as the closest form.
799
+ * - v7 (§9.3, §9.8): a non-empty `subject`, `anchorId` and
800
+ * `previousAnchorId`, each valid UTF-8 (no lone surrogate) as is
801
+ * `signingKeyId`; `range.startSeq` and
802
+ * `range.endSeq` integers from 0 to 2^63 - 1 with `startSeq <= endSeq`;
803
+ * `entryCount = endSeq - startSeq + 1`; `previousAnchorHash` 128 lowercase
804
+ * hex; `previousAnchorId` is the seed id exactly when `previousAnchorHash`
805
+ * is the seed hash; `chainHash` 128 lowercase hex; `createdAtMs >= 0`; no
806
+ * `verifiedThrough` and no entry-id range. v1-v6 carry no
807
+ * `previousAnchorHash` and no sequence range.
808
+ * - version invariants (§9.3): v1-v3 carry `verifiedThrough` 0; v4 and v6
809
+ * carry it positive; v6 carries a non-empty `subject`.
810
+ * - counts, v4 and v6 (§9.8): `entryCount >= 1`,
811
+ * `0 < verifiedThrough <= entryCount`, `createdAtMs >= 0`.
812
+ * - `signingKeyId` is not empty (§9.8).
813
+ *
814
+ * `version` is inside the signed bytes, so an anchor that breaks these carries
815
+ * a valid signature over malformed content. That is reported as malformed, not
816
+ * as tampering, and never as a bind outcome.
817
+ *
818
+ * # Inputs
819
+ *
820
+ * - anchor: The anchor to check
821
+ *
822
+ * # Outputs
823
+ *
824
+ * - void: Returns normally when the anchor is well formed
825
+ * - AnchorVerificationError: Thrown otherwise. Messages name a field, never
826
+ * its value.
827
+ *
828
+ * # Example
829
+ *
830
+ * checkAnchorWellFormed(parseAnchorText(text));
831
+ *
832
+ * # Limitations
833
+ *
834
+ * - Checks neither the signature nor the binding to a chain.
835
+ *
836
+ * # Assumptions
837
+ *
838
+ * - None.
839
+ */
840
+ declare function checkAnchorWellFormed(anchor: Anchor): void;
841
+ /**
842
+ * Version=7 canonical form (format-spec §9.2): the bytes a v7 signature covers.
843
+ *
844
+ * Ten fields, alphabetical: `anchor_id`, `chain_hash`, `created_at_ms`,
845
+ * `entry_count`, `previous_anchor_hash`, `previous_anchor_id`, `range`
846
+ * (`end_seq`, `start_seq`), `signing_key_id`, `subject`, `version`.
847
+ * `previous_anchor_hash` sorts before `previous_anchor_id`. `entry_count`,
848
+ * `end_seq` and `start_seq` are JSON STRINGS of their decimal value, so that a
849
+ * value above 2^53 survives every JSON reader exactly.
850
+ *
851
+ * Builds bytes only: call `checkAnchorWellFormed` first (`verifyAnchorLocally`
852
+ * does).
853
+ */
854
+ declare function canonicalizeAnchorV7(anchor: Anchor): string;
855
+
856
+ /**
857
+ * Verify an anchor signature without network access.
858
+ *
859
+ * # Description
860
+ *
861
+ * Verifies the ML-DSA-65 (FIPS 204) signature of an anchor using embedded
862
+ * or provided public keys. This enables fully offline anchor verification.
863
+ *
864
+ * No SHA-256 pre-hash is applied — ML-DSA-65 handles message hashing
865
+ * internally per FIPS 204, matching the Cloud KMS signing path.
866
+ *
867
+ * # Inputs
868
+ *
869
+ * - anchor: The anchor to verify
870
+ * - publicKey: Optional raw 1952-byte ML-DSA-65 public key. If undefined,
871
+ * uses embedded keys based on anchor.signingKeyId
872
+ *
873
+ * # Outputs
874
+ *
875
+ * - void: Returns normally if verification succeeds
876
+ * - AnchorVerificationError: Thrown if signature verification fails
877
+ * - Error: Thrown if public key ID is not found in embedded keys
878
+ *
879
+ * # Example
880
+ *
881
+ * const anchor = await loadAnchor('anchor-2026-01-20.json');
882
+ * try {
883
+ * verifyAnchorLocally(anchor);
884
+ * console.log('Anchor is cryptographically valid');
885
+ * } catch (e) {
886
+ * console.error('Verification failed:', e.message);
887
+ * }
888
+ *
889
+ * # Limitations
890
+ *
891
+ * - Requires embedded keys to be non-placeholder for production use
892
+ * - Does not verify anchor chain linkage (previousAnchorId), and takes no
893
+ * predecessor hash: binding an anchor to a chain and to its predecessor is
894
+ * `bindAnchorToEntries`
895
+ * - Refuses a malformed anchor before any cryptography (format-spec §9.3,
896
+ * §9.8): an unsupported or withdrawn version, a broken version invariant,
897
+ * v4/v6 counts out of range, an empty signingKeyId
898
+ *
899
+ * # Assumptions
900
+ *
901
+ * - anchor.signature is the canonical base64 (standard alphabet, padded, no
902
+ * line breaks, zero trailing bits) of an ML-DSA-65 signature (3309 bytes)
903
+ * - publicKey, if provided, is exactly MLDSA_PUB_KEY_SIZE (1952) bytes
904
+ */
905
+ declare function verifyAnchorLocally(anchor: Anchor, publicKey?: Uint8Array): void;
906
+ declare function canonicalizeAnchor(anchor: Anchor): string;
907
+ declare function parseAnchor(data: Record<string, unknown>): Anchor;
908
+ declare function parseExportedEntries(data: readonly Record<string, unknown>[]): ExportedEntry[];
909
+
910
+ /**
911
+ * Bind an anchor to the entries it covers, without a key.
912
+ *
913
+ * # Description
914
+ *
915
+ * Verifies the entries as a WHOLE chain (from its first entry: a segment
916
+ * cannot be bound, format-spec §9.8), then checks that the anchor describes
917
+ * EXACTLY those entries, in this order: the chain verifies, the first entry is
918
+ * `range.startEntryId`, the last is `range.endEntryId`, their number is
919
+ * `entryCount`, the predecessor is named consistently (§9.8), and the anchor
920
+ * chain hash recomputes (§9.4).
921
+ *
922
+ * A v7 anchor names its range by sequence and carries its predecessor's hash:
923
+ * it is bound by `bindAnchorV7ToEntries`, to which this delegates, with
924
+ * `previousAnchorHash` as the EXPECTED predecessor (§9.7 step 4) and no
925
+ * expected subject (`subjectChecked` false). What follows is v1-v6.
926
+ *
927
+ * PASS THE COVERED PREFIX. An anchor is bound against the entries it covered,
928
+ * as the chain was when it was signed (§9.7): entries `0 … entryCount - 1`.
929
+ * This function does not slice. Given a chain that has grown since the anchor,
930
+ * it reports `range_end_mismatch`.
931
+ *
932
+ * KEYLESS: the signature is NOT checked. Use `verifyAnchorLocally` for that; a
933
+ * bind without a signature check says only that the anchor's fields describe
934
+ * these entries, not who wrote them.
935
+ *
936
+ * WHAT A TOMBSTONE LEAVES UNBOUND (§5.5). A tombstone's chain hash is taken as
937
+ * stored, so nothing links the entries before it to the anchored tip. Whoever
938
+ * can write the store can replace every entry up to a tombstone and the bind
939
+ * still succeeds. `entriesNotBound` counts those entries. Never describe them
940
+ * as verified or as bound.
941
+ *
942
+ * # Inputs
943
+ *
944
+ * - anchor: The anchor, as `parseAnchorText` returns it
945
+ * - entries: The entries the anchor covers, from the chain's first entry
946
+ * - previousAnchorHash: The previous anchor's `chainHash`, or
947
+ * `SEED_ANCHOR_HASH` for the first anchor. Never guessed.
948
+ *
949
+ * # Outputs
950
+ *
951
+ * - Promise<AnchorBindResult>: the outcome; `bound` is true only for
952
+ * `"bound"`. `tombstones` and `entriesNotBound` are what the walk found,
953
+ * in every outcome.
954
+ * - AnchorVerificationError: the anchor is malformed (§9.3, §9.8 counts and
955
+ * key id), or `previousAnchorHash` is not a digest. An error, not an outcome.
956
+ * - ChainVerificationError: `entries` is empty
957
+ *
958
+ * # Example
959
+ *
960
+ * const anchor = parseAnchorText(anchorText);
961
+ * const entries = parseEntriesText(entriesText);
962
+ * const covered = entries.slice(0, Number(anchor.entryCount));
963
+ * const r = await bindAnchorToEntries(anchor, covered, SEED_ANCHOR_HASH);
964
+ * if (r.bound && r.entriesNotBound > 0) {
965
+ * console.log(`bound, except the first ${r.entriesNotBound} entries`);
966
+ * }
967
+ *
968
+ * # Limitations
969
+ *
970
+ * - Does not verify the signature, and does not check a series of anchors
971
+ * against each other.
972
+ * - Cannot detect a rewrite of the entries before a tombstone.
973
+ *
974
+ * # Assumptions
975
+ *
976
+ * - `entries[0]` is the chain's first entry.
977
+ */
978
+ declare function bindAnchorToEntries(anchor: Anchor, entries: readonly ExportedEntry[], previousAnchorHash: string): Promise<AnchorBindResult>;
979
+ /**
980
+ * Bind a v7 anchor to the entries it covers, without a key (format-spec §9.7).
981
+ *
982
+ * # Description
983
+ *
984
+ * Refuses, as errors with no outcome, an anchor that breaks format-spec §9.3
985
+ * or §9.8 and entries given as a segment (`expect.previousHash`). Then walks
986
+ * the entries from the chain's first entry and checks, in this order, the
987
+ * first failure being the outcome:
988
+ *
989
+ * 1. the chain verifies (§5.5): otherwise `chain_broken`;
990
+ * 2. the first entry's `globalSeq` is `range.startSeq`: otherwise
991
+ * `range_start_mismatch`;
992
+ * 3. an entry has `globalSeq` `range.endSeq`: otherwise `height_mismatch`
993
+ * (the anchor covers entries not given). Entries after it are not the
994
+ * anchor's concern: a chain that has grown since still binds;
995
+ * 4. `expect.previousAnchorHash`, if given, equals the anchor's
996
+ * `previousAnchorHash`: otherwise `predecessor_mismatch`;
997
+ * 5. `expect.subject`, if given, equals the anchor's `subject` byte for
998
+ * byte: otherwise `tenant_mismatch`;
999
+ * 6. the v7 anchor chain hash (§9.4) recomputes to `chainHash`: otherwise
1000
+ * `head_mismatch`. All six: `bound`.
1001
+ *
1002
+ * A v7 anchor carries its predecessor's hash in its signed bytes, so no
1003
+ * predecessor hash is needed to bind it; `expect.previousAnchorHash` only
1004
+ * CHECKS it against the one the caller holds.
1005
+ *
1006
+ * KEYLESS: the signature is NOT checked. Use `verifyAnchorLocally`.
1007
+ *
1008
+ * WHAT IS NOT CHECKED, SAID SO. Without `expect.subject`, `subjectChecked` is
1009
+ * false: the anchor may be about another chain. `entriesNotBound` counts the
1010
+ * entries up to and including the last v2/v3 tombstone at or before the tip
1011
+ * (a v3 prefix continued in v4 keeps exactly its guarantee, §2.5). Never
1012
+ * describe those entries as bound.
1013
+ *
1014
+ * # Inputs
1015
+ *
1016
+ * - anchor: a v7 anchor, as `parseAnchorText` returns it
1017
+ * - entries: the chain from its first entry (it may have grown since)
1018
+ * - expect: the subject and predecessor hash the caller expects; empty or
1019
+ * omitted means not given
1020
+ *
1021
+ * # Outputs
1022
+ *
1023
+ * - Promise<AnchorBindResult>: `bound` only for `"bound"`; `subjectChecked`
1024
+ * always set
1025
+ * - AnchorVerificationError: the anchor is not v7 or is malformed,
1026
+ * `expect.previousHash` is given, or `expect.previousAnchorHash` is not a
1027
+ * digest
1028
+ * - ChainVerificationError: `entries` is empty
1029
+ *
1030
+ * # Example
1031
+ *
1032
+ * const r = await bindAnchorV7ToEntries(parseAnchorText(text), entries, {
1033
+ * subject: "orders",
1034
+ * });
1035
+ *
1036
+ * # Limitations
1037
+ *
1038
+ * - Does not verify the signature, and does not check a series of anchors.
1039
+ *
1040
+ * # Assumptions
1041
+ *
1042
+ * - `entries[0]` is the chain's first entry.
1043
+ */
1044
+ declare function bindAnchorV7ToEntries(anchor: Anchor, entries: readonly ExportedEntry[], expect?: AnchorBindExpectations): Promise<AnchorBindResult>;
1045
+ /**
1046
+ * Verify a complete chain from exported data.
1047
+ *
1048
+ * Performs full chain verification using exported entries without
1049
+ * requiring network access to Aleutian APIs. Verifies field shapes, chain hash
1050
+ * continuity and tombstone format validity (format-spec §2, §4, §5), and
1051
+ * optionally binds the result to an anchor.
1052
+ *
1053
+ * The first `globalSeq` is not constrained: a chain may start at 0, at 1, or
1054
+ * (for a segment, with `options.previousHash`) anywhere.
1055
+ *
1056
+ * WHAT A TOMBSTONE LEAVES UNVERIFIED (format-spec §5.5). `valid` says the
1057
+ * entries are consistent with each other. It does NOT say the entries before a
1058
+ * tombstone are the ones originally written: a tombstone's chain hash is taken
1059
+ * as stored, so whoever can write the store can replace every entry up to it.
1060
+ * `entriesNotVerified` (and `lastTombstone`) report how many entries that is.
1061
+ * Never describe them as verified.
1062
+ *
1063
+ * WARNING (M-4): OfflineBreak contains entryId and hashes that may correlate
1064
+ * to PII. Consider truncating hashes before logging: hash.slice(0, 8) is sufficient.
1065
+ *
1066
+ * @param entries - Array of entries in globalSeq order
1067
+ * @param options - Verification options (anchor, previousHash, maxEntries, maxBreaks)
1068
+ * @returns Verification result with validity, breaks, and statistics
1069
+ *
1070
+ * @throws ChainVerificationError for unrecoverable errors (empty entries, or
1071
+ * an anchor together with `previousHash`)
1072
+ * @throws AnchorVerificationError when `options.anchor` is malformed
1073
+ * (format-spec §9.3, §9.8): an error, not a break
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * import {
1078
+ * loadExportedEntries,
1079
+ * loadAnchor,
1080
+ * verifyChainOffline
1081
+ * } from '@aleutian/verification';
1082
+ *
1083
+ * const entries = await loadExportedEntries('export-2026-01.json');
1084
+ * const anchor = await loadAnchor('anchor-2026-01-20.json');
1085
+ *
1086
+ * const result = await verifyChainOffline(entries, {
1087
+ * anchor,
1088
+ * previousAnchorHash: SEED_ANCHOR_HASH, // genesis; else prior anchor's chainHash
1089
+ * maxBreaks: 100, // Stop early if chain is severely corrupted
1090
+ * });
1091
+ *
1092
+ * if (result.valid) {
1093
+ * console.log(
1094
+ * `Chain consistent: ${result.entriesVerified} entries verified, ` +
1095
+ * `${result.entriesNotVerified} not verified (up to the last tombstone)`
1096
+ * );
1097
+ * } else {
1098
+ * console.log(`Chain invalid: ${result.breaks.length} breaks`);
1099
+ * }
1100
+ * ```
1101
+ */
1102
+ declare function verifyChainOffline(entries: readonly ExportedEntry[], options?: ChainVerifyOptions): Promise<OfflineVerificationResult>;
1103
+
1104
+ /** Chain format version 4 (format-spec §2.2). */
1105
+ declare const FORMAT_V4 = 4;
1106
+ /** The leaf hash's domain string (format-spec §2.0). */
1107
+ declare const CHAIN_LEAF_DOMAIN_V4 = "aleutian.chain.leaf.v4:";
1108
+ /** The v4 chain hash's domain string (format-spec §2.0). */
1109
+ declare const CHAIN_HASH_PREFIX_V4 = "aleutian.chain.v4:";
1110
+ /**
1111
+ * Compute the v4 leaf hash of a live entry (format-spec §2.0).
1112
+ *
1113
+ * # Description
1114
+ *
1115
+ * Validates every field, then hashes the domain, the length-prefixed id and
1116
+ * type (UTF-8, as given), the content hash and the salt.
1117
+ *
1118
+ * # Inputs
1119
+ *
1120
+ * - entryId: 1 to 256 UTF-8 bytes, no control characters; not `tomb_…`
1121
+ * - entryType: as entryId; not `tombstone`
1122
+ * - contentHash: 128 lowercase hex (v4 has no tombstone value)
1123
+ * - salt: 64 lowercase hex
1124
+ *
1125
+ * # Outputs
1126
+ *
1127
+ * - Promise<string>: 128 lowercase hex
1128
+ * - Error: thrown (as a rejection) when a field is outside its shape. The
1129
+ * message names the field, never its value.
1130
+ *
1131
+ * # Example
1132
+ *
1133
+ * const leaf = await computeLeafHashV4("ent_0001", "event", contentHash, salt);
1134
+ *
1135
+ * # Limitations
1136
+ *
1137
+ * - Does not mint salts: this SDK verifies, it never writes.
1138
+ *
1139
+ * # Assumptions
1140
+ *
1141
+ * - None: every field is validated.
1142
+ */
1143
+ declare function computeLeafHashV4(entryId: string, entryType: string, contentHash: string, salt: string): Promise<string>;
1144
+ /**
1145
+ * Compute the v4 chain hash of an entry (format-spec §2.0).
1146
+ *
1147
+ * # Description
1148
+ *
1149
+ * Validates the fields, including the first-entry rule (an empty
1150
+ * `previousHash` goes with `globalSeq` 0, a non-empty one with a `globalSeq`
1151
+ * above 0), then hashes the `|`-delimited preimage. For an erased entry, pass
1152
+ * its stored leaf hash: that is exactly what the verifier recomputes from.
1153
+ *
1154
+ * # Inputs
1155
+ *
1156
+ * - previousHash: "" for the chain's first entry, else 128 lowercase hex
1157
+ * - globalSeq: 0 to 2^63 - 1, as a bigint, a safe number, or base-10 text
1158
+ * - timestamp: exactly `YYYY-MM-DDTHH:MM:SS.ffffffZ` (format-spec §4)
1159
+ * - leafHash: 128 lowercase hex
1160
+ *
1161
+ * # Outputs
1162
+ *
1163
+ * - Promise<string>: 128 lowercase hex
1164
+ * - Error: thrown (as a rejection) when a field is malformed
1165
+ *
1166
+ * # Example
1167
+ *
1168
+ * const h = await computeChainHashV4(prev, 1n, "2026-10-16T08:00:01.111111Z", leaf);
1169
+ *
1170
+ * # Limitations
1171
+ *
1172
+ * - Checks one entry; linkage across entries is `verifyChainOffline`'s.
1173
+ *
1174
+ * # Assumptions
1175
+ *
1176
+ * - None: every field is validated.
1177
+ */
1178
+ declare function computeChainHashV4(previousHash: string, globalSeq: number | bigint | string, timestamp: string, leafHash: string): Promise<string>;
1179
+
1180
+ /**
1181
+ * Decode the text of an entries file, refusing anything that is not entries.
1182
+ *
1183
+ * # Description
1184
+ *
1185
+ * Accepts the three forms an export takes:
1186
+ *
1187
+ * - one JSON array of entry objects;
1188
+ * - JSONL: one entry object per line (blank lines are skipped);
1189
+ * - the hosted platform's wrapper: one JSON object with an `entries` member
1190
+ * whose value is an array of entry objects. The wrapper's other members
1191
+ * are ignored. Any other top-level object is refused.
1192
+ *
1193
+ * Every v2 or v3 entry must carry `entry_id`, `timestamp`, `content_hash` and
1194
+ * `chain_hash` as strings. A v4 entry (`format_version: 4`, format-spec §2.0)
1195
+ * must carry `timestamp` and `chain_hash`: an erased one has no id, type or
1196
+ * content hash. `salt`, `leaf_hash` (strings) and `erased` (a boolean) are
1197
+ * read on every entry when present, and on a v4 entry the names of the
1198
+ * members it carried are returned in `presentMembers`, so the verifier can
1199
+ * judge its shape (absent is not empty in v4). `entry_type` and `run_id` (strings), and
1200
+ * `sequence_num`, `global_seq` and `format_version` (integers) are read when
1201
+ * present and are "" / 0 / absent otherwise. Each member may be spelled in
1202
+ * snake_case or in the camelCase alias `parseExportedEntries` accepts
1203
+ * (`entryId`, `chainHash`, ...). Members with other names are ignored.
1204
+ *
1205
+ * Refused, as an error and never as a broken chain: text that is not JSON, an
1206
+ * empty document or an empty array, `{}`, `null`, an export bundle, an anchor,
1207
+ * two objects on one line, a member name repeated in any object, an entry
1208
+ * carrying both spellings of one member, a name that case-folds to a known
1209
+ * snake_case name (`CHAIN_HASH`, `ſalt` with U+017F), a missing required
1210
+ * member, and a member that is null or of the wrong type (a `global_seq`
1211
+ * written as a string, a fraction, an exponent or `-0`). Text that is not
1212
+ * valid UTF-8, or escapes a lone surrogate, is refused and never repaired.
1213
+ *
1214
+ * This checks that the text IS entries. Whether the entries verify (hash
1215
+ * shapes, timestamps, the chain) is `verifyChainOffline`'s to report as breaks.
1216
+ *
1217
+ * # Inputs
1218
+ *
1219
+ * - text: The file's content. Pass the raw bytes when you have them: a
1220
+ * string has already had invalid UTF-8 replaced by whatever decoded it.
1221
+ *
1222
+ * # Outputs
1223
+ *
1224
+ * - ExportedEntry[]: At least one entry. `globalSeq` is a `number` up to
1225
+ * `Number.MAX_SAFE_INTEGER` and a `bigint` beyond it.
1226
+ * - EntriesFormatError: Thrown when the text is not entries. The message
1227
+ * names a member, never a value.
1228
+ *
1229
+ * # Example
1230
+ *
1231
+ * const entries = parseEntriesText(await response.text());
1232
+ * const result = await verifyChainOffline(entries);
1233
+ *
1234
+ * # Limitations
1235
+ *
1236
+ * - Builds every entry in memory.
1237
+ * - A single entry object spread over several lines is neither a wrapper nor
1238
+ * JSONL, and is refused.
1239
+ *
1240
+ * # Assumptions
1241
+ *
1242
+ * - None.
1243
+ */
1244
+ declare function parseEntriesText(text: string | Uint8Array): ExportedEntry[];
1245
+ /**
1246
+ * Parse the text of an anchor strictly (format-spec §9.8).
1247
+ *
1248
+ * # Description
1249
+ *
1250
+ * An anchor is read strictly: only the member names of §9.1, each at most
1251
+ * once, and `range` likewise. The subject is under the name its version uses
1252
+ * (`subject` from v6, `company_id` before); an anchor carrying the other name,
1253
+ * or both, is refused. A lenient parser accepts members the signature does not
1254
+ * cover and lets two readers see two different anchors.
1255
+ *
1256
+ * Each member may be spelled in snake_case or in the camelCase alias
1257
+ * `parseAnchor` accepts (`anchorId`, `chainHash`, `subjectId`, ...). An anchor
1258
+ * carrying BOTH spellings of one member is refused, and so is a known
1259
+ * snake_case name in another letter case (`ENTRY_COUNT`).
1260
+ *
1261
+ * A member of the wrong JSON type is refused; integers are JSON integers (no
1262
+ * quotes, no fraction, no exponent, no `-0`, within 64 bits) and are never
1263
+ * rounded. An ABSENT member is not refused here: it reads as "" or 0 (an
1264
+ * absent `range` as two empty ids), and `checkAnchorWellFormed` judges the
1265
+ * result. Nor is the version judged here.
1266
+ *
1267
+ * A v7 anchor is read more strictly (§9.8): every member of §9.1 is
1268
+ * required, `signature` included; `verified_through` and an entry-id range
1269
+ * are refused; `entry_count`, `range.start_seq` and `range.end_seq` must be
1270
+ * JSON STRINGS in §2.1's decimal grammar (a JSON number is refused: it would
1271
+ * round above 2^53) and are returned as bigints. A v1-v6 anchor carrying
1272
+ * `previous_anchor_hash` or a sequence range is refused.
1273
+ *
1274
+ * This is PARSING. The version, its invariants and the counts (§9.3, §9.8)
1275
+ * are checked by `checkAnchorWellFormed`, which `verifyAnchorLocally` and
1276
+ * `bindAnchorToEntries` both call.
1277
+ *
1278
+ * # Inputs
1279
+ *
1280
+ * - text: The anchor's JSON. Pass the raw bytes when you have them.
1281
+ *
1282
+ * # Outputs
1283
+ *
1284
+ * - Anchor: The parsed anchor. int64 fields are `number` while exact and
1285
+ * `bigint` beyond `Number.MAX_SAFE_INTEGER`.
1286
+ * - AnchorVerificationError: Thrown when the anchor is refused. The message
1287
+ * names a member, never a value.
1288
+ *
1289
+ * # Example
1290
+ *
1291
+ * const anchor = parseAnchorText(await response.text());
1292
+ * verifyAnchorLocally(anchor, publicKey);
1293
+ *
1294
+ * # Limitations
1295
+ *
1296
+ * - Does not verify the signature or bind the anchor to a chain.
1297
+ * - Returns an anchor that may be malformed (a missing member, a withdrawn
1298
+ * version): parse, then check.
1299
+ *
1300
+ * # Assumptions
1301
+ *
1302
+ * - None.
1303
+ */
1304
+ declare function parseAnchorText(text: string | Uint8Array): Anchor;
1305
+
1306
+ /**
1307
+ * Trust-manifest freshness verdicts (chainlinker_modernization_12f_e).
1308
+ *
1309
+ * A verifier holding a trust manifest can only reflect revocations known
1310
+ * AT the manifest's generation date: a key revoked afterwards is invisible
1311
+ * ("the CRL problem" for offline verification). This module makes that
1312
+ * limitation explicit rather than emitting a bare "valid".
1313
+ *
1314
+ * v1 semantics (ADR-029 §D5): advisory report + warn, never hard-fail.
1315
+ * Byte-identical to the Go (`EvaluateManifestFreshness`) and Python
1316
+ * (`evaluate_manifest_freshness`) SDK helpers via the shared golden
1317
+ * vectors in `sdk/testdata/manifest_freshness_golden.json`.
1318
+ *
1319
+ * Browser-safe: depends only on `Date` + string ops (no Node built-ins).
1320
+ */
1321
+ /**
1322
+ * Soft-warn threshold (ADR-029 §D5). A manifest older than this many days
1323
+ * is reported STALE — advisory only; never changes validity, never fails.
1324
+ */
1325
+ declare const MANIFEST_FRESHNESS_MAX_AGE_DAYS = 30;
1326
+ /**
1327
+ * Structured, advisory freshness verdict for a trust manifest. All fields
1328
+ * are advisory in v1: they inform the verdict string and let a STATEFUL
1329
+ * caller act on `rollbackDetected`, but the SDK never converts any of them
1330
+ * into a hard verification failure.
1331
+ */
1332
+ interface ManifestFreshness {
1333
+ /** The manifest's monotonic version integer. */
1334
+ readonly version: number;
1335
+ /** The manifest's generation wall-clock time (UnixMilli, UTC). */
1336
+ readonly generatedAtMs: number;
1337
+ /**
1338
+ * floor((now - generatedAtMs) / 1 day), clamped to 0 for a manifest
1339
+ * whose generation stamp is in the future relative to `now`.
1340
+ */
1341
+ readonly ageDays: number;
1342
+ /** True when `ageDays > MANIFEST_FRESHNESS_MAX_AGE_DAYS`. Advisory. */
1343
+ readonly stale: boolean;
1344
+ /**
1345
+ * True only for a STATEFUL evaluation (a non-negative `lastSeenVersion`
1346
+ * was supplied) where `version` is LOWER than the last version this
1347
+ * verifier saw — a suppressed/rolled-back update that could hide a
1348
+ * revocation. A stateless one-shot verifier cannot detect this and
1349
+ * always reports false; its only defense is the reported date.
1350
+ */
1351
+ readonly rollbackDetected: boolean;
1352
+ }
1353
+ /**
1354
+ * Compute the advisory freshness verdict for a trust manifest.
1355
+ *
1356
+ * Pure function over the manifest's signed `version` + `generatedAtMs` and
1357
+ * a caller-supplied reference clock. Never fetches, never mutates, never
1358
+ * throws.
1359
+ *
1360
+ * @param version - the manifest's monotonic version integer.
1361
+ * @param generatedAtMs - the manifest's generation time (UnixMilli, UTC).
1362
+ * @param nowMs - reference clock (UnixMilli, UTC).
1363
+ * @param lastSeenVersion - the highest manifest version this verifier has
1364
+ * previously observed, for anti-rollback. Pass a NEGATIVE value (e.g.
1365
+ * `-1`) for a STATELESS verifier — rollback detection is then disabled.
1366
+ */
1367
+ declare function evaluateManifestFreshness(version: number, generatedAtMs: number, nowMs: number, lastSeenVersion: number): ManifestFreshness;
1368
+ /**
1369
+ * Return the manifest generation date as a UTC `YYYY-MM-DD` string. This is
1370
+ * the exact date form embedded in the verdict line and is byte-identical
1371
+ * across the Go/Python/JS SDKs.
1372
+ */
1373
+ declare function manifestGeneratedDate(f: ManifestFreshness): string;
1374
+ /**
1375
+ * Return the freshness caveat WITHOUT asserting signature validity. Use in
1376
+ * a context that reports manifest freshness but does not itself establish
1377
+ * that a signature verified — e.g. the offline chain CLI, which checks
1378
+ * chain continuity + anchor binding, not signatures against the manifest.
1379
+ */
1380
+ declare function manifestRevocationNote(f: ManifestFreshness): string;
1381
+ /**
1382
+ * Return the exact ADR-029 §D5 verdict line for a VALID signature:
1383
+ *
1384
+ * Signature valid. Revocation checked against trust manifest v{N} dated
1385
+ * {YYYY-MM-DD} — a key revoked after that date is not reflected.
1386
+ *
1387
+ * with ` [WARNING: manifest is {age} days old]` appended when stale. Never
1388
+ * a bare "valid". Only emit this where signature validity has already been
1389
+ * established; otherwise use {@link manifestRevocationNote}.
1390
+ */
1391
+ declare function manifestVerdictLine(f: ManifestFreshness): string;
1392
+
1393
+ /**
1394
+ * Processing mode canonical encoder for Aleutian AI v=2 chain entries.
1395
+ *
1396
+ * Cross-language byte-equality contract
1397
+ * ======================================
1398
+ * The output of {@link encodeProcessingModeCanonicalFields} is guaranteed to
1399
+ * be byte-identical to the Go function `EncodeProcessingModeCanonicalFields`
1400
+ * in `internal/storage/processing_mode_canonical.go` for the same inputs.
1401
+ *
1402
+ * This invariant is enforced by the shared golden vectors in
1403
+ * `sdk/testdata/processing_mode_golden.json`, which are generated by the
1404
+ * Go encoder and verified by the Go, Python, and TypeScript test suites.
1405
+ *
1406
+ * TLV encoding format (v=2)
1407
+ * ==========================
1408
+ * Each field is `u32 BE byte-length || raw UTF-8 bytes`, with the two
1409
+ * fields concatenated in alphabetical key order:
1410
+ *
1411
+ * 1. `processing_mode_at_write`: u32 BE length prefix + mode bytes
1412
+ * 2. `processing_mode_set_by_event_id`: u32 BE length prefix + event id bytes
1413
+ *
1414
+ * All length prefixes are big-endian unsigned 32-bit integers.
1415
+ *
1416
+ * Ticket reference: 18_05i
1417
+ * Finding reference: C-B4-F5 (Phase 4 review)
1418
+ *
1419
+ * @module
1420
+ */
1421
+ /** Canonical wire string for the zero-knowledge processing mode. */
1422
+ declare const ZK: "zk";
1423
+ /** Canonical wire string for the PII inspection processing mode. */
1424
+ declare const PII_INSPECTION: "pii_inspection";
1425
+ /** Union of the two valid processing mode wire strings. */
1426
+ type ProcessingModeWire = typeof ZK | typeof PII_INSPECTION;
1427
+ /**
1428
+ * Thrown when a string is not a known processing mode.
1429
+ *
1430
+ * The error message is a fixed string and never contains the rejected
1431
+ * input — this enforces Privacy Invariant 4 (no caller-supplied bytes
1432
+ * in SDK error messages) at the SDK boundary so callers cannot
1433
+ * accidentally surface untrusted input through logs.
1434
+ */
1435
+ declare class InvalidProcessingMode extends Error {
1436
+ constructor(_mode: string);
1437
+ }
1438
+ /**
1439
+ * Byte-strict parser for the processing mode field.
1440
+ *
1441
+ * Accepts only the exact canonical wire strings `"zk"` and
1442
+ * `"pii_inspection"`. No trimming, no case folding, and no Unicode
1443
+ * normalization is applied. Mirrors `ParseProcessingMode` in
1444
+ * `internal/storage/processing_mode_types.go` (ticket 18_01, Clarification 5).
1445
+ *
1446
+ * @param s - The raw string to validate.
1447
+ * @returns The input string typed as `ProcessingModeWire`.
1448
+ * @throws {@link InvalidProcessingMode} if `s` is not a known mode.
1449
+ *
1450
+ * @example
1451
+ * ```typescript
1452
+ * parseProcessingMode("zk"); // returns "zk"
1453
+ * parseProcessingMode("pii_inspection"); // returns "pii_inspection"
1454
+ * parseProcessingMode("ZK"); // throws InvalidProcessingMode
1455
+ * ```
1456
+ */
1457
+ declare function parseProcessingMode(s: string): ProcessingModeWire;
1458
+ /**
1459
+ * Encode the two v=2 canonical-form processing mode TLV fields.
1460
+ *
1461
+ * Produces the deterministic byte sequence for the
1462
+ * `processing_mode_at_write` and `processing_mode_set_by_event_id` fields
1463
+ * in a v=2 Aleutian consent chain entry. Output is byte-identical to
1464
+ * `EncodeProcessingModeCanonicalFields` in
1465
+ * `internal/storage/processing_mode_canonical.go` for the same inputs.
1466
+ *
1467
+ * Output format (alphabetical TLV):
1468
+ * ```
1469
+ * [u32 BE] len(mode bytes)
1470
+ * [bytes] mode bytes (UTF-8)
1471
+ * [u32 BE] len(setByEventId bytes)
1472
+ * [bytes] setByEventId bytes (UTF-8)
1473
+ * ```
1474
+ *
1475
+ * @param mode - The processing mode wire string; must be `"zk"` or
1476
+ * `"pii_inspection"` exactly.
1477
+ * @param setByEventId - The event id that set the processing mode. Any
1478
+ * non-empty string accepted by the internal `ValidateProcessingModeEventID`
1479
+ * (UUIDv4 or a recognised system sentinel). This function does NOT replicate
1480
+ * Go's event id validation — use the Go encoder at write time.
1481
+ * @returns A `Uint8Array` containing the TLV-encoded bytes.
1482
+ * @throws {@link InvalidProcessingMode} if `mode` is not a known mode.
1483
+ *
1484
+ * @example
1485
+ * ```typescript
1486
+ * const b = encodeProcessingModeCanonicalFields("zk", "system:bootstrap");
1487
+ * console.log(b.length); // 26 (4+2+4+16)
1488
+ * ```
1489
+ */
1490
+ declare function encodeProcessingModeCanonicalFields(mode: string, setByEventId: string): Uint8Array;
1491
+
1492
+ /** Domain-separation prefix for consent entry SHA-512 hashes. */
1493
+ declare const CONSENT_ENTRY_HASH_PREFIX: "aleutian.consent.entry.v2:";
1494
+ /** Domain-separation prefix for raw template SHA-512 hashes (hashTemplate).
1495
+ * Mirrors the Go constant consentTextHashPrefix.
1496
+ *
1497
+ * WARNING: Part of the signed consent record format. Do NOT change without
1498
+ * a versioned migration.
1499
+ */
1500
+ declare const CONSENT_TEXT_HASH_PREFIX: "aleutian.consent.text.v2:";
1501
+ /**
1502
+ * Domain-separation prefix for rendered consent text SHA-512 hashes
1503
+ * (hashRendered). Mirrors the Go constant consentRenderedHashPrefix in
1504
+ * internal/consent/canonical.go.
1505
+ *
1506
+ * 18_20 review finding B4: this prefix was split from CONSENT_TEXT_HASH_PREFIX
1507
+ * so hashTemplate(b) != hashRendered(b) for any input b, closing a
1508
+ * cross-protocol confusion attack. Use this prefix when re-deriving
1509
+ * consent_rendered_hash for GDPR Art. 7 verification.
1510
+ *
1511
+ * WARNING: Part of the GET /v1/consent/text response and POST
1512
+ * /v1/consent/grant verification contract. Do NOT change without a
1513
+ * versioned migration.
1514
+ */
1515
+ declare const CONSENT_RENDERED_HASH_PREFIX: "aleutian.consent.rendered.v2:";
1516
+ /**
1517
+ * The 15 canonical fields of a consent chain entry.
1518
+ *
1519
+ * Field names use snake_case (matching JSON storage) and mirror
1520
+ * internal/consent.ConsentChainEntry and the Python ConsentChainEntry dataclass.
1521
+ *
1522
+ * `entry_type` and `event_type` MUST be equal; both are included in the
1523
+ * canonical encoding for historical schema reasons. {@link consentCanonical}
1524
+ * throws {@link InvalidConsentEntryError} if they differ.
1525
+ *
1526
+ * `expires_at_ms` and `timestamp_ms` are JS `number`. Unix milliseconds for
1527
+ * dates through 2286 fit in Number.MAX_SAFE_INTEGER (2^53 − 1 ≈ 9e15 > 10^13).
1528
+ * Do NOT use bigint — it would prevent direct construction from JSON.parse output.
1529
+ *
1530
+ * WARNING (Privacy Inv 4): every field on this interface is at least
1531
+ * correlation-grade (`actor_pseudonym`, `consent_id`, `idempotency_key_hash`,
1532
+ * `signing_key_id`, content-policy signals) and several are outright PII
1533
+ * carriers. Do NOT pass a `ConsentChainEntry` to `console.log`,
1534
+ * `JSON.stringify`, structured loggers, or template strings. Wrap with
1535
+ * `redactedConsentChainEntry(entry)` from `./redactedView` first. The
1536
+ * interface is a plain object literal, so it cannot own a `toJSON`
1537
+ * override the way the Python dataclass owns `__repr__` — the wrap
1538
+ * step is the only sanctioned logging path.
1539
+ */
1540
+ interface ConsentChainEntry {
1541
+ readonly actor_pseudonym: string;
1542
+ readonly actor_pseudonym_key_version: string;
1543
+ readonly company_id: string;
1544
+ readonly consent_id: string;
1545
+ readonly consent_rendered_hash: string;
1546
+ readonly consent_template_hash: string;
1547
+ readonly consent_text_version: string;
1548
+ readonly entry_type: string;
1549
+ readonly event_type: string;
1550
+ readonly expires_at_ms: number;
1551
+ readonly idempotency_key_hash: string;
1552
+ readonly processing_mode_at_write: string;
1553
+ readonly processing_mode_set_by_event_id: string;
1554
+ readonly signing_key_id: string;
1555
+ readonly timestamp_ms: number;
1556
+ }
1557
+ /**
1558
+ * Return hex(SHA-512(CONSENT_TEXT_HASH_PREFIX + b)).
1559
+ *
1560
+ * # Description
1561
+ *
1562
+ * Mirrors internal/consent.HashTemplate exactly. Used by external callers to
1563
+ * independently verify a chain entry's consent_template_hash against a
1564
+ * template fetched from docs.aleutian.ai.
1565
+ *
1566
+ * Async because WebCrypto's digest API is always async (same constraint as
1567
+ * {@link deriveConsentContentHash}).
1568
+ *
1569
+ * # Inputs
1570
+ *
1571
+ * - b: Raw template bytes. The SDK does NOT normalize; caller must pass
1572
+ * exactly the bytes the server hashed.
1573
+ *
1574
+ * # Outputs
1575
+ *
1576
+ * - Promise<string>: 128 lowercase hex characters (SHA-512 output).
1577
+ *
1578
+ * # Assumptions
1579
+ *
1580
+ * - CONSENT_TEXT_HASH_PREFIX is stable (locked by the v2 schema).
1581
+ */
1582
+ declare function hashTemplate(b: Uint8Array): Promise<string>;
1583
+ /**
1584
+ * Serialize a ConsentChainEntry to the deterministic 15-field TLV byte sequence.
1585
+ *
1586
+ * # Description
1587
+ *
1588
+ * Byte-identical to internal/consent.ConsentCanonical and Python consent_canonical
1589
+ * for the same field values. Synchronous — no crypto; pure byte assembly.
1590
+ *
1591
+ * Fields are written in the locked alphabetical order (15 fields total).
1592
+ * Fields 12–13 (processing_mode_at_write, processing_mode_set_by_event_id) are
1593
+ * encoded by delegating to {@link encodeProcessingModeCanonicalFields}.
1594
+ *
1595
+ * # Inputs
1596
+ *
1597
+ * - entry: The ConsentChainEntry to serialize. All 15 fields must be populated.
1598
+ *
1599
+ * # Outputs
1600
+ *
1601
+ * - Uint8Array: Deterministic TLV bytes; always non-empty on success.
1602
+ * - InvalidConsentEntryError: Thrown on any field validation failure.
1603
+ *
1604
+ * # Limitations
1605
+ *
1606
+ * - Does not perform cross-tenant check; caller's responsibility.
1607
+ * - string fields must not exceed 1024 bytes (UTF-8 encoded).
1608
+ *
1609
+ * # Assumptions
1610
+ *
1611
+ * - All 15 fields are populated with valid values.
1612
+ */
1613
+ declare function consentCanonical(entry: ConsentChainEntry): Uint8Array;
1614
+ /**
1615
+ * Return hex(SHA-512(CONSENT_ENTRY_HASH_PREFIX + canonical)).
1616
+ *
1617
+ * # Description
1618
+ *
1619
+ * Async because WebCrypto's digest API is always async (mirrors chain.ts pattern).
1620
+ *
1621
+ * # Inputs
1622
+ *
1623
+ * - canonical: TLV bytes from consentCanonical for this entry.
1624
+ *
1625
+ * # Outputs
1626
+ *
1627
+ * - Promise<string>: 128 lowercase hex characters (SHA-512 output).
1628
+ *
1629
+ * # Assumptions
1630
+ *
1631
+ * - canonical is the output of consentCanonical for the same entry.
1632
+ */
1633
+ declare function deriveConsentContentHash(canonical: Uint8Array): Promise<string>;
1634
+ /**
1635
+ * Verify the ML-DSA-65 signature over canonical bytes.
1636
+ *
1637
+ * # Description
1638
+ *
1639
+ * Uses getPublicKey(keyId) from keys.ts to look up the embedded public key,
1640
+ * then calls ml_dsa65.verify(sigBytes, canonical, pubKey).
1641
+ *
1642
+ * Note the @noble/post-quantum API arg order: verify(sig, msg, pubKey).
1643
+ * This differs from many other libraries — do not swap.
1644
+ *
1645
+ * # Inputs
1646
+ *
1647
+ * - canonical: TLV bytes from consentCanonical for the entry.
1648
+ * - sigBytes: Raw ML-DSA-65 signature bytes (must be exactly 3309 bytes).
1649
+ * - keyId: The signing key ID from the consent_chain_entries row.
1650
+ *
1651
+ * # Outputs
1652
+ *
1653
+ * - Promise<void>: Resolves normally if verification succeeds.
1654
+ * - ConsentVerificationError: Thrown on unknown keyId, wrong sig length,
1655
+ * or failed ML-DSA-65 verification.
1656
+ *
1657
+ * # Limitations
1658
+ *
1659
+ * - Cross-tenant check is the caller's responsibility.
1660
+ *
1661
+ * # Assumptions
1662
+ *
1663
+ * - canonical is the output of consentCanonical for the same entry.
1664
+ * - sigBytes is the raw ML-DSA-65 signature (not base64).
1665
+ */
1666
+ declare function verifyConsentSignature(canonical: Uint8Array, sigBytes: Uint8Array, keyId: string): Promise<void>;
1667
+ /**
1668
+ * Verify a consent chain entry: canonicalize, check content hash, verify signature.
1669
+ *
1670
+ * # Description
1671
+ *
1672
+ * This is the primary entry point for consent entry verification. Mirrors
1673
+ * VerifyConsentEntry in the Go SDK and verify_consent_entry in the Python SDK.
1674
+ *
1675
+ * Steps (order is invariant — do not reorder):
1676
+ * 1. consentCanonical(entry) — derive canonical bytes.
1677
+ * 2. deriveConsentContentHash(canonical) — re-derive expected hash.
1678
+ * 3. Constant-time compare expected hash against storedContentHash.
1679
+ * MUST precede key lookup (step 4) to prevent key-ID oracle timing attacks.
1680
+ * 4. verifyConsentSignature(canonical, sigBytes, keyId) — ML-DSA-65 verify.
1681
+ *
1682
+ * # Inputs
1683
+ *
1684
+ * - entry: The ConsentChainEntry to verify.
1685
+ * - storedContentHash: 128-char hex string from audit_entries.content_hash.
1686
+ * - sigBytes: Raw ML-DSA-65 signature bytes (exactly 3309 bytes).
1687
+ * - keyId: The signing key ID from the consent_chain_entries row.
1688
+ *
1689
+ * # Outputs
1690
+ *
1691
+ * - Promise<void>: Resolves normally if verification succeeds.
1692
+ * - InvalidConsentEntryError: Thrown if entry field validation fails.
1693
+ * - ConsentVerificationError: Thrown if content hash mismatches or
1694
+ * signature verification fails.
1695
+ *
1696
+ * # Limitations
1697
+ *
1698
+ * - Cross-tenant check is the caller's responsibility:
1699
+ * ```typescript
1700
+ * if (entry.company_id !== owningChainCompanyId) throw new CrossTenantReplayError();
1701
+ * await verifyConsentEntry(entry, storedContentHash, sigBytes, keyId);
1702
+ * ```
1703
+ *
1704
+ * # Assumptions
1705
+ *
1706
+ * - storedContentHash is a 128-char lowercase hex string.
1707
+ * - sigBytes is the raw ML-DSA-65 signature (not base64).
1708
+ */
1709
+ declare function verifyConsentEntry(entry: ConsentChainEntry, storedContentHash: string, sigBytes: Uint8Array, keyId: string): Promise<void>;
1710
+
1711
+ /** Byte length of an ML-DSA-65 public key (FIPS 204). */
1712
+ declare const PUBLIC_KEY_SIZE = 1952;
1713
+ /** Byte length of an ML-DSA-65 signature (FIPS 204). */
1714
+ declare const SIGNATURE_SIZE = 3309;
1715
+ /** Discriminant codes for {@link MLDSAVerificationError}. */
1716
+ type MLDSAErrorCode = "invalid_public_key" | "invalid_signature_length" | "signature_verification_failed";
1717
+ /**
1718
+ * Base error thrown by {@link verify} on any failure. Branch on `code`.
1719
+ */
1720
+ declare class MLDSAVerificationError extends Error {
1721
+ readonly code: MLDSAErrorCode;
1722
+ constructor(code: MLDSAErrorCode, message: string);
1723
+ }
1724
+ /**
1725
+ * Verify an ML-DSA-65 signature in FIPS 204 pure mode.
1726
+ *
1727
+ * No pre-hashing is applied — the scheme hashes the message internally per
1728
+ * FIPS 204 §5.3. The empty context string matches Cloud KMS
1729
+ * PQ_SIGN_ML_DSA_65 and the Go and Python SDK counterparts. Do not change
1730
+ * without coordinated updates across every signing path.
1731
+ *
1732
+ * @param publicKey - Raw 1952-byte ML-DSA-65 public key.
1733
+ * @param message - Exact bytes that were signed (e.g., RFC 8785 canonical JSON).
1734
+ * @param signature - Raw 3309-byte ML-DSA-65 signature.
1735
+ * @throws {MLDSAVerificationError} with `code`:
1736
+ * - `invalid_public_key` if `publicKey` is not a Uint8Array of length
1737
+ * {@link PUBLIC_KEY_SIZE}.
1738
+ * - `invalid_signature_length` if `signature` is not a Uint8Array of
1739
+ * length {@link SIGNATURE_SIZE}.
1740
+ * - `signature_verification_failed` if the signature does not verify
1741
+ * against the given public key and message.
1742
+ *
1743
+ * Limitations:
1744
+ * - Does not resolve key IDs; callers provide raw public-key bytes.
1745
+ * - Does not check revocation status; that is the trust-store loader's job.
1746
+ * - No context string is supported; pure FIPS 204 mode only.
1747
+ *
1748
+ * Assumptions:
1749
+ * - `message` bytes are already canonicalized to match what was signed.
1750
+ * - `publicKey` is the raw 1952-byte encoding, not PEM or DER-wrapped.
1751
+ */
1752
+ declare function verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): void;
1753
+
1754
+ type mldsa_MLDSAErrorCode = MLDSAErrorCode;
1755
+ type mldsa_MLDSAVerificationError = MLDSAVerificationError;
1756
+ declare const mldsa_MLDSAVerificationError: typeof MLDSAVerificationError;
1757
+ declare const mldsa_PUBLIC_KEY_SIZE: typeof PUBLIC_KEY_SIZE;
1758
+ declare const mldsa_SIGNATURE_SIZE: typeof SIGNATURE_SIZE;
1759
+ declare const mldsa_verify: typeof verify;
1760
+ declare namespace mldsa {
1761
+ export { type mldsa_MLDSAErrorCode as MLDSAErrorCode, mldsa_MLDSAVerificationError as MLDSAVerificationError, mldsa_PUBLIC_KEY_SIZE as PUBLIC_KEY_SIZE, mldsa_SIGNATURE_SIZE as SIGNATURE_SIZE, mldsa_verify as verify };
1762
+ }
1763
+
1764
+ /** The domain string hashed before an ML-DSA SubjectPublicKeyInfo. */
1765
+ declare const KEYID_DOMAIN = "proof.keyid.v1:";
1766
+ /**
1767
+ * The SubjectPublicKeyInfo DER of a raw ML-DSA-65 public key.
1768
+ *
1769
+ * @throws Error when the key is not exactly 1952 bytes.
1770
+ */
1771
+ declare function mldsa65Spki(publicKey: Uint8Array): Uint8Array;
1772
+ /**
1773
+ * The key id of a raw ML-DSA-65 public key: 32 lowercase hex.
1774
+ *
1775
+ * @throws Error when the key is not exactly 1952 bytes.
1776
+ */
1777
+ declare function keyIdMLDSA65(publicKey: Uint8Array): Promise<string>;
1778
+ /**
1779
+ * The key id of a raw X-Wing public key (the legacy rule, no domain).
1780
+ *
1781
+ * @throws Error when the key is empty.
1782
+ */
1783
+ declare function keyIdXWing(publicKey: Uint8Array): Promise<string>;
1784
+
1785
+ /**
1786
+ * SAR (Data Subject Access Request) v3 chain entry types — TS verifier port.
1787
+ *
1788
+ * Cross-language reference
1789
+ * ========================
1790
+ * Mirror of {@link "../../verification-go/sar_entries_v3.go"} and
1791
+ * {@link "../../verification-python/src/aleutian_verification/sar_entries.py"}.
1792
+ * Field order in each interface MUST match the alphabetical body-field order
1793
+ * from `pqc_05c v3 §4.5–4.9`. The canonical encoder in `canonicalV3.ts`
1794
+ * (ticket dsar_chain_f_02) relies on declaration ordering — silent reorder
1795
+ * is a wire-format break.
1796
+ *
1797
+ * Sealed discriminated union
1798
+ * ==========================
1799
+ * TypeScript cannot enforce protocol sealing at the type-system level the
1800
+ * way Go can (no unexported parameter types). Instead this module defines
1801
+ * `CONCRETE_SAR_ENTRY_TYPE_TAGS` — an authoritative set of `entryType`
1802
+ * literal values — and the public encoder / verifier dispatch chain
1803
+ * (canonicalV3.chainCanonicalV3 in `_02`, verifyV3.verifyChainEntryV3
1804
+ * in `_04`) checks the tag at runtime before any work. Structural-typing
1805
+ * impostors that duck-type `ChainEntryV3` are refused at that boundary.
1806
+ * The `ChainEntryV3` discriminated union remains the static type for
1807
+ * annotation purposes; it is NOT the security boundary — the tag set is.
1808
+ *
1809
+ * Privacy posture (OQ-11, locked)
1810
+ * ================================
1811
+ * SAR entries are systematically actor-anonymous on chain. NONE of these
1812
+ * interfaces carry actor pseudonyms or email-derived hashes. Verifiers
1813
+ * cannot identify the data subject from the chain entry alone.
1814
+ *
1815
+ * Field naming
1816
+ * ============
1817
+ * camelCase per TS convention. The on-the-wire JSON form is snake_case;
1818
+ * the canonical encoder operates on these typed objects directly, NOT
1819
+ * on the JSON. Callers loading from chain rows should convert via the
1820
+ * snake-case→camel-case helper in `chainRowDecode.ts` (future ticket)
1821
+ * before passing entries into the encoder/verifier.
1822
+ *
1823
+ * @module
1824
+ */
1825
+ /** Chain entry discriminating tag (pqc_05c v3 §3.2). */
1826
+ type EventTypeV3 = "sar_intent" | "sar_acknowledged" | "sar_fulfilled" | "sar_rejected" | "sar_extended";
1827
+ /** Surface that originated a SAR intent (canonical on chain, only on intent). */
1828
+ type SARSourceChannelV3 = "portal" | "api";
1829
+ /** GDPR right being exercised. */
1830
+ type SARTypeV3 = "access" | "erasure" | "both";
1831
+ /** What the controller actually did at fulfillment time. */
1832
+ type SARFulfillmentKindV3 = "access" | "erasure" | "both";
1833
+ /**
1834
+ * Internal brand for nominal-typing on TenantHMACDigestV3.
1835
+ *
1836
+ * The brand is TYPE-ONLY — there is no runtime property of this name on
1837
+ * the value. `declare const ... unique symbol` is erased at runtime, so
1838
+ * the constructed object has only `hex` at runtime. The compile-time
1839
+ * brand prevents external code from forging a digest via plain object
1840
+ * literal: `{ hex: "..." } as TenantHMACDigestV3` fails type-check
1841
+ * because the synthetic brand property is unsatisfiable outside this
1842
+ * module.
1843
+ */
1844
+ declare const _digestBrand: unique symbol;
1845
+ /**
1846
+ * HMAC-SHA-512 digest under a per-tenant PII subkey (pqc_05c v3 §4.0).
1847
+ *
1848
+ * Stored as a hex string (or empty string for the zero / unset sentinel) —
1849
+ * matches Go SDK's `TenantHMACDigestV3` byte-for-byte so the canonical
1850
+ * encoder produces parity output. The encoder calls `.hex`; empty
1851
+ * string emits a 0-length TLV (sentinel for "caller did not supply
1852
+ * cleartext"), and 128-hex emits a 128-byte string.
1853
+ *
1854
+ * Construct via {@link tenantHMACDigestFromHex}, {@link tenantHMACDigestFromRaw},
1855
+ * or {@link tenantHMACDigestZero}. The type-only brand prevents
1856
+ * external code from forging a digest via `{} as TenantHMACDigestV3` —
1857
+ * the brand property is unsatisfiable outside this module.
1858
+ */
1859
+ interface TenantHMACDigestV3 {
1860
+ readonly hex: string;
1861
+ readonly [_digestBrand]: never;
1862
+ }
1863
+ /** Construct from a 128-char lowercase hex string. Empty string → zero digest. */
1864
+ declare function tenantHMACDigestFromHex(hex: string): TenantHMACDigestV3;
1865
+ /** Construct from 64 raw bytes. */
1866
+ declare function tenantHMACDigestFromRaw(raw: Uint8Array): TenantHMACDigestV3;
1867
+ /** Zero / unset digest — encodes as empty string on chain. */
1868
+ declare function tenantHMACDigestZero(): TenantHMACDigestV3;
1869
+ /** True iff the digest is the zero / unset sentinel. */
1870
+ declare function tenantHMACDigestIsZero(d: TenantHMACDigestV3): boolean;
1871
+ /**
1872
+ * Common header — present on every v3 chain entry.
1873
+ *
1874
+ * # Number vs BigInt typing (Phase 3 review H1)
1875
+ *
1876
+ * Int64 fields (`timestampMs`, `tombstoneCount`, `extendedDueAtMs`)
1877
+ * accept `bigint | number`. JS `Number` is double-precision IEEE-754,
1878
+ * so values above `Number.MAX_SAFE_INTEGER` (2^53 - 1) silently lose
1879
+ * precision. Realistic v3 values fit comfortably (e.g., year-3000
1880
+ * UnixMilli is ~3.3 × 10^13, well under 2^53 ≈ 9 × 10^15), but a
1881
+ * future caller pulling row data from BigQuery — where int64 may
1882
+ * surface as `bigint` — should be able to pass it through without a
1883
+ * cast or precision-loss risk.
1884
+ *
1885
+ * The encoder (`canonicalV3.appendU64`) accepts either form and
1886
+ * routes through `DataView.setBigUint64` for byte-identical BE
1887
+ * encoding regardless of the source representation.
1888
+ */
1889
+ interface _ChainEntryHeaderV3<T extends EventTypeV3> {
1890
+ readonly companyId: string;
1891
+ readonly entryType: T;
1892
+ readonly signingKeyId: string;
1893
+ readonly timestampMs: bigint | number;
1894
+ }
1895
+ /**
1896
+ * Filed-SAR entry. Header + 5 body fields (pqc_05c v3 §4.5).
1897
+ *
1898
+ * `piiDigestKeyVersion` (pii_hmac_key_rotation_R4): source-namespaced
1899
+ * PII-HMAC key version ("sm:v1") under which `reasonHash` was computed.
1900
+ * OPTIONAL on intent (reason_hash itself is optional). Empty string is
1901
+ * the legacy / pre-rotation sentinel — encoded ONLY when non-empty so a
1902
+ * legacy leaf hashes byte-identically to its original signing. Matches
1903
+ * the Go SDK's `SARIntentEntryV3.PIIDigestKeyVersion` (json
1904
+ * `pii_digest_key_version`).
1905
+ */
1906
+ interface SARIntentEntryV3 extends _ChainEntryHeaderV3<"sar_intent"> {
1907
+ readonly piiDigestKeyVersion: string;
1908
+ readonly reasonHash: TenantHMACDigestV3;
1909
+ readonly requestId: string;
1910
+ readonly sarType: SARTypeV3;
1911
+ readonly scope: string;
1912
+ readonly sourceChannel: SARSourceChannelV3;
1913
+ }
1914
+ /** Acknowledged-SAR entry. Header + 2 body fields (pqc_05c v3 §4.6). */
1915
+ interface SARAcknowledgedEntryV3 extends _ChainEntryHeaderV3<"sar_acknowledged"> {
1916
+ readonly intentEntryId: string;
1917
+ readonly requestId: string;
1918
+ }
1919
+ /**
1920
+ * Fulfilled-SAR entry. Header + 7 body fields (pqc_05c v3 §4.7).
1921
+ *
1922
+ * `certificateId` was added 2026-05-16 by `dsar_fulfill_b_pre_02e` as the
1923
+ * alphabetical-first body field. Per-kind shape rules:
1924
+ *
1925
+ * - fulfillmentKind=access → certificateId MUST be empty
1926
+ * - fulfillmentKind=erasure → certificateId MUST match cert_<32-hex>
1927
+ * - fulfillmentKind=both → certificateId MUST match cert_<32-hex>
1928
+ */
1929
+ interface SARFulfilledEntryV3 extends _ChainEntryHeaderV3<"sar_fulfilled"> {
1930
+ readonly certificateId: string;
1931
+ readonly fulfillmentKind: SARFulfillmentKindV3;
1932
+ readonly intentEntryId: string;
1933
+ readonly legalBasis: string;
1934
+ readonly manifestRoot: string;
1935
+ readonly requestId: string;
1936
+ readonly tombstoneCount: bigint | number;
1937
+ }
1938
+ /**
1939
+ * Rejected-SAR entry. Header + 4 body fields (pqc_05c v3 §4.8).
1940
+ *
1941
+ * `piiDigestKeyVersion` (pii_hmac_key_rotation_R4): PII-HMAC key version
1942
+ * ("sm:v1") under which `rationaleHash` was computed. REQUIRED in the
1943
+ * sense that rationale_hash is required on rejection, so a non-empty
1944
+ * version always has a digest to apply it to. Empty string is the legacy
1945
+ * sentinel — encoded ONLY when non-empty. Matches Go
1946
+ * `SARRejectedEntryV3.PIIDigestKeyVersion`.
1947
+ */
1948
+ interface SARRejectedEntryV3 extends _ChainEntryHeaderV3<"sar_rejected"> {
1949
+ readonly intentEntryId: string;
1950
+ readonly legalBasis: string;
1951
+ readonly piiDigestKeyVersion: string;
1952
+ readonly rationaleHash: TenantHMACDigestV3;
1953
+ readonly requestId: string;
1954
+ }
1955
+ /**
1956
+ * Extended-SAR entry. Header + 4 body fields (pqc_05c v3 §4.9).
1957
+ *
1958
+ * Body alphabetical: extendedDueAtMs, intentEntryId, piiDigestKeyVersion
1959
+ * (R4, iff non-empty), reasonHash, requestId.
1960
+ * `originalDueAtMs` is NOT bound — derivable from the intent entry.
1961
+ */
1962
+ interface SARExtendedEntryV3 extends _ChainEntryHeaderV3<"sar_extended"> {
1963
+ readonly extendedDueAtMs: bigint | number;
1964
+ readonly intentEntryId: string;
1965
+ /**
1966
+ * `piiDigestKeyVersion` (pii_hmac_key_rotation_R4): PII-HMAC key
1967
+ * version ("sm:v1") under which `reasonHash` was computed. reason_hash
1968
+ * is required on extension, so a non-empty version always has a digest
1969
+ * to apply it to. Empty string is the legacy sentinel — encoded ONLY
1970
+ * when non-empty. Matches Go `SARExtendedEntryV3.PIIDigestKeyVersion`.
1971
+ */
1972
+ readonly piiDigestKeyVersion: string;
1973
+ readonly reasonHash: TenantHMACDigestV3;
1974
+ readonly requestId: string;
1975
+ }
1976
+ /**
1977
+ * Discriminated union of all 5 v3 SAR chain entry types.
1978
+ *
1979
+ * The `entryType` literal field is the discriminant — TypeScript narrows
1980
+ * to a specific interface when you switch on it:
1981
+ *
1982
+ * ```ts
1983
+ * function describe(e: ChainEntryV3): string {
1984
+ * switch (e.entryType) {
1985
+ * case "sar_intent": return `intent ${e.requestId}`;
1986
+ * case "sar_fulfilled": return `fulfilled ${e.manifestRoot.slice(0, 8)}…`;
1987
+ * // ... compiler errors if you forget a case
1988
+ * }
1989
+ * }
1990
+ * ```
1991
+ */
1992
+ type ChainEntryV3 = SARIntentEntryV3 | SARAcknowledgedEntryV3 | SARFulfilledEntryV3 | SARRejectedEntryV3 | SARExtendedEntryV3;
1993
+ declare const CONCRETE_SAR_ENTRY_TYPE_TAGS: ReadonlySet<EventTypeV3>;
1994
+ /**
1995
+ * Runtime check that an unknown value carries a valid v3 SAR chain
1996
+ * entry TAG. Verifies the `entryType` discriminant is in the
1997
+ * concrete-tag allowlist; does NOT shape-check body fields — that
1998
+ * happens in `validateV3Entry` (see `_03`).
1999
+ *
2000
+ * Phase 3 review M4: renamed from the previous `isChainEntryV3` to
2001
+ * accurately reflect what it does. Code paths that need full shape
2002
+ * validation should call `validateV3Entry` from `canonicalV3.ts`.
2003
+ */
2004
+ declare function hasValidChainEntryTag(value: unknown): value is ChainEntryV3;
2005
+ /**
2006
+ * @deprecated Renamed to `hasValidChainEntryTag` (Phase 3 review M4)
2007
+ * to accurately describe the tag-only check. Will be removed in a
2008
+ * future minor version; switch call sites to the new name.
2009
+ */
2010
+ declare const isChainEntryV3: typeof hasValidChainEntryTag;
2011
+
2012
+ /**
2013
+ * Locked V3 domain prefix used for content-hash and signature
2014
+ * derivation. See Go SDK `DomainPrefixV3` for the producer-side
2015
+ * counterpart.
2016
+ *
2017
+ * # WARNING — load-bearing constant
2018
+ *
2019
+ * Changing this byte sequence invalidates every signed v3 chain
2020
+ * entry across all language ports.
2021
+ */
2022
+ declare const DOMAIN_PREFIX_V3 = "aleutian.chain.entry.v3:";
2023
+ /** Pre-computed `Uint8Array` form to avoid per-hash allocation. */
2024
+ declare const DOMAIN_PREFIX_V3_BYTES: Uint8Array;
2025
+ /**
2026
+ * v3 spec §7 byte cap on string fields post-NFC. Cross-language
2027
+ * locked value; matches Go's `maxV3StringFieldBytesSDK` and Python's
2028
+ * `_MAX_V3_STRING_FIELD_BYTES`.
2029
+ */
2030
+ declare const MAX_V3_STRING_FIELD_BYTES = 256;
2031
+ /** SHA-512 hex digest length (cross-language constant). */
2032
+ declare const V3_SHA512_HEX_LEN = 128;
2033
+ /**
2034
+ * Maximum canonical-byte sum per entry (cross-language locked
2035
+ * value; matches Go's `MaxCanonicalBytesV3` and Python's
2036
+ * `MAX_CANONICAL_BYTES_V3`). The widest current v3 SAR entry fits
2037
+ * comfortably under 1 KiB; 4× headroom absorbs future field
2038
+ * additions without forcing a SemVer-breaking bound bump.
2039
+ *
2040
+ * # Cross-language consistency is load-bearing
2041
+ *
2042
+ * A divergent value in any one SDK becomes a per-tenant DoS vector
2043
+ * against customers using the laxer SDK — they'd do unbounded work
2044
+ * on adversarial inputs. Pinned in
2045
+ * `verifyV3.test.ts::MAX_CANONICAL_BYTES_V3_LockedAtCrossLanguageValue`.
2046
+ */
2047
+ declare const MAX_CANONICAL_BYTES_V3 = 4096;
2048
+ /**
2049
+ * 14-month sanity bound for `extendedDueAtMs` past `timestampMs`.
2050
+ * Mirror of Go's `420 * 24 * 60 * 60 * 1000` literal.
2051
+ */
2052
+ declare const EXTENDED_SANITY_BOUND_MS: number;
2053
+ /**
2054
+ * Wrapped sentinel for encoder validation failures. Customer code
2055
+ * can `err instanceof ErrInvalidEntryV3` to distinguish encoder
2056
+ * rejections from signature/content-hash failures (which use
2057
+ * distinct sentinels; see verifyV3.ts).
2058
+ *
2059
+ * Per `sdk_error_hierarchy_spec`, this is the V3 entry-shape error.
2060
+ */
2061
+ declare class ErrInvalidEntryV3 extends Error {
2062
+ readonly name: string;
2063
+ constructor(message: string);
2064
+ }
2065
+ /**
2066
+ * Validates a V3 chain entry by dispatching to the per-type
2067
+ * validator. Throws `ErrInvalidEntryV3` on any failure. Called
2068
+ * automatically by `chainCanonicalV3` before encoding.
2069
+ *
2070
+ * Exported so customers can validate without encoding.
2071
+ */
2072
+ declare function validateV3Entry(entry: ChainEntryV3): void;
2073
+ /**
2074
+ * `V3Encoder` bundles a growing `Uint8Array` buffer. Per-event-type
2075
+ * body encoders append fields without explicit error plumbing — the
2076
+ * encoder throws synchronously on invariant violations (256-byte
2077
+ * cap, negative u64).
2078
+ *
2079
+ * Mirror of Go's `v3EncoderSDK`.
2080
+ *
2081
+ * Exported for the explicit purpose of unit-testing the encoder
2082
+ * mechanics (NFC normalization, BE encoding, length cap) without
2083
+ * routing through the full `chainCanonicalV3` pipeline (which
2084
+ * validates regex/vocabulary constraints first). Customers writing
2085
+ * normal verification code should call `chainCanonicalV3`.
2086
+ */
2087
+ declare class V3Encoder {
2088
+ private buf;
2089
+ private pos;
2090
+ private view;
2091
+ private readonly textEncoder;
2092
+ private readonly assumeNormalized;
2093
+ constructor(initialCapacity?: number, options?: {
2094
+ assumeNormalized?: boolean;
2095
+ });
2096
+ /** Final byte slice (a fresh allocation; safe to share with callers). */
2097
+ bytes(): Uint8Array;
2098
+ private grow;
2099
+ /**
2100
+ * Writes a length-prefixed UTF-8 string with the spec's "post-NFC,
2101
+ * post-UTF-8 byte length" semantics (pqc_05c v3 §7).
2102
+ *
2103
+ * Privacy Inv 4: errors do NOT echo the offending field length.
2104
+ *
2105
+ * # Double-NFC posture (M6, 2026-05-29)
2106
+ *
2107
+ * When called via `chainCanonicalV3`, `validateV3Entry` has
2108
+ * already NFC-normalized this field. NFC is idempotent so the
2109
+ * re-normalization here is a no-op semantically, but it does pay
2110
+ * one `String.normalize("NFC")` call per field. Construct the
2111
+ * encoder with `{ assumeNormalized: true }` to skip the redundant
2112
+ * call when the caller has already validated. Defaults to `false`
2113
+ * so direct unit-test use of `V3Encoder` remains safe.
2114
+ */
2115
+ appendStr(s: string): void;
2116
+ /**
2117
+ * Writes a u64 BE non-negative integer. v3 has no signed-int
2118
+ * fields; callers attempting to emit negative values indicate a
2119
+ * validator bug — the encoder traps it as a distinct invariant
2120
+ * violation.
2121
+ *
2122
+ * Accepts `bigint` or `number`. `number` inputs are checked for
2123
+ * `Number.isSafeInteger`; if you have a value above 2^53, pass it
2124
+ * as `bigint` (e.g. `BigInt(value)`).
2125
+ */
2126
+ appendU64(v: bigint | number): void;
2127
+ /**
2128
+ * Writes the 4-field common header in alphabetical order:
2129
+ * company_id, entry_type, signing_key_id, timestamp_ms.
2130
+ */
2131
+ appendV3Header(companyId: string, entryType: string, signingKeyId: string, timestampMs: bigint | number): void;
2132
+ }
2133
+ /**
2134
+ * Encode a typed V3 chain entry to canonical bytes per pqc_05c v3
2135
+ * §3 + §4. The single entry-point for cross-language byte-identical
2136
+ * encoding.
2137
+ *
2138
+ * Mirror of Go's `CanonicalBytesV3`.
2139
+ *
2140
+ * @param entry - A typed v3 chain entry (one of the 5 SAR variants).
2141
+ * @returns Canonical TLV bytes.
2142
+ * @throws {ErrInvalidEntryV3} On encoder invariant violation.
2143
+ */
2144
+ declare function chainCanonicalV3(entry: ChainEntryV3): Uint8Array;
2145
+ /**
2146
+ * Compute the V3 content hash: SHA-512(DOMAIN_PREFIX_V3_BYTES || canonical).
2147
+ *
2148
+ * Mirror of Go's `ContentHashV3` (which returns hex). This JS version
2149
+ * returns the raw 64-byte digest; hex encoding is the caller's job
2150
+ * via `bytesToHex` (caller-side utility).
2151
+ *
2152
+ * Uses `crypto.subtle.digest("SHA-512", ...)` from the Web Crypto API,
2153
+ * which is available in Node ≥16 (via `globalThis.crypto`) and all
2154
+ * modern browsers.
2155
+ *
2156
+ * @param canonical - Canonical bytes from `chainCanonicalV3`.
2157
+ * @returns The 64-byte SHA-512 digest.
2158
+ */
2159
+ declare function computeV3ContentHash(canonical: Uint8Array): Promise<Uint8Array>;
2160
+ /**
2161
+ * Convenience: encode + hash in one call. Mirror of Go's
2162
+ * `EncodeAndHashV3`.
2163
+ */
2164
+ declare function encodeAndHashV3(entry: ChainEntryV3): Promise<{
2165
+ canonical: Uint8Array;
2166
+ contentHash: Uint8Array;
2167
+ }>;
2168
+ /**
2169
+ * Lowercase hex encoder for digests. Mirror of Go's
2170
+ * `hex.EncodeToString`. Useful for cross-language hash comparison
2171
+ * tests (golden vectors carry hex strings).
2172
+ */
2173
+ declare function bytesToHex(bytes: Uint8Array): string;
2174
+
2175
+ /**
2176
+ * A capture-chain AI-request leaf. camelCase fields per JS convention; the
2177
+ * cross-language fixture `input` is snake_case (the golden test maps them).
2178
+ * entry_type is a constant, not a field.
2179
+ *
2180
+ * All hash/digest fields (`contentHash`, `userId`, `piiCategories`, `dlp`) are
2181
+ * 128 lowercase hex (SHA-512 / HMAC-SHA512), or empty for the optional digests.
2182
+ * The verifier checks SHAPE only; per-tenant HMAC keying is producer-side.
2183
+ */
2184
+ interface CaptureRequestEntryV3 {
2185
+ readonly companyId: string;
2186
+ readonly signingKeyId: string;
2187
+ /** Server-stamped ingested_at (UnixMilli). Always < 2^53 for real anchors. */
2188
+ readonly timestampMs: number;
2189
+ readonly captureMethod: string;
2190
+ readonly contentHash: string;
2191
+ readonly dlp: string;
2192
+ readonly encryptionMode: string;
2193
+ readonly model: string;
2194
+ readonly piiAction: string;
2195
+ readonly piiCategories: string;
2196
+ readonly piiDetected: boolean;
2197
+ readonly piiDigestKeyVersion: string;
2198
+ readonly processingMode: string;
2199
+ readonly provider: string;
2200
+ readonly region: string;
2201
+ readonly sourceType: string;
2202
+ readonly trustLevel: string;
2203
+ readonly userId: string;
2204
+ }
2205
+
2206
+ /**
2207
+ * Constant-time byte-array equality. Returns `true` iff `a` and `b`
2208
+ * are byte-identical.
2209
+ *
2210
+ * # Why constant-time matters here
2211
+ *
2212
+ * A short-circuiting compare leaks the position of the first
2213
+ * differing byte via timing, which on a hash-comparison gate
2214
+ * becomes a forgery oracle: an attacker can brute-force the hash
2215
+ * one byte at a time.
2216
+ *
2217
+ * # Implementation
2218
+ *
2219
+ * Under Node 16+ this delegates to `crypto.timingSafeEqual` (the
2220
+ * built-in C implementation). Under browsers it uses a pure-JS
2221
+ * fallback that iterates the longer of the two inputs and ORs
2222
+ * into an accumulator.
2223
+ */
2224
+ declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
2225
+
2226
+ /**
2227
+ * V3 chain entry signature verifier — TS port of
2228
+ * `sdk/verification-go/verify_v3.go::VerifyChainEntryV3`.
2229
+ *
2230
+ * # Pipeline
2231
+ *
2232
+ * 1. `validateV3Entry(entry)` — typed validation per pqc_05c v3 §3-§4.
2233
+ * 2. `chainCanonicalV3(entry)` — re-encode canonical bytes.
2234
+ * 3. `len(canonical) ≤ MAX_CANONICAL_BYTES_V3` — DoS bound.
2235
+ * 4. Bound input sizes on the CLAIM (`contentHash` 64 bytes,
2236
+ * `signature` 3309 bytes) BEFORE expensive ops.
2237
+ * 5. Recompute `SHA-512(prefix || canonical)`; constant-time
2238
+ * compare to the claimed `contentHash`.
2239
+ * 6. Resolve `signingKeyId` via the trust manifest.
2240
+ * 7. Verify ML-DSA-65 signature over `prefix || canonical`
2241
+ * (NOT over `contentHash` — pqc_05c v3 §9.1 option A).
2242
+ *
2243
+ * # Error contract (chain_d H2 — strict opacity)
2244
+ *
2245
+ * Error messages are intentionally content-agnostic. They surface
2246
+ * the typed error class only; never echo:
2247
+ * - claimed hash bytes,
2248
+ * - computed hash bytes,
2249
+ * - signingKeyId,
2250
+ * - raw ML-DSA library error strings.
2251
+ *
2252
+ * A public verifier (e.g. verify.aleutian.ai) leaking any of those
2253
+ * gives:
2254
+ * - a forgery oracle (computed hash echoed back lets attacker
2255
+ * pass the hash gate trivially, focusing on signature alone),
2256
+ * - operator-metadata leakage (signingKeyId reveals KMS key
2257
+ * version / rotation date / tenant region to anyone who
2258
+ * triggers a bad signature).
2259
+ *
2260
+ * Callers needing structured access for ops dashboards branch on
2261
+ * `err instanceof Err...V3` not on `.message` scraping.
2262
+ */
2263
+
2264
+ /** SHA-512 byte length — cross-language constant. */
2265
+ declare const CONTENT_HASH_V3_BYTES = 64;
2266
+ /**
2267
+ * Trust manifest does not contain the entry's signingKeyId.
2268
+ *
2269
+ * Mirror of Go's `ErrKeyNotInTrustManifestV3` / Python's
2270
+ * `ErrUnknownSigningKeyV3`.
2271
+ */
2272
+ declare class ErrUnknownSigningKeyV3 extends ErrInvalidEntryV3 {
2273
+ readonly name = "ErrUnknownSigningKeyV3";
2274
+ }
2275
+ /** Re-encoded `content_hash` does not match the supplied claim. */
2276
+ declare class ErrContentHashMismatchV3 extends ErrInvalidEntryV3 {
2277
+ readonly name = "ErrContentHashMismatchV3";
2278
+ }
2279
+ /** Signature parses + correct length, but does not verify under the key. */
2280
+ declare class ErrSignatureInvalidV3 extends ErrInvalidEntryV3 {
2281
+ readonly name = "ErrSignatureInvalidV3";
2282
+ }
2283
+ /** Signature bytes don't have a valid shape (wrong length, etc.). */
2284
+ declare class ErrSignatureMalformedV3 extends ErrInvalidEntryV3 {
2285
+ readonly name = "ErrSignatureMalformedV3";
2286
+ }
2287
+ /** A trust-manifest public key has malformed bytes. */
2288
+ declare class ErrPublicKeyMalformedV3 extends ErrInvalidEntryV3 {
2289
+ readonly name = "ErrPublicKeyMalformedV3";
2290
+ }
2291
+ /**
2292
+ * Lookup surface a verifier uses to resolve `signingKeyId` →
2293
+ * ML-DSA-65 public key. Mirror of Go's `TrustManifestV3` interface
2294
+ * and Python's `TrustManifestV3` dataclass.
2295
+ *
2296
+ * # Required guarantees (per pqc_05c v3 §3.1.1)
2297
+ *
2298
+ * - Append-only: once a `signingKeyId` is bound to a public key,
2299
+ * the binding MUST NOT change. Rotating a key requires minting
2300
+ * a NEW `signingKeyId` value.
2301
+ * - One-to-one: a `signingKeyId` MUST resolve to exactly one
2302
+ * public key over the manifest's lifetime.
2303
+ *
2304
+ * # Construction-time validation (chain_d H3)
2305
+ *
2306
+ * Each public key is validated against the ML-DSA-65 byte length
2307
+ * (1952) at construction. Operators detect misconfiguration at
2308
+ * manifest-load time, not lazily per-verify.
2309
+ *
2310
+ * # Concurrency
2311
+ *
2312
+ * Read-only after construction. Safe for concurrent reads.
2313
+ */
2314
+ declare class TrustManifestV3 {
2315
+ private readonly _keys;
2316
+ private constructor();
2317
+ /**
2318
+ * Build a manifest from a `Record<signingKeyId, publicKey>`. Each
2319
+ * public key is shape-validated at construction time. Each key
2320
+ * is defensively copied — post-construction mutation of the
2321
+ * caller's `Uint8Array` does not affect the manifest.
2322
+ *
2323
+ * Throws `ErrInvalidEntryV3` for empty signingKeyIds; throws
2324
+ * `ErrPublicKeyMalformedV3` for keys whose byte length is not
2325
+ * exactly `PUBLIC_KEY_SIZE`.
2326
+ */
2327
+ static fromMap(keys: Record<string, Uint8Array>): TrustManifestV3;
2328
+ /**
2329
+ * Returns the public key bytes for `signingKeyId`. Throws
2330
+ * `ErrUnknownSigningKeyV3` if missing.
2331
+ *
2332
+ * The returned slice is a DEFENSIVE COPY — callers may mutate it
2333
+ * without affecting subsequent lookups.
2334
+ */
2335
+ publicKeyForSigningKeyId(signingKeyId: string): Uint8Array;
2336
+ /** Number of bound keys (for ops dashboards / sanity checks). */
2337
+ get size(): number;
2338
+ }
2339
+
2340
+ /**
2341
+ * Options-object form of {@link verifyChainEntryV3} (M1, added 0.3.1).
2342
+ *
2343
+ * Preferred over the positional form going forward. Positional form
2344
+ * remains accepted on the 0.3.x track and emits a one-shot
2345
+ * `console.warn` deprecation notice on first call; removed in 0.4.0.
2346
+ */
2347
+ interface VerifyChainEntryV3Options {
2348
+ signature: Uint8Array;
2349
+ contentHash: Uint8Array;
2350
+ trustManifest: TrustManifestV3;
2351
+ }
2352
+ /**
2353
+ * Verify a v3 chain entry's `content_hash` + ML-DSA-65 signature.
2354
+ *
2355
+ * # Inputs
2356
+ *
2357
+ * Two call shapes are accepted on the 0.3.x track:
2358
+ *
2359
+ * 1. **Options-object (preferred, added 0.3.1):**
2360
+ *
2361
+ * ```ts
2362
+ * verifyChainEntryV3(entry, { signature, contentHash, trustManifest })
2363
+ * ```
2364
+ *
2365
+ * 2. **Positional (deprecated, removed in 0.4.0):**
2366
+ *
2367
+ * ```ts
2368
+ * verifyChainEntryV3(entry, signature, contentHash, trustManifest)
2369
+ * ```
2370
+ *
2371
+ * Emits a one-shot `console.warn` deprecation notice on first use.
2372
+ *
2373
+ * Field semantics (identical across both shapes):
2374
+ *
2375
+ * - entry: one of the 5 concrete SAR entry variants.
2376
+ * - signature: raw ML-DSA-65 signature bytes (3309 bytes).
2377
+ * - contentHash: raw SHA-512 bytes (64 bytes). Callers with a
2378
+ * hex-encoded claim should decode (`hexToBytes(...)`) first.
2379
+ * - trustManifest: lookup surface for signingKeyId.
2380
+ *
2381
+ * # Outputs
2382
+ *
2383
+ * - Promise resolves on successful verification.
2384
+ * - Throws one of:
2385
+ * - `ErrInvalidEntryV3` (validation, oversized canonical, missing args)
2386
+ * - `ErrContentHashMismatchV3` (claimed hash does not match recompute)
2387
+ * - `ErrSignatureMalformedV3` (signature wrong length / not Uint8Array)
2388
+ * - `ErrSignatureInvalidV3` (ML-DSA verify failed)
2389
+ * - `ErrUnknownSigningKeyV3` (signingKeyId not in manifest)
2390
+ * - `ErrPublicKeyMalformedV3` (manifest contains corrupt key)
2391
+ *
2392
+ * # Concurrency
2393
+ *
2394
+ * Pure function over its inputs. Safe for concurrent use.
2395
+ */
2396
+ declare function verifyChainEntryV3(entry: ChainEntryV3 | CaptureRequestEntryV3, options: VerifyChainEntryV3Options): Promise<void>;
2397
+ declare function verifyChainEntryV3(entry: ChainEntryV3 | CaptureRequestEntryV3, signature: Uint8Array, contentHash: Uint8Array, trustManifest: TrustManifestV3): Promise<void>;
2398
+
2399
+ /**
2400
+ * Tier-2 bundle wire-format types.
2401
+ *
2402
+ * Mirrors `sdk/verification-go/tier2/bundle.go` and
2403
+ * `aleutian_verification.tier2.bundle`. The JSON field names match the
2404
+ * on-wire encoding produced by the proprietary Tier-2 sealer; do not
2405
+ * rename without coordinating across all three SDKs.
2406
+ */
2407
+ /** One recipient's bundle-CEK wrap. */
2408
+ interface CEKWrapEnvelope {
2409
+ /** 32-char lowercase hex fingerprint of the recipient's X-Wing pubkey. */
2410
+ pubkey_fingerprint: string;
2411
+ /** 1120-byte X-Wing KEM ciphertext, base64. */
2412
+ kem_ciphertext: string;
2413
+ /** AES-256-GCM nonce (12 bytes, base64). */
2414
+ wrap_nonce: string;
2415
+ /** AEAD-sealed bundle CEK (48 bytes = 32-byte key + 16-byte tag), base64. */
2416
+ wrap_ciphertext: string;
2417
+ /** Producer-side sealing timestamp. NOT AAD-bound; operator audit only. */
2418
+ encapsulated_at_ms?: number;
2419
+ }
2420
+ /** The `manifest.tier2` block. */
2421
+ interface Tier2Manifest {
2422
+ /** Either `"standard"` or `"forensic"`. openBundle rejects non-standard. */
2423
+ mode: string;
2424
+ /** Per-recipient wraps. */
2425
+ cek_envelopes: CEKWrapEnvelope[];
2426
+ /**
2427
+ * Producer-reported recipient count. Defense-in-depth cross-check
2428
+ * against `cek_envelopes.length`; mismatch returns BundleMalformedError.
2429
+ * Absent / zero values are tolerated.
2430
+ */
2431
+ pubkey_count?: number;
2432
+ }
2433
+ /** Parsed manifest.json (Tier-2 subset). */
2434
+ interface Manifest {
2435
+ tier2: Tier2Manifest;
2436
+ }
2437
+ /** Customer-side projection of a Tier-2 audit bundle. */
2438
+ interface Bundle {
2439
+ /** Canonical bundle identifier; MUST match seal-time value (AAD-bound). */
2440
+ id: string;
2441
+ manifest: Manifest;
2442
+ }
2443
+ /** Successful openBundle return. */
2444
+ interface OpenBundleResult {
2445
+ /** Recovered 32-byte content-encryption key. */
2446
+ cek: Uint8Array;
2447
+ /** 0-based index of the envelope that decrypted successfully. */
2448
+ matchedEnvelopeIndex: number;
2449
+ /** Fingerprint of the envelope that decrypted. */
2450
+ matchedFingerprint: string;
2451
+ }
2452
+
2453
+ /**
2454
+ * Exception hierarchy for Tier-2 unseal.
2455
+ *
2456
+ * Mirrors the Go SDK sentinels and the Python SDK exception classes.
2457
+ * Catch `Tier2Error` to handle any unseal failure uniformly.
2458
+ */
2459
+ /** Base class for all Tier-2 unseal failures. */
2460
+ declare class Tier2Error extends Error {
2461
+ constructor(message: string);
2462
+ }
2463
+ /**
2464
+ * Raised when the supplied seed is not exactly 32 bytes.
2465
+ */
2466
+ declare class SeedInvalidError extends Tier2Error {
2467
+ constructor(message: string);
2468
+ }
2469
+ /**
2470
+ * Raised when the bundle is structurally invalid: missing or empty
2471
+ * `id`, `tier2.mode` not `"standard"`, empty `cek_envelopes`, or a
2472
+ * `pubkey_count` value that disagrees with `len(cek_envelopes)`.
2473
+ */
2474
+ declare class BundleMalformedError extends Tier2Error {
2475
+ constructor(message: string);
2476
+ }
2477
+ /**
2478
+ * Raised when the seed's fingerprint matches no envelope in the bundle.
2479
+ *
2480
+ * SECURITY: deliberately opaque. Does NOT include the seed's fingerprint,
2481
+ * the bundle's recipient list, or any slot label — a distinguishing
2482
+ * error would leak which envelopes a bundle carries to an attacker who
2483
+ * controls the seed.
2484
+ */
2485
+ declare class NoMatchingRecipientError extends Tier2Error {
2486
+ constructor(message: string);
2487
+ }
2488
+ /**
2489
+ * Raised when the matched envelope's AEAD decryption fails. The single-
2490
+ * sentinel design (wrong seed, tampered ciphertext, AAD mismatch all
2491
+ * collapse here) mirrors the Go/Python SDKs' anti-oracle posture.
2492
+ */
2493
+ declare class OpenFailedError extends Tier2Error {
2494
+ constructor(message: string);
2495
+ }
2496
+
2497
+ /**
2498
+ * Unseal a Tier-2 Standard-mode bundle using one of the customer's
2499
+ * X-Wing private-key seeds.
2500
+ *
2501
+ * Per ADR-024 Amendment (2026-05-31), bundles ship with N=2
2502
+ * cek_envelopes (primary + backup). Either seed unseals; the function
2503
+ * is slot-agnostic. Inspect `matchedEnvelopeIndex` to determine which
2504
+ * slot was used.
2505
+ *
2506
+ * @param seed - Exactly 32 bytes of X-Wing private-key seed.
2507
+ * @param bundle - Parsed bundle with `manifest.tier2.cek_envelopes` populated.
2508
+ * @returns Recovered CEK and matched envelope metadata.
2509
+ * @throws {SeedInvalidError} If seed length !== 32.
2510
+ * @throws {BundleMalformedError} On structural rejection.
2511
+ * @throws {NoMatchingRecipientError} If no envelope matches the seed's fingerprint.
2512
+ * @throws {OpenFailedError} If envelope AEAD open fails.
2513
+ */
2514
+ declare function openBundle(seed: Uint8Array, bundle: Bundle): Promise<OpenBundleResult>;
2515
+ /**
2516
+ * Compute the 32-char lowercase hex fingerprint from a 32-byte X-Wing
2517
+ * private-key seed.
2518
+ *
2519
+ * Matches `hex(SHA-512(canonical_pubkey_1216)[:16])` — the form stored
2520
+ * in `CEKWrapEnvelope.pubkey_fingerprint`.
2521
+ *
2522
+ * @throws {SeedInvalidError} If seed length !== 32.
2523
+ */
2524
+ declare function deriveFingerprint(seed: Uint8Array): Promise<string>;
2525
+
2526
+ /**
2527
+ * `@aleutian/verification/tier2` — customer-side unseal for Tier-2
2528
+ * Standard-mode audit bundles. Browser- and Node-safe.
2529
+ *
2530
+ * Per ADR-024 Amendment (2026-05-31), bundles ship with N=2 X-Wing
2531
+ * recipient envelopes (primary + backup). `openBundle` accepts a
2532
+ * single seed and locates the matching envelope via constant-time
2533
+ * fingerprint compare.
2534
+ *
2535
+ * @example
2536
+ *
2537
+ * ```ts
2538
+ * import { openBundle, deriveFingerprint } from "@aleutian/verification/tier2";
2539
+ *
2540
+ * const seed = hexToBytes(myXWingPrimarySeed);
2541
+ * const bundle = parseManifest(downloadedManifestJSON);
2542
+ * const { cek, matchedEnvelopeIndex } = await openBundle(seed, bundle);
2543
+ * console.log(`unsealed via envelope ${matchedEnvelopeIndex}`);
2544
+ * ```
2545
+ */
2546
+
2547
+ type index_Bundle = Bundle;
2548
+ type index_BundleMalformedError = BundleMalformedError;
2549
+ declare const index_BundleMalformedError: typeof BundleMalformedError;
2550
+ type index_CEKWrapEnvelope = CEKWrapEnvelope;
2551
+ type index_Manifest = Manifest;
2552
+ type index_NoMatchingRecipientError = NoMatchingRecipientError;
2553
+ declare const index_NoMatchingRecipientError: typeof NoMatchingRecipientError;
2554
+ type index_OpenBundleResult = OpenBundleResult;
2555
+ type index_OpenFailedError = OpenFailedError;
2556
+ declare const index_OpenFailedError: typeof OpenFailedError;
2557
+ type index_SeedInvalidError = SeedInvalidError;
2558
+ declare const index_SeedInvalidError: typeof SeedInvalidError;
2559
+ type index_Tier2Error = Tier2Error;
2560
+ declare const index_Tier2Error: typeof Tier2Error;
2561
+ type index_Tier2Manifest = Tier2Manifest;
2562
+ declare const index_deriveFingerprint: typeof deriveFingerprint;
2563
+ declare const index_openBundle: typeof openBundle;
2564
+ declare namespace index {
2565
+ export { type index_Bundle as Bundle, index_BundleMalformedError as BundleMalformedError, type index_CEKWrapEnvelope as CEKWrapEnvelope, type index_Manifest as Manifest, index_NoMatchingRecipientError as NoMatchingRecipientError, type index_OpenBundleResult as OpenBundleResult, index_OpenFailedError as OpenFailedError, index_SeedInvalidError as SeedInvalidError, index_Tier2Error as Tier2Error, type index_Tier2Manifest as Tier2Manifest, index_deriveFingerprint as deriveFingerprint, index_openBundle as openBundle };
2566
+ }
2567
+
2568
+ /** The result id. A consumer MUST refuse any other (§9.4). */
2569
+ declare const PROOF_BUNDLE_RESULT_ID = "aleutian.proof.bundle-result.v1";
2570
+ /** The v1 bundle format string (§4.1). */
2571
+ declare const PROOF_BUNDLE_FORMAT = "aleutian.proof.bundle.v1";
2572
+ /**
2573
+ * Why a bundle is unreadable (§9.1). Precedence: too-large (size), json,
2574
+ * too-large (values), duplicate-key, format, structure, too-large (counts).
2575
+ */
2576
+ type ProofBundleUnreadableReason = "too-large" | "json" | "duplicate-key" | "format" | "structure";
2577
+ /** A verdict (§1). */
2578
+ type ProofBundleVerdict = "ok" | "unauthenticated" | "failed";
2579
+ /** Whether a trust dimension was checked (§6). */
2580
+ type ProofBundleTrustState = "checked" | "not_checked";
2581
+ /** A chain's erasure state (§7.9). */
2582
+ type ProofBundleErasureState = "none" | "recorded" | "anchored" | "signed";
2583
+ /** The complete v1 list of problem codes (§12). */
2584
+ type ProofBundleProblemCode = "duplicate-chain" | "invalid-chain-id" | "field-shape" | "duplicate-entry-id" | "first-entry" | "sequence" | "link" | "chain-hash" | "entry-type" | "erasure-invalid" | "after-erasure" | "erased-event-disclosed" | "modified" | "checkpoint-malformed" | "checkpoint-subject" | "checkpoint-sequence" | "checkpoint-overreach" | "checkpoint-binding" | "checkpoint-unknown-key" | "checkpoint-bad-signature" | "unsigned" | "malformed-signature" | "unknown-key" | "bad-signature";
2585
+ /** One problem code with its count and lowest index (§9.2). */
2586
+ interface ProofBundleProblem {
2587
+ readonly code: ProofBundleProblemCode;
2588
+ readonly count: number;
2589
+ /** The lowest index; null only for `invalid-chain-id`. */
2590
+ readonly first: number | null;
2591
+ }
2592
+ /** A chain's counts (§9.2). Every count is always present. */
2593
+ interface ProofBundleChainCounts {
2594
+ readonly entries: number;
2595
+ readonly events: number;
2596
+ readonly erasures: number;
2597
+ readonly opened: number;
2598
+ readonly committed: number;
2599
+ readonly erased: number;
2600
+ readonly checkpoints: number;
2601
+ readonly unanchored: number;
2602
+ readonly authenticated: number;
2603
+ readonly unauthenticated: number;
2604
+ readonly checkpoint_authenticated: number;
2605
+ readonly record_authenticated: number;
2606
+ readonly record_signatures_present: number;
2607
+ }
2608
+ /** One chain's result (§9.2). */
2609
+ interface ProofBundleChainResult {
2610
+ readonly verdict: ProofBundleVerdict;
2611
+ readonly erasure: ProofBundleErasureState;
2612
+ readonly counts: ProofBundleChainCounts;
2613
+ /** Sorted by `code` in byte order. */
2614
+ readonly problems: ProofBundleProblem[];
2615
+ }
2616
+ /** An unreadable bundle (§9.1). */
2617
+ interface ProofBundleUnreadableResult {
2618
+ readonly result: typeof PROOF_BUNDLE_RESULT_ID;
2619
+ readonly readable: false;
2620
+ readonly reason: ProofBundleUnreadableReason;
2621
+ }
2622
+ /** A readable bundle (§9.2). */
2623
+ interface ProofBundleReadableResult {
2624
+ readonly result: typeof PROOF_BUNDLE_RESULT_ID;
2625
+ readonly readable: true;
2626
+ readonly checkpoint_trust: ProofBundleTrustState;
2627
+ readonly record_trust: ProofBundleTrustState;
2628
+ readonly verdict: ProofBundleVerdict;
2629
+ /** Sorted by `code` in byte order. */
2630
+ readonly bundle_problems: ProofBundleProblem[];
2631
+ /** Keyed by chain id, or `#<i>` for an invalid or duplicated id (§9.3). */
2632
+ readonly chains: Record<string, ProofBundleChainResult>;
2633
+ }
2634
+ /** The §9 result. */
2635
+ type ProofBundleResult = ProofBundleUnreadableResult | ProofBundleReadableResult;
2636
+ /** The verifier's trust (§6): raw 1952-byte ML-DSA-65 public keys. */
2637
+ interface ProofBundleTrust {
2638
+ /** Keys trusted for checkpoints. Empty: the dimension is not checked. */
2639
+ readonly checkpointKeys: readonly Uint8Array[];
2640
+ /** Keys trusted for record signatures. Empty: the dimension is not checked. */
2641
+ readonly recordKeys: readonly Uint8Array[];
2642
+ }
2643
+ /** The §5 limits. */
2644
+ interface ProofBundleLimits {
2645
+ readonly bundleBytes: number;
2646
+ /** JSON values in a v1 bundle, at every depth, the top-level value included. */
2647
+ readonly values: number;
2648
+ /**
2649
+ * JSON values in a v2 bundle (§16 §5: 17,000,000). When absent, `values`
2650
+ * applies to both formats.
2651
+ */
2652
+ readonly valuesV2?: number;
2653
+ readonly chains: number;
2654
+ readonly entries: number;
2655
+ readonly checkpoints: number;
2656
+ readonly checkpointTextBytes: number;
2657
+ }
2658
+ /** The normative limits (§5). Every implementation MUST accept a bundle within them. */
2659
+ declare const PROOF_BUNDLE_LIMITS: ProofBundleLimits;
2660
+ /** Thrown only for a caller error (bad trust keys) or a bug; never names a bundle value. */
2661
+ declare class ProofBundleVerifierError extends Error {
2662
+ constructor(message: string);
2663
+ }
2664
+ /**
2665
+ * Verify an `aleutian.proof.bundle.v1` or `aleutian.proof.bundle.v2` bundle.
2666
+ *
2667
+ * # Description
2668
+ *
2669
+ * Applies bundle-format §3–§9 to the bundle bytes under the caller's trust and
2670
+ * returns the §9 result: either unreadable with its lowest-numbered reason, or
2671
+ * readable with both trust dimensions, the bundle verdict, bundle problems and
2672
+ * a result per chain. A dimension is checked exactly when its key set is
2673
+ * non-empty.
2674
+ *
2675
+ * # Inputs
2676
+ *
2677
+ * - bundle: the bundle file's exact bytes
2678
+ * - trust: raw 1952-byte ML-DSA-65 public keys trusted for checkpoints and
2679
+ * for record signatures (see `parseProofPublicKeyFile` for key files)
2680
+ *
2681
+ * # Outputs
2682
+ *
2683
+ * - ProofBundleResult: the §9 result. An unreadable or failing bundle is a
2684
+ * result, never an exception.
2685
+ * - ProofBundleVerifierError: thrown only when `bundle` is not a Uint8Array,
2686
+ * `trust` is not an object with two arrays, or a trust key is not a
2687
+ * 1952-byte Uint8Array. The message names no bundle value.
2688
+ *
2689
+ * # Example
2690
+ *
2691
+ * const result = await verifyProofBundle(bytes, {
2692
+ * checkpointKeys: [checkpointKey],
2693
+ * recordKeys: [],
2694
+ * });
2695
+ * if (result.readable && result.verdict === "ok") { ... }
2696
+ *
2697
+ * # Limitations
2698
+ *
2699
+ * - Holds the bundle and its parsed chains in memory (at most 256 MiB of
2700
+ * input and 15,000,000 JSON values for v1, 17,000,000 for v2, §5, §16);
2701
+ * hostile input is refused without
2702
+ * building more than a readable bundle would.
2703
+ * - A passing bundle proves only what §1 says: not that it holds every
2704
+ * chain, not that a chain is complete to its end, and nothing against a
2705
+ * writer that controls every key the verifier trusts.
2706
+ *
2707
+ * # Assumptions
2708
+ *
2709
+ * - WebCrypto (`crypto.subtle`) is available (Node ≥ 19, modern browsers).
2710
+ * - The caller chose the trust keys out of band; a bundle never adds a key.
2711
+ */
2712
+ declare function verifyProofBundle(bundle: Uint8Array, trust: ProofBundleTrust): Promise<ProofBundleResult>;
2713
+ /**
2714
+ * The key ids present in both trust sets (§6: allowed, but one key for both
2715
+ * roles means one compromise forges both, so a verifier SHOULD warn).
2716
+ *
2717
+ * # Description
2718
+ *
2719
+ * Computes each key's id (format-spec §9.6) and returns those in both sets,
2720
+ * sorted.
2721
+ *
2722
+ * # Inputs
2723
+ *
2724
+ * - trust: the two key sets
2725
+ *
2726
+ * # Outputs
2727
+ *
2728
+ * - string[]: key ids trusted for both checkpoints and records
2729
+ * - ProofBundleVerifierError: thrown when `trust` is not an object with two
2730
+ * arrays, or a key is not a 1952-byte Uint8Array
2731
+ *
2732
+ * # Example
2733
+ *
2734
+ * for (const id of await keysTrustedForBothRoles(trust)) warn(id);
2735
+ *
2736
+ * # Limitations
2737
+ *
2738
+ * - Compares key ids, which name keys exactly (SHA-512 based).
2739
+ *
2740
+ * # Assumptions
2741
+ *
2742
+ * - None.
2743
+ */
2744
+ declare function keysTrustedForBothRoles(trust: ProofBundleTrust): Promise<string[]>;
2745
+
2746
+ /** Thrown for any key file that is not exactly what §6 allows. */
2747
+ declare class ProofKeyFileError extends Error {
2748
+ constructor(message: string);
2749
+ }
2750
+ /**
2751
+ * Parse a trust key file into a raw ML-DSA-65 public key.
2752
+ *
2753
+ * # Description
2754
+ *
2755
+ * Accepts one PEM block labelled `PUBLIC KEY`, optionally surrounded by
2756
+ * whitespace, whose body lines (ending in LF or CRLF) join to canonical base64
2757
+ * of exactly the RFC 9881 SPKI DER of a 1952-byte ML-DSA-65 key. Returns the
2758
+ * raw 1952-byte key.
2759
+ *
2760
+ * # Inputs
2761
+ *
2762
+ * - bytes: the whole key file
2763
+ *
2764
+ * # Outputs
2765
+ *
2766
+ * - Uint8Array: the raw 1952-byte public key
2767
+ * - ProofKeyFileError: thrown, with a fixed message that quotes nothing from
2768
+ * the file, for any other file
2769
+ *
2770
+ * # Example
2771
+ *
2772
+ * const pk = parseProofPublicKeyFile(await readFile("checkpoint.pub"));
2773
+ *
2774
+ * # Limitations
2775
+ *
2776
+ * - ML-DSA-65 only. Text before the block is refused (only whitespace may
2777
+ * surround it).
2778
+ *
2779
+ * # Assumptions
2780
+ *
2781
+ * - None.
2782
+ */
2783
+ declare function parseProofPublicKeyFile(bytes: Uint8Array): Uint8Array;
2784
+
2785
+ /** The commitment domain string (format-spec §8). */
2786
+ declare const COMMITMENT_DOMAIN = "aleutian.commit.v1:";
2787
+ /** The only nonce length a commitment accepts, in bytes. */
2788
+ declare const COMMITMENT_NONCE_SIZE = 32;
2789
+ /**
2790
+ * Compute a salted commitment.
2791
+ *
2792
+ * # Description
2793
+ *
2794
+ * Returns `hex(SHA-512("aleutian.commit.v1:" ‖ nonce ‖ SHA-512(content)))`.
2795
+ *
2796
+ * # Inputs
2797
+ *
2798
+ * - nonce: exactly 32 bytes
2799
+ * - content: the committed bytes, any length
2800
+ *
2801
+ * # Outputs
2802
+ *
2803
+ * - string: 128 lowercase hex characters
2804
+ * - Error: thrown when the nonce is not exactly 32 bytes
2805
+ *
2806
+ * # Example
2807
+ *
2808
+ * const c = await computeCommitment(nonce, new TextEncoder().encode("consent: yes"));
2809
+ *
2810
+ * # Limitations
2811
+ *
2812
+ * - Imposes no content size limit; a caller that has one enforces it.
2813
+ *
2814
+ * # Assumptions
2815
+ *
2816
+ * - WebCrypto (`crypto.subtle`) is available.
2817
+ */
2818
+ declare function computeCommitment(nonce: Uint8Array, content: Uint8Array): Promise<string>;
2819
+ /**
2820
+ * Check whether a nonce and content open a commitment.
2821
+ *
2822
+ * # Description
2823
+ *
2824
+ * True exactly when the nonce is 32 bytes and the recomputed commitment equals
2825
+ * `commitment` character for character. A nonce of any other length is a
2826
+ * refusal (false), not an error.
2827
+ *
2828
+ * # Inputs
2829
+ *
2830
+ * - nonce: the disclosed nonce
2831
+ * - content: the disclosed content
2832
+ * - commitment: the expected commitment (an entry's `content_hash`)
2833
+ *
2834
+ * # Outputs
2835
+ *
2836
+ * - boolean: whether the commitment opens
2837
+ *
2838
+ * # Example
2839
+ *
2840
+ * if (!(await openCommitment(nonce, content, entry.content_hash))) { ... }
2841
+ *
2842
+ * # Limitations
2843
+ *
2844
+ * - Not constant-time; commitments and disclosures are public values.
2845
+ *
2846
+ * # Assumptions
2847
+ *
2848
+ * - WebCrypto (`crypto.subtle`) is available.
2849
+ */
2850
+ declare function openCommitment(nonce: Uint8Array, content: Uint8Array, commitment: string): Promise<boolean>;
2851
+
2852
+ /** The envelope's domain string; a 0x00 byte follows it. */
2853
+ declare const RECORD_ENVELOPE_DOMAIN = "aleutian.proof.record.v1";
2854
+ /** The eight values a record signature covers, as stored strings. */
2855
+ interface RecordEnvelopeFields {
2856
+ /** A sink chain id (sink-format §1). */
2857
+ readonly chainId: string;
2858
+ /** `sink-` and 32 lowercase hex. */
2859
+ readonly entryId: string;
2860
+ /** `sink.event` or `sink.erasure`. */
2861
+ readonly entryType: string;
2862
+ /** Base-10, no sign or leading zeros, 0 ≤ n ≤ 2⁶³−1. */
2863
+ readonly globalSeq: string;
2864
+ /** 128 lowercase hex; empty exactly when `globalSeq` is "0". */
2865
+ readonly previousHash: string;
2866
+ /** `YYYY-MM-DDTHH:MM:SS.ffffffZ`, a real instant, year 0001–9999. */
2867
+ readonly timestamp: string;
2868
+ /** 128 lowercase hex. */
2869
+ readonly contentHash: string;
2870
+ /** The signing key's id: 32 lowercase hex. */
2871
+ readonly signingKeyId: string;
2872
+ }
2873
+ /**
2874
+ * Whether every value is inside sink-format §9.1's table, so an envelope may
2875
+ * be built from it.
2876
+ */
2877
+ declare function isValidRecordEnvelopeFields(f: RecordEnvelopeFields): boolean;
2878
+ /**
2879
+ * Build the exact bytes a record signature covers.
2880
+ *
2881
+ * # Description
2882
+ *
2883
+ * Writes the domain, a 0x00 byte, then the eight tagged, length-prefixed
2884
+ * fields in order. Values are the stored strings, never re-formatted.
2885
+ *
2886
+ * # Inputs
2887
+ *
2888
+ * - fields: the eight envelope values
2889
+ *
2890
+ * # Outputs
2891
+ *
2892
+ * - Uint8Array: the envelope bytes
2893
+ * - Error: thrown, with a fixed message that quotes no value, when any value
2894
+ * is outside sink-format §9.1's table (no envelope may be built)
2895
+ *
2896
+ * # Example
2897
+ *
2898
+ * const envelope = buildRecordEnvelopeV1({ chainId, entryId, ... });
2899
+ *
2900
+ * # Limitations
2901
+ *
2902
+ * - Builds bytes only; {@link verifyRecordSignatureV1} checks a signature.
2903
+ *
2904
+ * # Assumptions
2905
+ *
2906
+ * - None: every value is validated here.
2907
+ */
2908
+ declare function buildRecordEnvelopeV1(fields: RecordEnvelopeFields): Uint8Array;
2909
+ /**
2910
+ * Verify a pure ML-DSA-65 signature with an EMPTY context.
2911
+ *
2912
+ * # Description
2913
+ *
2914
+ * FIPS 204 pure mode (not HashML-DSA), context of zero bytes, as proof signs
2915
+ * checkpoints (format-spec §9.5) and records (sink-format §9.1). A signature
2916
+ * made with a non-empty context, or with HashML-DSA, does not verify.
2917
+ *
2918
+ * # Inputs
2919
+ *
2920
+ * - publicKey: raw 1952-byte ML-DSA-65 public key
2921
+ * - message: the exact signed bytes
2922
+ * - signature: raw 3309-byte signature
2923
+ *
2924
+ * # Outputs
2925
+ *
2926
+ * - boolean: true only when the signature verifies. Never throws: a wrong
2927
+ * size or a library error is false.
2928
+ *
2929
+ * # Example
2930
+ *
2931
+ * if (!verifyMLDSA65Pure(pk, envelope, sig)) { ... }
2932
+ *
2933
+ * # Limitations
2934
+ *
2935
+ * - Does not resolve key ids; the caller matches the key to its id.
2936
+ *
2937
+ * # Assumptions
2938
+ *
2939
+ * - `@noble/post-quantum` 0.5: `verify(sig, msg, publicKey, { context })`
2940
+ * builds the pure-mode message `0x00 ‖ len(ctx) ‖ ctx ‖ msg`.
2941
+ */
2942
+ declare function verifyMLDSA65Pure(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
2943
+ /**
2944
+ * Verify a pure ML-DSA-65 signature made with a given FIPS 204 context.
2945
+ *
2946
+ * # Description
2947
+ *
2948
+ * FIPS 204 pure mode (not HashML-DSA) with context `ctx` (0 to 255 bytes):
2949
+ * the signed message is `0x00 ‖ len(ctx) ‖ ctx ‖ message`. A signature made
2950
+ * with any other context, or none, does not verify. proof uses
2951
+ * `aleutian.proof.anchor.v7` for v7 anchors (format-spec §9.5) and
2952
+ * `aleutian.proof.record.v2` for record.v2 signatures (sink-format §9.1).
2953
+ *
2954
+ * # Inputs
2955
+ *
2956
+ * - publicKey: raw 1952-byte ML-DSA-65 public key
2957
+ * - message: the exact signed bytes
2958
+ * - signature: raw 3309-byte signature
2959
+ * - context: the context bytes (the caller chooses it from the declared
2960
+ * version, never from the signature)
2961
+ *
2962
+ * # Outputs
2963
+ *
2964
+ * - boolean: true only when the signature verifies. Never throws.
2965
+ *
2966
+ * # Example
2967
+ *
2968
+ * verifyMLDSA65PureWithContext(pk, envelope, sig, utf8("aleutian.proof.record.v2"));
2969
+ *
2970
+ * # Limitations
2971
+ *
2972
+ * - Does not resolve key ids.
2973
+ *
2974
+ * # Assumptions
2975
+ *
2976
+ * - `@noble/post-quantum` 0.5's `{ context }` option.
2977
+ */
2978
+ declare function verifyMLDSA65PureWithContext(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array, context: Uint8Array): boolean;
2979
+ /**
2980
+ * Verify a record signature over the `record.v1` envelope of `fields`.
2981
+ *
2982
+ * # Description
2983
+ *
2984
+ * Builds the envelope from the stored values and verifies the signature (pure
2985
+ * ML-DSA-65, empty context). False when no envelope can be built.
2986
+ *
2987
+ * # Inputs
2988
+ *
2989
+ * - publicKey: raw 1952-byte ML-DSA-65 public key, whose key id the caller
2990
+ * has checked equals `fields.signingKeyId`
2991
+ * - fields: the eight envelope values
2992
+ * - signature: raw 3309-byte signature
2993
+ *
2994
+ * # Outputs
2995
+ *
2996
+ * - boolean: whether the signature verifies. Never throws.
2997
+ *
2998
+ * # Example
2999
+ *
3000
+ * const ok = verifyRecordSignatureV1(pk, fields, sig);
3001
+ *
3002
+ * # Limitations
3003
+ *
3004
+ * - Does not check that `publicKey` has id `fields.signingKeyId`.
3005
+ *
3006
+ * # Assumptions
3007
+ *
3008
+ * - The caller decided the key is trusted for records.
3009
+ */
3010
+ declare function verifyRecordSignatureV1(publicKey: Uint8Array, fields: RecordEnvelopeFields, signature: Uint8Array): boolean;
3011
+ /** The record.v2 envelope's domain string; a 0x00 byte follows it. */
3012
+ declare const RECORD_ENVELOPE_DOMAIN_V2 = "aleutian.proof.record.v2";
3013
+ /**
3014
+ * The ML-DSA context a record.v2 signature is made with (sink-format §9.1).
3015
+ * record.v1 uses the empty context.
3016
+ */
3017
+ declare const RECORD_SIGNATURE_CONTEXT_V2 = "aleutian.proof.record.v2";
3018
+ /** The nine values a record.v2 signature covers: record.v1's eight and the leaf hash. */
3019
+ interface RecordEnvelopeFieldsV2 extends RecordEnvelopeFields {
3020
+ /** The entry's leaf hash (format-spec §2.0): 128 lowercase hex. */
3021
+ readonly leafHash: string;
3022
+ }
3023
+ /** Whether every value is inside sink-format §9.1's table, leaf hash included. */
3024
+ declare function isValidRecordEnvelopeFieldsV2(f: RecordEnvelopeFieldsV2): boolean;
3025
+ /**
3026
+ * Build the exact bytes a record.v2 signature covers (sink-format §9.1).
3027
+ *
3028
+ * # Description
3029
+ *
3030
+ * `"aleutian.proof.record.v2" 0x00`, then fields 0x01-0x08 as record.v1
3031
+ * (chain id, entry id, entry type, global seq, previous hash, timestamp,
3032
+ * content hash, signing key id), then 0x09 the leaf hash. Signing the leaf
3033
+ * hash binds the salt, which the content hash alone does not. Values are the
3034
+ * stored strings, never re-formatted.
3035
+ *
3036
+ * # Inputs
3037
+ *
3038
+ * - fields: the nine envelope values
3039
+ *
3040
+ * # Outputs
3041
+ *
3042
+ * - Uint8Array: the envelope bytes
3043
+ * - Error: thrown, with a fixed message, when a value is outside the table
3044
+ *
3045
+ * # Example
3046
+ *
3047
+ * const envelope = buildRecordEnvelopeV2({ ...fieldsV1, leafHash });
3048
+ *
3049
+ * # Limitations
3050
+ *
3051
+ * - Builds bytes only; {@link verifyRecordSignatureV2} checks a signature.
3052
+ *
3053
+ * # Assumptions
3054
+ *
3055
+ * - None: every value is validated here.
3056
+ */
3057
+ declare function buildRecordEnvelopeV2(fields: RecordEnvelopeFieldsV2): Uint8Array;
3058
+ /**
3059
+ * Verify a record signature over the record.v2 envelope of `fields`.
3060
+ *
3061
+ * # Description
3062
+ *
3063
+ * Builds the v2 envelope and verifies the signature as pure ML-DSA-65 with the
3064
+ * context `aleutian.proof.record.v2`. A record.v1 signature, or one made with
3065
+ * the empty or another context, does not verify. False when no envelope can
3066
+ * be built.
3067
+ *
3068
+ * # Inputs
3069
+ *
3070
+ * - publicKey: raw 1952-byte ML-DSA-65 public key, whose key id the caller
3071
+ * has checked equals `fields.signingKeyId`
3072
+ * - fields: the nine envelope values
3073
+ * - signature: raw 3309-byte signature
3074
+ *
3075
+ * # Outputs
3076
+ *
3077
+ * - boolean: whether the signature verifies. Never throws.
3078
+ *
3079
+ * # Example
3080
+ *
3081
+ * const ok = verifyRecordSignatureV2(pk, fields, sig);
3082
+ *
3083
+ * # Limitations
3084
+ *
3085
+ * - Does not check that `publicKey` has id `fields.signingKeyId`.
3086
+ *
3087
+ * # Assumptions
3088
+ *
3089
+ * - The caller decided the key is trusted for records.
3090
+ */
3091
+ declare function verifyRecordSignatureV2(publicKey: Uint8Array, fields: RecordEnvelopeFieldsV2, signature: Uint8Array): boolean;
3092
+
3093
+ export { ANCHOR_CHAIN_DOMAIN_V2, ANCHOR_CHAIN_DOMAIN_V7, ANCHOR_SIGNATURE_CONTEXT_V7, type Anchor, type AnchorBindExpectations, type AnchorBindOutcome, type AnchorBindResult, type AnchorRange, AnchorVerificationError, type BreakType, CHAIN_HASH_PREFIX, CHAIN_HASH_PREFIX_V4, CHAIN_LEAF_DOMAIN_V4, COMMITMENT_DOMAIN, COMMITMENT_NONCE_SIZE, CONCRETE_SAR_ENTRY_TYPE_TAGS, CONSENT_ENTRY_HASH_PREFIX, CONSENT_RENDERED_HASH_PREFIX, CONSENT_TEXT_HASH_PREFIX, CONTENT_HASH_V3_BYTES, type ChainEntryV3, ChainVerificationError, type ChainVerifyOptions, type ConsentChainEntry, ConsentVerificationError, DOMAIN_PREFIX_V3, DOMAIN_PREFIX_V3_BYTES, EMBEDDED_PUBLIC_KEYS, EXTENDED_SANITY_BOUND_MS, EntriesFormatError, ErrContentHashMismatchV3, ErrInvalidEntryV3, ErrPublicKeyMalformedV3, ErrSignatureInvalidV3, ErrSignatureMalformedV3, ErrUnknownSigningKeyV3, type EventTypeV3, type ExportedEntry, FORMAT_V4, InvalidConsentEntryError, InvalidProcessingMode, KEYID_DOMAIN, MANIFEST_FRESHNESS_MAX_AGE_DAYS, MAX_CANONICAL_BYTES_V3, MAX_V3_STRING_FIELD_BYTES, type MLDSAErrorCode, MLDSAVerificationError, PUBLIC_KEY_SIZE as MLDSA_PUBLIC_KEY_SIZE, MLDSA_PUB_KEY_SIZE, SIGNATURE_SIZE as MLDSA_SIGNATURE_SIZE, type ManifestFreshness, type OfflineBreak, type OfflineVerificationResult, PII_INSPECTION, PROOF_BUNDLE_FORMAT, PROOF_BUNDLE_LIMITS, PROOF_BUNDLE_RESULT_ID, PUBLIC_KEYS_ENDPOINT, type ProcessingModeWire, type ProofBundleChainCounts, type ProofBundleChainResult, type ProofBundleErasureState, type ProofBundleLimits, type ProofBundleProblem, type ProofBundleProblemCode, type ProofBundleReadableResult, type ProofBundleResult, type ProofBundleTrust, type ProofBundleTrustState, type ProofBundleUnreadableReason, type ProofBundleUnreadableResult, type ProofBundleVerdict, ProofBundleVerifierError, ProofKeyFileError, RECORD_ENVELOPE_DOMAIN, RECORD_ENVELOPE_DOMAIN_V2, RECORD_SIGNATURE_CONTEXT_V2, type RecordEnvelopeFields, type RecordEnvelopeFieldsV2, type SARAcknowledgedEntryV3, type SARExtendedEntryV3, type SARFulfilledEntryV3, type SARFulfillmentKindV3, type SARIntentEntryV3, type SARRejectedEntryV3, type SARSourceChannelV3, type SARTypeV3, SEED_ANCHOR_HASH, SEED_ANCHOR_ID, TOMBSTONE_CONTENT_HASH_PREFIX, TOMBSTONE_ENTRY_ID_PREFIX, TOMBSTONE_ENTRY_TYPE, type TenantHMACDigestV3, TrustManifestV3, V3Encoder, V3_SHA512_HEX_LEN, type VerifyChainEntryV3Options, ZK, bindAnchorToEntries, bindAnchorV7ToEntries, buildRecordEnvelopeV1, buildRecordEnvelopeV2, bytesToHex, canonicalizeAnchor, canonicalizeAnchorV7, chainCanonicalV3, checkAnchorWellFormed, computeAnchorChainHash, computeAnchorChainHashV7, computeChainHash, computeChainHashMs, computeChainHashV4, computeCommitment, computeLeafHashV4, computeV3ContentHash, consentCanonical, constantTimeEqual, deriveConsentContentHash, encodeAndHashV3, encodeProcessingModeCanonicalFields, evaluateManifestFreshness, formatTimestampMicro, getPublicKey, hasValidChainEntryTag, hashTemplate, isChainDigest, isChainEntryV3, isChainTimestamp, isKnownKey, isTombstone, isTombstoneContentHash, isValidRecordEnvelopeFields, isValidRecordEnvelopeFieldsV2, keyIdMLDSA65, keyIdXWing, keysTrustedForBothRoles, listKeyIds, manifestGeneratedDate, manifestRevocationNote, manifestVerdictLine, mldsa, mldsa65Spki, openCommitment, parseAnchor, parseAnchorText, parseEntriesText, parseExportedEntries, parseProcessingMode, parseProofPublicKeyFile, tenantHMACDigestFromHex, tenantHMACDigestFromRaw, tenantHMACDigestIsZero, tenantHMACDigestZero, index as tier2, validateEmbeddedKeys, validateTombstoneContentHash, validateV3Entry, verifyAnchorLocally, verifyChainEntryV3, verifyChainOffline, verifyConsentEntry, verifyConsentSignature, verifyMLDSA65Pure, verifyMLDSA65PureWithContext, verifyProofBundle, verifyRecordSignatureV1, verifyRecordSignatureV2 };