@bcts/envelope 1.0.0-alpha.5

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.
Files changed (44) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +23 -0
  3. package/dist/index.cjs +2646 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +978 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +978 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.iife.js +2644 -0
  10. package/dist/index.iife.js.map +1 -0
  11. package/dist/index.mjs +2552 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +85 -0
  14. package/src/base/assertion.ts +179 -0
  15. package/src/base/assertions.ts +304 -0
  16. package/src/base/cbor.ts +122 -0
  17. package/src/base/digest.ts +204 -0
  18. package/src/base/elide.ts +526 -0
  19. package/src/base/envelope-decodable.ts +229 -0
  20. package/src/base/envelope-encodable.ts +71 -0
  21. package/src/base/envelope.ts +790 -0
  22. package/src/base/error.ts +421 -0
  23. package/src/base/index.ts +56 -0
  24. package/src/base/leaf.ts +226 -0
  25. package/src/base/queries.ts +374 -0
  26. package/src/base/walk.ts +241 -0
  27. package/src/base/wrap.ts +72 -0
  28. package/src/extension/attachment.ts +369 -0
  29. package/src/extension/compress.ts +293 -0
  30. package/src/extension/encrypt.ts +379 -0
  31. package/src/extension/expression.ts +404 -0
  32. package/src/extension/index.ts +72 -0
  33. package/src/extension/proof.ts +276 -0
  34. package/src/extension/recipient.ts +557 -0
  35. package/src/extension/salt.ts +223 -0
  36. package/src/extension/signature.ts +463 -0
  37. package/src/extension/types.ts +222 -0
  38. package/src/format/diagnostic.ts +116 -0
  39. package/src/format/hex.ts +25 -0
  40. package/src/format/index.ts +13 -0
  41. package/src/format/tree.ts +168 -0
  42. package/src/index.ts +32 -0
  43. package/src/utils/index.ts +8 -0
  44. package/src/utils/string.ts +48 -0
