@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikeargento/bitgraph-player",
3
- "version": "0.8.1",
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
- "DOMAIN.md",
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, 4);
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, 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
 
@@ -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 onward",
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" | "domain" | "fused";
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
- /** /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 };
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, options?.from?.domain);
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: options?.from !== undefined ? "bitgraph-check/2" : "bitgraph-check/1",
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, fromDomain?: string): string[] {
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>] [--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.1";
34
+ export const PLAYER_VERSION = "0.10.0";
35
35
 
36
36
  /** The player package's own version. */
37
37
  export function playerVersion(): string {