@bcts/provenance-mark 1.0.0-alpha.11 → 1.0.0-alpha.13

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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Envelope support for Provenance Marks
3
+ *
4
+ * This module provides Gordian Envelope integration for ProvenanceMark and
5
+ * ProvenanceMarkGenerator, enabling them to be used with the bc-envelope
6
+ * ecosystem.
7
+ *
8
+ * Ported from provenance-mark-rust/src/mark.rs and generator.rs (envelope feature)
9
+ */
10
+
11
+ import { Envelope } from "@bcts/envelope";
12
+ import { type Cbor } from "@bcts/dcbor";
13
+ import { PROVENANCE_MARK } from "@bcts/tags";
14
+ import { ProvenanceMark } from "./mark.js";
15
+ 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
+ // ============================================================================
22
+ // Tag Registration
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Registers provenance mark tags in the global format context.
27
+ *
28
+ * This function sets up a summarizer for the PROVENANCE_MARK tag that displays
29
+ * provenance marks in a human-readable format.
30
+ */
31
+ export function registerTags(): void {
32
+ // In TypeScript, we don't have a global format context like Rust.
33
+ // Tag summarizers are typically handled at the envelope level.
34
+ // This function is provided for API parity.
35
+ registerTagsIn(globalTagsContext);
36
+ }
37
+
38
+ /**
39
+ * Registers provenance mark tags in a specific format context.
40
+ *
41
+ * @param context - The format context to register tags in
42
+ */
43
+ export function registerTagsIn(context: TagsContext): void {
44
+ context.setSummarizer(Number(PROVENANCE_MARK.value), (cborValue: Cbor) => {
45
+ const mark = ProvenanceMark.fromUntaggedCbor(cborValue);
46
+ return mark.toString();
47
+ });
48
+ }
49
+
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
+ // ============================================================================
63
+ // ProvenanceMark Envelope Support
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Convert a ProvenanceMark to an Envelope.
68
+ *
69
+ * The envelope contains the tagged CBOR representation of the mark.
70
+ *
71
+ * @param mark - The provenance mark to convert
72
+ * @returns An envelope containing the mark
73
+ */
74
+ export function provenanceMarkToEnvelope(mark: ProvenanceMark): Envelope {
75
+ return Envelope.new(mark.toCborData());
76
+ }
77
+
78
+ /**
79
+ * Extract a ProvenanceMark from an Envelope.
80
+ *
81
+ * @param envelope - The envelope to extract from
82
+ * @returns The extracted provenance mark
83
+ * @throws ProvenanceMarkError if extraction fails
84
+ */
85
+ export function provenanceMarkFromEnvelope(envelope: Envelope): ProvenanceMark {
86
+ // The envelope contains the CBOR-encoded bytes of the mark
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
+ });
106
+ }
107
+
108
+ // ============================================================================
109
+ // ProvenanceMarkGenerator Envelope Support
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Convert a ProvenanceMarkGenerator to an Envelope.
114
+ *
115
+ * The envelope contains structured assertions for all generator fields:
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
121
+ *
122
+ * @param generator - The generator to convert
123
+ * @returns An envelope containing the generator
124
+ */
125
+ export function provenanceMarkGeneratorToEnvelope(generator: ProvenanceMarkGenerator): Envelope {
126
+ // Create envelope with chain ID as subject
127
+ let envelope = Envelope.new(generator.chainId());
128
+
129
+ // Add type assertion
130
+ envelope = envelope.addAssertion("isA", "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;
145
+ }
146
+
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
+ /**
156
+ * Extract a ProvenanceMarkGenerator from an Envelope.
157
+ *
158
+ * @param envelope - The envelope to extract from
159
+ * @returns The extracted generator
160
+ * @throws ProvenanceMarkError if extraction fails
161
+ */
162
+ export function provenanceMarkGeneratorFromEnvelope(envelope: Envelope): ProvenanceMarkGenerator {
163
+ const env = envelope as EnvelopeExt;
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);
232
+ }
package/src/generator.ts CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  import { toBase64, fromBase64, bytesToHex } from "./utils.js";
4
4
  import { type Cbor } from "@bcts/dcbor";
5
+ import { Envelope } from "@bcts/envelope";
5
6
 
6
7
  import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
7
- import { type ProvenanceMarkResolution, linkLength } from "./resolution.js";
8
+ import {
9
+ type ProvenanceMarkResolution,
10
+ linkLength,
11
+ resolutionToNumber,
12
+ resolutionFromCbor,
13
+ } from "./resolution.js";
8
14
  import { ProvenanceSeed } from "./seed.js";
9
15
  import { RngState } from "./rng-state.js";
10
16
  import { sha256 } from "./crypto-utils.js";
@@ -179,4 +185,128 @@ export class ProvenanceMarkGenerator {
179
185
  const rngState = RngState.fromBytes(fromBase64(json["rngState"] as string));
180
186
  return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
181
187
  }
