@mikeargento/bitgraph-player 0.8.1 → 0.10.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 +3 -24
- package/dist/__tests__/check.test.js +1 -1
- package/dist/check.d.ts +2 -45
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +12 -93
- package/dist/check.js.map +1 -1
- package/dist/cli.js +11 -185
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/verdict.d.ts +1 -1
- package/dist/verdict.d.ts.map +1 -1
- package/dist/verdict.js +1 -1
- package/dist/verdict.js.map +1 -1
- package/dist-web/verify.html +5 -5
- package/package.json +2 -3
- package/src/__tests__/check.test.ts +1 -1
- package/src/check.ts +12 -144
- package/src/cli.ts +11 -189
- package/src/index.ts +1 -16
- package/src/verdict.ts +1 -1
- package/DOMAIN.md +0 -119
- package/dist/__tests__/domain.test.d.ts +0 -2
- package/dist/__tests__/domain.test.d.ts.map +0 -1
- package/dist/__tests__/domain.test.js +0 -265
- package/dist/__tests__/domain.test.js.map +0 -1
- package/dist/domain.d.ts +0 -64
- package/dist/domain.d.ts.map +0 -1
- package/dist/domain.js +0 -212
- package/dist/domain.js.map +0 -1
- package/dist/pin.d.ts +0 -55
- package/dist/pin.d.ts.map +0 -1
- package/dist/pin.js +0 -126
- package/dist/pin.js.map +0 -1
- package/src/__tests__/domain.test.ts +0 -321
- package/src/domain.ts +0 -245
- package/src/pin.ts +0 -155
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikeargento/bitgraph-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Deterministic evaluation of causal rules over BitGraph proof bundles: a pure function from (rule, verified evidence) to a reproducible verdict.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
"dist-web",
|
|
21
21
|
"src",
|
|
22
22
|
"SPEC.md",
|
|
23
|
-
|
|
24
|
-
"README.md",
|
|
23
|
+
"README.md",
|
|
25
24
|
"LICENSE"
|
|
26
25
|
],
|
|
27
26
|
"scripts": {
|
|
@@ -147,7 +147,7 @@ describe("check: real export fixture (disk)", () => {
|
|
|
147
147
|
it("recognizes the enclave v5 measurement from PINS.md", () => {
|
|
148
148
|
const enclave = report.recordings[0]!.lines.find((l) => l.name === "enclave")!;
|
|
149
149
|
assert.match(enclave.detail, /enclave v5/);
|
|
150
|
-
assert.equal(KNOWN_ENCLAVE_MEASUREMENTS.length,
|
|
150
|
+
assert.equal(KNOWN_ENCLAVE_MEASUREMENTS.length, 5);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
it("serializes deterministically: no timestamps, no machine paths", async () => {
|
package/src/check.ts
CHANGED
|
@@ -63,11 +63,10 @@ import type {
|
|
|
63
63
|
SegmentBound,
|
|
64
64
|
TemporalSegment,
|
|
65
65
|
} from "@mikeargento/bitgraph-audit";
|
|
66
|
-
import { auditIngest, AUDIT_VERSION,
|
|
66
|
+
import { auditIngest, AUDIT_VERSION, streamArtifactsByHash } from "@mikeargento/bitgraph-audit";
|
|
67
67
|
import { readFuseAttribution, verifyFuse, base64ToBytes, bytesToHex, resetEpochLinkState } from "@mikeargento/bitgraph-verify";
|
|
68
68
|
import type { FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
69
69
|
import type { ThreeValued } from "./types.js";
|
|
70
|
-
import { SIG_EVIDENCE_MAX_BYTES } from "./types.js";
|
|
71
70
|
import { kleeneAll } from "./logic.js";
|
|
72
71
|
import { PLAYER_VERSION } from "./verdict.js";
|
|
73
72
|
|
|
@@ -100,7 +99,12 @@ export const KNOWN_ENCLAVE_MEASUREMENTS: ReadonlyArray<{ pcr0: string; label: st
|
|
|
100
99
|
{
|
|
101
100
|
pcr0: "6483cedffed74680ffb287507744a398b288c3fb943eb3f2e4fe889f8b60b3d575ad8942350360b69a1bd7bf713df27f",
|
|
102
101
|
label: "enclave v5 (reproducible)",
|
|
103
|
-
period: "2026-07-29
|
|
102
|
+
period: "2026-07-29 to 2026-09-05",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
pcr0: "cd8ba52d340fb1be78610b59953ded2ceca23be1cfcc7ab504a26b8fdcd7ba92090f49e28a32d008df046ec4212f77bf",
|
|
106
|
+
label: "enclave v6 (reproducible)",
|
|
107
|
+
period: "2026-09-05 onward",
|
|
104
108
|
},
|
|
105
109
|
];
|
|
106
110
|
|
|
@@ -110,7 +114,7 @@ export const KNOWN_ENCLAVE_MEASUREMENTS: ReadonlyArray<{ pcr0: string; label: st
|
|
|
110
114
|
|
|
111
115
|
/** One checked property, three-valued, with a plain-language reason. */
|
|
112
116
|
export interface CheckLine {
|
|
113
|
-
name: "file" | "signature" | "attestation" | "enclave" | "witness" | "contradiction" | "
|
|
117
|
+
name: "file" | "signature" | "attestation" | "enclave" | "witness" | "contradiction" | "fused";
|
|
114
118
|
result: ThreeValued;
|
|
115
119
|
detail: string;
|
|
116
120
|
}
|
|
@@ -197,10 +201,7 @@ export interface CheckAnchor {
|
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
export interface CheckReport {
|
|
200
|
-
|
|
201
|
-
check: "bitgraph-check/1" | "bitgraph-check/2";
|
|
202
|
-
/** The pinned domain this report was checked against, when one was. */
|
|
203
|
-
from?: { domain: string; party: string };
|
|
204
|
+
check: "bitgraph-check/1";
|
|
204
205
|
result: ThreeValued;
|
|
205
206
|
/** One-sentence plain-language conclusion, deterministic. */
|
|
206
207
|
summary: string;
|
|
@@ -216,31 +217,6 @@ export interface CheckReport {
|
|
|
216
217
|
network: "none";
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
/**
|
|
220
|
-
* A pinned BitGraph Domain, as the report builder consults it: one line
|
|
221
|
-
* per recording, TRUE or UNDETERMINED, never FALSE (absence of domain
|
|
222
|
-
* evidence contradicts nothing; the open-world rule of SPEC §9.3).
|
|
223
|
-
*
|
|
224
|
-
* An interface here rather than an import from domain.ts, deliberately:
|
|
225
|
-
* this module is also built into the browser verifier, and the crypto the
|
|
226
|
-
* adapter needs (key decoding, signature verification) stays behind it.
|
|
227
|
-
* domain.ts's `checkDomain(file)` is the implementation; embedders may
|
|
228
|
-
* supply their own.
|
|
229
|
-
*/
|
|
230
|
-
export interface CheckDomain {
|
|
231
|
-
domain: string;
|
|
232
|
-
party: string;
|
|
233
|
-
keyCount: number;
|
|
234
|
-
/** Key name for an actor keyId (es256 fingerprint match), if published. */
|
|
235
|
-
actorKeyName(keyId: string): string | undefined;
|
|
236
|
-
/**
|
|
237
|
-
* Key name of the first pinned key a candidate bitgraph-sig/1 file
|
|
238
|
-
* verifies under, over this digest (lowercase hex). Deterministic:
|
|
239
|
-
* candidates ascending by content hash, keys in name order.
|
|
240
|
-
*/
|
|
241
|
-
signatureKeyName(targetSha256Hex: string, evidence: ReadonlyMap<string, Uint8Array>): string | undefined;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
220
|
export interface CheckOptions {
|
|
245
221
|
/**
|
|
246
222
|
* False when the environment cannot run the attestation's ECDSA P-384
|
|
@@ -248,19 +224,6 @@ export interface CheckOptions {
|
|
|
248
224
|
* with that reason instead of a false FALSE. Defaults to true.
|
|
249
225
|
*/
|
|
250
226
|
webCryptoAvailable?: boolean;
|
|
251
|
-
/**
|
|
252
|
-
* Check against a pinned domain: adds one "domain" line per recording
|
|
253
|
-
* and stamps the report bitgraph-check/2. The check itself stays
|
|
254
|
-
* offline; the pin was the one fetch, and it already happened.
|
|
255
|
-
*/
|
|
256
|
-
from?: CheckDomain;
|
|
257
|
-
/**
|
|
258
|
-
* Candidate signature bytes by content sha256 hex, for the domain
|
|
259
|
-
* line's detached-signature path (SPEC §9.4 discipline). checkIngest
|
|
260
|
-
* collects this from the bundle's matched artifacts when absent;
|
|
261
|
-
* embedders may supply additional candidates.
|
|
262
|
-
*/
|
|
263
|
-
sigEvidence?: ReadonlyMap<string, Uint8Array>;
|
|
264
227
|
/**
|
|
265
228
|
* Fuse evidence by proofHash: the verifyFuse result over the fused bytes
|
|
266
229
|
* (preferred) or the original, whichever the bundle holds. checkIngest
|
|
@@ -300,17 +263,6 @@ const EXCERPT_NORMAL_CODES: ReadonlySet<string> = new Set([
|
|
|
300
263
|
*/
|
|
301
264
|
export async function checkIngest(ingest: IngestResult, options?: CheckOptions): Promise<CheckReport> {
|
|
302
265
|
let opts = options;
|
|
303
|
-
// Domain checking's detached-signature path wants the same candidate set
|
|
304
|
-
// the evaluator uses (SPEC §9.4: matched artifacts, size-capped). Collect
|
|
305
|
-
// it here, where the ingest is in hand, unless the embedder supplied one.
|
|
306
|
-
if (opts?.from !== undefined && opts.sigEvidence === undefined) {
|
|
307
|
-
const collected = new Map<string, Uint8Array>();
|
|
308
|
-
for await (const matched of streamMatchedArtifacts(ingest)) {
|
|
309
|
-
if (matched.bytes.length > SIG_EVIDENCE_MAX_BYTES) continue;
|
|
310
|
-
if (!collected.has(matched.sha256Hex)) collected.set(matched.sha256Hex, matched.bytes);
|
|
311
|
-
}
|
|
312
|
-
opts = { ...opts, sigEvidence: collected };
|
|
313
|
-
}
|
|
314
266
|
const audit = await auditIngest(ingest, { startedAt: "" });
|
|
315
267
|
if (opts?.fuseEvidence === undefined) {
|
|
316
268
|
const collected = await collectFuseEvidence(ingest);
|
|
@@ -405,8 +357,6 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
405
357
|
segmentByHash.get(proof.proofHash),
|
|
406
358
|
artifactPathByProof.get(proof.proofHash),
|
|
407
359
|
webCrypto,
|
|
408
|
-
options?.from,
|
|
409
|
-
options?.sigEvidence,
|
|
410
360
|
options?.fuseEvidence,
|
|
411
361
|
anchorByHash
|
|
412
362
|
)
|
|
@@ -418,7 +368,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
418
368
|
sortByPosition(anchors);
|
|
419
369
|
const contradictions = collectContradictions(audit);
|
|
420
370
|
const notes = collectNotes(audit, recordings, anchors, options?.fuseOriginHexes);
|
|
421
|
-
const notChecked = collectNotChecked(anchors.length > 0
|
|
371
|
+
const notChecked = collectNotChecked(anchors.length > 0);
|
|
422
372
|
|
|
423
373
|
const allLines: ThreeValued[] = [
|
|
424
374
|
...recordings.map((r) => r.result),
|
|
@@ -428,10 +378,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
428
378
|
const result: ThreeValued = allLines.length === 0 ? "UNDETERMINED" : kleeneAll(allLines);
|
|
429
379
|
|
|
430
380
|
return {
|
|
431
|
-
check:
|
|
432
|
-
...(options?.from !== undefined
|
|
433
|
-
? { from: { domain: options.from.domain, party: options.from.party } }
|
|
434
|
-
: {}),
|
|
381
|
+
check: "bitgraph-check/1",
|
|
435
382
|
result,
|
|
436
383
|
summary: summarize(result, recordings, anchors, contradictions),
|
|
437
384
|
recordings,
|
|
@@ -474,8 +421,6 @@ function buildRecording(
|
|
|
474
421
|
segment: TemporalSegment | undefined,
|
|
475
422
|
filePath: string | undefined,
|
|
476
423
|
webCrypto: boolean,
|
|
477
|
-
from?: CheckDomain,
|
|
478
|
-
sigEvidence?: ReadonlyMap<string, Uint8Array>,
|
|
479
424
|
fuseEvidence?: ReadonlyMap<string, FuseVerifyResult>,
|
|
480
425
|
anchorByHash?: ReadonlyMap<string, AnchorRecord>
|
|
481
426
|
): CheckRecording {
|
|
@@ -529,18 +474,6 @@ function buildRecording(
|
|
|
529
474
|
// enclave: the attested PCR0 is a published BitGraph measurement.
|
|
530
475
|
lines.push(enclaveLine(att.attestedPcr0, att.line.result));
|
|
531
476
|
|
|
532
|
-
// domain: a key the pinned domain published stands behind this
|
|
533
|
-
// recording. TRUE or UNDETERMINED, never FALSE: domain evidence can
|
|
534
|
-
// exist outside any bundle, so its absence contradicts nothing
|
|
535
|
-
// (SPEC §9.3's open-world rule); tampering already reads FALSE on the
|
|
536
|
-
// lines above, and this line does not restate them.
|
|
537
|
-
if (from !== undefined) {
|
|
538
|
-
// Verified exactly when the signature line above reads TRUE: the
|
|
539
|
-
// integrity-tier "artifact-unavailable" status is a PASS (proof-only
|
|
540
|
-
// bundles verify; whether bytes are in hand is the file line's job).
|
|
541
|
-
const verified = v !== undefined && v.status !== "failed";
|
|
542
|
-
lines.push(domainLine(proof, verified, from, sigEvidence ?? EMPTY_EVIDENCE));
|
|
543
|
-
}
|
|
544
477
|
|
|
545
478
|
// fused: the proof's signed attribution marks a fused artifact (profile
|
|
546
479
|
// bitgraph-fuse/1). The commitment check runs over the fused bytes, or
|
|
@@ -741,63 +674,6 @@ function attestationLine(
|
|
|
741
674
|
};
|
|
742
675
|
}
|
|
743
676
|
|
|
744
|
-
const EMPTY_EVIDENCE: ReadonlyMap<string, Uint8Array> = new Map();
|
|
745
|
-
|
|
746
|
-
function domainLine(
|
|
747
|
-
proof: ObservedProof,
|
|
748
|
-
verified: boolean,
|
|
749
|
-
from: CheckDomain,
|
|
750
|
-
sigEvidence: ReadonlyMap<string, Uint8Array>
|
|
751
|
-
): CheckLine {
|
|
752
|
-
if (!verified) {
|
|
753
|
-
return {
|
|
754
|
-
name: "domain",
|
|
755
|
-
result: "UNDETERMINED",
|
|
756
|
-
detail: `the recording is not verified here, so nothing binds it to ${from.domain}`,
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
const actorKeyId = proof.proof.agency?.actor?.keyId;
|
|
760
|
-
if (actorKeyId !== undefined) {
|
|
761
|
-
const name = from.actorKeyName(actorKeyId);
|
|
762
|
-
if (name !== undefined) {
|
|
763
|
-
return {
|
|
764
|
-
name: "domain",
|
|
765
|
-
result: "TRUE",
|
|
766
|
-
detail: `actor key "${name}" · published by ${from.domain} (${from.party})`,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
const targetBytes = decodeDigestB64(proof.proof.artifact.digestB64);
|
|
771
|
-
if (targetBytes !== undefined && sigEvidence.size > 0) {
|
|
772
|
-
const name = from.signatureKeyName(targetBytes.toString("hex"), sigEvidence);
|
|
773
|
-
if (name !== undefined) {
|
|
774
|
-
return {
|
|
775
|
-
name: "domain",
|
|
776
|
-
result: "TRUE",
|
|
777
|
-
detail: `signature by "${name}" · published by ${from.domain} (${from.party})`,
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
if (actorKeyId !== undefined) {
|
|
782
|
-
return {
|
|
783
|
-
name: "domain",
|
|
784
|
-
result: "UNDETERMINED",
|
|
785
|
-
detail: `actor key ${actorKeyId.slice(0, 12)}… is not among the ${from.keyCount} key(s) ${from.domain} publishes`,
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
return {
|
|
789
|
-
name: "domain",
|
|
790
|
-
result: "UNDETERMINED",
|
|
791
|
-
detail: `no evidence binds this recording to ${from.domain}`,
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** Standard-base64 digest to bytes; undefined when it does not round-trip. */
|
|
796
|
-
function decodeDigestB64(digestB64: string): Buffer | undefined {
|
|
797
|
-
if (!/^[A-Za-z0-9+/]+=*$/.test(digestB64)) return undefined;
|
|
798
|
-
const bytes = Buffer.from(digestB64, "base64");
|
|
799
|
-
return bytes.toString("base64") === digestB64 ? bytes : undefined;
|
|
800
|
-
}
|
|
801
677
|
|
|
802
678
|
function enclaveLine(attestedPcr0: string | undefined, attestationResult: ThreeValued): CheckLine {
|
|
803
679
|
if (attestationResult !== "TRUE" || attestedPcr0 === undefined) {
|
|
@@ -1109,18 +985,13 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
|
|
|
1109
985
|
return notes;
|
|
1110
986
|
}
|
|
1111
987
|
|
|
1112
|
-
function collectNotChecked(hasAnchors: boolean
|
|
988
|
+
function collectNotChecked(hasAnchors: boolean): string[] {
|
|
1113
989
|
const out: string[] = [];
|
|
1114
990
|
if (hasAnchors) {
|
|
1115
991
|
out.push(
|
|
1116
992
|
"whether the anchored Ethereum blocks are canonical: their headers are recomputed here, but canonicality needs an Ethereum node or a block explorer"
|
|
1117
993
|
);
|
|
1118
994
|
}
|
|
1119
|
-
if (fromDomain !== undefined) {
|
|
1120
|
-
out.push(
|
|
1121
|
-
`whether ${fromDomain}'s published key file has changed since it was pinned: a pin is read as stored; pin the domain again to refresh it`
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
995
|
out.push(
|
|
1125
996
|
"whether the public ledger holds these exact recordings at these positions: this is an offline check; drop the file on bitgraph.ing to compare against the ledger"
|
|
1126
997
|
);
|
|
@@ -1175,9 +1046,6 @@ export function renderCheckText(report: CheckReport): string {
|
|
|
1175
1046
|
const out: string[] = [];
|
|
1176
1047
|
const mark = (r: ThreeValued): string => (r === "TRUE" ? "TRUE " : r === "FALSE" ? "FALSE" : "UNDET");
|
|
1177
1048
|
out.push(report.summary);
|
|
1178
|
-
if (report.from !== undefined) {
|
|
1179
|
-
out.push(`checked against ${report.from.domain} (${report.from.party}), from the stored pin`);
|
|
1180
|
-
}
|
|
1181
1049
|
out.push("");
|
|
1182
1050
|
report.recordings.forEach((rec, i) => {
|
|
1183
1051
|
out.push(
|
package/src/cli.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]
|
|
6
6
|
* bitgraph-play init <file>... [--out <rule.json>]
|
|
7
|
-
* bitgraph-play check <bundle-or-file>... [--json] [--out <file>]
|
|
8
|
-
* bitgraph-play pin [<domain>] [--forget <domain>] [--pins <dir>] [--yes]
|
|
7
|
+
* bitgraph-play check <bundle-or-file>... [--json] [--out <file>]
|
|
9
8
|
*
|
|
10
9
|
* Evaluate: runs a bitgraph-player/1 rule against a proof bundle
|
|
11
10
|
* (directory, .tar, .tar.gz, or .tgz) and writes the verdict JSON to
|
|
@@ -23,13 +22,6 @@
|
|
|
23
22
|
* what bounds it. Human text on stdout by default; --json for the
|
|
24
23
|
* bitgraph-check/1 report. Offline. See check.ts for the vocabulary.
|
|
25
24
|
*
|
|
26
|
-
* Pin: fetches https://<domain>/.well-known/bitgraph once (the ONLY
|
|
27
|
-
* network access anywhere in this package, and only when invoked), shows
|
|
28
|
-
* the party and every key's fingerprint, and stores the bytes verbatim
|
|
29
|
-
* after confirmation. `check --from <domain>` then adds one three-valued
|
|
30
|
-
* "domain" line per recording, offline, from the stored pin: TRUE or
|
|
31
|
-
* UNDETERMINED, never FALSE. Format and semantics: DOMAIN.md.
|
|
32
|
-
*
|
|
33
25
|
* Exit codes: 0 TRUE, 1 FALSE, 2 UNDETERMINED, 3 error (init: 0 or 3).
|
|
34
26
|
* Diagnostics go to stderr; stdout carries the verdict (or skeleton, or
|
|
35
27
|
* check report) bytes only.
|
|
@@ -43,14 +35,11 @@ import { createHash } from "node:crypto";
|
|
|
43
35
|
import { createReadStream, existsSync, writeFileSync } from "node:fs";
|
|
44
36
|
import { readFile, stat } from "node:fs/promises";
|
|
45
37
|
import { basename } from "node:path";
|
|
46
|
-
import { createInterface } from "node:readline/promises";
|
|
47
38
|
import { pipeline } from "node:stream/promises";
|
|
48
39
|
import type { AuditResult, BundleEntrySource } from "@mikeargento/bitgraph-audit";
|
|
49
40
|
import { ingestBundle, ingestEntries } from "@mikeargento/bitgraph-audit";
|
|
50
41
|
import type { CheckOptions } from "./check.js";
|
|
51
42
|
import { checkIngest, renderCheckText, serializeCheckReport } from "./check.js";
|
|
52
|
-
import { checkDomain, diffDomainFiles, domainKeyRefs, DomainFileError, isDomainName } from "./domain.js";
|
|
53
|
-
import { defaultPinsDir, fetchDomainFile, forgetPin, listPins, readPin, writePin } from "./pin.js";
|
|
54
43
|
import { scaffoldRule } from "./init.js";
|
|
55
44
|
import type { ScaffoldEntry } from "./init.js";
|
|
56
45
|
import { play, PlayError } from "./play.js";
|
|
@@ -61,12 +50,9 @@ function usage(): number {
|
|
|
61
50
|
"usage: bitgraph-play <rule.json> <bundle> [--out <file>] [--summary]\n" +
|
|
62
51
|
" bitgraph-play init <file>... [--out <rule.json>]\n" +
|
|
63
52
|
" bitgraph-play check <bundle-or-file>... [--json] [--out <file>]\n" +
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
' "check" or "pin" is evaluated with: bitgraph-play -- init <bundle>\n' +
|
|
68
|
-
" pin is the only command that touches the network; check --from\n" +
|
|
69
|
-
" reads the stored pin and runs offline\n" +
|
|
53
|
+
' "--" ends option parsing; a rule file literally named "init" or\n' +
|
|
54
|
+
' "check" is evaluated with: bitgraph-play -- init <bundle>\n' +
|
|
55
|
+
" nothing here touches the network\n" +
|
|
70
56
|
" exit codes: 0 TRUE, 1 FALSE, 2 UNDETERMINED, 3 error\n"
|
|
71
57
|
);
|
|
72
58
|
return 3;
|
|
@@ -107,14 +93,10 @@ interface ParsedArgs {
|
|
|
107
93
|
outFile?: string;
|
|
108
94
|
summary: boolean;
|
|
109
95
|
json: boolean;
|
|
110
|
-
from?: string;
|
|
111
|
-
pinsDir?: string;
|
|
112
|
-
forget?: string;
|
|
113
|
-
yes: boolean;
|
|
114
96
|
}
|
|
115
97
|
|
|
116
98
|
function parseArgs(args: string[]): ParsedArgs | undefined {
|
|
117
|
-
const parsed: ParsedArgs = { positional: [], summary: false, json: false
|
|
99
|
+
const parsed: ParsedArgs = { positional: [], summary: false, json: false };
|
|
118
100
|
const valueFor = (i: number): string | undefined => {
|
|
119
101
|
const next = args[i];
|
|
120
102
|
return next === undefined || next.startsWith("-") ? undefined : next;
|
|
@@ -129,24 +111,10 @@ function parseArgs(args: string[]): ParsedArgs | undefined {
|
|
|
129
111
|
const next = valueFor(++i);
|
|
130
112
|
if (next === undefined) return undefined;
|
|
131
113
|
parsed.outFile = next;
|
|
132
|
-
} else if (arg === "--from") {
|
|
133
|
-
const next = valueFor(++i);
|
|
134
|
-
if (next === undefined) return undefined;
|
|
135
|
-
parsed.from = next;
|
|
136
|
-
} else if (arg === "--pins") {
|
|
137
|
-
const next = valueFor(++i);
|
|
138
|
-
if (next === undefined) return undefined;
|
|
139
|
-
parsed.pinsDir = next;
|
|
140
|
-
} else if (arg === "--forget") {
|
|
141
|
-
const next = valueFor(++i);
|
|
142
|
-
if (next === undefined) return undefined;
|
|
143
|
-
parsed.forget = next;
|
|
144
114
|
} else if (arg === "--summary") {
|
|
145
115
|
parsed.summary = true;
|
|
146
116
|
} else if (arg === "--json") {
|
|
147
117
|
parsed.json = true;
|
|
148
|
-
} else if (arg === "--yes") {
|
|
149
|
-
parsed.yes = true;
|
|
150
118
|
} else if (arg.startsWith("-")) {
|
|
151
119
|
return undefined;
|
|
152
120
|
} else {
|
|
@@ -163,42 +131,11 @@ function parseArgs(args: string[]): ParsedArgs | undefined {
|
|
|
163
131
|
* `bitgraph-play check proof.json photo.jpg` works without a folder.
|
|
164
132
|
*/
|
|
165
133
|
async function runCheck(args: ParsedArgs): Promise<number> {
|
|
166
|
-
if (args.summary
|
|
167
|
-
if (args.pinsDir !== undefined && args.from === undefined) return usage();
|
|
134
|
+
if (args.summary) return usage();
|
|
168
135
|
const targets = args.positional;
|
|
169
136
|
if (targets.length === 0) return usage();
|
|
170
137
|
|
|
171
|
-
|
|
172
|
-
// in milliseconds, with its remedy, not after a long ingest. check
|
|
173
|
-
// itself NEVER fetches; the pin was the one network step, already done.
|
|
174
|
-
let options: CheckOptions | undefined;
|
|
175
|
-
if (args.from !== undefined) {
|
|
176
|
-
const domain = args.from.toLowerCase();
|
|
177
|
-
if (!isDomainName(domain)) {
|
|
178
|
-
process.stderr.write(`error: not a domain name: ${args.from}\n`);
|
|
179
|
-
return 3;
|
|
180
|
-
}
|
|
181
|
-
const pinsDir = args.pinsDir ?? defaultPinsDir();
|
|
182
|
-
let pin;
|
|
183
|
-
try {
|
|
184
|
-
pin = readPin(domain, pinsDir);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
process.stderr.write(`error: the stored pin for ${domain} is malformed`);
|
|
187
|
-
if (err instanceof DomainFileError && err.issues[0] !== undefined) {
|
|
188
|
-
process.stderr.write(`: ${err.issues[0]}`);
|
|
189
|
-
}
|
|
190
|
-
process.stderr.write(`\n pin it again: bitgraph-play pin ${domain}\n`);
|
|
191
|
-
return 3;
|
|
192
|
-
}
|
|
193
|
-
if (pin === undefined) {
|
|
194
|
-
process.stderr.write(
|
|
195
|
-
`error: no pin for ${domain}\n` +
|
|
196
|
-
` pin it once (the only step that needs the network): bitgraph-play pin ${domain}\n`
|
|
197
|
-
);
|
|
198
|
-
return 3;
|
|
199
|
-
}
|
|
200
|
-
options = { from: checkDomain(pin.file) };
|
|
201
|
-
}
|
|
138
|
+
const options: CheckOptions | undefined = undefined;
|
|
202
139
|
|
|
203
140
|
let ingest;
|
|
204
141
|
try {
|
|
@@ -240,7 +177,7 @@ function looksLikeArchive(path: string): boolean {
|
|
|
240
177
|
}
|
|
241
178
|
|
|
242
179
|
async function runInit(args: ParsedArgs): Promise<number> {
|
|
243
|
-
if (args.summary || args.json
|
|
180
|
+
if (args.summary || args.json) return usage();
|
|
244
181
|
const files = args.positional;
|
|
245
182
|
if (files.length === 0) return usage();
|
|
246
183
|
if (args.outFile !== undefined && existsSync(args.outFile)) {
|
|
@@ -296,7 +233,7 @@ async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
296
233
|
}
|
|
297
234
|
|
|
298
235
|
async function runEvaluate(args: ParsedArgs): Promise<number> {
|
|
299
|
-
if (args.json
|
|
236
|
+
if (args.json) return usage();
|
|
300
237
|
if (args.positional.length !== 2) return usage();
|
|
301
238
|
const [rulePath, bundlePath] = args.positional as [string, string];
|
|
302
239
|
|
|
@@ -324,126 +261,12 @@ async function runEvaluate(args: ParsedArgs): Promise<number> {
|
|
|
324
261
|
return result.exitCode;
|
|
325
262
|
}
|
|
326
263
|
|
|
327
|
-
/**
|
|
328
|
-
* pin: the only command in this package that touches the network, and
|
|
329
|
-
* only when invoked. Conversational output goes to stderr (there are no
|
|
330
|
-
* report bytes); the pin listing, which IS the output, goes to stdout.
|
|
331
|
-
*/
|
|
332
|
-
async function runPin(args: ParsedArgs): Promise<number> {
|
|
333
|
-
if (args.summary || args.json || args.outFile !== undefined || args.from !== undefined) return usage();
|
|
334
|
-
const pinsDir = args.pinsDir ?? defaultPinsDir();
|
|
335
|
-
|
|
336
|
-
if (args.forget !== undefined) {
|
|
337
|
-
if (args.positional.length !== 0) return usage();
|
|
338
|
-
const domain = args.forget.toLowerCase();
|
|
339
|
-
if (!isDomainName(domain)) {
|
|
340
|
-
process.stderr.write(`error: not a domain name: ${args.forget}\n`);
|
|
341
|
-
return 3;
|
|
342
|
-
}
|
|
343
|
-
if (forgetPin(domain, pinsDir)) {
|
|
344
|
-
process.stderr.write(`forgot ${domain}\n`);
|
|
345
|
-
return 0;
|
|
346
|
-
}
|
|
347
|
-
process.stderr.write(`error: no pin for ${domain}\n`);
|
|
348
|
-
return 3;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (args.positional.length === 0) {
|
|
352
|
-
const pins = listPins(pinsDir);
|
|
353
|
-
if (pins.length === 0) {
|
|
354
|
-
process.stderr.write(
|
|
355
|
-
"no pins yet\n pin a domain (the only step that needs the network): bitgraph-play pin <domain>\n"
|
|
356
|
-
);
|
|
357
|
-
return 0;
|
|
358
|
-
}
|
|
359
|
-
for (const pin of pins) {
|
|
360
|
-
process.stdout.write(
|
|
361
|
-
pin.malformed
|
|
362
|
-
? `${pin.domain} (malformed pin; pin it again or --forget it)\n`
|
|
363
|
-
: `${pin.domain} ${pin.party as string} ${pin.keyCount as number} key(s) pinned ${pin.pinnedAt.toISOString().slice(0, 10)}\n`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
return 0;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (args.positional.length !== 1) return usage();
|
|
370
|
-
const domain = (args.positional[0] as string).toLowerCase();
|
|
371
|
-
|
|
372
|
-
let fetched;
|
|
373
|
-
try {
|
|
374
|
-
fetched = await fetchDomainFile(domain);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
if (err instanceof DomainFileError) {
|
|
377
|
-
process.stderr.write(
|
|
378
|
-
`error: the file at https://${domain}/.well-known/bitgraph is not a valid bitgraph-domain/1 file:\n`
|
|
379
|
-
);
|
|
380
|
-
for (const issue of err.issues) process.stderr.write(` - ${issue}\n`);
|
|
381
|
-
} else {
|
|
382
|
-
process.stderr.write(`error: ${(err as Error).message}\n`);
|
|
383
|
-
}
|
|
384
|
-
return 3;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const refs = domainKeyRefs(fetched.file);
|
|
388
|
-
const nameWidth = refs.reduce((w, r) => Math.max(w, r.name.length), 4);
|
|
389
|
-
process.stderr.write(`\n${domain} · ${fetched.file.party}\n`);
|
|
390
|
-
for (const ref of refs) {
|
|
391
|
-
process.stderr.write(` ${ref.name.padEnd(nameWidth)} ${ref.key.alg.padEnd(7)} ${ref.fingerprint}\n`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let existing;
|
|
395
|
-
let existingMalformed = false;
|
|
396
|
-
try {
|
|
397
|
-
existing = readPin(domain, pinsDir);
|
|
398
|
-
} catch {
|
|
399
|
-
existingMalformed = true;
|
|
400
|
-
}
|
|
401
|
-
if (existingMalformed) {
|
|
402
|
-
process.stderr.write(`\nthe stored pin for ${domain} is malformed and will be replaced\n`);
|
|
403
|
-
} else if (existing !== undefined) {
|
|
404
|
-
const diff = diffDomainFiles(existing.file, fetched.file);
|
|
405
|
-
const changes: string[] = [];
|
|
406
|
-
if (diff.partyChanged !== undefined) {
|
|
407
|
-
changes.push(` party: "${diff.partyChanged.before}" is now "${diff.partyChanged.after}"`);
|
|
408
|
-
}
|
|
409
|
-
for (const ref of diff.added) changes.push(` + ${ref.name} ${ref.key.alg} ${ref.fingerprint}`);
|
|
410
|
-
for (const ref of diff.removed) changes.push(` - ${ref.name} ${ref.key.alg} ${ref.fingerprint}`);
|
|
411
|
-
for (const ch of diff.changed) changes.push(` ~ ${ch.name} now ${ch.after.key.alg} ${ch.after.fingerprint}`);
|
|
412
|
-
const pinnedOn = existing.pinnedAt.toISOString().slice(0, 10);
|
|
413
|
-
process.stderr.write(
|
|
414
|
-
changes.length === 0
|
|
415
|
-
? `\nunchanged since the stored pin (${pinnedOn})\n`
|
|
416
|
-
: `\nchanges since the stored pin (${pinnedOn}):\n${changes.join("\n")}\n`
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!args.yes) {
|
|
421
|
-
if (!process.stdin.isTTY) {
|
|
422
|
-
process.stderr.write("\nerror: not a terminal; pass --yes to pin non-interactively\n");
|
|
423
|
-
return 3;
|
|
424
|
-
}
|
|
425
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
426
|
-
const answer = (await rl.question(`\npin ${refs.length} key(s) for ${domain}? [y/N] `)).trim().toLowerCase();
|
|
427
|
-
rl.close();
|
|
428
|
-
if (answer !== "y" && answer !== "yes") {
|
|
429
|
-
process.stderr.write("not pinned\n");
|
|
430
|
-
return 3;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const path = writePin(domain, fetched.bytes, pinsDir);
|
|
435
|
-
process.stderr.write(
|
|
436
|
-
`pinned: ${path}\nchecks now run offline: bitgraph-play check <export> --from ${domain}\n`
|
|
437
|
-
);
|
|
438
|
-
return 0;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
264
|
async function main(): Promise<number> {
|
|
442
265
|
const argv = process.argv.slice(2);
|
|
443
266
|
// "--" as the first token forces evaluate mode: parseArgs treats
|
|
444
267
|
// everything after it as positional, so a rule file literally named
|
|
445
|
-
// "init"
|
|
446
|
-
const subcommand = argv[0] === "init" || argv[0] === "check"
|
|
268
|
+
// "init" or "check" is reachable as `bitgraph-play -- init <bundle>`.
|
|
269
|
+
const subcommand = argv[0] === "init" || argv[0] === "check" ? argv[0] : undefined;
|
|
447
270
|
if (subcommand !== undefined && existsSync(subcommand)) {
|
|
448
271
|
// Both readings are plausible here; a silent pick would hand a
|
|
449
272
|
// 0.1.1 caller a skeleton with exit 0 where the published contract
|
|
@@ -459,7 +282,6 @@ async function main(): Promise<number> {
|
|
|
459
282
|
if (parsed === undefined) return usage();
|
|
460
283
|
if (subcommand === "init") return runInit(parsed);
|
|
461
284
|
if (subcommand === "check") return runCheck(parsed);
|
|
462
|
-
if (subcommand === "pin") return runPin(parsed);
|
|
463
285
|
return runEvaluate(parsed);
|
|
464
286
|
}
|
|
465
287
|
|
package/src/index.ts
CHANGED
|
@@ -46,20 +46,5 @@ export type { Evaluation } from "./evaluate.js";
|
|
|
46
46
|
export { buildVerdict, serializeVerdict, playerVersion, PLAYER_VERSION } from "./verdict.js";
|
|
47
47
|
|
|
48
48
|
export { checkIngest, buildCheckReport, renderCheckText, serializeCheckReport, KNOWN_ENCLAVE_MEASUREMENTS } from "./check.js";
|
|
49
|
-
export type { CheckReport, CheckRecording, CheckAnchor, CheckLine, CheckBounds, CheckBound, CheckOptions
|
|
49
|
+
export type { CheckReport, CheckRecording, CheckAnchor, CheckLine, CheckBounds, CheckBound, CheckOptions } from "./check.js";
|
|
50
50
|
|
|
51
|
-
export {
|
|
52
|
-
parseDomainFile,
|
|
53
|
-
isDomainName,
|
|
54
|
-
keyFingerprint,
|
|
55
|
-
domainKeyRefs,
|
|
56
|
-
diffDomainFiles,
|
|
57
|
-
checkDomain,
|
|
58
|
-
DomainFileError,
|
|
59
|
-
DOMAIN_FILE_VERSION,
|
|
60
|
-
DOMAIN_WELL_KNOWN_PATH,
|
|
61
|
-
DOMAIN_FILE_MAX_BYTES,
|
|
62
|
-
} from "./domain.js";
|
|
63
|
-
export type { DomainFile, DomainKeyRef, DomainDiff } from "./domain.js";
|
|
64
|
-
export { defaultPinsDir, readPin, writePin, forgetPin, listPins, fetchDomainFile } from "./pin.js";
|
|
65
|
-
export type { StoredPin, PinListEntry, FetchLike } from "./pin.js";
|
package/src/verdict.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type { DeclaredEntry, Resolution, Rule, Verdict } from "./types.js";
|
|
|
31
31
|
* A unit test asserts this equals package.json's version, so the
|
|
32
32
|
* constant cannot drift silently across releases.
|
|
33
33
|
*/
|
|
34
|
-
export const PLAYER_VERSION = "0.
|
|
34
|
+
export const PLAYER_VERSION = "0.10.0";
|
|
35
35
|
|
|
36
36
|
/** The player package's own version. */
|
|
37
37
|
export function playerVersion(): string {
|