@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 +1 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +300 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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.
|
|
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",
|