@mikeargento/bitgraph-player 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -24
- package/dist/check.d.ts +2 -45
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +6 -92
- 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.js +1 -1
- package/dist-web/verify.html +5 -5
- package/package.json +2 -3
- package/src/check.ts +6 -143
- 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.9.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": {
|
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
|
|
|
@@ -110,7 +109,7 @@ export const KNOWN_ENCLAVE_MEASUREMENTS: ReadonlyArray<{ pcr0: string; label: st
|
|
|
110
109
|
|
|
111
110
|
/** One checked property, three-valued, with a plain-language reason. */
|
|
112
111
|
export interface CheckLine {
|
|
113
|
-
name: "file" | "signature" | "attestation" | "enclave" | "witness" | "contradiction" | "
|
|
112
|
+
name: "file" | "signature" | "attestation" | "enclave" | "witness" | "contradiction" | "fused";
|
|
114
113
|
result: ThreeValued;
|
|
115
114
|
detail: string;
|
|
116
115
|
}
|
|
@@ -197,10 +196,7 @@ export interface CheckAnchor {
|
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
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 };
|
|
199
|
+
check: "bitgraph-check/1";
|
|
204
200
|
result: ThreeValued;
|
|
205
201
|
/** One-sentence plain-language conclusion, deterministic. */
|
|
206
202
|
summary: string;
|
|
@@ -216,31 +212,6 @@ export interface CheckReport {
|
|
|
216
212
|
network: "none";
|
|
217
213
|
}
|
|
218
214
|
|
|
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
215
|
export interface CheckOptions {
|
|
245
216
|
/**
|
|
246
217
|
* False when the environment cannot run the attestation's ECDSA P-384
|
|
@@ -248,19 +219,6 @@ export interface CheckOptions {
|
|
|
248
219
|
* with that reason instead of a false FALSE. Defaults to true.
|
|
249
220
|
*/
|
|
250
221
|
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
222
|
/**
|
|
265
223
|
* Fuse evidence by proofHash: the verifyFuse result over the fused bytes
|
|
266
224
|
* (preferred) or the original, whichever the bundle holds. checkIngest
|
|
@@ -300,17 +258,6 @@ const EXCERPT_NORMAL_CODES: ReadonlySet<string> = new Set([
|
|
|
300
258
|
*/
|
|
301
259
|
export async function checkIngest(ingest: IngestResult, options?: CheckOptions): Promise<CheckReport> {
|
|
302
260
|
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
261
|
const audit = await auditIngest(ingest, { startedAt: "" });
|
|
315
262
|
if (opts?.fuseEvidence === undefined) {
|
|
316
263
|
const collected = await collectFuseEvidence(ingest);
|
|
@@ -405,8 +352,6 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
405
352
|
segmentByHash.get(proof.proofHash),
|
|
406
353
|
artifactPathByProof.get(proof.proofHash),
|
|
407
354
|
webCrypto,
|
|
408
|
-
options?.from,
|
|
409
|
-
options?.sigEvidence,
|
|
410
355
|
options?.fuseEvidence,
|
|
411
356
|
anchorByHash
|
|
412
357
|
)
|
|
@@ -418,7 +363,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
418
363
|
sortByPosition(anchors);
|
|
419
364
|
const contradictions = collectContradictions(audit);
|
|
420
365
|
const notes = collectNotes(audit, recordings, anchors, options?.fuseOriginHexes);
|
|
421
|
-
const notChecked = collectNotChecked(anchors.length > 0
|
|
366
|
+
const notChecked = collectNotChecked(anchors.length > 0);
|
|
422
367
|
|
|
423
368
|
const allLines: ThreeValued[] = [
|
|
424
369
|
...recordings.map((r) => r.result),
|
|
@@ -428,10 +373,7 @@ export function buildCheckReport(audit: AuditResult, options?: CheckOptions): Ch
|
|
|
428
373
|
const result: ThreeValued = allLines.length === 0 ? "UNDETERMINED" : kleeneAll(allLines);
|
|
429
374
|
|
|
430
375
|
return {
|
|
431
|
-
check:
|
|
432
|
-
...(options?.from !== undefined
|
|
433
|
-
? { from: { domain: options.from.domain, party: options.from.party } }
|
|
434
|
-
: {}),
|
|
376
|
+
check: "bitgraph-check/1",
|
|
435
377
|
result,
|
|
436
378
|
summary: summarize(result, recordings, anchors, contradictions),
|
|
437
379
|
recordings,
|
|
@@ -474,8 +416,6 @@ function buildRecording(
|
|
|
474
416
|
segment: TemporalSegment | undefined,
|
|
475
417
|
filePath: string | undefined,
|
|
476
418
|
webCrypto: boolean,
|
|
477
|
-
from?: CheckDomain,
|
|
478
|
-
sigEvidence?: ReadonlyMap<string, Uint8Array>,
|
|
479
419
|
fuseEvidence?: ReadonlyMap<string, FuseVerifyResult>,
|
|
480
420
|
anchorByHash?: ReadonlyMap<string, AnchorRecord>
|
|
481
421
|
): CheckRecording {
|
|
@@ -529,18 +469,6 @@ function buildRecording(
|
|
|
529
469
|
// enclave: the attested PCR0 is a published BitGraph measurement.
|
|
530
470
|
lines.push(enclaveLine(att.attestedPcr0, att.line.result));
|
|
531
471
|
|
|
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
472
|
|
|
545
473
|
// fused: the proof's signed attribution marks a fused artifact (profile
|
|
546
474
|
// bitgraph-fuse/1). The commitment check runs over the fused bytes, or
|
|
@@ -741,63 +669,6 @@ function attestationLine(
|
|
|
741
669
|
};
|
|
742
670
|
}
|
|
743
671
|
|
|
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
672
|
|
|
802
673
|
function enclaveLine(attestedPcr0: string | undefined, attestationResult: ThreeValued): CheckLine {
|
|
803
674
|
if (attestationResult !== "TRUE" || attestedPcr0 === undefined) {
|
|
@@ -1109,18 +980,13 @@ function collectNotes(audit: AuditResult, recordings: CheckRecording[], anchors:
|
|
|
1109
980
|
return notes;
|
|
1110
981
|
}
|
|
1111
982
|
|
|
1112
|
-
function collectNotChecked(hasAnchors: boolean
|
|
983
|
+
function collectNotChecked(hasAnchors: boolean): string[] {
|
|
1113
984
|
const out: string[] = [];
|
|
1114
985
|
if (hasAnchors) {
|
|
1115
986
|
out.push(
|
|
1116
987
|
"whether the anchored Ethereum blocks are canonical: their headers are recomputed here, but canonicality needs an Ethereum node or a block explorer"
|
|
1117
988
|
);
|
|
1118
989
|
}
|
|
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
990
|
out.push(
|
|
1125
991
|
"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
992
|
);
|
|
@@ -1175,9 +1041,6 @@ export function renderCheckText(report: CheckReport): string {
|
|
|
1175
1041
|
const out: string[] = [];
|
|
1176
1042
|
const mark = (r: ThreeValued): string => (r === "TRUE" ? "TRUE " : r === "FALSE" ? "FALSE" : "UNDET");
|
|
1177
1043
|
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
1044
|
out.push("");
|
|
1182
1045
|
report.recordings.forEach((rec, i) => {
|
|
1183
1046
|
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.9.0";
|
|
35
35
|
|
|
36
36
|
/** The player package's own version. */
|
|
37
37
|
export function playerVersion(): string {
|