@bcts/provenance-mark 1.0.0-alpha.9 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,68 +1,81 @@
1
- import { createRequire } from "node:module";
2
1
  import { cbor, cborData, decodeCbor, expectArray, expectBytes, expectUnsigned } from "@bcts/dcbor";
3
- import { sha256 as sha256$1 } from "@noble/hashes/sha256";
4
- import { hkdf } from "@noble/hashes/hkdf";
5
- import { chacha20 } from "@noble/ciphers/chacha";
6
2
  import { randomData } from "@bcts/rand";
3
+ import { sha256 as sha256$1 } from "@noble/hashes/sha2.js";
4
+ import { hkdf } from "@noble/hashes/hkdf.js";
5
+ import { chacha20 } from "@noble/ciphers/chacha.js";
7
6
  import { PROVENANCE_MARK } from "@bcts/tags";
8
- import { BytewordsStyle, UR, decodeBytewords, encodeBytemojisIdentifier, encodeBytewords, encodeBytewordsIdentifier } from "@bcts/uniform-resources";
9
-
10
- //#region rolldown:runtime
11
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
-
13
- //#endregion
7
+ import { BytewordsStyle, UR, decodeBytewords, encodeBytewords, encodeToBytemojis, encodeToMinimalBytewords, encodeToWords } from "@bcts/uniform-resources";
8
+ import { Envelope, FormatContext, registerTagsIn as registerTagsIn$1, withFormatContextMut } from "@bcts/envelope";
14
9
  //#region src/error.ts
15
10
  /**
11
+ * Copyright © 2023-2026 Blockchain Commons, LLC
12
+ * Copyright © 2025-2026 Parity Technologies
13
+ *
14
+ */
15
+ /**
16
16
  * Error types for Provenance Mark operations.
17
17
  */
