@bcts/envelope 1.0.0-alpha.10

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 +782 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +782 -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 +84 -0
  14. package/src/base/assertion.ts +179 -0
  15. package/src/base/assertions.ts +153 -0
  16. package/src/base/cbor.ts +122 -0
  17. package/src/base/digest.ts +204 -0
  18. package/src/base/elide.ts +390 -0
  19. package/src/base/envelope-decodable.ts +186 -0
  20. package/src/base/envelope-encodable.ts +71 -0
  21. package/src/base/envelope.ts +988 -0
  22. package/src/base/error.ts +421 -0
  23. package/src/base/index.ts +56 -0
  24. package/src/base/leaf.ts +147 -0
  25. package/src/base/queries.ts +244 -0
  26. package/src/base/walk.ts +215 -0
  27. package/src/base/wrap.ts +26 -0
  28. package/src/extension/attachment.ts +280 -0
  29. package/src/extension/compress.ts +176 -0
  30. package/src/extension/encrypt.ts +297 -0
  31. package/src/extension/expression.ts +404 -0
  32. package/src/extension/index.ts +72 -0
  33. package/src/extension/proof.ts +227 -0
  34. package/src/extension/recipient.ts +440 -0
  35. package/src/extension/salt.ts +114 -0
  36. package/src/extension/signature.ts +398 -0
  37. package/src/extension/types.ts +92 -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,280 @@
