@bcts/provenance-mark 1.0.0-alpha.8 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -1
- package/README.md +1 -1
- package/dist/index.cjs +1174 -584
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +489 -136
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +489 -136
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +1247 -659
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +1132 -563
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -22
- package/src/crypto-utils.ts +9 -3
- package/src/date.ts +42 -0
- package/src/envelope.ts +122 -0
- package/src/error.ts +21 -0
- package/src/generator.ts +153 -2
- package/src/index.ts +24 -0
- package/src/mark-info.ts +38 -17
- package/src/mark.ts +399 -45
- package/src/resolution.ts +32 -9
- package/src/rng-state.ts +6 -0
- package/src/seed.ts +6 -0
- package/src/utils.ts +68 -39
- package/src/validate.ts +63 -57
- package/src/xoshiro256starstar.ts +6 -0
package/src/utils.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*
|
|
2
6
|
* Utility functions for byte array conversions.
|
|
3
7
|
*
|
|
4
8
|
* These functions provide cross-platform support for common byte manipulation
|
|
5
9
|
* operations needed in provenance mark encoding.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
// Declare Node.js types for environments where they might not be available
|
|
9
|
-
declare const require: ((module: string) => unknown) | undefined;
|
|
10
|
-
|
|
11
12
|
/**
|
|
12
13
|
* Convert a Uint8Array to a lowercase hexadecimal string.
|
|
13
14
|
*
|
|
@@ -50,26 +51,13 @@ export function hexToBytes(hex: string): Uint8Array {
|
|
|
50
51
|
* @returns A base64-encoded string
|
|
51
52
|
*/
|
|
52
53
|
export function toBase64(data: Uint8Array): string {
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let binary = "";
|
|
59
|
-
for (const byte of data) {
|
|
60
|
-
binary += String.fromCharCode(byte);
|
|
61
|
-
}
|
|
62
|
-
return globalBtoa(binary);
|
|
54
|
+
// Convert bytes to binary string without spread operator to avoid
|
|
55
|
+
// call stack limits for large arrays
|
|
56
|
+
let binary = "";
|
|
57
|
+
for (const byte of data) {
|
|
58
|
+
binary += String.fromCharCode(byte);
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
const requireFn = require;
|
|
66
|
-
if (typeof requireFn === "function") {
|
|
67
|
-
const { Buffer: NodeBuffer } = requireFn("buffer") as {
|
|
68
|
-
Buffer: { from: (data: Uint8Array) => { toString: (encoding: string) => string } };
|
|
69
|
-
};
|
|
70
|
-
return NodeBuffer.from(data).toString("base64");
|
|
71
|
-
}
|
|
72
|
-
throw new Error("btoa not available and require is not defined");
|
|
60
|
+
return btoa(binary);
|
|
73
61
|
}
|
|
74
62
|
|
|
75
63
|
/**
|
|
@@ -81,23 +69,64 @@ export function toBase64(data: Uint8Array): string {
|
|
|
81
69
|
* @returns The decoded byte array
|
|
82
70
|
*/
|
|
83
71
|
export function fromBase64(base64: string): Uint8Array {
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const bytes = new Uint8Array(binary.length);
|
|
89
|
-
for (let i = 0; i < binary.length; i++) {
|
|
90
|
-
bytes[i] = binary.charCodeAt(i);
|
|
91
|
-
}
|
|
92
|
-
return bytes;
|
|
72
|
+
const binary = atob(base64);
|
|
73
|
+
const bytes = new Uint8Array(binary.length);
|
|
74
|
+
for (let i = 0; i < binary.length; i++) {
|
|
75
|
+
bytes[i] = binary.charCodeAt(i);
|
|
93
76
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
return bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a base64-encoded provenance seed.
|
|
82
|
+
*
|
|
83
|
+
* Mirrors Rust's `parse_seed` user helper
|
|
84
|
+
* (`provenance-mark-rust/src/util.rs:34-38`), which round-trips the
|
|
85
|
+
* input through serde JSON / `deserialize_block` and so requires the
|
|
86
|
+
* decoded bytes to be exactly {@link PROVENANCE_SEED_LENGTH} (32) long.
|
|
87
|
+
* The TS equivalent decodes the base64 directly and delegates to
|
|
88
|
+
* {@link ProvenanceSeed.fromBytes} for the length check.
|
|
89
|
+
*
|
|
90
|
+
* @param s - Base64-encoded 32-byte seed string.
|
|
91
|
+
* @returns The decoded {@link ProvenanceSeed}.
|
|
92
|
+
* @throws {ProvenanceMarkError} If the input is not valid base64 or the
|
|
93
|
+
* decoded length is not exactly 32 bytes.
|
|
94
|
+
*/
|
|
95
|
+
export function parseSeed(s: string): ProvenanceSeed {
|
|
96
|
+
let bytes: Uint8Array;
|
|
97
|
+
try {
|
|
98
|
+
bytes = fromBase64(s);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
throw new ProvenanceMarkError(
|
|
101
|
+
ProvenanceMarkErrorType.Base64Error,
|
|
102
|
+
"invalid base64 encoding for provenance seed",
|
|
103
|
+
{ details: e instanceof Error ? e.message : String(e) },
|
|
104
|
+
);
|
|
101
105
|
}
|
|
102
|
-
|
|
106
|
+
return ProvenanceSeed.fromBytes(bytes);
|
|
103
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a date string (`YYYY-MM-DD` or full RFC 3339) into a `Date`.
|
|
111
|
+
*
|
|
112
|
+
* Mirrors Rust's `parse_date` user helper
|
|
113
|
+
* (`provenance-mark-rust/src/util.rs:40-42`), which delegates to
|
|
114
|
+
* `Date::from_string` (the same parser the JSON deserializer uses).
|
|
115
|
+
* Accepts the same shapes the Rust parser does:
|
|
116
|
+
*
|
|
117
|
+
* - `YYYY-MM-DD` (interpreted as UTC midnight, matching JS spec).
|
|
118
|
+
* - Full RFC 3339, e.g. `2023-06-20T15:30:45Z` or
|
|
119
|
+
* `2023-06-20T15:30:45.123Z`.
|
|
120
|
+
*
|
|
121
|
+
* @throws {ProvenanceMarkError} If the input fails to parse.
|
|
122
|
+
*/
|
|
123
|
+
export function parseDate(s: string): Date {
|
|
124
|
+
const date = new Date(s);
|
|
125
|
+
if (Number.isNaN(date.getTime())) {
|
|
126
|
+
throw new ProvenanceMarkError(ProvenanceMarkErrorType.InvalidDate, `cannot parse date: ${s}`);
|
|
127
|
+
}
|
|
128
|
+
return date;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
import { ProvenanceSeed } from "./seed.js";
|
|
132
|
+
import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
|
package/src/validate.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
// Ported from provenance-mark-rust/src/validate.rs
|
|
2
8
|
|
|
3
9
|
import type { ProvenanceMark } from "./mark.js";
|
|
10
|
+
import { ProvenanceMarkError } from "./error.js";
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Format for validation report output.
|
|
@@ -186,7 +193,7 @@ function formatText(report: ValidationReport): string {
|
|
|
186
193
|
// Report each mark in the sequence
|
|
187
194
|
for (const flaggedMark of seq.marks) {
|
|
188
195
|
const mark = flaggedMark.mark;
|
|
189
|
-
const shortId = mark.
|
|
196
|
+
const shortId = mark.idHex().slice(0, 8);
|
|
190
197
|
const seqNum = mark.seq();
|
|
191
198
|
|
|
192
199
|
// Build the mark line with annotations
|
|
@@ -257,23 +264,47 @@ export function formatReport(report: ValidationReport, format: ValidationReportF
|
|
|
257
264
|
*/
|
|
258
265
|
function reportToJSON(report: ValidationReport): unknown {
|
|
259
266
|
return {
|
|
260
|
-
marks: report.marks.map((m) => m.
|
|
267
|
+
marks: report.marks.map((m) => m.urString()),
|
|
261
268
|
chains: report.chains.map((chain) => ({
|
|
262
269
|
chain_id: hexEncode(chain.chainId),
|
|
263
270
|
has_genesis: chain.hasGenesis,
|
|
264
|
-
marks: chain.marks.map((m) => m.
|
|
271
|
+
marks: chain.marks.map((m) => m.urString()),
|
|
265
272
|
sequences: chain.sequences.map((seq) => ({
|
|
266
273
|
start_seq: seq.startSeq,
|
|
267
274
|
end_seq: seq.endSeq,
|
|
268
275
|
marks: seq.marks.map((fm) => ({
|
|
269
|
-
mark: fm.mark.
|
|
270
|
-
issues: fm.issues,
|
|
276
|
+
mark: fm.mark.urString(),
|
|
277
|
+
issues: fm.issues.map(issueToJSON),
|
|
271
278
|
})),
|
|
272
279
|
})),
|
|
273
280
|
})),
|
|
274
281
|
};
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Convert a ValidationIssue to JSON matching Rust's serde format.
|
|
286
|
+
*
|
|
287
|
+
* Rust uses `#[serde(tag = "type", content = "data")]` which wraps
|
|
288
|
+
* struct variant data in a `"data"` field. Unit variants have no
|
|
289
|
+
* `"data"` field.
|
|
290
|
+
*/
|
|
291
|
+
function issueToJSON(issue: ValidationIssue): unknown {
|
|
292
|
+
switch (issue.type) {
|
|
293
|
+
case "HashMismatch":
|
|
294
|
+
return { type: "HashMismatch", data: { expected: issue.expected, actual: issue.actual } };
|
|
295
|
+
case "SequenceGap":
|
|
296
|
+
return { type: "SequenceGap", data: { expected: issue.expected, actual: issue.actual } };
|
|
297
|
+
case "DateOrdering":
|
|
298
|
+
return { type: "DateOrdering", data: { previous: issue.previous, next: issue.next } };
|
|
299
|
+
case "KeyMismatch":
|
|
300
|
+
return { type: "KeyMismatch" };
|
|
301
|
+
case "NonGenesisAtZero":
|
|
302
|
+
return { type: "NonGenesisAtZero" };
|
|
303
|
+
case "InvalidGenesisKey":
|
|
304
|
+
return { type: "InvalidGenesisKey" };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
277
308
|
/**
|
|
278
309
|
* Build sequence bins for a chain.
|
|
279
310
|
*/
|
|
@@ -301,8 +332,14 @@ function buildSequenceBins(marks: ProvenanceMark[]): SequenceReport[] {
|
|
|
301
332
|
sequences.push(createSequenceReport(currentSequence));
|
|
302
333
|
}
|
|
303
334
|
|
|
304
|
-
//
|
|
305
|
-
|
|
335
|
+
// Extract structured issue directly from the error
|
|
336
|
+
// Matches Rust: Error::Validation(v) => v, _ => ValidationIssue::KeyMismatch
|
|
337
|
+
let issue: ValidationIssue;
|
|
338
|
+
if (e instanceof ProvenanceMarkError && e.details?.["validationIssue"] !== undefined) {
|
|
339
|
+
issue = e.details["validationIssue"] as ValidationIssue;
|
|
340
|
+
} else {
|
|
341
|
+
issue = { type: "KeyMismatch" }; // Fallback
|
|
342
|
+
}
|
|
306
343
|
|
|
307
344
|
// Start new sequence with this mark, flagged with the issue
|
|
308
345
|
currentSequence = [{ mark, issues: [issue] }];
|
|
@@ -318,53 +355,6 @@ function buildSequenceBins(marks: ProvenanceMark[]): SequenceReport[] {
|
|
|
318
355
|
return sequences;
|
|
319
356
|
}
|
|
320
357
|
|
|
321
|
-
/**
|
|
322
|
-
* Parse a validation error into a ValidationIssue.
|
|
323
|
-
*/
|
|
324
|
-
function parseValidationError(
|
|
325
|
-
e: unknown,
|
|
326
|
-
prev: ProvenanceMark,
|
|
327
|
-
next: ProvenanceMark,
|
|
328
|
-
): ValidationIssue {
|
|
329
|
-
const message = e instanceof Error ? e.message : "";
|
|
330
|
-
|
|
331
|
-
if (message !== "" && message.includes("non-genesis mark at sequence 0")) {
|
|
332
|
-
return { type: "NonGenesisAtZero" };
|
|
333
|
-
}
|
|
334
|
-
if (message !== "" && message.includes("genesis mark must have key equal to chain_id")) {
|
|
335
|
-
return { type: "InvalidGenesisKey" };
|
|
336
|
-
}
|
|
337
|
-
if (message !== "" && message.includes("sequence gap")) {
|
|
338
|
-
const seqGapRegex = /expected (\d+), got (\d+)/;
|
|
339
|
-
const match = seqGapRegex.exec(message);
|
|
340
|
-
if (match !== null) {
|
|
341
|
-
return {
|
|
342
|
-
type: "SequenceGap",
|
|
343
|
-
expected: parseInt(match[1], 10),
|
|
344
|
-
actual: parseInt(match[2], 10),
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (message !== "" && message.includes("date ordering")) {
|
|
349
|
-
return {
|
|
350
|
-
type: "DateOrdering",
|
|
351
|
-
previous: prev.date().toISOString(),
|
|
352
|
-
next: next.date().toISOString(),
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
if (message !== "" && message.includes("hash mismatch")) {
|
|
356
|
-
const hashRegex = /expected: (\w+), actual: (\w+)/;
|
|
357
|
-
const match = hashRegex.exec(message);
|
|
358
|
-
if (match !== null) {
|
|
359
|
-
return { type: "HashMismatch", expected: match[1], actual: match[2] };
|
|
360
|
-
}
|
|
361
|
-
return { type: "HashMismatch", expected: "", actual: "" };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Fallback
|
|
365
|
-
return { type: "KeyMismatch" };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
358
|
/**
|
|
369
359
|
* Create a sequence report from flagged marks.
|
|
370
360
|
*/
|
|
@@ -379,10 +369,11 @@ function createSequenceReport(marks: FlaggedMark[]): SequenceReport {
|
|
|
379
369
|
*/
|
|
380
370
|
export function validate(marks: ProvenanceMark[]): ValidationReport {
|
|
381
371
|
// Deduplicate exact duplicates
|
|
372
|
+
// Matches Rust semantics: PartialEq compares (res, message())
|
|
382
373
|
const seen = new Set<string>();
|
|
383
374
|
const deduplicatedMarks: ProvenanceMark[] = [];
|
|
384
375
|
for (const mark of marks) {
|
|
385
|
-
const key = mark.
|
|
376
|
+
const key = `${mark.res()}:${hexEncode(mark.message())}`;
|
|
386
377
|
if (!seen.has(key)) {
|
|
387
378
|
seen.add(key);
|
|
388
379
|
deduplicatedMarks.push(mark);
|
|
@@ -422,8 +413,23 @@ export function validate(marks: ProvenanceMark[]): ValidationReport {
|
|
|
422
413
|
});
|
|
423
414
|
}
|
|
424
415
|
|
|
425
|
-
// Sort chains by chain ID for consistent output
|
|
426
|
-
|
|
416
|
+
// Sort chains by chain ID for consistent output.
|
|
417
|
+
//
|
|
418
|
+
// Mirrors Rust `chains.sort_by_key(|c| c.chain_id.clone())` which
|
|
419
|
+
// uses `Vec<u8>::cmp` (lexicographic byte comparison). We compare
|
|
420
|
+
// hex-encoded chain IDs with raw `<`/`>` rather than `localeCompare`,
|
|
421
|
+
// because hex-digit ordering is locale-independent under JS `<`/`>`
|
|
422
|
+
// (UTF-16 code-unit compare) but `localeCompare` is locale-aware and
|
|
423
|
+
// could in principle drift on a non-default locale. Using a pure
|
|
424
|
+
// bytewise compare here keeps the chain order byte-identical to Rust
|
|
425
|
+
// regardless of locale.
|
|
426
|
+
chains.sort((a, b) => {
|
|
427
|
+
const aHex = hexEncode(a.chainId);
|
|
428
|
+
const bHex = hexEncode(b.chainId);
|
|
429
|
+
if (aHex < bHex) return -1;
|
|
430
|
+
if (aHex > bHex) return 1;
|
|
431
|
+
return 0;
|
|
432
|
+
});
|
|
427
433
|
|
|
428
434
|
return { marks: deduplicatedMarks, chains };
|
|
429
435
|
}
|