@dorigjo/besa 0.1.0-alpha.1 → 0.1.0-beta.4

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/index.js CHANGED
@@ -1,34 +1,149 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { generateKeyPair } from "./crypto.js";
2
+ import { chmodSync, existsSync, lstatSync, mkdirSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { generateKeyPair, publicKeyId, validateKeyPair, } from "./crypto.js";
5
6
  import { loadManifest } from "./manifest.js";
6
- import { createReceipt, signManifest, verifySignedManifest } from "./signing.js";
7
- import { admit, getCount, increment, loadMeter, saveMeter } from "./admit.js";
7
+ import { createReceipt, hashRequest, signManifest, validateReceipt, validateSignedManifest, verifyReceiptDetailed, verifySignedManifest, } from "./signing.js";
8
+ import { admit, admitAndConsume, getCount, loadMeter, meterKey, } from "./admit.js";
8
9
  import { checkGrant, loadGrants } from "./grant.js";
10
+ import { addTrustAnchor, applyKeyRotation, checkTrustedKey, createKeyRotation, emptyTrustStore, revokeTrustAnchor, validateTrustStore, verifyKeyRotation, verifyTrustedSignedManifest, } from "./trust.js";
11
+ import { readJsonFile, writeJsonAtomic, writeJsonExclusive, } from "./io.js";
12
+ import { isStoredKeyPair, openKeyPair, sealKeyPair, } from "./keystore.js";
9
13
  const BESA_DIR = ".besa";
10
14
  const KEY_PATH = join(BESA_DIR, "key.json");
15
+ const KEYS_DIR = join(BESA_DIR, "keys");
11
16
  const METER_PATH = join(BESA_DIR, "meter.json");
12
17
  const ACTIVE_MANIFEST_PATH = join(BESA_DIR, "active-manifest.json");
13
18
  const RECEIPTS_DIR = join(BESA_DIR, "receipts");
19
+ const ROTATIONS_DIR = join(BESA_DIR, "rotations");
20
+ const TRUST_PATH = join(BESA_DIR, "trust.json");
21
+ const FLAGS_WITH_VALUES = new Set([
22
+ "--agent",
23
+ "--grants",
24
+ "--request",
25
+ "--trust",
26
+ ]);
27
+ const COMMAND_FLAGS = {
28
+ keys: new Set(["--trust"]),
29
+ trust: new Set(["--trust"]),
30
+ load: new Set(),
31
+ sign: new Set(["--trust"]),
32
+ verify: new Set(["--trust"]),
33
+ admit: new Set(["--trust", "--agent", "--grants"]),
34
+ receipt: new Set([
35
+ "--trust",
36
+ "--request",
37
+ "--agent",
38
+ "--grants",
39
+ ]),
40
+ "verify-receipt": new Set(["--trust"]),
41
+ };
14
42
  function readJson(path) {
15
- return JSON.parse(readFileSync(path, "utf8"));
43
+ return readJsonFile(path);
16
44
  }
17
- function writeJson(path, value) {
18
- writeFileSync(path, JSON.stringify(value, null, 2) + "\n", "utf8");
45
+ function writeJson(path, value, mode) {
46
+ writeJsonAtomic(path, value, mode ?? 0o600);
19
47
  }
20
48
  function ensureBesaDir() {
21
- mkdirSync(BESA_DIR, { recursive: true });
49
+ mkdirSync(BESA_DIR, { recursive: true, mode: 0o700 });
50
+ const stats = lstatSync(BESA_DIR);
51
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
52
+ throw new Error(`${BESA_DIR} must be a real private directory, not a link`);
53
+ }
54
+ if (process.platform !== "win32")
55
+ chmodSync(BESA_DIR, 0o700);
56
+ }
57
+ function protectKeyFile(path = KEY_PATH) {
58
+ if (lstatSync(path).isSymbolicLink()) {
59
+ throw new Error(`refusing to use symbolic-link key file at ${path}`);
60
+ }
61
+ if (process.platform !== "win32")
62
+ chmodSync(path, 0o600);
63
+ }
64
+ function keyPassphrase() {
65
+ const passphrase = process.env.BESA_KEY_PASSPHRASE;
66
+ if (!passphrase) {
67
+ throw new Error("BESA_KEY_PASSPHRASE is required and must contain at least 16 UTF-8 bytes");
68
+ }
69
+ return passphrase;
70
+ }
71
+ function loadExistingKeyPair() {
72
+ if (!existsSync(KEY_PATH)) {
73
+ throw new Error(`no signing key found at ${KEY_PATH}; run besa keys first`);
74
+ }
75
+ ensureBesaDir();
76
+ protectKeyFile();
77
+ const stored = readJson(KEY_PATH);
78
+ const passphrase = keyPassphrase();
79
+ if (isStoredKeyPair(stored)) {
80
+ return openKeyPair(stored, passphrase);
81
+ }
82
+ if (!validateKeyPair(stored)) {
83
+ throw new Error(`invalid or mismatched Ed25519 key pair at ${KEY_PATH}`);
84
+ }
85
+ writeJson(KEY_PATH, sealKeyPair(stored, passphrase), 0o600);
86
+ return stored;
22
87
  }
23
88
  function loadOrCreateKeyPair() {
24
89
  ensureBesaDir();
25
90
  if (existsSync(KEY_PATH)) {
26
- return readJson(KEY_PATH);
91
+ return loadExistingKeyPair();
27
92
  }
28
93
  const keypair = generateKeyPair();
29
- writeJson(KEY_PATH, keypair);
94
+ try {
95
+ writeJsonExclusive(KEY_PATH, sealKeyPair(keypair, keyPassphrase()), 0o600);
96
+ }
97
+ catch (error) {
98
+ if (error.code === "EEXIST") {
99
+ return loadExistingKeyPair();
100
+ }
101
+ throw error;
102
+ }
103
+ protectKeyFile();
30
104
  return keypair;
31
105
  }
106
+ function terminalText(value) {
107
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
108
+ }
109
+ function selectedTrustPath() {
110
+ const path = flagValue("--trust") ?? TRUST_PATH;
111
+ if (!path.endsWith(".json")) {
112
+ throw new Error(`trust store path must end in .json: ${terminalText(path)}`);
113
+ }
114
+ return path;
115
+ }
116
+ function loadTrustStore(path = selectedTrustPath()) {
117
+ if (!existsSync(path)) {
118
+ throw new Error(`no trust store found at ${path}; run besa trust add <signed-manifest> first`);
119
+ }
120
+ if (lstatSync(path).isSymbolicLink()) {
121
+ throw new Error(`refusing to use symbolic-link trust store at ${path}`);
122
+ }
123
+ const validation = validateTrustStore(readJson(path));
124
+ if (!validation.ok || !validation.trustStore) {
125
+ throw new Error(`invalid trust store at ${path}:\n - ${validation.errors.join("\n - ")}`);
126
+ }
127
+ return validation.trustStore;
128
+ }
129
+ function loadOrCreateTrustStore(path = selectedTrustPath()) {
130
+ return existsSync(path) ? loadTrustStore(path) : emptyTrustStore();
131
+ }
132
+ function saveTrustStore(store, path = selectedTrustPath()) {
133
+ if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
134
+ throw new Error(`refusing to write to symbolic-link trust store at ${path}`);
135
+ }
136
+ const validation = validateTrustStore(store);
137
+ if (!validation.ok) {
138
+ throw new Error(`refusing to save invalid trust store: ${validation.errors.join("; ")}`);
139
+ }
140
+ writeJson(path, store, 0o600);
141
+ }
142
+ function trustSignedManifestKey(signed) {
143
+ const path = selectedTrustPath();
144
+ const store = addTrustAnchor(loadOrCreateTrustStore(path), signed.publicKey);
145
+ saveTrustStore(store, path);
146
+ }
32
147
  function printJson(label, value) {
33
148
  console.log("");
34
149
  console.log(label + ":");
@@ -48,14 +163,35 @@ function signedOutPath(manifestPath) {
48
163
  }
49
164
  function flagValue(name) {
50
165
  const index = process.argv.indexOf(name);
51
- return index >= 0 ? process.argv[index + 1] : undefined;
166
+ if (index < 0) {
167
+ return undefined;
168
+ }
169
+ const value = process.argv[index + 1];
170
+ if (!value || value.startsWith("--")) {
171
+ throw new Error(`${name} requires a value`);
172
+ }
173
+ return value;
52
174
  }
53
- function positionals(args) {
175
+ function positionals(args, allowedFlags) {
54
176
  const values = [];
55
- const flagsWithValues = new Set(["--agent", "--grants"]);
177
+ const seenFlags = new Set();
56
178
  for (let index = 0; index < args.length; index += 1) {
57
179
  const value = args[index];
58
- if (value && flagsWithValues.has(value)) {
180
+ if (value?.startsWith("-")) {
181
+ if (!FLAGS_WITH_VALUES.has(value)) {
182
+ throw new Error(`unknown flag '${value}'`);
183
+ }
184
+ if (!allowedFlags.has(value)) {
185
+ throw new Error(`flag '${value}' is not supported by this command`);
186
+ }
187
+ if (seenFlags.has(value)) {
188
+ throw new Error(`duplicate flag '${value}'`);
189
+ }
190
+ const flagArgument = args[index + 1];
191
+ if (!flagArgument || flagArgument.startsWith("--")) {
192
+ throw new Error(`${value} requires a value`);
193
+ }
194
+ seenFlags.add(value);
59
195
  index += 1;
60
196
  continue;
61
197
  }
@@ -65,7 +201,52 @@ function positionals(args) {
65
201
  }
66
202
  return values;
67
203
  }
68
- function cmdKeys() {
204
+ function requireSignedManifest(value) {
205
+ const validation = validateSignedManifest(value);
206
+ if (!validation.ok || !validation.signedManifest) {
207
+ throw new Error(`invalid signed manifest:\n - ${validation.errors.join("\n - ")}`);
208
+ }
209
+ return validation.signedManifest;
210
+ }
211
+ function cmdRotateKeys() {
212
+ const previous = loadExistingKeyPair();
213
+ const next = generateKeyPair();
214
+ const rotation = createKeyRotation(previous, next);
215
+ const previousId = rotation.previousPublicKeyId;
216
+ const archivePath = join(KEYS_DIR, `${previousId}.json`);
217
+ const rotationPath = join(ROTATIONS_DIR, `${previousId}-to-${rotation.newPublicKeyId}.json`);
218
+ const path = selectedTrustPath();
219
+ const anchored = addTrustAnchor(loadOrCreateTrustStore(path), previous.publicKeyDer);
220
+ const rotatedStore = applyKeyRotation(anchored, rotation);
221
+ const passphrase = keyPassphrase();
222
+ // Pre-compute all crypto before touching the filesystem.
223
+ // If scrypt or key derivation throws, no files are written.
224
+ const sealedPrevious = sealKeyPair(previous, passphrase);
225
+ const sealedNext = sealKeyPair(next, passphrase);
226
+ writeJson(archivePath, sealedPrevious, 0o600);
227
+ protectKeyFile(archivePath);
228
+ writeJson(rotationPath, rotation);
229
+ writeJson(KEY_PATH, sealedNext, 0o600);
230
+ protectKeyFile();
231
+ saveTrustStore(rotatedStore, path);
232
+ printJson("keyRotation", rotation);
233
+ console.log("");
234
+ console.log(`OK: active key rotated to ${rotation.newPublicKeyId}`);
235
+ console.log(`OK: previous private key archived at ${terminalText(archivePath)}`);
236
+ console.log(`OK: rotation proof written to ${terminalText(rotationPath)}`);
237
+ console.log("NEXT: re-sign active manifests with the new key");
238
+ }
239
+ function cmdKeys(action) {
240
+ if (action === "rotate") {
241
+ cmdRotateKeys();
242
+ return;
243
+ }
244
+ if (action) {
245
+ throw new Error(`unknown keys action '${action}'`);
246
+ }
247
+ if (flagValue("--trust")) {
248
+ throw new Error("--trust is only supported by keys rotate");
249
+ }
69
250
  const keypair = loadOrCreateKeyPair();
70
251
  printJson("keypair", {
71
252
  publicKeyDer: keypair.publicKeyDer,
@@ -74,11 +255,77 @@ function cmdKeys() {
74
255
  console.log("");
75
256
  console.log("OK: keypair ready at " + KEY_PATH);
76
257
  }
258
+ function cmdTrustAdd(file) {
259
+ const raw = readJson(file);
260
+ const verification = verifySignedManifest(raw);
261
+ if (!verification.valid) {
262
+ throw new Error(`${verification.reasonCode}: ${verification.detail}`);
263
+ }
264
+ const signed = requireSignedManifest(raw);
265
+ const path = selectedTrustPath();
266
+ const store = addTrustAnchor(loadOrCreateTrustStore(path), signed.publicKey);
267
+ saveTrustStore(store, path);
268
+ console.log(`OK: trusted public key ${signed.publicKeyId} in ${terminalText(path)}`);
269
+ }
270
+ function cmdTrustApply(file) {
271
+ const rotation = readJson(file);
272
+ const verification = verifyKeyRotation(rotation);
273
+ if (!verification.valid) {
274
+ throw new Error(`${verification.reasonCode}: ${verification.detail}`);
275
+ }
276
+ const path = selectedTrustPath();
277
+ const store = applyKeyRotation(loadTrustStore(path), rotation);
278
+ saveTrustStore(store, path);
279
+ console.log(`OK: retired ${rotation.previousPublicKeyId} and trusted ${rotation.newPublicKeyId}`);
280
+ }
281
+ function cmdTrustRevoke(keyId) {
282
+ const path = selectedTrustPath();
283
+ const store = revokeTrustAnchor(loadTrustStore(path), keyId);
284
+ saveTrustStore(store, path);
285
+ console.log(`OK: revoked public key ${keyId} in ${terminalText(path)}`);
286
+ }
287
+ function cmdTrustList() {
288
+ const path = selectedTrustPath();
289
+ const store = loadTrustStore(path);
290
+ printJson("trustStore", store);
291
+ console.log("");
292
+ console.log(`OK: loaded ${String(store.keys.length)} trust anchor(s) from ${terminalText(path)}`);
293
+ }
294
+ function cmdTrust(action, value) {
295
+ switch (action) {
296
+ case "add":
297
+ if (!value) {
298
+ throw new Error("trust add requires a signed manifest path");
299
+ }
300
+ cmdTrustAdd(value);
301
+ break;
302
+ case "apply":
303
+ if (!value) {
304
+ throw new Error("trust apply requires a key rotation path");
305
+ }
306
+ cmdTrustApply(value);
307
+ break;
308
+ case "revoke":
309
+ if (!value) {
310
+ throw new Error("trust revoke requires a public key id");
311
+ }
312
+ cmdTrustRevoke(value);
313
+ break;
314
+ case "list":
315
+ if (value) {
316
+ throw new Error("trust list does not accept a positional value");
317
+ }
318
+ cmdTrustList();
319
+ break;
320
+ default:
321
+ throw new Error(`unknown trust action '${action}'`);
322
+ }
323
+ }
77
324
  function cmdLoad(file) {
78
325
  const manifest = loadManifest(file);
79
326
  printJson("manifest", manifest);
80
327
  console.log("");
81
- console.log("OK: loaded " + String(manifest.tools.length) + " tool(s) from " + file);
328
+ console.log("OK: loaded " + String(manifest.tools.length) + " tool(s) from " + terminalText(file));
82
329
  }
83
330
  function cmdSign(file) {
84
331
  const manifest = loadManifest(file);
@@ -88,13 +335,15 @@ function cmdSign(file) {
88
335
  writeJson(out, signed);
89
336
  ensureBesaDir();
90
337
  writeJson(ACTIVE_MANIFEST_PATH, signed);
338
+ trustSignedManifestKey(signed);
91
339
  printJson("signedManifest", signed);
92
340
  console.log("");
93
- console.log("OK: signed -> " + out + " with publicKeyId " + signed.publicKeyId);
341
+ console.log("OK: signed -> " + terminalText(out) + " with publicKeyId " + signed.publicKeyId);
342
+ console.log("OK: public key anchored in " + terminalText(selectedTrustPath()));
94
343
  }
95
344
  function cmdVerify(file) {
96
345
  const signed = readJson(file);
97
- const result = verifySignedManifest(signed);
346
+ const result = verifyTrustedSignedManifest(signed, loadTrustStore());
98
347
  printJson("verify", result);
99
348
  if (!result.valid) {
100
349
  process.exitCode = 1;
@@ -115,11 +364,14 @@ function denyFromVerification(toolName, reasonCode, detail) {
115
364
  }
116
365
  export function grantGate(toolName) {
117
366
  const grantsPath = flagValue("--grants");
367
+ const agentId = flagValue("--agent");
368
+ if (Boolean(grantsPath) !== Boolean(agentId)) {
369
+ throw new Error("--agent and --grants must be provided together");
370
+ }
118
371
  if (!grantsPath) {
119
372
  return undefined;
120
373
  }
121
- const agentId = flagValue("--agent") ?? "";
122
- const grant = checkGrant(loadGrants(grantsPath), agentId, toolName);
374
+ const grant = checkGrant(loadGrants(grantsPath), agentId ?? "", toolName);
123
375
  return {
124
376
  decision: grant.granted ? "allow" : "deny",
125
377
  reasonCode: grant.reasonCode,
@@ -129,14 +381,15 @@ export function grantGate(toolName) {
129
381
  };
130
382
  }
131
383
  function cmdAdmit(file, toolName) {
132
- const signed = readJson(file);
133
- const verified = verifySignedManifest(signed);
384
+ const raw = readJson(file);
385
+ const verified = verifyTrustedSignedManifest(raw, loadTrustStore(), "admit");
134
386
  if (!verified.valid) {
135
387
  const denied = denyFromVerification(toolName, verified.reasonCode, verified.detail);
136
388
  printJson("admission", denied);
137
389
  process.exitCode = 1;
138
390
  return;
139
391
  }
392
+ const signed = requireSignedManifest(raw);
140
393
  const grantDecision = grantGate(toolName);
141
394
  if (grantDecision && grantDecision.decision === "deny") {
142
395
  printJson("admission", grantDecision);
@@ -144,24 +397,37 @@ function cmdAdmit(file, toolName) {
144
397
  return;
145
398
  }
146
399
  const meter = loadMeter(METER_PATH);
147
- const count = getCount(meter, toolName);
148
- const decision = admit(signed.manifest, toolName, count);
400
+ const key = meterKey(signed.manifestHash, toolName);
401
+ const decision = admit(signed.manifest, toolName, getCount(meter, key));
149
402
  if (grantDecision?.agentId) {
150
403
  decision.agentId = grantDecision.agentId;
151
404
  }
152
405
  printJson("admission", decision);
406
+ console.log("");
407
+ console.log("[dry-run: budget not consumed — use 'besa receipt' to enforce and record]");
153
408
  if (decision.decision === "deny") {
154
409
  process.exitCode = 1;
155
410
  }
156
411
  }
412
+ function readRequest(toolName) {
413
+ const requestPath = flagValue("--request");
414
+ return requestPath ? readJson(requestPath) : { toolName };
415
+ }
157
416
  function cmdReceipt(toolName, file) {
158
417
  const signedPath = file ?? ACTIVE_MANIFEST_PATH;
159
418
  if (!existsSync(signedPath)) {
160
- throw new Error("no signed manifest found at " + signedPath + "; run besa sign <manifest> first");
419
+ throw new Error("no signed manifest found at " +
420
+ signedPath +
421
+ "; run besa sign <manifest> first");
161
422
  }
162
- const signed = readJson(signedPath);
163
- const keypair = loadOrCreateKeyPair();
164
- const verified = verifySignedManifest(signed);
423
+ const signed = requireSignedManifest(readJson(signedPath));
424
+ const keypair = loadExistingKeyPair();
425
+ const request = readRequest(toolName);
426
+ void hashRequest(request);
427
+ if (publicKeyId(keypair.publicKeyDer) !== signed.publicKeyId) {
428
+ throw new Error("local receipt key does not match the signed manifest publicKeyId");
429
+ }
430
+ const verified = verifyTrustedSignedManifest(signed, loadTrustStore(), "admit");
165
431
  let decision;
166
432
  let grantReasonCode;
167
433
  if (!verified.valid) {
@@ -174,14 +440,10 @@ function cmdReceipt(toolName, file) {
174
440
  decision = grantDecision;
175
441
  }
176
442
  else {
177
- const meter = loadMeter(METER_PATH);
178
- decision = admit(signed.manifest, toolName, getCount(meter, toolName));
443
+ decision = admitAndConsume(METER_PATH, signed.manifestHash, signed.manifest, toolName);
179
444
  if (grantDecision?.agentId) {
180
445
  decision.agentId = grantDecision.agentId;
181
446
  }
182
- if (decision.decision === "allow") {
183
- saveMeter(METER_PATH, increment(meter, toolName));
184
- }
185
447
  }
186
448
  }
187
449
  const receipt = createReceipt({
@@ -189,10 +451,7 @@ function cmdReceipt(toolName, file) {
189
451
  toolName,
190
452
  decision: decision.decision,
191
453
  reasonCode: decision.reasonCode,
192
- request: {
193
- toolName,
194
- signedManifest: signedPath,
195
- },
454
+ request,
196
455
  agentId: decision.agentId,
197
456
  grantReasonCode,
198
457
  }, keypair);
@@ -201,47 +460,140 @@ function cmdReceipt(toolName, file) {
201
460
  writeJson(receiptPath, receipt);
202
461
  printJson("receipt", receipt);
203
462
  console.log("");
204
- console.log(decision.decision.toUpperCase() + ": " + decision.reasonCode + " -> " + receiptPath);
463
+ console.log(decision.decision.toUpperCase() +
464
+ ": " +
465
+ decision.reasonCode +
466
+ " -> " +
467
+ receiptPath);
205
468
  if (decision.decision === "deny") {
206
469
  process.exitCode = 1;
207
470
  }
208
471
  }
472
+ function cmdVerifyReceipt(receiptFile, manifestFile) {
473
+ const signedPath = manifestFile ?? ACTIVE_MANIFEST_PATH;
474
+ if (!existsSync(signedPath)) {
475
+ throw new Error("no signed manifest found at " +
476
+ signedPath +
477
+ "; provide one or run besa sign <manifest> first");
478
+ }
479
+ const signedRaw = readJson(signedPath);
480
+ const trustStore = loadTrustStore();
481
+ const manifestVerification = verifyTrustedSignedManifest(signedRaw, trustStore);
482
+ if (!manifestVerification.valid) {
483
+ printJson("verifyReceipt", manifestVerification);
484
+ process.exitCode = 1;
485
+ return;
486
+ }
487
+ const signed = requireSignedManifest(signedRaw);
488
+ const receiptRaw = readJson(receiptFile);
489
+ const receiptValidation = validateReceipt(receiptRaw);
490
+ if (!receiptValidation.ok || !receiptValidation.receipt) {
491
+ const result = {
492
+ valid: false,
493
+ reasonCode: "E_RECEIPT_INVALID",
494
+ detail: receiptValidation.errors.join("; "),
495
+ };
496
+ printJson("verifyReceipt", result);
497
+ process.exitCode = 1;
498
+ return;
499
+ }
500
+ const receipt = receiptValidation.receipt;
501
+ if (receipt.manifestHash !== signed.manifestHash) {
502
+ const result = {
503
+ valid: false,
504
+ reasonCode: "E_RECEIPT_MANIFEST_MISMATCH",
505
+ detail: "receipt manifestHash does not match the signed manifest",
506
+ };
507
+ printJson("verifyReceipt", result);
508
+ process.exitCode = 1;
509
+ return;
510
+ }
511
+ const signatureResult = verifyReceiptDetailed(receipt, signed.publicKey);
512
+ const result = signatureResult.valid
513
+ ? checkTrustedKey(trustStore, signed.publicKey, receipt.timestamp)
514
+ : signatureResult;
515
+ printJson("verifyReceipt", result);
516
+ if (!result.valid) {
517
+ process.exitCode = 1;
518
+ return;
519
+ }
520
+ console.log("");
521
+ console.log("OK: receipt and signed manifest form a valid trust chain");
522
+ }
523
+ function readVersion() {
524
+ try {
525
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
526
+ const pkg = readJsonFile(join(packageRoot, "package.json"));
527
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
528
+ }
529
+ catch {
530
+ return "unknown";
531
+ }
532
+ }
209
533
  function usage() {
210
534
  console.log([
211
535
  "Besa - signed trust infrastructure for AI-agent tools",
212
536
  "",
213
537
  "Usage:",
214
- " besa keys",
215
- " besa load <manifest.yaml>",
216
- " besa sign <manifest.yaml>",
217
- " besa verify <manifest.signed.json>",
218
- " besa admit <manifest.signed.json> <tool-name> [--agent <agent-id> --grants <grants.yaml>]",
219
- " besa receipt <tool-name> [manifest.signed.json] [--agent <agent-id> --grants <grants.yaml>]",
538
+ " besa <command> [arguments] [options]",
539
+ " besa --help | --version",
540
+ "",
541
+ "Commands:",
542
+ " keys Show the local signing key, generating one if absent",
543
+ " keys rotate Rotate the signing key and emit a signed rotation proof",
544
+ " trust add Anchor a signed manifest's public key in a trust store",
545
+ " trust apply Apply a signed rotation proof to a trust store",
546
+ " trust revoke Revoke a public key in a trust store",
547
+ " trust list List trusted, retired, and revoked keys",
548
+ " load Load and validate a manifest (YAML or JSON)",
549
+ " sign Sign a manifest and anchor the publisher key",
550
+ " verify Verify a signed manifest against a trust store",
551
+ " admit Check whether a tool call is allowed (dry-run)",
552
+ " receipt Enforce budget and issue a signed execution receipt",
553
+ " verify-receipt Verify a receipt and its manifest trust chain",
554
+ "",
555
+ "Options:",
556
+ " --trust <file> Trust store path (default: .besa/trust.json)",
557
+ " --agent <id> Scope admission to a named agent (admit, receipt)",
558
+ " --grants <file> Grant set for agent-scoped admission (admit, receipt)",
559
+ " --request <file> Request payload hashed into the receipt (receipt)",
220
560
  "",
221
561
  "Examples:",
222
562
  " besa keys",
223
- " besa load examples/manifest.yaml",
224
563
  " besa sign examples/manifest.yaml",
564
+ " besa trust add examples/manifest.signed.json --trust consumer-trust.json",
225
565
  " besa verify examples/manifest.signed.json",
226
566
  " besa admit examples/manifest.signed.json crm.lookup",
227
567
  " besa admit examples/manifest.signed.json crm.lookup --agent agent-alpha --grants examples/grants.yaml",
228
- " besa admit examples/manifest.signed.json crm.delete --agent agent-alpha --grants examples/grants.yaml",
229
- " besa receipt crm.lookup examples/manifest.signed.json",
230
- " besa receipt crm.lookup examples/manifest.signed.json --agent agent-alpha --grants examples/grants.yaml",
568
+ " besa receipt crm.lookup examples/manifest.signed.json --request examples/request.json",
569
+ " besa verify-receipt .besa/receipts/<receipt-id>.json examples/manifest.signed.json",
570
+ "",
571
+ "Security:",
572
+ " Local developer beta. Private keys are encrypted at rest (AES-256-GCM + scrypt).",
573
+ " Never commit the .besa/ directory. Not hardened for production use yet.",
231
574
  ].join("\n"));
232
575
  }
233
- function requireArgs(args, expected, command) {
234
- if (args.length < expected) {
235
- throw new Error(command + " requires " + String(expected) + " argument(s)");
576
+ function requireArgs(args, minimum, command, maximum = minimum) {
577
+ if (args.length < minimum || args.length > maximum) {
578
+ const expected = minimum === maximum
579
+ ? String(minimum)
580
+ : `${String(minimum)}-${String(maximum)}`;
581
+ throw new Error(`${command} requires ${expected} argument(s), received ${String(args.length)}`);
236
582
  }
237
583
  }
238
584
  function main(argv) {
239
585
  const command = argv[0] ?? "";
240
- const args = positionals(argv.slice(1));
241
586
  try {
587
+ const allowedFlags = COMMAND_FLAGS[command] ?? new Set();
588
+ const args = positionals(argv.slice(1), allowedFlags);
242
589
  switch (command) {
243
590
  case "keys":
244
- cmdKeys();
591
+ requireArgs(args, 0, command, 1);
592
+ cmdKeys(args[0]);
593
+ break;
594
+ case "trust":
595
+ requireArgs(args, 1, command, 2);
596
+ cmdTrust(args[0] ?? "", args[1]);
245
597
  break;
246
598
  case "load":
247
599
  requireArgs(args, 1, command);
@@ -260,9 +612,18 @@ function main(argv) {
260
612
  cmdAdmit(args[0] ?? "", args[1] ?? "");
261
613
  break;
262
614
  case "receipt":
263
- requireArgs(args, 1, command);
615
+ requireArgs(args, 1, command, 2);
264
616
  cmdReceipt(args[0] ?? "", args[1]);
265
617
  break;
618
+ case "verify-receipt":
619
+ requireArgs(args, 1, command, 2);
620
+ cmdVerifyReceipt(args[0] ?? "", args[1]);
621
+ break;
622
+ case "version":
623
+ case "--version":
624
+ case "-v":
625
+ console.log("besa " + readVersion());
626
+ break;
266
627
  case "":
267
628
  case "help":
268
629
  case "--help":
@@ -270,7 +631,7 @@ function main(argv) {
270
631
  usage();
271
632
  break;
272
633
  default:
273
- console.error("Unknown command: " + command);
634
+ console.error("Unknown command: " + terminalText(command));
274
635
  usage();
275
636
  process.exitCode = 1;
276
637
  break;
@@ -278,7 +639,7 @@ function main(argv) {
278
639
  }
279
640
  catch (error) {
280
641
  const message = error instanceof Error ? error.message : String(error);
281
- console.error("Error: " + message);
642
+ console.error("Error: " + terminalText(message));
282
643
  process.exitCode = 1;
283
644
  }
284
645
  }
package/dist/io.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare const MAX_ARTIFACT_BYTES = 1048576;
2
+ export declare function readUtf8File(path: string, maximumBytes?: number): string;
3
+ export declare function readJsonFile(path: string): unknown;
4
+ export declare function writeJsonAtomic(path: string, value: unknown, mode?: number): void;
5
+ export declare function writeJsonExclusive(path: string, value: unknown, mode?: number): void;