@aarmos/avar-core 1.0.0 → 1.2.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/README.md CHANGED
@@ -53,4 +53,4 @@ Apache-2.0. See [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE).
53
53
 
54
54
  ## Spec revision
55
55
 
56
- Tracks `avar/1` spec revision `1.0`. See the [AVAR spec](https://github.com/Aarmatix/avar-spec/blob/main/SPEC.md) for the current revision and changelog.
56
+ Tracks `avar/1` spec revision `1.1` (release candidate; backward-compatible with `1.0-rc1`). See the [AVAR spec](https://github.com/Aarmatix/avar-spec/blob/main/SPEC.md) for the current revision and changelog.
package/dist/index.d.ts CHANGED
@@ -194,4 +194,43 @@ declare function signedBodyOf(entry: AvarEntry): Omit<AvarEntry, "signature" | "
194
194
 
195
195
  declare function verifyBundle(bundle: AvarBundle): Promise<VerificationReport>;
196
196
 
197
- export { type AvarBundle, type AvarEntry, type BundleManifest, type BundlePubKeys, type DecisionStep, GENESIS_PREV_HASH, GENESIS_PREV_STEP_HASH, type PolicyHit, type TextStep, type ToolStep, type TraceStep, type VerificationIssue, type VerificationReport, canonicalize, computeDeviceFingerprint, computeEntryHash, computeStepHash, sha256Hex, signedBodyOf, utf8, verifyBundle, verifySignature };
197
+ type AarmosErrorCode = "BUNDLE_OVERSIZED" | "BUNDLE_NOT_ZIP" | "BUNDLE_MISSING_FILES" | "BUNDLE_INVALID_JSON" | "BUNDLE_INVALID_NDJSON" | "BUNDLE_SPEC_UNSUPPORTED" | "BUNDLE_EMPTY" | "SPEC_VERSION_MISMATCH" | "ENTRIES_SHA256_MISMATCH" | "SIGNATURE_MISMATCH" | "FINGERPRINT_MISMATCH" | "CHAIN_BROKEN" | "STEP_CHAIN_BROKEN" | "MANIFEST_INVALID" | "FILE_NOT_FOUND" | "FILE_UNREADABLE" | "UNKNOWN";
198
+ interface AarmosError {
199
+ code: AarmosErrorCode;
200
+ message: string;
201
+ hint: string;
202
+ docsUrl: string;
203
+ }
204
+ declare const AARMOS_ERROR_TEMPLATES: Record<AarmosErrorCode, Omit<AarmosError, "code">>;
205
+ declare function aarmosError(code: AarmosErrorCode, overrides?: Partial<Omit<AarmosError, "code">>): AarmosError;
206
+ /** One-line human-readable format used by the CLI on stderr / bridge diagnostics. */
207
+ declare function formatAarmosErrorLine(err: AarmosError): string;
208
+ /**
209
+ * Report-level classifier. Given an invalid VerificationReport, return the
210
+ * single most-important AarmosError to surface. Priority reflects severity:
211
+ * envelope > signature > fingerprint > chain > step-chain > format.
212
+ */
213
+ declare function classifyReport(report: {
214
+ formatOk: boolean;
215
+ entriesSha256Ok: boolean;
216
+ signaturesOk: boolean;
217
+ fingerprintsOk: boolean;
218
+ chainOk: boolean;
219
+ perStepChainOk: boolean;
220
+ verdict: "valid" | "invalid" | "valid-with-warnings";
221
+ issues: {
222
+ kind: string;
223
+ detail?: string;
224
+ }[];
225
+ }): AarmosError | null;
226
+ /**
227
+ * Optional Error subclass so throw/catch sites can propagate an AarmosError
228
+ * through code that expects `Error`. Consumers can detect via
229
+ * `err instanceof AarmosErrorException` or `err && (err as any).aarmos`.
230
+ */
231
+ declare class AarmosErrorException extends Error {
232
+ readonly aarmos: AarmosError;
233
+ constructor(err: AarmosError);
234
+ }
235
+
236
+ export { AARMOS_ERROR_TEMPLATES, type AarmosError, type AarmosErrorCode, AarmosErrorException, type AvarBundle, type AvarEntry, type BundleManifest, type BundlePubKeys, type DecisionStep, GENESIS_PREV_HASH, GENESIS_PREV_STEP_HASH, type PolicyHit, type TextStep, type ToolStep, type TraceStep, type VerificationIssue, type VerificationReport, aarmosError, canonicalize, classifyReport, computeDeviceFingerprint, computeEntryHash, computeStepHash, formatAarmosErrorLine, sha256Hex, signedBodyOf, utf8, verifyBundle, verifySignature };
package/dist/index.js CHANGED
@@ -294,13 +294,313 @@ function computeChainHead(entries) {
294
294
  }
295
295
  return { entryHash: "", index: -1 };
296
296
  }
297
+
298
+ // src/errors.ts
299
+ var SPEC_URL = "https://aarmos.io/docs/avar-spec";
300
+ var VERIFY_URL = "https://aarmos.io/trust/verify";
301
+ var QUICKSTART_URL = "https://aarmos.io/docs/quickstart";
302
+ var AARMOS_ERROR_TEMPLATES = {
303
+ BUNDLE_OVERSIZED: {
304
+ message: "That bundle is larger than the 25 MB browser limit.",
305
+ hint: "Run `aarmos verify <bundle>` locally instead \u2014 the CLI has no size cap.",
306
+ docsUrl: QUICKSTART_URL
307
+ },
308
+ BUNDLE_NOT_ZIP: {
309
+ message: "That file is not a valid .avar.zip.",
310
+ hint: "Bundles are zip archives. Rename or re-export the file, then try again.",
311
+ docsUrl: SPEC_URL
312
+ },
313
+ BUNDLE_MISSING_FILES: {
314
+ message: "Bundle is missing one or more required files.",
315
+ hint: "A valid AVAR bundle contains SPEC-VERSION, manifest.json, entries.ndjson, and pubkeys.json.",
316
+ docsUrl: SPEC_URL
317
+ },
318
+ BUNDLE_INVALID_JSON: {
319
+ message: "manifest.json or pubkeys.json is not valid JSON.",
320
+ hint: "The file exists but cannot be parsed. Re-export from the CLI to regenerate a clean bundle.",
321
+ docsUrl: SPEC_URL
322
+ },
323
+ BUNDLE_INVALID_NDJSON: {
324
+ message: "entries.ndjson contains a malformed line.",
325
+ hint: "Each line must be a self-contained JSON entry. One corrupt line usually means the file was edited by hand.",
326
+ docsUrl: SPEC_URL
327
+ },
328
+ BUNDLE_SPEC_UNSUPPORTED: {
329
+ message: "This bundle was produced by a spec version this verifier does not support.",
330
+ hint: "Update to the latest verifier (`npm i -g @aarmos/avar-core`) or re-export with a matching spec version.",
331
+ docsUrl: SPEC_URL
332
+ },
333
+ BUNDLE_EMPTY: {
334
+ message: "Bundle contains no entries.",
335
+ hint: "An empty ledger is unusual \u2014 check that the run actually produced receipts before exporting.",
336
+ docsUrl: SPEC_URL
337
+ },
338
+ SPEC_VERSION_MISMATCH: {
339
+ message: "This bundle uses a spec version this verifier does not recognize.",
340
+ hint: "Update the verifier (`npm i -g @aarmos/avar-core`) or re-export the bundle with matching spec version.",
341
+ docsUrl: SPEC_URL
342
+ },
343
+ ENTRIES_SHA256_MISMATCH: {
344
+ message: "entries.ndjson has been modified since the bundle was signed.",
345
+ hint: "The envelope hash in manifest.json no longer matches the file. Do not trust this bundle \u2014 re-export from the CLI.",
346
+ docsUrl: VERIFY_URL
347
+ },
348
+ SIGNATURE_MISMATCH: {
349
+ message: "One or more entry signatures do not match the embedded public keys.",
350
+ hint: "The bundle was modified after signing, or the wrong pubkeys.json is bundled.",
351
+ docsUrl: VERIFY_URL
352
+ },
353
+ FINGERPRINT_MISMATCH: {
354
+ message: "Device fingerprint on one or more entries does not match its public key.",
355
+ hint: "The entry claims a device whose public key does not hash to the declared fingerprint. Treat this bundle as untrusted.",
356
+ docsUrl: VERIFY_URL
357
+ },
358
+ CHAIN_BROKEN: {
359
+ message: "The per-entry hash chain is broken.",
360
+ hint: "An entry was inserted, removed, or edited after signing. The break index is shown in the issues list.",
361
+ docsUrl: VERIFY_URL
362
+ },
363
+ STEP_CHAIN_BROKEN: {
364
+ message: "The per-step hash chain inside one or more entries is broken.",
365
+ hint: "A decision or tool step was tampered with. The affected entry index is in the issues list.",
366
+ docsUrl: VERIFY_URL
367
+ },
368
+ MANIFEST_INVALID: {
369
+ message: "manifest.json is missing required fields or uses an unsupported format.",
370
+ hint: "Re-export the bundle from the CLI \u2014 do not edit manifest.json by hand.",
371
+ docsUrl: SPEC_URL
372
+ },
373
+ FILE_NOT_FOUND: {
374
+ message: "Receipt not found at the given path.",
375
+ hint: "Check the path \u2014 a common cause is running from a different working directory than expected.",
376
+ docsUrl: QUICKSTART_URL
377
+ },
378
+ FILE_UNREADABLE: {
379
+ message: "Receipt exists but could not be read.",
380
+ hint: "Check filesystem permissions on the file.",
381
+ docsUrl: QUICKSTART_URL
382
+ },
383
+ UNKNOWN: {
384
+ message: "Something went wrong while verifying that bundle.",
385
+ hint: "Try again with a fresh export. If the error persists, run `aarmos verify` locally to see the raw error.",
386
+ docsUrl: QUICKSTART_URL
387
+ }
388
+ };
389
+ function aarmosError(code, overrides = {}) {
390
+ const base = AARMOS_ERROR_TEMPLATES[code];
391
+ return {
392
+ code,
393
+ message: overrides.message ?? base.message,
394
+ hint: overrides.hint ?? base.hint,
395
+ docsUrl: overrides.docsUrl ?? base.docsUrl
396
+ };
397
+ }
398
+ function formatAarmosErrorLine(err) {
399
+ return `\u2717 ${err.code}: ${err.message}
400
+ Next: ${err.hint}
401
+ Docs: ${err.docsUrl}`;
402
+ }
403
+ function classifyReport(report) {
404
+ if (report.verdict !== "invalid") return null;
405
+ if (!report.entriesSha256Ok) return aarmosError("ENTRIES_SHA256_MISMATCH");
406
+ if (!report.signaturesOk) return aarmosError("SIGNATURE_MISMATCH");
407
+ if (!report.fingerprintsOk) return aarmosError("FINGERPRINT_MISMATCH");
408
+ if (!report.chainOk) return aarmosError("CHAIN_BROKEN");
409
+ if (!report.perStepChainOk) return aarmosError("STEP_CHAIN_BROKEN");
410
+ if (!report.formatOk) {
411
+ const specIssue = report.issues.find((i) => i.kind === "spec-version-mismatch");
412
+ if (specIssue) {
413
+ return aarmosError("SPEC_VERSION_MISMATCH", {
414
+ message: specIssue.detail ? `Spec mismatch \u2014 ${specIssue.detail}` : void 0
415
+ });
416
+ }
417
+ return aarmosError("MANIFEST_INVALID");
418
+ }
419
+ return aarmosError("UNKNOWN");
420
+ }
421
+ var AarmosErrorException = class extends Error {
422
+ aarmos;
423
+ constructor(err) {
424
+ super(`${err.code}: ${err.message}`);
425
+ this.name = "AarmosErrorException";
426
+ this.aarmos = err;
427
+ }
428
+ };
429
+
430
+ // src/diff.ts
431
+ function isPlainObject(v) {
432
+ return typeof v === "object" && v !== null && !Array.isArray(v);
433
+ }
434
+ function escapePointer(seg) {
435
+ return seg.replace(/~/g, "~0").replace(/\//g, "~1");
436
+ }
437
+ function join(base, seg) {
438
+ return `${base}/${typeof seg === "number" ? seg : escapePointer(seg)}`;
439
+ }
440
+ function diffCanonical(a, b) {
441
+ const ops = [];
442
+ walk(a, b, "", ops);
443
+ return { equal: ops.length === 0, ops };
444
+ }
445
+ function walk(a, b, path, ops) {
446
+ if (a === b) return;
447
+ if ((a === null || typeof a !== "object") && (b === null || typeof b !== "object")) {
448
+ if (canonicalize(a) !== canonicalize(b)) {
449
+ ops.push({ op: "replace", path: path || "/", oldValue: a, value: b });
450
+ }
451
+ return;
452
+ }
453
+ if (Array.isArray(a) && Array.isArray(b)) {
454
+ const min = Math.min(a.length, b.length);
455
+ for (let i = 0; i < min; i++) walk(a[i], b[i], join(path, i), ops);
456
+ if (b.length > a.length) {
457
+ for (let i = a.length; i < b.length; i++) {
458
+ ops.push({ op: "add", path: join(path, i), value: b[i] });
459
+ }
460
+ } else if (a.length > b.length) {
461
+ for (let i = b.length; i < a.length; i++) {
462
+ ops.push({ op: "remove", path: join(path, i), oldValue: a[i] });
463
+ }
464
+ }
465
+ return;
466
+ }
467
+ if (isPlainObject(a) && isPlainObject(b)) {
468
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
469
+ const sorted = [...keys].sort();
470
+ for (const k of sorted) {
471
+ const inA = Object.prototype.hasOwnProperty.call(a, k);
472
+ const inB = Object.prototype.hasOwnProperty.call(b, k);
473
+ if (inA && !inB) ops.push({ op: "remove", path: join(path, k), oldValue: a[k] });
474
+ else if (!inA && inB) ops.push({ op: "add", path: join(path, k), value: b[k] });
475
+ else walk(a[k], b[k], join(path, k), ops);
476
+ }
477
+ return;
478
+ }
479
+ ops.push({ op: "replace", path: path || "/", oldValue: a, value: b });
480
+ }
481
+ function isBundle(v) {
482
+ return isPlainObject(v) && typeof v.specVersion === "string" && isPlainObject(v.manifest) && Array.isArray(v.entries);
483
+ }
484
+ function isEntry(v) {
485
+ return isPlainObject(v) && typeof v.id === "string" && Array.isArray(v.steps);
486
+ }
487
+ function diffEntryPair(a, b, indexA, indexB) {
488
+ const { entriesNdjsonBytes: _skip1, ...aClean } = a;
489
+ const { entriesNdjsonBytes: _skip2, ...bClean } = b;
490
+ void _skip1;
491
+ void _skip2;
492
+ const { ops } = diffCanonical(aClean, bClean);
493
+ const signatureChanged = ops.some((o) => o.path === "/signature" || o.path.startsWith("/signature/"));
494
+ const entryHashChanged = ops.some((o) => o.path === "/entryHash");
495
+ const stepsChanged = ops.some((o) => o.path === "/steps" || o.path.startsWith("/steps/"));
496
+ return {
497
+ id: a.id,
498
+ indexA,
499
+ indexB,
500
+ ops,
501
+ signatureChanged,
502
+ entryHashChanged,
503
+ stepsChanged
504
+ };
505
+ }
506
+ function diffReceipts(a, b) {
507
+ if (isEntry(a) && isEntry(b)) {
508
+ const change = diffEntryPair(a, b, 0, 0);
509
+ return {
510
+ equal: change.ops.length === 0,
511
+ kind: "entry",
512
+ specVersion: null,
513
+ chainHead: null,
514
+ devicePublicKeys: { added: [], removed: [] },
515
+ entries: {
516
+ added: [],
517
+ removed: [],
518
+ modified: change.ops.length === 0 ? [] : [change],
519
+ unchanged: change.ops.length === 0 ? 1 : 0
520
+ },
521
+ ops: change.ops
522
+ };
523
+ }
524
+ if (!isBundle(a) || !isBundle(b)) {
525
+ throw new Error("diffReceipts: inputs must be AvarBundle or AvarEntry values.");
526
+ }
527
+ const specVersion = a.specVersion === b.specVersion ? null : { from: a.specVersion, to: b.specVersion };
528
+ const headA = a.manifest.chainHead;
529
+ const headB = b.manifest.chainHead;
530
+ const chainHead = headA.entryHash === headB.entryHash && headA.index === headB.index ? null : {
531
+ from: headA,
532
+ to: headB,
533
+ extended: headB.index > headA.index
534
+ };
535
+ const keysA = new Set(a.manifest.devicePublicKeys ?? []);
536
+ const keysB = new Set(b.manifest.devicePublicKeys ?? []);
537
+ const devicePublicKeys = {
538
+ added: [...keysB].filter((k) => !keysA.has(k)).sort(),
539
+ removed: [...keysA].filter((k) => !keysB.has(k)).sort()
540
+ };
541
+ const byIdA = /* @__PURE__ */ new Map();
542
+ a.entries.forEach((e, i) => byIdA.set(e.id, { entry: e, index: i }));
543
+ const byIdB = /* @__PURE__ */ new Map();
544
+ b.entries.forEach((e, i) => byIdB.set(e.id, { entry: e, index: i }));
545
+ const added = [];
546
+ const removed = [];
547
+ const modified = [];
548
+ let unchanged = 0;
549
+ for (const [id, { entry, index }] of byIdB) {
550
+ const prior = byIdA.get(id);
551
+ if (!prior) {
552
+ added.push({ id, index });
553
+ continue;
554
+ }
555
+ const change = diffEntryPair(prior.entry, entry, prior.index, index);
556
+ if (change.ops.length === 0) unchanged++;
557
+ else modified.push(change);
558
+ }
559
+ for (const [id, { index }] of byIdA) {
560
+ if (!byIdB.has(id)) removed.push({ id, index });
561
+ }
562
+ added.sort((x, y) => x.index - y.index);
563
+ removed.sort((x, y) => x.index - y.index);
564
+ modified.sort((x, y) => x.indexA - y.indexA);
565
+ const strip = (bundle) => ({
566
+ specVersion: bundle.specVersion,
567
+ manifest: bundle.manifest,
568
+ pubkeys: bundle.pubkeys
569
+ });
570
+ const headerDiff = diffCanonical(strip(a), strip(b));
571
+ const equal = added.length === 0 && removed.length === 0 && modified.length === 0 && headerDiff.equal;
572
+ return {
573
+ equal,
574
+ kind: "bundle",
575
+ specVersion,
576
+ chainHead,
577
+ devicePublicKeys,
578
+ entries: { added, removed, modified, unchanged },
579
+ ops: headerDiff.ops
580
+ };
581
+ }
582
+ function diffPolicies(a, b) {
583
+ return diffCanonical(a, b);
584
+ }
585
+ function diffToolManifests(a, b) {
586
+ return diffCanonical(a, b);
587
+ }
297
588
  export {
589
+ AARMOS_ERROR_TEMPLATES,
590
+ AarmosErrorException,
298
591
  GENESIS_PREV_HASH,
299
592
  GENESIS_PREV_STEP_HASH,
593
+ aarmosError,
300
594
  canonicalize,
595
+ classifyReport,
301
596
  computeDeviceFingerprint,
302
597
  computeEntryHash,
303
598
  computeStepHash,
599
+ diffCanonical,
600
+ diffPolicies,
601
+ diffReceipts,
602
+ diffToolManifests,
603
+ formatAarmosErrorLine,
304
604
  sha256Hex,
305
605
  signedBodyOf,
306
606
  utf8,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarmos/avar-core",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Reference implementation of the AVAR (Aarmos Verifiable Action Record) spec — canonical JSON, Ed25519 signature verification, hash-chain math, and bundle verification. Pure, zero-dep, browser + Node.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",