188
+
189
+ // ============================================================================
190
+ // Envelope Support (EnvelopeEncodable)
191
+ // ============================================================================
192
+
193
+ /**
194
+ * Convert this generator to a Gordian Envelope.
195
+ *
196
+ * The envelope contains structured assertions for all generator fields:
197
+ * - isA: "provenance-generator"
198
+ * - res: The resolution
199
+ * - seed: The seed
200
+ * - next-seq: The next sequence number
201
+ * - rng-state: The RNG state
202
+ *
203
+ * Note: Use provenanceMarkGeneratorToEnvelope() for a standalone function alternative.
204
+ */
205
+ intoEnvelope(): Envelope {
206
+ // Create envelope with chain ID as subject
207
+ let envelope = Envelope.new(this._chainId);
208
+
209
+ // Add type assertion
210
+ envelope = envelope.addAssertion("isA", "provenance-generator");
211
+
212
+ // Add resolution
213
+ envelope = envelope.addAssertion("res", resolutionToNumber(this._res));
214
+
215
+ // Add seed
216
+ envelope = envelope.addAssertion("seed", this._seed.toBytes());
217
+
218
+ // Add next sequence number
219
+ envelope = envelope.addAssertion("next-seq", this._nextSeq);
220
+
221
+ // Add RNG state
222
+ envelope = envelope.addAssertion("rng-state", this._rngState.toBytes());
223
+
224
+ return envelope;
225
+ }
226
+
227
+ /**
228
+ * Extract a ProvenanceMarkGenerator from a Gordian Envelope.
229
+ *
230
+ * @param envelope - The envelope to extract from
231
+ * @returns The extracted generator
232
+ * @throws ProvenanceMarkError if extraction fails
233
+ */
234
+ static fromEnvelope(envelope: Envelope): ProvenanceMarkGenerator {
235
+ type EnvelopeExt = Envelope & {
236
+ asByteString(): Uint8Array | undefined;
237
+ hasType(t: string): boolean;
238
+ assertionsWithPredicate(p: string): Envelope[];
239
+ subject(): Envelope;
240
+ };
241
+
242
+ const env = envelope as EnvelopeExt;
243
+
244
+ // Check type
245
+ if (!env.hasType("provenance-generator")) {
246
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
247
+ message: "Envelope is not a provenance-generator",
248
+ });
249
+ }
250
+
251
+ // Extract chain ID from subject
252
+ const subject = env.subject() as EnvelopeExt;
253
+ const chainId = subject.asByteString();
254
+ if (chainId === undefined) {
255
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
256
+ message: "Could not extract chain ID",
257
+ });
258
+ }
259
+
260
+ // Helper to extract assertion object value
261
+ const extractAssertion = (predicate: string): { cbor: Cbor; bytes: Uint8Array | undefined } => {
262
+ const assertions = env.assertionsWithPredicate(predicate);
263
+ if (assertions.length === 0) {
264
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
265
+ message: `Missing ${predicate} assertion`,
266
+ });
267
+ }
268
+ const assertionCase = assertions[0].case();
269
+ if (assertionCase.type !== "assertion") {
270
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
271
+ message: `Invalid ${predicate} assertion`,
272
+ });
273
+ }
274
+ const obj = assertionCase.assertion.object() as EnvelopeExt;
275
+ const objCase = obj.case();
276
+ if (objCase.type === "leaf") {
277
+ return { cbor: objCase.cbor, bytes: obj.asByteString() };
278
+ }
279
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
280
+ message: `Invalid ${predicate} value`,
281
+ });
282
+ };
283
+
284
+ // Extract resolution
285
+ const resValue = extractAssertion("res");
286
+ const res = resolutionFromCbor(resValue.cbor);
287
+
288
+ // Extract seed
289
+ const seedValue = extractAssertion("seed");
290
+ if (seedValue.bytes === undefined) {
291
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
292
+ message: "Invalid seed data",
293
+ });
294
+ }
295
+ const seed = ProvenanceSeed.fromBytes(seedValue.bytes);
296
+
297
+ // Extract next-seq
298
+ const seqValue = extractAssertion("next-seq");
299
+ const nextSeq = Number(seqValue.cbor);
300
+
301
+ // Extract rng-state
302
+ const rngValue = extractAssertion("rng-state");
303
+ if (rngValue.bytes === undefined) {
304
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
305
+ message: "Invalid rng-state data",
306
+ });
307
+ }
308
+ const rngState = RngState.fromBytes(rngValue.bytes);
309
+
310
+ return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
311
+ }
182
312
  }
