@mikeargento/bitgraph-player 0.8.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikeargento/bitgraph-player",
3
- "version": "0.8.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
- "DOMAIN.md",
24
- "README.md",
23
+ "README.md",
25
24
  "LICENSE"
26
25
  ],
27
26
  "scripts": {
@@ -36,7 +35,7 @@
36
35
  "node": ">=20"
37
36
  },
38
37
  "dependencies": {
39
- "@mikeargento/bitgraph-audit": "^0.4.0",
38
+ "@mikeargento/bitgraph-audit": "^0.4.1",
40
39
  "@mikeargento/bitgraph-verify": "^1.4.0"
41
40
  },
42
41
  "devDependencies": {
@@ -29,6 +29,16 @@ async function entries(spec: Record<string, string>): Promise<BundleEntrySource[
29
29
  }
30
30
 
31
31
  describe("fused recordings in check", () => {
32
+ it("a Frame beside the fused bytes: the nested proof is the recording, the Frame is not an artifact", async () => {
33
+ const report = await checkIngest(await ingestEntries(await entries({ "photo.bitgraph-fuse.json": "trailer.bitgraph-fuse.json", "photo.bin": "fused-trailer.bin" })));
34
+ assert.equal(report.recordings.length, 1, "the Frame's nested proof is observed");
35
+ const rec = report.recordings[0]!;
36
+ assert.equal(rec.lines.find((l) => l.name === "file")?.result, "TRUE");
37
+ assert.equal(rec.lines.find((l) => l.name === "fused")?.result, "TRUE");
38
+ assert.equal(rec.fused?.category, "FUSED_DIRECT");
39
+ assert.ok(!report.notes.some((n) => /photo\.bitgraph-fuse\.json/.test(n)), "the Frame is not listed as an unmatched file");
40
+ });
41
+
32
42
  it("fused bytes beside the proof: fused line TRUE, floor undetermined without anchors, statements carried", async () => {
33
43
  const report = await checkIngest(await ingestEntries(await entries({ "proof.json": "trailer.proof.json", "photo.bin": "fused-trailer.bin" })));
34
44
  assert.equal(report.recordings.length, 1);
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, streamMatchedArtifacts, streamArtifactsByHash } from "@mikeargento/bitgraph-audit";
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" | "domain" | "fused";
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
- /** /2 exactly when the report was built against a pinned domain (`from`). */
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, options?.from?.domain);
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: options?.from !== undefined ? "bitgraph-check/2" : "bitgraph-check/1",
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, fromDomain?: string): string[] {
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>] [--from <domain> [--pins <dir>]]
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
- " [--from <domain> [--pins <dir>]]\n" +
65
- " bitgraph-play pin [<domain>] [--forget <domain>] [--pins <dir>] [--yes]\n" +
66
- ' "--" ends option parsing; a rule file literally named "init",\n' +
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, yes: 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 || args.yes || args.forget !== undefined) return usage();
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
- // Resolve the pin before touching the bundle: a missing pin should fail
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 || args.from !== undefined || args.pinsDir !== undefined || args.yes || args.forget !== undefined) return usage();
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 || args.from !== undefined || args.pinsDir !== undefined || args.yes || args.forget !== undefined) return usage();
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", "check" or "pin" is reachable as `bitgraph-play -- init <bundle>`.
446
- const subcommand = argv[0] === "init" || argv[0] === "check" || argv[0] === "pin" ? argv[0] : undefined;
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, CheckDomain } from "./check.js";
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.8.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 {