@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.
- 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 +782 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +782 -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 +84 -0
- package/src/base/assertion.ts +179 -0
- package/src/base/assertions.ts +153 -0
- package/src/base/cbor.ts +122 -0
- package/src/base/digest.ts +204 -0
- package/src/base/elide.ts +390 -0
- package/src/base/envelope-decodable.ts +186 -0
- package/src/base/envelope-encodable.ts +71 -0
- package/src/base/envelope.ts +988 -0
- package/src/base/error.ts +421 -0
- package/src/base/index.ts +56 -0
- package/src/base/leaf.ts +147 -0
- package/src/base/queries.ts +244 -0
- package/src/base/walk.ts +215 -0
- package/src/base/wrap.ts +26 -0
- package/src/extension/attachment.ts +280 -0
- package/src/extension/compress.ts +176 -0
- package/src/extension/encrypt.ts +297 -0
- package/src/extension/expression.ts +404 -0
- package/src/extension/index.ts +72 -0
- package/src/extension/proof.ts +227 -0
- package/src/extension/recipient.ts +440 -0
- package/src/extension/salt.ts +114 -0
- package/src/extension/signature.ts +398 -0
- package/src/extension/types.ts +92 -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,204 @@
|
|
|
1
|
+
import { sha256 } from "@bcts/crypto";
|
|
2
|
+
|
|
3
|
+
/// A cryptographic digest used to uniquely identify digital objects.
|
|
4
|
+
///
|
|
5
|
+
/// Digests in Gordian Envelope are always SHA-256 hashes (32 bytes).
|
|
6
|
+
/// This is a fundamental building block for the Merkle-like digest tree
|
|
7
|
+
/// that enables privacy features while maintaining integrity.
|
|
8
|
+
///
|
|
9
|
+
/// Based on BCR-2021-002: Digests for Digital Objects
|
|
10
|
+
/// @see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2021-002-digest.md
|
|
11
|
+
export class Digest {
|
|
12
|
+
readonly #data: Uint8Array;
|
|
13
|
+
|
|
14
|
+
/// Creates a new Digest from raw bytes.
|
|
15
|
+
///
|
|
16
|
+
/// @param data - The 32-byte digest data
|
|
17
|
+
/// @throws {Error} If data is not exactly 32 bytes
|
|
18
|
+
constructor(data: Uint8Array) {
|
|
19
|
+
if (data.length !== 32) {
|
|
20
|
+
throw new Error(`Digest must be exactly 32 bytes, got ${data.length} bytes`);
|
|
21
|
+
}
|
|
22
|
+
this.#data = data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Returns the raw digest bytes.
|
|
26
|
+
///
|
|
27
|
+
/// @returns A Uint8Array containing the 32-byte digest
|
|
28
|
+
data(): Uint8Array {
|
|
29
|
+
return this.#data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Creates a digest from an image (arbitrary byte array).
|
|
33
|
+
///
|
|
34
|
+
/// This is the primary way to create a digest from data. The data is
|
|
35
|
+
/// hashed using SHA-256 to produce a 32-byte digest.
|
|
36
|
+
///
|
|
37
|
+
/// @param image - The data to hash
|
|
38
|
+
/// @returns A new Digest instance
|
|
39
|
+
///
|
|
40
|
+
/// @example
|
|
41
|
+
/// ```typescript
|
|
42
|
+
/// const digest = Digest.fromImage(new TextEncoder().encode("Hello, world!"));
|
|
43
|
+
/// ```
|
|
44
|
+
static fromImage(image: Uint8Array): Digest {
|
|
45
|
+
const hash = sha256(image);
|
|
46
|
+
return new Digest(hash);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Creates a digest from multiple digests.
|
|
50
|
+
///
|
|
51
|
+
/// This is used to combine digests in the Merkle-like tree structure.
|
|
52
|
+
/// The digests are concatenated and then hashed.
|
|
53
|
+
///
|
|
54
|
+
/// @param digests - An array of digests to combine
|
|
55
|
+
/// @returns A new Digest instance representing the combined digests
|
|
56
|
+
///
|
|
57
|
+
/// @example
|
|
58
|
+
/// ```typescript
|
|
59
|
+
/// const digest1 = Digest.fromImage(data1);
|
|
60
|
+
/// const digest2 = Digest.fromImage(data2);
|
|
61
|
+
/// const combined = Digest.fromDigests([digest1, digest2]);
|
|
62
|
+
/// ```
|
|
63
|
+
static fromDigests(digests: Digest[]): Digest {
|
|
64
|
+
const totalLength = digests.length * 32;
|
|
65
|
+
const combined = new Uint8Array(totalLength);
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const digest of digests) {
|
|
68
|
+
combined.set(digest.data(), offset);
|
|
69
|
+
offset += 32;
|
|
70
|
+
}
|
|
71
|
+
return Digest.fromImage(combined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Returns the hexadecimal string representation of the digest.
|
|
75
|
+
///
|
|
76
|
+
/// @returns A 64-character hexadecimal string
|
|
77
|
+
///
|
|
78
|
+
/// @example
|
|
79
|
+
/// ```typescript
|
|
80
|
+
/// const digest = Digest.fromImage(data);
|
|
81
|
+
/// console.log(digest.hex()); // "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"
|
|
82
|
+
/// ```
|
|
83
|
+
hex(): string {
|
|
84
|
+
return Array.from(this.#data)
|
|
85
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
86
|
+
.join("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Returns an abbreviated hexadecimal representation for visual comparison.
|
|
90
|
+
///
|
|
91
|
+
/// Following Blockchain Commons conventions, this returns the first 7
|
|
92
|
+
/// hexadecimal digits of the digest, which provides sufficient entropy
|
|
93
|
+
/// for human visual comparison while being easy to read.
|
|
94
|
+
///
|
|
95
|
+
/// @returns A 7-character hexadecimal string
|
|
96
|
+
///
|
|
97
|
+
/// @example
|
|
98
|
+
/// ```typescript
|
|
99
|
+
/// const digest = Digest.fromImage(data);
|
|
100
|
+
/// console.log(digest.short()); // "5feceb6"
|
|
101
|
+
/// ```
|
|
102
|
+
short(): string {
|
|
103
|
+
return this.hex().substring(0, 7);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Creates a digest from a hexadecimal string.
|
|
107
|
+
///
|
|
108
|
+
/// @param hex - A 64-character hexadecimal string
|
|
109
|
+
/// @returns A new Digest instance
|
|
110
|
+
/// @throws {Error} If the hex string is not exactly 64 characters
|
|
111
|
+
///
|
|
112
|
+
/// @example
|
|
113
|
+
/// ```typescript
|
|
114
|
+
/// const digest = Digest.fromHex("5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9");
|
|
115
|
+
/// ```
|
|
116
|
+
static fromHex(hex: string): Digest {
|
|
117
|
+
if (hex.length !== 64) {
|
|
118
|
+
throw new Error(`Hex string must be exactly 64 characters, got ${hex.length}`);
|
|
119
|
+
}
|
|
120
|
+
const data = new Uint8Array(32);
|
|
121
|
+
for (let i = 0; i < 32; i++) {
|
|
122
|
+
data[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
123
|
+
}
|
|
124
|
+
return new Digest(data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Checks if two digests are equal.
|
|
128
|
+
///
|
|
129
|
+
/// @param other - The other digest to compare with
|
|
130
|
+
/// @returns `true` if the digests are equal, `false` otherwise
|
|
131
|
+
equals(other: Digest): boolean {
|
|
132
|
+
if (this.#data.length !== other.#data.length) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < this.#data.length; i++) {
|
|
136
|
+
if (this.#data[i] !== other.#data[i]) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Returns a string representation of the digest (short form).
|
|
144
|
+
///
|
|
145
|
+
/// @returns The short hexadecimal representation
|
|
146
|
+
toString(): string {
|
|
147
|
+
return this.short();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Creates a deep copy of the digest.
|
|
151
|
+
///
|
|
152
|
+
/// @returns A new Digest instance with the same data
|
|
153
|
+
clone(): Digest {
|
|
154
|
+
return new Digest(new Uint8Array(this.#data));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Trait for types that can provide a digest.
|
|
159
|
+
///
|
|
160
|
+
/// This is equivalent to Rust's `DigestProvider` trait. Types that
|
|
161
|
+
/// implement this interface can be used in contexts where a digest
|
|
162
|
+
/// is needed for identity or integrity verification.
|
|
163
|
+
export interface DigestProvider {
|
|
164
|
+
/// Returns the digest of this object.
|
|
165
|
+
///
|
|
166
|
+
/// The digest uniquely identifies the semantic content of the object,
|
|
167
|
+
/// regardless of whether parts of it are elided, encrypted, or compressed.
|
|
168
|
+
digest(): Digest;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Helper function to create a digest from a string.
|
|
172
|
+
///
|
|
173
|
+
/// This is a convenience function for creating digests from text strings,
|
|
174
|
+
/// which are encoded as UTF-8 before hashing.
|
|
175
|
+
///
|
|
176
|
+
/// @param text - The text to hash
|
|
177
|
+
/// @returns A new Digest instance
|
|
178
|
+
///
|
|
179
|
+
/// @example
|
|
180
|
+
/// ```typescript
|
|
181
|
+
/// const digest = digestFromString("Hello, world!");
|
|
182
|
+
/// ```
|
|
183
|
+
export function digestFromString(text: string): Digest {
|
|
184
|
+
const encoder = new TextEncoder();
|
|
185
|
+
return Digest.fromImage(encoder.encode(text));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Helper function to create a digest from a number.
|
|
189
|
+
///
|
|
190
|
+
/// The number is converted to a big-endian byte representation before hashing.
|
|
191
|
+
///
|
|
192
|
+
/// @param num - The number to hash
|
|
193
|
+
/// @returns A new Digest instance
|
|
194
|
+
///
|
|
195
|
+
/// @example
|
|
196
|
+
/// ```typescript
|
|
197
|
+
/// const digest = digestFromNumber(42);
|
|
198
|
+
/// ```
|
|
199
|
+
export function digestFromNumber(num: number): Digest {
|
|
200
|
+
const buffer = new ArrayBuffer(8);
|
|
201
|
+
const view = new DataView(buffer);
|
|
202
|
+
view.setFloat64(0, num, false); // big-endian
|
|
203
|
+
return Digest.fromImage(new Uint8Array(buffer));
|
|
204
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
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
|
+
/// Implementation of elide()
|
|
37
|
+
Envelope.prototype.elide = function (this: Envelope): Envelope {
|
|
38
|
+
const c = this.case();
|
|
39
|
+
if (c.type === "elided") {
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
return Envelope.newElided(this.digest());
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/// Core elision logic
|
|
46
|
+
function elideSetWithAction(
|
|
47
|
+
envelope: Envelope,
|
|
48
|
+
target: Set<Digest>,
|
|
49
|
+
isRevealing: boolean,
|
|
50
|
+
action: ObscureAction,
|
|
51
|
+
): Envelope {
|
|
52
|
+
const selfDigest = envelope.digest();
|
|
53
|
+
const targetContainsSelf = Array.from(target).some((d) => d.equals(selfDigest));
|
|
54
|
+
|
|
55
|
+
// Target Matches isRevealing elide
|
|
56
|
+
// false false false
|
|
57
|
+
// false true true
|
|
58
|
+
// true false true
|
|
59
|
+
// true true false
|
|
60
|
+
|
|
61
|
+
if (targetContainsSelf !== isRevealing) {
|
|
62
|
+
// Should obscure this envelope
|
|
63
|
+
if (action.type === "elide") {
|
|
64
|
+
return envelope.elide();
|
|
65
|
+
} else if (action.type === "encrypt") {
|
|
66
|
+
// TODO: Implement encryption
|
|
67
|
+
throw new Error("Encryption not yet implemented");
|
|
68
|
+
} else if (action.type === "compress") {
|
|
69
|
+
// TODO: Implement compression
|
|
70
|
+
throw new Error("Compression not yet implemented");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const c = envelope.case();
|
|
75
|
+
|
|
76
|
+
// Recursively process structure
|
|
77
|
+
if (c.type === "assertion") {
|
|
78
|
+
const predicate = elideSetWithAction(c.assertion.predicate(), target, isRevealing, action);
|
|
79
|
+
const object = elideSetWithAction(c.assertion.object(), target, isRevealing, action);
|
|
80
|
+
const elidedAssertion = new Assertion(predicate, object);
|
|
81
|
+
return Envelope.newWithAssertion(elidedAssertion);
|
|
82
|
+
} else if (c.type === "node") {
|
|
83
|
+
const elidedSubject = elideSetWithAction(c.subject, target, isRevealing, action);
|
|
84
|
+
const elidedAssertions = c.assertions.map((a) =>
|
|
85
|
+
elideSetWithAction(a, target, isRevealing, action),
|
|
86
|
+
);
|
|
87
|
+
return Envelope.newWithUncheckedAssertions(elidedSubject, elidedAssertions);
|
|
88
|
+
} else if (c.type === "wrapped") {
|
|
89
|
+
const elidedEnvelope = elideSetWithAction(c.envelope, target, isRevealing, action);
|
|
90
|
+
return Envelope.newWrapped(elidedEnvelope);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return envelope;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Implementation of elideRemovingSetWithAction
|
|
97
|
+
Envelope.prototype.elideRemovingSetWithAction = function (
|
|
98
|
+
this: Envelope,
|
|
99
|
+
target: Set<Digest>,
|
|
100
|
+
action: ObscureAction,
|
|
101
|
+
): Envelope {
|
|
102
|
+
return elideSetWithAction(this, target, false, action);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/// Implementation of elideRemovingSet
|
|
106
|
+
Envelope.prototype.elideRemovingSet = function (this: Envelope, target: Set<Digest>): Envelope {
|
|
107
|
+
return elideSetWithAction(this, target, false, elideAction());
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/// Implementation of elideRemovingArrayWithAction
|
|
111
|
+
Envelope.prototype.elideRemovingArrayWithAction = function (
|
|
112
|
+
this: Envelope,
|
|
113
|
+
target: DigestProvider[],
|
|
114
|
+
action: ObscureAction,
|
|
115
|
+
): Envelope {
|
|
116
|
+
const targetSet = new Set(target.map((p) => p.digest()));
|
|
117
|
+
return elideSetWithAction(this, targetSet, false, action);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/// Implementation of elideRemovingArray
|
|
121
|
+
Envelope.prototype.elideRemovingArray = function (
|
|
122
|
+
this: Envelope,
|
|
123
|
+
target: DigestProvider[],
|
|
124
|
+
): Envelope {
|
|
125
|
+
const targetSet = new Set(target.map((p) => p.digest()));
|
|
126
|
+
return elideSetWithAction(this, targetSet, false, elideAction());
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/// Implementation of elideRemovingTargetWithAction
|
|
130
|
+
Envelope.prototype.elideRemovingTargetWithAction = function (
|
|
131
|
+
this: Envelope,
|
|
132
|
+
target: DigestProvider,
|
|
133
|
+
action: ObscureAction,
|
|
134
|
+
): Envelope {
|
|
135
|
+
return this.elideRemovingArrayWithAction([target], action);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/// Implementation of elideRemovingTarget
|
|
139
|
+
Envelope.prototype.elideRemovingTarget = function (
|
|
140
|
+
this: Envelope,
|
|
141
|
+
target: DigestProvider,
|
|
142
|
+
): Envelope {
|
|
143
|
+
return this.elideRemovingArray([target]);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/// Implementation of elideRevealingSetWithAction
|
|
147
|
+
Envelope.prototype.elideRevealingSetWithAction = function (
|
|
148
|
+
this: Envelope,
|
|
149
|
+
target: Set<Digest>,
|
|
150
|
+
action: ObscureAction,
|
|
151
|
+
): Envelope {
|
|
152
|
+
return elideSetWithAction(this, target, true, action);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/// Implementation of elideRevealingSet
|
|
156
|
+
Envelope.prototype.elideRevealingSet = function (this: Envelope, target: Set<Digest>): Envelope {
|
|
157
|
+
return elideSetWithAction(this, target, true, elideAction());
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/// Implementation of elideRevealingArrayWithAction
|
|
161
|
+
Envelope.prototype.elideRevealingArrayWithAction = function (
|
|
162
|
+
this: Envelope,
|
|
163
|
+
target: DigestProvider[],
|
|
164
|
+
action: ObscureAction,
|
|
165
|
+
): Envelope {
|
|
166
|
+
const targetSet = new Set(target.map((p) => p.digest()));
|
|
167
|
+
return elideSetWithAction(this, targetSet, true, action);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/// Implementation of elideRevealingArray
|
|
171
|
+
Envelope.prototype.elideRevealingArray = function (
|
|
172
|
+
this: Envelope,
|
|
173
|
+
target: DigestProvider[],
|
|
174
|
+
): Envelope {
|
|
175
|
+
const targetSet = new Set(target.map((p) => p.digest()));
|
|
176
|
+
return elideSetWithAction(this, targetSet, true, elideAction());
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/// Implementation of elideRevealingTargetWithAction
|
|
180
|
+
Envelope.prototype.elideRevealingTargetWithAction = function (
|
|
181
|
+
this: Envelope,
|
|
182
|
+
target: DigestProvider,
|
|
183
|
+
action: ObscureAction,
|
|
184
|
+
): Envelope {
|
|
185
|
+
return this.elideRevealingArrayWithAction([target], action);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/// Implementation of elideRevealingTarget
|
|
189
|
+
Envelope.prototype.elideRevealingTarget = function (
|
|
190
|
+
this: Envelope,
|
|
191
|
+
target: DigestProvider,
|
|
192
|
+
): Envelope {
|
|
193
|
+
return this.elideRevealingArray([target]);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/// Implementation of unelide
|
|
197
|
+
Envelope.prototype.unelide = function (this: Envelope, envelope: Envelope): Envelope {
|
|
198
|
+
if (this.digest().equals(envelope.digest())) {
|
|
199
|
+
return envelope;
|
|
200
|
+
}
|
|
201
|
+
throw EnvelopeError.invalidDigest();
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/// Implementation of nodesMatching
|
|
205
|
+
Envelope.prototype.nodesMatching = function (
|
|
206
|
+
this: Envelope,
|
|
207
|
+
targetDigests: Set<Digest> | undefined,
|
|
208
|
+
obscureTypes: ObscureType[],
|
|
209
|
+
): Set<Digest> {
|
|
210
|
+
const result = new Set<Digest>();
|
|
211
|
+
|
|
212
|
+
const visitor = (envelope: Envelope): void => {
|
|
213
|
+
// Check if this node matches the target digests
|
|
214
|
+
const digestMatches =
|
|
215
|
+
targetDigests === undefined ||
|
|
216
|
+
Array.from(targetDigests).some((d) => d.equals(envelope.digest()));
|
|
217
|
+
|
|
218
|
+
if (!digestMatches) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If no obscure types specified, include all nodes
|
|
223
|
+
if (obscureTypes.length === 0) {
|
|
224
|
+
result.add(envelope.digest());
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if this node matches any of the specified obscure types
|
|
229
|
+
const c = envelope.case();
|
|
230
|
+
const typeMatches = obscureTypes.some((obscureType) => {
|
|
231
|
+
if (obscureType === ObscureType.Elided && c.type === "elided") {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (obscureType === ObscureType.Encrypted && c.type === "encrypted") {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (obscureType === ObscureType.Compressed && c.type === "compressed") {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (typeMatches) {
|
|
244
|
+
result.add(envelope.digest());
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Walk the envelope tree
|
|
249
|
+
walkEnvelope(this, visitor);
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/// Helper to walk envelope tree
|
|
255
|
+
function walkEnvelope(envelope: Envelope, visitor: (e: Envelope) => void): void {
|
|
256
|
+
visitor(envelope);
|
|
257
|
+
|
|
258
|
+
const c = envelope.case();
|
|
259
|
+
if (c.type === "node") {
|
|
260
|
+
walkEnvelope(c.subject, visitor);
|
|
261
|
+
for (const assertion of c.assertions) {
|
|
262
|
+
walkEnvelope(assertion, visitor);
|
|
263
|
+
}
|
|
264
|
+
} else if (c.type === "assertion") {
|
|
265
|
+
walkEnvelope(c.assertion.predicate(), visitor);
|
|
266
|
+
walkEnvelope(c.assertion.object(), visitor);
|
|
267
|
+
} else if (c.type === "wrapped") {
|
|
268
|
+
walkEnvelope(c.envelope, visitor);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Implementation of walkUnelide
|
|
273
|
+
Envelope.prototype.walkUnelide = function (this: Envelope, envelopes: Envelope[]): Envelope {
|
|
274
|
+
// Build a lookup map of digest -> envelope
|
|
275
|
+
const envelopeMap = new Map<string, Envelope>();
|
|
276
|
+
for (const env of envelopes) {
|
|
277
|
+
envelopeMap.set(env.digest().hex(), env);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return walkUnelideWithMap(this, envelopeMap);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/// Helper for walkUnelide with map
|
|
284
|
+
function walkUnelideWithMap(envelope: Envelope, envelopeMap: Map<string, Envelope>): Envelope {
|
|
285
|
+
const c = envelope.case();
|
|
286
|
+
|
|
287
|
+
if (c.type === "elided") {
|
|
288
|
+
// Try to find a matching envelope to restore
|
|
289
|
+
const replacement = envelopeMap.get(envelope.digest().hex());
|
|
290
|
+
return replacement ?? envelope;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (c.type === "node") {
|
|
294
|
+
const newSubject = walkUnelideWithMap(c.subject, envelopeMap);
|
|
295
|
+
const newAssertions = c.assertions.map((a) => walkUnelideWithMap(a, envelopeMap));
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
newSubject.isIdenticalTo(c.subject) &&
|
|
299
|
+
newAssertions.every((a, i) => a.isIdenticalTo(c.assertions[i]))
|
|
300
|
+
) {
|
|
301
|
+
return envelope;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Envelope.newWithUncheckedAssertions(newSubject, newAssertions);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (c.type === "wrapped") {
|
|
308
|
+
const newEnvelope = walkUnelideWithMap(c.envelope, envelopeMap);
|
|
309
|
+
if (newEnvelope.isIdenticalTo(c.envelope)) {
|
|
310
|
+
return envelope;
|
|
311
|
+
}
|
|
312
|
+
return Envelope.newWrapped(newEnvelope);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (c.type === "assertion") {
|
|
316
|
+
const newPredicate = walkUnelideWithMap(c.assertion.predicate(), envelopeMap);
|
|
317
|
+
const newObject = walkUnelideWithMap(c.assertion.object(), envelopeMap);
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
newPredicate.isIdenticalTo(c.assertion.predicate()) &&
|
|
321
|
+
newObject.isIdenticalTo(c.assertion.object())
|
|
322
|
+
) {
|
|
323
|
+
return envelope;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return Envelope.newAssertion(newPredicate, newObject);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return envelope;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Implementation of walkReplace
|
|
333
|
+
Envelope.prototype.walkReplace = function (
|
|
334
|
+
this: Envelope,
|
|
335
|
+
target: Set<Digest>,
|
|
336
|
+
replacement: Envelope,
|
|
337
|
+
): Envelope {
|
|
338
|
+
// Check if this node matches the target
|
|
339
|
+
if (Array.from(target).some((d) => d.equals(this.digest()))) {
|
|
340
|
+
return replacement;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const c = this.case();
|
|
344
|
+
|
|
345
|
+
if (c.type === "node") {
|
|
346
|
+
const newSubject = c.subject.walkReplace(target, replacement);
|
|
347
|
+
const newAssertions = c.assertions.map((a) => a.walkReplace(target, replacement));
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
newSubject.isIdenticalTo(c.subject) &&
|
|
351
|
+
newAssertions.every((a, i) => a.isIdenticalTo(c.assertions[i]))
|
|
352
|
+
) {
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Validate that all assertions are either assertions or obscured
|
|
357
|
+
return Envelope.newWithAssertions(newSubject, newAssertions);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (c.type === "wrapped") {
|
|
361
|
+
const newEnvelope = c.envelope.walkReplace(target, replacement);
|
|
362
|
+
if (newEnvelope.isIdenticalTo(c.envelope)) {
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
return Envelope.newWrapped(newEnvelope);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (c.type === "assertion") {
|
|
369
|
+
const newPredicate = c.assertion.predicate().walkReplace(target, replacement);
|
|
370
|
+
const newObject = c.assertion.object().walkReplace(target, replacement);
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
newPredicate.isIdenticalTo(c.assertion.predicate()) &&
|
|
374
|
+
newObject.isIdenticalTo(c.assertion.object())
|
|
375
|
+
) {
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return Envelope.newAssertion(newPredicate, newObject);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return this;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/// Implementation of isIdenticalTo
|
|
386
|
+
Envelope.prototype.isIdenticalTo = function (this: Envelope, other: Envelope): boolean {
|
|
387
|
+
// Two envelopes are identical if they have the same digest
|
|
388
|
+
// and the same case type (to handle wrapped vs unwrapped with same content)
|
|
389
|
+
return this.digest().equals(other.digest()) && this.case().type === other.case().type;
|
|
390
|
+
};
|