@@ -0,0 +1,790 @@
1
+ import { Digest, type DigestProvider } from "./digest";
2
+ import { Assertion } from "./assertion";
3
+ import { EnvelopeError } from "./error";
4
+ import type { EnvelopeEncodableValue } from "./envelope-encodable";
5
+ import { KnownValue } from "@bcts/known-values";
6
+ import type { Cbor } from "@bcts/dcbor";
7
+ import {
8
+ cbor,
9
+ cborData,
10
+ toTaggedValue,
11
+ TAG_ENCODED_CBOR,
12
+ MajorType,
13
+ asByteString,
14
+ asCborArray,
15
+ asCborMap,
16
+ asTaggedValue,
17
+ tryExpectedTaggedValue,
18
+ } from "@bcts/dcbor";
19
+ import { ENVELOPE, LEAF, ENCRYPTED, COMPRESSED } from "@bcts/components";
20
+
21
+ /// Import tag values from the tags registry
22
+ /// These match the Rust reference implementation in bc-tags-rust
23
+ const TAG_ENVELOPE = ENVELOPE.value;
24
+ const TAG_LEAF = LEAF.value;
25
+ const TAG_ENCRYPTED = ENCRYPTED.value;
26
+ const TAG_COMPRESSED = COMPRESSED.value;
27
+
28
+ /// The core structural variants of a Gordian Envelope.
29
+ ///
30
+ /// Each variant represents a different structural form that an
31
+ /// envelope can take, as defined in the Gordian Envelope IETF Internet Draft.
32
+ /// The different cases provide different capabilities and serve different
33
+ /// purposes in the envelope ecosystem.
34
+ ///
35
+ /// The `EnvelopeCase` is the internal representation of an envelope's
36
+ /// structure. While each case has unique properties, they all maintain a digest
37
+ /// that ensures the integrity of the envelope.
38
+ ///
39
+ /// It is advised to use the other Envelope APIs for most uses. Please see the
40
+ /// queries module for more information on how to interact with envelopes.
41
+ export type EnvelopeCase =
42
+ | {
43
+ type: "node";
44
+ /// The subject of the node
45
+ subject: Envelope;
46
+ /// The assertions attached to the subject
47
+ assertions: Envelope[];
48
+ /// The digest of the node
49
+ digest: Digest;
50
+ }
51
+ | {
52
+ type: "leaf";
53
+ /// The CBOR value contained in the leaf
54
+ cbor: Cbor;
55
+ /// The digest of the leaf
56
+ digest: Digest;
57
+ }
58
+ | {
59
+ type: "wrapped";
60
+ /// The envelope being wrapped
61
+ envelope: Envelope;
62
+ /// The digest of the wrapped envelope
63
+ digest: Digest;
64
+ }
65
+ | {
66
+ type: "assertion";
67
+ /// The assertion
68
+ assertion: Assertion;
69
+ }
70
+ | {
71
+ type: "elided";
72
+ /// The digest of the elided content
73
+ digest: Digest;
74
+ }
75
+ | {
76
+ type: "knownValue";
77
+ /// The known value instance
78
+ value: KnownValue;
79
+ /// The digest of the known value
80
+ digest: Digest;
81
+ }
82
+ | {
83
+ type: "encrypted";
84
+ /// The encrypted message
85
+ message: EncryptedMessage;
86
+ }
87
+ | {
88
+ type: "compressed";
89
+ /// The compressed data
90
+ value: Compressed;
91
+ };
92
+
93
+ // Import types from extension modules (will be available at runtime)
94
+ import { Compressed } from "../extension/compress";
95
+ import { EncryptedMessage } from "../extension/encrypt";
96
+
97
+ /// A flexible container for structured data with built-in integrity
98
+ /// verification.
99
+ ///
100
+ /// Gordian Envelope is the primary data structure of this library. It provides a
101
+ /// way to encapsulate and organize data with cryptographic integrity, privacy
102
+ /// features, and selective disclosure capabilities.
103
+ ///
104
+ /// Key characteristics of envelopes:
105
+ ///
106
+ /// - **Immutability**: Envelopes are immutable. Operations that appear to
107
+ /// "modify" an envelope actually create a new envelope. This immutability is
108
+ /// fundamental to maintaining the integrity of the envelope's digest tree.
109
+ ///
110
+ /// - **Efficient Cloning**: Envelopes use shallow copying for efficient O(1)
111
+ /// cloning. Since they're immutable, clones share the same underlying data.
112
+ ///
113
+ /// - **Semantic Structure**: Envelopes can represent various semantic
114
+ /// relationships through subjects, predicates, and objects (similar to RDF
115
+ /// triples).
116
+ ///
117
+ /// - **Digest Tree**: Each envelope maintains a Merkle-like digest tree that
118
+ /// ensures the integrity of its contents and enables verification of
119
+ /// individual parts.
120
+ ///
121
+ /// - **Privacy Features**: Envelopes support selective disclosure through
122
+ /// elision, encryption, and compression of specific parts, while maintaining
123
+ /// the overall integrity of the structure.
124
+ ///
125
+ /// - **Deterministic Representation**: Envelopes use deterministic CBOR
126
+ /// encoding to ensure consistent serialization across platforms.
127
+ ///
128
+ /// The Gordian Envelope specification is defined in an IETF Internet Draft, and
129
+ /// this implementation closely follows that specification.
130
+ ///
131
+ /// @example
132
+ /// ```typescript
133
+ /// // Create an envelope representing a person
134
+ /// const person = Envelope.new("person")
135
+ /// .addAssertion("name", "Alice")
136
+ /// .addAssertion("age", 30)
137
+ /// .addAssertion("email", "alice@example.com");
138
+ ///
139
+ /// // Create a partially redacted version by eliding the email
140
+ /// const redacted = person.elideRemovingTarget(
141
+ /// person.assertionWithPredicate("email")
142
+ /// );
143
+ ///
144
+ /// // The digest of both envelopes remains the same
145
+ /// assert(person.digest().equals(redacted.digest()));
146
+ /// ```
147
+ export class Envelope implements DigestProvider {
148
+ readonly #case: EnvelopeCase;
149
+
150
+ /// Private constructor. Use static factory methods to create envelopes.
151
+ ///
152
+ /// @param envelopeCase - The envelope case variant
153
+ private constructor(envelopeCase: EnvelopeCase) {
154
+ this.#case = envelopeCase;
155
+ }
156
+
157
+ /// Returns a reference to the underlying envelope case.
158
+ ///
159
+ /// The `EnvelopeCase` enum represents the specific structural variant of
160
+ /// this envelope. This method provides access to that underlying
161
+ /// variant for operations that need to differentiate between the
162
+ /// different envelope types.
163
+ ///
164
+ /// @returns The `EnvelopeCase` that defines this envelope's structure.
165
+ case(): EnvelopeCase {
166
+ return this.#case;
167
+ }
168
+
169
+ /// Creates an envelope with a subject, which can be any value that
170
+ /// can be encoded as an envelope.
171
+ ///
172
+ /// @param subject - The subject value
173
+ /// @returns A new envelope containing the subject
174
+ ///
175
+ /// @example
176
+ /// ```typescript
177
+ /// const envelope = Envelope.new("Hello, world!");
178
+ /// const numberEnvelope = Envelope.new(42);
179
+ /// const binaryEnvelope = Envelope.new(new Uint8Array([1, 2, 3]));
180
+ /// ```
181
+ static new(subject: EnvelopeEncodableValue): Envelope {
182
+ // Convert the subject to an envelope
183
+ if (subject instanceof Envelope) {
184
+ return subject;
185
+ }
186
+
187
+ // Handle primitives and create leaf envelopes
188
+ return Envelope.newLeaf(subject);
189
+ }
190
+
191
+ /// Creates an envelope with a subject, or null if subject is undefined.
192
+ ///
193
+ /// @param subject - The optional subject value
194
+ /// @returns A new envelope or null envelope
195
+ static newOrNull(subject: EnvelopeEncodableValue | undefined): Envelope {
196
+ if (subject === undefined || subject === null) {
197
+ return Envelope.null();
198
+ }
199
+ return Envelope.new(subject);
200
+ }
201
+
202
+ /// Creates an envelope with a subject, or undefined if subject is undefined.
203
+ ///
204
+ /// @param subject - The optional subject value
205
+ /// @returns A new envelope or undefined
206
+ static newOrNone(subject: EnvelopeEncodableValue | undefined): Envelope | undefined {
207
+ if (subject === undefined || subject === null) {
208
+ return undefined;
209
+ }
210
+ return Envelope.new(subject);
211
+ }
212
+
213
+ /// Creates an envelope from an EnvelopeCase.
214
+ ///
215
+ /// This is an internal method used by extensions to create envelopes
216
+ /// from custom case types like compressed or encrypted.
217
+ ///
218
+ /// @param envelopeCase - The envelope case to wrap
219
+ /// @returns A new envelope with the given case
220
+ static fromCase(envelopeCase: EnvelopeCase): Envelope {
221
+ return new Envelope(envelopeCase);
222
+ }
223
+
224
+ /// Creates an assertion envelope with a predicate and object.
225
+ ///
226
+ /// @param predicate - The predicate of the assertion
227
+ /// @param object - The object of the assertion
228
+ /// @returns A new assertion envelope
229
+ ///
230
+ /// @example
231
+ /// ```typescript
232
+ /// const assertion = Envelope.newAssertion("name", "Alice");
233
+ /// ```
234
+ static newAssertion(predicate: EnvelopeEncodableValue, object: EnvelopeEncodableValue): Envelope {
235
+ const predicateEnv = predicate instanceof Envelope ? predicate : Envelope.new(predicate);
236
+ const objectEnv = object instanceof Envelope ? object : Envelope.new(object);
237
+ return Envelope.newWithAssertion(new Assertion(predicateEnv, objectEnv));
238
+ }
239
+
240
+ /// Creates a null envelope (containing CBOR null).
241
+ ///
242
+ /// @returns A null envelope
243
+ static null(): Envelope {
244
+ return Envelope.newLeaf(null);
245
+ }
246
+
247
+ //
248
+ // Internal constructors
249
+ //
250
+
251
+ /// Creates an envelope with a subject and unchecked assertions.
252
+ ///
253
+ /// The assertions are sorted by digest and the envelope's digest is calculated.
254
+ ///
255
+ /// @param subject - The subject envelope
256
+ /// @param uncheckedAssertions - The assertions to attach
257
+ /// @returns A new node envelope
258
+ static newWithUncheckedAssertions(subject: Envelope, uncheckedAssertions: Envelope[]): Envelope {
259
+ if (uncheckedAssertions.length === 0) {
260
+ throw new Error("Assertions array cannot be empty");
261
+ }
262
+
263
+ // Sort assertions by digest
264
+ const sortedAssertions = [...uncheckedAssertions].sort((a, b) => {
265
+ const aHex = a.digest().hex();
266
+ const bHex = b.digest().hex();
267
+ return aHex.localeCompare(bHex);
268
+ });
269
+
270
+ // Calculate digest from subject and all assertions
271
+ const digests = [subject.digest(), ...sortedAssertions.map((a) => a.digest())];
272
+ const digest = Digest.fromDigests(digests);
273
+
274
+ return new Envelope({
275
+ type: "node",
276
+ subject,
277
+ assertions: sortedAssertions,
278
+ digest,
279
+ });
280
+ }
281
+
282
+ /// Creates an envelope with a subject and validated assertions.
283
+ ///
284
+ /// All assertions must be assertion or obscured envelopes.
285
+ ///
286
+ /// @param subject - The subject envelope
287
+ /// @param assertions - The assertions to attach
288
+ /// @returns A new node envelope
289
+ /// @throws {EnvelopeError} If any assertion is not valid
290
+ static newWithAssertions(subject: Envelope, assertions: Envelope[]): Envelope {
291
+ // Validate that all assertions are assertion or obscured envelopes
292
+ for (const assertion of assertions) {
293
+ if (!assertion.isSubjectAssertion() && !assertion.isSubjectObscured()) {
294
+ throw EnvelopeError.invalidFormat();
295
+ }
296
+ }
297
+
298
+ return Envelope.newWithUncheckedAssertions(subject, assertions);
299
+ }
300
+
301
+ /// Creates an envelope with an assertion as its subject.
302
+ ///
303
+ /// @param assertion - The assertion
304
+ /// @returns A new assertion envelope
305
+ static newWithAssertion(assertion: Assertion): Envelope {
306
+ return new Envelope({
307
+ type: "assertion",
308
+ assertion,
309
+ });
310
+ }
311
+
312
+ /// Creates an envelope with a known value.
313
+ ///
314
+ /// @param value - The known value (can be a KnownValue instance or a number/bigint)
315
+ /// @returns A new known value envelope
316
+ static newWithKnownValue(value: KnownValue | number | bigint): Envelope {
317
+ const knownValue = value instanceof KnownValue ? value : new KnownValue(value);
318
+ // Calculate digest from CBOR encoding of the known value
319
+ const digest = Digest.fromImage(knownValue.toCborData());
320
+ return new Envelope({
321
+ type: "knownValue",
322
+ value: knownValue,
323
+ digest,
324
+ });
325
+ }
326
+
327
+ /// Creates an envelope with encrypted content.
328
+ ///
329
+ /// @param encryptedMessage - The encrypted message
330
+ /// @returns A new encrypted envelope
331
+ /// @throws {EnvelopeError} If the encrypted message doesn't have a digest
332
+ static newWithEncrypted(encryptedMessage: EncryptedMessage): Envelope {
333
+ // TODO: Validate that encrypted message has digest
334
+ // if (!encryptedMessage.hasDigest()) {
335
+ // throw EnvelopeError.missingDigest();
336
+ // }
337
+ return new Envelope({
338
+ type: "encrypted",
339
+ message: encryptedMessage,
340
+ });
341
+ }
342
+
343
+ /// Creates an envelope with compressed content.
344
+ ///
345
+ /// @param compressed - The compressed data
346
+ /// @returns A new compressed envelope
347
+ /// @throws {EnvelopeError} If the compressed data doesn't have a digest
348
+ static newWithCompressed(compressed: Compressed): Envelope {
349
+ // TODO: Validate that compressed has digest
350
+ // if (!compressed.hasDigest()) {
351
+ // throw EnvelopeError.missingDigest();
352
+ // }
353
+ return new Envelope({
354
+ type: "compressed",
355
+ value: compressed,
356
+ });
357
+ }
358
+
359
+ /// Creates an elided envelope containing only a digest.
360
+ ///
361
+ /// @param digest - The digest of the elided content
362
+ /// @returns A new elided envelope
363
+ static newElided(digest: Digest): Envelope {
364
+ return new Envelope({
365
+ type: "elided",
366
+ digest,
367
+ });
368
+ }
369
+
370
+ /// Creates a leaf envelope containing a CBOR value.
371
+ ///
372
+ /// @param value - The value to encode as CBOR
373
+ /// @returns A new leaf envelope
374
+ static newLeaf(value: unknown): Envelope {
375
+ // Convert value to CBOR
376
+ const cbor = Envelope.valueToCbor(value);
377
+
378
+ // Calculate digest from CBOR bytes
379
+ const cborBytes = Envelope.cborToBytes(cbor);
380
+ const digest = Digest.fromImage(cborBytes);
381
+
382
+ return new Envelope({
383
+ type: "leaf",
384
+ cbor,
385
+ digest,
386
+ });
387
+ }
388
+
389
+ /// Creates a wrapped envelope.
390
+ ///
391
+ /// @param envelope - The envelope to wrap
392
+ /// @returns A new wrapped envelope
393
+ static newWrapped(envelope: Envelope): Envelope {
394
+ const digest = Digest.fromDigests([envelope.digest()]);
395
+ return new Envelope({
396
+ type: "wrapped",
397
+ envelope,
398
+ digest,
399
+ });
400
+ }
401
+
402
+ /// Returns the digest of this envelope.
403
+ ///
404
+ /// Implementation of DigestProvider interface.
405
+ ///
406
+ /// @returns The envelope's digest
407
+ digest(): Digest {
408
+ const c = this.#case;
409
+ switch (c.type) {
410
+ case "node":
411
+ case "leaf":
412
+ case "wrapped":
413
+ case "elided":
414
+ case "knownValue":
415
+ return c.digest;
416
+ case "assertion":
417
+ return c.assertion.digest();
418
+ case "encrypted": {
419
+ // Get digest from encrypted message (AAD)
420
+ const digest = c.message.aadDigest();
421
+ if (digest === undefined) {
422
+ throw new Error("Encrypted envelope missing digest");
423
+ }
424
+ return digest;
425
+ }
426
+ case "compressed": {
427
+ // Get digest from compressed value
428
+ const digest = c.value.digestOpt();
429
+ if (digest === undefined) {
430
+ throw new Error("Compressed envelope missing digest");
431
+ }
432
+ return digest;
433
+ }
434
+ }
435
+ }
436
+
437
+ /// Returns the subject of this envelope.
438
+ ///
439
+ /// For different envelope cases:
440
+ /// - Node: Returns the subject envelope
441
+ /// - Other cases: Returns the envelope itself
442
+ ///
443
+ /// @returns The subject envelope
444
+ subject(): Envelope {
445
+ const c = this.#case;
446
+ switch (c.type) {
447
+ case "node":
448
+ return c.subject;
449
+ case "leaf":
450
+ case "wrapped":
451
+ case "assertion":
452
+ case "elided":
453
+ case "knownValue":
454
+ case "encrypted":
455
+ case "compressed":
456
+ return this;
457
+ }
458
+ }
459
+
460
+ /// Checks if the envelope's subject is an assertion.
461
+ ///
462
+ /// @returns `true` if the subject is an assertion, `false` otherwise
463
+ isSubjectAssertion(): boolean {
464
+ return this.#case.type === "assertion";
465
+ }
466
+
467
+ /// Checks if the envelope's subject is obscured (elided, encrypted, or compressed).
468
+ ///
469
+ /// @returns `true` if the subject is obscured, `false` otherwise
470
+ isSubjectObscured(): boolean {
471
+ const t = this.#case.type;
472
+ return t === "elided" || t === "encrypted" || t === "compressed";
473
+ }
474
+
475
+ //
476
+ // CBOR conversion helpers
477
+ //
478
+
479
+ /// Converts a value to CBOR.
480
+ ///
481
+ /// @param value - The value to convert
482
+ /// @returns A CBOR representation
483
+ private static valueToCbor(value: unknown): Cbor {
484
+ // Import cbor function at runtime to avoid circular dependencies
485
+
486
+ return cbor(value as Parameters<typeof cbor>[0]);
487
+ }
488
+
489
+ /// Converts CBOR to bytes.
490
+ ///
491
+ /// @param cbor - The CBOR value
492
+ /// @returns Byte representation
493
+ private static cborToBytes(cbor: Cbor): Uint8Array {
494
+ // Import cborData function at runtime to avoid circular dependencies
495
+
496
+ return cborData(cbor);
497
+ }
498
+
499
+ /// Returns the untagged CBOR representation of this envelope.
500
+ ///
501
+ /// @returns The untagged CBOR
502
+ untaggedCbor(): Cbor {
503
+ const c = this.#case;
504
+ switch (c.type) {
505
+ case "node": {
506
+ // Array with subject followed by assertions
507
+ const result = [c.subject.untaggedCbor()];
508
+ for (const assertion of c.assertions) {
509
+ result.push(assertion.untaggedCbor());
510
+ }
511
+ return Envelope.valueToCbor(result);
512
+ }
513
+ case "leaf":
514
+ // Tagged with TAG_LEAF (204)
515
+ return toTaggedValue(TAG_LEAF, c.cbor);
516
+ case "wrapped":
517
+ // Wrapped envelopes are tagged with TAG_ENVELOPE
518
+ return c.envelope.taggedCbor();
519
+ case "assertion":
520
+ // Assertions convert to CBOR maps
521
+ return c.assertion.toCbor();
522
+ case "elided":
523
+ // Elided is just the digest bytes
524
+ return Envelope.valueToCbor(c.digest.data());
525
+ case "knownValue":
526
+ // TODO: Implement known value encoding
527
+ throw new Error("Known value encoding not yet implemented");
528
+ case "encrypted": {
529
+ // Encrypted is tagged with TAG_ENCRYPTED (40002)
530
+ // Contains: [ciphertext, nonce, auth, optional_aad_digest]
531
+ // Per BCR-2023-004 and BCR-2022-001
532
+ const message = c.message;
533
+ const digest = message.aadDigest();
534
+ const arr =
535
+ digest !== undefined
536
+ ? [message.ciphertext(), message.nonce(), message.authTag(), digest.data()]
537
+ : [message.ciphertext(), message.nonce(), message.authTag()];
538
+ return toTaggedValue(TAG_ENCRYPTED, Envelope.valueToCbor(arr));
539
+ }
540
+ case "compressed": {
541
+ // Compressed is tagged with TAG_COMPRESSED (40003)
542
+ // and contains an array: [compressed_data, optional_digest]
543
+ const digest = c.value.digestOpt();
544
+ const data = c.value.compressedData();
545
+ const arr = digest !== undefined ? [data, digest.data()] : [data];
546
+ return toTaggedValue(TAG_COMPRESSED, Envelope.valueToCbor(arr));
547
+ }
548
+ }
549
+ }
550
+
551
+ /// Returns the tagged CBOR representation of this envelope.
552
+ ///
553
+ /// All envelopes are tagged with TAG_ENVELOPE (200).
554
+ ///
555
+ /// @returns The tagged CBOR
556
+ taggedCbor(): Cbor {
557
+ return toTaggedValue(TAG_ENVELOPE, this.untaggedCbor());
558
+ }
559
+
560
+ /// Creates an envelope from untagged CBOR.
561
+ ///
562
+ /// @param cbor - The untagged CBOR value
563
+ /// @returns A new envelope
564
+ static fromUntaggedCbor(cbor: Cbor): Envelope {
565
+ // Check if it's a tagged value
566
+ const tagged = asTaggedValue(cbor);
567
+ if (tagged !== undefined) {
568
+ const [tag, item] = tagged;
569
+ switch (tag.value) {
570
+ case TAG_LEAF:
571
+ case TAG_ENCODED_CBOR:
572
+ // Leaf envelope
573
+ return Envelope.newLeaf(item);
574
+ case TAG_ENVELOPE: {
575
+ // Wrapped envelope
576
+ const envelope = Envelope.fromUntaggedCbor(item);
577
+ return Envelope.newWrapped(envelope);
578
+ }
579
+ case TAG_COMPRESSED: {
580
+ // Compressed envelope: array with [compressed_data, optional_digest]
581
+ const arr = asCborArray(item);
582
+ if (arr === undefined || arr.length < 1 || arr.length > 2) {
583
+ throw EnvelopeError.cbor("compressed envelope must have 1 or 2 elements");
584
+ }
585
+ // We've already checked arr.length >= 1 above
586
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
587
+ const compressedData = asByteString(arr.get(0)!);
588
+ if (compressedData === undefined) {
589
+ throw EnvelopeError.cbor("compressed data must be byte string");
590
+ }
591
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
592
+ const digestBytes = arr.length === 2 ? asByteString(arr.get(1)!) : undefined;
593
+ if (arr.length === 2 && digestBytes === undefined) {
594
+ throw EnvelopeError.cbor("digest must be byte string");
595
+ }
596
+ const digest = digestBytes !== undefined ? new Digest(digestBytes) : undefined;
597
+
598
+ // Import Compressed class at runtime to avoid circular dependency
599
+
600
+ const compressed = new Compressed(compressedData, digest);
601
+ return Envelope.fromCase({ type: "compressed", value: compressed });
602
+ }
603
+ case TAG_ENCRYPTED: {
604
+ // Encrypted envelope: array with [ciphertext, nonce, auth, optional_aad_digest]
605
+ // Per BCR-2023-004 and BCR-2022-001
606
+ const arr = asCborArray(item);
607
+ if (arr === undefined || arr.length < 3 || arr.length > 4) {
608
+ throw EnvelopeError.cbor("encrypted envelope must have 3 or 4 elements");
609
+ }
610
+ // We've already checked arr.length >= 3 above
611
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
612
+ const ciphertext = asByteString(arr.get(0)!);
613
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
614
+ const nonce = asByteString(arr.get(1)!);
615
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
616
+ const authTag = asByteString(arr.get(2)!);
617
+ if (ciphertext === undefined || nonce === undefined || authTag === undefined) {
618
+ throw EnvelopeError.cbor("ciphertext, nonce, and auth must be byte strings");
619
+ }
620
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
621
+ const digestBytes = arr.length === 4 ? asByteString(arr.get(3)!) : undefined;
622
+ if (arr.length === 4 && digestBytes === undefined) {
623
+ throw EnvelopeError.cbor("aad digest must be byte string");
624
+ }
625
+ const digest = digestBytes !== undefined ? new Digest(digestBytes) : undefined;
626
+
627
+ const message = new EncryptedMessage(ciphertext, nonce, authTag, digest);
628
+ return Envelope.fromCase({ type: "encrypted", message });
629
+ }
630
+ default:
631
+ throw EnvelopeError.cbor(`unknown envelope tag: ${tag.value}`);
632
+ }
633
+ }
634
+
635
+ // Check if it's a byte string (elided)
636
+ const bytes = asByteString(cbor);
637
+ if (bytes !== undefined) {
638
+ if (bytes.length !== 32) {
639
+ throw EnvelopeError.cbor("elided digest must be 32 bytes");
640
+ }
641
+ return Envelope.newElided(new Digest(bytes));
642
+ }
643
+
644
+ // Check if it's an array (node)
645
+ const array = asCborArray(cbor);
646
+ if (array !== undefined) {
647
+ if (array.length < 2) {
648
+ throw EnvelopeError.cbor("node must have at least two elements");
649
+ }
650
+ const subjectCbor = array.get(0);
651
+ if (subjectCbor === undefined) {
652
+ throw EnvelopeError.cbor("node subject is missing");
653
+ }
654
+ const subject = Envelope.fromUntaggedCbor(subjectCbor);
655
+ const assertions: Envelope[] = [];
656
+ for (let i = 1; i < array.length; i++) {
657
+ const assertionCbor = array.get(i);
658
+ if (assertionCbor === undefined) {
659
+ throw EnvelopeError.cbor(`node assertion at index ${i} is missing`);
660
+ }
661
+ assertions.push(Envelope.fromUntaggedCbor(assertionCbor));
662
+ }
663
+ return Envelope.newWithAssertions(subject, assertions);
664
+ }
665
+
666
+ // Check if it's a map (assertion)
667
+ const map = asCborMap(cbor);
668
+ if (map !== undefined) {
669
+ const assertion = Assertion.fromCborMap(map);
670
+ return Envelope.newWithAssertion(assertion);
671
+ }
672
+
673
+ // Handle known values (unsigned integers)
674
+ if (cbor.type === MajorType.Unsigned) {
675
+ const knownValue = new KnownValue(cbor.value as number | bigint);
676
+ return Envelope.newWithKnownValue(knownValue);
677
+ }
678
+
679
+ throw EnvelopeError.cbor("invalid envelope format");
680
+ }
681
+
682
+ /// Creates an envelope from tagged CBOR.
683
+ ///
684
+ /// @param cbor - The tagged CBOR value (should have TAG_ENVELOPE)
685
+ /// @returns A new envelope
686
+ static fromTaggedCbor(cbor: Cbor): Envelope {
687
+ try {
688
+ const untagged = tryExpectedTaggedValue(cbor, TAG_ENVELOPE);
689
+ return Envelope.fromUntaggedCbor(untagged);
690
+ } catch (error) {
691
+ throw EnvelopeError.cbor(
692
+ `expected TAG_ENVELOPE (${TAG_ENVELOPE})`,
693
+ error instanceof Error ? error : undefined,
694
+ );
695
+ }
696
+ }
697
+
698
+ /// Adds an assertion to this envelope.
699
+ ///
700
+ /// @param predicate - The assertion predicate
701
+ /// @param object - The assertion object
702
+ /// @returns A new envelope with the assertion added
703
+ ///
704
+ /// @example
705
+ /// ```typescript
706
+ /// const person = Envelope.new("Alice")
707
+ /// .addAssertion("age", 30)
708
+ /// .addAssertion("city", "Boston");
709
+ /// ```
710
+ addAssertion(predicate: EnvelopeEncodableValue, object: EnvelopeEncodableValue): Envelope {
711
+ const assertion = Envelope.newAssertion(predicate, object);
712
+ return this.addAssertionEnvelope(assertion);
713
+ }
714
+
715
+ /// Adds an assertion envelope to this envelope.
716
+ ///
717
+ /// @param assertion - The assertion envelope
718
+ /// @returns A new envelope with the assertion added
719
+ addAssertionEnvelope(assertion: Envelope): Envelope {
720
+ const c = this.#case;
721
+
722
+ // If this is already a node, add to existing assertions
723
+ if (c.type === "node") {
724
+ return Envelope.newWithAssertions(c.subject, [...c.assertions, assertion]);
725
+ }
726
+
727
+ // Otherwise, create a new node with this envelope as subject
728
+ return Envelope.newWithAssertions(this, [assertion]);
729
+ }
730
+
731
+ /// Creates a string representation of this envelope.
732
+ ///
733
+ /// @returns A string representation
734
+ toString(): string {
735
+ return `Envelope(${this.#case.type})`;
736
+ }
737
+
738
+ /// Creates a shallow copy of this envelope.
739
+ ///
740
+ /// Since envelopes are immutable, this returns the same instance.
741
+ ///
742
+ /// @returns This envelope
743
+ clone(): Envelope {
744
+ return this;
745
+ }
746
+
747
+ //
748
+ // Format methods (implemented via prototype extension in format module)
749
+ //
750
+
751
+ /// Returns a tree-formatted string representation of the envelope.
752
+ ///
753
+ /// The tree format displays the hierarchical structure of the envelope,
754
+ /// showing subjects, assertions, and their relationships.
755
+ ///
756
+ /// @param options - Optional formatting options
757
+ /// @returns A tree-formatted string
758
+ declare treeFormat: (options?: {
759
+ hideNodes?: boolean;
760
+ highlightDigests?: Set<string>;
761
+ digestDisplay?: "short" | "full";
762
+ }) => string;
763
+
764
+ /// Returns a short identifier for this envelope based on its digest.
765
+ ///
766
+ /// @param format - Format for the digest ('short' or 'full')
767
+ /// @returns A digest identifier string
768
+ declare shortId: (format?: "short" | "full") => string;
769
+
770
+ /// Returns a summary string for this envelope.
771
+ ///
772
+ /// @param maxLength - Maximum length of the summary
773
+ /// @returns A summary string
774
+ declare summary: (maxLength?: number) => string;
775
+
776
+ /// Returns a hex representation of the envelope's CBOR encoding.
777
+ ///
778
+ /// @returns A hex string
779
+ declare hex: () => string;
780
+
781
+ /// Returns the CBOR-encoded bytes of the envelope.
782
+ ///
783
+ /// @returns The CBOR bytes
784
+ declare cborBytes: () => Uint8Array;
785
+
786
+ /// Returns a CBOR diagnostic notation string for the envelope.
787
+ ///
788
+ /// @returns A diagnostic string
789
+ declare diagnostic: () => string;
790
+ }