@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.
- package/LICENSE +48 -0
- package/README.md +23 -0
- package/dist/index.cjs +2646 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +978 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +978 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +2644 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +2552 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +85 -0
- package/src/base/assertion.ts +179 -0
- package/src/base/assertions.ts +304 -0
- package/src/base/cbor.ts +122 -0
- package/src/base/digest.ts +204 -0
- package/src/base/elide.ts +526 -0
- package/src/base/envelope-decodable.ts +229 -0
- package/src/base/envelope-encodable.ts +71 -0
- package/src/base/envelope.ts +790 -0
- package/src/base/error.ts +421 -0
- package/src/base/index.ts +56 -0
- package/src/base/leaf.ts +226 -0
- package/src/base/queries.ts +374 -0
- package/src/base/walk.ts +241 -0
- package/src/base/wrap.ts +72 -0
- package/src/extension/attachment.ts +369 -0
- package/src/extension/compress.ts +293 -0
- package/src/extension/encrypt.ts +379 -0
- package/src/extension/expression.ts +404 -0
- package/src/extension/index.ts +72 -0
- package/src/extension/proof.ts +276 -0
- package/src/extension/recipient.ts +557 -0
- package/src/extension/salt.ts +223 -0
- package/src/extension/signature.ts +463 -0
- package/src/extension/types.ts +222 -0
- package/src/format/diagnostic.ts +116 -0
- package/src/format/hex.ts +25 -0
- package/src/format/index.ts +13 -0
- package/src/format/tree.ts +168 -0
- package/src/index.ts +32 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/string.ts +48 -0
|
@@ -0,0 +1,369 @@
|
|
|
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
|
+
/**
|
|
132
|
+
* Support for attachments in envelopes.
|
|
133
|
+
*/
|
|
134
|
+
declare module "../base/envelope" {
|
|
135
|
+
interface Envelope {
|
|
136
|
+
/**
|
|
137
|
+
* Creates a new envelope with an attachment as its subject.
|
|
138
|
+
*
|
|
139
|
+
* The attachment consists of:
|
|
140
|
+
* - The predicate 'attachment'
|
|
141
|
+
* - An object that is a wrapped envelope containing:
|
|
142
|
+
* - The payload (as the subject)
|
|
143
|
+
* - A required 'vendor': String assertion
|
|
144
|
+
* - An optional 'conformsTo': String assertion
|
|
145
|
+
*
|
|
146
|
+
* @param payload - The content of the attachment
|
|
147
|
+
* @param vendor - A string identifying the entity (typically reverse domain name)
|
|
148
|
+
* @param conformsTo - Optional URI identifying the format of the attachment
|
|
149
|
+
* @returns A new attachment envelope
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const attachment = Envelope.newAttachment(
|
|
154
|
+
* "Custom data",
|
|
155
|
+
* "com.example",
|
|
156
|
+
* "https://example.com/format/v1"
|
|
157
|
+
* );
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
addAttachment(payload: EnvelopeEncodableValue, vendor: string, conformsTo?: string): Envelope;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns the payload of an attachment envelope.
|
|
164
|
+
*
|
|
165
|
+
* @returns The payload envelope
|
|
166
|
+
* @throws {EnvelopeError} If the envelope is not a valid attachment
|
|
167
|
+
*/
|
|
168
|
+
attachmentPayload(): Envelope;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the vendor identifier of an attachment envelope.
|
|
172
|
+
*
|
|
173
|
+
* @returns The vendor string
|
|
174
|
+
* @throws {EnvelopeError} If the envelope is not a valid attachment
|
|
175
|
+
*/
|
|
176
|
+
attachmentVendor(): string;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns the optional conformsTo URI of an attachment envelope.
|
|
180
|
+
*
|
|
181
|
+
* @returns The conformsTo string if present, or undefined
|
|
182
|
+
* @throws {EnvelopeError} If the envelope is not a valid attachment
|
|
183
|
+
*/
|
|
184
|
+
attachmentConformsTo(): string | undefined;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Returns all attachment assertions in this envelope.
|
|
188
|
+
*
|
|
189
|
+
* @returns Array of attachment envelopes
|
|
190
|
+
*/
|
|
191
|
+
attachments(): Envelope[];
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Searches for attachments matching the given vendor and conformsTo.
|
|
195
|
+
*
|
|
196
|
+
* @param vendor - Optional vendor identifier to match
|
|
197
|
+
* @param conformsTo - Optional conformsTo URI to match
|
|
198
|
+
* @returns Array of matching attachment envelopes
|
|
199
|
+
*/
|
|
200
|
+
attachmentsWithVendorAndConformsTo(vendor?: string, conformsTo?: string): Envelope[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
namespace Envelope {
|
|
204
|
+
/**
|
|
205
|
+
* Creates a new attachment envelope.
|
|
206
|
+
*
|
|
207
|
+
* @param payload - The content of the attachment
|
|
208
|
+
* @param vendor - A string identifying the entity
|
|
209
|
+
* @param conformsTo - Optional URI identifying the format
|
|
210
|
+
* @returns A new attachment envelope
|
|
211
|
+
*/
|
|
212
|
+
function newAttachment(
|
|
213
|
+
payload: EnvelopeEncodableValue,
|
|
214
|
+
vendor: string,
|
|
215
|
+
conformsTo?: string,
|
|
216
|
+
): Envelope;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Implementation
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Creates a new attachment envelope.
|
|
224
|
+
*/
|
|
225
|
+
Envelope.newAttachment = function (
|
|
226
|
+
payload: EnvelopeEncodableValue,
|
|
227
|
+
vendor: string,
|
|
228
|
+
conformsTo?: string,
|
|
229
|
+
): Envelope {
|
|
230
|
+
// Create the payload envelope wrapped with vendor assertion
|
|
231
|
+
let attachmentObj = Envelope.new(payload).wrap().addAssertion(VENDOR, vendor);
|
|
232
|
+
|
|
233
|
+
// Add optional conformsTo
|
|
234
|
+
if (conformsTo !== undefined) {
|
|
235
|
+
attachmentObj = attachmentObj.addAssertion(CONFORMS_TO, conformsTo);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Create an assertion with 'attachment' as predicate and the wrapped payload as object
|
|
239
|
+
// This returns an assertion envelope
|
|
240
|
+
const attachmentPredicate = Envelope.new(ATTACHMENT);
|
|
241
|
+
return attachmentPredicate.addAssertion(ATTACHMENT, attachmentObj).assertions()[0];
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Adds an attachment to an envelope.
|
|
246
|
+
*/
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
248
|
+
if (Envelope?.prototype) {
|
|
249
|
+
Envelope.prototype.addAttachment = function (
|
|
250
|
+
this: Envelope,
|
|
251
|
+
payload: EnvelopeEncodableValue,
|
|
252
|
+
vendor: string,
|
|
253
|
+
conformsTo?: string,
|
|
254
|
+
): Envelope {
|
|
255
|
+
let attachmentObj = Envelope.new(payload).wrap().addAssertion(VENDOR, vendor);
|
|
256
|
+
|
|
257
|
+
if (conformsTo !== undefined) {
|
|
258
|
+
attachmentObj = attachmentObj.addAssertion(CONFORMS_TO, conformsTo);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return this.addAssertion(ATTACHMENT, attachmentObj);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Returns the payload of an attachment envelope.
|
|
266
|
+
*/
|
|
267
|
+
Envelope.prototype.attachmentPayload = function (this: Envelope): Envelope {
|
|
268
|
+
const c = this.case();
|
|
269
|
+
if (c.type !== "assertion") {
|
|
270
|
+
throw EnvelopeError.general("Envelope is not an attachment assertion");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const obj = c.assertion.object();
|
|
274
|
+
return obj.unwrap();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Returns the vendor of an attachment envelope.
|
|
279
|
+
*/
|
|
280
|
+
Envelope.prototype.attachmentVendor = function (this: Envelope): string {
|
|
281
|
+
const c = this.case();
|
|
282
|
+
if (c.type !== "assertion") {
|
|
283
|
+
throw EnvelopeError.general("Envelope is not an attachment assertion");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const obj = c.assertion.object();
|
|
287
|
+
const vendorEnv = obj.objectForPredicate(VENDOR);
|
|
288
|
+
const vendor = vendorEnv.asText();
|
|
289
|
+
|
|
290
|
+
if (vendor === undefined || vendor === "") {
|
|
291
|
+
throw EnvelopeError.general("Attachment has no vendor");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return vendor;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns the conformsTo of an attachment envelope.
|
|
299
|
+
*/
|
|
300
|
+
Envelope.prototype.attachmentConformsTo = function (this: Envelope): string | undefined {
|
|
301
|
+
const c = this.case();
|
|
302
|
+
if (c.type !== "assertion") {
|
|
303
|
+
throw EnvelopeError.general("Envelope is not an attachment assertion");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const obj = c.assertion.object();
|
|
307
|
+
const conformsToEnv = obj.optionalObjectForPredicate(CONFORMS_TO);
|
|
308
|
+
|
|
309
|
+
if (conformsToEnv === undefined) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return conformsToEnv.asText();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Returns all attachment assertions.
|
|
318
|
+
*/
|
|
319
|
+
Envelope.prototype.attachments = function (this: Envelope): Envelope[] {
|
|
320
|
+
return this.assertionsWithPredicate(ATTACHMENT).map((a) => {
|
|
321
|
+
const c = a.case();
|
|
322
|
+
if (c.type === "assertion") {
|
|
323
|
+
return c.assertion.object();
|
|
324
|
+
}
|
|
325
|
+
throw EnvelopeError.general("Invalid attachment assertion");
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Returns attachments matching vendor and/or conformsTo.
|
|
331
|
+
*/
|
|
332
|
+
Envelope.prototype.attachmentsWithVendorAndConformsTo = function (
|
|
333
|
+
this: Envelope,
|
|
334
|
+
vendor?: string,
|
|
335
|
+
conformsTo?: string,
|
|
336
|
+
): Envelope[] {
|
|
337
|
+
const allAttachments = this.attachments();
|
|
338
|
+
|
|
339
|
+
return allAttachments.filter((attachment) => {
|
|
340
|
+
try {
|
|
341
|
+
// The attachment is already a wrapped envelope with vendor/conformsTo assertions
|
|
342
|
+
// Check vendor if specified
|
|
343
|
+
if (vendor !== undefined) {
|
|
344
|
+
const vendorEnv = attachment.objectForPredicate(VENDOR);
|
|
345
|
+
const attachmentVendor = vendorEnv.asText();
|
|
346
|
+
if (attachmentVendor !== vendor) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Check conformsTo if specified
|
|
352
|
+
if (conformsTo !== undefined) {
|
|
353
|
+
const conformsToEnv = attachment.optionalObjectForPredicate(CONFORMS_TO);
|
|
354
|
+
if (conformsToEnv === undefined) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const conformsToText = conformsToEnv.asText();
|
|
358
|
+
if (conformsToText !== conformsTo) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
declare module "../base/envelope" {
|
|
73
|
+
interface Envelope {
|
|
74
|
+
/// Returns a compressed version of this envelope.
|
|
75
|
+
///
|
|
76
|
+
/// This method compresses the envelope using the DEFLATE algorithm,
|
|
77
|
+
/// creating a more space-efficient representation while preserving the
|
|
78
|
+
/// envelope's digest and semantic content. The compressed envelope
|
|
79
|
+
/// maintains the same digest as the original, ensuring compatibility
|
|
80
|
+
/// with the envelope's digest tree structure.
|
|
81
|
+
///
|
|
82
|
+
/// When an envelope is compressed, the entire envelope structure (including
|
|
83
|
+
/// its subject and assertions) is compressed as a single unit. The
|
|
84
|
+
/// compression preserves all the information but reduces the size of
|
|
85
|
+
/// the serialized envelope.
|
|
86
|
+
///
|
|
87
|
+
/// @returns The compressed envelope
|
|
88
|
+
/// @throws {EnvelopeError} If the envelope is already encrypted or elided
|
|
89
|
+
///
|
|
90
|
+
/// @example
|
|
91
|
+
/// ```typescript
|
|
92
|
+
/// // Create an envelope with some content
|
|
93
|
+
/// const text = "This is a fairly long text that will benefit from compression.";
|
|
94
|
+
/// const envelope = Envelope.new(text);
|
|
95
|
+
///
|
|
96
|
+
/// // Compress the envelope
|
|
97
|
+
/// const compressed = envelope.compress();
|
|
98
|
+
///
|
|
99
|
+
/// // Check that the compressed version has the same digest
|
|
100
|
+
/// console.log(envelope.digest().equals(compressed.digest())); // true
|
|
101
|
+
///
|
|
102
|
+
/// // Verify that the compressed version takes less space
|
|
103
|
+
/// console.log(compressed.cborBytes().length < envelope.cborBytes().length);
|
|
104
|
+
/// ```
|
|
105
|
+
compress(): Envelope;
|
|
106
|
+
|
|
107
|
+
/// Returns the decompressed variant of this envelope.
|
|
108
|
+
///
|
|
109
|
+
/// This method reverses the compression process, restoring the envelope to
|
|
110
|
+
/// its original decompressed form. The decompressed envelope will have
|
|
111
|
+
/// the same digest as the compressed version.
|
|
112
|
+
///
|
|
113
|
+
/// @returns The decompressed envelope
|
|
114
|
+
/// @throws {EnvelopeError} If the envelope is not compressed, missing digest, or has invalid digest
|
|
115
|
+
///
|
|
116
|
+
/// @example
|
|
117
|
+
/// ```typescript
|
|
118
|
+
/// // Create and compress an envelope
|
|
119
|
+
/// const original = Envelope.new("Hello, world!");
|
|
120
|
+
/// const compressed = original.compress();
|
|
121
|
+
///
|
|
122
|
+
/// // Decompress it
|
|
123
|
+
/// const decompressed = compressed.decompress();
|
|
124
|
+
///
|
|
125
|
+
/// // The decompressed envelope should match the original
|
|
126
|
+
/// console.log(decompressed.asText() === "Hello, world!"); // true
|
|
127
|
+
/// console.log(decompressed.digest().equals(original.digest())); // true
|
|
128
|
+
/// ```
|
|
129
|
+
decompress(): Envelope;
|
|
130
|
+
|
|
131
|
+
/// Returns this envelope with its subject compressed.
|
|
132
|
+
///
|
|
133
|
+
/// Unlike `compress()` which compresses the entire envelope, this method
|
|
134
|
+
/// only compresses the subject of the envelope, leaving the assertions
|
|
135
|
+
/// decompressed. This is useful when you want to compress a large
|
|
136
|
+
/// subject while keeping the assertions readable and accessible.
|
|
137
|
+
///
|
|
138
|
+
/// @returns A new envelope with a compressed subject
|
|
139
|
+
///
|
|
140
|
+
/// @example
|
|
141
|
+
/// ```typescript
|
|
142
|
+
/// // Create an envelope with a large subject and some assertions
|
|
143
|
+
/// const lorem = "Lorem ipsum dolor sit amet...";
|
|
144
|
+
/// const envelope = Envelope.new(lorem)
|
|
145
|
+
/// .addAssertion("note", "This is a metadata note");
|
|
146
|
+
///
|
|
147
|
+
/// // Compress just the subject
|
|
148
|
+
/// const subjectCompressed = envelope.compressSubject();
|
|
149
|
+
///
|
|
150
|
+
/// // The envelope's digest is preserved
|
|
151
|
+
/// console.log(envelope.digest().equals(subjectCompressed.digest())); // true
|
|
152
|
+
///
|
|
153
|
+
/// // The subject is now compressed
|
|
154
|
+
/// console.log(subjectCompressed.subject().isCompressed()); // true
|
|
155
|
+
/// ```
|
|
156
|
+
compressSubject(): Envelope;
|
|
157
|
+
|
|
158
|
+
/// Returns this envelope with its subject decompressed.
|
|
159
|
+
///
|
|
160
|
+
/// This method reverses the effect of `compressSubject()`, decompressing
|
|
161
|
+
/// the subject of the envelope while leaving the rest of the envelope
|
|
162
|
+
/// unchanged.
|
|
163
|
+
///
|
|
164
|
+
/// @returns A new envelope with a decompressed subject
|
|
165
|
+
///
|
|
166
|
+
/// @example
|
|
167
|
+
/// ```typescript
|
|
168
|
+
/// // Create an envelope and compress its subject
|
|
169
|
+
/// const original = Envelope.new("Hello, world!")
|
|
170
|
+
/// .addAssertion("note", "Test note");
|
|
171
|
+
/// const compressed = original.compressSubject();
|
|
172
|
+
///
|
|
173
|
+
/// // Verify the subject is compressed
|
|
174
|
+
/// console.log(compressed.subject().isCompressed()); // true
|
|
175
|
+
///
|
|
176
|
+
/// // Decompress the subject
|
|
177
|
+
/// const decompressed = compressed.decompressSubject();
|
|
178
|
+
///
|
|
179
|
+
/// // Verify the subject is now decompressed
|
|
180
|
+
/// console.log(!decompressed.subject().isCompressed()); // true
|
|
181
|
+
/// ```
|
|
182
|
+
decompressSubject(): Envelope;
|
|
183
|
+
|
|
184
|
+
/// Checks if this envelope is compressed
|
|
185
|
+
isCompressed(): boolean;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Register compression extension methods on Envelope prototype
|
|
190
|
+
/// This function is exported and called during module initialization
|
|
191
|
+
/// to ensure Envelope is fully defined before attaching methods.
|
|
192
|
+
export function registerCompressExtension(): void {
|
|
193
|
+
if (Envelope?.prototype === undefined) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Skip if already registered
|
|
198
|
+
if (typeof Envelope.prototype.compress === "function") {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Envelope.prototype.compress = function (this: Envelope): Envelope {
|
|
203
|
+
const c = this.case();
|
|
204
|
+
|
|
205
|
+
// If already compressed, return as-is
|
|
206
|
+
if (c.type === "compressed") {
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Can't compress encrypted or elided envelopes
|
|
211
|
+
if (c.type === "encrypted") {
|
|
212
|
+
throw EnvelopeError.general("Cannot compress encrypted envelope");
|
|
213
|
+
}
|
|
214
|
+
if (c.type === "elided") {
|
|
215
|
+
throw EnvelopeError.general("Cannot compress elided envelope");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Compress the entire envelope
|
|
219
|
+
const cbor = this.taggedCbor();
|
|
220
|
+
|
|
221
|
+
const decompressedData = cborData(cbor);
|
|
222
|
+
|
|
223
|
+
const compressed = Compressed.fromDecompressedData(decompressedData, this.digest());
|
|
224
|
+
|
|
225
|
+
// Create a compressed envelope case
|
|
226
|
+
return Envelope.fromCase({ type: "compressed", value: compressed });
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/// Implementation of decompress()
|
|
230
|
+
Envelope.prototype.decompress = function (this: Envelope): Envelope {
|
|
231
|
+
const c = this.case();
|
|
232
|
+
|
|
233
|
+
if (c.type !== "compressed") {
|
|
234
|
+
throw EnvelopeError.general("Envelope is not compressed");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const compressed = c.value;
|
|
238
|
+
const digest = compressed.digestOpt();
|
|
239
|
+
|
|
240
|
+
if (digest === undefined) {
|
|
241
|
+
throw EnvelopeError.general("Missing digest in compressed envelope");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Verify the digest matches
|
|
245
|
+
if (!digest.equals(this.digest())) {
|
|
246
|
+
throw EnvelopeError.general("Invalid digest in compressed envelope");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Decompress the data
|
|
250
|
+
const decompressedData = compressed.decompress();
|
|
251
|
+
|
|
252
|
+
// Parse back to envelope
|
|
253
|
+
|
|
254
|
+
const cbor = decodeCbor(decompressedData);
|
|
255
|
+
const envelope = Envelope.fromTaggedCbor(cbor);
|
|
256
|
+
|
|
257
|
+
// Verify the decompressed envelope has the correct digest
|
|
258
|
+
if (!envelope.digest().equals(digest)) {
|
|
259
|
+
throw EnvelopeError.general("Invalid digest after decompression");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return envelope;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/// Implementation of compressSubject()
|
|
266
|
+
Envelope.prototype.compressSubject = function (this: Envelope): Envelope {
|
|
267
|
+
if (this.subject().isCompressed()) {
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const subject = this.subject().compress();
|
|
272
|
+
return this.replaceSubject(subject);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/// Implementation of decompressSubject()
|
|
276
|
+
Envelope.prototype.decompressSubject = function (this: Envelope): Envelope {
|
|
277
|
+
if (this.subject().isCompressed()) {
|
|
278
|
+
const subject = this.subject().decompress();
|
|
279
|
+
return this.replaceSubject(subject);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/// Implementation of isCompressed()
|
|
286
|
+
Envelope.prototype.isCompressed = function (this: Envelope): boolean {
|
|
287
|
+
return this.case().type === "compressed";
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Auto-register on module load - will be called again from index.ts
|
|
292
|
+
// to ensure proper ordering after all modules are loaded
|
|
293
|
+
registerCompressExtension();
|