@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,526 @@
1
+ import { type Digest, type DigestProvider } from "./digest";
2
+ import { Envelope } from "./envelope";
3
+ import { Assertion } from "./assertion";
4
+ import { EnvelopeError } from "./error";
5
+
6
+ /// Types of obscuration that can be applied to envelope elements.
7
+ ///
8
+ /// This enum identifies the different ways an envelope element can be obscured.
9
+ export enum ObscureType {
10
+ /// The element has been elided, showing only its digest.
11
+ Elided = "elided",
12
+
13
+ /// The element has been encrypted using symmetric encryption.
14
+ /// TODO: Implement when encrypt feature is added
15
+ Encrypted = "encrypted",
16
+
17
+ /// The element has been compressed to reduce its size.
18
+ /// TODO: Implement when compress feature is added
19
+ Compressed = "compressed",
20
+ }
21
+
22
+ /// Actions that can be performed on parts of an envelope to obscure them.
23
+ ///
24
+ /// Gordian Envelope supports several ways to obscure parts of an envelope while
25
+ /// maintaining its semantic integrity and digest tree.
26
+ export type ObscureAction =
27
+ | { type: "elide" }
28
+ | { type: "encrypt"; key: unknown } // TODO: SymmetricKey type
29
+ | { type: "compress" };
30
+
31
+ /// Helper to create elide action
32
+ export function elideAction(): ObscureAction {
33
+ return { type: "elide" };
34
+ }
35
+
36
+ /// Support for eliding elements from envelopes.
37
+ declare module "./envelope" {
38
+ interface Envelope {
39
+ /// Returns the elided variant of this envelope.
40
+ ///
41
+ /// Elision replaces an envelope with just its digest, hiding its content
42
+ /// while maintaining the integrity of the envelope's digest tree.
43
+ ///
44
+ /// @returns The elided envelope
45
+ elide(): Envelope;
46
+
47
+ /// Returns a version of this envelope with elements in the target set
48
+ /// obscured using the specified action.
49
+ ///
50
+ /// @param target - The set of digests that identify elements to be obscured
51
+ /// @param action - The action to perform on the targeted elements
52
+ /// @returns The modified envelope
53
+ elideRemovingSetWithAction(target: Set<Digest>, action: ObscureAction): Envelope;
54
+
55
+ /// Returns a version of this envelope with elements in the target set
56
+ /// elided.
57
+ ///
58
+ /// @param target - The set of digests that identify elements to be elided
59
+ /// @returns The modified envelope
60
+ elideRemovingSet(target: Set<Digest>): Envelope;
61
+
62
+ /// Returns a version of this envelope with elements in the target array
63
+ /// obscured using the specified action.
64
+ ///
65
+ /// @param target - An array of DigestProviders
66
+ /// @param action - The action to perform
67
+ /// @returns The modified envelope
68
+ elideRemovingArrayWithAction(target: DigestProvider[], action: ObscureAction): Envelope;
69
+
70
+ /// Returns a version of this envelope with elements in the target array
71
+ /// elided.
72
+ ///
73
+ /// @param target - An array of DigestProviders
74
+ /// @returns The modified envelope
75
+ elideRemovingArray(target: DigestProvider[]): Envelope;
76
+
77
+ /// Returns a version of this envelope with the target element obscured.
78
+ ///
79
+ /// @param target - A DigestProvider
80
+ /// @param action - The action to perform
81
+ /// @returns The modified envelope
82
+ elideRemovingTargetWithAction(target: DigestProvider, action: ObscureAction): Envelope;
83
+
84
+ /// Returns a version of this envelope with the target element elided.
85
+ ///
86
+ /// @param target - A DigestProvider
87
+ /// @returns The modified envelope
88
+ elideRemovingTarget(target: DigestProvider): Envelope;
89
+
90
+ /// Returns a version of this envelope with only elements in the target set
91
+ /// revealed, and all other elements obscured.
92
+ ///
93
+ /// @param target - The set of digests that identify elements to be revealed
94
+ /// @param action - The action to perform on other elements
95
+ /// @returns The modified envelope
96
+ elideRevealingSetWithAction(target: Set<Digest>, action: ObscureAction): Envelope;
97
+
98
+ /// Returns a version of this envelope with only elements in the target set
99
+ /// revealed.
100
+ ///
101
+ /// @param target - The set of digests that identify elements to be revealed
102
+ /// @returns The modified envelope
103
+ elideRevealingSet(target: Set<Digest>): Envelope;
104
+
105
+ /// Returns a version of this envelope with elements not in the target array
106
+ /// obscured.
107
+ ///
108
+ /// @param target - An array of DigestProviders
109
+ /// @param action - The action to perform
110
+ /// @returns The modified envelope
111
+ elideRevealingArrayWithAction(target: DigestProvider[], action: ObscureAction): Envelope;
112
+
113
+ /// Returns a version of this envelope with elements not in the target array
114
+ /// elided.
115
+ ///
116
+ /// @param target - An array of DigestProviders
117
+ /// @returns The modified envelope
118
+ elideRevealingArray(target: DigestProvider[]): Envelope;
119
+
120
+ /// Returns a version of this envelope with all elements except the target
121
+ /// element obscured.
122
+ ///
123
+ /// @param target - A DigestProvider
124
+ /// @param action - The action to perform
125
+ /// @returns The modified envelope
126
+ elideRevealingTargetWithAction(target: DigestProvider, action: ObscureAction): Envelope;
127
+
128
+ /// Returns a version of this envelope with all elements except the target
129
+ /// element elided.
130
+ ///
131
+ /// @param target - A DigestProvider
132
+ /// @returns The modified envelope
133
+ elideRevealingTarget(target: DigestProvider): Envelope;
134
+
135
+ /// Returns the unelided variant of this envelope by revealing the original
136
+ /// content.
137
+ ///
138
+ /// @param envelope - The original unelided envelope
139
+ /// @returns The revealed envelope
140
+ /// @throws {EnvelopeError} If digests don't match
141
+ unelide(envelope: Envelope): Envelope;
142
+
143
+ /// Returns the set of digests of nodes matching the specified criteria.
144
+ ///
145
+ /// @param targetDigests - Optional set of digests to filter by
146
+ /// @param obscureTypes - Array of ObscureType values to match against
147
+ /// @returns A Set of matching digests
148
+ nodesMatching(targetDigests: Set<Digest> | undefined, obscureTypes: ObscureType[]): Set<Digest>;
149
+
150
+ /// Returns a new envelope with elided nodes restored from the provided set.
151
+ ///
152
+ /// @param envelopes - An array of envelopes that may match elided nodes
153
+ /// @returns The envelope with restored nodes
154
+ walkUnelide(envelopes: Envelope[]): Envelope;
155
+
156
+ /// Returns a new envelope with nodes matching target digests replaced.
157
+ ///
158
+ /// @param target - Set of digests identifying nodes to replace
159
+ /// @param replacement - The envelope to use for replacement
160
+ /// @returns The modified envelope
161
+ /// @throws {EnvelopeError} If replacement is invalid
162
+ walkReplace(target: Set<Digest>, replacement: Envelope): Envelope;
163
+
164
+ /// Checks if two envelopes are identical (same structure and content).
165
+ ///
166
+ /// @param other - The other envelope to compare with
167
+ /// @returns `true` if identical
168
+ isIdenticalTo(other: Envelope): boolean;
169
+ }
170
+ }
171
+
172
+ /// Implementation of elide()
173
+ Envelope.prototype.elide = function (this: Envelope): Envelope {
174
+ const c = this.case();
175
+ if (c.type === "elided") {
176
+ return this;
177
+ }
178
+ return Envelope.newElided(this.digest());
179
+ };
180
+
181
+ /// Core elision logic
182
+ function elideSetWithAction(
183
+ envelope: Envelope,
184
+ target: Set<Digest>,
185
+ isRevealing: boolean,
186
+ action: ObscureAction,
187
+ ): Envelope {
188
+ const selfDigest = envelope.digest();
189
+ const targetContainsSelf = Array.from(target).some((d) => d.equals(selfDigest));
190
+
191
+ // Target Matches isRevealing elide
192
+ // false false false
193
+ // false true true
194
+ // true false true
195
+ // true true false
196
+
197
+ if (targetContainsSelf !== isRevealing) {
198
+ // Should obscure this envelope
199
+ if (action.type === "elide") {
200
+ return envelope.elide();
201
+ } else if (action.type === "encrypt") {
202
+ // TODO: Implement encryption
203
+ throw new Error("Encryption not yet implemented");
204
+ } else if (action.type === "compress") {
205
+ // TODO: Implement compression
206
+ throw new Error("Compression not yet implemented");
207
+ }
208
+ }
209
+
210
+ const c = envelope.case();
211
+
212
+ // Recursively process structure
213
+ if (c.type === "assertion") {
214
+ const predicate = elideSetWithAction(c.assertion.predicate(), target, isRevealing, action);
215
+ const object = elideSetWithAction(c.assertion.object(), target, isRevealing, action);
216
+ const elidedAssertion = new Assertion(predicate, object);
217
+ return Envelope.newWithAssertion(elidedAssertion);
218
+ } else if (c.type === "node") {
219
+ const elidedSubject = elideSetWithAction(c.subject, target, isRevealing, action);
220
+ const elidedAssertions = c.assertions.map((a) =>
221
+ elideSetWithAction(a, target, isRevealing, action),
222
+ );
223
+ return Envelope.newWithUncheckedAssertions(elidedSubject, elidedAssertions);
224
+ } else if (c.type === "wrapped") {
225
+ const elidedEnvelope = elideSetWithAction(c.envelope, target, isRevealing, action);
226
+ return Envelope.newWrapped(elidedEnvelope);
227
+ }
228
+
229
+ return envelope;
230
+ }
231
+
232
+ /// Implementation of elideRemovingSetWithAction
233
+ Envelope.prototype.elideRemovingSetWithAction = function (
234
+ this: Envelope,
235
+ target: Set<Digest>,
236
+ action: ObscureAction,
237
+ ): Envelope {
238
+ return elideSetWithAction(this, target, false, action);
239
+ };
240
+
241
+ /// Implementation of elideRemovingSet
242
+ Envelope.prototype.elideRemovingSet = function (this: Envelope, target: Set<Digest>): Envelope {
243
+ return elideSetWithAction(this, target, false, elideAction());
244
+ };
245
+
246
+ /// Implementation of elideRemovingArrayWithAction
247
+ Envelope.prototype.elideRemovingArrayWithAction = function (
248
+ this: Envelope,
249
+ target: DigestProvider[],
250
+ action: ObscureAction,
251
+ ): Envelope {
252
+ const targetSet = new Set(target.map((p) => p.digest()));
253
+ return elideSetWithAction(this, targetSet, false, action);
254
+ };
255
+
256
+ /// Implementation of elideRemovingArray
257
+ Envelope.prototype.elideRemovingArray = function (
258
+ this: Envelope,
259
+ target: DigestProvider[],
260
+ ): Envelope {
261
+ const targetSet = new Set(target.map((p) => p.digest()));
262
+ return elideSetWithAction(this, targetSet, false, elideAction());
263
+ };
264
+
265
+ /// Implementation of elideRemovingTargetWithAction
266
+ Envelope.prototype.elideRemovingTargetWithAction = function (
267
+ this: Envelope,
268
+ target: DigestProvider,
269
+ action: ObscureAction,
270
+ ): Envelope {
271
+ return this.elideRemovingArrayWithAction([target], action);
272
+ };
273
+
274
+ /// Implementation of elideRemovingTarget
275
+ Envelope.prototype.elideRemovingTarget = function (
276
+ this: Envelope,
277
+ target: DigestProvider,
278
+ ): Envelope {
279
+ return this.elideRemovingArray([target]);
280
+ };
281
+
282
+ /// Implementation of elideRevealingSetWithAction
283
+ Envelope.prototype.elideRevealingSetWithAction = function (
284
+ this: Envelope,
285
+ target: Set<Digest>,
286
+ action: ObscureAction,
287
+ ): Envelope {
288
+ return elideSetWithAction(this, target, true, action);
289
+ };
290
+
291
+ /// Implementation of elideRevealingSet
292
+ Envelope.prototype.elideRevealingSet = function (this: Envelope, target: Set<Digest>): Envelope {
293
+ return elideSetWithAction(this, target, true, elideAction());
294
+ };
295
+
296
+ /// Implementation of elideRevealingArrayWithAction
297
+ Envelope.prototype.elideRevealingArrayWithAction = function (
298
+ this: Envelope,
299
+ target: DigestProvider[],
300
+ action: ObscureAction,
301
+ ): Envelope {
302
+ const targetSet = new Set(target.map((p) => p.digest()));
303
+ return elideSetWithAction(this, targetSet, true, action);
304
+ };
305
+
306
+ /// Implementation of elideRevealingArray
307
+ Envelope.prototype.elideRevealingArray = function (
308
+ this: Envelope,
309
+ target: DigestProvider[],
310
+ ): Envelope {
311
+ const targetSet = new Set(target.map((p) => p.digest()));
312
+ return elideSetWithAction(this, targetSet, true, elideAction());
313
+ };
314
+
315
+ /// Implementation of elideRevealingTargetWithAction
316
+ Envelope.prototype.elideRevealingTargetWithAction = function (
317
+ this: Envelope,
318
+ target: DigestProvider,
319
+ action: ObscureAction,
320
+ ): Envelope {
321
+ return this.elideRevealingArrayWithAction([target], action);
322
+ };
323
+
324
+ /// Implementation of elideRevealingTarget
325
+ Envelope.prototype.elideRevealingTarget = function (
326
+ this: Envelope,
327
+ target: DigestProvider,
328
+ ): Envelope {
329
+ return this.elideRevealingArray([target]);
330
+ };
331
+
332
+ /// Implementation of unelide
333
+ Envelope.prototype.unelide = function (this: Envelope, envelope: Envelope): Envelope {
334
+ if (this.digest().equals(envelope.digest())) {
335
+ return envelope;
336
+ }
337
+ throw EnvelopeError.invalidDigest();
338
+ };
339
+
340
+ /// Implementation of nodesMatching
341
+ Envelope.prototype.nodesMatching = function (
342
+ this: Envelope,
343
+ targetDigests: Set<Digest> | undefined,
344
+ obscureTypes: ObscureType[],
345
+ ): Set<Digest> {
346
+ const result = new Set<Digest>();
347
+
348
+ const visitor = (envelope: Envelope): void => {
349
+ // Check if this node matches the target digests
350
+ const digestMatches =
351
+ targetDigests === undefined ||
352
+ Array.from(targetDigests).some((d) => d.equals(envelope.digest()));
353
+
354
+ if (!digestMatches) {
355
+ return;
356
+ }
357
+
358
+ // If no obscure types specified, include all nodes
359
+ if (obscureTypes.length === 0) {
360
+ result.add(envelope.digest());
361
+ return;
362
+ }
363
+
364
+ // Check if this node matches any of the specified obscure types
365
+ const c = envelope.case();
366
+ const typeMatches = obscureTypes.some((obscureType) => {
367
+ if (obscureType === ObscureType.Elided && c.type === "elided") {
368
+ return true;
369
+ }
370
+ if (obscureType === ObscureType.Encrypted && c.type === "encrypted") {
371
+ return true;
372
+ }
373
+ if (obscureType === ObscureType.Compressed && c.type === "compressed") {
374
+ return true;
375
+ }
376
+ return false;
377
+ });
378
+
379
+ if (typeMatches) {
380
+ result.add(envelope.digest());
381
+ }
382
+ };
383
+
384
+ // Walk the envelope tree
385
+ walkEnvelope(this, visitor);
386
+
387
+ return result;
388
+ };
389
+
390
+ /// Helper to walk envelope tree
391
+ function walkEnvelope(envelope: Envelope, visitor: (e: Envelope) => void): void {
392
+ visitor(envelope);
393
+
394
+ const c = envelope.case();
395
+ if (c.type === "node") {
396
+ walkEnvelope(c.subject, visitor);
397
+ for (const assertion of c.assertions) {
398
+ walkEnvelope(assertion, visitor);
399
+ }
400
+ } else if (c.type === "assertion") {
401
+ walkEnvelope(c.assertion.predicate(), visitor);
402
+ walkEnvelope(c.assertion.object(), visitor);
403
+ } else if (c.type === "wrapped") {
404
+ walkEnvelope(c.envelope, visitor);
405
+ }
406
+ }
407
+
408
+ /// Implementation of walkUnelide
409
+ Envelope.prototype.walkUnelide = function (this: Envelope, envelopes: Envelope[]): Envelope {
410
+ // Build a lookup map of digest -> envelope
411
+ const envelopeMap = new Map<string, Envelope>();
412
+ for (const env of envelopes) {
413
+ envelopeMap.set(env.digest().hex(), env);
414
+ }
415
+
416
+ return walkUnelideWithMap(this, envelopeMap);
417
+ };
418
+
419
+ /// Helper for walkUnelide with map
420
+ function walkUnelideWithMap(envelope: Envelope, envelopeMap: Map<string, Envelope>): Envelope {
421
+ const c = envelope.case();
422
+
423
+ if (c.type === "elided") {
424
+ // Try to find a matching envelope to restore
425
+ const replacement = envelopeMap.get(envelope.digest().hex());
426
+ return replacement ?? envelope;
427
+ }
428
+
429
+ if (c.type === "node") {
430
+ const newSubject = walkUnelideWithMap(c.subject, envelopeMap);
431
+ const newAssertions = c.assertions.map((a) => walkUnelideWithMap(a, envelopeMap));
432
+
433
+ if (
434
+ newSubject.isIdenticalTo(c.subject) &&
435
+ newAssertions.every((a, i) => a.isIdenticalTo(c.assertions[i]))
436
+ ) {
437
+ return envelope;
438
+ }
439
+
440
+ return Envelope.newWithUncheckedAssertions(newSubject, newAssertions);
441
+ }
442
+
443
+ if (c.type === "wrapped") {
444
+ const newEnvelope = walkUnelideWithMap(c.envelope, envelopeMap);
445
+ if (newEnvelope.isIdenticalTo(c.envelope)) {
446
+ return envelope;
447
+ }
448
+ return Envelope.newWrapped(newEnvelope);
449
+ }
450
+
451
+ if (c.type === "assertion") {
452
+ const newPredicate = walkUnelideWithMap(c.assertion.predicate(), envelopeMap);
453
+ const newObject = walkUnelideWithMap(c.assertion.object(), envelopeMap);
454
+
455
+ if (
456
+ newPredicate.isIdenticalTo(c.assertion.predicate()) &&
457
+ newObject.isIdenticalTo(c.assertion.object())
458
+ ) {
459
+ return envelope;
460
+ }
461
+
462
+ return Envelope.newAssertion(newPredicate, newObject);
463
+ }
464
+
465
+ return envelope;
466
+ }
467
+
468
+ /// Implementation of walkReplace
469
+ Envelope.prototype.walkReplace = function (
470
+ this: Envelope,
471
+ target: Set<Digest>,
472
+ replacement: Envelope,
473
+ ): Envelope {
474
+ // Check if this node matches the target
475
+ if (Array.from(target).some((d) => d.equals(this.digest()))) {
476
+ return replacement;
477
+ }
478
+
479
+ const c = this.case();
480
+
481
+ if (c.type === "node") {
482
+ const newSubject = c.subject.walkReplace(target, replacement);
483
+ const newAssertions = c.assertions.map((a) => a.walkReplace(target, replacement));
484
+
485
+ if (
486
+ newSubject.isIdenticalTo(c.subject) &&
487
+ newAssertions.every((a, i) => a.isIdenticalTo(c.assertions[i]))
488
+ ) {
489
+ return this;
490
+ }
491
+
492
+ // Validate that all assertions are either assertions or obscured
493
+ return Envelope.newWithAssertions(newSubject, newAssertions);
494
+ }
495
+
496
+ if (c.type === "wrapped") {
497
+ const newEnvelope = c.envelope.walkReplace(target, replacement);
498
+ if (newEnvelope.isIdenticalTo(c.envelope)) {
499
+ return this;
500
+ }
501
+ return Envelope.newWrapped(newEnvelope);
502
+ }
503
+
504
+ if (c.type === "assertion") {
505
+ const newPredicate = c.assertion.predicate().walkReplace(target, replacement);
506
+ const newObject = c.assertion.object().walkReplace(target, replacement);
507
+
508
+ if (
509
+ newPredicate.isIdenticalTo(c.assertion.predicate()) &&
510
+ newObject.isIdenticalTo(c.assertion.object())
511
+ ) {
512
+ return this;
513
+ }
514
+
515
+ return Envelope.newAssertion(newPredicate, newObject);
516
+ }
517
+
518
+ return this;
519
+ };
520
+
521
+ /// Implementation of isIdenticalTo
522
+ Envelope.prototype.isIdenticalTo = function (this: Envelope, other: Envelope): boolean {
523
+ // Two envelopes are identical if they have the same digest
524
+ // and the same case type (to handle wrapped vs unwrapped with same content)
525
+ return this.digest().equals(other.digest()) && this.case().type === other.case().type;
526
+ };