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