18
- let ProvenanceMarkErrorType = /* @__PURE__ */ function(ProvenanceMarkErrorType$1) {
18
+ let ProvenanceMarkErrorType = /* @__PURE__ */ function(ProvenanceMarkErrorType) {
19
19
  /** Invalid Seed length */
20
- ProvenanceMarkErrorType$1["InvalidSeedLength"] = "InvalidSeedLength";
20
+ ProvenanceMarkErrorType["InvalidSeedLength"] = "InvalidSeedLength";
21
21
  /** Duplicate key */
22
- ProvenanceMarkErrorType$1["DuplicateKey"] = "DuplicateKey";
22
+ ProvenanceMarkErrorType["DuplicateKey"] = "DuplicateKey";
23
23
  /** Missing key */
24
- ProvenanceMarkErrorType$1["MissingKey"] = "MissingKey";
24
+ ProvenanceMarkErrorType["MissingKey"] = "MissingKey";
25
25
  /** Invalid key */
26
- ProvenanceMarkErrorType$1["InvalidKey"] = "InvalidKey";
26
+ ProvenanceMarkErrorType["InvalidKey"] = "InvalidKey";
27
27
  /** Extra keys */
28
- ProvenanceMarkErrorType$1["ExtraKeys"] = "ExtraKeys";
28
+ ProvenanceMarkErrorType["ExtraKeys"] = "ExtraKeys";
29
29
  /** Invalid key length for the given resolution */
30
- ProvenanceMarkErrorType$1["InvalidKeyLength"] = "InvalidKeyLength";
30
+ ProvenanceMarkErrorType["InvalidKeyLength"] = "InvalidKeyLength";
31
31
  /** Invalid next key length for the given resolution */
32
- ProvenanceMarkErrorType$1["InvalidNextKeyLength"] = "InvalidNextKeyLength";
32
+ ProvenanceMarkErrorType["InvalidNextKeyLength"] = "InvalidNextKeyLength";
33
33
  /** Invalid chain ID length for the given resolution */
34
- ProvenanceMarkErrorType$1["InvalidChainIdLength"] = "InvalidChainIdLength";
34
+ ProvenanceMarkErrorType["InvalidChainIdLength"] = "InvalidChainIdLength";
35
35
  /** Invalid message length for the given resolution */
36
- ProvenanceMarkErrorType$1["InvalidMessageLength"] = "InvalidMessageLength";
36
+ ProvenanceMarkErrorType["InvalidMessageLength"] = "InvalidMessageLength";
37
37
  /** Invalid CBOR data in info field */
38
- ProvenanceMarkErrorType$1["InvalidInfoCbor"] = "InvalidInfoCbor";
38
+ ProvenanceMarkErrorType["InvalidInfoCbor"] = "InvalidInfoCbor";
39
39
  /** Date out of range for serialization */
40
- ProvenanceMarkErrorType$1["DateOutOfRange"] = "DateOutOfRange";
40
+ ProvenanceMarkErrorType["DateOutOfRange"] = "DateOutOfRange";
41
41
  /** Invalid date components */
42
- ProvenanceMarkErrorType$1["InvalidDate"] = "InvalidDate";
42
+ ProvenanceMarkErrorType["InvalidDate"] = "InvalidDate";
43
43
  /** Missing required URL parameter */
44
- ProvenanceMarkErrorType$1["MissingUrlParameter"] = "MissingUrlParameter";
44
+ ProvenanceMarkErrorType["MissingUrlParameter"] = "MissingUrlParameter";
45
45
  /** Year out of range for 2-byte serialization */
46
- ProvenanceMarkErrorType$1["YearOutOfRange"] = "YearOutOfRange";
46
+ ProvenanceMarkErrorType["YearOutOfRange"] = "YearOutOfRange";
47
47
  /** Invalid month or day */
48
- ProvenanceMarkErrorType$1["InvalidMonthOrDay"] = "InvalidMonthOrDay";
48
+ ProvenanceMarkErrorType["InvalidMonthOrDay"] = "InvalidMonthOrDay";
49
49
  /** Resolution serialization error */
50
- ProvenanceMarkErrorType$1["ResolutionError"] = "ResolutionError";
50
+ ProvenanceMarkErrorType["ResolutionError"] = "ResolutionError";
51
51
  /** Bytewords encoding/decoding error */
52
- ProvenanceMarkErrorType$1["BytewordsError"] = "BytewordsError";
52
+ ProvenanceMarkErrorType["BytewordsError"] = "BytewordsError";
53
53
  /** CBOR encoding/decoding error */
54
- ProvenanceMarkErrorType$1["CborError"] = "CborError";
54
+ ProvenanceMarkErrorType["CborError"] = "CborError";
55
55
  /** URL parsing error */
56
- ProvenanceMarkErrorType$1["UrlError"] = "UrlError";
56
+ ProvenanceMarkErrorType["UrlError"] = "UrlError";
57
57
  /** Base64 decoding error */
58
- ProvenanceMarkErrorType$1["Base64Error"] = "Base64Error";
58
+ ProvenanceMarkErrorType["Base64Error"] = "Base64Error";
59
59
  /** JSON serialization error */
60
- ProvenanceMarkErrorType$1["JsonError"] = "JsonError";
60
+ ProvenanceMarkErrorType["JsonError"] = "JsonError";
61
61
  /** Integer conversion error */
62
- ProvenanceMarkErrorType$1["IntegerConversionError"] = "IntegerConversionError";
62
+ ProvenanceMarkErrorType["IntegerConversionError"] = "IntegerConversionError";
63
63
  /** Validation error */
64
- ProvenanceMarkErrorType$1["ValidationError"] = "ValidationError";
65
- return ProvenanceMarkErrorType$1;
64
+ ProvenanceMarkErrorType["ValidationError"] = "ValidationError";
65
+ /**
66
+ * Envelope serialization/deserialization error.
67
+ *
68
+ * Mirrors Rust `Error::Envelope(...)`
69
+ * (`provenance-mark-rust/src/error.rs`). The Rust enum surfaces
70
+ * envelope-format failures as their own variant; in earlier
71
+ * revisions of this port they collapsed into `CborError`. Both
72
+ * shapes are still emitted in practice (CBOR errors during envelope
73
+ * round-trip stay as `CborError`); this variant exists for the
74
+ * structural-level mismatches the Rust port tags as
75
+ * `Error::Envelope`.
76
+ */
77
+ ProvenanceMarkErrorType["EnvelopeError"] = "EnvelopeError";
78
+ return ProvenanceMarkErrorType;
66
79
  }({});
67
80
  /**
68
81
  * Error class for Provenance Mark operations.
@@ -85,37 +98,42 @@ var ProvenanceMarkError = class ProvenanceMarkError extends Error {
85
98
  return JSON.stringify(value);
86
99
  };
87
100
  switch (type) {
88
- case ProvenanceMarkErrorType.InvalidSeedLength: return `invalid seed length: expected 32 bytes, got ${d("actual")} bytes`;
89
- case ProvenanceMarkErrorType.DuplicateKey: return `duplicate key: ${d("key")}`;
90
- case ProvenanceMarkErrorType.MissingKey: return `missing key: ${d("key")}`;
91
- case ProvenanceMarkErrorType.InvalidKey: return `invalid key: ${d("key")}`;
92
- case ProvenanceMarkErrorType.ExtraKeys: return `wrong number of keys: expected ${d("expected")}, got ${d("actual")}`;
93
- case ProvenanceMarkErrorType.InvalidKeyLength: return `invalid key length: expected ${d("expected")}, got ${d("actual")}`;
94
- case ProvenanceMarkErrorType.InvalidNextKeyLength: return `invalid next key length: expected ${d("expected")}, got ${d("actual")}`;
95
- case ProvenanceMarkErrorType.InvalidChainIdLength: return `invalid chain ID length: expected ${d("expected")}, got ${d("actual")}`;
96
- case ProvenanceMarkErrorType.InvalidMessageLength: return `invalid message length: expected at least ${d("expected")}, got ${d("actual")}`;
97
- case ProvenanceMarkErrorType.InvalidInfoCbor: return "invalid CBOR data in info field";
98
- case ProvenanceMarkErrorType.DateOutOfRange: return `date out of range: ${d("details")}`;
99
- case ProvenanceMarkErrorType.InvalidDate: return `invalid date: ${d("details")}`;
100
- case ProvenanceMarkErrorType.MissingUrlParameter: return `missing required URL parameter: ${d("parameter")}`;
101
- case ProvenanceMarkErrorType.YearOutOfRange: return `year out of range for 2-byte serialization: must be between 2023-2150, got ${d("year")}`;
102
- case ProvenanceMarkErrorType.InvalidMonthOrDay: return `invalid month (${d("month")}) or day (${d("day")}) for year ${d("year")}`;
103
- case ProvenanceMarkErrorType.ResolutionError: return `resolution serialization error: ${d("details")}`;
104
- case ProvenanceMarkErrorType.BytewordsError: return `bytewords error: ${d("message")}`;
105
- case ProvenanceMarkErrorType.CborError: return `CBOR error: ${d("message")}`;
106
- case ProvenanceMarkErrorType.UrlError: return `URL parsing error: ${d("message")}`;
107
- case ProvenanceMarkErrorType.Base64Error: return `base64 decoding error: ${d("message")}`;
108
- case ProvenanceMarkErrorType.JsonError: return `JSON error: ${d("message")}`;
109
- case ProvenanceMarkErrorType.IntegerConversionError: return `integer conversion error: ${d("message")}`;
110
- case ProvenanceMarkErrorType.ValidationError: return `validation error: ${d("message")}`;
101
+ case "InvalidSeedLength": return `invalid seed length: expected 32 bytes, got ${d("actual")} bytes`;
102
+ case "DuplicateKey": return `duplicate key: ${d("key")}`;
103
+ case "MissingKey": return `missing key: ${d("key")}`;
104
+ case "InvalidKey": return `invalid key: ${d("key")}`;
105
+ case "ExtraKeys": return `wrong number of keys: expected ${d("expected")}, got ${d("actual")}`;
106
+ case "InvalidKeyLength": return `invalid key length: expected ${d("expected")}, got ${d("actual")}`;
107
+ case "InvalidNextKeyLength": return `invalid next key length: expected ${d("expected")}, got ${d("actual")}`;
108
+ case "InvalidChainIdLength": return `invalid chain ID length: expected ${d("expected")}, got ${d("actual")}`;
109
+ case "InvalidMessageLength": return `invalid message length: expected at least ${d("expected")}, got ${d("actual")}`;
110
+ case "InvalidInfoCbor": return "invalid CBOR data in info field";
111
+ case "DateOutOfRange": return `date out of range: ${d("details")}`;
112
+ case "InvalidDate": return `invalid date: ${d("details")}`;
113
+ case "MissingUrlParameter": return `missing required URL parameter: ${d("parameter")}`;
114
+ case "YearOutOfRange": return `year out of range for 2-byte serialization: must be between 2023-2150, got ${d("year")}`;
115
+ case "InvalidMonthOrDay": return `invalid month (${d("month")}) or day (${d("day")}) for year ${d("year")}`;
116
+ case "ResolutionError": return `resolution serialization error: ${d("details")}`;
117
+ case "BytewordsError": return `bytewords error: ${d("message")}`;
118
+ case "CborError": return `CBOR error: ${d("message")}`;
119
+ case "UrlError": return `URL parsing error: ${d("message")}`;
120
+ case "Base64Error": return `base64 decoding error: ${d("message")}`;
121
+ case "JsonError": return `JSON error: ${d("message")}`;
122
+ case "IntegerConversionError": return `integer conversion error: ${d("message")}`;
123
+ case "ValidationError": return `validation error: ${d("message")}`;
124
+ case "EnvelopeError": return `envelope error: ${d("message")}`;
111
125
  default: return type;
112
126
  }
113
127
  }
114
128
  };
115
-
116
129
  //#endregion
117
130
  //#region src/date.ts
118
131
  /**
132
+ * Copyright © 2023-2026 Blockchain Commons, LLC
133
+ * Copyright © 2025-2026 Parity Technologies
134
+ *
135
+ */
136
+ /**
119
137
  * Reference date for 4-byte and 6-byte serialization (2001-01-01T00:00:00Z).
120
138
  */
121
139
  const REFERENCE_DATE = Date.UTC(2001, 0, 1, 0, 0, 0, 0);
@@ -146,8 +164,8 @@ function serialize2Bytes(date) {
146
164
  const month = date.getUTCMonth() + 1;
147
165
  const day = date.getUTCDate();
148
166
  const yy = year - 2023;
149
- if (yy < 0 || yy >= 128) throw new ProvenanceMarkError(ProvenanceMarkErrorType.YearOutOfRange, void 0, { year });
150
- if (month < 1 || month > 12 || day < 1 || day > 31) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidMonthOrDay, void 0, {
167
+ if (yy < 0 || yy >= 128) throw new ProvenanceMarkError("YearOutOfRange", void 0, { year });
168
+ if (month < 1 || month > 12 || day < 1 || day > 31) throw new ProvenanceMarkError("InvalidMonthOrDay", void 0, {
151
169
  year,
152
170
  month,
153
171
  day
@@ -162,12 +180,12 @@ function serialize2Bytes(date) {
162
180
  * Deserialize 2 bytes to a date.
163
181
  */
164
182
  function deserialize2Bytes(bytes) {
165
- if (bytes.length !== 2) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidDate, void 0, { details: `expected 2 bytes, got ${bytes.length}` });
183
+ if (bytes.length !== 2) throw new ProvenanceMarkError("InvalidDate", void 0, { details: `expected 2 bytes, got ${bytes.length}` });
166
184
  const value = bytes[0] << 8 | bytes[1];
167
185
  const day = value & 31;
168
186
  const month = value >> 5 & 15;
169
187
  const year = (value >> 9 & 127) + 2023;
170
- if (month < 1 || month > 12 || !isValidDay(year, month, day)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidMonthOrDay, void 0, {
188
+ if (month < 1 || month > 12 || !isValidDay(year, month, day)) throw new ProvenanceMarkError("InvalidMonthOrDay", void 0, {
171
189
  year,
172
190
  month,
173
191
  day
@@ -180,7 +198,7 @@ function deserialize2Bytes(bytes) {
180
198
  function serialize4Bytes(date) {
181
199
  const duration = date.getTime() - REFERENCE_DATE;
182
200
  const seconds = Math.floor(duration / 1e3);
183
- if (seconds < 0 || seconds > 4294967295) throw new ProvenanceMarkError(ProvenanceMarkErrorType.DateOutOfRange, void 0, { details: "seconds value out of range for u32" });
201
+ if (seconds < 0 || seconds > 4294967295) throw new ProvenanceMarkError("DateOutOfRange", void 0, { details: "seconds value out of range for u32" });
184
202
  const buf = new Uint8Array(4);
185
203
  buf[0] = seconds >> 24 & 255;
186
204
  buf[1] = seconds >> 16 & 255;
@@ -192,7 +210,7 @@ function serialize4Bytes(date) {
192
210
  * Deserialize 4 bytes to a date.
193
211
  */
194
212
  function deserialize4Bytes(bytes) {
195
- if (bytes.length !== 4) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidDate, void 0, { details: `expected 4 bytes, got ${bytes.length}` });
213
+ if (bytes.length !== 4) throw new ProvenanceMarkError("InvalidDate", void 0, { details: `expected 4 bytes, got ${bytes.length}` });
196
214
  const seconds = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
197
215
  return new Date(REFERENCE_DATE + seconds * 1e3);
198
216
  }
@@ -202,7 +220,7 @@ function deserialize4Bytes(bytes) {
202
220
  function serialize6Bytes(date) {
203
221
  const duration = date.getTime() - REFERENCE_DATE;
204
222
  const milliseconds = BigInt(duration);
205
- if (milliseconds < 0n || milliseconds > BigInt(MAX_6_BYTE_VALUE)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.DateOutOfRange, void 0, { details: "date exceeds maximum representable value" });
223
+ if (milliseconds < 0n || milliseconds > BigInt(MAX_6_BYTE_VALUE)) throw new ProvenanceMarkError("DateOutOfRange", void 0, { details: "date exceeds maximum representable value" });
206
224
  const buf = new Uint8Array(6);
207
225
  buf[0] = Number(milliseconds >> 40n & 255n);
208
226
  buf[1] = Number(milliseconds >> 32n & 255n);
@@ -216,9 +234,9 @@ function serialize6Bytes(date) {
216
234
  * Deserialize 6 bytes to a date.
217
235
  */
218
236
  function deserialize6Bytes(bytes) {
219
- if (bytes.length !== 6) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidDate, void 0, { details: `expected 6 bytes, got ${bytes.length}` });
237
+ if (bytes.length !== 6) throw new ProvenanceMarkError("InvalidDate", void 0, { details: `expected 6 bytes, got ${bytes.length}` });
220
238
  const milliseconds = BigInt(bytes[0]) << 40n | BigInt(bytes[1]) << 32n | BigInt(bytes[2]) << 24n | BigInt(bytes[3]) << 16n | BigInt(bytes[4]) << 8n | BigInt(bytes[5]);
221
- if (milliseconds > BigInt(MAX_6_BYTE_VALUE)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.DateOutOfRange, void 0, { details: "date exceeds maximum representable value" });
239
+ if (milliseconds > BigInt(MAX_6_BYTE_VALUE)) throw new ProvenanceMarkError("DateOutOfRange", void 0, { details: "date exceeds maximum representable value" });
222
240
  return new Date(REFERENCE_DATE + Number(milliseconds));
223
241
  }
224
242
  /**
@@ -241,7 +259,7 @@ function dateToIso8601(date) {
241
259
  */
242
260
  function dateFromIso8601(str) {
243
261
  const date = new Date(str);
244
- if (isNaN(date.getTime())) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidDate, void 0, { details: `cannot parse date: ${str}` });
262
+ if (isNaN(date.getTime())) throw new ProvenanceMarkError("InvalidDate", void 0, { details: `cannot parse date: ${str}` });
245
263
  return date;
246
264
  }
247
265
  /**
@@ -250,19 +268,50 @@ function dateFromIso8601(str) {
250
268
  function dateToDateString(date) {
251
269
  return `${date.getUTCFullYear()}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`;
252
270
  }
253
-
271
+ /**
272
+ * Renders a `Date` the way Rust's `dcbor::Date::Display` does
273
+ * (`bc-dcbor-rust/src/date.rs:485-492`):
274
+ *
275
+ * - When the UTC time is exactly `00:00:00` (subsecond precision is
276
+ * ignored — Rust's check is `hour == 0 && minute == 0 && second == 0`,
277
+ * matching `chrono::SecondsFormat::Secs`), emit just `YYYY-MM-DD`.
278
+ * - Otherwise emit RFC 3339 with second precision (no fractional
279
+ * seconds), e.g. `2023-02-08T15:30:45Z`.
280
+ *
281
+ * This is the canonical "Rust string" for dates across the
282
+ * provenance-mark public surface — `mark.toDebugString`,
283
+ * `mark.precedesOpt` `DateOrdering` issue, `markdownSummary`, and the
284
+ * validation report's `DateOrdering` payload all use it. Centralising
285
+ * here keeps every call site in lockstep with the Rust output.
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * dateToDisplay(new Date("2023-06-20T00:00:00Z")); // "2023-06-20"
290
+ * dateToDisplay(new Date("2023-06-20T15:30:45Z")); // "2023-06-20T15:30:45Z"
291
+ * dateToDisplay(new Date("2023-06-20T15:30:45.123Z")); // "2023-06-20T15:30:45Z"
292
+ * ```
293
+ */
294
+ function dateToDisplay(date) {
295
+ if (!(date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0)) return dateToDateString(date);
296
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
297
+ }
254
298
  //#endregion
255
299
  //#region src/resolution.ts
256
300
  /**
301
+ * Copyright © 2023-2026 Blockchain Commons, LLC
302
+ * Copyright © 2025-2026 Parity Technologies
303
+ *
304
+ */
305
+ /**
257
306
  * Resolution levels for provenance marks.
258
307
  * Higher resolution provides more security but larger mark sizes.
259
308
  */
260
- let ProvenanceMarkResolution = /* @__PURE__ */ function(ProvenanceMarkResolution$1) {
261
- ProvenanceMarkResolution$1[ProvenanceMarkResolution$1["Low"] = 0] = "Low";
262
- ProvenanceMarkResolution$1[ProvenanceMarkResolution$1["Medium"] = 1] = "Medium";
263
- ProvenanceMarkResolution$1[ProvenanceMarkResolution$1["Quartile"] = 2] = "Quartile";
264
- ProvenanceMarkResolution$1[ProvenanceMarkResolution$1["High"] = 3] = "High";
265
- return ProvenanceMarkResolution$1;
309
+ let ProvenanceMarkResolution = /* @__PURE__ */ function(ProvenanceMarkResolution) {
310
+ ProvenanceMarkResolution[ProvenanceMarkResolution["Low"] = 0] = "Low";
311
+ ProvenanceMarkResolution[ProvenanceMarkResolution["Medium"] = 1] = "Medium";
312
+ ProvenanceMarkResolution[ProvenanceMarkResolution["Quartile"] = 2] = "Quartile";
313
+ ProvenanceMarkResolution[ProvenanceMarkResolution["High"] = 3] = "High";
314
+ return ProvenanceMarkResolution;
266
315
  }({});
267
316
  /**
268
317
  * Convert a resolution to its numeric value.
@@ -275,11 +324,11 @@ function resolutionToNumber(res) {
275
324
  */
276
325
  function resolutionFromNumber(value) {
277
326
  switch (value) {
278
- case 0: return ProvenanceMarkResolution.Low;
279
- case 1: return ProvenanceMarkResolution.Medium;
280
- case 2: return ProvenanceMarkResolution.Quartile;
281
- case 3: return ProvenanceMarkResolution.High;
282
- default: throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid provenance mark resolution value: ${value}` });
327
+ case 0: return 0;
328
+ case 1: return 1;
329
+ case 2: return 2;
330
+ case 3: return 3;
331
+ default: throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid provenance mark resolution value: ${value}` });
283
332
  }
284
333
  }
285
334
  /**
@@ -287,10 +336,10 @@ function resolutionFromNumber(value) {
287
336
  */
288
337
  function linkLength(res) {
289
338
  switch (res) {
290
- case ProvenanceMarkResolution.Low: return 4;
291
- case ProvenanceMarkResolution.Medium: return 8;
292
- case ProvenanceMarkResolution.Quartile: return 16;
293
- case ProvenanceMarkResolution.High: return 32;
339
+ case 0: return 4;
340
+ case 1: return 8;
341
+ case 2: return 16;
342
+ case 3: return 32;
294
343
  }
295
344
  }
296
345
  /**
@@ -298,10 +347,10 @@ function linkLength(res) {
298
347
  */
299
348
  function seqBytesLength(res) {
300
349
  switch (res) {
301
- case ProvenanceMarkResolution.Low: return 2;
302
- case ProvenanceMarkResolution.Medium:
303
- case ProvenanceMarkResolution.Quartile:
304
- case ProvenanceMarkResolution.High: return 4;
350
+ case 0: return 2;
351
+ case 1:
352
+ case 2:
353
+ case 3: return 4;
305
354
  }
306
355
  }
307
356
  /**
@@ -309,10 +358,10 @@ function seqBytesLength(res) {
309
358
  */
310
359
  function dateBytesLength(res) {
311
360
  switch (res) {
312
- case ProvenanceMarkResolution.Low: return 2;
313
- case ProvenanceMarkResolution.Medium: return 4;
314
- case ProvenanceMarkResolution.Quartile:
315
- case ProvenanceMarkResolution.High: return 6;
361
+ case 0: return 2;
362
+ case 1: return 4;
363
+ case 2:
364
+ case 3: return 6;
316
365
  }
317
366
  }
318
367
  /**
@@ -380,10 +429,10 @@ function infoRangeStart(res) {
380
429
  */
381
430
  function serializeDate(res, date) {
382
431
  switch (res) {
383
- case ProvenanceMarkResolution.Low: return serialize2Bytes(date);
384
- case ProvenanceMarkResolution.Medium: return serialize4Bytes(date);
385
- case ProvenanceMarkResolution.Quartile:
386
- case ProvenanceMarkResolution.High: return serialize6Bytes(date);
432
+ case 0: return serialize2Bytes(date);
433
+ case 1: return serialize4Bytes(date);
434
+ case 2:
435
+ case 3: return serialize6Bytes(date);
387
436
  }
388
437
  }
389
438
  /**
@@ -391,46 +440,55 @@ function serializeDate(res, date) {
391
440
  */
392
441
  function deserializeDate(res, data) {
393
442
  switch (res) {
394
- case ProvenanceMarkResolution.Low:
395
- if (data.length !== 2) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid date length: expected 2 bytes, got ${data.length}` });
443
+ case 0:
444
+ if (data.length !== 2) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid date length: expected 2 bytes, got ${data.length}` });
396
445
  return deserialize2Bytes(data);
397
- case ProvenanceMarkResolution.Medium:
398
- if (data.length !== 4) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid date length: expected 4 bytes, got ${data.length}` });
446
+ case 1:
447
+ if (data.length !== 4) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid date length: expected 4 bytes, got ${data.length}` });
399
448
  return deserialize4Bytes(data);
400
- case ProvenanceMarkResolution.Quartile:
401
- case ProvenanceMarkResolution.High:
402
- if (data.length !== 6) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid date length: expected 6 bytes, got ${data.length}` });
449
+ case 2:
450
+ case 3:
451
+ if (data.length !== 6) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid date length: expected 6 bytes, got ${data.length}` });
403
452
  return deserialize6Bytes(data);
404
453
  }
405
454
  }
406
455
  /**
407
456
  * Serialize a sequence number into bytes based on the resolution.
457
+ *
458
+ * Mirrors Rust's typed `u32` parameter (`generator.rs::serialize_seq`)
459
+ * — the input must be a non-negative integer in `[0, 2^32-1]` (a u32).
460
+ * For Low resolution the upper bound additionally narrows to `2^16-1`
461
+ * (a u16) per Rust `if seq > 0xFFFF`. Earlier revisions of this port
462
+ * accepted any JS `number` for the 4-byte branch and would silently
463
+ * truncate values above `2^32-1`; now we raise `ResolutionError` so
464
+ * the wire output never deviates from Rust's u32 contract.
408
465
  */
409
466
  function serializeSeq(res, seq) {
467
+ if (!Number.isInteger(seq) || seq < 0) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `sequence number must be a non-negative integer, got ${seq}` });
410
468
  if (seqBytesLength(res) === 2) {
411
- if (seq > 65535) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `sequence number ${seq} out of range for 2-byte format (max 65535)` });
469
+ if (seq > 65535) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `sequence number ${seq} out of range for 2-byte format (max 65535)` });
412
470
  const buf = new Uint8Array(2);
413
471
  buf[0] = seq >> 8 & 255;
414
472
  buf[1] = seq & 255;
415
473
  return buf;
416
- } else {
417
- const buf = new Uint8Array(4);
418
- buf[0] = seq >> 24 & 255;
419
- buf[1] = seq >> 16 & 255;
420
- buf[2] = seq >> 8 & 255;
421
- buf[3] = seq & 255;
422
- return buf;
423
474
  }
475
+ if (seq > 4294967295) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `sequence number ${seq} out of range for 4-byte format (max 4294967295)` });
476
+ const buf = new Uint8Array(4);
477
+ buf[0] = seq >>> 24 & 255;
478
+ buf[1] = seq >>> 16 & 255;
479
+ buf[2] = seq >>> 8 & 255;
480
+ buf[3] = seq & 255;
481
+ return buf;
424
482
  }
