@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,297 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { EnvelopeError } from "../base/error";
3
+ import { type Digest } from "../base/digest";
4
+ import { cborData, decodeCbor } from "@bcts/dcbor";
5
+ import {
6
+ aeadChaCha20Poly1305EncryptWithAad,
7
+ aeadChaCha20Poly1305DecryptWithAad,
8
+ SYMMETRIC_KEY_SIZE,
9
+ SYMMETRIC_NONCE_SIZE,
10
+ } from "@bcts/crypto";
11
+ import { SecureRandomNumberGenerator, rngRandomData, type RandomNumberGenerator } from "@bcts/rand";
12
+
13
+ /// Extension for encrypting and decrypting envelopes using symmetric encryption.
14
+ ///
15
+ /// This module extends Gordian Envelope with functions for symmetric encryption
16
+ /// and decryption using the IETF-ChaCha20-Poly1305 construct. It enables
17
+ /// privacy-enhancing operations by allowing envelope elements to be encrypted
18
+ /// without changing the envelope's digest, similar to elision.
19
+ ///
20
+ /// The encryption process preserves the envelope's digest tree structure, which
21
+ /// means signatures, proofs, and other cryptographic artifacts remain valid
22
+ /// even when parts of the envelope are encrypted.
23
+ ///
24
+ /// @example
25
+ /// ```typescript
26
+ /// // Create an envelope
27
+ /// const envelope = Envelope.new("Hello world");
28
+ ///
29
+ /// // Generate a symmetric key for encryption
30
+ /// const key = SymmetricKey.generate();
31
+ ///
32
+ /// // Encrypt the envelope's subject
33
+ /// const encrypted = envelope.encryptSubject(key);
34
+ ///
35
+ /// // The encrypted envelope has the same digest as the original
36
+ /// console.log(envelope.digest().equals(encrypted.digest())); // true
37
+ ///
38
+ /// // The subject is now encrypted
39
+ /// console.log(encrypted.subject().isEncrypted()); // true
40
+ ///
41
+ /// // Decrypt the envelope
42
+ /// const decrypted = encrypted.decryptSubject(key);
43
+ ///
44
+ /// // The decrypted envelope is equivalent to the original
45
+ /// console.log(envelope.digest().equals(decrypted.digest())); // true
46
+ /// ```
47
+
48
+ /// Helper function to create a secure RNG
49
+ function createSecureRng(): RandomNumberGenerator {
50
+ return new SecureRandomNumberGenerator();
51
+ }
52
+
53
+ /// Represents a symmetric encryption key (256-bit)
54
+ /// Matches bc-components-rust/src/symmetric/symmetric_key.rs
55
+ export class SymmetricKey {
56
+ readonly #key: Uint8Array;
57
+
58
+ constructor(key: Uint8Array) {
59
+ if (key.length !== SYMMETRIC_KEY_SIZE) {
60
+ throw new Error(`Symmetric key must be ${SYMMETRIC_KEY_SIZE} bytes`);
61
+ }
62
+ this.#key = key;
63
+ }
64
+
65
+ /// Generates a new random symmetric key
66
+ static generate(): SymmetricKey {
67
+ const rng = createSecureRng();
68
+ const key = rngRandomData(rng, SYMMETRIC_KEY_SIZE);
69
+ return new SymmetricKey(key);
70
+ }
71
+
72
+ /// Creates a symmetric key from existing bytes
73
+ static from(key: Uint8Array): SymmetricKey {
74
+ return new SymmetricKey(key);
75
+ }
76
+
77
+ /// Returns the raw key bytes
78
+ data(): Uint8Array {
79
+ return this.#key;
80
+ }
81
+
82
+ /// Encrypts data with associated digest (AAD)
83
+ /// Uses IETF ChaCha20-Poly1305 with 12-byte nonce
84
+ encrypt(plaintext: Uint8Array, digest: Digest): EncryptedMessage {
85
+ const rng = createSecureRng();
86
+
87
+ // Generate a random nonce (12 bytes for IETF ChaCha20-Poly1305)
88
+ const nonce = rngRandomData(rng, SYMMETRIC_NONCE_SIZE);
89
+
90
+ // Use digest as additional authenticated data (AAD)
91
+ const aad = digest.data();
92
+
93
+ // Encrypt using IETF ChaCha20-Poly1305
94
+ const [ciphertext, authTag] = aeadChaCha20Poly1305EncryptWithAad(
95
+ plaintext,
96
+ this.#key,
97
+ nonce,
98
+ aad,
99
+ );
100
+
101
+ return new EncryptedMessage(ciphertext, nonce, authTag, digest);
102
+ }
103
+
104
+ /// Decrypts an encrypted message
105
+ decrypt(message: EncryptedMessage): Uint8Array {
106
+ const digest = message.aadDigest();
107
+ if (digest === undefined) {
108
+ throw EnvelopeError.general("Missing digest in encrypted message");
109
+ }
110
+
111
+ const aad = digest.data();
112
+
113
+ try {
114
+ const plaintext = aeadChaCha20Poly1305DecryptWithAad(
115
+ message.ciphertext(),
116
+ this.#key,
117
+ message.nonce(),
118
+ aad,
119
+ message.authTag(),
120
+ );
121
+
122
+ return plaintext;
123
+ } catch (_error) {
124
+ throw EnvelopeError.general("Decryption failed: invalid key or corrupted data");
125
+ }
126
+ }
127
+ }
128
+
129
+ /// Represents an encrypted message with nonce, auth tag, and optional AAD digest
130
+ /// Matches bc-components-rust/src/symmetric/encrypted_message.rs
131
+ export class EncryptedMessage {
132
+ readonly #ciphertext: Uint8Array;
133
+ readonly #nonce: Uint8Array;
134
+ readonly #authTag: Uint8Array;
135
+ readonly #aadDigest?: Digest;
136
+
137
+ constructor(ciphertext: Uint8Array, nonce: Uint8Array, authTag: Uint8Array, aadDigest?: Digest) {
138
+ this.#ciphertext = ciphertext;
139
+ this.#nonce = nonce;
140
+ this.#authTag = authTag;
141
+ if (aadDigest !== undefined) {
142
+ this.#aadDigest = aadDigest;
143
+ }
144
+ }
145
+
146
+ /// Returns the ciphertext
147
+ ciphertext(): Uint8Array {
148
+ return this.#ciphertext;
149
+ }
150
+
151
+ /// Returns the nonce
152
+ nonce(): Uint8Array {
153
+ return this.#nonce;
154
+ }
155
+
156
+ /// Returns the authentication tag
157
+ authTag(): Uint8Array {
158
+ return this.#authTag;
159
+ }
160
+
161
+ /// Returns the optional AAD digest
162
+ aadDigest(): Digest | undefined {
163
+ return this.#aadDigest;
164
+ }
165
+
166
+ /// Returns the digest of this encrypted message (the AAD digest)
167
+ digest(): Digest {
168
+ if (this.#aadDigest === undefined) {
169
+ throw new Error("Encrypted message missing AAD digest");
170
+ }
171
+ return this.#aadDigest;
172
+ }
173
+ }
174
+
175
+ /// Register encryption extension methods on Envelope prototype
176
+ /// This function is exported and called during module initialization
177
+ /// to ensure Envelope is fully defined before attaching methods.
178
+ export function registerEncryptExtension(): void {
179
+ if (Envelope?.prototype === undefined) {
180
+ return;
181
+ }
182
+
183
+ // Skip if already registered
184
+ if (typeof Envelope.prototype.encryptSubject === "function") {
185
+ return;
186
+ }
187
+
188
+ Envelope.prototype.encryptSubject = function (this: Envelope, key: SymmetricKey): Envelope {
189
+ const c = this.case();
190
+
191
+ // Can't encrypt if already encrypted or elided
192
+ if (c.type === "encrypted") {
193
+ throw EnvelopeError.general("Envelope is already encrypted");
194
+ }
195
+ if (c.type === "elided") {
196
+ throw EnvelopeError.general("Cannot encrypt elided envelope");
197
+ }
198
+
199
+ // For node case, encrypt just the subject
200
+ if (c.type === "node") {
201
+ if (c.subject.isEncrypted()) {
202
+ throw EnvelopeError.general("Subject is already encrypted");
203
+ }
204
+
205
+ // Get the subject's CBOR data
206
+ const subjectCbor = c.subject.taggedCbor();
207
+ const encodedCbor = cborData(subjectCbor);
208
+ const subjectDigest = c.subject.digest();
209
+
210
+ // Encrypt the subject
211
+ const encryptedMessage = key.encrypt(encodedCbor, subjectDigest);
212
+
213
+ // Create encrypted envelope
214
+ const encryptedSubject = Envelope.fromCase({
215
+ type: "encrypted",
216
+ message: encryptedMessage,
217
+ });
218
+
219
+ // Rebuild the node with encrypted subject and same assertions
220
+ return Envelope.newWithAssertions(encryptedSubject, c.assertions);
221
+ }
222
+
223
+ // For other cases, encrypt the entire envelope
224
+ const cbor = this.taggedCbor();
225
+ const encodedCbor = cborData(cbor);
226
+ const digest = this.digest();
227
+
228
+ const encryptedMessage = key.encrypt(encodedCbor, digest);
229
+
230
+ return Envelope.fromCase({
231
+ type: "encrypted",
232
+ message: encryptedMessage,
233
+ });
234
+ };
235
+
236
+ /// Implementation of decryptSubject()
237
+ Envelope.prototype.decryptSubject = function (this: Envelope, key: SymmetricKey): Envelope {
238
+ const subjectCase = this.subject().case();
239
+
240
+ if (subjectCase.type !== "encrypted") {
241
+ throw EnvelopeError.general("Subject is not encrypted");
242
+ }
243
+
244
+ const message = subjectCase.message;
245
+ const subjectDigest = message.aadDigest();
246
+
247
+ if (subjectDigest === undefined) {
248
+ throw EnvelopeError.general("Missing digest in encrypted message");
249
+ }
250
+
251
+ // Decrypt the subject
252
+ const decryptedData = key.decrypt(message);
253
+
254
+ // Parse back to envelope
255
+ const cbor = decodeCbor(decryptedData);
256
+ const resultSubject = Envelope.fromTaggedCbor(cbor);
257
+
258
+ // Verify digest
259
+ if (!resultSubject.digest().equals(subjectDigest)) {
260
+ throw EnvelopeError.general("Invalid digest after decryption");
261
+ }
262
+
263
+ const c = this.case();
264
+
265
+ // If this is a node, rebuild with decrypted subject
266
+ if (c.type === "node") {
267
+ const result = Envelope.newWithAssertions(resultSubject, c.assertions);
268
+ if (!result.digest().equals(c.digest)) {
269
+ throw EnvelopeError.general("Invalid envelope digest after decryption");
270
+ }
271
+ return result;
272
+ }
273
+
274
+ // Otherwise just return the decrypted subject
275
+ return resultSubject;
276
+ };
277
+
278
+ /// Implementation of encrypt() - convenience method
279
+ Envelope.prototype.encrypt = function (this: Envelope, key: SymmetricKey): Envelope {
280
+ return this.wrap().encryptSubject(key);
281
+ };
282
+
283
+ /// Implementation of decrypt() - convenience method
284
+ Envelope.prototype.decrypt = function (this: Envelope, key: SymmetricKey): Envelope {
285
+ const decrypted = this.decryptSubject(key);
286
+ return decrypted.unwrap();
287
+ };
288
+
289
+ /// Implementation of isEncrypted()
290
+ Envelope.prototype.isEncrypted = function (this: Envelope): boolean {
291
+ return this.case().type === "encrypted";
292
+ };
293
+ }
294
+
295
+ // Auto-register on module load - will be called again from index.ts
296
+ // to ensure proper ordering after all modules are loaded
297
+ registerEncryptExtension();
@@ -0,0 +1,404 @@
1
+ import { Envelope } from "../base/envelope";
2
+ import { type EnvelopeEncodableValue } from "../base/envelope-encodable";
3
+ import { EnvelopeError } from "../base/error";
4
+
5
+ /// Extension for envelope expressions.
6
+ ///
7
+ /// This module implements the Gordian Envelope expression syntax as specified
8
+ /// in BCR-2023-012. Expressions enable encoding of machine-evaluatable
9
+ /// expressions using envelopes, providing a foundation for distributed
10
+ /// function calls and computation.
11
+ ///
12
+ /// ## Expression Structure
13
+ ///
14
+ /// An expression consists of:
15
+ /// - A function identifier (the subject)
16
+ /// - Zero or more parameters (as assertions)
17
+ /// - Optional metadata (non-parameter assertions)
18
+ ///
19
+ /// ## CBOR Tags
20
+ ///
21
+ /// - Function: #6.40006
22
+ /// - Parameter: #6.40007
23
+ /// - Placeholder: #6.40008
24
+ /// - Replacement: #6.40009
25
+ ///
26
+ /// @example
27
+ /// ```typescript
28
+ /// // Create a simple addition expression: add(lhs: 2, rhs: 3)
29
+ /// const expr = new Function('add')
30
+ /// .withParameter('lhs', 2)
31
+ /// .withParameter('rhs', 3);
32
+ ///
33
+ /// const envelope = expr.envelope();
34
+ /// ```
35
+
36
+ /// CBOR tag for function identifiers
37
+ export const CBOR_TAG_FUNCTION = 40006;
38
+
39
+ /// CBOR tag for parameter identifiers
40
+ export const CBOR_TAG_PARAMETER = 40007;
41
+
42
+ /// CBOR tag for placeholder identifiers
43
+ export const CBOR_TAG_PLACEHOLDER = 40008;
44
+
45
+ /// CBOR tag for replacement identifiers
46
+ export const CBOR_TAG_REPLACEMENT = 40009;
47
+
48
+ /// Well-known function identifiers (numeric)
49
+ export const FUNCTION_IDS = {
50
+ ADD: 1, // addition
51
+ SUB: 2, // subtraction
52
+ MUL: 3, // multiplication
53
+ DIV: 4, // division
54
+ NEG: 5, // unary negation
55
+ LT: 6, // less than
56
+ LE: 7, // less than or equal
57
+ GT: 8, // greater than
58
+ GE: 9, // greater than or equal
59
+ EQ: 10, // equal to
60
+ NE: 11, // not equal to
61
+ AND: 12, // logical and
62
+ OR: 13, // logical or
63
+ XOR: 14, // logical xor
64
+ NOT: 15, // logical not
65
+ } as const;
66
+
67
+ /// Well-known parameter identifiers (numeric)
68
+ export const PARAMETER_IDS = {
69
+ BLANK: 1, // blank/implicit parameter (_)
70
+ LHS: 2, // left-hand side
71
+ RHS: 3, // right-hand side
72
+ } as const;
73
+
74
+ /// Type for function identifier (number or string)
75
+ export type FunctionID = number | string;
76
+
77
+ /// Type for parameter identifier (number or string)
78
+ export type ParameterID = number | string;
79
+
80
+ /// Represents a function identifier in an expression
81
+ export class Function {
82
+ readonly #id: FunctionID;
83
+
84
+ constructor(id: FunctionID) {
85
+ this.#id = id;
86
+ }
87
+
88
+ /// Returns the function identifier
89
+ id(): FunctionID {
90
+ return this.#id;
91
+ }
92
+
93
+ /// Returns true if this is a numeric function ID
94
+ isNumeric(): boolean {
95
+ return typeof this.#id === "number";
96
+ }
97
+
98
+ /// Returns true if this is a string function ID
99
+ isString(): boolean {
100
+ return typeof this.#id === "string";
101
+ }
102
+
103
+ /// Creates an expression envelope with this function as the subject
104
+ envelope(): Envelope {
105
+ // For now, create a simple envelope with the function ID
106
+ // In a full implementation, this would use CBOR tag 40006
107
+ const functionStr = typeof this.#id === "number" ? `«${this.#id}»` : `«"${this.#id}"»`;
108
+ return Envelope.new(functionStr);
109
+ }
110
+
111
+ /// Creates an expression with a parameter
112
+ withParameter(param: ParameterID, value: EnvelopeEncodableValue): Expression {
113
+ const expr = new Expression(this);
114
+ return expr.withParameter(param, value);
115
+ }
116
+
117
+ /// Creates a function from a known numeric ID
118
+ static fromNumeric(id: number): Function {
119
+ return new Function(id);
120
+ }
121
+
122
+ /// Creates a function from a string name
123
+ static fromString(name: string): Function {
124
+ return new Function(name);
125
+ }
126
+
127
+ /// Returns a string representation for display
128
+ toString(): string {
129
+ return typeof this.#id === "number" ? `«${this.#id}»` : `«"${this.#id}"»`;
130
+ }
131
+ }
132
+
133
+ /// Represents a parameter in an expression
134
+ export class Parameter {
135
+ readonly #id: ParameterID;
136
+ readonly #value: Envelope;
137
+
138
+ constructor(id: ParameterID, value: Envelope) {
139
+ this.#id = id;
140
+ this.#value = value;
141
+ }
142
+
143
+ /// Returns the parameter identifier
144
+ id(): ParameterID {
145
+ return this.#id;
146
+ }
147
+
148
+ /// Returns the parameter value as an envelope
149
+ value(): Envelope {
150
+ return this.#value;
151
+ }
152
+
153
+ /// Returns true if this is a numeric parameter ID
154
+ isNumeric(): boolean {
155
+ return typeof this.#id === "number";
156
+ }
157
+
158
+ /// Returns true if this is a string parameter ID
159
+ isString(): boolean {
160
+ return typeof this.#id === "string";
161
+ }
162
+
163
+ /// Creates a parameter envelope
164
+ /// In a full implementation, this would use CBOR tag 40007
165
+ envelope(): Envelope {
166
+ const paramStr = typeof this.#id === "number" ? `❰${this.#id}❱` : `❰"${this.#id}"❱`;
167
+ return Envelope.newAssertion(paramStr, this.#value);
168
+ }
169
+
170
+ /// Creates a parameter from known IDs
171
+ static blank(value: EnvelopeEncodableValue): Parameter {
172
+ return new Parameter(PARAMETER_IDS.BLANK, Envelope.new(value));
173
+ }
174
+
175
+ static lhs(value: EnvelopeEncodableValue): Parameter {
176
+ return new Parameter(PARAMETER_IDS.LHS, Envelope.new(value));
177
+ }
178
+
179
+ static rhs(value: EnvelopeEncodableValue): Parameter {
180
+ return new Parameter(PARAMETER_IDS.RHS, Envelope.new(value));
181
+ }
182
+
183
+ /// Returns a string representation for display
184
+ toString(): string {
185
+ const idStr = typeof this.#id === "number" ? `❰${this.#id}❱` : `❰"${this.#id}"❱`;
186
+ return `${idStr}: ${this.#value.asText()}`;
187
+ }
188
+ }
189
+
190
+ /// Represents a complete expression with function and parameters
191
+ export class Expression {
192
+ readonly #function: Function;
193
+ readonly #parameters = new Map<string, Parameter>();
194
+ #envelope: Envelope | null = null;
195
+
196
+ constructor(func: Function) {
197
+ this.#function = func;
198
+ }
199
+
200
+ /// Returns the function
201
+ function(): Function {
202
+ return this.#function;
203
+ }
204
+
205
+ /// Returns all parameters
206
+ parameters(): Parameter[] {
207
+ return Array.from(this.#parameters.values());
208
+ }
209
+
210
+ /// Adds a parameter to the expression
211
+ withParameter(param: ParameterID, value: EnvelopeEncodableValue): Expression {
212
+ const key = typeof param === "number" ? param.toString() : param;
213
+ this.#parameters.set(key, new Parameter(param, Envelope.new(value)));
214
+ this.#envelope = null; // Invalidate cached envelope
215
+ return this;
216
+ }
217
+
218
+ /// Adds multiple parameters at once
219
+ withParameters(params: Record<string, EnvelopeEncodableValue>): Expression {
220
+ for (const [key, value] of Object.entries(params)) {
221
+ this.withParameter(key, value);
222
+ }
223
+ return this;
224
+ }
225
+
226
+ /// Gets a parameter value by ID
227
+ getParameter(param: ParameterID): Envelope | undefined {
228
+ const key = typeof param === "number" ? param.toString() : param;
229
+ return this.#parameters.get(key)?.value();
230
+ }
231
+
232
+ /// Checks if a parameter exists
233
+ hasParameter(param: ParameterID): boolean {
234
+ const key = typeof param === "number" ? param.toString() : param;
235
+ return this.#parameters.has(key);
236
+ }
237
+
238
+ /// Converts the expression to an envelope
239
+ envelope(): Envelope {
240
+ if (this.#envelope !== null) {
241
+ return this.#envelope;
242
+ }
243
+
244
+ // Start with function envelope
245
+ let env = this.#function.envelope();
246
+
247
+ // Add all parameters as assertions
248
+ for (const param of this.#parameters.values()) {
249
+ const paramEnv = param.envelope();
250
+ // Extract the assertion from the parameter envelope
251
+ const assertion = paramEnv.assertions()[0];
252
+ if (assertion !== undefined) {
253
+ const predicate = assertion.subject().asPredicate();
254
+ const object = assertion.subject().asObject();
255
+ if (predicate !== undefined && object !== undefined) {
256
+ env = env.addAssertion(predicate.asText(), object);
257
+ }
258
+ }
259
+ }
260
+
261
+ this.#envelope = env;
262
+ return env;
263
+ }
264
+
265
+ /// Creates an expression from an envelope
266
+ /// Note: This is a simplified implementation
267
+ static fromEnvelope(envelope: Envelope): Expression {
268
+ // Extract function from subject
269
+ const subject = envelope.subject();
270
+ const subjectText = subject.asText();
271
+ if (subjectText === undefined) {
272
+ throw EnvelopeError.general("Not a valid function envelope");
273
+ }
274
+
275
+ // Parse function identifier
276
+ let funcId: FunctionID;
277
+ if (subjectText.startsWith("«") && subjectText.endsWith("»")) {
278
+ const inner = subjectText.slice(1, -1);
279
+ if (inner.startsWith('"') && inner.endsWith('"')) {
280
+ funcId = inner.slice(1, -1); // String function
281
+ } else {
282
+ funcId = parseInt(inner, 10); // Numeric function
283
+ }
284
+ } else {
285
+ throw EnvelopeError.general("Not a valid function envelope");
286
+ }
287
+
288
+ const func = new Function(funcId);
289
+ const expr = new Expression(func);
290
+
291
+ // Extract parameters from assertions
292
+ for (const assertion of envelope.assertions()) {
293
+ try {
294
+ const pred = assertion.subject().asPredicate();
295
+ const obj = assertion.subject().asObject();
296
+
297
+ if (pred !== undefined && obj !== undefined) {
298
+ const predText = pred.asText();
299
+ if (predText !== undefined && predText.startsWith("❰") && predText.endsWith("❱")) {
300
+ const inner = predText.slice(1, -1);
301
+ let paramId: ParameterID;
302
+ if (inner.startsWith('"') && inner.endsWith('"')) {
303
+ paramId = inner.slice(1, -1);
304
+ } else {
305
+ paramId = parseInt(inner, 10);
306
+ }
307
+ expr.withParameter(paramId, obj);
308
+ }
309
+ }
310
+ } catch {
311
+ // Skip non-parameter assertions
312
+ continue;
313
+ }
314
+ }
315
+
316
+ return expr;
317
+ }
318
+
319
+ /// Returns a string representation for display
320
+ toString(): string {
321
+ const params = Array.from(this.#parameters.values())
322
+ .map((p) => p.toString())
323
+ .join(", ");
324
+ return `${this.#function.toString()} [${params}]`;
325
+ }
326
+ }
327
+
328
+ /// Helper functions for creating common expressions
329
+
330
+ /// Creates an addition expression: lhs + rhs
331
+ export function add(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
332
+ return Function.fromNumeric(FUNCTION_IDS.ADD)
333
+ .withParameter(PARAMETER_IDS.LHS, lhs)
334
+ .withParameter(PARAMETER_IDS.RHS, rhs);
335
+ }
336
+
337
+ /// Creates a subtraction expression: lhs - rhs
338
+ export function sub(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
339
+ return Function.fromNumeric(FUNCTION_IDS.SUB)
340
+ .withParameter(PARAMETER_IDS.LHS, lhs)
341
+ .withParameter(PARAMETER_IDS.RHS, rhs);
342
+ }
343
+
344
+ /// Creates a multiplication expression: lhs * rhs
345
+ export function mul(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
346
+ return Function.fromNumeric(FUNCTION_IDS.MUL)
347
+ .withParameter(PARAMETER_IDS.LHS, lhs)
348
+ .withParameter(PARAMETER_IDS.RHS, rhs);
349
+ }
350
+
351
+ /// Creates a division expression: lhs / rhs
352
+ export function div(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
353
+ return Function.fromNumeric(FUNCTION_IDS.DIV)
354
+ .withParameter(PARAMETER_IDS.LHS, lhs)
355
+ .withParameter(PARAMETER_IDS.RHS, rhs);
356
+ }
357
+
358
+ /// Creates a negation expression: -value
359
+ export function neg(value: EnvelopeEncodableValue): Expression {
360
+ return Function.fromNumeric(FUNCTION_IDS.NEG).withParameter(PARAMETER_IDS.BLANK, value);
361
+ }
362
+
363
+ /// Creates a less-than expression: lhs < rhs
364
+ export function lt(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
365
+ return Function.fromNumeric(FUNCTION_IDS.LT)
366
+ .withParameter(PARAMETER_IDS.LHS, lhs)
367
+ .withParameter(PARAMETER_IDS.RHS, rhs);
368
+ }
369
+
370
+ /// Creates a greater-than expression: lhs > rhs
371
+ export function gt(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
372
+ return Function.fromNumeric(FUNCTION_IDS.GT)
373
+ .withParameter(PARAMETER_IDS.LHS, lhs)
374
+ .withParameter(PARAMETER_IDS.RHS, rhs);
375
+ }
376
+
377
+ /// Creates an equality expression: lhs == rhs
378
+ export function eq(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
379
+ return Function.fromNumeric(FUNCTION_IDS.EQ)
380
+ .withParameter(PARAMETER_IDS.LHS, lhs)
381
+ .withParameter(PARAMETER_IDS.RHS, rhs);
382
+ }
383
+
384
+ /// Creates a logical AND expression: lhs && rhs
385
+ export function and(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
386
+ return Function.fromNumeric(FUNCTION_IDS.AND)
387
+ .withParameter(PARAMETER_IDS.LHS, lhs)
388
+ .withParameter(PARAMETER_IDS.RHS, rhs);
389
+ }
390
+
391
+ /// Creates a logical OR expression: lhs || rhs
392
+ export function or(lhs: EnvelopeEncodableValue, rhs: EnvelopeEncodableValue): Expression {
393
+ return Function.fromNumeric(FUNCTION_IDS.OR)
394
+ .withParameter(PARAMETER_IDS.LHS, lhs)
395
+ .withParameter(PARAMETER_IDS.RHS, rhs);
396
+ }
397
+
398
+ /// Creates a logical NOT expression: !value
399
+ export function not(value: EnvelopeEncodableValue): Expression {
400
+ return Function.fromNumeric(FUNCTION_IDS.NOT).withParameter(PARAMETER_IDS.BLANK, value);
401
+ }
402
+
403
+ // Export types and classes
404
+ export {};