1
+ /**
2
+ * Attachment Extension for Gordian Envelope
3
+ *
4
+ * Provides functionality for attaching vendor-specific metadata to envelopes.
5
+ * Attachments enable flexible, extensible data storage without modifying
6
+ * the core data model, facilitating interoperability and future compatibility.
7
+ *
8
+ * Each attachment has:
9
+ * - A payload (arbitrary data)
10
+ * - A required vendor identifier (typically a reverse domain name)
11
+ * - An optional conformsTo URI that indicates the format of the attachment
12
+ *
13
+ * See BCR-2023-006: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2023-006-envelope-attachment.md
14
+ */
15
+
16
+ import { Envelope } from "../base/envelope";
17
+ import { type Digest } from "../base/digest";
18
+ import { EnvelopeError } from "../base/error";
19
+ import type { EnvelopeEncodableValue } from "../base/envelope-encodable";
20
+
21
+ /**
22
+ * Known value for the 'attachment' predicate.
23
+ */
24
+ export const ATTACHMENT = "attachment";
25
+
26
+ /**
27
+ * Known value for the 'vendor' predicate.
28
+ */
29
+ export const VENDOR = "vendor";
30
+
31
+ /**
32
+ * Known value for the 'conformsTo' predicate.
33
+ */
34
+ export const CONFORMS_TO = "conformsTo";
35
+
36
+ /**
37
+ * A container for vendor-specific metadata attachments.
38
+ *
39
+ * Attachments provides a flexible mechanism for attaching arbitrary metadata
40
+ * to envelopes without modifying their core structure.
41
+ */
42
+ export class Attachments {
43
+ readonly #envelopes = new Map<string, Envelope>();
44
+
45
+ /**
46
+ * Creates a new empty attachments container.
47
+ */
48
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
49
+ constructor() {}
50
+
51
+ /**
52
+ * Adds a new attachment with the specified payload and metadata.
53
+ *
54
+ * @param payload - The data to attach
55
+ * @param vendor - A string identifying the entity that defined the attachment format
56
+ * @param conformsTo - Optional URI identifying the structure the payload conforms to
57
+ */
58
+ add(payload: EnvelopeEncodableValue, vendor: string, conformsTo?: string): void {
59
+ const attachment = Envelope.newAttachment(payload, vendor, conformsTo);
60
+ this.#envelopes.set(attachment.digest().hex(), attachment);
61
+ }
62
+
63
+ /**
64
+ * Retrieves an attachment by its digest.
65
+ *
66
+ * @param digest - The unique digest of the attachment to retrieve
67
+ * @returns The envelope if found, or undefined
68
+ */
69
+ get(digest: Digest): Envelope | undefined {
70
+ return this.#envelopes.get(digest.hex());
71
+ }
72
+
73
+ /**
74
+ * Removes an attachment by its digest.
75
+ *
76
+ * @param digest - The unique digest of the attachment to remove
77
+ * @returns The removed envelope if found, or undefined
78
+ */
79
+ remove(digest: Digest): Envelope | undefined {
80
+ const envelope = this.#envelopes.get(digest.hex());
81
+ this.#envelopes.delete(digest.hex());
82
+ return envelope;
83
+ }
84
+
85
+ /**
86
+ * Removes all attachments from the container.
87
+ */
88
+ clear(): void {
89
+ this.#envelopes.clear();
90
+ }
91
+
92
+ /**
93
+ * Returns whether the container has any attachments.
94
+ */
95
+ isEmpty(): boolean {
96
+ return this.#envelopes.size === 0;
97
+ }
98
+
99
+ /**
100
+ * Adds all attachments from this container to an envelope.
101
+ *
102
+ * @param envelope - The envelope to add attachments to
103
+ * @returns A new envelope with all attachments added as assertions
104
+ */
105
+ addToEnvelope(envelope: Envelope): Envelope {
106
+ let result = envelope;
107
+ for (const attachment of this.#envelopes.values()) {
108
+ result = result.addAssertion(ATTACHMENT, attachment);
109
+ }
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Creates an Attachments container from an envelope's attachment assertions.
115
+ *
116
+ * @param envelope - The envelope to extract attachments from
117
+ * @returns A new Attachments container with the envelope's attachments
118
+ */
119
+ static fromEnvelope(envelope: Envelope): Attachments {
120
+ const attachments = new Attachments();
121
+ const attachmentEnvelopes = envelope.attachments();
122
+
123
+ for (const attachment of attachmentEnvelopes) {
124
+ attachments.#envelopes.set(attachment.digest().hex(), attachment);
125
+ }
126
+
127
+ return attachments;
128
+ }
129
+ }
130
+
131
+ // Implementation
132
+
133
+ /**
134
+ * Creates a new attachment envelope.
135
+ */
136
+ Envelope.newAttachment = function (
137
+ payload: EnvelopeEncodableValue,
138
+ vendor: string,
139
+ conformsTo?: string,
140
+ ): Envelope {
141
+ // Create the payload envelope wrapped with vendor assertion
142
+ let attachmentObj = Envelope.new(payload).wrap().addAssertion(VENDOR, vendor);
143
+
144
+ // Add optional conformsTo
145
+ if (conformsTo !== undefined) {
146
+ attachmentObj = attachmentObj.addAssertion(CONFORMS_TO, conformsTo);
147
+ }
148
+
149
+ // Create an assertion with 'attachment' as predicate and the wrapped payload as object
150
+ // This returns an assertion envelope
151
+ const attachmentPredicate = Envelope.new(ATTACHMENT);
152
+ return attachmentPredicate.addAssertion(ATTACHMENT, attachmentObj).assertions()[0];
153
+ };
154
+
155
+ /**
156
+ * Adds an attachment to an envelope.
157
+ */
158
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
159
+ if (Envelope?.prototype) {
160
+ Envelope.prototype.addAttachment = function (
161
+ this: Envelope,
162
+ payload: EnvelopeEncodableValue,
163
+ vendor: string,
164
+ conformsTo?: string,
165
+ ): Envelope {
166
+ let attachmentObj = Envelope.new(payload).wrap().addAssertion(VENDOR, vendor);
167
+
168
+ if (conformsTo !== undefined) {
169
+ attachmentObj = attachmentObj.addAssertion(CONFORMS_TO, conformsTo);
170
+ }
171
+
172
+ return this.addAssertion(ATTACHMENT, attachmentObj);
173
+ };
174
+
175
+ /**
176
+ * Returns the payload of an attachment envelope.
177
+ */
178
+ Envelope.prototype.attachmentPayload = function (this: Envelope): Envelope {
179
+ const c = this.case();
180
+ if (c.type !== "assertion") {
181
+ throw EnvelopeError.general("Envelope is not an attachment assertion");
182
+ }
183
+
184
+ const obj = c.assertion.object();
185
+ return obj.unwrap();
186
+ };
187
+
188
+ /**
189
+ * Returns the vendor of an attachment envelope.
190
+ */
191
+ Envelope.prototype.attachmentVendor = function (this: Envelope): string {
192
+ const c = this.case();
193
+ if (c.type !== "assertion") {
194
+ throw EnvelopeError.general("Envelope is not an attachment assertion");
195
+ }
196
+
197
+ const obj = c.assertion.object();
198
+ const vendorEnv = obj.objectForPredicate(VENDOR);
199
+ const vendor = vendorEnv.asText();
200
+
201
+ if (vendor === undefined || vendor === "") {
202
+ throw EnvelopeError.general("Attachment has no vendor");
203
+ }
204
+
205
+ return vendor;
206
+ };
207
+
208
+ /**
209
+ * Returns the conformsTo of an attachment envelope.
210
+ */
211
+ Envelope.prototype.attachmentConformsTo = function (this: Envelope): string | undefined {
212
+ const c = this.case();
213
+ if (c.type !== "assertion") {
214
+ throw EnvelopeError.general("Envelope is not an attachment assertion");
215
+ }
216
+
217
+ const obj = c.assertion.object();
218
+ const conformsToEnv = obj.optionalObjectForPredicate(CONFORMS_TO);
219
+
220
+ if (conformsToEnv === undefined) {
221
+ return undefined;
222
+ }
223
+
224
+ return conformsToEnv.asText();
225
+ };
226
+
227
+ /**
228
+ * Returns all attachment assertions.
229
+ */
230
+ Envelope.prototype.attachments = function (this: Envelope): Envelope[] {
231
+ return this.assertionsWithPredicate(ATTACHMENT).map((a) => {
232
+ const c = a.case();
233
+ if (c.type === "assertion") {
234
+ return c.assertion.object();
235
+ }
236
+ throw EnvelopeError.general("Invalid attachment assertion");
237
+ });
238
+ };
239
+
240
+ /**
241
+ * Returns attachments matching vendor and/or conformsTo.
242
+ */
243
+ Envelope.prototype.attachmentsWithVendorAndConformsTo = function (
244
+ this: Envelope,
245
+ vendor?: string,
246
+ conformsTo?: string,
247
+ ): Envelope[] {
248
+ const allAttachments = this.attachments();
249
+
250
+ return allAttachments.filter((attachment) => {
251
+ try {
252
+ // The attachment is already a wrapped envelope with vendor/conformsTo assertions
253
+ // Check vendor if specified
254
+ if (vendor !== undefined) {
255
+ const vendorEnv = attachment.objectForPredicate(VENDOR);
256
+ const attachmentVendor = vendorEnv.asText();
257
+ if (attachmentVendor !== vendor) {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ // Check conformsTo if specified
263
+ if (conformsTo !== undefined) {
264
+ const conformsToEnv = attachment.optionalObjectForPredicate(CONFORMS_TO);
265
+ if (conformsToEnv === undefined) {
266
+ return false;
267
+ }
268
+ const conformsToText = conformsToEnv.asText();
269
+ if (conformsToText !== conformsTo) {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ return true;
275
+ } catch {
276
+ return false;
277
+ }
278
+ });
279
+ };
280
+ }
@@ -0,0 +1,176 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import { type Digest } from "../base/digest";
4
+ import * as pako from "pako";
5
+ import { cborData, decodeCbor } from "@bcts/dcbor";
6
+
7
+ /// Extension for compressing and decompressing envelopes.
8
+ ///
9
+ /// This module provides functionality for compressing envelopes to reduce their
10
+ /// size while maintaining their digests. Unlike elision, which removes content,
11
+ /// compression preserves all the information in the envelope but represents it
12
+ /// more efficiently.
13
+ ///
14
+ /// Compression is implemented using the DEFLATE algorithm (via pako) and preserves
15
+ /// the envelope's digest, making it compatible with the envelope's hierarchical
16
+ /// digest tree structure.
17
+ ///
18
+ /// @example
19
+ /// ```typescript
20
+ /// // Create an envelope with some larger, compressible content
21
+ /// const lorem = "Lorem ipsum dolor sit amet...".repeat(10);
22
+ /// const envelope = Envelope.new(lorem);
23
+ ///
24
+ /// // Compress the envelope
25
+ /// const compressed = envelope.compress();
26
+ ///
27
+ /// // The compressed envelope has the same digest as the original
28
+ /// console.log(envelope.digest().equals(compressed.digest())); // true
29
+ ///
30
+ /// // But it takes up less space when serialized
31
+ /// console.log(compressed.cborBytes().length < envelope.cborBytes().length); // true
32
+ ///
33
+ /// // The envelope can be decompressed to recover the original content
34
+ /// const decompressed = compressed.decompress();
35
+ /// console.log(decompressed.asText() === lorem); // true
36
+ /// ```
37
+
38
+ /// Represents compressed data with optional digest
39
+ export class Compressed {
40
+ readonly #compressedData: Uint8Array;
41
+ readonly #digest?: Digest;
42
+
43
+ constructor(compressedData: Uint8Array, digest?: Digest) {
44
+ this.#compressedData = compressedData;
45
+ if (digest !== undefined) {
46
+ this.#digest = digest;
47
+ }
48
+ }
49
+
50
+ /// Creates a Compressed instance from decompressed data
51
+ static fromDecompressedData(decompressedData: Uint8Array, digest?: Digest): Compressed {
52
+ const compressed = pako.deflate(decompressedData);
53
+ return new Compressed(compressed, digest);
54
+ }
55
+
56
+ /// Returns the compressed data
57
+ compressedData(): Uint8Array {
58
+ return this.#compressedData;
59
+ }
60
+
61
+ /// Returns the optional digest
62
+ digestOpt(): Digest | undefined {
63
+ return this.#digest;
64
+ }
65
+
66
+ /// Decompresses the data
67
+ decompress(): Uint8Array {
68
+ return pako.inflate(this.#compressedData);
69
+ }
70
+ }
71
+
72
+ /// Register compression extension methods on Envelope prototype
73
+ /// This function is exported and called during module initialization
74
+ /// to ensure Envelope is fully defined before attaching methods.
75
+ export function registerCompressExtension(): void {
76
+ if (Envelope?.prototype === undefined) {
77
+ return;
78
+ }
79
+
80
+ // Skip if already registered
81
+ if (typeof Envelope.prototype.compress === "function") {
82
+ return;
83
+ }
84
+
85
+ Envelope.prototype.compress = function (this: Envelope): Envelope {
86
+ const c = this.case();
87
+
88
+ // If already compressed, return as-is
89
+ if (c.type === "compressed") {
90
+ return this;
91
+ }
92
+
93
+ // Can't compress encrypted or elided envelopes
94
+ if (c.type === "encrypted") {
95
+ throw EnvelopeError.general("Cannot compress encrypted envelope");
96
+ }
97
+ if (c.type === "elided") {
98
+ throw EnvelopeError.general("Cannot compress elided envelope");
99
+ }
100
+
101
+ // Compress the entire envelope
102
+ const cbor = this.taggedCbor();
103
+
104
+ const decompressedData = cborData(cbor);
105
+
106
+ const compressed = Compressed.fromDecompressedData(decompressedData, this.digest());
107
+
108
+ // Create a compressed envelope case
109
+ return Envelope.fromCase({ type: "compressed", value: compressed });
110
+ };
111
+
112
+ /// Implementation of decompress()
113
+ Envelope.prototype.decompress = function (this: Envelope): Envelope {
114
+ const c = this.case();
115
+
116
+ if (c.type !== "compressed") {
117
+ throw EnvelopeError.general("Envelope is not compressed");
118
+ }
119
+
120
+ const compressed = c.value;
121
+ const digest = compressed.digestOpt();
122
+
123
+ if (digest === undefined) {
124
+ throw EnvelopeError.general("Missing digest in compressed envelope");
125
+ }
126
+
127
+ // Verify the digest matches
128
+ if (!digest.equals(this.digest())) {
129
+ throw EnvelopeError.general("Invalid digest in compressed envelope");
130
+ }
131
+
132
+ // Decompress the data
133
+ const decompressedData = compressed.decompress();
134
+
135
+ // Parse back to envelope
136
+
137
+ const cbor = decodeCbor(decompressedData);
138
+ const envelope = Envelope.fromTaggedCbor(cbor);
139
+
140
+ // Verify the decompressed envelope has the correct digest
141
+ if (!envelope.digest().equals(digest)) {
142
+ throw EnvelopeError.general("Invalid digest after decompression");
143
+ }
144
+
145
+ return envelope;
146
+ };
147
+
148
+ /// Implementation of compressSubject()
149
+ Envelope.prototype.compressSubject = function (this: Envelope): Envelope {
150
+ if (this.subject().isCompressed()) {
151
+ return this;
152
+ }
153
+
154
+ const subject = this.subject().compress();
155
+ return this.replaceSubject(subject);
156
+ };
157
+
158
+ /// Implementation of decompressSubject()
159
+ Envelope.prototype.decompressSubject = function (this: Envelope): Envelope {
160
+ if (this.subject().isCompressed()) {
161
+ const subject = this.subject().decompress();
162
+ return this.replaceSubject(subject);
163
+ }
164
+
165
+ return this;
166
+ };
167
+
168
+ /// Implementation of isCompressed()
169
+ Envelope.prototype.isCompressed = function (this: Envelope): boolean {
170
+ return this.case().type === "compressed";
171
+ };
172
+ }
173
+
174
+ // Auto-register on module load - will be called again from index.ts
175
+ // to ensure proper ordering after all modules are loaded
176
+ registerCompressExtension();