@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,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature Extension for Gordian Envelope
|
|
3
|
+
*
|
|
4
|
+
* Provides functionality for digitally signing Envelopes and verifying signatures,
|
|
5
|
+
* with optional metadata support.
|
|
6
|
+
*
|
|
7
|
+
* The signature extension allows:
|
|
8
|
+
* - Signing envelope subjects to validate their authenticity
|
|
9
|
+
* - Adding metadata to signatures (e.g., signer identity, date, purpose)
|
|
10
|
+
* - Verification of signatures, both with and without metadata
|
|
11
|
+
* - Support for multiple signatures on a single envelope
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Envelope } from "../base/envelope";
|
|
15
|
+
import { EnvelopeError } from "../base/error";
|
|
16
|
+
import {
|
|
17
|
+
ecdsaSign,
|
|
18
|
+
ecdsaVerify,
|
|
19
|
+
ecdsaPublicKeyFromPrivateKey,
|
|
20
|
+
ECDSA_PRIVATE_KEY_SIZE,
|
|
21
|
+
ECDSA_PUBLIC_KEY_SIZE,
|
|
22
|
+
ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE,
|
|
23
|
+
} from "@bcts/crypto";
|
|
24
|
+
import { SecureRandomNumberGenerator, rngRandomData } from "@bcts/rand";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Known value for the 'signed' predicate.
|
|
28
|
+
* This is the standard predicate used for signature assertions.
|
|
29
|
+
*/
|
|
30
|
+
export const SIGNED = "signed";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Known value for the 'verifiedBy' predicate.
|
|
34
|
+
* Used to indicate verification status.
|
|
35
|
+
*/
|
|
36
|
+
export const VERIFIED_BY = "verifiedBy";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Known value for the 'note' predicate.
|
|
40
|
+
* Used for adding notes/comments to signatures.
|
|
41
|
+
*/
|
|
42
|
+
export const NOTE = "note";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents a cryptographic signature.
|
|
46
|
+
*/
|
|
47
|
+
export class Signature {
|
|
48
|
+
readonly #data: Uint8Array;
|
|
49
|
+
|
|
50
|
+
constructor(data: Uint8Array) {
|
|
51
|
+
this.#data = data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns the raw signature bytes.
|
|
56
|
+
*/
|
|
57
|
+
data(): Uint8Array {
|
|
58
|
+
return this.#data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the hex-encoded signature.
|
|
63
|
+
*/
|
|
64
|
+
hex(): string {
|
|
65
|
+
return Array.from(this.#data)
|
|
66
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
67
|
+
.join("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a Signature from hex string.
|
|
72
|
+
*/
|
|
73
|
+
static fromHex(hex: string): Signature {
|
|
74
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
75
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
76
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
77
|
+
}
|
|
78
|
+
return new Signature(bytes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Interface for types that can sign data.
|
|
84
|
+
*/
|
|
85
|
+
export interface Signer {
|
|
86
|
+
/**
|
|
87
|
+
* Signs the provided data and returns a Signature.
|
|
88
|
+
*/
|
|
89
|
+
sign(data: Uint8Array): Signature;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Interface for types that can verify signatures.
|
|
94
|
+
*/
|
|
95
|
+
export interface Verifier {
|
|
96
|
+
/**
|
|
97
|
+
* Verifies a signature against the provided data.
|
|
98
|
+
* Returns true if the signature is valid.
|
|
99
|
+
*/
|
|
100
|
+
verify(data: Uint8Array, signature: Signature): boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ECDSA signing key using secp256k1 curve.
|
|
105
|
+
* Uses @bcts/crypto functions.
|
|
106
|
+
*/
|
|
107
|
+
export class SigningPrivateKey implements Signer {
|
|
108
|
+
readonly #privateKey: Uint8Array;
|
|
109
|
+
|
|
110
|
+
constructor(privateKey: Uint8Array) {
|
|
111
|
+
if (privateKey.length !== ECDSA_PRIVATE_KEY_SIZE) {
|
|
112
|
+
throw new Error(`Private key must be ${ECDSA_PRIVATE_KEY_SIZE} bytes`);
|
|
113
|
+
}
|
|
114
|
+
this.#privateKey = privateKey;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generates a new random private key.
|
|
119
|
+
*/
|
|
120
|
+
static generate(): SigningPrivateKey {
|
|
121
|
+
const rng = new SecureRandomNumberGenerator();
|
|
122
|
+
const privateKey = rngRandomData(rng, ECDSA_PRIVATE_KEY_SIZE);
|
|
123
|
+
return new SigningPrivateKey(privateKey);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a private key from hex string.
|
|
128
|
+
*/
|
|
129
|
+
static fromHex(hex: string): SigningPrivateKey {
|
|
130
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
131
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
132
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
133
|
+
}
|
|
134
|
+
return new SigningPrivateKey(bytes);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns the corresponding public key.
|
|
139
|
+
*/
|
|
140
|
+
publicKey(): SigningPublicKey {
|
|
141
|
+
const publicKey = ecdsaPublicKeyFromPrivateKey(this.#privateKey);
|
|
142
|
+
return new SigningPublicKey(publicKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Signs data and returns a Signature.
|
|
147
|
+
*/
|
|
148
|
+
sign(data: Uint8Array): Signature {
|
|
149
|
+
const signatureBytes = ecdsaSign(this.#privateKey, data);
|
|
150
|
+
return new Signature(signatureBytes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns the raw private key bytes.
|
|
155
|
+
*/
|
|
156
|
+
data(): Uint8Array {
|
|
157
|
+
return this.#privateKey;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ECDSA public key for signature verification using secp256k1 curve.
|
|
163
|
+
* Uses @bcts/crypto functions.
|
|
164
|
+
*/
|
|
165
|
+
export class SigningPublicKey implements Verifier {
|
|
166
|
+
readonly #publicKey: Uint8Array;
|
|
167
|
+
|
|
168
|
+
constructor(publicKey: Uint8Array) {
|
|
169
|
+
if (
|
|
170
|
+
publicKey.length !== ECDSA_PUBLIC_KEY_SIZE &&
|
|
171
|
+
publicKey.length !== ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Public key must be ${ECDSA_PUBLIC_KEY_SIZE} bytes (compressed) or ${ECDSA_UNCOMPRESSED_PUBLIC_KEY_SIZE} bytes (uncompressed)`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
this.#publicKey = publicKey;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates a public key from hex string.
|
|
182
|
+
*/
|
|
183
|
+
static fromHex(hex: string): SigningPublicKey {
|
|
184
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
185
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
186
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
187
|
+
}
|
|
188
|
+
return new SigningPublicKey(bytes);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Verifies a signature against the provided data.
|
|
193
|
+
*/
|
|
194
|
+
verify(data: Uint8Array, signature: Signature): boolean {
|
|
195
|
+
try {
|
|
196
|
+
return ecdsaVerify(this.#publicKey, signature.data(), data);
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the raw public key bytes.
|
|
204
|
+
*/
|
|
205
|
+
data(): Uint8Array {
|
|
206
|
+
return this.#publicKey;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Returns the hex-encoded public key.
|
|
211
|
+
*/
|
|
212
|
+
hex(): string {
|
|
213
|
+
return Array.from(this.#publicKey)
|
|
214
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
215
|
+
.join("");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Metadata that can be attached to a signature.
|
|
221
|
+
*/
|
|
222
|
+
export class SignatureMetadata {
|
|
223
|
+
readonly #assertions: [string, unknown][] = [];
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Adds an assertion to the metadata.
|
|
227
|
+
*/
|
|
228
|
+
withAssertion(predicate: string, object: unknown): SignatureMetadata {
|
|
229
|
+
const metadata = new SignatureMetadata();
|
|
230
|
+
metadata.#assertions.push(...this.#assertions);
|
|
231
|
+
metadata.#assertions.push([predicate, object]);
|
|
232
|
+
return metadata;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns all assertions in the metadata.
|
|
237
|
+
*/
|
|
238
|
+
assertions(): [string, unknown][] {
|
|
239
|
+
return this.#assertions;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns true if this metadata has any assertions.
|
|
244
|
+
*/
|
|
245
|
+
hasAssertions(): boolean {
|
|
246
|
+
return this.#assertions.length > 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Implementation
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
252
|
+
if (Envelope?.prototype) {
|
|
253
|
+
Envelope.prototype.addSignature = function (this: Envelope, signer: Signer): Envelope {
|
|
254
|
+
return this.addSignatureWithMetadata(signer, undefined);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
Envelope.prototype.addSignatureWithMetadata = function (
|
|
258
|
+
this: Envelope,
|
|
259
|
+
signer: Signer,
|
|
260
|
+
metadata?: SignatureMetadata,
|
|
261
|
+
): Envelope {
|
|
262
|
+
const digest = this.subject().digest();
|
|
263
|
+
const signature = signer.sign(digest.data());
|
|
264
|
+
let signatureEnvelope = Envelope.new(signature.data());
|
|
265
|
+
|
|
266
|
+
if (metadata?.hasAssertions() === true) {
|
|
267
|
+
// Add metadata assertions to the signature
|
|
268
|
+
for (const [predicate, object] of metadata.assertions()) {
|
|
269
|
+
signatureEnvelope = signatureEnvelope.addAssertion(
|
|
270
|
+
predicate,
|
|
271
|
+
object as string | number | boolean,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Wrap the signature with metadata
|
|
276
|
+
signatureEnvelope = signatureEnvelope.wrap();
|
|
277
|
+
|
|
278
|
+
// Sign the wrapped envelope
|
|
279
|
+
const outerSignature = signer.sign(signatureEnvelope.digest().data());
|
|
280
|
+
signatureEnvelope = signatureEnvelope.addAssertion(SIGNED, outerSignature.data());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.addAssertion(SIGNED, signatureEnvelope);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
Envelope.prototype.addSignatures = function (this: Envelope, signers: Signer[]): Envelope {
|
|
287
|
+
return signers.reduce((envelope, signer) => envelope.addSignature(signer), this);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
Envelope.prototype.hasSignatureFrom = function (this: Envelope, verifier: Verifier): boolean {
|
|
291
|
+
const subjectDigest = this.subject().digest();
|
|
292
|
+
const signatures = this.signatures();
|
|
293
|
+
|
|
294
|
+
for (const sigEnvelope of signatures) {
|
|
295
|
+
const c = sigEnvelope.case();
|
|
296
|
+
|
|
297
|
+
if (c.type === "leaf") {
|
|
298
|
+
// Simple signature - verify directly
|
|
299
|
+
try {
|
|
300
|
+
const sigData = sigEnvelope.asByteString();
|
|
301
|
+
if (sigData !== undefined) {
|
|
302
|
+
const signature = new Signature(sigData);
|
|
303
|
+
if (verifier.verify(subjectDigest.data(), signature)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
} else if (c.type === "node") {
|
|
311
|
+
// Signature with metadata - it's a node with 'signed' assertion
|
|
312
|
+
// The structure is: { wrapped_signature [signed: outer_signature] }
|
|
313
|
+
// Check if this node has a 'signed' assertion
|
|
314
|
+
const outerSigs = sigEnvelope.assertions().filter((a) => {
|
|
315
|
+
const aC = a.case();
|
|
316
|
+
if (aC.type === "assertion") {
|
|
317
|
+
const pred = aC.assertion.predicate();
|
|
318
|
+
try {
|
|
319
|
+
return pred.asText() === SIGNED;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
for (const outerSig of outerSigs) {
|
|
328
|
+
const outerSigCase = outerSig.case();
|
|
329
|
+
if (outerSigCase.type === "assertion") {
|
|
330
|
+
const outerSigObj = outerSigCase.assertion.object();
|
|
331
|
+
try {
|
|
332
|
+
const outerSigData = outerSigObj.asByteString();
|
|
333
|
+
if (outerSigData !== undefined) {
|
|
334
|
+
const outerSignature = new Signature(outerSigData);
|
|
335
|
+
|
|
336
|
+
// The subject of this node should be a wrapped envelope
|
|
337
|
+
const nodeSubject = c.subject;
|
|
338
|
+
const nodeSubjectCase = nodeSubject.case();
|
|
339
|
+
|
|
340
|
+
// Verify outer signature against the wrapped envelope
|
|
341
|
+
if (
|
|
342
|
+
nodeSubjectCase.type === "wrapped" &&
|
|
343
|
+
verifier.verify(nodeSubject.digest().data(), outerSignature)
|
|
344
|
+
) {
|
|
345
|
+
// Now verify inner signature
|
|
346
|
+
const wrapped = nodeSubjectCase.envelope;
|
|
347
|
+
const innerSig = wrapped.subject();
|
|
348
|
+
const innerSigData = innerSig.asByteString();
|
|
349
|
+
if (innerSigData !== undefined) {
|
|
350
|
+
const innerSignature = new Signature(innerSigData);
|
|
351
|
+
if (verifier.verify(subjectDigest.data(), innerSignature)) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return false;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
Envelope.prototype.verifySignatureFrom = function (this: Envelope, verifier: Verifier): Envelope {
|
|
369
|
+
if (this.hasSignatureFrom(verifier)) {
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
throw EnvelopeError.general("No valid signature found from the given verifier");
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
Envelope.prototype.signatures = function (this: Envelope): Envelope[] {
|
|
376
|
+
const assertions = this.assertions();
|
|
377
|
+
return assertions
|
|
378
|
+
.filter((a) => {
|
|
379
|
+
const c = a.case();
|
|
380
|
+
if (c.type === "assertion") {
|
|
381
|
+
const pred = c.assertion.predicate();
|
|
382
|
+
try {
|
|
383
|
+
return pred.asText() === SIGNED;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
})
|
|
390
|
+
.map((a) => {
|
|
391
|
+
const c = a.case();
|
|
392
|
+
if (c.type === "assertion") {
|
|
393
|
+
return c.assertion.object();
|
|
394
|
+
}
|
|
395
|
+
throw EnvelopeError.general("Invalid signature assertion");
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Envelope } from "../base/envelope";
|
|
2
|
+
import { type EnvelopeEncodableValue } from "../base/envelope-encodable";
|
|
3
|
+
import { EnvelopeError } from "../base/error";
|
|
4
|
+
|
|
5
|
+
/// Type system for Gordian Envelopes.
|
|
6
|
+
///
|
|
7
|
+
/// This module provides functionality for adding, querying, and verifying types
|
|
8
|
+
/// within envelopes. In Gordian Envelope, types are implemented using the
|
|
9
|
+
/// special `'isA'` predicate (the string "isA"), which is semantically
|
|
10
|
+
/// equivalent to the RDF `rdf:type` concept.
|
|
11
|
+
///
|
|
12
|
+
/// Type information enables:
|
|
13
|
+
/// - Semantic classification of envelopes
|
|
14
|
+
/// - Type verification before processing content
|
|
15
|
+
/// - Conversion between domain objects and envelopes
|
|
16
|
+
/// - Schema validation
|
|
17
|
+
///
|
|
18
|
+
/// ## Type Representation
|
|
19
|
+
///
|
|
20
|
+
/// Types are represented as assertions with the `'isA'` predicate and an object
|
|
21
|
+
/// that specifies the type. The type object is typically a string or an envelope.
|
|
22
|
+
///
|
|
23
|
+
/// ## Usage Patterns
|
|
24
|
+
///
|
|
25
|
+
/// The type system is commonly used in two ways:
|
|
26
|
+
///
|
|
27
|
+
/// 1. **Type Tagging**: Adding type information to envelopes to indicate their
|
|
28
|
+
/// semantic meaning
|
|
29
|
+
///
|
|
30
|
+
/// ```typescript
|
|
31
|
+
/// // Create an envelope representing a person
|
|
32
|
+
/// const person = Envelope.new("Alice")
|
|
33
|
+
/// .addType("Person")
|
|
34
|
+
/// .addAssertion("age", 30);
|
|
35
|
+
/// ```
|
|
36
|
+
///
|
|
37
|
+
/// 2. **Type Checking**: Verifying that an envelope has the expected type
|
|
38
|
+
/// before processing
|
|
39
|
+
///
|
|
40
|
+
/// ```typescript
|
|
41
|
+
/// function processPerson(envelope: Envelope): void {
|
|
42
|
+
/// // Verify this is a person before processing
|
|
43
|
+
/// envelope.checkType("Person");
|
|
44
|
+
///
|
|
45
|
+
/// // Now we can safely extract person-specific information
|
|
46
|
+
/// const name = envelope.subject().extractString();
|
|
47
|
+
/// const age = envelope.objectForPredicate("age").extractNumber();
|
|
48
|
+
///
|
|
49
|
+
/// console.log(`${name} is ${age} years old`);
|
|
50
|
+
/// }
|
|
51
|
+
/// ```
|
|
52
|
+
|
|
53
|
+
/// The standard predicate for type assertions
|
|
54
|
+
export const IS_A = "isA";
|
|
55
|
+
|
|
56
|
+
/// Implementation of addType()
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
58
|
+
if (Envelope?.prototype) {
|
|
59
|
+
Envelope.prototype.addType = function (this: Envelope, object: EnvelopeEncodableValue): Envelope {
|
|
60
|
+
return this.addAssertion(IS_A, object);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/// Implementation of types()
|
|
64
|
+
Envelope.prototype.types = function (this: Envelope): Envelope[] {
|
|
65
|
+
return this.objectsForPredicate(IS_A);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/// Implementation of getType()
|
|
69
|
+
Envelope.prototype.getType = function (this: Envelope): Envelope {
|
|
70
|
+
const t = this.types();
|
|
71
|
+
if (t.length === 0) {
|
|
72
|
+
throw EnvelopeError.invalidType();
|
|
73
|
+
}
|
|
74
|
+
if (t.length === 1) {
|
|
75
|
+
return t[0];
|
|
76
|
+
}
|
|
77
|
+
throw EnvelopeError.ambiguousType();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/// Implementation of hasType()
|
|
81
|
+
Envelope.prototype.hasType = function (this: Envelope, t: EnvelopeEncodableValue): boolean {
|
|
82
|
+
const e = Envelope.new(t);
|
|
83
|
+
return this.types().some((x) => x.digest().equals(e.digest()));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/// Implementation of checkType()
|
|
87
|
+
Envelope.prototype.checkType = function (this: Envelope, t: EnvelopeEncodableValue): void {
|
|
88
|
+
if (!this.hasType(t)) {
|
|
89
|
+
throw EnvelopeError.invalidType();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Envelope } from "../base/envelope";
|
|
2
|
+
|
|
3
|
+
// Type for CBOR values that can appear in diagnostic notation
|
|
4
|
+
type CborValue =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| null
|
|
9
|
+
| Uint8Array
|
|
10
|
+
| CborValue[]
|
|
11
|
+
| Map<CborValue, CborValue>
|
|
12
|
+
| { tag: number; value: CborValue }
|
|
13
|
+
| { type: number; value: unknown };
|
|
14
|
+
|
|
15
|
+
/// Diagnostic notation formatting for Gordian Envelopes.
|
|
16
|
+
///
|
|
17
|
+
/// This module provides methods for converting envelopes to CBOR diagnostic
|
|
18
|
+
/// notation, a human-readable text format defined in RFC 8949 §8.
|
|
19
|
+
///
|
|
20
|
+
/// See [RFC-8949 §8](https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation)
|
|
21
|
+
/// for information on CBOR diagnostic notation.
|
|
22
|
+
|
|
23
|
+
// Note: Method declarations are in the base Envelope class.
|
|
24
|
+
// This module provides the prototype implementations.
|
|
25
|
+
|
|
26
|
+
/// Converts a CBOR value to diagnostic notation
|
|
27
|
+
function cborToDiagnostic(cbor: CborValue, indent = 0): string {
|
|
28
|
+
// Handle tagged values (CBOR tags)
|
|
29
|
+
if (typeof cbor === "object" && cbor !== null && "tag" in cbor && "value" in cbor) {
|
|
30
|
+
const tagged = cbor as { tag: number; value: CborValue };
|
|
31
|
+
return `${tagged.tag}(${cborToDiagnostic(tagged.value, indent)})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle arrays
|
|
35
|
+
if (Array.isArray(cbor)) {
|
|
36
|
+
if (cbor.length === 0) {
|
|
37
|
+
return "[]";
|
|
38
|
+
}
|
|
39
|
+
const items = cbor.map((item) => cborToDiagnostic(item, indent + 2));
|
|
40
|
+
return `[${items.join(", ")}]`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle Maps
|
|
44
|
+
if (cbor instanceof Map) {
|
|
45
|
+
if (cbor.size === 0) {
|
|
46
|
+
return "{}";
|
|
47
|
+
}
|
|
48
|
+
const entries: string[] = [];
|
|
49
|
+
for (const [key, value] of cbor) {
|
|
50
|
+
const keyStr = cborToDiagnostic(key, indent + 2);
|
|
51
|
+
const valueStr = cborToDiagnostic(value, indent + 2);
|
|
52
|
+
entries.push(`${keyStr}: ${valueStr}`);
|
|
53
|
+
}
|
|
54
|
+
return `{${entries.join(", ")}}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle Uint8Array (byte strings)
|
|
58
|
+
if (cbor instanceof Uint8Array) {
|
|
59
|
+
const hex = Array.from(cbor)
|
|
60
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
61
|
+
.join("");
|
|
62
|
+
return `h'${hex}'`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle strings
|
|
66
|
+
if (typeof cbor === "string") {
|
|
67
|
+
return JSON.stringify(cbor);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle CBOR objects with type information
|
|
71
|
+
if (typeof cbor === "object" && cbor !== null && "type" in cbor) {
|
|
72
|
+
const typed = cbor as { type: number; value: unknown };
|
|
73
|
+
switch (typed.type) {
|
|
74
|
+
case 0: // Unsigned
|
|
75
|
+
return String(typed.value);
|
|
76
|
+
case 1: // Negative
|
|
77
|
+
return String(-1 - Number(typed.value));
|
|
78
|
+
case 7: {
|
|
79
|
+
// Simple
|
|
80
|
+
const simpleValue = typed.value;
|
|
81
|
+
if (simpleValue !== null && typeof simpleValue === "object" && "type" in simpleValue) {
|
|
82
|
+
const floatValue = simpleValue as { type: string; value: unknown };
|
|
83
|
+
if (floatValue.type === "Float") {
|
|
84
|
+
return String(floatValue.value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (simpleValue === 20) return "false";
|
|
88
|
+
if (simpleValue === 21) return "true";
|
|
89
|
+
if (simpleValue === 22) return "null";
|
|
90
|
+
if (simpleValue === 23) return "undefined";
|
|
91
|
+
return `simple(${String(simpleValue)})`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback for primitives
|
|
97
|
+
if (typeof cbor === "boolean") return String(cbor);
|
|
98
|
+
if (typeof cbor === "number") return String(cbor);
|
|
99
|
+
if (typeof cbor === "bigint") return String(cbor);
|
|
100
|
+
if (cbor === null) return "null";
|
|
101
|
+
if (cbor === undefined) return "undefined";
|
|
102
|
+
|
|
103
|
+
// Unknown type - try JSON stringify
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(cbor);
|
|
106
|
+
} catch {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
108
|
+
return String(cbor);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Implementation of diagnostic()
|
|
113
|
+
Envelope.prototype.diagnostic = function (this: Envelope): string {
|
|
114
|
+
const cbor = this.taggedCbor();
|
|
115
|
+
return cborToDiagnostic(cbor);
|
|
116
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Envelope } from "../base/envelope";
|
|
2
|
+
import { cborData } from "@bcts/dcbor";
|
|
3
|
+
|
|
4
|
+
/// Hex formatting for Gordian Envelopes.
|
|
5
|
+
///
|
|
6
|
+
/// This module provides methods for converting envelopes to hexadecimal
|
|
7
|
+
/// representations of their CBOR encoding, useful for debugging and
|
|
8
|
+
/// low-level inspection.
|
|
9
|
+
|
|
10
|
+
// Note: Method declarations are in the base Envelope class.
|
|
11
|
+
// This module provides the prototype implementations.
|
|
12
|
+
|
|
13
|
+
/// Implementation of hex()
|
|
14
|
+
Envelope.prototype.hex = function (this: Envelope): string {
|
|
15
|
+
const bytes = this.cborBytes();
|
|
16
|
+
return Array.from(bytes)
|
|
17
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
18
|
+
.join("");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/// Implementation of cborBytes()
|
|
22
|
+
Envelope.prototype.cborBytes = function (this: Envelope): Uint8Array {
|
|
23
|
+
const cbor = this.taggedCbor();
|
|
24
|
+
return cborData(cbor);
|
|
25
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// Format module exports for Gordian Envelope.
|
|
2
|
+
///
|
|
3
|
+
/// This module provides various formatting options for displaying and
|
|
4
|
+
/// serializing envelopes, including hex, diagnostic, notation, tree,
|
|
5
|
+
/// and mermaid diagram formats.
|
|
6
|
+
|
|
7
|
+
// Export types
|
|
8
|
+
export type { TreeFormatOptions } from "./tree";
|
|
9
|
+
|
|
10
|
+
// Import side-effect modules to register prototype extensions
|
|
11
|
+
import "./hex";
|
|
12
|
+
import "./diagnostic";
|
|
13
|
+
import "./tree";
|