package/src/index.ts CHANGED
@@ -88,3 +88,14 @@ export {
88
88
 
89
89
  // Mark Info
90
90
  export { ProvenanceMarkInfo } from "./mark-info.js";
91
+
92
+ // Envelope support
93
+ export {
94
+ registerTags,
95
+ registerTagsIn,
96
+ provenanceMarkToEnvelope,
97
+ provenanceMarkFromEnvelope,
98
+ provenanceMarkGeneratorToEnvelope,
99
+ provenanceMarkGeneratorFromEnvelope,
100
+ type TagsContext,
101
+ } from "./envelope.js";
package/src/mark.ts CHANGED
@@ -9,7 +9,9 @@ import {
9
9
  decodeBytewords,
10
10
  encodeBytewordsIdentifier,
11
11
  encodeBytemojisIdentifier,
12
+ UR,
12
13
  } from "@bcts/uniform-resources";
14
+ import { Envelope } from "@bcts/envelope";
13
15
 
14
16
  import { ProvenanceMarkError, ProvenanceMarkErrorType } from "./error.js";
15
17
  import {
@@ -403,7 +405,7 @@ export class ProvenanceMark {
403
405
  }
404
406
 
405
407
  /**
406
- * Encode for URL (minimal bytewords of CBOR).
408
+ * Encode for URL (minimal bytewords of tagged CBOR).
407
409
  */
408
410
  toUrlEncoding(): string {
409
411
  return encodeBytewords(this.toCborData(), BytewordsStyle.Minimal);
@@ -418,6 +420,27 @@ export class ProvenanceMark {
418
420
  return ProvenanceMark.fromTaggedCbor(cborValue);
419
421
  }
420
422
 
423
+ /**
424
+ * Get the UR string representation (e.g., "ur:provenance/...").
425
+ */
426
+ urString(): string {
427
+ const ur = UR.new("provenance", this.untaggedCbor());
428
+ return ur.string();
429
+ }
430
+
431
+ /**
432
+ * Create from a UR string.
433
+ */
434
+ static fromURString(urString: string): ProvenanceMark {
435
+ const ur = UR.fromURString(urString);
436
+ if (ur.urTypeStr() !== "provenance") {
437
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
438
+ message: `Expected UR type 'provenance', got '${ur.urTypeStr()}'`,
439
+ });
440
+ }
441
+ return ProvenanceMark.fromUntaggedCbor(ur.cbor());
442
+ }
443
+
421
444
  /**
422
445
  * Build a URL with this mark as a query parameter.
423
446
  */
@@ -518,19 +541,29 @@ export class ProvenanceMark {
518
541
 
519
542
  /**
520
543
  * Detailed debug representation.
544
+ * Matches Rust format exactly for parity.
521
545
  */
522
546
  toDebugString(): string {
547
+ // Format date without milliseconds to match Rust format
548
+ const dateStr = this._date.toISOString().replace(".000Z", "Z");
523
549
  const components = [
524
550
  `key: ${bytesToHex(this._key)}`,
525
551
  `hash: ${bytesToHex(this._hash)}`,
526
552
  `chainID: ${bytesToHex(this._chainId)}`,
527
553
  `seq: ${this._seq}`,
528
- `date: ${this._date.toISOString()}`,
554
+ `date: ${dateStr}`,
529
555
  ];
530
556
 
531
557
  const info = this.info();
532
558
  if (info !== undefined) {
533
- components.push(`info: ${JSON.stringify(info)}`);
559
+ // Format info as the underlying string value, matching Rust Debug format
560
+ const textValue = info.asText();
561
+ if (textValue !== undefined) {
562
+ components.push(`info: "${textValue}"`);
563
+ } else {
564
+ // For non-text values, use diagnostic format
565
+ components.push(`info: ${info.toDiagnostic()}`);
566
+ }
534
567
  }
535
568
 
536
569
  return `ProvenanceMark(${components.join(", ")})`;
@@ -583,6 +616,51 @@ export class ProvenanceMark {
583
616
 
584
617
  return new ProvenanceMark(res, key, hash, chainId, seqBytes, dateBytes, infoBytes, seq, date);
585
618
  }
619
+
620
+ // ============================================================================
621
+ // Envelope Support (EnvelopeEncodable)
622
+ // ============================================================================
623
+
624
+ /**
625
+ * Convert this provenance mark to a Gordian Envelope.
626
+ *
627
+ * The envelope contains the tagged CBOR representation of the mark.
628
+ *
629
+ * Note: Use provenanceMarkToEnvelope() for a standalone function alternative.
630
+ */
631
+ intoEnvelope(): Envelope {
632
+ return Envelope.new(this.toCborData());
633
+ }
634
+
635
+ /**
636
+ * Extract a ProvenanceMark from a Gordian Envelope.
637
+ *
638
+ * @param envelope - The envelope to extract from
639
+ * @returns The extracted provenance mark
640
+ * @throws ProvenanceMarkError if extraction fails
641
+ */
642
+ static fromEnvelope(envelope: Envelope): ProvenanceMark {
643
+ // The envelope contains the CBOR-encoded bytes of the mark
644
+ // Use asByteString to extract the raw bytes, then decode
645
+ const bytes = envelope.asByteString();
646
+ if (bytes !== undefined) {
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
+ }
658
+ }
659
+
660
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, undefined, {
661
+ message: "Could not extract ProvenanceMark from envelope",
662
+ });
663
+ }
586
664
  }
587
665
 
588
666
  /**