@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/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@bcts/provenance-mark",
3
- "version": "1.0.0-alpha.8",
3
+ "version": "1.0.0-beta.0",
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",
7
- "author": "Leonardo Custodio <leonardo@custodio.me>",
8
- "homepage": "https://github.com/leonardocustodio/bcts",
7
+ "author": "Parity Technologies <admin@parity.io> (https://parity.io)",
8
+ "homepage": "https://bcts.dev",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/leonardocustodio/bcts",
11
+ "url": "https://github.com/paritytech/bcts",
12
12
  "directory": "packages/provenance-mark"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/leonardocustodio/bcts/issues"
15
+ "url": "https://github.com/paritytech/bcts/issues"
16
16
  },
17
17
  "main": "dist/index.cjs",
18
18
  "module": "dist/index.mjs",
@@ -33,11 +33,10 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "build": "tsdown",
36
- "dev": "tsdown --watch",
37
36
  "test": "vitest run",
38
37
  "test:watch": "vitest",
39
- "lint": "eslint 'src/**/*.ts'",
40
- "lint:fix": "eslint 'src/**/*.ts' --fix",
38
+ "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
39
+ "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
41
40
  "typecheck": "tsc --noEmit",
42
41
  "clean": "rm -rf dist",
43
42
  "docs": "typedoc",
@@ -57,22 +56,23 @@
57
56
  "devDependencies": {
58
57
  "@bcts/eslint": "^0.1.0",
59
58
  "@bcts/tsconfig": "^0.1.0",
60
- "@eslint/js": "^9.39.1",
61
- "@typescript-eslint/eslint-plugin": "^8.49.0",
62
- "@typescript-eslint/parser": "^8.49.0",
63
- "eslint": "^9.39.1",
59
+ "@eslint/js": "^10.0.1",
60
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
61
+ "@typescript-eslint/parser": "^8.59.0",
62
+ "eslint": "^10.2.1",
64
63
  "ts-node": "^10.9.2",
65
- "tsdown": "^0.17.2",
66
- "typedoc": "^0.28.15",
67
- "typescript": "^5.9.3",
68
- "vitest": "^3.2.4"
64
+ "tsdown": "^0.21.0",
65
+ "typedoc": "^0.28.19",
66
+ "typescript": "^6.0.3",
67
+ "vitest": "^4.1.5"
69
68
  },
70
69
  "dependencies": {
71
- "@bcts/dcbor": "^1.0.0-alpha.8",
72
- "@bcts/rand": "^1.0.0-alpha.8",
73
- "@bcts/tags": "^1.0.0-alpha.8",
74
- "@bcts/uniform-resources": "^1.0.0-alpha.8",
75
- "@noble/ciphers": "^1.3.0",
76
- "@noble/hashes": "^1.8.0"
70
+ "@bcts/dcbor": "^1.0.0-beta.0",
71
+ "@bcts/envelope": "^1.0.0-beta.0",
72
+ "@bcts/rand": "^1.0.0-beta.0",
73
+ "@bcts/tags": "^1.0.0-beta.0",
74
+ "@bcts/uniform-resources": "^1.0.0-beta.0",
75
+ "@noble/ciphers": "^2.2.0",
76
+ "@noble/hashes": "^2.2.0"
77
77
  }
78
78
  }
@@ -1,8 +1,14 @@
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/crypto_utils.rs
2
8
 
3
- import { sha256 as sha256Hash } from "@noble/hashes/sha256";
4
- import { hkdf } from "@noble/hashes/hkdf";
5
- import { chacha20 } from "@noble/ciphers/chacha";
9
+ import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
10
+ import { hkdf } from "@noble/hashes/hkdf.js";
11
+ import { chacha20 } from "@noble/ciphers/chacha.js";
6
12
 
7
13
  export const SHA256_SIZE = 32;
8
14
 
package/src/date.ts CHANGED
@@ -1,3 +1,9 @@
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/date.rs
2
8
 
3
9
  import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
@@ -214,3 +220,39 @@ export function dateToDateString(date: Date): string {
214
220
  const day = date.getUTCDate().toString().padStart(2, "0");
215
221
  return `${year}-${month}-${day}`;
216
222
  }
