@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,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 {};
|