425
483
  /**
426
484
  * Deserialize bytes into a sequence number based on the resolution.
427
485
  */
428
486
  function deserializeSeq(res, data) {
429
487
  if (seqBytesLength(res) === 2) {
430
- if (data.length !== 2) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid sequence number length: expected 2 bytes, got ${data.length}` });
488
+ if (data.length !== 2) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid sequence number length: expected 2 bytes, got ${data.length}` });
431
489
  return data[0] << 8 | data[1];
432
490
  } else {
433
- if (data.length !== 4) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ResolutionError, void 0, { details: `invalid sequence number length: expected 4 bytes, got ${data.length}` });
491
+ if (data.length !== 4) throw new ProvenanceMarkError("ResolutionError", void 0, { details: `invalid sequence number length: expected 4 bytes, got ${data.length}` });
434
492
  return (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0;
435
493
  }
436
494
  }
@@ -439,10 +497,10 @@ function deserializeSeq(res, data) {
439
497
  */
440
498
  function resolutionToString(res) {
441
499
  switch (res) {
442
- case ProvenanceMarkResolution.Low: return "low";
443
- case ProvenanceMarkResolution.Medium: return "medium";
444
- case ProvenanceMarkResolution.Quartile: return "quartile";
445
- case ProvenanceMarkResolution.High: return "high";
500
+ case 0: return "low";
501
+ case 1: return "medium";
502
+ case 2: return "quartile";
503
+ case 3: return "high";
446
504
  }
447
505
  }
448
506
  /**
@@ -458,9 +516,13 @@ function resolutionFromCbor(cborValue) {
458
516
  const value = expectUnsigned(cborValue);
459
517
  return resolutionFromNumber(Number(value));
460
518
  }
461
-
462
519
  //#endregion
463
520
  //#region src/crypto-utils.ts
521
+ /**
522
+ * Copyright © 2023-2026 Blockchain Commons, LLC
523
+ * Copyright © 2025-2026 Parity Technologies
524
+ *
525
+ */
464
526
  const SHA256_SIZE = 32;
465
527
  /**
466
528
  * Compute SHA-256 hash of data.
@@ -497,10 +559,181 @@ function obfuscate(key, message) {
497
559
  for (let i = 0; i < 12; i++) iv[i] = extendedKey[31 - i];
498
560
  return chacha20(extendedKey, iv, message instanceof Uint8Array ? message : new Uint8Array(message));
499
561
  }
500
-
562
+ //#endregion
563
+ //#region src/seed.ts
564
+ /**
565
+ * Copyright © 2023-2026 Blockchain Commons, LLC
566
+ * Copyright © 2025-2026 Parity Technologies
567
+ *
568
+ */
569
+ const PROVENANCE_SEED_LENGTH = 32;
570
+ /**
571
+ * A seed for generating provenance marks.
572
+ */
573
+ var ProvenanceSeed = class ProvenanceSeed {
574
+ data;
575
+ constructor(data) {
576
+ this.data = data;
577
+ }
578
+ /**
579
+ * Create a new random seed using secure random number generation.
580
+ */
581
+ static new() {
582
+ const data = randomData(32);
583
+ return ProvenanceSeed.fromBytes(data);
584
+ }
585
+ /**
586
+ * Create a new seed using custom random data.
587
+ */
588
+ static newUsing(randomData) {
589
+ if (randomData.length < 32) throw new ProvenanceMarkError("InvalidSeedLength", void 0, { actual: randomData.length });
590
+ return ProvenanceSeed.fromBytes(randomData.slice(0, 32));
591
+ }
592
+ /**
593
+ * Create a new seed from a passphrase.
594
+ */
595
+ static newWithPassphrase(passphrase) {
596
+ const seedData = extendKey(new TextEncoder().encode(passphrase));
597
+ return ProvenanceSeed.fromBytes(seedData);
598
+ }
599
+ /**
600
+ * Get the raw bytes.
601
+ */
602
+ toBytes() {
603
+ return new Uint8Array(this.data);
604
+ }
605
+ /**
606
+ * Create from a 32-byte array.
607
+ */
608
+ static fromBytes(bytes) {
609
+ if (bytes.length !== 32) throw new ProvenanceMarkError("InvalidSeedLength", void 0, { actual: bytes.length });
610
+ return new ProvenanceSeed(new Uint8Array(bytes));
611
+ }
612
+ /**
613
+ * Create from a slice (validates length).
614
+ */
615
+ static fromSlice(bytes) {
616
+ return ProvenanceSeed.fromBytes(bytes);
617
+ }
618
+ /**
619
+ * Get the hex representation.
620
+ */
621
+ hex() {
622
+ return Array.from(this.data).map((b) => b.toString(16).padStart(2, "0")).join("");
623
+ }
624
+ /**
625
+ * Convert to CBOR (byte string).
626
+ */
627
+ toCbor() {
628
+ return cbor(this.data);
629
+ }
630
+ /**
631
+ * Create from CBOR (byte string).
632
+ */
633
+ static fromCbor(cborValue) {
634
+ const bytes = expectBytes(cborValue);
635
+ return ProvenanceSeed.fromBytes(bytes);
636
+ }
637
+ };
638
+ //#endregion
639
+ //#region src/utils.ts
640
+ /**
641
+ * Copyright © 2023-2026 Blockchain Commons, LLC
642
+ * Copyright © 2025-2026 Parity Technologies
643
+ *
644
+ *
645
+ * Utility functions for byte array conversions.
646
+ *
647
+ * These functions provide cross-platform support for common byte manipulation
648
+ * operations needed in provenance mark encoding.
649
+ */
650
+ /**
651
+ * Convert a Uint8Array to a lowercase hexadecimal string.
652
+ *
653
+ * @param data - The byte array to convert
654
+ * @returns A lowercase hex string representation (2 characters per byte)
655
+ */
656
+ function bytesToHex(data) {
657
+ return Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
658
+ }
659
+ /**
660
+ * Convert a Uint8Array to a base64-encoded string.
661
+ *
662
+ * This function works in both browser and Node.js environments.
663
+ *
664
+ * @param data - The byte array to encode
665
+ * @returns A base64-encoded string
666
+ */
667
+ function toBase64(data) {
668
+ let binary = "";
669
+ for (const byte of data) binary += String.fromCharCode(byte);
670
+ return btoa(binary);
671
+ }
672
+ /**
673
+ * Convert a base64-encoded string to a Uint8Array.
674
+ *
675
+ * This function works in both browser and Node.js environments.
676
+ *
677
+ * @param base64 - A base64-encoded string
678
+ * @returns The decoded byte array
679
+ */
680
+ function fromBase64(base64) {
681
+ const binary = atob(base64);
682
+ const bytes = new Uint8Array(binary.length);
683
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
684
+ return bytes;
685
+ }
686
+ /**
687
+ * Parse a base64-encoded provenance seed.
688
+ *
689
+ * Mirrors Rust's `parse_seed` user helper
690
+ * (`provenance-mark-rust/src/util.rs:34-38`), which round-trips the
691
+ * input through serde JSON / `deserialize_block` and so requires the
692
+ * decoded bytes to be exactly {@link PROVENANCE_SEED_LENGTH} (32) long.
693
+ * The TS equivalent decodes the base64 directly and delegates to
694
+ * {@link ProvenanceSeed.fromBytes} for the length check.
695
+ *
696
+ * @param s - Base64-encoded 32-byte seed string.
697
+ * @returns The decoded {@link ProvenanceSeed}.
698
+ * @throws {ProvenanceMarkError} If the input is not valid base64 or the
699
+ * decoded length is not exactly 32 bytes.
700
+ */
701
+ function parseSeed(s) {
702
+ let bytes;
703
+ try {
704
+ bytes = fromBase64(s);
705
+ } catch (e) {
706
+ throw new ProvenanceMarkError("Base64Error", "invalid base64 encoding for provenance seed", { details: e instanceof Error ? e.message : String(e) });
707
+ }
708
+ return ProvenanceSeed.fromBytes(bytes);
709
+ }
710
+ /**
711
+ * Parse a date string (`YYYY-MM-DD` or full RFC 3339) into a `Date`.
712
+ *
713
+ * Mirrors Rust's `parse_date` user helper
714
+ * (`provenance-mark-rust/src/util.rs:40-42`), which delegates to
715
+ * `Date::from_string` (the same parser the JSON deserializer uses).
716
+ * Accepts the same shapes the Rust parser does:
717
+ *
718
+ * - `YYYY-MM-DD` (interpreted as UTC midnight, matching JS spec).
719
+ * - Full RFC 3339, e.g. `2023-06-20T15:30:45Z` or
720
+ * `2023-06-20T15:30:45.123Z`.
721
+ *
722
+ * @throws {ProvenanceMarkError} If the input fails to parse.
723
+ */
724
+ function parseDate(s) {
725
+ const date = new Date(s);
726
+ if (Number.isNaN(date.getTime())) throw new ProvenanceMarkError("InvalidDate", `cannot parse date: ${s}`);
727
+ return date;
728
+ }
501
729
  //#endregion
502
730
  //#region src/xoshiro256starstar.ts
503
731
  /**
732
+ * Copyright © 2023-2026 Blockchain Commons, LLC
733
+ * Copyright © 2025-2026 Parity Technologies
734
+ *
735
+ */
736
+ /**
504
737
  * Xoshiro256** PRNG implementation.
505
738
  * A fast, high-quality pseudorandom number generator.
506
739
  */
@@ -620,9 +853,13 @@ var Xoshiro256StarStar = class Xoshiro256StarStar {
620
853
  this.s[3] = this.rotateLeft64(this.s[3], 45n);
621
854
  }
622
855
  };
623
-
624
856
  //#endregion
625
857
  //#region src/rng-state.ts
858
+ /**
859
+ * Copyright © 2023-2026 Blockchain Commons, LLC
860
+ * Copyright © 2025-2026 Parity Technologies
861
+ *
862
+ */
626
863
  const RNG_STATE_LENGTH = 32;
627
864
  /**
628
865
  * RNG state for provenance marks (32 bytes).
@@ -642,7 +879,7 @@ var RngState = class RngState {
642
879
  * Create from a 32-byte array.
643
880
  */
644
881
  static fromBytes(bytes) {
645
- if (bytes.length !== RNG_STATE_LENGTH) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidSeedLength, void 0, { actual: bytes.length });
882
+ if (bytes.length !== 32) throw new ProvenanceMarkError("InvalidSeedLength", void 0, { actual: bytes.length });
646
883
  return new RngState(new Uint8Array(bytes));
647
884
  }
648
885
  /**
@@ -671,139 +908,292 @@ var RngState = class RngState {
671
908
  return RngState.fromBytes(bytes);
672
909
  }
673
910
  };
674
-
675
911
  //#endregion
676
- //#region src/seed.ts
677
- const PROVENANCE_SEED_LENGTH = 32;
912
+ //#region src/validate.ts
678
913
  /**
679
- * A seed for generating provenance marks.
914
+ * Format for validation report output.
680
915
  */
