@altopelago/aeon-cli 0.9.0
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/dist/main.d.ts +53 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +2731 -0
- package/dist/main.js.map +1 -0
- package/dist/runtime-bind.d.ts +56 -0
- package/dist/runtime-bind.d.ts.map +1 -0
- package/dist/runtime-bind.js +264 -0
- package/dist/runtime-bind.js.map +1 -0
- package/package.json +34 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2731 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @altopelago/aeon-cli - AEON Command Line Interface
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* - aeon version Show version
|
|
7
|
+
* - aeon check <file> Validate AEON document (CI-friendly)
|
|
8
|
+
* - aeon doctor Check environment and contract wiring
|
|
9
|
+
* - aeon fmt [file] Format AEON document (stdout by default)
|
|
10
|
+
* - aeon inspect <file> Inspect AEON document (human-readable)
|
|
11
|
+
* - aeon finalize <file> Finalize AEON document to JSON
|
|
12
|
+
* - aeon bind <file> Run typed runtime binding with schema JSON
|
|
13
|
+
* - aeon integrity validate <file> Validate integrity envelope
|
|
14
|
+
* - aeon integrity verify <file> Verify integrity envelope hashes
|
|
15
|
+
* - aeon integrity sign <file> Generate integrity envelope snippet
|
|
16
|
+
*
|
|
17
|
+
* Flags:
|
|
18
|
+
* - --json Output as JSON (inspect/finalize/integrity)
|
|
19
|
+
* - --contract-registry Trusted contract registry JSON path (doctor/bind)
|
|
20
|
+
* - --write Write formatted output back to file (fmt only)
|
|
21
|
+
* - --annotations Include annotation stream records in inspect/bind output
|
|
22
|
+
* - --annotations-only Output only annotation stream records in inspect output
|
|
23
|
+
* - --sort-annotations Sort annotation records deterministically before output (inspect/bind)
|
|
24
|
+
* - --map Output finalized map (finalize only)
|
|
25
|
+
* - --scope Finalization scope: payload|header|full (finalize/bind)
|
|
26
|
+
* - --projected Materialize only explicitly included canonical paths (finalize/bind)
|
|
27
|
+
* - --include-path Canonical path to include in projected materialization (repeatable; finalize/bind)
|
|
28
|
+
* - --recovery Enable recovery mode (partial results with errors)
|
|
29
|
+
* - --max-input-bytes Maximum UTF-8 input size in bytes
|
|
30
|
+
* - --max-attribute-depth Maximum attribute selector depth
|
|
31
|
+
* - --max-separator-depth Maximum separator-spec depth
|
|
32
|
+
* - --max-generic-depth Maximum nested generic type depth
|
|
33
|
+
* - --max-nesting-depth Maximum container nesting depth
|
|
34
|
+
* - --max-materialized-weight Maximum cumulative clone materialization weight
|
|
35
|
+
* - --max-reference-depth Maximum clone resolution depth
|
|
36
|
+
* - --schema Schema JSON path (bind only)
|
|
37
|
+
* - --profile Profile id (bind only)
|
|
38
|
+
* - --contract-registry Trusted contract registry JSON path (bind only)
|
|
39
|
+
* - --trailing-separator-delimiter-policy off|warn|error (bind only)
|
|
40
|
+
* - --datatype-policy reserved_only|allow_custom (check/inspect/finalize/bind)
|
|
41
|
+
* - --rich Preset alias for --datatype-policy allow_custom
|
|
42
|
+
* - --strict Strict mode (default)
|
|
43
|
+
* - --loose Loose mode (warnings only)
|
|
44
|
+
* - --public-key Public key path for signature verification
|
|
45
|
+
* - --private-key Private key path for signing
|
|
46
|
+
* - --receipt Receipt sidecar path override
|
|
47
|
+
* - --write Write generated envelope to file (sign only)
|
|
48
|
+
* - --replace Replace existing envelope (sign only)
|
|
49
|
+
* - --include-bytes Include bytes_hash in envelope (sign only)
|
|
50
|
+
* - --include-checksum Include checksum_value in envelope (sign only)
|
|
51
|
+
*/
|
|
52
|
+
import * as fs from 'node:fs';
|
|
53
|
+
import * as path from 'node:path';
|
|
54
|
+
import { fileURLToPath } from 'node:url';
|
|
55
|
+
import { createHash } from 'node:crypto';
|
|
56
|
+
import { canonicalize } from '@altopelago/aeon-canonical';
|
|
57
|
+
import { compile, VERSION, formatPath } from '@altopelago/aeon-core';
|
|
58
|
+
import { finalizeJson, finalizeMap } from '@altopelago/aeon-finalize';
|
|
59
|
+
import { buildCanonicalReceipt, computeCanonicalHash, computeByteHash, signStringPayload, validateEnvelopeEvents, verifyStringPayloadSignature, } from '@altopelago/aeon-integrity';
|
|
60
|
+
import { tokenize } from '@altopelago/aeon-lexer';
|
|
61
|
+
import { parse } from '@altopelago/aeon-parser';
|
|
62
|
+
import { inspectHeader } from '@altopelago/aeon-transport';
|
|
63
|
+
import { runTypedRuntime } from './runtime-bind.js';
|
|
64
|
+
const GP_SECURITY_CONVENTIONS = [
|
|
65
|
+
'aeon.gp.security.v1',
|
|
66
|
+
'aeon.gp.integrity.v1',
|
|
67
|
+
'aeon.gp.signature.v1',
|
|
68
|
+
];
|
|
69
|
+
const ENVELOPE_DATATYPE = 'envelope';
|
|
70
|
+
const ENVELOPE_CONVENTION_KEY = 'close';
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// MAIN CLI ENTRY POINT
|
|
73
|
+
// =============================================================================
|
|
74
|
+
const args = process.argv.slice(2);
|
|
75
|
+
const command = args[0];
|
|
76
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
77
|
+
const __dirname = path.dirname(__filename);
|
|
78
|
+
const cliPackageRoot = path.resolve(__dirname, '..');
|
|
79
|
+
const workspaceRoot = path.resolve(__dirname, '../../..');
|
|
80
|
+
const repoRoot = path.resolve(__dirname, '../../../../../');
|
|
81
|
+
switch (command) {
|
|
82
|
+
case 'version':
|
|
83
|
+
case '--version':
|
|
84
|
+
case '-v':
|
|
85
|
+
showVersion();
|
|
86
|
+
break;
|
|
87
|
+
case 'check':
|
|
88
|
+
check(args.slice(1));
|
|
89
|
+
break;
|
|
90
|
+
case 'doctor':
|
|
91
|
+
doctor(args.slice(1));
|
|
92
|
+
break;
|
|
93
|
+
case 'fmt':
|
|
94
|
+
fmt(args.slice(1));
|
|
95
|
+
break;
|
|
96
|
+
case 'inspect':
|
|
97
|
+
inspect(args.slice(1));
|
|
98
|
+
break;
|
|
99
|
+
case 'finalize':
|
|
100
|
+
finalize(args.slice(1));
|
|
101
|
+
break;
|
|
102
|
+
case 'bind':
|
|
103
|
+
bind(args.slice(1));
|
|
104
|
+
break;
|
|
105
|
+
case 'integrity':
|
|
106
|
+
integrity(args.slice(1));
|
|
107
|
+
break;
|
|
108
|
+
case 'help':
|
|
109
|
+
case '--help':
|
|
110
|
+
case '-h':
|
|
111
|
+
case undefined:
|
|
112
|
+
showHelp();
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
console.error(`Error: Unknown command: ${command}`);
|
|
116
|
+
console.error('Usage: aeon <command> [options] [file]');
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// COMMANDS
|
|
121
|
+
// =============================================================================
|
|
122
|
+
function showVersion() {
|
|
123
|
+
console.log(`aeon ${VERSION}`);
|
|
124
|
+
}
|
|
125
|
+
function showHelp() {
|
|
126
|
+
console.log(`
|
|
127
|
+
AEON CLI v${VERSION}
|
|
128
|
+
|
|
129
|
+
Usage: aeon <command> [options] [file]
|
|
130
|
+
|
|
131
|
+
Commands:
|
|
132
|
+
version Show version
|
|
133
|
+
check <file> Validate AEON document (CI-friendly)
|
|
134
|
+
doctor Check environment and contract wiring
|
|
135
|
+
fmt [file] Format AEON document (stdout by default)
|
|
136
|
+
inspect <file> Inspect AEON document
|
|
137
|
+
finalize <file> Finalize AEON document to JSON
|
|
138
|
+
bind <file> Run typed runtime binding with schema JSON
|
|
139
|
+
integrity validate <file> Validate integrity envelope
|
|
140
|
+
integrity verify <file> Verify integrity envelope hashes
|
|
141
|
+
integrity sign <file> Generate integrity envelope snippet
|
|
142
|
+
|
|
143
|
+
Options:
|
|
144
|
+
--write Write formatted output back to file (fmt only)
|
|
145
|
+
--contract-registry Trusted contract registry JSON path (doctor/bind)
|
|
146
|
+
--json Output as JSON (inspect/finalize)
|
|
147
|
+
--annotations Include annotation stream records in inspect/bind output
|
|
148
|
+
--annotations-only Output only annotation stream records in inspect output
|
|
149
|
+
--sort-annotations Sort annotation records deterministically before output (inspect/bind)
|
|
150
|
+
--map Output finalized map (finalize only)
|
|
151
|
+
--scope Finalization scope: payload|header|full (finalize/bind)
|
|
152
|
+
--projected Materialize only explicitly included canonical paths (finalize/bind)
|
|
153
|
+
--include-path Canonical path to include in projected materialization (repeatable; finalize/bind)
|
|
154
|
+
--recovery Enable recovery mode (partial results)
|
|
155
|
+
--max-input-bytes Maximum UTF-8 input size in bytes
|
|
156
|
+
--max-attribute-depth Maximum attribute selector depth
|
|
157
|
+
--max-separator-depth Maximum separator-spec depth
|
|
158
|
+
--max-generic-depth Maximum nested generic type depth
|
|
159
|
+
--max-nesting-depth Maximum container nesting depth
|
|
160
|
+
--schema Schema JSON path (bind only)
|
|
161
|
+
--profile Profile id (bind only)
|
|
162
|
+
--contract-registry Trusted contract registry JSON path (bind only)
|
|
163
|
+
--trailing-separator-delimiter-policy off|warn|error (bind only)
|
|
164
|
+
--datatype-policy reserved_only|allow_custom (check/inspect/finalize/bind)
|
|
165
|
+
--rich Preset alias for --datatype-policy allow_custom
|
|
166
|
+
--strict Strict mode (default)
|
|
167
|
+
--loose Loose mode (warnings only)
|
|
168
|
+
--public-key Public key path for signature verification
|
|
169
|
+
--private-key Private key path for signing
|
|
170
|
+
--receipt Receipt sidecar path override
|
|
171
|
+
--write Write generated envelope to file (sign only)
|
|
172
|
+
--replace Replace existing envelope (sign only)
|
|
173
|
+
--include-bytes Include bytes_hash in envelope (sign only)
|
|
174
|
+
--include-checksum Include checksum_value in envelope (sign only)
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
aeon check config.aeon
|
|
178
|
+
aeon doctor
|
|
179
|
+
aeon doctor --json
|
|
180
|
+
aeon doctor --contract-registry ./contracts/registry.json
|
|
181
|
+
aeon fmt config.aeon
|
|
182
|
+
cat config.aeon | aeon fmt
|
|
183
|
+
aeon fmt config.aeon --write
|
|
184
|
+
aeon inspect config.aeon
|
|
185
|
+
aeon inspect config.aeon --json
|
|
186
|
+
aeon inspect config.aeon --json --annotations
|
|
187
|
+
aeon inspect config.aeon --json --annotations-only
|
|
188
|
+
aeon inspect config.aeon --json --annotations-only --sort-annotations
|
|
189
|
+
aeon inspect config.aeon --recovery
|
|
190
|
+
aeon finalize config.aeon
|
|
191
|
+
aeon finalize config.aeon --json
|
|
192
|
+
aeon finalize config.aeon --map
|
|
193
|
+
aeon finalize config.aeon --scope full
|
|
194
|
+
aeon finalize config.aeon --loose
|
|
195
|
+
aeon finalize config.aeon --projected --include-path '$.app.name'
|
|
196
|
+
aeon finalize config.aeon --map --include-path '$.app.name' --include-path '$.app.port'
|
|
197
|
+
aeon bind config.aeon --schema config.schema.json
|
|
198
|
+
aeon bind config.aeon --schema config.schema.json --profile aeon.gp.profile.v1
|
|
199
|
+
aeon bind config.aeon --contract-registry contracts/registry.json
|
|
200
|
+
aeon bind config.aeon --schema config.schema.json --include-path '$.app.name'
|
|
201
|
+
aeon bind config.aeon --schema config.schema.json --include-path '$.app.name' --include-path '$.app.port'
|
|
202
|
+
aeon bind config.aeon --schema config.schema.json --trailing-separator-delimiter-policy warn
|
|
203
|
+
aeon inspect config.aeon --datatype-policy allow_custom
|
|
204
|
+
aeon inspect config.aeon --rich
|
|
205
|
+
aeon bind config.aeon --schema config.schema.json --datatype-policy allow_custom
|
|
206
|
+
aeon bind config.aeon --schema config.schema.json --rich
|
|
207
|
+
aeon bind config.aeon --schema config.schema.json --loose
|
|
208
|
+
aeon bind config.aeon --schema config.schema.json --annotations
|
|
209
|
+
aeon bind config.aeon --schema config.schema.json --annotations --sort-annotations
|
|
210
|
+
aeon integrity validate config.aeon
|
|
211
|
+
aeon integrity verify config.aeon
|
|
212
|
+
aeon integrity verify config.aeon --public-key ./aeon.pub
|
|
213
|
+
aeon integrity verify config.aeon --receipt ./config.aeon.receipt.json
|
|
214
|
+
aeon integrity sign config.aeon --private-key ./aeon.key
|
|
215
|
+
aeon integrity sign config.aeon --private-key ./aeon.key --write
|
|
216
|
+
aeon integrity sign config.aeon --private-key ./aeon.key --write --receipt ./config.aeon.receipt.json
|
|
217
|
+
aeon integrity sign config.aeon --private-key ./aeon.key --write --replace
|
|
218
|
+
aeon integrity sign config.aeon --private-key ./aeon.key --include-bytes
|
|
219
|
+
aeon integrity sign config.aeon --private-key ./aeon.key --include-checksum
|
|
220
|
+
`.trim());
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* aeon check <file>
|
|
224
|
+
* Purpose: validation only (CI-friendly)
|
|
225
|
+
* Exit code: 0 = valid, 1 = errors
|
|
226
|
+
*/
|
|
227
|
+
function check(args) {
|
|
228
|
+
const file = findFile(args);
|
|
229
|
+
if (!file) {
|
|
230
|
+
console.error('Error: No file specified');
|
|
231
|
+
console.error('Usage: aeon check <file>');
|
|
232
|
+
process.exit(2);
|
|
233
|
+
}
|
|
234
|
+
const datatypePolicy = resolveDatatypePolicy(args);
|
|
235
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
236
|
+
if (args.includes('--datatype-policy') && !datatypePolicy) {
|
|
237
|
+
console.error('Error: Invalid value for --datatype-policy (expected reserved_only or allow_custom)');
|
|
238
|
+
console.error('Usage: aeon check <file> [--datatype-policy <reserved_only|allow_custom>]');
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
if (maxInputBytes === null) {
|
|
242
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
243
|
+
process.exit(2);
|
|
244
|
+
}
|
|
245
|
+
const input = readFile(file);
|
|
246
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
247
|
+
const result = compile(input, {
|
|
248
|
+
...(datatypePolicy ? { datatypePolicy } : {}),
|
|
249
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
250
|
+
});
|
|
251
|
+
if (result.errors.length === 0) {
|
|
252
|
+
console.log('OK');
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
for (const error of result.errors) {
|
|
257
|
+
console.log(formatErrorLine(error));
|
|
258
|
+
}
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* aeon doctor [--json] [--contract-registry <registry.json>]
|
|
264
|
+
* Purpose: environment and contract wiring diagnostics
|
|
265
|
+
*/
|
|
266
|
+
function doctor(args) {
|
|
267
|
+
const jsonOutput = args.includes('--json');
|
|
268
|
+
const registryFlagPresent = args.includes('--contract-registry');
|
|
269
|
+
const registryFlagValue = getFlagValue(args, '--contract-registry');
|
|
270
|
+
if (registryFlagPresent && !registryFlagValue) {
|
|
271
|
+
console.error('Error: Missing value for --contract-registry <registry.json>');
|
|
272
|
+
console.error('Usage: aeon doctor [--json] [--contract-registry <registry.json>]');
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
const doctorResult = runDoctor({
|
|
276
|
+
contractRegistryPath: registryFlagValue
|
|
277
|
+
? path.resolve(process.cwd(), registryFlagValue)
|
|
278
|
+
: getDefaultContractRegistryPath(),
|
|
279
|
+
});
|
|
280
|
+
if (jsonOutput) {
|
|
281
|
+
console.log(JSON.stringify(doctorResult, null, 2));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
outputDoctorHuman(doctorResult);
|
|
285
|
+
}
|
|
286
|
+
process.exit(doctorResult.ok ? 0 : 1);
|
|
287
|
+
}
|
|
288
|
+
function getDefaultContractRegistryPath() {
|
|
289
|
+
const specsRoot = process.env.AEONITE_SPECS_ROOT
|
|
290
|
+
? path.resolve(process.env.AEONITE_SPECS_ROOT)
|
|
291
|
+
: path.resolve(repoRoot, '..', '..', 'aeonite-org', 'aeonite-specs');
|
|
292
|
+
return path.resolve(specsRoot, 'aeon/v1/drafts/contracts/registry.json');
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* aeon fmt [file] [--write]
|
|
296
|
+
* Purpose: deterministic, idempotent formatting
|
|
297
|
+
*/
|
|
298
|
+
function fmt(args) {
|
|
299
|
+
const writeOutput = args.includes('--write');
|
|
300
|
+
const file = findFile(args);
|
|
301
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
302
|
+
if (writeOutput && !file) {
|
|
303
|
+
console.error('Error: --write requires a file path');
|
|
304
|
+
console.error('Usage: aeon fmt [file] [--write]');
|
|
305
|
+
process.exit(2);
|
|
306
|
+
}
|
|
307
|
+
if (maxInputBytes === null) {
|
|
308
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
309
|
+
process.exit(2);
|
|
310
|
+
}
|
|
311
|
+
const input = file ? readFile(file) : readStdin();
|
|
312
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
313
|
+
const result = canonicalize(input);
|
|
314
|
+
if (result.errors.length > 0) {
|
|
315
|
+
for (const error of result.errors) {
|
|
316
|
+
console.log(formatGenericErrorLine(error));
|
|
317
|
+
}
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const formatted = ensureTrailingNewline(result.text);
|
|
321
|
+
if (writeOutput) {
|
|
322
|
+
if (input !== formatted) {
|
|
323
|
+
writeFileWithBackup(file, formatted);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(formatted);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* aeon inspect <file> [--json] [--recovery] [--annotations] [--annotations-only] [--sort-annotations]
|
|
331
|
+
* Purpose: human inspection (default) or JSON output
|
|
332
|
+
*/
|
|
333
|
+
function inspect(args) {
|
|
334
|
+
const inspectUsage = 'Usage: aeon inspect <file> [--json] [--recovery] [--annotations] [--annotations-only] [--sort-annotations] [--datatype-policy <reserved_only|allow_custom>] [--max-input-bytes <n>] [--max-attribute-depth <n>] [--max-separator-depth <n>] [--max-generic-depth <n>] [--max-nesting-depth <n>]';
|
|
335
|
+
const file = findFileWithValueFlags(args, ['--datatype-policy', '--max-input-bytes', '--max-attribute-depth', '--max-separator-depth', '--max-generic-depth', '--max-nesting-depth']);
|
|
336
|
+
const jsonOutput = args.includes('--json');
|
|
337
|
+
const recovery = args.includes('--recovery');
|
|
338
|
+
const annotationsOnly = args.includes('--annotations-only');
|
|
339
|
+
const includeAnnotations = args.includes('--annotations');
|
|
340
|
+
const sortAnnotations = args.includes('--sort-annotations');
|
|
341
|
+
const datatypePolicy = resolveDatatypePolicy(args);
|
|
342
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
343
|
+
const maxAttributeDepth = resolveDepthOption(args, '--max-attribute-depth');
|
|
344
|
+
const maxSeparatorDepth = resolveDepthOption(args, '--max-separator-depth');
|
|
345
|
+
const maxGenericDepth = resolveDepthOption(args, '--max-generic-depth');
|
|
346
|
+
const maxNestingDepth = resolveDepthOption(args, '--max-nesting-depth');
|
|
347
|
+
if (!file) {
|
|
348
|
+
console.error('Error: No file specified');
|
|
349
|
+
console.error(inspectUsage);
|
|
350
|
+
process.exit(2);
|
|
351
|
+
}
|
|
352
|
+
if (args.includes('--datatype-policy') && !datatypePolicy) {
|
|
353
|
+
console.error('Error: Invalid value for --datatype-policy (expected reserved_only or allow_custom)');
|
|
354
|
+
console.error(inspectUsage);
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
if (maxInputBytes === null) {
|
|
358
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
359
|
+
process.exit(2);
|
|
360
|
+
}
|
|
361
|
+
if (maxAttributeDepth === null) {
|
|
362
|
+
console.error('Error: Invalid value for --max-attribute-depth (expected a non-negative integer)');
|
|
363
|
+
process.exit(2);
|
|
364
|
+
}
|
|
365
|
+
if (maxSeparatorDepth === null) {
|
|
366
|
+
console.error('Error: Invalid value for --max-separator-depth (expected a non-negative integer)');
|
|
367
|
+
process.exit(2);
|
|
368
|
+
}
|
|
369
|
+
if (maxGenericDepth === null) {
|
|
370
|
+
console.error('Error: Invalid value for --max-generic-depth (expected a non-negative integer)');
|
|
371
|
+
process.exit(2);
|
|
372
|
+
}
|
|
373
|
+
if (maxNestingDepth === null) {
|
|
374
|
+
console.error('Error: Invalid value for --max-nesting-depth (expected a non-negative integer)');
|
|
375
|
+
process.exit(2);
|
|
376
|
+
}
|
|
377
|
+
const input = readFile(file);
|
|
378
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
379
|
+
const result = compile(input, {
|
|
380
|
+
recovery,
|
|
381
|
+
emitAnnotations: includeAnnotations || annotationsOnly,
|
|
382
|
+
...(datatypePolicy ? { datatypePolicy } : {}),
|
|
383
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
384
|
+
...(maxAttributeDepth !== undefined ? { maxAttributeDepth } : {}),
|
|
385
|
+
...(maxSeparatorDepth !== undefined ? { maxSeparatorDepth } : {}),
|
|
386
|
+
...(maxGenericDepth !== undefined ? { maxGenericDepth } : {}),
|
|
387
|
+
...(maxNestingDepth !== undefined ? { maxNestingDepth } : {}),
|
|
388
|
+
});
|
|
389
|
+
const headerInfo = extractHeaderInfo(input);
|
|
390
|
+
const mode = headerInfo.mode;
|
|
391
|
+
if (jsonOutput) {
|
|
392
|
+
outputJSON(result, { includeAnnotations, annotationsOnly, sortAnnotations }, headerInfo);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
outputMarkdown(file, result, {
|
|
396
|
+
recovery,
|
|
397
|
+
mode,
|
|
398
|
+
version: headerInfo.version,
|
|
399
|
+
profile: headerInfo.profile,
|
|
400
|
+
schema: headerInfo.schema,
|
|
401
|
+
schemas: headerInfo.schemas,
|
|
402
|
+
includeAnnotations: includeAnnotations || annotationsOnly,
|
|
403
|
+
annotationsOnly,
|
|
404
|
+
sortAnnotations,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// Exit with error code if errors present
|
|
408
|
+
if (result.errors.length > 0) {
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* aeon finalize <file> [--json|--map] [--recovery] [--strict|--loose]
|
|
414
|
+
* Purpose: finalize AES into JSON output
|
|
415
|
+
*/
|
|
416
|
+
function finalize(args) {
|
|
417
|
+
const finalizeUsage = 'Usage: aeon finalize <file> [--json|--map] [--recovery] [--strict|--loose] [--projected] [--include-path <$.path>] [--scope <payload|header|full>] [--datatype-policy <reserved_only|allow_custom>] [--max-input-bytes <n>] [--max-materialized-weight <n>] [--max-reference-depth <n>]';
|
|
418
|
+
const file = findFileWithValueFlags(args, ['--datatype-policy', '--include-path', '--scope', '--max-input-bytes', '--max-materialized-weight', '--max-reference-depth']);
|
|
419
|
+
const recovery = args.includes('--recovery');
|
|
420
|
+
const mode = resolveFinalizeMode(args);
|
|
421
|
+
const outputMap = args.includes('--map');
|
|
422
|
+
const datatypePolicy = resolveDatatypePolicy(args);
|
|
423
|
+
const scope = resolveFinalizeScope(args);
|
|
424
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
425
|
+
const maxMaterializedWeight = resolveDepthOption(args, '--max-materialized-weight');
|
|
426
|
+
const maxReferenceDepth = resolveDepthOption(args, '--max-reference-depth');
|
|
427
|
+
const includePaths = getFlagValues(args, '--include-path');
|
|
428
|
+
const projected = args.includes('--projected') || includePaths.length > 0;
|
|
429
|
+
if (!file) {
|
|
430
|
+
console.error('Error: No file specified');
|
|
431
|
+
console.error(finalizeUsage);
|
|
432
|
+
process.exit(2);
|
|
433
|
+
}
|
|
434
|
+
if (!mode) {
|
|
435
|
+
console.error('Error: Cannot use both --strict and --loose');
|
|
436
|
+
console.error(finalizeUsage);
|
|
437
|
+
process.exit(2);
|
|
438
|
+
}
|
|
439
|
+
if (args.includes('--datatype-policy') && !datatypePolicy) {
|
|
440
|
+
console.error('Error: Invalid value for --datatype-policy (expected reserved_only or allow_custom)');
|
|
441
|
+
console.error(finalizeUsage);
|
|
442
|
+
process.exit(2);
|
|
443
|
+
}
|
|
444
|
+
if (!scope) {
|
|
445
|
+
console.error('Error: Invalid value for --scope (expected payload, header, or full)');
|
|
446
|
+
process.exit(2);
|
|
447
|
+
}
|
|
448
|
+
if (maxInputBytes === null) {
|
|
449
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
450
|
+
process.exit(2);
|
|
451
|
+
}
|
|
452
|
+
if (maxMaterializedWeight === null) {
|
|
453
|
+
console.error('Error: Invalid value for --max-materialized-weight (expected a non-negative integer)');
|
|
454
|
+
process.exit(2);
|
|
455
|
+
}
|
|
456
|
+
if (maxReferenceDepth === null) {
|
|
457
|
+
console.error('Error: Invalid value for --max-reference-depth (expected a non-negative integer)');
|
|
458
|
+
process.exit(2);
|
|
459
|
+
}
|
|
460
|
+
if (args.includes('--include-path') && includePaths.length === 0) {
|
|
461
|
+
console.error('Error: Missing value for --include-path <$.path>');
|
|
462
|
+
console.error(finalizeUsage);
|
|
463
|
+
process.exit(2);
|
|
464
|
+
}
|
|
465
|
+
if (projected && includePaths.length === 0) {
|
|
466
|
+
console.error('Error: --projected requires at least one --include-path <$.path>');
|
|
467
|
+
console.error(finalizeUsage);
|
|
468
|
+
process.exit(2);
|
|
469
|
+
}
|
|
470
|
+
const input = readFile(file);
|
|
471
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
472
|
+
const result = compile(input, {
|
|
473
|
+
recovery,
|
|
474
|
+
...(datatypePolicy ? { datatypePolicy } : {}),
|
|
475
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
476
|
+
});
|
|
477
|
+
const finalizeOptions = {
|
|
478
|
+
mode,
|
|
479
|
+
scope,
|
|
480
|
+
...(result.header ? { header: result.header } : {}),
|
|
481
|
+
...(projected ? { materialization: 'projected', includePaths } : {}),
|
|
482
|
+
...(maxMaterializedWeight !== undefined ? { maxMaterializedWeight } : {}),
|
|
483
|
+
...(maxReferenceDepth !== undefined ? { maxReferenceDepth } : {}),
|
|
484
|
+
};
|
|
485
|
+
const output = outputMap
|
|
486
|
+
? finalizeMapOutput(result, finalizeOptions)
|
|
487
|
+
: finalizeJsonOutput(result, finalizeOptions);
|
|
488
|
+
console.log(JSON.stringify(output, null, 2));
|
|
489
|
+
const hasErrors = (output.meta?.errors?.length ?? 0) > 0;
|
|
490
|
+
if (hasErrors) {
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]
|
|
496
|
+
* Purpose: run phase-ordered runtime binding with schema validation
|
|
497
|
+
*/
|
|
498
|
+
function bind(args) {
|
|
499
|
+
const mode = resolveFinalizeMode(args);
|
|
500
|
+
if (!mode) {
|
|
501
|
+
console.error('Error: Cannot use both --strict and --loose');
|
|
502
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
503
|
+
process.exit(2);
|
|
504
|
+
}
|
|
505
|
+
const file = findFileWithValueFlags(args, ['--schema', '--profile', '--contract-registry', '--trailing-separator-delimiter-policy', '--datatype-policy', '--include-path']);
|
|
506
|
+
if (!file) {
|
|
507
|
+
console.error('Error: No file specified');
|
|
508
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
509
|
+
process.exit(2);
|
|
510
|
+
}
|
|
511
|
+
const schemaPath = getFlagValue(args, '--schema');
|
|
512
|
+
const hasProfileFlag = args.includes('--profile');
|
|
513
|
+
const profile = getFlagValue(args, '--profile');
|
|
514
|
+
const hasRegistryFlag = args.includes('--contract-registry');
|
|
515
|
+
const contractRegistryPath = getFlagValue(args, '--contract-registry');
|
|
516
|
+
const trailingSeparatorPolicyFlag = '--trailing-separator-delimiter-policy';
|
|
517
|
+
const hasTrailingSeparatorPolicy = args.includes(trailingSeparatorPolicyFlag);
|
|
518
|
+
const trailingSeparatorPolicyValue = getFlagValue(args, trailingSeparatorPolicyFlag);
|
|
519
|
+
const trailingSeparatorDelimiterPolicy = trailingSeparatorPolicyValue === 'off' ||
|
|
520
|
+
trailingSeparatorPolicyValue === 'warn' ||
|
|
521
|
+
trailingSeparatorPolicyValue === 'error'
|
|
522
|
+
? trailingSeparatorPolicyValue
|
|
523
|
+
: undefined;
|
|
524
|
+
const datatypePolicy = resolveDatatypePolicy(args);
|
|
525
|
+
const scope = resolveFinalizeScope(args);
|
|
526
|
+
const includeAnnotations = args.includes('--annotations');
|
|
527
|
+
const sortAnnotations = args.includes('--sort-annotations');
|
|
528
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
529
|
+
const includePaths = getFlagValues(args, '--include-path');
|
|
530
|
+
const projected = args.includes('--projected') || includePaths.length > 0;
|
|
531
|
+
if (hasProfileFlag && !profile) {
|
|
532
|
+
console.error('Error: Missing value for --profile <id>');
|
|
533
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
534
|
+
process.exit(2);
|
|
535
|
+
}
|
|
536
|
+
if (hasRegistryFlag && !contractRegistryPath) {
|
|
537
|
+
console.error('Error: Missing value for --contract-registry <registry.json>');
|
|
538
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
539
|
+
process.exit(2);
|
|
540
|
+
}
|
|
541
|
+
if (hasTrailingSeparatorPolicy && !trailingSeparatorPolicyValue) {
|
|
542
|
+
console.error('Error: Missing value for --trailing-separator-delimiter-policy <off|warn|error>');
|
|
543
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
544
|
+
process.exit(2);
|
|
545
|
+
}
|
|
546
|
+
if (hasTrailingSeparatorPolicy && !trailingSeparatorDelimiterPolicy) {
|
|
547
|
+
console.error(`Error: Invalid value for --trailing-separator-delimiter-policy: ${trailingSeparatorPolicyValue}`);
|
|
548
|
+
console.error('Allowed values: off, warn, error');
|
|
549
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
550
|
+
process.exit(2);
|
|
551
|
+
}
|
|
552
|
+
if (args.includes('--datatype-policy') && !datatypePolicy) {
|
|
553
|
+
console.error('Error: Invalid value for --datatype-policy (expected reserved_only or allow_custom)');
|
|
554
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
555
|
+
process.exit(2);
|
|
556
|
+
}
|
|
557
|
+
if (!scope) {
|
|
558
|
+
console.error('Error: Invalid value for --scope (expected payload, header, or full)');
|
|
559
|
+
process.exit(2);
|
|
560
|
+
}
|
|
561
|
+
if (maxInputBytes === null) {
|
|
562
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
563
|
+
process.exit(2);
|
|
564
|
+
}
|
|
565
|
+
if (args.includes('--include-path') && includePaths.length === 0) {
|
|
566
|
+
console.error('Error: Missing value for --include-path <$.path>');
|
|
567
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
568
|
+
process.exit(2);
|
|
569
|
+
}
|
|
570
|
+
if (projected && includePaths.length === 0) {
|
|
571
|
+
console.error('Error: --projected requires at least one --include-path <$.path>');
|
|
572
|
+
console.error('Usage: aeon bind <file> [--schema <schema.json>] [--profile <id>] [--contract-registry <registry.json>] [--trailing-separator-delimiter-policy <off|warn|error>] [--datatype-policy <reserved_only|allow_custom>] [--strict|--loose] [--projected] [--include-path <$.path>] [--annotations] [--sort-annotations]');
|
|
573
|
+
process.exit(2);
|
|
574
|
+
}
|
|
575
|
+
const input = readFile(file);
|
|
576
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
577
|
+
const headerInfo = extractHeaderInfo(input);
|
|
578
|
+
const resolvedRegistryPath = contractRegistryPath
|
|
579
|
+
? path.resolve(process.cwd(), contractRegistryPath)
|
|
580
|
+
: null;
|
|
581
|
+
const registry = resolvedRegistryPath ? readContractRegistryFile(resolvedRegistryPath) : null;
|
|
582
|
+
const effectiveProfile = profile ?? headerInfo.profile;
|
|
583
|
+
if (registry && effectiveProfile) {
|
|
584
|
+
const entry = resolveContractEntry(registry, effectiveProfile, 'profile');
|
|
585
|
+
if (!entry) {
|
|
586
|
+
failContract('CONTRACT_UNKNOWN_PROFILE_ID', `Unknown profile contract id in registry: ${effectiveProfile}`);
|
|
587
|
+
}
|
|
588
|
+
const verified = verifyContractArtifact(entry, resolvedRegistryPath);
|
|
589
|
+
if (!verified.ok) {
|
|
590
|
+
failContract(verified.code, verified.error);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
let loadedSchema;
|
|
594
|
+
if (schemaPath) {
|
|
595
|
+
loadedSchema = isSchemaContractAeonPath(schemaPath)
|
|
596
|
+
? readSchemaContractAeonFile(schemaPath)
|
|
597
|
+
: readSchemaFile(schemaPath);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
if (!registry || !resolvedRegistryPath) {
|
|
601
|
+
console.error('Error: Missing required --schema <schema.json> (or provide --contract-registry with aeon:schema header id)');
|
|
602
|
+
process.exit(2);
|
|
603
|
+
}
|
|
604
|
+
if (!headerInfo.schema) {
|
|
605
|
+
console.error('Error: Missing schema contract id (aeon:schema) for registry resolution');
|
|
606
|
+
process.exit(2);
|
|
607
|
+
}
|
|
608
|
+
const entry = resolveContractEntry(registry, headerInfo.schema, 'schema');
|
|
609
|
+
if (!entry) {
|
|
610
|
+
failContract('CONTRACT_UNKNOWN_SCHEMA_ID', `Unknown schema contract id in registry: ${headerInfo.schema}`);
|
|
611
|
+
}
|
|
612
|
+
const verified = verifyContractArtifact(entry, resolvedRegistryPath);
|
|
613
|
+
if (!verified.ok) {
|
|
614
|
+
failContract(verified.code, verified.error);
|
|
615
|
+
}
|
|
616
|
+
loadedSchema = readSchemaContractAeonFile(verified.resolvedPath, entry.id);
|
|
617
|
+
}
|
|
618
|
+
const result = runTypedRuntime(input, {
|
|
619
|
+
schema: loadedSchema.schema,
|
|
620
|
+
mode,
|
|
621
|
+
...(datatypePolicy ? { datatypePolicy } : {}),
|
|
622
|
+
includeAnnotations,
|
|
623
|
+
scope,
|
|
624
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
625
|
+
...(projected ? { materialization: 'projected', includePaths } : {}),
|
|
626
|
+
...(trailingSeparatorDelimiterPolicy ? { trailingSeparatorDelimiterPolicy } : {}),
|
|
627
|
+
...(effectiveProfile ? { profile: effectiveProfile } : {}),
|
|
628
|
+
});
|
|
629
|
+
const contractWarnings = collectDeclaredContractOverrideWarnings(headerInfo, loadedSchema.schemaId, effectiveProfile);
|
|
630
|
+
const contractMeta = buildBindContractMeta(headerInfo, loadedSchema.schemaId, effectiveProfile);
|
|
631
|
+
const meta = {
|
|
632
|
+
...result.meta,
|
|
633
|
+
warnings: [...result.meta.warnings, ...contractWarnings],
|
|
634
|
+
...(contractMeta ? { contracts: contractMeta } : {}),
|
|
635
|
+
};
|
|
636
|
+
const annotations = includeAnnotations && result.annotations
|
|
637
|
+
? (sortAnnotations ? sortAnnotationRecords(result.annotations) : result.annotations)
|
|
638
|
+
: undefined;
|
|
639
|
+
console.log(JSON.stringify({
|
|
640
|
+
...(result.document !== undefined ? { document: result.document } : {}),
|
|
641
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
642
|
+
meta,
|
|
643
|
+
}, null, 2));
|
|
644
|
+
if (meta.errors.length > 0) {
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* aeon integrity <validate|verify> <file> [--strict|--loose] [--public-key <path>]
|
|
650
|
+
* Purpose: validate/verify integrity envelopes
|
|
651
|
+
*/
|
|
652
|
+
function integrity(args) {
|
|
653
|
+
const subcommand = args[0];
|
|
654
|
+
if (!subcommand) {
|
|
655
|
+
console.error('Error: Missing integrity subcommand');
|
|
656
|
+
console.error('Usage: aeon integrity <validate|verify> <file> [--strict|--loose]');
|
|
657
|
+
process.exit(2);
|
|
658
|
+
}
|
|
659
|
+
switch (subcommand) {
|
|
660
|
+
case 'validate':
|
|
661
|
+
integrityValidate(args.slice(1));
|
|
662
|
+
break;
|
|
663
|
+
case 'verify':
|
|
664
|
+
integrityVerify(args.slice(1));
|
|
665
|
+
break;
|
|
666
|
+
case 'sign':
|
|
667
|
+
integritySign(args.slice(1));
|
|
668
|
+
break;
|
|
669
|
+
default:
|
|
670
|
+
console.error(`Error: Unknown integrity subcommand: ${subcommand}`);
|
|
671
|
+
console.error('Usage: aeon integrity <validate|verify|sign> <file> [options]');
|
|
672
|
+
process.exit(2);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function integrityValidate(args) {
|
|
676
|
+
const mode = resolveIntegrityMode(args);
|
|
677
|
+
const jsonOutput = args.includes('--json');
|
|
678
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
679
|
+
if (!mode) {
|
|
680
|
+
console.error('Error: Cannot use both --strict and --loose');
|
|
681
|
+
console.error('Usage: aeon integrity validate <file> [--strict|--loose]');
|
|
682
|
+
process.exit(2);
|
|
683
|
+
}
|
|
684
|
+
if (maxInputBytes === null) {
|
|
685
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
686
|
+
process.exit(2);
|
|
687
|
+
}
|
|
688
|
+
const file = findFileWithValueFlags(args, ['--public-key', '--pubkey', '--receipt']);
|
|
689
|
+
if (!file) {
|
|
690
|
+
console.error('Error: No file specified');
|
|
691
|
+
console.error('Usage: aeon integrity validate <file> [--strict|--loose]');
|
|
692
|
+
process.exit(2);
|
|
693
|
+
}
|
|
694
|
+
const input = readFile(file);
|
|
695
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
696
|
+
const compileResult = compile(input, {
|
|
697
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
698
|
+
});
|
|
699
|
+
if (compileResult.errors.length > 0) {
|
|
700
|
+
const errors = [{
|
|
701
|
+
level: 'error',
|
|
702
|
+
code: 'ENVELOPE_PARSE_ERROR',
|
|
703
|
+
message: 'Invalid AEON document for envelope validation',
|
|
704
|
+
}];
|
|
705
|
+
if (jsonOutput) {
|
|
706
|
+
outputEnvelopeJson(errors, [], false);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
outputEnvelopeDiagnostics(errors, 'ERROR');
|
|
710
|
+
}
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const result = validateEnvelopeEvents(compileResult.events, { mode });
|
|
714
|
+
if (jsonOutput) {
|
|
715
|
+
outputEnvelopeJson(result.errors, result.warnings, result.errors.length === 0);
|
|
716
|
+
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
if (result.errors.length === 0) {
|
|
720
|
+
if (result.warnings.length > 0) {
|
|
721
|
+
outputEnvelopeDiagnostics(result.warnings, 'WARN');
|
|
722
|
+
}
|
|
723
|
+
console.log('OK');
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
outputEnvelopeDiagnostics(result.errors, 'ERROR');
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function integrityVerify(args) {
|
|
731
|
+
const mode = resolveIntegrityMode(args);
|
|
732
|
+
const jsonOutput = args.includes('--json');
|
|
733
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
734
|
+
if (!mode) {
|
|
735
|
+
console.error('Error: Cannot use both --strict and --loose');
|
|
736
|
+
console.error('Usage: aeon integrity verify <file> [--strict|--loose] [--public-key <path>] [--receipt <path>]');
|
|
737
|
+
process.exit(2);
|
|
738
|
+
}
|
|
739
|
+
if (maxInputBytes === null) {
|
|
740
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
741
|
+
process.exit(2);
|
|
742
|
+
}
|
|
743
|
+
const file = findFileWithValueFlags(args, ['--public-key', '--pubkey']);
|
|
744
|
+
if (!file) {
|
|
745
|
+
console.error('Error: No file specified');
|
|
746
|
+
console.error('Usage: aeon integrity verify <file> [--strict|--loose] [--public-key <path>] [--receipt <path>]');
|
|
747
|
+
process.exit(2);
|
|
748
|
+
}
|
|
749
|
+
const input = readFile(file);
|
|
750
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
751
|
+
const baseInput = removeEnvelope(input);
|
|
752
|
+
const compileResult = compile(input, {
|
|
753
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
754
|
+
});
|
|
755
|
+
const baseCompileResult = compile(baseInput, {
|
|
756
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
757
|
+
});
|
|
758
|
+
if (compileResult.errors.length > 0) {
|
|
759
|
+
const errors = [{
|
|
760
|
+
level: 'error',
|
|
761
|
+
code: 'ENVELOPE_PARSE_ERROR',
|
|
762
|
+
message: 'Invalid AEON document for envelope validation',
|
|
763
|
+
}];
|
|
764
|
+
if (jsonOutput) {
|
|
765
|
+
outputEnvelopeJson(errors, [], false);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
outputEnvelopeDiagnostics(errors, 'ERROR');
|
|
769
|
+
}
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
if (baseCompileResult.errors.length > 0) {
|
|
773
|
+
const errors = [{
|
|
774
|
+
level: 'error',
|
|
775
|
+
code: 'ENVELOPE_PARSE_ERROR',
|
|
776
|
+
message: 'Invalid AEON document body for envelope verification',
|
|
777
|
+
}];
|
|
778
|
+
if (jsonOutput) {
|
|
779
|
+
outputEnvelopeJson(errors, [], false);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
outputEnvelopeDiagnostics(errors, 'ERROR');
|
|
783
|
+
}
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
const validation = validateEnvelopeEvents(compileResult.events, { mode });
|
|
787
|
+
if (validation.errors.length > 0) {
|
|
788
|
+
if (jsonOutput) {
|
|
789
|
+
outputEnvelopeJson(validation.errors, validation.warnings, false);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
outputEnvelopeDiagnostics(validation.errors, 'ERROR');
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
const diagnostics = {
|
|
796
|
+
errors: [...validation.errors],
|
|
797
|
+
warnings: [...validation.warnings],
|
|
798
|
+
};
|
|
799
|
+
const { fields, errors: parseErrors } = extractEnvelopeFields(input);
|
|
800
|
+
diagnostics.errors.push(...parseErrors);
|
|
801
|
+
if (!fields) {
|
|
802
|
+
diagnostics.errors.push({
|
|
803
|
+
level: 'error',
|
|
804
|
+
code: 'ENVELOPE_MISSING',
|
|
805
|
+
message: 'an :envelope binding is required for verification',
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const verificationMeta = {
|
|
809
|
+
canonical: { present: false },
|
|
810
|
+
bytes: { present: false },
|
|
811
|
+
checksum: { present: false },
|
|
812
|
+
signature: { present: false },
|
|
813
|
+
replay: { performed: false, status: 'unavailable' },
|
|
814
|
+
};
|
|
815
|
+
let receipt = resolveReceiptSidecarForVerify(file, args);
|
|
816
|
+
if (fields) {
|
|
817
|
+
const strict = mode === 'strict';
|
|
818
|
+
let hasVerificationTarget = false;
|
|
819
|
+
const canonicalHashValue = readEnvelopeFieldAny(fields, [
|
|
820
|
+
'canonical_hash',
|
|
821
|
+
'canonical:hash',
|
|
822
|
+
'integrity.hash',
|
|
823
|
+
'integrity.hash:string',
|
|
824
|
+
'integrity:integrityBlock.hash',
|
|
825
|
+
'integrity:integrityBlock.hash:string',
|
|
826
|
+
], diagnostics);
|
|
827
|
+
const canonicalAlgValue = readEnvelopeFieldAny(fields, [
|
|
828
|
+
'canonical_hash_alg',
|
|
829
|
+
'canonical:hash_alg',
|
|
830
|
+
'integrity.alg',
|
|
831
|
+
'integrity.alg:string',
|
|
832
|
+
'integrity:integrityBlock.alg',
|
|
833
|
+
'integrity:integrityBlock.alg:string',
|
|
834
|
+
], diagnostics);
|
|
835
|
+
if (canonicalHashValue) {
|
|
836
|
+
hasVerificationTarget = true;
|
|
837
|
+
verificationMeta.canonical.present = true;
|
|
838
|
+
const alg = canonicalAlgValue ?? 'sha-256';
|
|
839
|
+
if (!canonicalAlgValue) {
|
|
840
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_HASH_ALG_DEFAULTED', 'canonical_hash_alg missing; defaulting to sha-256');
|
|
841
|
+
}
|
|
842
|
+
const normalizedAlg = normalizeHashAlgorithm(alg, diagnostics, strict, 'canonical_hash_alg');
|
|
843
|
+
if (normalizedAlg) {
|
|
844
|
+
const computed = computeCanonicalHash(baseCompileResult.events, { algorithm: normalizedAlg });
|
|
845
|
+
verificationMeta.canonical.algorithm = normalizedAlg;
|
|
846
|
+
verificationMeta.canonical.expected = canonicalHashValue;
|
|
847
|
+
verificationMeta.canonical.computed = computed.hash;
|
|
848
|
+
verificationMeta.canonicalStream = { length: computed.stream.length };
|
|
849
|
+
verificationMeta.replay = {
|
|
850
|
+
performed: true,
|
|
851
|
+
status: normalizeHash(canonicalHashValue) === normalizeHash(computed.hash) ? 'match' : 'divergent',
|
|
852
|
+
expected: canonicalHashValue,
|
|
853
|
+
computed: computed.hash,
|
|
854
|
+
};
|
|
855
|
+
receipt ??= createCanonicalReceipt(baseInput, baseCompileResult.events, {
|
|
856
|
+
embedCanonicalPayload: false,
|
|
857
|
+
canonicalHashAlgorithm: normalizedAlg,
|
|
858
|
+
receiptDigestOverride: canonicalHashValue,
|
|
859
|
+
});
|
|
860
|
+
if (normalizeHash(canonicalHashValue) !== normalizeHash(computed.hash)) {
|
|
861
|
+
diagnostics.errors.push({
|
|
862
|
+
level: 'error',
|
|
863
|
+
code: 'ENVELOPE_HASH_MISMATCH',
|
|
864
|
+
message: 'canonical_hash does not match computed AES hash',
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
else if (canonicalAlgValue) {
|
|
870
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_HASH_MISSING', 'canonical_hash_alg present but canonical_hash is missing');
|
|
871
|
+
}
|
|
872
|
+
const bytesHashValue = readEnvelopeFieldAny(fields, [
|
|
873
|
+
'bytes_hash',
|
|
874
|
+
'bytes:hash',
|
|
875
|
+
'integrity.bytes_hash',
|
|
876
|
+
'integrity.bytes_hash:string',
|
|
877
|
+
'integrity:integrityBlock.bytes_hash',
|
|
878
|
+
'integrity:integrityBlock.bytes_hash:string',
|
|
879
|
+
], diagnostics);
|
|
880
|
+
const bytesAlgValue = readEnvelopeFieldAny(fields, [
|
|
881
|
+
'bytes_hash_alg',
|
|
882
|
+
'bytes:hash_alg',
|
|
883
|
+
'integrity.bytes_hash_alg',
|
|
884
|
+
'integrity.bytes_hash_alg:string',
|
|
885
|
+
'integrity:integrityBlock.bytes_hash_alg',
|
|
886
|
+
'integrity:integrityBlock.bytes_hash_alg:string',
|
|
887
|
+
], diagnostics);
|
|
888
|
+
if (bytesHashValue) {
|
|
889
|
+
hasVerificationTarget = true;
|
|
890
|
+
verificationMeta.bytes.present = true;
|
|
891
|
+
const alg = bytesAlgValue ?? 'sha-256';
|
|
892
|
+
if (!bytesAlgValue) {
|
|
893
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_BYTES_ALG_DEFAULTED', 'bytes_hash_alg missing; defaulting to sha-256');
|
|
894
|
+
}
|
|
895
|
+
const normalizedAlg = normalizeHashAlgorithm(alg, diagnostics, strict, 'bytes_hash_alg');
|
|
896
|
+
if (normalizedAlg) {
|
|
897
|
+
const computed = computeByteHash(baseInput, { algorithm: normalizedAlg });
|
|
898
|
+
verificationMeta.bytes.algorithm = normalizedAlg;
|
|
899
|
+
verificationMeta.bytes.expected = bytesHashValue;
|
|
900
|
+
verificationMeta.bytes.computed = computed.hash;
|
|
901
|
+
if (normalizeHash(bytesHashValue) !== normalizeHash(computed.hash)) {
|
|
902
|
+
diagnostics.errors.push({
|
|
903
|
+
level: 'error',
|
|
904
|
+
code: 'ENVELOPE_BYTES_MISMATCH',
|
|
905
|
+
message: 'bytes_hash does not match computed document hash',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else if (bytesAlgValue) {
|
|
911
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_BYTES_HASH_MISSING', 'bytes_hash_alg present but bytes_hash is missing');
|
|
912
|
+
}
|
|
913
|
+
const checksumValue = readEnvelopeFieldAny(fields, [
|
|
914
|
+
'checksum_value',
|
|
915
|
+
'checksum:value',
|
|
916
|
+
'integrity.checksum_value',
|
|
917
|
+
'integrity.checksum_value:string',
|
|
918
|
+
'integrity:integrityBlock.checksum_value',
|
|
919
|
+
'integrity:integrityBlock.checksum_value:string',
|
|
920
|
+
], diagnostics);
|
|
921
|
+
const checksumAlg = readEnvelopeFieldAny(fields, [
|
|
922
|
+
'checksum_alg',
|
|
923
|
+
'checksum:alg',
|
|
924
|
+
'integrity.checksum_alg',
|
|
925
|
+
'integrity.checksum_alg:string',
|
|
926
|
+
'integrity:integrityBlock.checksum_alg',
|
|
927
|
+
'integrity:integrityBlock.checksum_alg:string',
|
|
928
|
+
], diagnostics);
|
|
929
|
+
if (checksumValue) {
|
|
930
|
+
hasVerificationTarget = true;
|
|
931
|
+
verificationMeta.checksum.present = true;
|
|
932
|
+
const alg = checksumAlg ?? 'sha-256';
|
|
933
|
+
if (!checksumAlg) {
|
|
934
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_CHECKSUM_ALG_DEFAULTED', 'checksum_alg missing; defaulting to sha-256');
|
|
935
|
+
}
|
|
936
|
+
const normalizedAlg = normalizeHashAlgorithm(alg, diagnostics, strict, 'checksum_alg');
|
|
937
|
+
if (normalizedAlg) {
|
|
938
|
+
const computed = computeByteHash(baseInput, { algorithm: normalizedAlg });
|
|
939
|
+
verificationMeta.checksum.algorithm = normalizedAlg;
|
|
940
|
+
verificationMeta.checksum.expected = checksumValue;
|
|
941
|
+
verificationMeta.checksum.computed = computed.hash;
|
|
942
|
+
if (normalizeHash(checksumValue) !== normalizeHash(computed.hash)) {
|
|
943
|
+
diagnostics.errors.push({
|
|
944
|
+
level: 'error',
|
|
945
|
+
code: 'ENVELOPE_CHECKSUM_MISMATCH',
|
|
946
|
+
message: 'checksum_value does not match computed document hash',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
else if (checksumAlg) {
|
|
952
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_CHECKSUM_MISSING', 'checksum_alg present but checksum_value is missing');
|
|
953
|
+
}
|
|
954
|
+
const signature = readEnvelopeFieldAny(fields, [
|
|
955
|
+
'sig',
|
|
956
|
+
'signatures[0].sig',
|
|
957
|
+
'signatures[0].sig:string',
|
|
958
|
+
'signatures:signatureSet[0].sig',
|
|
959
|
+
'signatures:signatureSet[0].sig:string',
|
|
960
|
+
], diagnostics);
|
|
961
|
+
if (signature) {
|
|
962
|
+
hasVerificationTarget = true;
|
|
963
|
+
verificationMeta.signature.present = true;
|
|
964
|
+
const publicKeyPath = getFlagValue(args, '--public-key') ?? getFlagValue(args, '--pubkey');
|
|
965
|
+
if (!publicKeyPath) {
|
|
966
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_SIGNATURE_KEY_MISSING', 'sig present but no --public-key provided; signature not verified');
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
const publicKey = readFile(publicKeyPath);
|
|
970
|
+
const payload = verificationMeta.canonical.expected ?? computeCanonicalHash(baseCompileResult.events, { algorithm: 'sha-256' }).hash;
|
|
971
|
+
const ok = verifyStringPayloadSignature(payload, signature, publicKey, { algorithm: 'ed25519' });
|
|
972
|
+
verificationMeta.signature.verified = ok;
|
|
973
|
+
if (!ok) {
|
|
974
|
+
diagnostics.errors.push({
|
|
975
|
+
level: 'error',
|
|
976
|
+
code: 'ENVELOPE_SIGNATURE_INVALID',
|
|
977
|
+
message: 'signature verification failed',
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!hasVerificationTarget) {
|
|
983
|
+
pushModeDiagnostic(diagnostics, strict, 'ENVELOPE_NO_HASH', 'envelope contains no verifiable hash fields');
|
|
984
|
+
}
|
|
985
|
+
if (!verificationMeta.canonicalStream) {
|
|
986
|
+
const stream = computeCanonicalHash(baseCompileResult.events, { algorithm: 'sha-256' }).stream;
|
|
987
|
+
verificationMeta.canonicalStream = { length: stream.length };
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (diagnostics.errors.length > 0) {
|
|
991
|
+
if (jsonOutput) {
|
|
992
|
+
outputEnvelopeJson(diagnostics.errors, diagnostics.warnings, false, verificationMeta, receipt);
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
outputEnvelopeDiagnostics(diagnostics.errors, 'ERROR');
|
|
997
|
+
if (diagnostics.warnings.length > 0) {
|
|
998
|
+
outputEnvelopeDiagnostics(diagnostics.warnings, 'WARN');
|
|
999
|
+
}
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (diagnostics.warnings.length > 0) {
|
|
1004
|
+
if (jsonOutput) {
|
|
1005
|
+
outputEnvelopeJson(diagnostics.errors, diagnostics.warnings, true, verificationMeta, receipt);
|
|
1006
|
+
process.exit(0);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
outputEnvelopeDiagnostics(diagnostics.warnings, 'WARN');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (jsonOutput) {
|
|
1013
|
+
outputEnvelopeJson(diagnostics.errors, diagnostics.warnings, true, verificationMeta, receipt);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
console.log('OK');
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function integritySign(args) {
|
|
1020
|
+
const file = findFileWithValueFlags(args, ['--private-key', '--privkey', '--receipt']);
|
|
1021
|
+
const jsonOutput = args.includes('--json');
|
|
1022
|
+
const writeOutput = args.includes('--write');
|
|
1023
|
+
const replaceOutput = args.includes('--replace');
|
|
1024
|
+
const includeBytes = args.includes('--include-bytes');
|
|
1025
|
+
const includeChecksum = args.includes('--include-checksum');
|
|
1026
|
+
const maxInputBytes = resolveMaxInputBytes(args);
|
|
1027
|
+
if (!file) {
|
|
1028
|
+
console.error('Error: No file specified');
|
|
1029
|
+
console.error('Usage: aeon integrity sign <file> --private-key <path> [--receipt <path>]');
|
|
1030
|
+
process.exit(2);
|
|
1031
|
+
}
|
|
1032
|
+
if (maxInputBytes === null) {
|
|
1033
|
+
console.error('Error: Invalid value for --max-input-bytes (expected a non-negative integer)');
|
|
1034
|
+
process.exit(2);
|
|
1035
|
+
}
|
|
1036
|
+
const privateKeyPath = getFlagValue(args, '--private-key') ?? getFlagValue(args, '--privkey');
|
|
1037
|
+
if (!privateKeyPath) {
|
|
1038
|
+
console.error('Error: Missing --private-key');
|
|
1039
|
+
console.error('Usage: aeon integrity sign <file> --private-key <path> [--receipt <path>]');
|
|
1040
|
+
process.exit(2);
|
|
1041
|
+
}
|
|
1042
|
+
const input = readFile(file);
|
|
1043
|
+
enforceInputByteLimitOrExit(input, maxInputBytes);
|
|
1044
|
+
const baseInput = removeEnvelope(input);
|
|
1045
|
+
const envelopePresence = extractEnvelopeFields(input);
|
|
1046
|
+
if (envelopePresence.errors.length > 0) {
|
|
1047
|
+
if (jsonOutput) {
|
|
1048
|
+
outputEnvelopeJson(envelopePresence.errors, [], false);
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
outputEnvelopeDiagnostics(envelopePresence.errors, 'ERROR');
|
|
1052
|
+
}
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
if (envelopePresence.fields && !replaceOutput) {
|
|
1056
|
+
const errors = [{
|
|
1057
|
+
level: 'error',
|
|
1058
|
+
code: 'ENVELOPE_EXISTS',
|
|
1059
|
+
message: 'document already contains an :envelope binding',
|
|
1060
|
+
}];
|
|
1061
|
+
if (jsonOutput) {
|
|
1062
|
+
outputEnvelopeJson(errors, [], false);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
outputEnvelopeDiagnostics(errors, 'ERROR');
|
|
1066
|
+
}
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
const compileResult = compile(baseInput, {
|
|
1070
|
+
datatypePolicy: 'allow_custom',
|
|
1071
|
+
...(maxInputBytes !== undefined ? { maxInputBytes } : {}),
|
|
1072
|
+
});
|
|
1073
|
+
if (compileResult.errors.length > 0) {
|
|
1074
|
+
if (jsonOutput) {
|
|
1075
|
+
const errors = compileResult.errors.map((error) => ({
|
|
1076
|
+
level: 'error',
|
|
1077
|
+
code: error.code ?? 'AEON_ERROR',
|
|
1078
|
+
message: error.message,
|
|
1079
|
+
}));
|
|
1080
|
+
outputEnvelopeJson(errors, [], false);
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
for (const error of compileResult.errors) {
|
|
1084
|
+
console.log(formatErrorLine(error));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
const receipt = createCanonicalReceipt(baseInput, compileResult.events, {
|
|
1090
|
+
embedCanonicalPayload: true,
|
|
1091
|
+
});
|
|
1092
|
+
const receiptPath = resolveReceiptSidecarPathForSign(file, args, writeOutput);
|
|
1093
|
+
const canonical = computeCanonicalHash(compileResult.events, { algorithm: 'sha-256' });
|
|
1094
|
+
const bytes = includeBytes ? computeByteHash(baseInput, { algorithm: 'sha-256' }) : null;
|
|
1095
|
+
const checksum = includeChecksum ? computeByteHash(baseInput, { algorithm: 'sha-256' }) : null;
|
|
1096
|
+
const privateKey = readFile(privateKeyPath);
|
|
1097
|
+
const signature = signStringPayload(canonical.hash, privateKey, { algorithm: 'ed25519' });
|
|
1098
|
+
const lines = [
|
|
1099
|
+
`${ENVELOPE_CONVENTION_KEY}:envelope = {`,
|
|
1100
|
+
' integrity:integrityBlock = {',
|
|
1101
|
+
' alg:string = "sha-256"',
|
|
1102
|
+
` hash:string = "${canonical.hash}"`,
|
|
1103
|
+
];
|
|
1104
|
+
if (bytes) {
|
|
1105
|
+
lines.push(' bytes_hash_alg:string = "sha-256"');
|
|
1106
|
+
lines.push(` bytes_hash:string = "${bytes.hash}"`);
|
|
1107
|
+
}
|
|
1108
|
+
if (checksum) {
|
|
1109
|
+
lines.push(' checksum_alg:string = "sha-256"');
|
|
1110
|
+
lines.push(` checksum_value:string = "${checksum.hash}"`);
|
|
1111
|
+
}
|
|
1112
|
+
lines.push(' }');
|
|
1113
|
+
lines.push(' signatures:signatureSet = [');
|
|
1114
|
+
lines.push(' {');
|
|
1115
|
+
lines.push(' alg:string = "ed25519"');
|
|
1116
|
+
lines.push(' kid:string = "default"');
|
|
1117
|
+
lines.push(` sig:string = "${signature.signature}"`);
|
|
1118
|
+
lines.push(' }');
|
|
1119
|
+
lines.push(' ]');
|
|
1120
|
+
lines.push('}');
|
|
1121
|
+
const snippet = lines.join('\n');
|
|
1122
|
+
if (writeOutput) {
|
|
1123
|
+
const prepared = ensureGpSecurityConventions(baseInput);
|
|
1124
|
+
const nextContent = appendEnvelope(prepared.source, snippet);
|
|
1125
|
+
writeFileWithBackup(file, nextContent);
|
|
1126
|
+
if (receiptPath) {
|
|
1127
|
+
writeReceiptSidecar(receiptPath, receipt);
|
|
1128
|
+
}
|
|
1129
|
+
if (jsonOutput) {
|
|
1130
|
+
const envelope = {
|
|
1131
|
+
integrity: {
|
|
1132
|
+
alg: 'sha-256',
|
|
1133
|
+
hash: canonical.hash,
|
|
1134
|
+
...(bytes ? { bytes_hash_alg: 'sha-256', bytes_hash: bytes.hash } : {}),
|
|
1135
|
+
...(checksum ? { checksum_alg: 'sha-256', checksum_value: checksum.hash } : {}),
|
|
1136
|
+
},
|
|
1137
|
+
signatures: [
|
|
1138
|
+
{
|
|
1139
|
+
alg: 'ed25519',
|
|
1140
|
+
kid: 'default',
|
|
1141
|
+
sig: signature.signature,
|
|
1142
|
+
},
|
|
1143
|
+
],
|
|
1144
|
+
};
|
|
1145
|
+
console.log(JSON.stringify({
|
|
1146
|
+
ok: true,
|
|
1147
|
+
written: true,
|
|
1148
|
+
replaced: replaceOutput,
|
|
1149
|
+
conventionsApplied: prepared.changed,
|
|
1150
|
+
receipt,
|
|
1151
|
+
envelope,
|
|
1152
|
+
}, null, 2));
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
console.log(`Wrote envelope to ${file}`);
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (jsonOutput) {
|
|
1160
|
+
if (receiptPath) {
|
|
1161
|
+
writeReceiptSidecar(receiptPath, receipt);
|
|
1162
|
+
}
|
|
1163
|
+
const envelope = {
|
|
1164
|
+
integrity: {
|
|
1165
|
+
alg: 'sha-256',
|
|
1166
|
+
hash: canonical.hash,
|
|
1167
|
+
...(bytes ? { bytes_hash_alg: 'sha-256', bytes_hash: bytes.hash } : {}),
|
|
1168
|
+
...(checksum ? { checksum_alg: 'sha-256', checksum_value: checksum.hash } : {}),
|
|
1169
|
+
},
|
|
1170
|
+
signatures: [
|
|
1171
|
+
{
|
|
1172
|
+
alg: 'ed25519',
|
|
1173
|
+
kid: 'default',
|
|
1174
|
+
sig: signature.signature,
|
|
1175
|
+
},
|
|
1176
|
+
],
|
|
1177
|
+
};
|
|
1178
|
+
console.log(JSON.stringify({ ok: true, receipt, envelope }, null, 2));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (receiptPath) {
|
|
1182
|
+
writeReceiptSidecar(receiptPath, receipt);
|
|
1183
|
+
}
|
|
1184
|
+
console.log(snippet);
|
|
1185
|
+
}
|
|
1186
|
+
// =============================================================================
|
|
1187
|
+
// OUTPUT FORMATTERS
|
|
1188
|
+
// =============================================================================
|
|
1189
|
+
/**
|
|
1190
|
+
* Markdown output (default for inspect)
|
|
1191
|
+
*/
|
|
1192
|
+
/**
|
|
1193
|
+
* JSON output (--json flag)
|
|
1194
|
+
*/
|
|
1195
|
+
function outputJSON(result, options, headerInfo) {
|
|
1196
|
+
const visibleEvents = result.events.filter(e => !e.key.startsWith('aeon:'));
|
|
1197
|
+
const annotations = options.sortAnnotations
|
|
1198
|
+
? sortAnnotationRecords(result.annotations ?? [])
|
|
1199
|
+
: (result.annotations ?? []);
|
|
1200
|
+
if (options.annotationsOnly) {
|
|
1201
|
+
console.log(JSON.stringify({ annotations }, null, 2));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const output = {
|
|
1205
|
+
events: visibleEvents.map(event => ({
|
|
1206
|
+
path: formatPath(event.path),
|
|
1207
|
+
key: event.key,
|
|
1208
|
+
datatype: event.datatype ?? null,
|
|
1209
|
+
span: event.span,
|
|
1210
|
+
// Preserve AST-like shape (no coercion/inference)
|
|
1211
|
+
value: jsonSafe(event.value),
|
|
1212
|
+
})),
|
|
1213
|
+
errors: result.errors.map(error => ({
|
|
1214
|
+
code: error.code,
|
|
1215
|
+
path: getErrorPath(error) ?? '$',
|
|
1216
|
+
span: error.span,
|
|
1217
|
+
...(getPhaseLabel(error)
|
|
1218
|
+
? { phaseLabel: getPhaseLabel(error) }
|
|
1219
|
+
: {}),
|
|
1220
|
+
message: error.message,
|
|
1221
|
+
})),
|
|
1222
|
+
};
|
|
1223
|
+
if (options.includeAnnotations) {
|
|
1224
|
+
output.annotations = annotations;
|
|
1225
|
+
}
|
|
1226
|
+
const contractMeta = headerInfo ? buildDeclaredInspectContractMeta(headerInfo) : null;
|
|
1227
|
+
if (contractMeta) {
|
|
1228
|
+
Object.assign(output, { contracts: contractMeta });
|
|
1229
|
+
}
|
|
1230
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1231
|
+
}
|
|
1232
|
+
function jsonSafe(value) {
|
|
1233
|
+
if (value instanceof Map) {
|
|
1234
|
+
return Object.fromEntries(Array.from(value.entries(), ([key, entry]) => [String(key), jsonSafe(entry)]));
|
|
1235
|
+
}
|
|
1236
|
+
if (Array.isArray(value)) {
|
|
1237
|
+
return value.map(jsonSafe);
|
|
1238
|
+
}
|
|
1239
|
+
if (value && typeof value === 'object') {
|
|
1240
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsonSafe(entry)]));
|
|
1241
|
+
}
|
|
1242
|
+
return value;
|
|
1243
|
+
}
|
|
1244
|
+
function resolveFinalizeMode(args) {
|
|
1245
|
+
const hasStrict = args.includes('--strict');
|
|
1246
|
+
const hasLoose = args.includes('--loose');
|
|
1247
|
+
if (hasStrict && hasLoose)
|
|
1248
|
+
return null;
|
|
1249
|
+
if (hasLoose)
|
|
1250
|
+
return 'loose';
|
|
1251
|
+
return 'strict';
|
|
1252
|
+
}
|
|
1253
|
+
function resolveFinalizeScope(args) {
|
|
1254
|
+
const value = getFlagValue(args, '--scope');
|
|
1255
|
+
if (!value)
|
|
1256
|
+
return 'payload';
|
|
1257
|
+
if (value === 'payload' || value === 'header' || value === 'full') {
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
function resolveDatatypePolicy(args) {
|
|
1263
|
+
const hasRichPreset = args.includes('--rich');
|
|
1264
|
+
const value = getFlagValue(args, '--datatype-policy');
|
|
1265
|
+
if (value === undefined) {
|
|
1266
|
+
if (args.includes('--datatype-policy'))
|
|
1267
|
+
return null;
|
|
1268
|
+
return hasRichPreset ? 'allow_custom' : null;
|
|
1269
|
+
}
|
|
1270
|
+
if (hasRichPreset && value === 'reserved_only')
|
|
1271
|
+
return null;
|
|
1272
|
+
if (value === 'reserved_only' || value === 'allow_custom') {
|
|
1273
|
+
return value;
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
function resolveIntegrityMode(args) {
|
|
1278
|
+
const hasStrict = args.includes('--strict');
|
|
1279
|
+
const hasLoose = args.includes('--loose');
|
|
1280
|
+
if (hasStrict && hasLoose)
|
|
1281
|
+
return null;
|
|
1282
|
+
if (hasLoose)
|
|
1283
|
+
return 'loose';
|
|
1284
|
+
return 'strict';
|
|
1285
|
+
}
|
|
1286
|
+
function mergeDiagnostics(finalized, errors) {
|
|
1287
|
+
const mergedErrors = [];
|
|
1288
|
+
const mergedWarnings = [];
|
|
1289
|
+
if (finalized.meta?.errors)
|
|
1290
|
+
mergedErrors.push(...finalized.meta.errors.map(toOutputDiagnostic));
|
|
1291
|
+
if (finalized.meta?.warnings)
|
|
1292
|
+
mergedWarnings.push(...finalized.meta.warnings.map(toOutputDiagnostic));
|
|
1293
|
+
for (const error of errors) {
|
|
1294
|
+
mergedErrors.push(toDiagnosticFromError(error));
|
|
1295
|
+
}
|
|
1296
|
+
const meta = {};
|
|
1297
|
+
if (mergedErrors.length > 0)
|
|
1298
|
+
meta.errors = mergedErrors;
|
|
1299
|
+
if (mergedWarnings.length > 0)
|
|
1300
|
+
meta.warnings = mergedWarnings;
|
|
1301
|
+
return meta;
|
|
1302
|
+
}
|
|
1303
|
+
function toOutputDiagnostic(diagnostic) {
|
|
1304
|
+
const phaseLabel = getPhaseLabel(diagnostic);
|
|
1305
|
+
return {
|
|
1306
|
+
...diagnostic,
|
|
1307
|
+
...(phaseLabel ? { phaseLabel } : {}),
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function finalizeJsonOutput(result, options) {
|
|
1311
|
+
const finalized = finalizeJson(result.events, {
|
|
1312
|
+
...options,
|
|
1313
|
+
...(result.header ? { header: result.header } : {}),
|
|
1314
|
+
});
|
|
1315
|
+
const meta = mergeDiagnostics(finalized, result.errors);
|
|
1316
|
+
return Object.keys(meta).length > 0
|
|
1317
|
+
? { document: finalized.document, meta }
|
|
1318
|
+
: { document: finalized.document };
|
|
1319
|
+
}
|
|
1320
|
+
function finalizeMapOutput(result, options) {
|
|
1321
|
+
const finalized = finalizeMap(result.events, {
|
|
1322
|
+
...options,
|
|
1323
|
+
...(result.header ? { header: result.header } : {}),
|
|
1324
|
+
});
|
|
1325
|
+
const meta = mergeDiagnostics(finalized, result.errors);
|
|
1326
|
+
const entries = Array.from(finalized.document.entries.values()).map(entryToJson);
|
|
1327
|
+
const document = { entries };
|
|
1328
|
+
return Object.keys(meta).length > 0
|
|
1329
|
+
? { document, meta }
|
|
1330
|
+
: { document };
|
|
1331
|
+
}
|
|
1332
|
+
function entryToJson(entry) {
|
|
1333
|
+
return {
|
|
1334
|
+
path: entry.path,
|
|
1335
|
+
value: entry.value,
|
|
1336
|
+
span: entry.span,
|
|
1337
|
+
...(entry.datatype ? { datatype: entry.datatype } : {}),
|
|
1338
|
+
...(entry.annotations ? { annotations: mapAnnotations(entry.annotations) } : {}),
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function mapAnnotations(annotations) {
|
|
1342
|
+
const entries = {};
|
|
1343
|
+
for (const [key, value] of annotations.entries()) {
|
|
1344
|
+
entries[key] = {
|
|
1345
|
+
value: value.value,
|
|
1346
|
+
...(value.datatype ? { datatype: value.datatype } : {}),
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
return entries;
|
|
1350
|
+
}
|
|
1351
|
+
function toDiagnosticFromError(error) {
|
|
1352
|
+
const code = error.code;
|
|
1353
|
+
const span = error.span;
|
|
1354
|
+
const path = getErrorPath(error);
|
|
1355
|
+
const phaseLabel = getPhaseLabel(error);
|
|
1356
|
+
return {
|
|
1357
|
+
level: 'error',
|
|
1358
|
+
message: error.message,
|
|
1359
|
+
...(code ? { code } : {}),
|
|
1360
|
+
...(path ? { path } : {}),
|
|
1361
|
+
...(span ? { span } : {}),
|
|
1362
|
+
...(phaseLabel ? { phaseLabel } : {}),
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function runDoctor(options) {
|
|
1366
|
+
const checks = [];
|
|
1367
|
+
const nodeMajor = Number.parseInt(process.version.replace(/^v/, '').split('.')[0] ?? '', 10);
|
|
1368
|
+
const workspacePackage = readJsonFileSafe(path.resolve(workspaceRoot, 'package.json'));
|
|
1369
|
+
const declaredPnpm = typeof workspacePackage?.packageManager === 'string'
|
|
1370
|
+
? workspacePackage.packageManager
|
|
1371
|
+
: null;
|
|
1372
|
+
checks.push({
|
|
1373
|
+
name: 'node-version',
|
|
1374
|
+
status: Number.isFinite(nodeMajor) && nodeMajor >= 20 ? 'pass' : 'fail',
|
|
1375
|
+
message: Number.isFinite(nodeMajor) && nodeMajor >= 20
|
|
1376
|
+
? `Node ${process.version} satisfies workspace requirement >=20`
|
|
1377
|
+
: `Node ${process.version} does not satisfy workspace requirement >=20`,
|
|
1378
|
+
details: {
|
|
1379
|
+
actual: process.version,
|
|
1380
|
+
required: '>=20.0.0',
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
checks.push({
|
|
1384
|
+
name: 'pnpm-version',
|
|
1385
|
+
status: declaredPnpm ? 'pass' : 'warn',
|
|
1386
|
+
message: declaredPnpm
|
|
1387
|
+
? `Workspace declares ${declaredPnpm}`
|
|
1388
|
+
: 'Workspace packageManager field is not set',
|
|
1389
|
+
...(declaredPnpm ? { details: { declared: declaredPnpm } } : {}),
|
|
1390
|
+
});
|
|
1391
|
+
const requiredPackages = [
|
|
1392
|
+
'@altopelago/aeon-core',
|
|
1393
|
+
'@altopelago/aeon-finalize',
|
|
1394
|
+
'@altopelago/aeon-integrity',
|
|
1395
|
+
'@altopelago/aeon-profiles',
|
|
1396
|
+
'@altopelago/aeon-tonic',
|
|
1397
|
+
'@altopelago/aeos-core',
|
|
1398
|
+
];
|
|
1399
|
+
const packageStatuses = requiredPackages.map((packageName) => resolveInstalledPackage(packageName));
|
|
1400
|
+
const missingPackages = packageStatuses.filter((entry) => !entry.ok);
|
|
1401
|
+
checks.push({
|
|
1402
|
+
name: 'package-availability',
|
|
1403
|
+
status: missingPackages.length === 0 ? 'pass' : 'fail',
|
|
1404
|
+
message: missingPackages.length === 0
|
|
1405
|
+
? 'Required CLI/runtime packages are installed'
|
|
1406
|
+
: `Missing required packages: ${missingPackages.map((entry) => entry.name).join(', ')}`,
|
|
1407
|
+
details: {
|
|
1408
|
+
packages: packageStatuses.map((entry) => ({
|
|
1409
|
+
name: entry.name,
|
|
1410
|
+
ok: entry.ok,
|
|
1411
|
+
...(entry.version ? { version: entry.version } : {}),
|
|
1412
|
+
})),
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
checks.push(inspectContractRegistry(options.contractRegistryPath));
|
|
1416
|
+
checks.push({
|
|
1417
|
+
name: 'policy-surface',
|
|
1418
|
+
status: 'pass',
|
|
1419
|
+
message: 'CLI/runtime policy surface is available',
|
|
1420
|
+
details: {
|
|
1421
|
+
datatypePolicy: ['reserved_only', 'allow_custom'],
|
|
1422
|
+
finalizeMode: ['strict', 'loose'],
|
|
1423
|
+
trailingSeparatorDelimiterPolicy: ['off', 'warn', 'error'],
|
|
1424
|
+
recovery: true,
|
|
1425
|
+
},
|
|
1426
|
+
});
|
|
1427
|
+
return {
|
|
1428
|
+
ok: checks.every((check) => check.status !== 'fail'),
|
|
1429
|
+
checks,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function outputDoctorHuman(result) {
|
|
1433
|
+
for (const check of result.checks) {
|
|
1434
|
+
const label = check.status === 'pass' ? 'PASS' : check.status === 'warn' ? 'WARN' : 'FAIL';
|
|
1435
|
+
console.log(`[${label}] ${check.name} ${check.message}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function inspectContractRegistry(registryPath) {
|
|
1439
|
+
if (!fs.existsSync(registryPath)) {
|
|
1440
|
+
return {
|
|
1441
|
+
name: 'contract-registry',
|
|
1442
|
+
status: 'fail',
|
|
1443
|
+
message: `Contract registry not found: ${registryPath}`,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
const registry = readContractRegistryFileSafe(registryPath);
|
|
1447
|
+
if (!registry) {
|
|
1448
|
+
return {
|
|
1449
|
+
name: 'contract-registry',
|
|
1450
|
+
status: 'fail',
|
|
1451
|
+
message: `Contract registry is unreadable: ${registryPath}`,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
const entryResults = registry.contracts.map((entry) => {
|
|
1455
|
+
const verified = verifyContractArtifact(entry, registryPath);
|
|
1456
|
+
return {
|
|
1457
|
+
id: entry.id,
|
|
1458
|
+
kind: entry.kind,
|
|
1459
|
+
status: verified.ok ? 'pass' : 'fail',
|
|
1460
|
+
...(verified.ok
|
|
1461
|
+
? { path: verified.resolvedPath }
|
|
1462
|
+
: { error: verified.error, code: verified.code }),
|
|
1463
|
+
};
|
|
1464
|
+
});
|
|
1465
|
+
const failures = entryResults.filter((entry) => entry.status === 'fail');
|
|
1466
|
+
return {
|
|
1467
|
+
name: 'contract-registry',
|
|
1468
|
+
status: failures.length === 0 ? 'pass' : 'fail',
|
|
1469
|
+
message: failures.length === 0
|
|
1470
|
+
? `Verified ${entryResults.length} contract artifact(s) from ${registryPath}`
|
|
1471
|
+
: `Registry verification failed for ${failures.length} contract artifact(s)`,
|
|
1472
|
+
details: {
|
|
1473
|
+
path: registryPath,
|
|
1474
|
+
entries: entryResults,
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function resolveInstalledPackage(packageName) {
|
|
1479
|
+
const packageJsonPath = path.resolve(cliPackageRoot, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
1480
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1481
|
+
return { name: packageName, ok: false };
|
|
1482
|
+
}
|
|
1483
|
+
const pkg = readJsonFileSafe(packageJsonPath);
|
|
1484
|
+
return {
|
|
1485
|
+
name: packageName,
|
|
1486
|
+
ok: true,
|
|
1487
|
+
...(typeof pkg?.version === 'string' ? { version: pkg.version } : {}),
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function readJsonFileSafe(file) {
|
|
1491
|
+
try {
|
|
1492
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1493
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
return parsed;
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function readContractRegistryFileSafe(file) {
|
|
1503
|
+
const parsed = readJsonFileSafe(file);
|
|
1504
|
+
if (!parsed || !Array.isArray(parsed.contracts)) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
const contracts = parsed.contracts;
|
|
1508
|
+
for (const entry of contracts) {
|
|
1509
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
const candidate = entry;
|
|
1513
|
+
if (typeof candidate.id !== 'string' ||
|
|
1514
|
+
(candidate.kind !== 'profile' && candidate.kind !== 'schema') ||
|
|
1515
|
+
typeof candidate.version !== 'string' ||
|
|
1516
|
+
typeof candidate.path !== 'string' ||
|
|
1517
|
+
typeof candidate.sha256 !== 'string' ||
|
|
1518
|
+
(candidate.status !== 'active' && candidate.status !== 'deprecated')) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return parsed;
|
|
1523
|
+
}
|
|
1524
|
+
// =============================================================================
|
|
1525
|
+
// HELPERS
|
|
1526
|
+
// =============================================================================
|
|
1527
|
+
function formatGenericErrorLine(error) {
|
|
1528
|
+
const code = error.code ?? 'UNKNOWN';
|
|
1529
|
+
const span = formatSpan(error.span);
|
|
1530
|
+
const message = String(error.message).replace(/[\r\n]+/g, ' ');
|
|
1531
|
+
const phaseLabel = getPhaseLabel(error);
|
|
1532
|
+
const prefix = phaseLabel ? `${phaseLabel}: ` : '';
|
|
1533
|
+
return `${prefix}${message} [${code}] path=$ span=${span}`;
|
|
1534
|
+
}
|
|
1535
|
+
function findFile(args) {
|
|
1536
|
+
return args.find(arg => !arg.startsWith('--'));
|
|
1537
|
+
}
|
|
1538
|
+
function findFileWithValueFlags(args, valueFlags) {
|
|
1539
|
+
const skip = new Set();
|
|
1540
|
+
for (let i = 0; i < args.length; i++) {
|
|
1541
|
+
if (valueFlags.includes(args[i] ?? '') && i + 1 < args.length) {
|
|
1542
|
+
skip.add(i + 1);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (let i = 0; i < args.length; i++) {
|
|
1546
|
+
const arg = args[i] ?? '';
|
|
1547
|
+
if (arg.startsWith('--'))
|
|
1548
|
+
continue;
|
|
1549
|
+
if (skip.has(i))
|
|
1550
|
+
continue;
|
|
1551
|
+
return arg;
|
|
1552
|
+
}
|
|
1553
|
+
return undefined;
|
|
1554
|
+
}
|
|
1555
|
+
function getFlagValue(args, flag) {
|
|
1556
|
+
const index = args.indexOf(flag);
|
|
1557
|
+
if (index === -1)
|
|
1558
|
+
return undefined;
|
|
1559
|
+
const value = args[index + 1];
|
|
1560
|
+
if (!value || value.startsWith('--'))
|
|
1561
|
+
return undefined;
|
|
1562
|
+
return value;
|
|
1563
|
+
}
|
|
1564
|
+
function getFlagValues(args, flag) {
|
|
1565
|
+
const values = [];
|
|
1566
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1567
|
+
if (args[i] !== flag)
|
|
1568
|
+
continue;
|
|
1569
|
+
const value = args[i + 1];
|
|
1570
|
+
if (!value || value.startsWith('--'))
|
|
1571
|
+
continue;
|
|
1572
|
+
values.push(value);
|
|
1573
|
+
}
|
|
1574
|
+
return values;
|
|
1575
|
+
}
|
|
1576
|
+
function resolveMaxInputBytes(args) {
|
|
1577
|
+
if (!args.includes('--max-input-bytes'))
|
|
1578
|
+
return undefined;
|
|
1579
|
+
const value = getFlagValue(args, '--max-input-bytes');
|
|
1580
|
+
if (value === undefined)
|
|
1581
|
+
return null;
|
|
1582
|
+
if (!/^\d+$/.test(value))
|
|
1583
|
+
return null;
|
|
1584
|
+
return Number.parseInt(value, 10);
|
|
1585
|
+
}
|
|
1586
|
+
function resolveDepthOption(args, flag) {
|
|
1587
|
+
if (!args.includes(flag))
|
|
1588
|
+
return undefined;
|
|
1589
|
+
const value = getFlagValue(args, flag);
|
|
1590
|
+
if (value === undefined)
|
|
1591
|
+
return null;
|
|
1592
|
+
if (!/^\d+$/.test(value))
|
|
1593
|
+
return null;
|
|
1594
|
+
return Number.parseInt(value, 10);
|
|
1595
|
+
}
|
|
1596
|
+
function enforceInputByteLimitOrExit(input, maxInputBytes) {
|
|
1597
|
+
if (maxInputBytes === undefined)
|
|
1598
|
+
return;
|
|
1599
|
+
const actualBytes = Buffer.byteLength(input, 'utf8');
|
|
1600
|
+
if (actualBytes <= maxInputBytes)
|
|
1601
|
+
return;
|
|
1602
|
+
console.error(`Error: Input size ${actualBytes} bytes exceeds configured limit of ${maxInputBytes} bytes`);
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
function readFile(file) {
|
|
1606
|
+
try {
|
|
1607
|
+
return fs.readFileSync(file, 'utf-8');
|
|
1608
|
+
}
|
|
1609
|
+
catch (err) {
|
|
1610
|
+
console.error(`Error: Cannot read file: ${file}`);
|
|
1611
|
+
process.exit(2);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function readStdin() {
|
|
1615
|
+
try {
|
|
1616
|
+
return fs.readFileSync(0, 'utf-8');
|
|
1617
|
+
}
|
|
1618
|
+
catch {
|
|
1619
|
+
console.error('Error: Cannot read stdin');
|
|
1620
|
+
process.exit(2);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function ensureTrailingNewline(text) {
|
|
1624
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
1625
|
+
}
|
|
1626
|
+
function readSchemaFile(file) {
|
|
1627
|
+
const raw = readFile(file);
|
|
1628
|
+
let parsed;
|
|
1629
|
+
try {
|
|
1630
|
+
parsed = JSON.parse(raw);
|
|
1631
|
+
}
|
|
1632
|
+
catch {
|
|
1633
|
+
console.error(`Error: Schema file is not valid JSON: ${file}`);
|
|
1634
|
+
process.exit(2);
|
|
1635
|
+
}
|
|
1636
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1637
|
+
console.error(`Error: Schema file must be a JSON object: ${file}`);
|
|
1638
|
+
process.exit(2);
|
|
1639
|
+
}
|
|
1640
|
+
return normalizeLegacySchemaContractDoc(parsed, file);
|
|
1641
|
+
}
|
|
1642
|
+
function normalizeLegacySchemaContractDoc(doc, file, expectedSchemaId) {
|
|
1643
|
+
const schemaId = doc['schema_id'];
|
|
1644
|
+
const schemaVersion = doc['schema_version'];
|
|
1645
|
+
const rulesRaw = doc['rules'];
|
|
1646
|
+
const world = doc['world'];
|
|
1647
|
+
const referencePolicy = doc['reference_policy'];
|
|
1648
|
+
const datatypeRules = doc['datatype_rules'];
|
|
1649
|
+
const datatypeAllowlist = doc['datatype_allowlist'];
|
|
1650
|
+
const allowedTopLevel = new Set([
|
|
1651
|
+
'schema_id',
|
|
1652
|
+
'schema_version',
|
|
1653
|
+
'rules',
|
|
1654
|
+
'world',
|
|
1655
|
+
'reference_policy',
|
|
1656
|
+
'datatype_rules',
|
|
1657
|
+
'datatype_allowlist',
|
|
1658
|
+
]);
|
|
1659
|
+
for (const key of Object.keys(doc)) {
|
|
1660
|
+
if (!allowedTopLevel.has(key)) {
|
|
1661
|
+
console.error(`Error: Unknown schema contract key '${key}' in ${file}`);
|
|
1662
|
+
process.exit(2);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (typeof schemaId !== 'string' || schemaId.length === 0) {
|
|
1666
|
+
console.error(`Error: Schema contract missing required string field 'schema_id': ${file}`);
|
|
1667
|
+
process.exit(2);
|
|
1668
|
+
}
|
|
1669
|
+
if (expectedSchemaId && schemaId !== expectedSchemaId) {
|
|
1670
|
+
console.error(`Error: Schema contract id mismatch. Expected '${expectedSchemaId}', found '${schemaId}' in ${file}`);
|
|
1671
|
+
process.exit(2);
|
|
1672
|
+
}
|
|
1673
|
+
if (typeof schemaVersion !== 'string' || schemaVersion.length === 0) {
|
|
1674
|
+
console.error(`Error: Schema contract missing required string field 'schema_version': ${file}`);
|
|
1675
|
+
process.exit(2);
|
|
1676
|
+
}
|
|
1677
|
+
if (!Array.isArray(rulesRaw)) {
|
|
1678
|
+
console.error(`Error: Schema contract missing required array field 'rules': ${file}`);
|
|
1679
|
+
process.exit(2);
|
|
1680
|
+
}
|
|
1681
|
+
if (world !== undefined && world !== 'open' && world !== 'closed') {
|
|
1682
|
+
console.error(`Error: Schema contract field 'world' must be "open" or "closed": ${file}`);
|
|
1683
|
+
process.exit(2);
|
|
1684
|
+
}
|
|
1685
|
+
if (referencePolicy !== undefined && referencePolicy !== 'allow' && referencePolicy !== 'forbid') {
|
|
1686
|
+
console.error(`Error: Schema contract field 'reference_policy' must be "allow" or "forbid": ${file}`);
|
|
1687
|
+
process.exit(2);
|
|
1688
|
+
}
|
|
1689
|
+
if (datatypeRules !== undefined) {
|
|
1690
|
+
if (!datatypeRules || typeof datatypeRules !== 'object' || Array.isArray(datatypeRules)) {
|
|
1691
|
+
console.error(`Error: Schema contract field 'datatype_rules' must be object: ${file}`);
|
|
1692
|
+
process.exit(2);
|
|
1693
|
+
}
|
|
1694
|
+
for (const [key, value] of Object.entries(datatypeRules)) {
|
|
1695
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1696
|
+
console.error(`Error: Schema contract datatype_rules['${key}'] must be object: ${file}`);
|
|
1697
|
+
process.exit(2);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (datatypeAllowlist !== undefined) {
|
|
1702
|
+
if (!Array.isArray(datatypeAllowlist) || datatypeAllowlist.some((v) => typeof v !== 'string')) {
|
|
1703
|
+
console.error(`Error: Schema contract field 'datatype_allowlist' must be array<string>: ${file}`);
|
|
1704
|
+
process.exit(2);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const rules = rulesRaw.map((rule, index) => {
|
|
1708
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
1709
|
+
console.error(`Error: Schema contract rule at index ${index} is not an object: ${file}`);
|
|
1710
|
+
process.exit(2);
|
|
1711
|
+
}
|
|
1712
|
+
const ruleObj = rule;
|
|
1713
|
+
if (typeof ruleObj.path !== 'string' || !ruleObj.path) {
|
|
1714
|
+
console.error(`Error: Schema contract rule at index ${index} missing string 'path': ${file}`);
|
|
1715
|
+
process.exit(2);
|
|
1716
|
+
}
|
|
1717
|
+
if (!ruleObj.constraints || typeof ruleObj.constraints !== 'object' || Array.isArray(ruleObj.constraints)) {
|
|
1718
|
+
console.error(`Error: Schema contract rule at index ${index} missing object 'constraints': ${file}`);
|
|
1719
|
+
process.exit(2);
|
|
1720
|
+
}
|
|
1721
|
+
return {
|
|
1722
|
+
path: ruleObj.path,
|
|
1723
|
+
constraints: projectConstraints(ruleObj.constraints, String(ruleObj.path), file),
|
|
1724
|
+
};
|
|
1725
|
+
});
|
|
1726
|
+
const schema = {
|
|
1727
|
+
rules,
|
|
1728
|
+
...(world !== undefined ? { world: world } : {}),
|
|
1729
|
+
...(referencePolicy !== undefined ? { reference_policy: referencePolicy } : {}),
|
|
1730
|
+
...(datatypeRules && typeof datatypeRules === 'object' && !Array.isArray(datatypeRules)
|
|
1731
|
+
? { datatype_rules: projectDatatypeRules(datatypeRules, file) }
|
|
1732
|
+
: {}),
|
|
1733
|
+
...(Array.isArray(datatypeAllowlist)
|
|
1734
|
+
? { datatype_allowlist: datatypeAllowlist }
|
|
1735
|
+
: {}),
|
|
1736
|
+
};
|
|
1737
|
+
return { schema, schemaId };
|
|
1738
|
+
}
|
|
1739
|
+
function readSchemaContractAeonFile(file, expectedSchemaId) {
|
|
1740
|
+
const source = readFile(file);
|
|
1741
|
+
const compiled = compile(source, { datatypePolicy: 'allow_custom' });
|
|
1742
|
+
if (compiled.errors.length > 0) {
|
|
1743
|
+
console.error(`Error: Schema contract AEON file failed to parse: ${file}`);
|
|
1744
|
+
for (const error of compiled.errors) {
|
|
1745
|
+
console.error(` - ${error.code ?? 'AEON_ERROR'}: ${error.message}`);
|
|
1746
|
+
}
|
|
1747
|
+
process.exit(2);
|
|
1748
|
+
}
|
|
1749
|
+
const finalized = finalizeJson(compiled.events, { mode: 'strict' });
|
|
1750
|
+
if ((finalized.meta?.errors?.length ?? 0) > 0) {
|
|
1751
|
+
console.error(`Error: Schema contract AEON file failed to finalize: ${file}`);
|
|
1752
|
+
for (const error of finalized.meta?.errors ?? []) {
|
|
1753
|
+
console.error(` - ${error.message}`);
|
|
1754
|
+
}
|
|
1755
|
+
process.exit(2);
|
|
1756
|
+
}
|
|
1757
|
+
const document = finalized.document;
|
|
1758
|
+
const rootEvent = findAeosSchemaRootEvent(compiled.events);
|
|
1759
|
+
if (!rootEvent) {
|
|
1760
|
+
return normalizeLegacySchemaContractDoc(document, file, expectedSchemaId);
|
|
1761
|
+
}
|
|
1762
|
+
const aeosRoot = document['aeos'];
|
|
1763
|
+
if (!aeosRoot || typeof aeosRoot !== 'object' || Array.isArray(aeosRoot)) {
|
|
1764
|
+
console.error(`Error: Schema document missing required '$.aeos' object: ${file}`);
|
|
1765
|
+
process.exit(2);
|
|
1766
|
+
}
|
|
1767
|
+
return normalizeAeosSchemaDoc(aeosRoot, file, expectedSchemaId);
|
|
1768
|
+
}
|
|
1769
|
+
function normalizeAeosSchemaDoc(doc, file, expectedSchemaId) {
|
|
1770
|
+
const schemaId = doc['id'];
|
|
1771
|
+
const schemaVersion = doc['version'];
|
|
1772
|
+
const rulesRaw = doc['rules'];
|
|
1773
|
+
const world = doc['world'];
|
|
1774
|
+
const referencePolicy = doc['reference_policy'];
|
|
1775
|
+
const datatypeRules = doc['datatype_rules'];
|
|
1776
|
+
const datatypeAllowlist = doc['datatype_allowlist'];
|
|
1777
|
+
const allowedTopLevel = new Set([
|
|
1778
|
+
'id',
|
|
1779
|
+
'version',
|
|
1780
|
+
'rules',
|
|
1781
|
+
'patterns',
|
|
1782
|
+
'charsets',
|
|
1783
|
+
'world',
|
|
1784
|
+
'reference_policy',
|
|
1785
|
+
'datatype_rules',
|
|
1786
|
+
'datatype_allowlist',
|
|
1787
|
+
]);
|
|
1788
|
+
for (const key of Object.keys(doc)) {
|
|
1789
|
+
if (!allowedTopLevel.has(key)) {
|
|
1790
|
+
console.error(`Error: Unknown schema document key '${key}' in ${file}`);
|
|
1791
|
+
process.exit(2);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (typeof schemaId !== 'string' || schemaId.length === 0) {
|
|
1795
|
+
console.error(`Error: Schema document missing required string field 'id': ${file}`);
|
|
1796
|
+
process.exit(2);
|
|
1797
|
+
}
|
|
1798
|
+
if (expectedSchemaId && schemaId !== expectedSchemaId) {
|
|
1799
|
+
console.error(`Error: Schema contract id mismatch. Expected '${expectedSchemaId}', found '${schemaId}' in ${file}`);
|
|
1800
|
+
process.exit(2);
|
|
1801
|
+
}
|
|
1802
|
+
if (typeof schemaVersion !== 'string' || schemaVersion.length === 0) {
|
|
1803
|
+
console.error(`Error: Schema document missing required string field 'version': ${file}`);
|
|
1804
|
+
process.exit(2);
|
|
1805
|
+
}
|
|
1806
|
+
if (!rulesRaw || (typeof rulesRaw !== 'object')) {
|
|
1807
|
+
console.error(`Error: Schema document missing required field 'rules': ${file}`);
|
|
1808
|
+
process.exit(2);
|
|
1809
|
+
}
|
|
1810
|
+
if (world !== undefined && world !== 'open' && world !== 'closed') {
|
|
1811
|
+
console.error(`Error: Schema document field 'world' must be "open" or "closed": ${file}`);
|
|
1812
|
+
process.exit(2);
|
|
1813
|
+
}
|
|
1814
|
+
if (referencePolicy !== undefined && referencePolicy !== 'allow' && referencePolicy !== 'forbid') {
|
|
1815
|
+
console.error(`Error: Schema document field 'reference_policy' must be "allow" or "forbid": ${file}`);
|
|
1816
|
+
process.exit(2);
|
|
1817
|
+
}
|
|
1818
|
+
if (datatypeRules !== undefined && (!datatypeRules || typeof datatypeRules !== 'object' || Array.isArray(datatypeRules))) {
|
|
1819
|
+
console.error(`Error: Schema document field 'datatype_rules' must be object: ${file}`);
|
|
1820
|
+
process.exit(2);
|
|
1821
|
+
}
|
|
1822
|
+
if (datatypeAllowlist !== undefined) {
|
|
1823
|
+
if (!Array.isArray(datatypeAllowlist) || datatypeAllowlist.some((v) => typeof v !== 'string')) {
|
|
1824
|
+
console.error(`Error: Schema document field 'datatype_allowlist' must be array<string>: ${file}`);
|
|
1825
|
+
process.exit(2);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const rules = normalizeAeosRules(rulesRaw, file);
|
|
1829
|
+
const schema = {
|
|
1830
|
+
rules,
|
|
1831
|
+
...(world !== undefined ? { world: world } : {}),
|
|
1832
|
+
...(referencePolicy !== undefined ? { reference_policy: referencePolicy } : {}),
|
|
1833
|
+
...(datatypeRules && typeof datatypeRules === 'object' && !Array.isArray(datatypeRules)
|
|
1834
|
+
? { datatype_rules: projectDatatypeRules(datatypeRules, file) }
|
|
1835
|
+
: {}),
|
|
1836
|
+
...(Array.isArray(datatypeAllowlist)
|
|
1837
|
+
? { datatype_allowlist: datatypeAllowlist }
|
|
1838
|
+
: {}),
|
|
1839
|
+
};
|
|
1840
|
+
return { schema, schemaId };
|
|
1841
|
+
}
|
|
1842
|
+
function normalizeAeosRules(rulesRaw, file) {
|
|
1843
|
+
return Object.entries(rulesRaw).map(([rulePath, constraints]) => {
|
|
1844
|
+
if (!constraints || typeof constraints !== 'object' || Array.isArray(constraints)) {
|
|
1845
|
+
console.error(`Error: Schema rule '${rulePath}' must be an object of constraints: ${file}`);
|
|
1846
|
+
process.exit(2);
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
path: rulePath,
|
|
1850
|
+
constraints: projectConstraints(constraints, rulePath, file),
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
function projectDatatypeRules(datatypeRules, file) {
|
|
1855
|
+
const projected = {};
|
|
1856
|
+
for (const [key, value] of Object.entries(datatypeRules)) {
|
|
1857
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1858
|
+
console.error(`Error: Schema contract datatype_rules['${key}'] must be object: ${file}`);
|
|
1859
|
+
process.exit(2);
|
|
1860
|
+
}
|
|
1861
|
+
projected[key] = projectConstraints(value, `datatype_rules.${key}`, file);
|
|
1862
|
+
}
|
|
1863
|
+
return projected;
|
|
1864
|
+
}
|
|
1865
|
+
function projectConstraints(constraints, owner, file) {
|
|
1866
|
+
const projected = { ...constraints };
|
|
1867
|
+
const pathSelector = projected.reference_target_path;
|
|
1868
|
+
delete projected.reference_target_path;
|
|
1869
|
+
if (pathSelector !== undefined) {
|
|
1870
|
+
if (projected.reference_target_pattern !== undefined) {
|
|
1871
|
+
console.error(`Error: Schema rule '${owner}' cannot declare both 'reference_target_path' and 'reference_target_pattern': ${file}`);
|
|
1872
|
+
process.exit(2);
|
|
1873
|
+
}
|
|
1874
|
+
if (typeof pathSelector !== 'string' || pathSelector.length === 0) {
|
|
1875
|
+
console.error(`Error: Schema rule '${owner}' field 'reference_target_path' must be a non-empty string: ${file}`);
|
|
1876
|
+
process.exit(2);
|
|
1877
|
+
}
|
|
1878
|
+
projected.reference_target_pattern = referenceTargetPathToPattern(pathSelector);
|
|
1879
|
+
}
|
|
1880
|
+
return projected;
|
|
1881
|
+
}
|
|
1882
|
+
function referenceTargetPathToPattern(selector) {
|
|
1883
|
+
if (selector.replaceAll('[*]', '').includes('*')) {
|
|
1884
|
+
console.error(`Error: Unsupported reference_target_path selector: ${selector}`);
|
|
1885
|
+
process.exit(2);
|
|
1886
|
+
}
|
|
1887
|
+
const placeholder = '__AEOS_WILDCARD_INDEX__';
|
|
1888
|
+
return `^${escapeRegex(selector.replaceAll('[*]', placeholder)).replace(escapeRegex(placeholder), String.raw `\[\d+\]`)}$`;
|
|
1889
|
+
}
|
|
1890
|
+
function escapeRegex(value) {
|
|
1891
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1892
|
+
}
|
|
1893
|
+
function findAeosSchemaRootEvent(events) {
|
|
1894
|
+
for (const event of events) {
|
|
1895
|
+
if (event.key === 'aeos' && event.datatype === 'schema' && formatPath(event.path) === '$.aeos') {
|
|
1896
|
+
return event;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
function isSchemaContractAeonPath(file) {
|
|
1902
|
+
const lower = file.toLowerCase();
|
|
1903
|
+
return lower.endsWith('.aeon') || lower.endsWith('.aeos');
|
|
1904
|
+
}
|
|
1905
|
+
function readContractRegistryFile(file) {
|
|
1906
|
+
const raw = readFile(file);
|
|
1907
|
+
let parsed;
|
|
1908
|
+
try {
|
|
1909
|
+
parsed = JSON.parse(raw);
|
|
1910
|
+
}
|
|
1911
|
+
catch {
|
|
1912
|
+
console.error(`Error: Contract registry file is not valid JSON: ${file}`);
|
|
1913
|
+
process.exit(2);
|
|
1914
|
+
}
|
|
1915
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.contracts)) {
|
|
1916
|
+
console.error(`Error: Contract registry JSON must contain a top-level 'contracts' array: ${file}`);
|
|
1917
|
+
process.exit(2);
|
|
1918
|
+
}
|
|
1919
|
+
const contracts = parsed.contracts;
|
|
1920
|
+
for (let i = 0; i < contracts.length; i++) {
|
|
1921
|
+
const entry = contracts[i];
|
|
1922
|
+
if (!entry || typeof entry !== 'object') {
|
|
1923
|
+
console.error(`Error: Invalid contract registry entry at index ${i}`);
|
|
1924
|
+
process.exit(2);
|
|
1925
|
+
}
|
|
1926
|
+
const candidate = entry;
|
|
1927
|
+
const kind = candidate.kind;
|
|
1928
|
+
const status = candidate.status;
|
|
1929
|
+
if (typeof candidate.id !== 'string' ||
|
|
1930
|
+
(kind !== 'profile' && kind !== 'schema') ||
|
|
1931
|
+
typeof candidate.version !== 'string' ||
|
|
1932
|
+
typeof candidate.path !== 'string' ||
|
|
1933
|
+
!candidate.path.toLowerCase().endsWith('.aeon') ||
|
|
1934
|
+
typeof candidate.sha256 !== 'string' ||
|
|
1935
|
+
!/^[a-f0-9]{64}$/i.test(candidate.sha256) ||
|
|
1936
|
+
(status !== 'active' && status !== 'deprecated')) {
|
|
1937
|
+
console.error(`Error: Invalid contract registry entry shape at index ${i}`);
|
|
1938
|
+
process.exit(2);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return parsed;
|
|
1942
|
+
}
|
|
1943
|
+
function resolveContractEntry(registry, id, kind) {
|
|
1944
|
+
const entry = registry.contracts.find((contract) => contract.id === id && contract.kind === kind);
|
|
1945
|
+
if (!entry)
|
|
1946
|
+
return null;
|
|
1947
|
+
if (entry.status !== 'active')
|
|
1948
|
+
return null;
|
|
1949
|
+
return entry;
|
|
1950
|
+
}
|
|
1951
|
+
function failContract(code, message) {
|
|
1952
|
+
console.error(`Error [${code}]: ${message}`);
|
|
1953
|
+
process.exit(2);
|
|
1954
|
+
}
|
|
1955
|
+
function verifyContractArtifact(entry, registryPath) {
|
|
1956
|
+
const baseDir = path.dirname(path.resolve(registryPath));
|
|
1957
|
+
const resolvedPath = path.resolve(baseDir, entry.path);
|
|
1958
|
+
let fileBuffer;
|
|
1959
|
+
try {
|
|
1960
|
+
fileBuffer = fs.readFileSync(resolvedPath);
|
|
1961
|
+
}
|
|
1962
|
+
catch {
|
|
1963
|
+
return {
|
|
1964
|
+
ok: false,
|
|
1965
|
+
code: 'CONTRACT_ARTIFACT_MISSING',
|
|
1966
|
+
error: `Missing contract artifact for '${entry.id}' at ${resolvedPath}`,
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
const actual = createHash('sha256').update(fileBuffer).digest('hex');
|
|
1970
|
+
if (actual !== entry.sha256.toLowerCase()) {
|
|
1971
|
+
return {
|
|
1972
|
+
ok: false,
|
|
1973
|
+
code: 'CONTRACT_ARTIFACT_HASH_MISMATCH',
|
|
1974
|
+
error: `Contract artifact hash mismatch for '${entry.id}' at ${resolvedPath}`,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
return { ok: true, resolvedPath };
|
|
1978
|
+
}
|
|
1979
|
+
function appendEnvelope(source, envelope) {
|
|
1980
|
+
const trimmed = source.trimEnd();
|
|
1981
|
+
const separator = trimmed.length === 0 || trimmed.endsWith('\n') ? '' : '\n';
|
|
1982
|
+
return `${trimmed}${separator}\n${envelope}\n`;
|
|
1983
|
+
}
|
|
1984
|
+
function removeEnvelope(source) {
|
|
1985
|
+
const trimmed = source.trimEnd();
|
|
1986
|
+
const lines = trimmed.split('\n');
|
|
1987
|
+
let startIndex = -1;
|
|
1988
|
+
let openBraces = 0;
|
|
1989
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1990
|
+
const line = lines[i] ?? '';
|
|
1991
|
+
if (startIndex === -1) {
|
|
1992
|
+
if (isEnvelopeStartLine(line)) {
|
|
1993
|
+
startIndex = i;
|
|
1994
|
+
openBraces += countChar(line, '{') - countChar(line, '}');
|
|
1995
|
+
if (openBraces <= 0) {
|
|
1996
|
+
const before = lines.slice(0, i);
|
|
1997
|
+
const after = lines.slice(i + 1);
|
|
1998
|
+
return [...before, ...after].join('\n').trimEnd();
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
openBraces += countChar(line, '{') - countChar(line, '}');
|
|
2004
|
+
if (openBraces <= 0) {
|
|
2005
|
+
const before = lines.slice(0, startIndex);
|
|
2006
|
+
const after = lines.slice(i + 1);
|
|
2007
|
+
return [...before, ...after].join('\n').trimEnd();
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return source.trimEnd();
|
|
2012
|
+
}
|
|
2013
|
+
function countChar(value, ch) {
|
|
2014
|
+
let count = 0;
|
|
2015
|
+
for (let i = 0; i < value.length; i++) {
|
|
2016
|
+
if (value[i] === ch)
|
|
2017
|
+
count += 1;
|
|
2018
|
+
}
|
|
2019
|
+
return count;
|
|
2020
|
+
}
|
|
2021
|
+
function writeFileWithBackup(file, contents) {
|
|
2022
|
+
const backupPath = nextBackupPath(file);
|
|
2023
|
+
fs.copyFileSync(file, backupPath);
|
|
2024
|
+
fs.writeFileSync(file, contents, 'utf-8');
|
|
2025
|
+
}
|
|
2026
|
+
function nextBackupPath(file) {
|
|
2027
|
+
let candidate = `${file}.bak`;
|
|
2028
|
+
if (!fs.existsSync(candidate))
|
|
2029
|
+
return candidate;
|
|
2030
|
+
let index = 1;
|
|
2031
|
+
while (fs.existsSync(`${file}.bak${index}`)) {
|
|
2032
|
+
index += 1;
|
|
2033
|
+
}
|
|
2034
|
+
return `${file}.bak${index}`;
|
|
2035
|
+
}
|
|
2036
|
+
function ensureGpSecurityConventions(source) {
|
|
2037
|
+
const structured = findStructuredHeaderRange(source);
|
|
2038
|
+
if (!structured) {
|
|
2039
|
+
const header = renderSecurityHeader();
|
|
2040
|
+
return {
|
|
2041
|
+
source: `${header}\n\n${source.trimStart()}`.trimEnd(),
|
|
2042
|
+
changed: true,
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
const headerBlock = source.slice(structured.start, structured.end);
|
|
2046
|
+
const existing = new Set(extractHeaderConventions(headerBlock));
|
|
2047
|
+
const missing = GP_SECURITY_CONVENTIONS.filter((entry) => !existing.has(entry));
|
|
2048
|
+
if (missing.length === 0) {
|
|
2049
|
+
return { source, changed: false };
|
|
2050
|
+
}
|
|
2051
|
+
const updated = mergeSecurityConventionsIntoHeader(headerBlock, missing);
|
|
2052
|
+
return {
|
|
2053
|
+
source: `${source.slice(0, structured.start)}${updated}${source.slice(structured.end)}`,
|
|
2054
|
+
changed: true,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function renderSecurityHeader() {
|
|
2058
|
+
return [
|
|
2059
|
+
'aeon:header = {',
|
|
2060
|
+
' conventions:conventionSet = [',
|
|
2061
|
+
...GP_SECURITY_CONVENTIONS.map((entry) => ` "${entry}"`),
|
|
2062
|
+
' ]',
|
|
2063
|
+
'}',
|
|
2064
|
+
].join('\n');
|
|
2065
|
+
}
|
|
2066
|
+
function findStructuredHeaderRange(source) {
|
|
2067
|
+
const marker = /aeon:header\s*=\s*\{/g;
|
|
2068
|
+
const match = marker.exec(source);
|
|
2069
|
+
if (!match)
|
|
2070
|
+
return null;
|
|
2071
|
+
const start = match.index;
|
|
2072
|
+
const openIndex = source.indexOf('{', match.index);
|
|
2073
|
+
if (openIndex === -1)
|
|
2074
|
+
return null;
|
|
2075
|
+
let depth = 0;
|
|
2076
|
+
let inString = null;
|
|
2077
|
+
let escaping = false;
|
|
2078
|
+
for (let i = openIndex; i < source.length; i++) {
|
|
2079
|
+
const ch = source[i];
|
|
2080
|
+
if (inString) {
|
|
2081
|
+
if (escaping) {
|
|
2082
|
+
escaping = false;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (ch === '\\') {
|
|
2086
|
+
escaping = true;
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
if (ch === inString) {
|
|
2090
|
+
inString = null;
|
|
2091
|
+
}
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
2095
|
+
inString = ch;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (ch === '{')
|
|
2099
|
+
depth += 1;
|
|
2100
|
+
if (ch === '}') {
|
|
2101
|
+
depth -= 1;
|
|
2102
|
+
if (depth === 0) {
|
|
2103
|
+
let end = i + 1;
|
|
2104
|
+
while (end < source.length && (source[end] === '\n' || source[end] === '\r')) {
|
|
2105
|
+
end += 1;
|
|
2106
|
+
}
|
|
2107
|
+
return { start, end };
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
function extractHeaderConventions(headerBlock) {
|
|
2114
|
+
const match = headerBlock.match(/(^|\n)([ \t]*)conventions(?:\s*:[^=\n]+)?\s*=\s*\[([\s\S]*?)\n\2\]/);
|
|
2115
|
+
if (!match)
|
|
2116
|
+
return [];
|
|
2117
|
+
const body = match[3] ?? '';
|
|
2118
|
+
return [...body.matchAll(/"([^"]+)"/g)].map((entry) => entry[1]).filter(Boolean);
|
|
2119
|
+
}
|
|
2120
|
+
function mergeSecurityConventionsIntoHeader(headerBlock, missing) {
|
|
2121
|
+
const listPattern = /(^|\n)([ \t]*)conventions(?:\s*:[^=\n]+)?\s*=\s*\[([\s\S]*?)\n\2\]/;
|
|
2122
|
+
const match = headerBlock.match(listPattern);
|
|
2123
|
+
if (!match) {
|
|
2124
|
+
const insertAt = headerBlock.indexOf('{') + 1;
|
|
2125
|
+
const prefix = headerBlock.slice(0, insertAt);
|
|
2126
|
+
const suffix = headerBlock.slice(insertAt);
|
|
2127
|
+
const snippet = [
|
|
2128
|
+
'',
|
|
2129
|
+
' conventions:conventionSet = [',
|
|
2130
|
+
...missing.map((entry) => ` "${entry}"`),
|
|
2131
|
+
' ]',
|
|
2132
|
+
].join('\n');
|
|
2133
|
+
return `${prefix}${snippet}${suffix}`;
|
|
2134
|
+
}
|
|
2135
|
+
const indent = match[2] ?? '';
|
|
2136
|
+
const existingBody = (match[3] ?? '').trimEnd();
|
|
2137
|
+
const extraEntries = missing.map((entry) => `${indent} "${entry}"`);
|
|
2138
|
+
const body = existingBody.length > 0
|
|
2139
|
+
? `${existingBody}\n${extraEntries.join('\n')}`
|
|
2140
|
+
: extraEntries.join('\n');
|
|
2141
|
+
const replacement = `${match[1]}${indent}conventions:conventionSet = [\n${body}\n${indent}]`;
|
|
2142
|
+
return headerBlock.replace(listPattern, replacement);
|
|
2143
|
+
}
|
|
2144
|
+
function declaredContractOverrideWarning(phase, code, message) {
|
|
2145
|
+
return {
|
|
2146
|
+
level: 'warning',
|
|
2147
|
+
phase,
|
|
2148
|
+
phaseLabel: phase === 5 ? 'Profile Compilation' : 'Schema Validation',
|
|
2149
|
+
code,
|
|
2150
|
+
message,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function collectDeclaredContractOverrideWarnings(headerInfo, appliedSchemaId, appliedProfile) {
|
|
2154
|
+
const warnings = [];
|
|
2155
|
+
if (headerInfo.schema && appliedSchemaId && headerInfo.schema !== appliedSchemaId) {
|
|
2156
|
+
warnings.push(declaredContractOverrideWarning(6, 'DECLARED_SCHEMA_OVERRIDDEN', `Applied schema '${appliedSchemaId}' overrides document-declared schema '${headerInfo.schema}'`));
|
|
2157
|
+
}
|
|
2158
|
+
if (appliedProfile && headerInfo.profile && appliedProfile !== headerInfo.profile) {
|
|
2159
|
+
warnings.push(declaredContractOverrideWarning(5, 'DECLARED_PROFILE_OVERRIDDEN', `Applied profile '${appliedProfile}' overrides document-declared profile '${headerInfo.profile}'`));
|
|
2160
|
+
}
|
|
2161
|
+
return warnings;
|
|
2162
|
+
}
|
|
2163
|
+
function buildBindContractMeta(headerInfo, appliedSchemaId, appliedProfile) {
|
|
2164
|
+
const declared = {
|
|
2165
|
+
...(headerInfo.schema ? { schema: headerInfo.schema } : {}),
|
|
2166
|
+
...(headerInfo.profile ? { profile: headerInfo.profile } : {}),
|
|
2167
|
+
...(Object.keys(headerInfo.schemas).length > 0 ? { schemas: headerInfo.schemas } : {}),
|
|
2168
|
+
};
|
|
2169
|
+
const applied = {
|
|
2170
|
+
...(appliedSchemaId ? { schema: appliedSchemaId } : {}),
|
|
2171
|
+
...(appliedProfile ? { profile: appliedProfile } : {}),
|
|
2172
|
+
};
|
|
2173
|
+
if (Object.keys(declared).length === 0 && Object.keys(applied).length === 0) {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
...(Object.keys(declared).length > 0 ? { declared } : {}),
|
|
2178
|
+
...(Object.keys(applied).length > 0 ? { applied } : {}),
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
function buildDeclaredInspectContractMeta(headerInfo) {
|
|
2182
|
+
const declared = {
|
|
2183
|
+
...(headerInfo.schema ? { schema: headerInfo.schema } : {}),
|
|
2184
|
+
...(headerInfo.profile ? { profile: headerInfo.profile } : {}),
|
|
2185
|
+
...(Object.keys(headerInfo.schemas).length > 0 ? { schemas: headerInfo.schemas } : {}),
|
|
2186
|
+
};
|
|
2187
|
+
return Object.keys(declared).length > 0 ? { declared } : null;
|
|
2188
|
+
}
|
|
2189
|
+
function extractHeaderInfo(input) {
|
|
2190
|
+
try {
|
|
2191
|
+
const header = inspectHeader(input).header;
|
|
2192
|
+
return {
|
|
2193
|
+
mode: header.mode ?? 'transport',
|
|
2194
|
+
version: header.version ?? null,
|
|
2195
|
+
profile: header.profile ?? null,
|
|
2196
|
+
schema: header.schema ?? null,
|
|
2197
|
+
schemas: header.schemas ?? {},
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
catch {
|
|
2201
|
+
return { mode: 'transport', version: null, profile: null, schema: null, schemas: {} };
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function createCanonicalReceipt(source, events, overrides = {}) {
|
|
2205
|
+
const header = extractHeaderInfo(source);
|
|
2206
|
+
const receipt = buildCanonicalReceipt(source, events, {
|
|
2207
|
+
canonicalMode: header.mode,
|
|
2208
|
+
canonicalProfile: header.profile ?? 'core',
|
|
2209
|
+
canonicalSpecRelease: 'v1',
|
|
2210
|
+
canonicalHashAlgorithm: overrides.canonicalHashAlgorithm ?? 'sha-256',
|
|
2211
|
+
embedCanonicalPayload: overrides.embedCanonicalPayload ?? true,
|
|
2212
|
+
producer: {
|
|
2213
|
+
implementation: 'aeon-cli-ts',
|
|
2214
|
+
version: VERSION,
|
|
2215
|
+
},
|
|
2216
|
+
});
|
|
2217
|
+
if (!overrides.receiptDigestOverride) {
|
|
2218
|
+
return receipt;
|
|
2219
|
+
}
|
|
2220
|
+
return {
|
|
2221
|
+
...receipt,
|
|
2222
|
+
canonical: {
|
|
2223
|
+
...receipt.canonical,
|
|
2224
|
+
digest: overrides.receiptDigestOverride,
|
|
2225
|
+
},
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function defaultReceiptSidecarPath(file) {
|
|
2229
|
+
return `${file}.receipt.json`;
|
|
2230
|
+
}
|
|
2231
|
+
function resolveReceiptSidecarPathForSign(file, args, writeOutput) {
|
|
2232
|
+
const explicit = getFlagValue(args, '--receipt');
|
|
2233
|
+
if (args.includes('--receipt') && explicit === undefined) {
|
|
2234
|
+
console.error('Error: Missing value for --receipt <path>');
|
|
2235
|
+
process.exit(2);
|
|
2236
|
+
}
|
|
2237
|
+
if (explicit)
|
|
2238
|
+
return explicit;
|
|
2239
|
+
return writeOutput ? defaultReceiptSidecarPath(file) : undefined;
|
|
2240
|
+
}
|
|
2241
|
+
function resolveReceiptSidecarForVerify(file, args) {
|
|
2242
|
+
const explicit = getFlagValue(args, '--receipt');
|
|
2243
|
+
if (args.includes('--receipt') && explicit === undefined) {
|
|
2244
|
+
console.error('Error: Missing value for --receipt <path>');
|
|
2245
|
+
process.exit(2);
|
|
2246
|
+
}
|
|
2247
|
+
const candidate = explicit ?? defaultReceiptSidecarPath(file);
|
|
2248
|
+
if (!fs.existsSync(candidate)) {
|
|
2249
|
+
return undefined;
|
|
2250
|
+
}
|
|
2251
|
+
return readReceiptSidecar(candidate);
|
|
2252
|
+
}
|
|
2253
|
+
function readReceiptSidecar(file) {
|
|
2254
|
+
let parsed;
|
|
2255
|
+
try {
|
|
2256
|
+
parsed = JSON.parse(readFile(file));
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
console.error(`Error: Receipt file is not valid JSON: ${file}`);
|
|
2260
|
+
process.exit(2);
|
|
2261
|
+
}
|
|
2262
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
2263
|
+
console.error(`Error: Receipt file must be a JSON object: ${file}`);
|
|
2264
|
+
process.exit(2);
|
|
2265
|
+
}
|
|
2266
|
+
return parsed;
|
|
2267
|
+
}
|
|
2268
|
+
function writeReceiptSidecar(file, receipt) {
|
|
2269
|
+
fs.writeFileSync(file, JSON.stringify(receipt, null, 2), 'utf-8');
|
|
2270
|
+
}
|
|
2271
|
+
function extractEnvelopeFields(input) {
|
|
2272
|
+
const lex = tokenize(input);
|
|
2273
|
+
const parseResult = parse(lex.tokens);
|
|
2274
|
+
if (lex.errors.length > 0 || parseResult.errors.length > 0 || !parseResult.document) {
|
|
2275
|
+
return {
|
|
2276
|
+
fields: null,
|
|
2277
|
+
errors: [{ level: 'error', code: 'ENVELOPE_PARSE_ERROR', message: 'Unable to parse envelope source' }],
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
const binding = parseResult.document.bindings.find((entry) => isEnvelopeBinding(entry));
|
|
2281
|
+
if (!binding) {
|
|
2282
|
+
return { fields: null, errors: [] };
|
|
2283
|
+
}
|
|
2284
|
+
if (binding.value.type !== 'ObjectNode') {
|
|
2285
|
+
return {
|
|
2286
|
+
fields: null,
|
|
2287
|
+
errors: [{ level: 'error', code: 'ENVELOPE_NOT_OBJECT', message: 'envelope binding must be an object' }],
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
const fields = new Map();
|
|
2291
|
+
for (const entry of binding.value.bindings) {
|
|
2292
|
+
collectEnvelopeFields(fields, entry, []);
|
|
2293
|
+
}
|
|
2294
|
+
return { fields, errors: [] };
|
|
2295
|
+
}
|
|
2296
|
+
function isEnvelopeStartLine(line) {
|
|
2297
|
+
return /^\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*envelope\b/.test(line);
|
|
2298
|
+
}
|
|
2299
|
+
function isEnvelopeBinding(binding) {
|
|
2300
|
+
return datatypeBase(binding.datatype?.name) === ENVELOPE_DATATYPE;
|
|
2301
|
+
}
|
|
2302
|
+
function datatypeBase(datatype) {
|
|
2303
|
+
if (!datatype)
|
|
2304
|
+
return null;
|
|
2305
|
+
const genericIdx = datatype.indexOf('<');
|
|
2306
|
+
const separatorIdx = datatype.indexOf('[');
|
|
2307
|
+
const endIdx = [genericIdx, separatorIdx]
|
|
2308
|
+
.filter((idx) => idx >= 0)
|
|
2309
|
+
.reduce((min, idx) => Math.min(min, idx), datatype.length);
|
|
2310
|
+
return datatype.slice(0, endIdx).toLowerCase();
|
|
2311
|
+
}
|
|
2312
|
+
function collectEnvelopeFields(fields, binding, parents) {
|
|
2313
|
+
const prefixes = formatEnvelopePrefixes(binding, parents);
|
|
2314
|
+
for (const prefix of prefixes) {
|
|
2315
|
+
fields.set(prefix, binding.value);
|
|
2316
|
+
}
|
|
2317
|
+
if (binding.value.type === 'ObjectNode') {
|
|
2318
|
+
for (const child of binding.value.bindings) {
|
|
2319
|
+
collectEnvelopeFields(fields, child, prefixes);
|
|
2320
|
+
}
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (binding.value.type === 'ListNode') {
|
|
2324
|
+
for (let index = 0; index < binding.value.elements.length; index++) {
|
|
2325
|
+
const element = binding.value.elements[index];
|
|
2326
|
+
for (const prefix of prefixes) {
|
|
2327
|
+
fields.set(`${prefix}[${index}]`, element);
|
|
2328
|
+
}
|
|
2329
|
+
if (element.type === 'ObjectNode') {
|
|
2330
|
+
for (const child of element.bindings) {
|
|
2331
|
+
collectEnvelopeFields(fields, child, prefixes.map((prefix) => `${prefix}[${index}]`));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
function formatEnvelopePrefixes(binding, parents) {
|
|
2338
|
+
const base = parents.length > 0 ? parents.map((parent) => `${parent}.${binding.key}`) : [binding.key];
|
|
2339
|
+
if (!binding.datatype?.name) {
|
|
2340
|
+
return base;
|
|
2341
|
+
}
|
|
2342
|
+
const typed = parents.length > 0
|
|
2343
|
+
? parents.map((parent) => `${parent}.${binding.key}:${binding.datatype.name}`)
|
|
2344
|
+
: [`${binding.key}:${binding.datatype.name}`];
|
|
2345
|
+
return [...new Set([...base, ...typed])];
|
|
2346
|
+
}
|
|
2347
|
+
function readEnvelopeField(fields, key, diagnostics) {
|
|
2348
|
+
const value = fields.get(key);
|
|
2349
|
+
if (!value)
|
|
2350
|
+
return null;
|
|
2351
|
+
const literal = readLiteralString(value);
|
|
2352
|
+
if (literal === null) {
|
|
2353
|
+
diagnostics.errors.push({
|
|
2354
|
+
level: 'error',
|
|
2355
|
+
code: 'ENVELOPE_FIELD_TYPE',
|
|
2356
|
+
message: `${key} must be a literal string value`,
|
|
2357
|
+
});
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
return literal;
|
|
2361
|
+
}
|
|
2362
|
+
function readEnvelopeFieldAny(fields, keys, diagnostics) {
|
|
2363
|
+
for (const key of keys) {
|
|
2364
|
+
const value = readEnvelopeField(fields, key, diagnostics);
|
|
2365
|
+
if (value !== null)
|
|
2366
|
+
return value;
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
function readLiteralString(value) {
|
|
2371
|
+
switch (value.type) {
|
|
2372
|
+
case 'TypedValue':
|
|
2373
|
+
return readLiteralString(value.value);
|
|
2374
|
+
case 'StringLiteral':
|
|
2375
|
+
case 'SeparatorLiteral':
|
|
2376
|
+
case 'DateLiteral':
|
|
2377
|
+
case 'DateTimeLiteral':
|
|
2378
|
+
case 'EncodingLiteral':
|
|
2379
|
+
case 'HexLiteral':
|
|
2380
|
+
case 'RadixLiteral':
|
|
2381
|
+
case 'InfinityLiteral':
|
|
2382
|
+
case 'NaNLiteral':
|
|
2383
|
+
case 'NullLiteral':
|
|
2384
|
+
return String(value.raw);
|
|
2385
|
+
case 'NumberLiteral':
|
|
2386
|
+
case 'BooleanLiteral':
|
|
2387
|
+
case 'ToggleLiteral':
|
|
2388
|
+
case 'CloneReference':
|
|
2389
|
+
case 'PointerReference':
|
|
2390
|
+
case 'ObjectNode':
|
|
2391
|
+
case 'ListNode':
|
|
2392
|
+
default:
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function normalizeHash(value) {
|
|
2397
|
+
return value.trim().replace(/^#/, '').toLowerCase();
|
|
2398
|
+
}
|
|
2399
|
+
function normalizeHashAlgorithm(algorithm, diagnostics, strict, field) {
|
|
2400
|
+
const normalized = algorithm.trim().toLowerCase();
|
|
2401
|
+
if (normalized === 'sha-256' || normalized === 'sha256')
|
|
2402
|
+
return 'sha-256';
|
|
2403
|
+
if (normalized === 'sha-512' || normalized === 'sha512')
|
|
2404
|
+
return 'sha-512';
|
|
2405
|
+
const message = `${field} must be sha-256 or sha-512 (received "${algorithm}")`;
|
|
2406
|
+
if (strict) {
|
|
2407
|
+
diagnostics.errors.push({ level: 'error', code: 'ENVELOPE_HASH_ALG_UNSUPPORTED', message });
|
|
2408
|
+
}
|
|
2409
|
+
else {
|
|
2410
|
+
diagnostics.warnings.push({ level: 'warning', code: 'ENVELOPE_HASH_ALG_UNSUPPORTED', message });
|
|
2411
|
+
}
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
function pushModeDiagnostic(diagnostics, strict, code, message) {
|
|
2415
|
+
if (strict) {
|
|
2416
|
+
diagnostics.errors.push({ level: 'error', code, message });
|
|
2417
|
+
}
|
|
2418
|
+
else {
|
|
2419
|
+
diagnostics.warnings.push({ level: 'warning', code, message });
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function outputEnvelopeDiagnostics(diagnostics, label) {
|
|
2423
|
+
for (const diagnostic of diagnostics) {
|
|
2424
|
+
const code = diagnostic.code ?? 'ENVELOPE';
|
|
2425
|
+
const message = diagnostic.message.replace(/[\r\n]+/g, ' ');
|
|
2426
|
+
console.log(`${label} [${code}] ${message}`);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
function outputEnvelopeJson(errors, warnings, ok, verification, receipt) {
|
|
2430
|
+
const payload = {
|
|
2431
|
+
ok,
|
|
2432
|
+
errors: errors.map((diag) => ({
|
|
2433
|
+
code: diag.code,
|
|
2434
|
+
message: diag.message,
|
|
2435
|
+
})),
|
|
2436
|
+
warnings: warnings.map((diag) => ({
|
|
2437
|
+
code: diag.code,
|
|
2438
|
+
message: diag.message,
|
|
2439
|
+
})),
|
|
2440
|
+
...(receipt ? { receipt } : {}),
|
|
2441
|
+
...(verification ? { verification } : {}),
|
|
2442
|
+
};
|
|
2443
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2444
|
+
}
|
|
2445
|
+
function outputMarkdown(file, result, info) {
|
|
2446
|
+
const visibleEvents = result.events.filter(e => !e.key.startsWith('aeon:'));
|
|
2447
|
+
const annotations = info.sortAnnotations
|
|
2448
|
+
? sortAnnotationRecords(result.annotations ?? [])
|
|
2449
|
+
: (result.annotations ?? []);
|
|
2450
|
+
if (info.annotationsOnly) {
|
|
2451
|
+
console.log('# AEON Annotations');
|
|
2452
|
+
console.log('');
|
|
2453
|
+
console.log(`- Count: ${annotations.length}`);
|
|
2454
|
+
if (annotations.length > 0) {
|
|
2455
|
+
console.log('');
|
|
2456
|
+
console.log('## Annotation Records');
|
|
2457
|
+
for (const annotation of annotations) {
|
|
2458
|
+
console.log(`- ${formatAnnotationLine(annotation)}`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
console.log('# AEON Inspect');
|
|
2464
|
+
if (info.recovery) {
|
|
2465
|
+
console.log('> WARNING: recovery mode enabled (tooling-only); output may be partial');
|
|
2466
|
+
}
|
|
2467
|
+
console.log('');
|
|
2468
|
+
console.log('## Summary');
|
|
2469
|
+
console.log(`- File: ${path.basename(file)}`);
|
|
2470
|
+
console.log(`- Version: ${info.version ?? '—'}`);
|
|
2471
|
+
console.log(`- Mode: ${info.mode}`);
|
|
2472
|
+
console.log(`- Profile: ${info.profile ?? '—'}`);
|
|
2473
|
+
console.log(`- Schema: ${info.schema ?? '—'}`);
|
|
2474
|
+
console.log(`- Recovery: ${info.recovery ? 'true' : 'false'}`);
|
|
2475
|
+
console.log(`- Events: ${visibleEvents.length}`);
|
|
2476
|
+
if (info.includeAnnotations) {
|
|
2477
|
+
console.log(`- Annotations: ${annotations.length}`);
|
|
2478
|
+
}
|
|
2479
|
+
console.log(`- Errors: ${result.errors.length}`);
|
|
2480
|
+
if (info.profile || info.schema || Object.keys(info.schemas).length > 0) {
|
|
2481
|
+
console.log('');
|
|
2482
|
+
console.log('## Declared Contracts');
|
|
2483
|
+
if (info.profile)
|
|
2484
|
+
console.log(`- Profile: ${info.profile}`);
|
|
2485
|
+
if (info.schema)
|
|
2486
|
+
console.log(`- Schema: ${info.schema}`);
|
|
2487
|
+
for (const [context, value] of Object.entries(info.schemas)) {
|
|
2488
|
+
console.log(`- Schema Context (${context}): ${value}`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
if (result.errors.length > 0) {
|
|
2492
|
+
console.log('');
|
|
2493
|
+
console.log('## Errors');
|
|
2494
|
+
for (const error of result.errors) {
|
|
2495
|
+
console.log(`- ${formatErrorLine(error)}`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (visibleEvents.length > 0) {
|
|
2499
|
+
console.log('');
|
|
2500
|
+
console.log('## Assignment Events');
|
|
2501
|
+
for (const event of visibleEvents) {
|
|
2502
|
+
console.log(`- ${formatEventLine(event)}`);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
const refs = findReferences(visibleEvents);
|
|
2506
|
+
if (refs.length > 0) {
|
|
2507
|
+
console.log('');
|
|
2508
|
+
console.log('## References');
|
|
2509
|
+
for (const ref of refs) {
|
|
2510
|
+
console.log(`- ${ref}`);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (info.includeAnnotations && annotations.length > 0) {
|
|
2514
|
+
console.log('');
|
|
2515
|
+
console.log('## Annotation Records');
|
|
2516
|
+
for (const annotation of annotations) {
|
|
2517
|
+
console.log(`- ${formatAnnotationLine(annotation)}`);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
function sortAnnotationRecords(records) {
|
|
2522
|
+
return [...records]
|
|
2523
|
+
.map((record, index) => ({ record, index }))
|
|
2524
|
+
.sort((left, right) => {
|
|
2525
|
+
const byStart = left.record.span.start.offset - right.record.span.start.offset;
|
|
2526
|
+
if (byStart !== 0)
|
|
2527
|
+
return byStart;
|
|
2528
|
+
const byEnd = left.record.span.end.offset - right.record.span.end.offset;
|
|
2529
|
+
if (byEnd !== 0)
|
|
2530
|
+
return byEnd;
|
|
2531
|
+
const byKind = left.record.kind.localeCompare(right.record.kind);
|
|
2532
|
+
if (byKind !== 0)
|
|
2533
|
+
return byKind;
|
|
2534
|
+
const byForm = left.record.form.localeCompare(right.record.form);
|
|
2535
|
+
if (byForm !== 0)
|
|
2536
|
+
return byForm;
|
|
2537
|
+
const byRaw = left.record.raw.localeCompare(right.record.raw);
|
|
2538
|
+
if (byRaw !== 0)
|
|
2539
|
+
return byRaw;
|
|
2540
|
+
return left.index - right.index;
|
|
2541
|
+
})
|
|
2542
|
+
.map((entry) => entry.record);
|
|
2543
|
+
}
|
|
2544
|
+
function formatAnnotationLine(annotation) {
|
|
2545
|
+
const target = (() => {
|
|
2546
|
+
if (annotation.target.kind === 'path') {
|
|
2547
|
+
return annotation.target.path;
|
|
2548
|
+
}
|
|
2549
|
+
if (annotation.target.kind === 'span') {
|
|
2550
|
+
return `span(${formatSpan(annotation.target.span)})`;
|
|
2551
|
+
}
|
|
2552
|
+
return `unbound(${annotation.target.reason})`;
|
|
2553
|
+
})();
|
|
2554
|
+
const subtype = annotation.subtype ? `/${annotation.subtype}` : '';
|
|
2555
|
+
return `${annotation.kind}${subtype} ${annotation.form} -> ${target} raw=${JSON.stringify(annotation.raw)}`;
|
|
2556
|
+
}
|
|
2557
|
+
function formatSpan(span) {
|
|
2558
|
+
if (!span || typeof span !== 'object')
|
|
2559
|
+
return '?:?-?:?';
|
|
2560
|
+
const start = span.start;
|
|
2561
|
+
const end = span.end;
|
|
2562
|
+
if (!start || !end || typeof start !== 'object' || typeof end !== 'object')
|
|
2563
|
+
return '?:?-?:?';
|
|
2564
|
+
const sl = start.line;
|
|
2565
|
+
const sc = start.column;
|
|
2566
|
+
const el = end.line;
|
|
2567
|
+
const ec = end.column;
|
|
2568
|
+
if ([sl, sc, el, ec].some(v => typeof v !== 'number'))
|
|
2569
|
+
return '?:?-?:?';
|
|
2570
|
+
return `${sl}:${sc}-${el}:${ec}`;
|
|
2571
|
+
}
|
|
2572
|
+
function formatErrorLine(error) {
|
|
2573
|
+
const code = error.code ?? 'UNKNOWN';
|
|
2574
|
+
const errPath = getErrorPath(error) ?? '$';
|
|
2575
|
+
const span = formatSpan(error.span);
|
|
2576
|
+
const message = String(error.message).replace(/[\r\n]+/g, ' ');
|
|
2577
|
+
const phaseLabel = getPhaseLabel(error);
|
|
2578
|
+
const prefix = phaseLabel ? `${phaseLabel}: ` : '';
|
|
2579
|
+
return `${prefix}${message} [${code}] path=${errPath} span=${span}`;
|
|
2580
|
+
}
|
|
2581
|
+
function phaseNumberLabel(phase) {
|
|
2582
|
+
switch (phase) {
|
|
2583
|
+
case 0:
|
|
2584
|
+
return 'Input Validation';
|
|
2585
|
+
case 5:
|
|
2586
|
+
return 'Profile Compilation';
|
|
2587
|
+
case 6:
|
|
2588
|
+
return 'Schema Validation';
|
|
2589
|
+
case 7:
|
|
2590
|
+
return 'Reference Resolution';
|
|
2591
|
+
case 8:
|
|
2592
|
+
return 'Finalization';
|
|
2593
|
+
default:
|
|
2594
|
+
return undefined;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
function inferPhaseLabelFromCode(code) {
|
|
2598
|
+
switch (code) {
|
|
2599
|
+
case 'INPUT_SIZE_EXCEEDED':
|
|
2600
|
+
return 'Input Validation';
|
|
2601
|
+
case 'UNEXPECTED_CHARACTER':
|
|
2602
|
+
case 'UNTERMINATED_BLOCK_COMMENT':
|
|
2603
|
+
case 'UNTERMINATED_STRING':
|
|
2604
|
+
case 'UNTERMINATED_TRIMTICK':
|
|
2605
|
+
return 'Lexical Analysis';
|
|
2606
|
+
case 'SYNTAX_ERROR':
|
|
2607
|
+
case 'INVALID_DATE':
|
|
2608
|
+
case 'INVALID_TIME':
|
|
2609
|
+
case 'INVALID_DATETIME':
|
|
2610
|
+
case 'INVALID_SEPARATOR_CHAR':
|
|
2611
|
+
case 'SEPARATOR_DEPTH_EXCEEDED':
|
|
2612
|
+
case 'GENERIC_DEPTH_EXCEEDED':
|
|
2613
|
+
return 'Parsing';
|
|
2614
|
+
case 'HEADER_CONFLICT':
|
|
2615
|
+
case 'DUPLICATE_KEY':
|
|
2616
|
+
case 'DUPLICATE_CANONICAL_PATH':
|
|
2617
|
+
case 'DATATYPE_LITERAL_MISMATCH':
|
|
2618
|
+
return 'Core Validation';
|
|
2619
|
+
case 'MISSING_REFERENCE_TARGET':
|
|
2620
|
+
case 'FORWARD_REFERENCE':
|
|
2621
|
+
case 'SELF_REFERENCE':
|
|
2622
|
+
case 'ATTRIBUTE_DEPTH_EXCEEDED':
|
|
2623
|
+
return 'Reference Validation';
|
|
2624
|
+
case 'UNTYPED_TOGGLE_LITERAL':
|
|
2625
|
+
case 'UNTYPED_VALUE_IN_STRICT_MODE':
|
|
2626
|
+
case 'CUSTOM_TOGGLE_ALIAS_NOT_ALLOWED':
|
|
2627
|
+
case 'CUSTOM_DATATYPE_NOT_ALLOWED':
|
|
2628
|
+
case 'INVALID_NODE_HEAD_DATATYPE':
|
|
2629
|
+
return 'Mode Enforcement';
|
|
2630
|
+
case 'PROFILE_NOT_FOUND':
|
|
2631
|
+
case 'PROFILE_PROCESSORS_SKIPPED':
|
|
2632
|
+
return 'Profile Compilation';
|
|
2633
|
+
case 'TYPE_GUARD_FAILED':
|
|
2634
|
+
return 'Finalization';
|
|
2635
|
+
default:
|
|
2636
|
+
return code?.startsWith('FINALIZE_') ? 'Finalization' : undefined;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function getPhaseLabel(error) {
|
|
2640
|
+
const phase = typeof error.phase === 'number' ? error.phase : undefined;
|
|
2641
|
+
return phaseNumberLabel(phase) ?? inferPhaseLabelFromCode(error.code);
|
|
2642
|
+
}
|
|
2643
|
+
function formatEventLine(event) {
|
|
2644
|
+
const p = formatPath(event.path);
|
|
2645
|
+
const t = event.datatype ? ` :${event.datatype}` : '';
|
|
2646
|
+
return `${p}${t} = ${renderValue(event.value)}`;
|
|
2647
|
+
}
|
|
2648
|
+
function renderValue(value) {
|
|
2649
|
+
const type = String(value.type);
|
|
2650
|
+
switch (type) {
|
|
2651
|
+
case 'TypedValue': {
|
|
2652
|
+
const datatype = value.datatype && typeof value.datatype === 'object' ? value.datatype.name : null;
|
|
2653
|
+
const inner = value.value && typeof value.value === 'object' ? renderValue(value.value) : '';
|
|
2654
|
+
return `:${typeof datatype === 'string' ? datatype : 'unknown'} = ${inner}`;
|
|
2655
|
+
}
|
|
2656
|
+
case 'StringLiteral':
|
|
2657
|
+
return JSON.stringify(String(value.value ?? ''));
|
|
2658
|
+
case 'InfinityLiteral':
|
|
2659
|
+
case 'NaNLiteral':
|
|
2660
|
+
return String(value.raw ?? value.value ?? '');
|
|
2661
|
+
case 'NullLiteral':
|
|
2662
|
+
return String(value.raw ?? '');
|
|
2663
|
+
case 'NumberLiteral':
|
|
2664
|
+
return String(value.raw ?? value.value ?? '');
|
|
2665
|
+
case 'BooleanLiteral':
|
|
2666
|
+
return String(value.raw ?? value.value ?? '');
|
|
2667
|
+
case 'ToggleLiteral':
|
|
2668
|
+
return String(value.raw ?? value.value ?? '');
|
|
2669
|
+
case 'HexLiteral':
|
|
2670
|
+
case 'RadixLiteral':
|
|
2671
|
+
case 'EncodingLiteral':
|
|
2672
|
+
case 'SeparatorLiteral':
|
|
2673
|
+
case 'DateLiteral':
|
|
2674
|
+
case 'DateTimeLiteral':
|
|
2675
|
+
return String(value.raw ?? value.value ?? '');
|
|
2676
|
+
case 'CloneReference':
|
|
2677
|
+
return `~${Array.isArray(value.path) ? value.path.join('.') : ''}`;
|
|
2678
|
+
case 'PointerReference':
|
|
2679
|
+
return `~>${Array.isArray(value.path) ? value.path.join('.') : ''}`;
|
|
2680
|
+
case 'ObjectNode': {
|
|
2681
|
+
const bindings = Array.isArray(value.bindings) ? value.bindings : [];
|
|
2682
|
+
const rendered = bindings
|
|
2683
|
+
.map(b => renderBindingInline(b))
|
|
2684
|
+
.filter(s => s.length > 0)
|
|
2685
|
+
.join(', ');
|
|
2686
|
+
return `{ ${rendered} }`;
|
|
2687
|
+
}
|
|
2688
|
+
case 'ListNode': {
|
|
2689
|
+
const elements = Array.isArray(value.elements) ? value.elements : [];
|
|
2690
|
+
const rendered = elements
|
|
2691
|
+
.map(e => renderValue(e))
|
|
2692
|
+
.join(', ');
|
|
2693
|
+
return `[ ${rendered} ]`;
|
|
2694
|
+
}
|
|
2695
|
+
default:
|
|
2696
|
+
return type;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
function renderBindingInline(binding) {
|
|
2700
|
+
const key = typeof binding.key === 'string' ? binding.key : '';
|
|
2701
|
+
const datatype = binding.datatype && typeof binding.datatype === 'object' ? binding.datatype.name : null;
|
|
2702
|
+
const value = binding.value && typeof binding.value === 'object' ? binding.value : null;
|
|
2703
|
+
if (!key || !value)
|
|
2704
|
+
return '';
|
|
2705
|
+
const dt = typeof datatype === 'string' ? `:${datatype}` : '';
|
|
2706
|
+
return `${key}${dt} = ${renderValue(value)}`;
|
|
2707
|
+
}
|
|
2708
|
+
function getErrorPath(error) {
|
|
2709
|
+
const candidate = error.path;
|
|
2710
|
+
if (typeof candidate === 'string') {
|
|
2711
|
+
return candidate;
|
|
2712
|
+
}
|
|
2713
|
+
if (candidate && typeof candidate === 'object' && 'segments' in candidate) {
|
|
2714
|
+
return formatPath(candidate);
|
|
2715
|
+
}
|
|
2716
|
+
return undefined;
|
|
2717
|
+
}
|
|
2718
|
+
function findReferences(events) {
|
|
2719
|
+
const refs = [];
|
|
2720
|
+
for (const event of events) {
|
|
2721
|
+
const value = event.value;
|
|
2722
|
+
if (value.type === 'CloneReference') {
|
|
2723
|
+
refs.push(`${formatPath(event.path)} = ~${value.path.join('.')}`);
|
|
2724
|
+
}
|
|
2725
|
+
else if (value.type === 'PointerReference') {
|
|
2726
|
+
refs.push(`${formatPath(event.path)} = ~>${value.path.join('.')}`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
return refs;
|
|
2730
|
+
}
|
|
2731
|
+
//# sourceMappingURL=main.js.map
|