223
+
224
+ /**
225
+ * Renders a `Date` the way Rust's `dcbor::Date::Display` does
226
+ * (`bc-dcbor-rust/src/date.rs:485-492`):
227
+ *
228
+ * - When the UTC time is exactly `00:00:00` (subsecond precision is
229
+ * ignored — Rust's check is `hour == 0 && minute == 0 && second == 0`,
230
+ * matching `chrono::SecondsFormat::Secs`), emit just `YYYY-MM-DD`.
231
+ * - Otherwise emit RFC 3339 with second precision (no fractional
232
+ * seconds), e.g. `2023-02-08T15:30:45Z`.
233
+ *
234
+ * This is the canonical "Rust string" for dates across the
235
+ * provenance-mark public surface — `mark.toDebugString`,
236
+ * `mark.precedesOpt` `DateOrdering` issue, `markdownSummary`, and the
237
+ * validation report's `DateOrdering` payload all use it. Centralising
238
+ * here keeps every call site in lockstep with the Rust output.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * dateToDisplay(new Date("2023-06-20T00:00:00Z")); // "2023-06-20"
243
+ * dateToDisplay(new Date("2023-06-20T15:30:45Z")); // "2023-06-20T15:30:45Z"
244
+ * dateToDisplay(new Date("2023-06-20T15:30:45.123Z")); // "2023-06-20T15:30:45Z"
245
+ * ```
246
+ */
247
+ export function dateToDisplay(date: Date): string {
248
+ const hasTime =
249
+ date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0;
250
+ if (!hasTime) {
251
+ // Midnight (subsecond precision ignored) — show only the date.
252
+ return dateToDateString(date);
253
+ }
254
+ // Full RFC 3339, second precision. JS `toISOString()` always emits
255
+ // millisecond precision (`.NNNZ`); strip the fractional component to
256
+ // match Rust's `SecondsFormat::Secs`.
257
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
258
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
6
+ * Envelope support for Provenance Marks
7
+ *
8
+ * This module provides Gordian Envelope integration for ProvenanceMark and
9
+ * ProvenanceMarkGenerator, enabling them to be used with the bc-envelope
10
+ * ecosystem.
11
+ *
12
+ * Ported from provenance-mark-rust/src/mark.rs and generator.rs (envelope feature)
13
+ */
14
+
15
+ import {
16
+ type Envelope,
17
+ type FormatContext,
18
+ withFormatContextMut,
19
+ registerTagsIn as envelopeRegisterTagsIn,
20
+ } from "@bcts/envelope";
21
+ import { type Cbor, type SummarizerResult } from "@bcts/dcbor";
22
+ import { PROVENANCE_MARK } from "@bcts/tags";
23
+ import { ProvenanceMark } from "./mark.js";
24
+ import { ProvenanceMarkGenerator } from "./generator.js";
25
+
26
+ // ============================================================================
27
+ // Tag Registration
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Registers provenance mark tags in the global format context.
32
+ *
33
+ * Matches Rust: register_tags()
34
+ */
35
+ export function registerTags(): void {
36
+ withFormatContextMut((context) => {
37
+ registerTagsIn(context);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Registers provenance mark tags in a specific format context.
43
+ *
44
+ * Matches Rust: register_tags_in()
45
+ *
46
+ * @param context - The format context to register tags in
47
+ */
48
+ export function registerTagsIn(context: FormatContext): void {
49
+ envelopeRegisterTagsIn(context);
50
+
51
+ context
52
+ .tags()
53
+ .setSummarizer(
54
+ BigInt(PROVENANCE_MARK.value),
55
+ (untaggedCbor: Cbor, _flat: boolean): SummarizerResult => {
56
+ try {
57
+ const mark = ProvenanceMark.fromUntaggedCbor(untaggedCbor);
58
+ return { ok: true, value: mark.toString() };
59
+ } catch {
60
+ return { ok: false, error: { type: "Custom", message: "invalid provenance mark" } };
61
+ }
62
+ },
63
+ );
64
+ }
65
+
66
+ // ============================================================================
67
+ // ProvenanceMark Envelope Support
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Convert a ProvenanceMark to an Envelope.
72
+ *
73
+ * Delegates to ProvenanceMark.intoEnvelope() — single source of truth.
74
+ *
75
+ * @param mark - The provenance mark to convert
76
+ * @returns An envelope containing the mark
77
+ */
78
+ export function provenanceMarkToEnvelope(mark: ProvenanceMark): Envelope {
79
+ return mark.intoEnvelope();
80
+ }
81
+
82
+ /**
83
+ * Extract a ProvenanceMark from an Envelope.
84
+ *
85
+ * Delegates to ProvenanceMark.fromEnvelope() — single source of truth.
86
+ *
87
+ * @param envelope - The envelope to extract from
88
+ * @returns The extracted provenance mark
89
+ * @throws ProvenanceMarkError if extraction fails
90
+ */
91
+ export function provenanceMarkFromEnvelope(envelope: Envelope): ProvenanceMark {
92
+ return ProvenanceMark.fromEnvelope(envelope);
93
+ }
94
+
95
+ // ============================================================================
96
+ // ProvenanceMarkGenerator Envelope Support
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Convert a ProvenanceMarkGenerator to an Envelope.
101
+ *
102
+ * Delegates to ProvenanceMarkGenerator.intoEnvelope() — single source of truth.
103
+ *
104
+ * @param generator - The generator to convert
105
+ * @returns An envelope containing the generator
106
+ */
107
+ export function provenanceMarkGeneratorToEnvelope(generator: ProvenanceMarkGenerator): Envelope {
108
+ return generator.intoEnvelope();
109
+ }
110
+
111
+ /**
112
+ * Extract a ProvenanceMarkGenerator from an Envelope.
113
+ *
114
+ * Delegates to ProvenanceMarkGenerator.fromEnvelope() — single source of truth.
115
+ *
116
+ * @param envelope - The envelope to extract from
117
+ * @returns The extracted generator
118
+ * @throws ProvenanceMarkError if extraction fails
119
+ */
120
+ export function provenanceMarkGeneratorFromEnvelope(envelope: Envelope): ProvenanceMarkGenerator {
121
+ return ProvenanceMarkGenerator.fromEnvelope(envelope);
122
+ }
package/src/error.ts CHANGED
@@ -1,3 +1,9 @@
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/error.rs
2
8
 
3
9
  /**
@@ -50,6 +56,19 @@ export enum ProvenanceMarkErrorType {
50
56
  IntegerConversionError = "IntegerConversionError",
51
57
  /** Validation error */
52
58
  ValidationError = "ValidationError",
59
+ /**
60
+ * Envelope serialization/deserialization error.
61
+ *
62
+ * Mirrors Rust `Error::Envelope(...)`
63
+ * (`provenance-mark-rust/src/error.rs`). The Rust enum surfaces
64
+ * envelope-format failures as their own variant; in earlier
65
+ * revisions of this port they collapsed into `CborError`. Both
66
+ * shapes are still emitted in practice (CBOR errors during envelope
67
+ * round-trip stay as `CborError`); this variant exists for the
68
+ * structural-level mismatches the Rust port tags as
69
+ * `Error::Envelope`.
70
+ */
71
+ EnvelopeError = "EnvelopeError",
53
72
  }
54
73
 
55
74
  /**
@@ -129,6 +148,8 @@ export class ProvenanceMarkError extends Error {
129
148
  return `integer conversion error: ${d("message")}`;
130
149
  case ProvenanceMarkErrorType.ValidationError:
131
150
  return `validation error: ${d("message")}`;
151
+ case ProvenanceMarkErrorType.EnvelopeError:
152
+ return `envelope error: ${d("message")}`;
132
153
  default:
133
154
  return type;
134
155
  }
package/src/generator.ts CHANGED
@@ -1,10 +1,22 @@
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/generator.rs
2
8
 
3
9
  import { toBase64, fromBase64, bytesToHex } from "./utils.js";
4
10
  import { type Cbor } from "@bcts/dcbor";
11
+ import { Envelope } from "@bcts/envelope";
5
12
 
6
13
  import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
7
- import { type ProvenanceMarkResolution, linkLength } from "./resolution.js";
14
+ import {
15
+ type ProvenanceMarkResolution,
16
+ linkLength,
17
+ resolutionToNumber,
18
+ resolutionFromCbor,
19
+ } from "./resolution.js";
8
20
  import { ProvenanceSeed } from "./seed.js";
9
21
  import { RngState } from "./rng-state.js";
10
22
  import { sha256 } from "./crypto-utils.js";
@@ -150,9 +162,24 @@ export class ProvenanceMarkGenerator {
150
162
 
151
163
  /**
152
164
  * String representation.
165
+ *
166
+ * Mirrors Rust `Display for ProvenanceMarkGenerator`
167
+ * (`provenance-mark-rust/src/generator.rs:135-147`):
168
+ *
169
+ * ```rust
170
+ * write!(f, "ProvenanceMarkGenerator(chainID: {}, res: {}, seed: {}, nextSeq: {}, rngState: {:?})",
171
+ * hex::encode(&self.chain_id), self.res, self.seed.hex(), self.next_seq, self.rng_state)
172
+ * ```
173
+ *
174
+ * The `rngState` field uses Rust's `{:?}` (Debug) format, which on a
175
+ * `RngState([u8; 32])` tuple struct produces `RngState([n0, n1, ...])`
176
+ * with each byte rendered as a decimal integer. Earlier revisions of
177
+ * this port omitted `rngState` entirely from `toString()`, so the
178
+ * output diverged from Rust's `Display`.
153
179
  */
154
180
  toString(): string {
155
- return `ProvenanceMarkGenerator(chainID: ${bytesToHex(this._chainId)}, res: ${this._res}, seed: ${this._seed.hex()}, nextSeq: ${this._nextSeq})`;
181
+ const rngBytes = Array.from(this._rngState.toBytes()).join(", ");
182
+ return `ProvenanceMarkGenerator(chainID: ${bytesToHex(this._chainId)}, res: ${this._res}, seed: ${this._seed.hex()}, nextSeq: ${this._nextSeq}, rngState: RngState([${rngBytes}]))`;
156
183
  }
157
184
 
158
185
  /**
@@ -179,4 +206,128 @@ export class ProvenanceMarkGenerator {
179
206
  const rngState = RngState.fromBytes(fromBase64(json["rngState"] as string));
180
207
  return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
181
208
  }
209
+
210
+ // ============================================================================
211
+ // Envelope Support (EnvelopeEncodable)
212
+ // ============================================================================
213
+
214
+ /**
215
+ * Convert this generator to a Gordian Envelope.
216
+ *
217
+ * The envelope contains structured assertions for all generator fields:
218
+ * - isA: "provenance-generator"
219
+ * - res: The resolution
220
+ * - seed: The seed
221
+ * - next-seq: The next sequence number
222
+ * - rng-state: The RNG state
223
+ *
224
+ * Note: Use provenanceMarkGeneratorToEnvelope() for a standalone function alternative.
225
+ */
226
+ intoEnvelope(): Envelope {
227
+ // Create envelope with chain ID as subject
228
+ let envelope = Envelope.new(this._chainId);
229
+
230
+ // Add type assertion (using addType() which uses IS_A KnownValue, like Rust's add_type())
231
+ envelope = envelope.addType("provenance-generator");
232
+
233
+ // Add resolution
234
+ envelope = envelope.addAssertion("res", resolutionToNumber(this._res));
235
+
236
+ // Add seed
237
+ envelope = envelope.addAssertion("seed", this._seed.toBytes());
238
+
239
+ // Add next sequence number
240
+ envelope = envelope.addAssertion("next-seq", this._nextSeq);
241
+
242
+ // Add RNG state
243
+ envelope = envelope.addAssertion("rng-state", this._rngState.toBytes());
244
+
245
+ return envelope;
246
+ }
247
+
248
+ /**
249
+ * Extract a ProvenanceMarkGenerator from a Gordian Envelope.
250
+ *
251
+ * @param envelope - The envelope to extract from
252
+ * @returns The extracted generator
253
+ * @throws ProvenanceMarkError if extraction fails
254
+ */
255
+ static fromEnvelope(envelope: Envelope): ProvenanceMarkGenerator {
256
+ type EnvelopeExt = Envelope & {
257
+ asByteString(): Uint8Array | undefined;
258
+ hasType(t: string): boolean;
259
+ assertionsWithPredicate(p: string): Envelope[];
260
+ subject(): Envelope;
261
+ };
262
+
263
+ const env = envelope as EnvelopeExt;
264
+
265
+ // Check type
266
+ if (!env.hasType("provenance-generator")) {
267
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
268
+ message: "Envelope is not a provenance-generator",
269
+ });
270
+ }
271
+
272
+ // Extract chain ID from subject
273
+ const subject = env.subject() as EnvelopeExt;
274
+ const chainId = subject.asByteString();
275
+ if (chainId === undefined) {
276
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
277
+ message: "Could not extract chain ID",
278
+ });
279
+ }
280
+
281
+ // Helper to extract assertion object value
282
+ const extractAssertion = (predicate: string): { cbor: Cbor; bytes: Uint8Array | undefined } => {
283
+ const assertions = env.assertionsWithPredicate(predicate);
284
+ if (assertions.length === 0) {
285
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
286
+ message: `Missing ${predicate} assertion`,
287
+ });
288
+ }
289
+ const assertionCase = assertions[0].case();
290
+ if (assertionCase.type !== "assertion") {
291
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
292
+ message: `Invalid ${predicate} assertion`,
293
+ });
294
+ }
295
+ const obj = assertionCase.assertion.object() as EnvelopeExt;
296
+ const objCase = obj.case();
297
+ if (objCase.type === "leaf") {
298
+ return { cbor: objCase.cbor, bytes: obj.asByteString() };
299
+ }
300
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
301
+ message: `Invalid ${predicate} value`,
302
+ });
303
+ };
304
+
305
+ // Extract resolution
306
+ const resValue = extractAssertion("res");
307
+ const res = resolutionFromCbor(resValue.cbor);
308
+
309
+ // Extract seed
310
+ const seedValue = extractAssertion("seed");
311
+ if (seedValue.bytes === undefined) {
312
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
313
+ message: "Invalid seed data",
314
+ });
315
+ }
316
+ const seed = ProvenanceSeed.fromBytes(seedValue.bytes);
317
+
318
+ // Extract next-seq
319
+ const seqValue = extractAssertion("next-seq");
320
+ const nextSeq = Number(seqValue.cbor);
321
+
322
+ // Extract rng-state
323
+ const rngValue = extractAssertion("rng-state");
324
+ if (rngValue.bytes === undefined) {
325
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
326
+ message: "Invalid rng-state data",
327
+ });
328
+ }
329
+ const rngState = RngState.fromBytes(rngValue.bytes);
330
+
331
+ return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
332
+ }
182
333
  }