681
- var ProvenanceSeed = class ProvenanceSeed {
682
- data;
683
- constructor(data) {
684
- this.data = data;
685
- }
686
- /**
687
- * Create a new random seed using secure random number generation.
688
- */
689
- static new() {
690
- const data = randomData(PROVENANCE_SEED_LENGTH);
691
- return ProvenanceSeed.fromBytes(data);
692
- }
693
- /**
694
- * Create a new seed using custom random data.
695
- */
696
- static newUsing(randomData$1) {
697
- if (randomData$1.length < PROVENANCE_SEED_LENGTH) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidSeedLength, void 0, { actual: randomData$1.length });
698
- return ProvenanceSeed.fromBytes(randomData$1.slice(0, PROVENANCE_SEED_LENGTH));
699
- }
700
- /**
701
- * Create a new seed from a passphrase.
702
- */
703
- static newWithPassphrase(passphrase) {
704
- const seedData = extendKey(new TextEncoder().encode(passphrase));
705
- return ProvenanceSeed.fromBytes(seedData);
706
- }
707
- /**
708
- * Get the raw bytes.
709
- */
710
- toBytes() {
711
- return new Uint8Array(this.data);
712
- }
713
- /**
714
- * Create from a 32-byte array.
715
- */
716
- static fromBytes(bytes) {
717
- if (bytes.length !== PROVENANCE_SEED_LENGTH) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidSeedLength, void 0, { actual: bytes.length });
718
- return new ProvenanceSeed(new Uint8Array(bytes));
719
- }
720
- /**
721
- * Create from a slice (validates length).
722
- */
723
- static fromSlice(bytes) {
724
- return ProvenanceSeed.fromBytes(bytes);
725
- }
726
- /**
727
- * Get the hex representation.
728
- */
729
- hex() {
730
- return Array.from(this.data).map((b) => b.toString(16).padStart(2, "0")).join("");
731
- }
732
- /**
733
- * Convert to CBOR (byte string).
734
- */
735
- toCbor() {
736
- return cbor(this.data);
737
- }
738
- /**
739
- * Create from CBOR (byte string).
740
- */
741
- static fromCbor(cborValue) {
742
- const bytes = expectBytes(cborValue);
743
- return ProvenanceSeed.fromBytes(bytes);
744
- }
745
- };
746
-
747
- //#endregion
748
- //#region src/utils.ts
916
+ let ValidationReportFormat = /* @__PURE__ */ function(ValidationReportFormat) {
917
+ /** Human-readable text format */
918
+ ValidationReportFormat["Text"] = "text";
919
+ /** Compact JSON format (no whitespace) */
920
+ ValidationReportFormat["JsonCompact"] = "json-compact";
921
+ /** Pretty-printed JSON format (with indentation) */
922
+ ValidationReportFormat["JsonPretty"] = "json-pretty";
923
+ return ValidationReportFormat;
924
+ }({});
749
925
  /**
750
- * Convert a Uint8Array to a lowercase hexadecimal string.
751
- *
752
- * @param data - The byte array to convert
753
- * @returns A lowercase hex string representation (2 characters per byte)
926
+ * Format a validation issue as a string.
754
927
  */
