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