package/src/index.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  // Ported from provenance-mark-rust
2
8
 
3
9
  // Error types
@@ -44,8 +50,13 @@ export {
44
50
  dateToIso8601,
45
51
  dateFromIso8601,
46
52
  dateToDateString,
53
+ dateToDisplay,
47
54
  } from "./date.js";
48
55
 
56
+ // User-facing parsers (mirroring Rust `util::parse_seed` /
57
+ // `util::parse_date`).
58
+ export { parseSeed, parseDate } from "./utils.js";
59
+
49
60
  // Crypto utilities
50
61
  export {
51
62
  SHA256_SIZE,
@@ -88,3 +99,16 @@ export {
88
99
 
89
100
  // Mark Info
90
101
  export { ProvenanceMarkInfo } from "./mark-info.js";
102
+
103
+ // Envelope support
104
+ export {
105
+ registerTags,
106
+ registerTagsIn,
107
+ provenanceMarkToEnvelope,
108
+ provenanceMarkFromEnvelope,
109
+ provenanceMarkGeneratorToEnvelope,
110
+ provenanceMarkGeneratorFromEnvelope,
111
+ } from "./envelope.js";
112
+
113
+ // Re-export FormatContext for registerTagsIn callers
114
+ export { FormatContext } from "@bcts/envelope";
package/src/mark-info.ts CHANGED
@@ -1,10 +1,15 @@
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/mark_info.rs
2
8
 
3
- import { UR } from "@bcts/uniform-resources";
4
- import { decodeCbor, cborData } from "@bcts/dcbor";
5
- import { PROVENANCE_MARK } from "@bcts/tags";
9
+ import type { UR } from "@bcts/uniform-resources";
6
10
 
7
11
  import { ProvenanceMark } from "./mark.js";
12
+ import { dateToDisplay } from "./date.js";
8
13
 
9
14
  /**
10
15
  * Wrapper for a provenance mark with additional display information.
@@ -32,16 +37,21 @@ export class ProvenanceMarkInfo {
32
37
 
33
38
  /**
34
39
  * Create a new ProvenanceMarkInfo from a mark.
40
+ *
41
+ * Mirrors Rust `ProvenanceMarkInfo::new`
42
+ * (`provenance-mark-rust/src/mark_info.rs`), which calls
43
+ * `mark.ur()` — i.e. the `UREncodable` implementation, whose
44
+ * payload is the **untagged** CBOR with type `"provenance"`. Earlier
45
+ * revisions of this port called `decodeCbor(mark.toCborData())` and
46
+ * wrapped the resulting *tagged* CBOR in `UR.new("provenance", ...)`,
47
+ * which prepended the CBOR tag to the UR bytewords and broke
48
+ * cross-impl interop (UR strings produced by Rust would not parse,
49
+ * and vice versa).
35
50
  */
36
51
  static new(mark: ProvenanceMark, comment = ""): ProvenanceMarkInfo {
37
- const tagName = PROVENANCE_MARK.name;
38
- if (tagName === undefined) {
39
- throw new Error("PROVENANCE_MARK tag has no name");
40
- }
41
- const cborValue = decodeCbor(mark.toCborData());
42
- const ur = UR.new(tagName, cborValue);
43
- const bytewords = mark.bytewordsIdentifier(true);
44
- const bytemoji = mark.bytemojiIdentifier(true);
52
+ const ur = mark.ur();
53
+ const bytewords = mark.idBytewords(4, true);
54
+ const bytemoji = mark.idBytemoji(4, true);
45
55
  return new ProvenanceMarkInfo(mark, ur, bytewords, bytemoji, comment);
46
56
  }
47
57
 
@@ -67,6 +77,10 @@ export class ProvenanceMarkInfo {
67
77
 
68
78
  /**
69
79
  * Generate a markdown summary of the mark.
80
+ *
81
+ * Date rendering uses {@link dateToDisplay} so midnight-UTC dates
82
+ * appear as `YYYY-MM-DD` (matching Rust `format!("{}",
83
+ * self.mark.date())`), not as `YYYY-MM-DDT00:00:00Z`.
70
84
  */
71
85
  markdownSummary(): string {
72
86
  const lines: string[] = [];
@@ -74,7 +88,7 @@ export class ProvenanceMarkInfo {
74
88
  lines.push("---");
75
89
 
76
90
  lines.push("");
77
- lines.push(this._mark.date().toISOString());
91
+ lines.push(dateToDisplay(this._mark.date()));
78
92
 
79
93
  lines.push("");
80
94
  lines.push(`#### ${this._ur.toString()}`);
@@ -95,29 +109,36 @@ export class ProvenanceMarkInfo {
95
109
  }
96
110
 
97
111
  /**
98
- * JSON serialization.
112
+ * JSON serialization. Field order mirrors Rust's `#[derive(Serialize)]`
113
+ * on `ProvenanceMarkInfo` (provenance-mark-rust/src/mark_info.rs):
114
+ * `ur, bytewords, bytemoji, [comment,] mark` — `comment` (when present)
115
+ * comes BEFORE `mark`. Rust uses `skip_serializing_if = "String::is_empty"`,
116
+ * matched here by the `if (...length > 0)` guard.
99
117
  */
100
118
  toJSON(): Record<string, unknown> {
101
119
  const result: Record<string, unknown> = {
102
120
  ur: this._ur.toString(),
103
121
  bytewords: this._bytewords,
104
122
  bytemoji: this._bytemoji,
105
- mark: this._mark.toJSON(),
106
123
  };
107
124
  if (this._comment.length > 0) {
108
125
  result["comment"] = this._comment;
109
126
  }
127
+ result["mark"] = this._mark.toJSON();
110
128
  return result;
111
129
  }
112
130
 
113
131
  /**
114
132
  * Create from JSON object.
133
+ *
134
+ * Decodes the UR string through {@link ProvenanceMark.fromURString},
135
+ * which correctly handles the **untagged** CBOR payload that
136
+ * `mark.ur()` produces — symmetric with the constructor.
115
137
  */
116
138
  static fromJSON(json: Record<string, unknown>): ProvenanceMarkInfo {
117
139
  const urString = json["ur"] as string;
118
- const ur = UR.fromURString(urString);
119
- const cborBytes = cborData(ur.cbor());
120
- const mark = ProvenanceMark.fromCborData(cborBytes);
140
+ const mark = ProvenanceMark.fromURString(urString);
141
+ const ur = mark.ur();
121
142
  const bytewords = json["bytewords"] as string;
122
143
  const bytemoji = json["bytemoji"] as string;
123
144
  const comment = typeof json["comment"] === "string" ? json["comment"] : "";