755
- function bytesToHex(data) {
756
- return Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
928
+ function formatValidationIssue(issue) {
929
+ switch (issue.type) {
930
+ case "HashMismatch": return `hash mismatch: expected ${issue.expected}, got ${issue.actual}`;
931
+ case "KeyMismatch": return "key mismatch: current hash was not generated from next key";
932
+ case "SequenceGap": return `sequence number gap: expected ${issue.expected}, got ${issue.actual}`;
933
+ case "DateOrdering": return `date must be equal or later: previous is ${issue.previous}, next is ${issue.next}`;
934
+ case "NonGenesisAtZero": return "non-genesis mark at sequence 0";
935
+ case "InvalidGenesisKey": return "genesis mark must have key equal to chain_id";
936
+ }
757
937
  }
758
938
  /**
759
- * Convert a Uint8Array to a base64-encoded string.
760
- *
761
- * This function works in both browser and Node.js environments.
762
- *
763
- * @param data - The byte array to encode
764
- * @returns A base64-encoded string
939
+ * Get the chain ID as a hex string for display.
765
940
  */
766
- function toBase64(data) {
767
- const globalBtoa = globalThis.btoa;
768
- if (typeof globalBtoa === "function") {
769
- let binary = "";
770
- for (const byte of data) binary += String.fromCharCode(byte);
771
- return globalBtoa(binary);
772
- }
773
- const requireFn = __require;
774
- if (typeof requireFn === "function") {
775
- const { Buffer: NodeBuffer } = requireFn("buffer");
776
- return NodeBuffer.from(data).toString("base64");
777
- }
778
- throw new Error("btoa not available and require is not defined");
941
+ function chainIdHex(report) {
942
+ return hexEncode(report.chainId);
779
943
  }
780
944
  /**
781
- * Convert a base64-encoded string to a Uint8Array.
782
- *
783
- * This function works in both browser and Node.js environments.
784
- *
785
- * @param base64 - A base64-encoded string
786
- * @returns The decoded byte array
945
+ * Check if the validation report has any issues.
787
946
  */
788
- function fromBase64(base64) {
789
- const globalAtob = globalThis.atob;
790
- if (typeof globalAtob === "function") {
791
- const binary = globalAtob(base64);
792
- const bytes = new Uint8Array(binary.length);
793
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
794
- return bytes;
947
+ function hasIssues(report) {
948
+ for (const chain of report.chains) if (!chain.hasGenesis) return true;
949
+ for (const chain of report.chains) for (const seq of chain.sequences) for (const mark of seq.marks) if (mark.issues.length > 0) return true;
950
+ if (report.chains.length > 1) return true;
951
+ if (report.chains.length === 1 && report.chains[0].sequences.length > 1) return true;
952
+ return false;
953
+ }
954
+ /**
955
+ * Check if the validation report contains interesting information.
956
+ */
957
+ function isInteresting(report) {
958
+ if (report.chains.length === 0) return false;
959
+ for (const chain of report.chains) if (!chain.hasGenesis) return true;
960
+ if (report.chains.length === 1) {
961
+ const chain = report.chains[0];
962
+ if (chain.sequences.length === 1) {
963
+ if (chain.sequences[0].marks.every((m) => m.issues.length === 0)) return false;
964
+ }
965
+ }
966
+ return true;
967
+ }
968
+ /**
969
+ * Format the validation report as human-readable text.
970
+ */
971
+ function formatText(report) {
972
+ if (!isInteresting(report)) return "";
973
+ const lines = [];
974
+ lines.push(`Total marks: ${report.marks.length}`);
975
+ lines.push(`Chains: ${report.chains.length}`);
976
+ lines.push("");
977
+ for (let chainIdx = 0; chainIdx < report.chains.length; chainIdx++) {
978
+ const chain = report.chains[chainIdx];
979
+ const chainIdStr = chainIdHex(chain);
980
+ const shortChainId = chainIdStr.length > 8 ? chainIdStr.slice(0, 8) : chainIdStr;
981
+ lines.push(`Chain ${chainIdx + 1}: ${shortChainId}`);
982
+ if (!chain.hasGenesis) lines.push(" Warning: No genesis mark found");
983
+ for (const seq of chain.sequences) for (const flaggedMark of seq.marks) {
984
+ const mark = flaggedMark.mark;
985
+ const shortId = mark.idHex().slice(0, 8);
986
+ const seqNum = mark.seq();
987
+ const annotations = [];
988
+ if (mark.isGenesis()) annotations.push("genesis mark");
989
+ for (const issue of flaggedMark.issues) {
990
+ let issueStr;
991
+ switch (issue.type) {
992
+ case "SequenceGap":
993
+ issueStr = `gap: ${issue.expected} missing`;
994
+ break;
995
+ case "DateOrdering":
996
+ issueStr = `date ${issue.previous} < ${issue.next}`;
997
+ break;
998
+ case "HashMismatch":
999
+ issueStr = "hash mismatch";
1000
+ break;
1001
+ case "KeyMismatch":
1002
+ issueStr = "key mismatch";
1003
+ break;
1004
+ case "NonGenesisAtZero":
1005
+ issueStr = "non-genesis at seq 0";
1006
+ break;
1007
+ case "InvalidGenesisKey":
1008
+ issueStr = "invalid genesis key";
1009
+ break;
1010
+ }
1011
+ annotations.push(issueStr);
1012
+ }
1013
+ if (annotations.length === 0) lines.push(` ${seqNum}: ${shortId}`);
1014
+ else lines.push(` ${seqNum}: ${shortId} (${annotations.join(", ")})`);
1015
+ }
1016
+ lines.push("");
795
1017
  }
796
- const requireFn = __require;
797
- if (typeof requireFn === "function") {
798
- const { Buffer: NodeBuffer } = requireFn("buffer");
799
- return new Uint8Array(NodeBuffer.from(base64, "base64"));
1018
+ return lines.join("\n").trimEnd();
1019
+ }
1020
+ /**
1021
+ * Format the validation report.
1022
+ */
1023
+ function formatReport(report, format) {
1024
+ switch (format) {
1025
+ case "text": return formatText(report);
1026
+ case "json-compact": return JSON.stringify(reportToJSON(report));
1027
+ case "json-pretty": return JSON.stringify(reportToJSON(report), null, 2);
800
1028
  }
801
- throw new Error("atob not available and require is not defined");
802
1029
  }
803
-
1030
+ /**
1031
+ * Convert a report to a JSON-serializable object.
1032
+ */
1033
+ function reportToJSON(report) {
1034
+ return {
1035
+ marks: report.marks.map((m) => m.urString()),
1036
+ chains: report.chains.map((chain) => ({
1037
+ chain_id: hexEncode(chain.chainId),
1038
+ has_genesis: chain.hasGenesis,
1039
+ marks: chain.marks.map((m) => m.urString()),
1040
+ sequences: chain.sequences.map((seq) => ({
1041
+ start_seq: seq.startSeq,
1042
+ end_seq: seq.endSeq,
1043
+ marks: seq.marks.map((fm) => ({
1044
+ mark: fm.mark.urString(),
1045
+ issues: fm.issues.map(issueToJSON)
1046
+ }))
1047
+ }))
1048
+ }))
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Convert a ValidationIssue to JSON matching Rust's serde format.
1053
+ *
1054
+ * Rust uses `#[serde(tag = "type", content = "data")]` which wraps
1055
+ * struct variant data in a `"data"` field. Unit variants have no
1056
+ * `"data"` field.
1057
+ */
1058
+ function issueToJSON(issue) {
1059
+ switch (issue.type) {
1060
+ case "HashMismatch": return {
1061
+ type: "HashMismatch",
1062
+ data: {
1063
+ expected: issue.expected,
1064
+ actual: issue.actual
1065
+ }
1066
+ };
1067
+ case "SequenceGap": return {
1068
+ type: "SequenceGap",
1069
+ data: {
1070
+ expected: issue.expected,
1071
+ actual: issue.actual
1072
+ }
1073
+ };
1074
+ case "DateOrdering": return {
1075
+ type: "DateOrdering",
1076
+ data: {
1077
+ previous: issue.previous,
1078
+ next: issue.next
1079
+ }
1080
+ };
1081
+ case "KeyMismatch": return { type: "KeyMismatch" };
1082
+ case "NonGenesisAtZero": return { type: "NonGenesisAtZero" };
1083
+ case "InvalidGenesisKey": return { type: "InvalidGenesisKey" };
1084
+ }
1085
+ }
1086
+ /**
1087
+ * Build sequence bins for a chain.
1088
+ */
1089
+ function buildSequenceBins(marks) {
1090
+ const sequences = [];
1091
+ let currentSequence = [];
1092
+ for (let i = 0; i < marks.length; i++) {
1093
+ const mark = marks[i];
1094
+ if (i === 0) currentSequence.push({
1095
+ mark,
1096
+ issues: []
1097
+ });
1098
+ else {
1099
+ const prev = marks[i - 1];
1100
+ try {
1101
+ prev.precedesOpt(mark);
1102
+ currentSequence.push({
1103
+ mark,
1104
+ issues: []
1105
+ });
1106
+ } catch (e) {
1107
+ if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1108
+ let issue;
1109
+ if (e instanceof ProvenanceMarkError && e.details?.["validationIssue"] !== void 0) issue = e.details["validationIssue"];
1110
+ else issue = { type: "KeyMismatch" };
1111
+ currentSequence = [{
1112
+ mark,
1113
+ issues: [issue]
1114
+ }];
1115
+ }
1116
+ }
1117
+ }
1118
+ if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1119
+ return sequences;
1120
+ }
1121
+ /**
1122
+ * Create a sequence report from flagged marks.
1123
+ */
1124
+ function createSequenceReport(marks) {
1125
+ return {
1126
+ startSeq: marks.length > 0 ? marks[0].mark.seq() : 0,
1127
+ endSeq: marks.length > 0 ? marks[marks.length - 1].mark.seq() : 0,
1128
+ marks
1129
+ };
1130
+ }
1131
+ /**
1132
+ * Validate a collection of provenance marks.
1133
+ */
1134
+ function validate(marks) {
1135
+ const seen = /* @__PURE__ */ new Set();
1136
+ const deduplicatedMarks = [];
1137
+ for (const mark of marks) {
1138
+ const key = `${mark.res()}:${hexEncode(mark.message())}`;
1139
+ if (!seen.has(key)) {
1140
+ seen.add(key);
1141
+ deduplicatedMarks.push(mark);
1142
+ }
1143
+ }
1144
+ const chainBins = /* @__PURE__ */ new Map();
1145
+ for (const mark of deduplicatedMarks) {
1146
+ const chainIdKey = hexEncode(mark.chainId());
1147
+ const bin = chainBins.get(chainIdKey);
1148
+ if (bin !== void 0) bin.push(mark);
1149
+ else chainBins.set(chainIdKey, [mark]);
1150
+ }
1151
+ const chains = [];
1152
+ for (const [chainIdKey, chainMarks] of chainBins) {
1153
+ chainMarks.sort((a, b) => a.seq() - b.seq());
1154
+ const hasGenesis = chainMarks.length > 0 && chainMarks[0].seq() === 0 && chainMarks[0].isGenesis();
1155
+ const sequences = buildSequenceBins(chainMarks);
1156
+ chains.push({
1157
+ chainId: hexDecode(chainIdKey),
1158
+ hasGenesis,
1159
+ marks: chainMarks,
1160
+ sequences
1161
+ });
1162
+ }
1163
+ chains.sort((a, b) => {
1164
+ const aHex = hexEncode(a.chainId);
1165
+ const bHex = hexEncode(b.chainId);
1166
+ if (aHex < bHex) return -1;
1167
+ if (aHex > bHex) return 1;
1168
+ return 0;
1169
+ });
1170
+ return {
1171
+ marks: deduplicatedMarks,
1172
+ chains
1173
+ };
1174
+ }
1175
+ /**
1176
+ * Helper function to encode bytes as hex.
1177
+ */
1178
+ function hexEncode(bytes) {
1179
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1180
+ }
1181
+ /**
1182
+ * Helper function to decode hex to bytes.
1183
+ */
1184
+ function hexDecode(hex) {
1185
+ const bytes = new Uint8Array(hex.length / 2);
1186
+ for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
1187
+ return bytes;
1188
+ }
804
1189
  //#endregion
805
1190
  //#region src/mark.ts
806
1191
  /**
1192
+ * Copyright © 2023-2026 Blockchain Commons, LLC
1193
+ * Copyright © 2025-2026 Parity Technologies
1194
+ *
1195
+ */
1196
+ /**
807
1197
  * A cryptographically-secured provenance mark.
808
1198
  */
809
1199
  var ProvenanceMark = class ProvenanceMark {
@@ -877,15 +1267,15 @@ var ProvenanceMark = class ProvenanceMark {
877
1267
  */
878
1268
  static new(res, key, nextKey, chainId, seq, date, info) {
879
1269
  const linkLen = linkLength(res);
880
- if (key.length !== linkLen) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidKeyLength, void 0, {
1270
+ if (key.length !== linkLen) throw new ProvenanceMarkError("InvalidKeyLength", void 0, {
881
1271
  expected: linkLen,
882
1272
  actual: key.length
883
1273
  });
884
- if (nextKey.length !== linkLen) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidNextKeyLength, void 0, {
1274
+ if (nextKey.length !== linkLen) throw new ProvenanceMarkError("InvalidNextKeyLength", void 0, {
885
1275
  expected: linkLen,
886
1276
  actual: nextKey.length
887
1277
  });
888
- if (chainId.length !== linkLen) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidChainIdLength, void 0, {
1278
+ if (chainId.length !== linkLen) throw new ProvenanceMarkError("InvalidChainIdLength", void 0, {
889
1279
  expected: linkLen,
890
1280
  actual: chainId.length
891
1281
  });
@@ -901,7 +1291,7 @@ var ProvenanceMark = class ProvenanceMark {
901
1291
  */
902
1292
  static fromMessage(res, message) {
903
1293
  const minLen = fixedLength(res);
904
- if (message.length < minLen) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidMessageLength, void 0, {
1294
+ if (message.length < minLen) throw new ProvenanceMarkError("InvalidMessageLength", void 0, {
905
1295
  expected: minLen,
906
1296
  actual: message.length
907
1297
  });
@@ -924,7 +1314,7 @@ var ProvenanceMark = class ProvenanceMark {
924
1314
  if (infoBytes.length > 0) try {
925
1315
  decodeCbor(infoBytes);
926
1316
  } catch {
927
- throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidInfoCbor);
1317
+ throw new ProvenanceMarkError("InvalidInfoCbor");
928
1318
  }
929
1319
  return new ProvenanceMark(res, new Uint8Array(key), new Uint8Array(hash), new Uint8Array(chainId), new Uint8Array(seqBytes), new Uint8Array(dateBytes), new Uint8Array(infoBytes), seq, date);
930
1320
  }
@@ -939,24 +1329,174 @@ var ProvenanceMark = class ProvenanceMark {
939
1329
  ]), linkLength(res));
940
1330
  }
941
1331
  /**
942
- * Get the first four bytes of the hash as a hex string identifier.
1332
+ * The 32-byte Mark ID.
1333
+ *
1334
+ * The first `linkLength` bytes are the mark's stored hash. The remaining
1335
+ * bytes come from the mark's fingerprint (SHA-256 of CBOR encoding),
1336
+ * ensuring a full 32-byte value is always available regardless of
1337
+ * resolution.
1338
+ */
1339
+ id() {
1340
+ const result = new Uint8Array(32);
1341
+ const n = this._hash.length;
1342
+ result.set(this._hash, 0);
1343
+ if (n < 32) {
1344
+ const fp = this.fingerprint();
1345
+ result.set(fp.subarray(0, 32 - n), n);
1346
+ }
1347
+ return result;
1348
+ }
1349
+ /**
1350
+ * The full 32-byte Mark ID as a 64-character hex string.
1351
+ */
1352
+ idHex() {
1353
+ return bytesToHex(this.id());
1354
+ }
1355
+ /**
1356
+ * The first `wordCount` bytes of the Mark ID as upper-case ByteWords.
1357
+ *
1358
+ * @param wordCount Number of bytes to encode, must be in `4..=32`.
1359
+ * @param prefix If `true`, prepends the provenance-mark prefix character.
1360
+ * @throws if `wordCount` is not in the range `4..=32`.
1361
+ */
1362
+ idBytewords(wordCount, prefix) {
1363
+ if (!Number.isInteger(wordCount) || wordCount < 4 || wordCount > 32) throw new Error(`word_count must be 4..=32, got ${wordCount}`);
1364
+ const s = encodeToWords(this.id().subarray(0, wordCount)).toUpperCase();
1365
+ return prefix ? `\u{1F15F} ${s}` : s;
1366
+ }
1367
+ /**
1368
+ * The first `wordCount` bytes of the Mark ID as Bytemoji.
1369
+ *
1370
+ * @param wordCount Number of bytes to encode, must be in `4..=32`.
1371
+ * @param prefix If `true`, prepends the provenance-mark prefix character.
1372
+ * @throws if `wordCount` is not in the range `4..=32`.
1373
+ */
1374
+ idBytemoji(wordCount, prefix) {
1375
+ if (!Number.isInteger(wordCount) || wordCount < 4 || wordCount > 32) throw new Error(`word_count must be 4..=32, got ${wordCount}`);
1376
+ const s = encodeToBytemojis(this.id().subarray(0, wordCount)).toUpperCase();
1377
+ return prefix ? `\u{1F15F} ${s}` : s;
1378
+ }
1379
+ /**
1380
+ * The first `wordCount` bytes of the Mark ID as upper-case minimal
1381
+ * ByteWords (2 letters per byte, concatenated without separator).
1382
+ *
1383
+ * @param wordCount Number of bytes to encode, must be in `4..=32`.
1384
+ * @param prefix If `true`, prepends the provenance-mark prefix character.
1385
+ * @throws if `wordCount` is not in the range `4..=32`.
1386
+ */
1387
+ idBytewordsMinimal(wordCount, prefix) {
1388
+ if (!Number.isInteger(wordCount) || wordCount < 4 || wordCount > 32) throw new Error(`word_count must be 4..=32, got ${wordCount}`);
1389
+ const s = encodeToMinimalBytewords(this.id().subarray(0, wordCount)).toUpperCase();
1390
+ return prefix ? `\u{1F15F} ${s}` : s;
1391
+ }
1392
+ /**
1393
+ * Legacy 8-character hex identifier — the first 4 bytes of the Mark ID.
1394
+ *
1395
+ * @deprecated Use {@link idHex} for the full 64-char hex, or
1396
+ * `idHex().slice(0, 8)` for this legacy short form. Retained for
1397
+ * backwards compatibility; will be removed in a future alpha.
943
1398
  */
944
1399
  identifier() {
945
- return Array.from(this._hash.slice(0, 4)).map((b) => b.toString(16).padStart(2, "0")).join("");
1400
+ return this.idHex().slice(0, 8);
946
1401
  }
947
1402
  /**
948
- * Get the first four bytes of the hash as upper-case ByteWords.
1403
+ * Legacy 4-byte upper-case ByteWords identifier.
1404
+ *
1405
+ * @deprecated Equivalent to `idBytewords(4, prefix)`. Retained for
1406
+ * backwards compatibility; will be removed in a future alpha.
949
1407
  */
950
1408
  bytewordsIdentifier(prefix) {
951
- const s = encodeBytewordsIdentifier(this._hash.slice(0, 4)).toUpperCase();
952
- return prefix ? `\u{1F151} ${s}` : s;
1409
+ return this.idBytewords(4, prefix);
953
1410
  }
954
1411
  /**
955
- * Get the first four bytes of the hash as Bytemoji.
1412
+ * Legacy 8-letter minimal ByteWords identifier (first+last letter of each
1413
+ * of the 4 ByteWords). Example: "ABLE ACID ALSO APEX" -> "AEADAOAX".
1414
+ *
1415
+ * @deprecated Equivalent to `idBytewordsMinimal(4, prefix)`. Retained
1416
+ * for backwards compatibility; will be removed in a future alpha.
1417
+ */
1418
+ bytewordsMinimalIdentifier(prefix) {
1419
+ return this.idBytewordsMinimal(4, prefix);
1420
+ }
1421
+ /**
1422
+ * Legacy 4-byte upper-case Bytemoji identifier.
1423
+ *
1424
+ * @deprecated Equivalent to `idBytemoji(4, prefix)`. Retained for
1425
+ * backwards compatibility; will be removed in a future alpha.
956
1426
  */
957
1427
  bytemojiIdentifier(prefix) {
958
- const s = encodeBytemojisIdentifier(this._hash.slice(0, 4)).toUpperCase();
959
- return prefix ? `\u{1F151} ${s}` : s;
1428
+ return this.idBytemoji(4, prefix);
1429
+ }
1430
+ /**
1431
+ * Computes the minimum prefix length (in bytes, `4..=32`) each mark needs
1432
+ * so that every mark in the set has a unique Mark ID prefix.
1433
+ *
1434
+ * Non-colliding marks get the minimum of 4. Only marks whose 4-byte
1435
+ * prefixes collide are extended.
1436
+ */
1437
+ static minimalNoncollidingPrefixLengths(ids) {
1438
+ const n = ids.length;
1439
+ const lengths = new Array(n).fill(4);
1440
+ const groups = /* @__PURE__ */ new Map();
1441
+ for (let i = 0; i < n; i++) {
1442
+ const key = bytesToHex(ids[i].subarray(0, 4));
1443
+ const g = groups.get(key);
1444
+ if (g !== void 0) g.push(i);
1445
+ else groups.set(key, [i]);
1446
+ }
1447
+ for (const indices of groups.values()) {
1448
+ if (indices.length <= 1) continue;
1449
+ ProvenanceMark.resolveCollisionGroup(ids, indices, lengths);
1450
+ }
1451
+ return lengths;
1452
+ }
1453
+ static resolveCollisionGroup(ids, initialIndices, lengths) {
1454
+ let unresolved = [...initialIndices];
1455
+ for (let prefixLen = 5; prefixLen <= 32; prefixLen++) {
1456
+ const subGroups = /* @__PURE__ */ new Map();
1457
+ for (const i of unresolved) {
1458
+ const key = bytesToHex(ids[i].subarray(0, prefixLen));
1459
+ const g = subGroups.get(key);
1460
+ if (g !== void 0) g.push(i);
1461
+ else subGroups.set(key, [i]);
1462
+ }
1463
+ const nextUnresolved = [];
1464
+ for (const subIndices of subGroups.values()) if (subIndices.length === 1) lengths[subIndices[0]] = prefixLen;
1465
+ else nextUnresolved.push(...subIndices);
1466
+ if (nextUnresolved.length === 0) return;
1467
+ unresolved = nextUnresolved;
1468
+ }
1469
+ for (const i of unresolved) lengths[i] = 32;
1470
+ }
1471
+ /**
1472
+ * Returns disambiguated upper-case ByteWords Mark IDs for a set of marks.
1473
+ *
1474
+ * Non-colliding marks get 4-word identifiers. Only marks whose 4-byte
1475
+ * prefixes collide are extended with additional words (up to 32 bytes
1476
+ * per identifier).
1477
+ */
1478
+ static disambiguatedIdBytewords(marks, prefix) {
1479
+ const ids = marks.map((m) => m.id());
1480
+ const lengths = ProvenanceMark.minimalNoncollidingPrefixLengths(ids);
1481
+ return ids.map((id, i) => {
1482
+ const s = encodeToWords(id.subarray(0, lengths[i])).toUpperCase();
1483
+ return prefix ? `\u{1F15F} ${s}` : s;
1484
+ });
1485
+ }
1486
+ /**
1487
+ * Returns disambiguated Bytemoji Mark IDs for a set of marks.
1488
+ *
1489
+ * Non-colliding marks get 4-emoji identifiers. Only marks whose 4-byte
1490
+ * prefixes collide are extended with additional emojis (up to 32 bytes
1491
+ * per identifier).
1492
+ */
1493
+ static disambiguatedIdBytemoji(marks, prefix) {
1494
+ const ids = marks.map((m) => m.id());
1495
+ const lengths = ProvenanceMark.minimalNoncollidingPrefixLengths(ids);
1496
+ return ids.map((id, i) => {
1497
+ const s = encodeToBytemojis(id.subarray(0, lengths[i])).toUpperCase();
1498
+ return prefix ? `\u{1F15F} ${s}` : s;
1499
+ });
960
1500
  }
961
1501
  /**
962
1502
  * Check if this mark precedes another mark in the chain.
@@ -971,18 +1511,39 @@ var ProvenanceMark = class ProvenanceMark {
971
1511
  }
972
1512
  /**
973
1513
  * Check if this mark precedes another mark, throwing on validation errors.
1514
+ * Errors carry a structured `validationIssue` in their details, matching Rust's
1515
+ * `Error::Validation(ValidationIssue)` pattern.
974
1516
  */
975
1517
  precedesOpt(next) {
976
- if (next._seq === 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: "non-genesis mark at sequence 0" });
977
- if (arraysEqual(next._key, next._chainId)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: "genesis mark must have key equal to chain_id" });
978
- if (this._seq !== next._seq - 1) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: `sequence gap: expected ${this._seq + 1}, got ${next._seq}` });
979
- if (this._date > next._date) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: `date ordering: ${this._date.toISOString()} > ${next._date.toISOString()}` });
1518
+ if (next._seq === 0) throw new ProvenanceMarkError("ValidationError", "non-genesis mark at sequence 0", { validationIssue: { type: "NonGenesisAtZero" } });
1519
+ if (arraysEqual(next._key, next._chainId)) throw new ProvenanceMarkError("ValidationError", "genesis mark must have key equal to chain_id", { validationIssue: { type: "InvalidGenesisKey" } });
1520
+ if (this._seq !== next._seq - 1) {
1521
+ const issue = {
1522
+ type: "SequenceGap",
1523
+ expected: this._seq + 1,
1524
+ actual: next._seq
1525
+ };
1526
+ throw new ProvenanceMarkError("ValidationError", `sequence gap: expected ${this._seq + 1}, got ${next._seq}`, { validationIssue: issue });
1527
+ }
1528
+ if (this._date > next._date) {
1529
+ const dateStr = dateToDisplay(this._date);
1530
+ const nextDateStr = dateToDisplay(next._date);
1531
+ const issue = {
1532
+ type: "DateOrdering",
1533
+ previous: dateStr,
1534
+ next: nextDateStr
1535
+ };
1536
+ throw new ProvenanceMarkError("ValidationError", `date ordering: ${dateStr} > ${nextDateStr}`, { validationIssue: issue });
1537
+ }
980
1538
  const expectedHash = ProvenanceMark.makeHash(this._res, this._key, next._key, this._chainId, this._seqBytes, this._dateBytes, this._infoBytes);
981
- if (!arraysEqual(this._hash, expectedHash)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, {
982
- message: "hash mismatch",
983
- expected: Array.from(expectedHash).map((b) => b.toString(16).padStart(2, "0")).join(""),
984
- actual: Array.from(this._hash).map((b) => b.toString(16).padStart(2, "0")).join("")
985
- });
1539
+ if (!arraysEqual(this._hash, expectedHash)) {
1540
+ const issue = {
1541
+ type: "HashMismatch",
1542
+ expected: bytesToHex(expectedHash),
1543
+ actual: bytesToHex(this._hash)
1544
+ };
1545
+ throw new ProvenanceMarkError("ValidationError", `hash mismatch: expected ${bytesToHex(expectedHash)}, got ${bytesToHex(this._hash)}`, { validationIssue: issue });
1546
+ }
986
1547
  }
987
1548
  /**
988
1549
  * Check if a sequence of marks is valid.
@@ -1019,7 +1580,7 @@ var ProvenanceMark = class ProvenanceMark {
1019
1580
  return ProvenanceMark.fromMessage(res, message);
1020
1581
  }
1021
1582
  /**
1022
- * Encode for URL (minimal bytewords of CBOR).
1583
+ * Encode for URL (minimal bytewords of tagged CBOR).
1023
1584
  */
1024
1585
  toUrlEncoding() {
1025
1586
  return encodeBytewords(this.toCborData(), BytewordsStyle.Minimal);
@@ -1032,6 +1593,32 @@ var ProvenanceMark = class ProvenanceMark {
1032
1593
  return ProvenanceMark.fromTaggedCbor(cborValue);
1033
1594
  }
1034
1595
  /**
1596
+ * Returns the {@link UR} representation of this mark (untagged CBOR
1597
+ * with type `"provenance"`).
1598
+ *
1599
+ * Mirrors Rust `UREncodable::ur()` for `ProvenanceMark` — the
1600
+ * blanket impl on `CBORTaggedEncodable` produces a UR whose
1601
+ * payload is the *untagged* CBOR (the type name itself stands in
1602
+ * for the tag). See `bc-ur-rust/src/ur_encodable.rs:8-18`.
1603
+ */
1604
+ ur() {
1605
+ return UR.new("provenance", this.untaggedCbor());
1606
+ }
1607
+ /**
1608
+ * Get the UR string representation (e.g., "ur:provenance/...").
1609
+ */
1610
+ urString() {
1611
+ return this.ur().string();
1612
+ }
1613
+ /**
1614
+ * Create from a UR string.
1615
+ */
1616
+ static fromURString(urString) {
1617
+ const ur = UR.fromURString(urString);
1618
+ if (ur.urTypeStr() !== "provenance") throw new ProvenanceMarkError("CborError", void 0, { message: `Expected UR type 'provenance', got '${ur.urTypeStr()}'` });
1619
+ return ProvenanceMark.fromUntaggedCbor(ur.cbor());
1620
+ }
1621
+ /**
1035
1622
  * Build a URL with this mark as a query parameter.
1036
1623
  */
1037
1624
  toUrl(base) {
@@ -1044,7 +1631,7 @@ var ProvenanceMark = class ProvenanceMark {
1044
1631
  */
1045
1632
  static fromUrl(url) {
1046
1633
  const param = url.searchParams.get("provenance");
1047
- if (param === null || param === "") throw new ProvenanceMarkError(ProvenanceMarkErrorType.MissingUrlParameter, void 0, { parameter: "provenance" });
1634
+ if (param === null || param === "") throw new ProvenanceMarkError("MissingUrlParameter", void 0, { parameter: "provenance" });
1048
1635
  return ProvenanceMark.fromUrlEncoding(param);
1049
1636
  }
1050
1637
  /**
@@ -1073,7 +1660,7 @@ var ProvenanceMark = class ProvenanceMark {
1073
1660
  */
1074
1661
  static fromUntaggedCbor(cborValue) {
1075
1662
  const arr = expectArray(cborValue);
1076
- if (arr.length !== 2) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Invalid provenance mark length" });
1663
+ if (arr.length !== 2) throw new ProvenanceMarkError("CborError", void 0, { message: "Invalid provenance mark length" });
1077
1664
  const res = resolutionFromCbor(arr[0]);
1078
1665
  const message = expectBytes(arr[1]);
1079
1666
  return ProvenanceMark.fromMessage(res, message);
@@ -1083,8 +1670,8 @@ var ProvenanceMark = class ProvenanceMark {
1083
1670
  */
1084
1671
  static fromTaggedCbor(cborValue) {
1085
1672
  const cborObj = cborValue;
1086
- if (cborObj.tag !== PROVENANCE_MARK.value) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: `Expected tag ${PROVENANCE_MARK.value}, got ${String(cborObj.tag)}` });
1087
- if (cborObj.value === void 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Tagged CBOR value is missing" });
1673
+ if (cborObj.tag !== PROVENANCE_MARK.value) throw new ProvenanceMarkError("CborError", void 0, { message: `Expected tag ${PROVENANCE_MARK.value}, got ${String(cborObj.tag)}` });
1674
+ if (cborObj.value === void 0) throw new ProvenanceMarkError("CborError", void 0, { message: "Tagged CBOR value is missing" });
1088
1675
  return ProvenanceMark.fromUntaggedCbor(cborObj.value);
1089
1676
  }
1090
1677
  /**
@@ -1102,23 +1689,42 @@ var ProvenanceMark = class ProvenanceMark {
1102
1689
  }
1103
1690
  /**
1104
1691
  * Debug string representation.
1692
+ *
1693
+ * As of provenance-mark v0.24, this includes the full 64-character Mark ID
1694
+ * hex (matching rust's `Display` impl). Pre-v0.24 callers that depended on
1695
+ * the 8-character prefix should use `idHex().slice(0, 8)` directly.
1105
1696
  */
1106
1697
  toString() {
1107
- return `ProvenanceMark(${this.identifier()})`;
1698
+ return `ProvenanceMark(${this.idHex()})`;
1108
1699
  }
1109
1700
  /**
1110
1701
  * Detailed debug representation.
1702
+ *
1703
+ * Mirrors Rust `Mark::Debug` exactly: every field is rendered
1704
+ * Rust-style (hex bytes for keys/hashes/IDs, plain integer for
1705
+ * `seq`, `Date::Display` for the date). The Low-resolution test
1706
+ * vector in Rust `tests/mark.rs::test_low_resolution` ends with
1707
+ * `date: 2023-06-20` — i.e. midnight-UTC dates are rendered without
1708
+ * a time suffix. We use {@link dateToDisplay} to mirror that
1709
+ * exactly; earlier revisions of this port stripped just the
1710
+ * `.000Z` fractional component, which left `2023-06-20T00:00:00Z`
1711
+ * and broke the Low-resolution debug-string parity.
1111
1712
  */
1112
1713
  toDebugString() {
1714
+ const dateStr = dateToDisplay(this._date);
1113
1715
  const components = [
1114
1716
  `key: ${bytesToHex(this._key)}`,
1115
1717
  `hash: ${bytesToHex(this._hash)}`,
1116
1718
  `chainID: ${bytesToHex(this._chainId)}`,
1117
1719
  `seq: ${this._seq}`,
1118
- `date: ${this._date.toISOString()}`
1720
+ `date: ${dateStr}`
1119
1721
  ];
1120
1722
  const info = this.info();
1121
- if (info !== void 0) components.push(`info: ${JSON.stringify(info)}`);
1723
+ if (info !== void 0) {
1724
+ const textValue = info.asText();
1725
+ if (textValue !== void 0) components.push(`info: "${textValue}"`);
1726
+ else components.push(`info: ${info.toDiagnostic()}`);
1727
+ }
1122
1728
  return `ProvenanceMark(${components.join(", ")})`;
1123
1729
  }
1124
1730
  /**
@@ -1128,16 +1734,20 @@ var ProvenanceMark = class ProvenanceMark {
1128
1734
  return this._res === other._res && arraysEqual(this.message(), other.message());
1129
1735
  }
1130
1736
  /**
1131
- * JSON serialization.
1737
+ * JSON serialization. Field order, names, and date format mirror Rust's
1738
+ * `#[derive(Serialize)]` on `ProvenanceMark` (provenance-mark-rust/src/mark.rs):
1739
+ * `seq, date, res, chain_id, key, hash[, info_bytes]`. The date uses
1740
+ * `dateToDisplay()` (date-only when midnight, RFC3339-seconds with `Z`
1741
+ * otherwise), matching Rust's `serialize_iso8601` / `Date::to_string()`.
1132
1742
  */
1133
1743
  toJSON() {
1134
1744
  const result = {
1745
+ seq: this._seq,
1746
+ date: dateToDisplay(this._date),
1135
1747
  res: this._res,
1748
+ chain_id: toBase64(this._chainId),
1136
1749
  key: toBase64(this._key),
1137
- hash: toBase64(this._hash),
1138
- chainID: toBase64(this._chainId),
1139
- seq: this._seq,
1140
- date: this._date.toISOString()
1750
+ hash: toBase64(this._hash)
1141
1751
  };
1142
1752
  if (this._infoBytes.length > 0) result["info_bytes"] = toBase64(this._infoBytes);
1143
1753
  return result;
@@ -1149,16 +1759,55 @@ var ProvenanceMark = class ProvenanceMark {
1149
1759
  const res = json["res"];
1150
1760
  const key = fromBase64(json["key"]);
1151
1761
  const hash = fromBase64(json["hash"]);
1152
- const chainId = fromBase64(json["chainID"]);
1762
+ const chainId = fromBase64(json["chain_id"] ?? json["chainID"]);
1153
1763
  const seq = json["seq"];
1154
1764
  const dateStr = json["date"];
1155
1765
  const date = new Date(dateStr);
1156
1766
  const seqBytes = serializeSeq(res, seq);
1157
1767
  const dateBytes = serializeDate(res, date);
1158
1768
  let infoBytes = new Uint8Array(0);
1159
- if (typeof json["info_bytes"] === "string") infoBytes = fromBase64(json["info_bytes"]);
1769
+ if (typeof json["info_bytes"] === "string") {
1770
+ infoBytes = fromBase64(json["info_bytes"]);
1771
+ if (infoBytes.length > 0) try {
1772
+ decodeCbor(infoBytes);
1773
+ } catch (e) {
1774
+ throw new ProvenanceMarkError("CborError", "info_bytes is not valid CBOR", { details: e instanceof Error ? e.message : String(e) });
1775
+ }
1776
+ }
1160
1777
  return new ProvenanceMark(res, key, hash, chainId, seqBytes, dateBytes, infoBytes, seq, date);
1161
1778
  }
1779
+ /**
1780
+ * Validate a collection of provenance marks.
1781
+ *
1782
+ * Matches Rust: `ProvenanceMark::validate()` which delegates to
1783
+ * `ValidationReport::validate()`.
1784
+ */
1785
+ static validate(marks) {
1786
+ return validate(marks);
1787
+ }
1788
+ /**
1789
+ * Convert this provenance mark to a Gordian Envelope.
1790
+ *
1791
+ * Creates a leaf envelope containing the tagged CBOR representation.
1792
+ * Matches Rust: `Envelope::new(mark.to_cbor())` which creates a CBOR leaf.
1793
+ */
1794
+ intoEnvelope() {
1795
+ return Envelope.newLeaf(this.taggedCbor());
1796
+ }
1797
+ /**
1798
+ * Extract a ProvenanceMark from a Gordian Envelope.
1799
+ *
1800
+ * Matches Rust: `envelope.subject().try_leaf()?.try_into()`
1801
+ *
1802
+ * @param envelope - The envelope to extract from
1803
+ * @returns The extracted provenance mark
1804
+ * @throws ProvenanceMarkError if extraction fails
1805
+ */
1806
+ static fromEnvelope(envelope) {
1807
+ const leaf = envelope.subject().asLeaf();
1808
+ if (leaf !== void 0) return ProvenanceMark.fromTaggedCbor(leaf);
1809
+ throw new ProvenanceMarkError("CborError", void 0, { message: "Could not extract ProvenanceMark from envelope" });
1810
+ }
1162
1811
  };
1163
1812
  /**
1164
1813
  * Helper function to compare two Uint8Arrays.
@@ -1168,10 +1817,14 @@ function arraysEqual(a, b) {
1168
1817
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1169
1818
  return true;
1170
1819
  }
1171
-
1172
1820
  //#endregion
1173
1821
  //#region src/generator.ts
1174
1822
  /**
1823
+ * Copyright © 2023-2026 Blockchain Commons, LLC
1824
+ * Copyright © 2025-2026 Parity Technologies
1825
+ *
1826
+ */
1827
+ /**
1175
1828
  * Generator for creating provenance mark chains.
1176
1829
  */
1177
1830
  var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
@@ -1221,8 +1874,8 @@ var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
1221
1874
  /**
1222
1875
  * Create a new generator with custom random data.
1223
1876
  */
1224
- static newUsing(res, randomData$1) {
1225
- const seed = ProvenanceSeed.newUsing(randomData$1);
1877
+ static newUsing(res, randomData) {
1878
+ const seed = ProvenanceSeed.newUsing(randomData);
1226
1879
  return ProvenanceMarkGenerator.newWithSeed(res, seed);
1227
1880
  }
1228
1881
  /**
@@ -1237,7 +1890,7 @@ var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
1237
1890
  */
1238
1891
  static new(res, seed, chainId, nextSeq, rngState) {
1239
1892
  const linkLen = linkLength(res);
1240
- if (chainId.length !== linkLen) throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidChainIdLength, void 0, {
1893
+ if (chainId.length !== linkLen) throw new ProvenanceMarkError("InvalidChainIdLength", void 0, {
1241
1894
  expected: linkLen,
1242
1895
  actual: chainId.length
1243
1896
  });
@@ -1263,9 +1916,24 @@ var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
1263
1916
  }
1264
1917
  /**
1265
1918
  * String representation.
1919
+ *
1920
+ * Mirrors Rust `Display for ProvenanceMarkGenerator`
1921
+ * (`provenance-mark-rust/src/generator.rs:135-147`):
1922
+ *
1923
+ * ```rust
1924
+ * write!(f, "ProvenanceMarkGenerator(chainID: {}, res: {}, seed: {}, nextSeq: {}, rngState: {:?})",
1925
+ * hex::encode(&self.chain_id), self.res, self.seed.hex(), self.next_seq, self.rng_state)
1926
+ * ```
1927
+ *
1928
+ * The `rngState` field uses Rust's `{:?}` (Debug) format, which on a
1929
+ * `RngState([u8; 32])` tuple struct produces `RngState([n0, n1, ...])`
1930
+ * with each byte rendered as a decimal integer. Earlier revisions of
1931
+ * this port omitted `rngState` entirely from `toString()`, so the
1932
+ * output diverged from Rust's `Display`.
1266
1933
  */
1267
1934
  toString() {
1268
- return `ProvenanceMarkGenerator(chainID: ${bytesToHex(this._chainId)}, res: ${this._res}, seed: ${this._seed.hex()}, nextSeq: ${this._nextSeq})`;
1935
+ const rngBytes = Array.from(this._rngState.toBytes()).join(", ");
1936
+ return `ProvenanceMarkGenerator(chainID: ${bytesToHex(this._chainId)}, res: ${this._res}, seed: ${this._seed.hex()}, nextSeq: ${this._nextSeq}, rngState: RngState([${rngBytes}]))`;
1269
1937
  }
1270
1938
  /**
1271
1939
  * JSON serialization.
@@ -1290,278 +1958,64 @@ var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
1290
1958
  const rngState = RngState.fromBytes(fromBase64(json["rngState"]));
1291
1959
  return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
1292
1960
  }
1293
- };
1294
-
1295
- //#endregion
1296
- //#region src/validate.ts
1297
- /**
1298
- * Format for validation report output.
1299
- */
1300
- let ValidationReportFormat = /* @__PURE__ */ function(ValidationReportFormat$1) {
1301
- /** Human-readable text format */
1302
- ValidationReportFormat$1["Text"] = "text";
1303
- /** Compact JSON format (no whitespace) */
1304
- ValidationReportFormat$1["JsonCompact"] = "json-compact";
1305
- /** Pretty-printed JSON format (with indentation) */
1306
- ValidationReportFormat$1["JsonPretty"] = "json-pretty";
1307
- return ValidationReportFormat$1;
1308
- }({});
1309
- /**
1310
- * Format a validation issue as a string.
1311
- */
1312
- function formatValidationIssue(issue) {
1313
- switch (issue.type) {
1314
- case "HashMismatch": return `hash mismatch: expected ${issue.expected}, got ${issue.actual}`;
1315
- case "KeyMismatch": return "key mismatch: current hash was not generated from next key";
1316
- case "SequenceGap": return `sequence number gap: expected ${issue.expected}, got ${issue.actual}`;
1317
- case "DateOrdering": return `date must be equal or later: previous is ${issue.previous}, next is ${issue.next}`;
1318
- case "NonGenesisAtZero": return "non-genesis mark at sequence 0";
1319
- case "InvalidGenesisKey": return "genesis mark must have key equal to chain_id";
1320
- }
1321
- }
1322
- /**
1323
- * Get the chain ID as a hex string for display.
1324
- */
1325
- function chainIdHex(report) {
1326
- return hexEncode(report.chainId);
1327
- }
1328
- /**
1329
- * Check if the validation report has any issues.
1330
- */
1331
- function hasIssues(report) {
1332
- for (const chain of report.chains) if (!chain.hasGenesis) return true;
1333
- for (const chain of report.chains) for (const seq of chain.sequences) for (const mark of seq.marks) if (mark.issues.length > 0) return true;
1334
- if (report.chains.length > 1) return true;
1335
- if (report.chains.length === 1 && report.chains[0].sequences.length > 1) return true;
1336
- return false;
1337
- }
1338
- /**
1339
- * Check if the validation report contains interesting information.
1340
- */
1341
- function isInteresting(report) {
1342
- if (report.chains.length === 0) return false;
1343
- for (const chain of report.chains) if (!chain.hasGenesis) return true;
1344
- if (report.chains.length === 1) {
1345
- const chain = report.chains[0];
1346
- if (chain.sequences.length === 1) {
1347
- if (chain.sequences[0].marks.every((m) => m.issues.length === 0)) return false;
1348
- }
1349
- }
1350
- return true;
1351
- }
1352
- /**
1353
- * Format the validation report as human-readable text.
1354
- */
1355
- function formatText(report) {
1356
- if (!isInteresting(report)) return "";
1357
- const lines = [];
1358
- lines.push(`Total marks: ${report.marks.length}`);
1359
- lines.push(`Chains: ${report.chains.length}`);
1360
- lines.push("");
1361
- for (let chainIdx = 0; chainIdx < report.chains.length; chainIdx++) {
1362
- const chain = report.chains[chainIdx];
1363
- const chainIdStr = chainIdHex(chain);
1364
- const shortChainId = chainIdStr.length > 8 ? chainIdStr.slice(0, 8) : chainIdStr;
1365
- lines.push(`Chain ${chainIdx + 1}: ${shortChainId}`);
1366
- if (!chain.hasGenesis) lines.push(" Warning: No genesis mark found");
1367
- for (const seq of chain.sequences) for (const flaggedMark of seq.marks) {
1368
- const mark = flaggedMark.mark;
1369
- const shortId = mark.identifier();
1370
- const seqNum = mark.seq();
1371
- const annotations = [];
1372
- if (mark.isGenesis()) annotations.push("genesis mark");
1373
- for (const issue of flaggedMark.issues) {
1374
- let issueStr;
1375
- switch (issue.type) {
1376
- case "SequenceGap":
1377
- issueStr = `gap: ${issue.expected} missing`;
1378
- break;
1379
- case "DateOrdering":
1380
- issueStr = `date ${issue.previous} < ${issue.next}`;
1381
- break;
1382
- case "HashMismatch":
1383
- issueStr = "hash mismatch";
1384
- break;
1385
- case "KeyMismatch":
1386
- issueStr = "key mismatch";
1387
- break;
1388
- case "NonGenesisAtZero":
1389
- issueStr = "non-genesis at seq 0";
1390
- break;
1391
- case "InvalidGenesisKey":
1392
- issueStr = "invalid genesis key";
1393
- break;
1394
- }
1395
- annotations.push(issueStr);
1396
- }
1397
- if (annotations.length === 0) lines.push(` ${seqNum}: ${shortId}`);
1398
- else lines.push(` ${seqNum}: ${shortId} (${annotations.join(", ")})`);
1399
- }
1400
- lines.push("");
1401
- }
1402
- return lines.join("\n").trimEnd();
1403
- }
1404
- /**
1405
- * Format the validation report.
1406
- */
1407
- function formatReport(report, format) {
1408
- switch (format) {
1409
- case ValidationReportFormat.Text: return formatText(report);
1410
- case ValidationReportFormat.JsonCompact: return JSON.stringify(reportToJSON(report));
1411
- case ValidationReportFormat.JsonPretty: return JSON.stringify(reportToJSON(report), null, 2);
1412
- }
1413
- }
1414
- /**
1415
- * Convert a report to a JSON-serializable object.
1416
- */
1417
- function reportToJSON(report) {
1418
- return {
1419
- marks: report.marks.map((m) => m.toUrlEncoding()),
1420
- chains: report.chains.map((chain) => ({
1421
- chain_id: hexEncode(chain.chainId),
1422
- has_genesis: chain.hasGenesis,
1423
- marks: chain.marks.map((m) => m.toUrlEncoding()),
1424
- sequences: chain.sequences.map((seq) => ({
1425
- start_seq: seq.startSeq,
1426
- end_seq: seq.endSeq,
1427
- marks: seq.marks.map((fm) => ({
1428
- mark: fm.mark.toUrlEncoding(),
1429
- issues: fm.issues
1430
- }))
1431
- }))
1432
- }))
1433
- };
1434
- }
1435
- /**
1436
- * Build sequence bins for a chain.
1437
- */
1438
- function buildSequenceBins(marks) {
1439
- const sequences = [];
1440
- let currentSequence = [];
1441
- for (let i = 0; i < marks.length; i++) {
1442
- const mark = marks[i];
1443
- if (i === 0) currentSequence.push({
1444
- mark,
1445
- issues: []
1446
- });
1447
- else {
1448
- const prev = marks[i - 1];
1449
- try {
1450
- prev.precedesOpt(mark);
1451
- currentSequence.push({
1452
- mark,
1453
- issues: []
1454
- });
1455
- } catch (e) {
1456
- if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1457
- currentSequence = [{
1458
- mark,
1459
- issues: [parseValidationError(e, prev, mark)]
1460
- }];
1461
- }
1462
- }
1463
- }
1464
- if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1465
- return sequences;
1466
- }
1467
- /**
1468
- * Parse a validation error into a ValidationIssue.
1469
- */
1470
- function parseValidationError(e, prev, next) {
1471
- const message = e instanceof Error ? e.message : "";
1472
- if (message !== "" && message.includes("non-genesis mark at sequence 0")) return { type: "NonGenesisAtZero" };
1473
- if (message !== "" && message.includes("genesis mark must have key equal to chain_id")) return { type: "InvalidGenesisKey" };
1474
- if (message !== "" && message.includes("sequence gap")) {
1475
- const match = /expected (\d+), got (\d+)/.exec(message);
1476
- if (match !== null) return {
1477
- type: "SequenceGap",
1478
- expected: parseInt(match[1], 10),
1479
- actual: parseInt(match[2], 10)
1480
- };
1481
- }
1482
- if (message !== "" && message.includes("date ordering")) return {
1483
- type: "DateOrdering",
1484
- previous: prev.date().toISOString(),
1485
- next: next.date().toISOString()
1486
- };
1487
- if (message !== "" && message.includes("hash mismatch")) {
1488
- const match = /expected: (\w+), actual: (\w+)/.exec(message);
1489
- if (match !== null) return {
1490
- type: "HashMismatch",
1491
- expected: match[1],
1492
- actual: match[2]
1493
- };
1494
- return {
1495
- type: "HashMismatch",
1496
- expected: "",
1497
- actual: ""
1961
+ /**
1962
+ * Convert this generator to a Gordian Envelope.
1963
+ *
1964
+ * The envelope contains structured assertions for all generator fields:
1965
+ * - isA: "provenance-generator"
1966
+ * - res: The resolution
1967
+ * - seed: The seed
1968
+ * - next-seq: The next sequence number
1969
+ * - rng-state: The RNG state
1970
+ *
1971
+ * Note: Use provenanceMarkGeneratorToEnvelope() for a standalone function alternative.
1972
+ */
1973
+ intoEnvelope() {
1974
+ let envelope = Envelope.new(this._chainId);
1975
+ envelope = envelope.addType("provenance-generator");
1976
+ envelope = envelope.addAssertion("res", resolutionToNumber(this._res));
1977
+ envelope = envelope.addAssertion("seed", this._seed.toBytes());
1978
+ envelope = envelope.addAssertion("next-seq", this._nextSeq);
1979
+ envelope = envelope.addAssertion("rng-state", this._rngState.toBytes());
1980
+ return envelope;
1981
+ }
1982
+ /**
1983
+ * Extract a ProvenanceMarkGenerator from a Gordian Envelope.
1984
+ *
1985
+ * @param envelope - The envelope to extract from
1986
+ * @returns The extracted generator
1987
+ * @throws ProvenanceMarkError if extraction fails
1988
+ */
1989
+ static fromEnvelope(envelope) {
1990
+ const env = envelope;
1991
+ if (!env.hasType("provenance-generator")) throw new ProvenanceMarkError("CborError", void 0, { message: "Envelope is not a provenance-generator" });
1992
+ const chainId = env.subject().asByteString();
1993
+ if (chainId === void 0) throw new ProvenanceMarkError("CborError", void 0, { message: "Could not extract chain ID" });
1994
+ const extractAssertion = (predicate) => {
1995
+ const assertions = env.assertionsWithPredicate(predicate);
1996
+ if (assertions.length === 0) throw new ProvenanceMarkError("CborError", void 0, { message: `Missing ${predicate} assertion` });
1997
+ const assertionCase = assertions[0].case();
1998
+ if (assertionCase.type !== "assertion") throw new ProvenanceMarkError("CborError", void 0, { message: `Invalid ${predicate} assertion` });
1999
+ const obj = assertionCase.assertion.object();
2000
+ const objCase = obj.case();
2001
+ if (objCase.type === "leaf") return {
2002
+ cbor: objCase.cbor,
2003
+ bytes: obj.asByteString()
2004
+ };
2005
+ throw new ProvenanceMarkError("CborError", void 0, { message: `Invalid ${predicate} value` });
1498
2006
  };
2007
+ const res = resolutionFromCbor(extractAssertion("res").cbor);
2008
+ const seedValue = extractAssertion("seed");
2009
+ if (seedValue.bytes === void 0) throw new ProvenanceMarkError("CborError", void 0, { message: "Invalid seed data" });
2010
+ const seed = ProvenanceSeed.fromBytes(seedValue.bytes);
2011
+ const seqValue = extractAssertion("next-seq");
2012
+ const nextSeq = Number(seqValue.cbor);
2013
+ const rngValue = extractAssertion("rng-state");
2014
+ if (rngValue.bytes === void 0) throw new ProvenanceMarkError("CborError", void 0, { message: "Invalid rng-state data" });
2015
+ const rngState = RngState.fromBytes(rngValue.bytes);
2016
+ return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
1499
2017
  }
1500
- return { type: "KeyMismatch" };
1501
- }
1502
- /**
1503
- * Create a sequence report from flagged marks.
1504
- */
1505
- function createSequenceReport(marks) {
1506
- return {
1507
- startSeq: marks.length > 0 ? marks[0].mark.seq() : 0,
1508
- endSeq: marks.length > 0 ? marks[marks.length - 1].mark.seq() : 0,
1509
- marks
1510
- };
1511
- }
1512
- /**
1513
- * Validate a collection of provenance marks.
1514
- */
1515
- function validate(marks) {
1516
- const seen = /* @__PURE__ */ new Set();
1517
- const deduplicatedMarks = [];
1518
- for (const mark of marks) {
1519
- const key = mark.toUrlEncoding();
1520
- if (!seen.has(key)) {
1521
- seen.add(key);
1522
- deduplicatedMarks.push(mark);
1523
- }
1524
- }
1525
- const chainBins = /* @__PURE__ */ new Map();
1526
- for (const mark of deduplicatedMarks) {
1527
- const chainIdKey = hexEncode(mark.chainId());
1528
- const bin = chainBins.get(chainIdKey);
1529
- if (bin !== void 0) bin.push(mark);
1530
- else chainBins.set(chainIdKey, [mark]);
1531
- }
1532
- const chains = [];
1533
- for (const [chainIdKey, chainMarks] of chainBins) {
1534
- chainMarks.sort((a, b) => a.seq() - b.seq());
1535
- const hasGenesis = chainMarks.length > 0 && chainMarks[0].seq() === 0 && chainMarks[0].isGenesis();
1536
- const sequences = buildSequenceBins(chainMarks);
1537
- chains.push({
1538
- chainId: hexDecode(chainIdKey),
1539
- hasGenesis,
1540
- marks: chainMarks,
1541
- sequences
1542
- });
1543
- }
1544
- chains.sort((a, b) => hexEncode(a.chainId).localeCompare(hexEncode(b.chainId)));
1545
- return {
1546
- marks: deduplicatedMarks,
1547
- chains
1548
- };
1549
- }
1550
- /**
1551
- * Helper function to encode bytes as hex.
1552
- */
1553
- function hexEncode(bytes) {
1554
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1555
- }
1556
- /**
1557
- * Helper function to decode hex to bytes.
1558
- */
1559
- function hexDecode(hex) {
1560
- const bytes = new Uint8Array(hex.length / 2);
1561
- for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
1562
- return bytes;
1563
- }
1564
-
2018
+ };
1565
2019
  //#endregion
1566
2020
  //#region src/mark-info.ts
1567
2021
  /**
@@ -1582,12 +2036,19 @@ var ProvenanceMarkInfo = class ProvenanceMarkInfo {
1582
2036
  }
1583
2037
  /**
1584
2038
  * Create a new ProvenanceMarkInfo from a mark.
2039
+ *
2040
+ * Mirrors Rust `ProvenanceMarkInfo::new`
2041
+ * (`provenance-mark-rust/src/mark_info.rs`), which calls
2042
+ * `mark.ur()` — i.e. the `UREncodable` implementation, whose
2043
+ * payload is the **untagged** CBOR with type `"provenance"`. Earlier
2044
+ * revisions of this port called `decodeCbor(mark.toCborData())` and
2045
+ * wrapped the resulting *tagged* CBOR in `UR.new("provenance", ...)`,
2046
+ * which prepended the CBOR tag to the UR bytewords and broke
2047
+ * cross-impl interop (UR strings produced by Rust would not parse,
2048
+ * and vice versa).
1585
2049
  */
1586
2050
  static new(mark, comment = "") {
1587
- const tagName = PROVENANCE_MARK.name;
1588
- if (tagName === void 0) throw new Error("PROVENANCE_MARK tag has no name");
1589
- const cborValue = decodeCbor(mark.toCborData());
1590
- return new ProvenanceMarkInfo(mark, UR.new(tagName, cborValue), mark.bytewordsIdentifier(true), mark.bytemojiIdentifier(true), comment);
2051
+ return new ProvenanceMarkInfo(mark, mark.ur(), mark.idBytewords(4, true), mark.idBytemoji(4, true), comment);
1591
2052
  }
1592
2053
  mark() {
1593
2054
  return this._mark;
@@ -1606,12 +2067,16 @@ var ProvenanceMarkInfo = class ProvenanceMarkInfo {
1606
2067
  }
1607
2068
  /**
1608
2069
  * Generate a markdown summary of the mark.
2070
+ *
2071
+ * Date rendering uses {@link dateToDisplay} so midnight-UTC dates
2072
+ * appear as `YYYY-MM-DD` (matching Rust `format!("{}",
2073
+ * self.mark.date())`), not as `YYYY-MM-DDT00:00:00Z`.
1609
2074
  */
1610
2075
  markdownSummary() {
1611
2076
  const lines = [];
1612
2077
  lines.push("---");
1613
2078
  lines.push("");
1614
- lines.push(this._mark.date().toISOString());
2079
+ lines.push(dateToDisplay(this._mark.date()));
1615
2080
  lines.push("");
1616
2081
  lines.push(`#### ${this._ur.toString()}`);
1617
2082
  lines.push("");
@@ -1626,32 +2091,136 @@ var ProvenanceMarkInfo = class ProvenanceMarkInfo {
1626
2091
  return lines.join("\n");
1627
2092
  }
1628
2093
  /**
1629
- * JSON serialization.
2094
+ * JSON serialization. Field order mirrors Rust's `#[derive(Serialize)]`
2095
+ * on `ProvenanceMarkInfo` (provenance-mark-rust/src/mark_info.rs):
2096
+ * `ur, bytewords, bytemoji, [comment,] mark` — `comment` (when present)
2097
+ * comes BEFORE `mark`. Rust uses `skip_serializing_if = "String::is_empty"`,
2098
+ * matched here by the `if (...length > 0)` guard.
1630
2099
  */
1631
2100
  toJSON() {
1632
2101
  const result = {
1633
2102
  ur: this._ur.toString(),
1634
2103
  bytewords: this._bytewords,
1635
- bytemoji: this._bytemoji,
1636
- mark: this._mark.toJSON()
2104
+ bytemoji: this._bytemoji
1637
2105
  };
1638
2106
  if (this._comment.length > 0) result["comment"] = this._comment;
2107
+ result["mark"] = this._mark.toJSON();
1639
2108
  return result;
1640
2109
  }
1641
2110
  /**
1642
2111
  * Create from JSON object.
2112
+ *
2113
+ * Decodes the UR string through {@link ProvenanceMark.fromURString},
2114
+ * which correctly handles the **untagged** CBOR payload that
2115
+ * `mark.ur()` produces — symmetric with the constructor.
1643
2116
  */
1644
2117
  static fromJSON(json) {
1645
2118
  const urString = json["ur"];
1646
- const ur = UR.fromURString(urString);
1647
- const cborBytes = cborData(ur.cbor());
1648
- const mark = ProvenanceMark.fromCborData(cborBytes);
2119
+ const mark = ProvenanceMark.fromURString(urString);
2120
+ const ur = mark.ur();
1649
2121
  const bytewords = json["bytewords"];
1650
2122
  const bytemoji = json["bytemoji"];
1651
2123
  return new ProvenanceMarkInfo(mark, ur, bytewords, bytemoji, typeof json["comment"] === "string" ? json["comment"] : "");
1652
2124
  }
1653
2125
  };
1654
-
1655
2126
  //#endregion
1656
- export { PROVENANCE_SEED_LENGTH, ProvenanceMark, ProvenanceMarkError, ProvenanceMarkErrorType, ProvenanceMarkGenerator, ProvenanceMarkInfo, ProvenanceMarkResolution, ProvenanceSeed, RNG_STATE_LENGTH, RngState, SHA256_SIZE, ValidationReportFormat, Xoshiro256StarStar, chainIdHex, chainIdRange, dateBytesLength, dateBytesRange, dateFromIso8601, dateToDateString, dateToIso8601, deserialize2Bytes, deserialize4Bytes, deserialize6Bytes, deserializeDate, deserializeSeq, extendKey, fixedLength, formatReport, formatValidationIssue, hasIssues, hashRange, hkdfHmacSha256, infoRangeStart, keyRange, linkLength, obfuscate, rangeOfDaysInMonth, resolutionFromCbor, resolutionFromNumber, resolutionToCbor, resolutionToNumber, resolutionToString, seqBytesLength, seqBytesRange, serialize2Bytes, serialize4Bytes, serialize6Bytes, serializeDate, serializeSeq, sha256, sha256Prefix, validate };
2127
+ //#region src/envelope.ts
2128
+ /**
2129
+ * Copyright © 2023-2026 Blockchain Commons, LLC
2130
+ * Copyright © 2025-2026 Parity Technologies
2131
+ *
2132
+ *
2133
+ * Envelope support for Provenance Marks
2134
+ *
2135
+ * This module provides Gordian Envelope integration for ProvenanceMark and
2136
+ * ProvenanceMarkGenerator, enabling them to be used with the bc-envelope
2137
+ * ecosystem.
2138
+ *
2139
+ * Ported from provenance-mark-rust/src/mark.rs and generator.rs (envelope feature)
2140
+ */
2141
+ /**
2142
+ * Registers provenance mark tags in the global format context.
2143
+ *
2144
+ * Matches Rust: register_tags()
2145
+ */
2146
+ function registerTags() {
2147
+ withFormatContextMut((context) => {
2148
+ registerTagsIn(context);
2149
+ });
2150
+ }
2151
+ /**
2152
+ * Registers provenance mark tags in a specific format context.
2153
+ *
2154
+ * Matches Rust: register_tags_in()
2155
+ *
2156
+ * @param context - The format context to register tags in
2157
+ */
2158
+ function registerTagsIn(context) {
2159
+ registerTagsIn$1(context);
2160
+ context.tags().setSummarizer(BigInt(PROVENANCE_MARK.value), (untaggedCbor, _flat) => {
2161
+ try {
2162
+ return {
2163
+ ok: true,
2164
+ value: ProvenanceMark.fromUntaggedCbor(untaggedCbor).toString()
2165
+ };
2166
+ } catch {
2167
+ return {
2168
+ ok: false,
2169
+ error: {
2170
+ type: "Custom",
2171
+ message: "invalid provenance mark"
2172
+ }
2173
+ };
2174
+ }
2175
+ });
2176
+ }
2177
+ /**
2178
+ * Convert a ProvenanceMark to an Envelope.
2179
+ *
2180
+ * Delegates to ProvenanceMark.intoEnvelope() — single source of truth.
2181
+ *
2182
+ * @param mark - The provenance mark to convert
2183
+ * @returns An envelope containing the mark
2184
+ */
2185
+ function provenanceMarkToEnvelope(mark) {
2186
+ return mark.intoEnvelope();
2187
+ }
2188
+ /**
2189
+ * Extract a ProvenanceMark from an Envelope.
2190
+ *
2191
+ * Delegates to ProvenanceMark.fromEnvelope() — single source of truth.
2192
+ *
2193
+ * @param envelope - The envelope to extract from
2194
+ * @returns The extracted provenance mark
2195
+ * @throws ProvenanceMarkError if extraction fails
2196
+ */
2197
+ function provenanceMarkFromEnvelope(envelope) {
2198
+ return ProvenanceMark.fromEnvelope(envelope);
2199
+ }
2200
+ /**
2201
+ * Convert a ProvenanceMarkGenerator to an Envelope.
2202
+ *
2203
+ * Delegates to ProvenanceMarkGenerator.intoEnvelope() — single source of truth.
2204
+ *
2205
+ * @param generator - The generator to convert
2206
+ * @returns An envelope containing the generator
2207
+ */
2208
+ function provenanceMarkGeneratorToEnvelope(generator) {
2209
+ return generator.intoEnvelope();
2210
+ }
2211
+ /**
2212
+ * Extract a ProvenanceMarkGenerator from an Envelope.
2213
+ *
2214
+ * Delegates to ProvenanceMarkGenerator.fromEnvelope() — single source of truth.
2215
+ *
2216
+ * @param envelope - The envelope to extract from
2217
+ * @returns The extracted generator
2218
+ * @throws ProvenanceMarkError if extraction fails
2219
+ */
2220
+ function provenanceMarkGeneratorFromEnvelope(envelope) {
2221
+ return ProvenanceMarkGenerator.fromEnvelope(envelope);
2222
+ }
2223
+ //#endregion
2224
+ export { FormatContext, PROVENANCE_SEED_LENGTH, ProvenanceMark, ProvenanceMarkError, ProvenanceMarkErrorType, ProvenanceMarkGenerator, ProvenanceMarkInfo, ProvenanceMarkResolution, ProvenanceSeed, RNG_STATE_LENGTH, RngState, SHA256_SIZE, ValidationReportFormat, Xoshiro256StarStar, chainIdHex, chainIdRange, dateBytesLength, dateBytesRange, dateFromIso8601, dateToDateString, dateToDisplay, dateToIso8601, deserialize2Bytes, deserialize4Bytes, deserialize6Bytes, deserializeDate, deserializeSeq, extendKey, fixedLength, formatReport, formatValidationIssue, hasIssues, hashRange, hkdfHmacSha256, infoRangeStart, keyRange, linkLength, obfuscate, parseDate, parseSeed, provenanceMarkFromEnvelope, provenanceMarkGeneratorFromEnvelope, provenanceMarkGeneratorToEnvelope, provenanceMarkToEnvelope, rangeOfDaysInMonth, registerTags, registerTagsIn, resolutionFromCbor, resolutionFromNumber, resolutionToCbor, resolutionToNumber, resolutionToString, seqBytesLength, seqBytesRange, serialize2Bytes, serialize4Bytes, serialize6Bytes, serializeDate, serializeSeq, sha256, sha256Prefix, validate };
2225
+
1657
2226
  //# sourceMappingURL=index.mjs.map