@bcts/provenance-mark 1.0.0-alpha.17 → 1.0.0-alpha.18
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/README.md +1 -1
- package/dist/index.cjs +385 -347
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -104
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +120 -104
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +385 -347
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +381 -349
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/envelope.ts +39 -153
- package/src/index.ts +3 -1
- package/src/mark-info.ts +1 -1
- package/src/mark.ts +107 -40
- package/src/validate.ts +39 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bcts/provenance-mark",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blockchain Commons Provenance Mark for TypeScript - A cryptographically-secured system for establishing and verifying the authenticity of works",
|
|
6
6
|
"license": "BSD-2-Clause-Patent",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@bcts/eslint": "^0.1.0",
|
|
69
69
|
"@bcts/tsconfig": "^0.1.0",
|
|
70
70
|
"@eslint/js": "^9.39.2",
|
|
71
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
72
|
-
"@typescript-eslint/parser": "^8.
|
|
71
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
72
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
73
73
|
"eslint": "^9.39.2",
|
|
74
74
|
"ts-node": "^10.9.2",
|
|
75
75
|
"tsdown": "^0.20.1",
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
"vitest": "^4.0.18"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@bcts/dcbor": "^1.0.0-alpha.
|
|
82
|
-
"@bcts/envelope": "^1.0.0-alpha.
|
|
83
|
-
"@bcts/rand": "^1.0.0-alpha.
|
|
84
|
-
"@bcts/tags": "^1.0.0-alpha.
|
|
85
|
-
"@bcts/uniform-resources": "^1.0.0-alpha.
|
|
81
|
+
"@bcts/dcbor": "^1.0.0-alpha.18",
|
|
82
|
+
"@bcts/envelope": "^1.0.0-alpha.18",
|
|
83
|
+
"@bcts/rand": "^1.0.0-alpha.18",
|
|
84
|
+
"@bcts/tags": "^1.0.0-alpha.18",
|
|
85
|
+
"@bcts/uniform-resources": "^1.0.0-alpha.18",
|
|
86
86
|
"@noble/ciphers": "^2.1.1",
|
|
87
87
|
"@noble/hashes": "^2.0.1"
|
|
88
88
|
}
|
package/src/envelope.ts
CHANGED
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
* Ported from provenance-mark-rust/src/mark.rs and generator.rs (envelope feature)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
type Envelope,
|
|
13
|
+
type FormatContext,
|
|
14
|
+
withFormatContextMut,
|
|
15
|
+
registerTagsIn as envelopeRegisterTagsIn,
|
|
16
|
+
} from "@bcts/envelope";
|
|
17
|
+
import { type Cbor, type SummarizerResult } from "@bcts/dcbor";
|
|
13
18
|
import { PROVENANCE_MARK } from "@bcts/tags";
|
|
14
19
|
import { ProvenanceMark } from "./mark.js";
|
|
15
20
|
import { ProvenanceMarkGenerator } from "./generator.js";
|
|
16
|
-
import { resolutionFromCbor, resolutionToNumber } from "./resolution.js";
|
|
17
|
-
import { ProvenanceSeed } from "./seed.js";
|
|
18
|
-
import { RngState } from "./rng-state.js";
|
|
19
|
-
import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
|
|
20
21
|
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// Tag Registration
|
|
@@ -25,40 +26,39 @@ import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
|
|
|
25
26
|
/**
|
|
26
27
|
* Registers provenance mark tags in the global format context.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
* provenance marks in a human-readable format.
|
|
29
|
+
* Matches Rust: register_tags()
|
|
30
30
|
*/
|
|
31
31
|
export function registerTags(): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
registerTagsIn(globalTagsContext);
|
|
32
|
+
withFormatContextMut((context) => {
|
|
33
|
+
registerTagsIn(context);
|
|
34
|
+
});
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* Registers provenance mark tags in a specific format context.
|
|
40
39
|
*
|
|
40
|
+
* Matches Rust: register_tags_in()
|
|
41
|
+
*
|
|
41
42
|
* @param context - The format context to register tags in
|
|
42
43
|
*/
|
|
43
|
-
export function registerTagsIn(context:
|
|
44
|
-
context
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
export function registerTagsIn(context: FormatContext): void {
|
|
45
|
+
envelopeRegisterTagsIn(context);
|
|
46
|
+
|
|
47
|
+
context
|
|
48
|
+
.tags()
|
|
49
|
+
.setSummarizer(
|
|
50
|
+
BigInt(PROVENANCE_MARK.value),
|
|
51
|
+
(untaggedCbor: Cbor, _flat: boolean): SummarizerResult => {
|
|
52
|
+
try {
|
|
53
|
+
const mark = ProvenanceMark.fromUntaggedCbor(untaggedCbor);
|
|
54
|
+
return { ok: true, value: mark.toString() };
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, error: { type: "Custom", message: "invalid provenance mark" } };
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
);
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
// Simple tags context interface for registration
|
|
51
|
-
export interface TagsContext {
|
|
52
|
-
setSummarizer(tag: number, summarizer: (cbor: Cbor) => string): void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Global tags context (minimal implementation)
|
|
56
|
-
const globalTagsContext: TagsContext = {
|
|
57
|
-
setSummarizer(_tag: number, _summarizer: (cbor: Cbor) => string): void {
|
|
58
|
-
// Tag summarizers are handled by the envelope package's format context
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
62
|
// ============================================================================
|
|
63
63
|
// ProvenanceMark Envelope Support
|
|
64
64
|
// ============================================================================
|
|
@@ -66,43 +66,26 @@ const globalTagsContext: TagsContext = {
|
|
|
66
66
|
/**
|
|
67
67
|
* Convert a ProvenanceMark to an Envelope.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
69
|
+
* Delegates to ProvenanceMark.intoEnvelope() — single source of truth.
|
|
70
70
|
*
|
|
71
71
|
* @param mark - The provenance mark to convert
|
|
72
72
|
* @returns An envelope containing the mark
|
|
73
73
|
*/
|
|
74
74
|
export function provenanceMarkToEnvelope(mark: ProvenanceMark): Envelope {
|
|
75
|
-
return
|
|
75
|
+
return mark.intoEnvelope();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Extract a ProvenanceMark from an Envelope.
|
|
80
80
|
*
|
|
81
|
+
* Delegates to ProvenanceMark.fromEnvelope() — single source of truth.
|
|
82
|
+
*
|
|
81
83
|
* @param envelope - The envelope to extract from
|
|
82
84
|
* @returns The extracted provenance mark
|
|
83
85
|
* @throws ProvenanceMarkError if extraction fails
|
|
84
86
|
*/
|
|
85
87
|
export function provenanceMarkFromEnvelope(envelope: Envelope): ProvenanceMark {
|
|
86
|
-
|
|
87
|
-
// Use asByteString to extract the raw bytes, then decode
|
|
88
|
-
const bytes = envelope.asByteString();
|
|
89
|
-
if (bytes !== undefined) {
|
|
90
|
-
return ProvenanceMark.fromCborData(bytes);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Try extracting from subject if it's a node
|
|
94
|
-
const envCase = envelope.case();
|
|
95
|
-
if (envCase.type === "node") {
|
|
96
|
-
const subject = envCase.subject;
|
|
97
|
-
const subjectBytes = subject.asByteString();
|
|
98
|
-
if (subjectBytes !== undefined) {
|
|
99
|
-
return ProvenanceMark.fromCborData(subjectBytes);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
104
|
-
message: "Could not extract ProvenanceMark from envelope",
|
|
105
|
-
});
|
|
88
|
+
return ProvenanceMark.fromEnvelope(envelope);
|
|
106
89
|
}
|
|
107
90
|
|
|
108
91
|
// ============================================================================
|
|
@@ -112,121 +95,24 @@ export function provenanceMarkFromEnvelope(envelope: Envelope): ProvenanceMark {
|
|
|
112
95
|
/**
|
|
113
96
|
* Convert a ProvenanceMarkGenerator to an Envelope.
|
|
114
97
|
*
|
|
115
|
-
*
|
|
116
|
-
* - type: "provenance-generator"
|
|
117
|
-
* - res: The resolution
|
|
118
|
-
* - seed: The seed
|
|
119
|
-
* - next-seq: The next sequence number
|
|
120
|
-
* - rng-state: The RNG state
|
|
98
|
+
* Delegates to ProvenanceMarkGenerator.intoEnvelope() — single source of truth.
|
|
121
99
|
*
|
|
122
100
|
* @param generator - The generator to convert
|
|
123
101
|
* @returns An envelope containing the generator
|
|
124
102
|
*/
|
|
125
103
|
export function provenanceMarkGeneratorToEnvelope(generator: ProvenanceMarkGenerator): Envelope {
|
|
126
|
-
|
|
127
|
-
let envelope = Envelope.new(generator.chainId());
|
|
128
|
-
|
|
129
|
-
// Add type assertion (using addType() which uses IS_A KnownValue, like Rust's add_type())
|
|
130
|
-
envelope = envelope.addType("provenance-generator");
|
|
131
|
-
|
|
132
|
-
// Add resolution
|
|
133
|
-
envelope = envelope.addAssertion("res", resolutionToNumber(generator.res()));
|
|
134
|
-
|
|
135
|
-
// Add seed
|
|
136
|
-
envelope = envelope.addAssertion("seed", generator.seed().toBytes());
|
|
137
|
-
|
|
138
|
-
// Add next sequence number
|
|
139
|
-
envelope = envelope.addAssertion("next-seq", generator.nextSeq());
|
|
140
|
-
|
|
141
|
-
// Add RNG state
|
|
142
|
-
envelope = envelope.addAssertion("rng-state", generator.rngState().toBytes());
|
|
143
|
-
|
|
144
|
-
return envelope;
|
|
104
|
+
return generator.intoEnvelope();
|
|
145
105
|
}
|
|
146
106
|
|
|
147
|
-
// Type extension for envelope with extra methods
|
|
148
|
-
type EnvelopeExt = Envelope & {
|
|
149
|
-
asByteString(): Uint8Array | undefined;
|
|
150
|
-
hasType(t: string): boolean;
|
|
151
|
-
assertionsWithPredicate(p: string): Envelope[];
|
|
152
|
-
subject(): Envelope;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
107
|
/**
|
|
156
108
|
* Extract a ProvenanceMarkGenerator from an Envelope.
|
|
157
109
|
*
|
|
110
|
+
* Delegates to ProvenanceMarkGenerator.fromEnvelope() — single source of truth.
|
|
111
|
+
*
|
|
158
112
|
* @param envelope - The envelope to extract from
|
|
159
113
|
* @returns The extracted generator
|
|
160
114
|
* @throws ProvenanceMarkError if extraction fails
|
|
161
115
|
*/
|
|
162
116
|
export function provenanceMarkGeneratorFromEnvelope(envelope: Envelope): ProvenanceMarkGenerator {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Check type
|
|
166
|
-
if (!env.hasType("provenance-generator")) {
|
|
167
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
168
|
-
message: "Envelope is not a provenance-generator",
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Extract chain ID from subject
|
|
173
|
-
const subject = env.subject() as EnvelopeExt;
|
|
174
|
-
const chainId = subject.asByteString();
|
|
175
|
-
if (chainId === undefined) {
|
|
176
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
177
|
-
message: "Could not extract chain ID",
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Helper to extract assertion object value
|
|
182
|
-
const extractAssertion = (predicate: string): { cbor: Cbor; bytes: Uint8Array | undefined } => {
|
|
183
|
-
const assertions = env.assertionsWithPredicate(predicate);
|
|
184
|
-
if (assertions.length === 0) {
|
|
185
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
186
|
-
message: `Missing ${predicate} assertion`,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
const assertionCase = assertions[0].case();
|
|
190
|
-
if (assertionCase.type !== "assertion") {
|
|
191
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
192
|
-
message: `Invalid ${predicate} assertion`,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
const obj = assertionCase.assertion.object() as EnvelopeExt;
|
|
196
|
-
const objCase = obj.case();
|
|
197
|
-
if (objCase.type === "leaf") {
|
|
198
|
-
return { cbor: objCase.cbor, bytes: obj.asByteString() };
|
|
199
|
-
}
|
|
200
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
201
|
-
message: `Invalid ${predicate} value`,
|
|
202
|
-
});
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// Extract resolution
|
|
206
|
-
const resValue = extractAssertion("res");
|
|
207
|
-
const res = resolutionFromCbor(resValue.cbor);
|
|
208
|
-
|
|
209
|
-
// Extract seed
|
|
210
|
-
const seedValue = extractAssertion("seed");
|
|
211
|
-
if (seedValue.bytes === undefined) {
|
|
212
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
213
|
-
message: "Invalid seed data",
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
const seed = ProvenanceSeed.fromBytes(seedValue.bytes);
|
|
217
|
-
|
|
218
|
-
// Extract next-seq
|
|
219
|
-
const seqValue = extractAssertion("next-seq");
|
|
220
|
-
const nextSeq = Number(seqValue.cbor);
|
|
221
|
-
|
|
222
|
-
// Extract rng-state
|
|
223
|
-
const rngValue = extractAssertion("rng-state");
|
|
224
|
-
if (rngValue.bytes === undefined) {
|
|
225
|
-
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
|
226
|
-
message: "Invalid rng-state data",
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
const rngState = RngState.fromBytes(rngValue.bytes);
|
|
230
|
-
|
|
231
|
-
return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
|
|
117
|
+
return ProvenanceMarkGenerator.fromEnvelope(envelope);
|
|
232
118
|
}
|
package/src/index.ts
CHANGED
|
@@ -97,5 +97,7 @@ export {
|
|
|
97
97
|
provenanceMarkFromEnvelope,
|
|
98
98
|
provenanceMarkGeneratorToEnvelope,
|
|
99
99
|
provenanceMarkGeneratorFromEnvelope,
|
|
100
|
-
type TagsContext,
|
|
101
100
|
} from "./envelope.js";
|
|
101
|
+
|
|
102
|
+
// Re-export FormatContext for registerTagsIn callers
|
|
103
|
+
export { FormatContext } from "@bcts/envelope";
|
package/src/mark-info.ts
CHANGED
|
@@ -74,7 +74,7 @@ export class ProvenanceMarkInfo {
|
|
|
74
74
|
lines.push("---");
|
|
75
75
|
|
|
76
76
|
lines.push("");
|
|
77
|
-
lines.push(this._mark.date().toISOString());
|
|
77
|
+
lines.push(this._mark.date().toISOString().replace(".000Z", "Z"));
|
|
78
78
|
|
|
79
79
|
lines.push("");
|
|
80
80
|
lines.push(`#### ${this._ur.toString()}`);
|
package/src/mark.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import { Envelope } from "@bcts/envelope";
|
|
15
15
|
|
|
16
16
|
import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
|
|
17
|
+
import { validate as validateMarks } from "./validate.js";
|
|
18
|
+
import type { ValidationIssue, ValidationReport } from "./validate.js";
|
|
17
19
|
import {
|
|
18
20
|
type ProvenanceMarkResolution,
|
|
19
21
|
linkLength,
|
|
@@ -286,6 +288,42 @@ export class ProvenanceMark {
|
|
|
286
288
|
return prefix ? `\u{1F151} ${s}` : s;
|
|
287
289
|
}
|
|
288
290
|
|
|
291
|
+
/**
|
|
292
|
+
* A compact 8-letter identifier derived from the upper-case ByteWords
|
|
293
|
+
* identifier by taking the first and last letter of each ByteWords word
|
|
294
|
+
* (4 words x 2 letters = 8 letters).
|
|
295
|
+
*
|
|
296
|
+
* Example: "ABLE ACID ALSO APEX" -> "AEADAOAX"
|
|
297
|
+
* If prefix is true, prepends the provenance mark prefix character.
|
|
298
|
+
*/
|
|
299
|
+
bytewordsMinimalIdentifier(prefix: boolean): string {
|
|
300
|
+
const full = encodeBytewordsIdentifier(this._hash.slice(0, 4));
|
|
301
|
+
|
|
302
|
+
const words = full.split(/\s+/);
|
|
303
|
+
let out = "";
|
|
304
|
+
if (words.length === 4) {
|
|
305
|
+
for (const w of words) {
|
|
306
|
+
if (w.length === 0) continue;
|
|
307
|
+
out += w[0].toUpperCase();
|
|
308
|
+
out += w[w.length - 1].toUpperCase();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Conservative fallback: if the input wasn't in the expected
|
|
313
|
+
// space-separated 4-word format, remove whitespace and chunk the
|
|
314
|
+
// remaining letters.
|
|
315
|
+
if (out.length !== 8) {
|
|
316
|
+
out = "";
|
|
317
|
+
const compact = full.replace(/[^a-zA-Z]/g, "").toUpperCase();
|
|
318
|
+
for (let i = 0; i + 3 < compact.length; i += 4) {
|
|
319
|
+
out += compact[i];
|
|
320
|
+
out += compact[i + 3];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return prefix ? `\u{1F151} ${out}` : out;
|
|
325
|
+
}
|
|
326
|
+
|
|
289
327
|
/**
|
|
290
328
|
* Get the first four bytes of the hash as Bytemoji.
|
|
291
329
|
*/
|
|
@@ -309,30 +347,54 @@ export class ProvenanceMark {
|
|
|
309
347
|
|
|
310
348
|
/**
|
|
311
349
|
* Check if this mark precedes another mark, throwing on validation errors.
|
|
350
|
+
* Errors carry a structured `validationIssue` in their details, matching Rust's
|
|
351
|
+
* `Error::Validation(ValidationIssue)` pattern.
|
|
312
352
|
*/
|
|
313
353
|
precedesOpt(next: ProvenanceMark): void {
|
|
314
354
|
// `next` can't be a genesis
|
|
315
355
|
if (next._seq === 0) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
356
|
+
const issue: ValidationIssue = { type: "NonGenesisAtZero" };
|
|
357
|
+
throw new ProvenanceMarkError(
|
|
358
|
+
ProvenanceMarkErrorType.ValidationError,
|
|
359
|
+
"non-genesis mark at sequence 0",
|
|
360
|
+
{ validationIssue: issue },
|
|
361
|
+
);
|
|
319
362
|
}
|
|
320
363
|
if (arraysEqual(next._key, next._chainId)) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
364
|
+
const issue: ValidationIssue = { type: "InvalidGenesisKey" };
|
|
365
|
+
throw new ProvenanceMarkError(
|
|
366
|
+
ProvenanceMarkErrorType.ValidationError,
|
|
367
|
+
"genesis mark must have key equal to chain_id",
|
|
368
|
+
{ validationIssue: issue },
|
|
369
|
+
);
|
|
324
370
|
}
|
|
325
371
|
// `next` must have the next highest sequence number
|
|
326
372
|
if (this._seq !== next._seq - 1) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
373
|
+
const issue: ValidationIssue = {
|
|
374
|
+
type: "SequenceGap",
|
|
375
|
+
expected: this._seq + 1,
|
|
376
|
+
actual: next._seq,
|
|
377
|
+
};
|
|
378
|
+
throw new ProvenanceMarkError(
|
|
379
|
+
ProvenanceMarkErrorType.ValidationError,
|
|
380
|
+
`sequence gap: expected ${this._seq + 1}, got ${next._seq}`,
|
|
381
|
+
{ validationIssue: issue },
|
|
382
|
+
);
|
|
330
383
|
}
|
|
331
384
|
// `next` must have an equal or later date
|
|
332
385
|
if (this._date > next._date) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
386
|
+
const dateStr = this._date.toISOString().replace(".000Z", "Z");
|
|
387
|
+
const nextDateStr = next._date.toISOString().replace(".000Z", "Z");
|
|
388
|
+
const issue: ValidationIssue = {
|
|
389
|
+
type: "DateOrdering",
|
|
390
|
+
previous: dateStr,
|
|
391
|
+
next: nextDateStr,
|
|
392
|
+
};
|
|
393
|
+
throw new ProvenanceMarkError(
|
|
394
|
+
ProvenanceMarkErrorType.ValidationError,
|
|
395
|
+
`date ordering: ${dateStr} > ${nextDateStr}`,
|
|
396
|
+
{ validationIssue: issue },
|
|
397
|
+
);
|
|
336
398
|
}
|
|
337
399
|
// `next` must reveal the key that was used to generate this mark's hash
|
|
338
400
|
const expectedHash = ProvenanceMark.makeHash(
|
|
@@ -345,15 +407,16 @@ export class ProvenanceMark {
|
|
|
345
407
|
this._infoBytes,
|
|
346
408
|
);
|
|
347
409
|
if (!arraysEqual(this._hash, expectedHash)) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
expected:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
410
|
+
const issue: ValidationIssue = {
|
|
411
|
+
type: "HashMismatch",
|
|
412
|
+
expected: bytesToHex(expectedHash),
|
|
413
|
+
actual: bytesToHex(this._hash),
|
|
414
|
+
};
|
|
415
|
+
throw new ProvenanceMarkError(
|
|
416
|
+
ProvenanceMarkErrorType.ValidationError,
|
|
417
|
+
`hash mismatch: expected ${bytesToHex(expectedHash)}, got ${bytesToHex(this._hash)}`,
|
|
418
|
+
{ validationIssue: issue },
|
|
419
|
+
);
|
|
357
420
|
}
|
|
358
421
|
}
|
|
359
422
|
|
|
@@ -617,6 +680,20 @@ export class ProvenanceMark {
|
|
|
617
680
|
return new ProvenanceMark(res, key, hash, chainId, seqBytes, dateBytes, infoBytes, seq, date);
|
|
618
681
|
}
|
|
619
682
|
|
|
683
|
+
// ============================================================================
|
|
684
|
+
// Validation (delegate to ValidationReport)
|
|
685
|
+
// ============================================================================
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Validate a collection of provenance marks.
|
|
689
|
+
*
|
|
690
|
+
* Matches Rust: `ProvenanceMark::validate()` which delegates to
|
|
691
|
+
* `ValidationReport::validate()`.
|
|
692
|
+
*/
|
|
693
|
+
static validate(marks: ProvenanceMark[]): ValidationReport {
|
|
694
|
+
return validateMarks(marks);
|
|
695
|
+
}
|
|
696
|
+
|
|
620
697
|
// ============================================================================
|
|
621
698
|
// Envelope Support (EnvelopeEncodable)
|
|
622
699
|
// ============================================================================
|
|
@@ -624,37 +701,27 @@ export class ProvenanceMark {
|
|
|
624
701
|
/**
|
|
625
702
|
* Convert this provenance mark to a Gordian Envelope.
|
|
626
703
|
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
* Note: Use provenanceMarkToEnvelope() for a standalone function alternative.
|
|
704
|
+
* Creates a leaf envelope containing the tagged CBOR representation.
|
|
705
|
+
* Matches Rust: `Envelope::new(mark.to_cbor())` which creates a CBOR leaf.
|
|
630
706
|
*/
|
|
631
707
|
intoEnvelope(): Envelope {
|
|
632
|
-
return Envelope.
|
|
708
|
+
return Envelope.newLeaf(this.taggedCbor());
|
|
633
709
|
}
|
|
634
710
|
|
|
635
711
|
/**
|
|
636
712
|
* Extract a ProvenanceMark from a Gordian Envelope.
|
|
637
713
|
*
|
|
714
|
+
* Matches Rust: `envelope.subject().try_leaf()?.try_into()`
|
|
715
|
+
*
|
|
638
716
|
* @param envelope - The envelope to extract from
|
|
639
717
|
* @returns The extracted provenance mark
|
|
640
718
|
* @throws ProvenanceMarkError if extraction fails
|
|
641
719
|
*/
|
|
642
720
|
static fromEnvelope(envelope: Envelope): ProvenanceMark {
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return ProvenanceMark.fromCborData(bytes);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Try extracting from subject if it's a node
|
|
651
|
-
const envCase = envelope.case();
|
|
652
|
-
if (envCase.type === "node") {
|
|
653
|
-
const subject = envCase.subject;
|
|
654
|
-
const subjectBytes = subject.asByteString();
|
|
655
|
-
if (subjectBytes !== undefined) {
|
|
656
|
-
return ProvenanceMark.fromCborData(subjectBytes);
|
|
657
|
-
}
|
|
721
|
+
// Extract the CBOR leaf from the envelope subject, matching Rust's try_leaf()
|
|
722
|
+
const leaf = envelope.subject().asLeaf();
|
|
723
|
+
if (leaf !== undefined) {
|
|
724
|
+
return ProvenanceMark.fromTaggedCbor(leaf);
|
|
658
725
|
}
|
|
659
726
|
|
|
660
727
|
throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
|
package/src/validate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Ported from provenance-mark-rust/src/validate.rs
|
|
2
2
|
|
|
3
3
|
import type { ProvenanceMark } from "./mark.js";
|
|
4
|
+
import { ProvenanceMarkError } from "./error.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Format for validation report output.
|
|
@@ -257,23 +258,47 @@ export function formatReport(report: ValidationReport, format: ValidationReportF
|
|
|
257
258
|
*/
|
|
258
259
|
function reportToJSON(report: ValidationReport): unknown {
|
|
259
260
|
return {
|
|
260
|
-
marks: report.marks.map((m) => m.
|
|
261
|
+
marks: report.marks.map((m) => m.urString()),
|
|
261
262
|
chains: report.chains.map((chain) => ({
|
|
262
263
|
chain_id: hexEncode(chain.chainId),
|
|
263
264
|
has_genesis: chain.hasGenesis,
|
|
264
|
-
marks: chain.marks.map((m) => m.
|
|
265
|
+
marks: chain.marks.map((m) => m.urString()),
|
|
265
266
|
sequences: chain.sequences.map((seq) => ({
|
|
266
267
|
start_seq: seq.startSeq,
|
|
267
268
|
end_seq: seq.endSeq,
|
|
268
269
|
marks: seq.marks.map((fm) => ({
|
|
269
|
-
mark: fm.mark.
|
|
270
|
-
issues: fm.issues,
|
|
270
|
+
mark: fm.mark.urString(),
|
|
271
|
+
issues: fm.issues.map(issueToJSON),
|
|
271
272
|
})),
|
|
272
273
|
})),
|
|
273
274
|
})),
|
|
274
275
|
};
|
|
275
276
|
}
|
|
276
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Convert a ValidationIssue to JSON matching Rust's serde format.
|
|
280
|
+
*
|
|
281
|
+
* Rust uses `#[serde(tag = "type", content = "data")]` which wraps
|
|
282
|
+
* struct variant data in a `"data"` field. Unit variants have no
|
|
283
|
+
* `"data"` field.
|
|
284
|
+
*/
|
|
285
|
+
function issueToJSON(issue: ValidationIssue): unknown {
|
|
286
|
+
switch (issue.type) {
|
|
287
|
+
case "HashMismatch":
|
|
288
|
+
return { type: "HashMismatch", data: { expected: issue.expected, actual: issue.actual } };
|
|
289
|
+
case "SequenceGap":
|
|
290
|
+
return { type: "SequenceGap", data: { expected: issue.expected, actual: issue.actual } };
|
|
291
|
+
case "DateOrdering":
|
|
292
|
+
return { type: "DateOrdering", data: { previous: issue.previous, next: issue.next } };
|
|
293
|
+
case "KeyMismatch":
|
|
294
|
+
return { type: "KeyMismatch" };
|
|
295
|
+
case "NonGenesisAtZero":
|
|
296
|
+
return { type: "NonGenesisAtZero" };
|
|
297
|
+
case "InvalidGenesisKey":
|
|
298
|
+
return { type: "InvalidGenesisKey" };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
277
302
|
/**
|
|
278
303
|
* Build sequence bins for a chain.
|
|
279
304
|
*/
|
|
@@ -301,8 +326,14 @@ function buildSequenceBins(marks: ProvenanceMark[]): SequenceReport[] {
|
|
|
301
326
|
sequences.push(createSequenceReport(currentSequence));
|
|
302
327
|
}
|
|
303
328
|
|
|
304
|
-
//
|
|
305
|
-
|
|
329
|
+
// Extract structured issue directly from the error
|
|
330
|
+
// Matches Rust: Error::Validation(v) => v, _ => ValidationIssue::KeyMismatch
|
|
331
|
+
let issue: ValidationIssue;
|
|
332
|
+
if (e instanceof ProvenanceMarkError && e.details?.["validationIssue"] !== undefined) {
|
|
333
|
+
issue = e.details["validationIssue"] as ValidationIssue;
|
|
334
|
+
} else {
|
|
335
|
+
issue = { type: "KeyMismatch" }; // Fallback
|
|
336
|
+
}
|
|
306
337
|
|
|
307
338
|
// Start new sequence with this mark, flagged with the issue
|
|
308
339
|
currentSequence = [{ mark, issues: [issue] }];
|
|
@@ -318,53 +349,6 @@ function buildSequenceBins(marks: ProvenanceMark[]): SequenceReport[] {
|
|
|
318
349
|
return sequences;
|
|
319
350
|
}
|
|
320
351
|
|
|
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
352
|
/**
|
|
369
353
|
* Create a sequence report from flagged marks.
|
|
370
354
|
*/
|
|
@@ -379,10 +363,11 @@ function createSequenceReport(marks: FlaggedMark[]): SequenceReport {
|
|
|
379
363
|
*/
|
|
380
364
|
export function validate(marks: ProvenanceMark[]): ValidationReport {
|
|
381
365
|
// Deduplicate exact duplicates
|
|
366
|
+
// Matches Rust semantics: PartialEq compares (res, message())
|
|
382
367
|
const seen = new Set<string>();
|
|
383
368
|
const deduplicatedMarks: ProvenanceMark[] = [];
|
|
384
369
|
for (const mark of marks) {
|
|
385
|
-
const key = mark.
|
|
370
|
+
const key = `${mark.res()}:${hexEncode(mark.message())}`;
|
|
386
371
|
if (!seen.has(key)) {
|
|
387
372
|
seen.add(key);
|
|
388
373
|
deduplicatedMarks.push(mark);
|