@mikeargento/bitgraph-player 0.5.1 → 0.6.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.
Files changed (50) hide show
  1. package/DOMAIN.md +119 -0
  2. package/README.md +25 -5
  3. package/dist/__tests__/domain.test.d.ts +2 -0
  4. package/dist/__tests__/domain.test.d.ts.map +1 -0
  5. package/dist/__tests__/domain.test.js +265 -0
  6. package/dist/__tests__/domain.test.js.map +1 -0
  7. package/dist/check.d.ts +45 -2
  8. package/dist/check.d.ts.map +1 -1
  9. package/dist/check.js +94 -7
  10. package/dist/check.js.map +1 -1
  11. package/dist/cli.js +194 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/domain.d.ts +64 -0
  14. package/dist/domain.d.ts.map +1 -0
  15. package/dist/domain.js +212 -0
  16. package/dist/domain.js.map +1 -0
  17. package/dist/index.d.ts +5 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/pin.d.ts +55 -0
  22. package/dist/pin.d.ts.map +1 -0
  23. package/dist/pin.js +126 -0
  24. package/dist/pin.js.map +1 -0
  25. package/dist/play.d.ts +6 -1
  26. package/dist/play.d.ts.map +1 -1
  27. package/dist/play.js +6 -1
  28. package/dist/play.js.map +1 -1
  29. package/dist/sig.d.ts +2 -0
  30. package/dist/sig.d.ts.map +1 -1
  31. package/dist/sig.js +1 -1
  32. package/dist/sig.js.map +1 -1
  33. package/dist/types.d.ts +7 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +7 -0
  36. package/dist/types.js.map +1 -1
  37. package/dist/verdict.d.ts +1 -1
  38. package/dist/verdict.js +1 -1
  39. package/dist-web/verify.html +3 -3
  40. package/package.json +4 -3
  41. package/src/__tests__/domain.test.ts +321 -0
  42. package/src/check.ts +149 -9
  43. package/src/cli.ts +199 -12
  44. package/src/domain.ts +245 -0
  45. package/src/index.ts +17 -1
  46. package/src/pin.ts +155 -0
  47. package/src/play.ts +6 -1
  48. package/src/sig.ts +1 -1
  49. package/src/types.ts +8 -0
  50. package/src/verdict.ts +1 -1
package/src/pin.ts ADDED
@@ -0,0 +1,155 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+
3
+ /**
4
+ * The pin store, and the fetch behind `bitgraph-play pin`: the ONLY code
5
+ * in this package that touches the network, and only when the reader
6
+ * invokes it. A pin is the reader's act and the reader's record: the
7
+ * domain's file is stored byte-verbatim on the reader's machine, and
8
+ * `check --from` reads the store and never fetches. A missing pin is an
9
+ * invocation error with the pin command as its remedy, never a verdict,
10
+ * and a stored pin outlives the file that provided it, deliberately.
11
+ */
12
+
13
+ import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import type { DomainFile } from "./domain.js";
17
+ import { DOMAIN_FILE_MAX_BYTES, DOMAIN_WELL_KNOWN_PATH, isDomainName, parseDomainFile } from "./domain.js";
18
+
19
+ /** ~/.bitgraph/pins, or BITGRAPH_PINS, or the --pins flag above this. */
20
+ export function defaultPinsDir(): string {
21
+ const env = process.env["BITGRAPH_PINS"];
22
+ return env !== undefined && env.length > 0 ? env : join(homedir(), ".bitgraph", "pins");
23
+ }
24
+
25
+ function pinPath(pinsDir: string, domain: string): string {
26
+ if (!isDomainName(domain)) throw new Error(`not a domain name: ${domain}`);
27
+ // The domain grammar admits no path separators, so the name is the file.
28
+ return join(pinsDir, domain);
29
+ }
30
+
31
+ export interface StoredPin {
32
+ domain: string;
33
+ file: DomainFile;
34
+ bytes: Buffer;
35
+ pinnedAt: Date;
36
+ }
37
+
38
+ /**
39
+ * Read one stored pin. Undefined when no pin exists; throws
40
+ * DomainFileError when the stored bytes no longer parse (the remedy is to
41
+ * pin again, or --forget).
42
+ */
43
+ export function readPin(domain: string, pinsDir: string = defaultPinsDir()): StoredPin | undefined {
44
+ const path = pinPath(pinsDir, domain);
45
+ let bytes: Buffer;
46
+ let pinnedAt: Date;
47
+ try {
48
+ bytes = readFileSync(path);
49
+ pinnedAt = statSync(path).mtime;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ const file = parseDomainFile(bytes, domain);
54
+ return { domain, file, bytes, pinnedAt };
55
+ }
56
+
57
+ /** Store the fetched bytes verbatim. Overwrites: a pin is current by choice. */
58
+ export function writePin(domain: string, bytes: Uint8Array, pinsDir: string = defaultPinsDir()): string {
59
+ const path = pinPath(pinsDir, domain);
60
+ mkdirSync(pinsDir, { recursive: true });
61
+ writeFileSync(path, bytes);
62
+ return path;
63
+ }
64
+
65
+ /** Remove one pin. False when there was none. */
66
+ export function forgetPin(domain: string, pinsDir: string = defaultPinsDir()): boolean {
67
+ try {
68
+ unlinkSync(pinPath(pinsDir, domain));
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ export interface PinListEntry {
76
+ domain: string;
77
+ /** Undefined when the stored bytes no longer parse. */
78
+ party?: string;
79
+ keyCount?: number;
80
+ pinnedAt: Date;
81
+ malformed: boolean;
82
+ }
83
+
84
+ /** Every stored pin, domain order. Malformed entries are listed, flagged. */
85
+ export function listPins(pinsDir: string = defaultPinsDir()): PinListEntry[] {
86
+ let names: string[];
87
+ try {
88
+ names = readdirSync(pinsDir);
89
+ } catch {
90
+ return [];
91
+ }
92
+ const entries: PinListEntry[] = [];
93
+ for (const name of names.sort()) {
94
+ if (!isDomainName(name)) continue;
95
+ try {
96
+ const pin = readPin(name, pinsDir);
97
+ if (pin === undefined) continue;
98
+ entries.push({
99
+ domain: name,
100
+ party: pin.file.party,
101
+ keyCount: Object.keys(pin.file.keys).length,
102
+ pinnedAt: pin.pinnedAt,
103
+ malformed: false,
104
+ });
105
+ } catch {
106
+ entries.push({ domain: name, pinnedAt: statSync(join(pinsDir, name)).mtime, malformed: true });
107
+ }
108
+ }
109
+ return entries;
110
+ }
111
+
112
+ /** The slice of fetch() this module uses; injectable for tests. */
113
+ export interface FetchLike {
114
+ (url: string, init?: { signal?: AbortSignal; redirect?: "follow" }): Promise<{
115
+ ok: boolean;
116
+ status: number;
117
+ headers: { get(name: string): string | null };
118
+ arrayBuffer(): Promise<ArrayBuffer>;
119
+ }>;
120
+ }
121
+
122
+ /**
123
+ * Fetch a domain's bitgraph-domain/1 file: one HTTPS GET of the fixed
124
+ * well-known path. Redirects are followed, but the file's own `domain`
125
+ * field must equal the requested domain (parseDomainFile enforces it), so
126
+ * a redirect cannot repoint the name. Throws on any failure; a malformed
127
+ * file is refused here and never stored.
128
+ */
129
+ export async function fetchDomainFile(
130
+ domain: string,
131
+ fetchImpl: FetchLike = globalThis.fetch as unknown as FetchLike
132
+ ): Promise<{ bytes: Buffer; file: DomainFile }> {
133
+ if (!isDomainName(domain)) {
134
+ const hint = domain.includes("://") ? " (pass the bare hostname, without scheme or path)" : "";
135
+ throw new Error(`not a domain name: ${domain}${hint}`);
136
+ }
137
+ const url = `https://${domain}${DOMAIN_WELL_KNOWN_PATH}`;
138
+ let res: Awaited<ReturnType<FetchLike>>;
139
+ try {
140
+ res = await fetchImpl(url, { signal: AbortSignal.timeout(15_000), redirect: "follow" });
141
+ } catch (err) {
142
+ throw new Error(`cannot fetch ${url}: ${(err as Error).message}`);
143
+ }
144
+ if (!res.ok) throw new Error(`no domain file at ${url} (HTTP ${res.status})`);
145
+ const declared = res.headers.get("content-length");
146
+ if (declared !== null && Number(declared) > DOMAIN_FILE_MAX_BYTES) {
147
+ throw new Error(`the file at ${url} declares ${declared} bytes; the cap is ${DOMAIN_FILE_MAX_BYTES}`);
148
+ }
149
+ const bytes = Buffer.from(await res.arrayBuffer());
150
+ if (bytes.length > DOMAIN_FILE_MAX_BYTES) {
151
+ throw new Error(`the file at ${url} is ${bytes.length} bytes; the cap is ${DOMAIN_FILE_MAX_BYTES}`);
152
+ }
153
+ const file = parseDomainFile(bytes, domain);
154
+ return { bytes, file };
155
+ }
package/src/play.ts CHANGED
@@ -60,8 +60,13 @@ export function claimUsesSignatures(claim: Claim): boolean {
60
60
  * evaluation. Signature files are a few hundred bytes; the cap only
61
61
  * exists so a bundle full of large artifacts never inflates memory.
62
62
  * A file above the cap is simply not signature evidence.
63
+ *
64
+ * The constant lives in types.ts (dependency-free) so check.ts, which is
65
+ * also built into the browser verifier, can use it without pulling this
66
+ * module's node:fs import into that bundle. Re-exported here unchanged.
63
67
  */
64
- export const SIG_EVIDENCE_MAX_BYTES = 1_048_576;
68
+ import { SIG_EVIDENCE_MAX_BYTES } from "./types.js";
69
+ export { SIG_EVIDENCE_MAX_BYTES };
65
70
 
66
71
  /** The pure tail of the pipeline: no filesystem, no network. */
67
72
  export function playAudit(
package/src/sig.ts CHANGED
@@ -93,7 +93,7 @@ export function parseSigFile(bytes: Uint8Array): SigFile | undefined {
93
93
  }
94
94
 
95
95
  /** Strict base64 decode: the spelling must round-trip byte-exactly. */
96
- function decodeB64Strict(s: string): Buffer | undefined {
96
+ export function decodeB64Strict(s: string): Buffer | undefined {
97
97
  if (!/^[A-Za-z0-9+/]+=*$/.test(s)) return undefined;
98
98
  const bytes = Buffer.from(s, "base64");
99
99
  return bytes.toString("base64") === s ? bytes : undefined;
package/src/types.ts CHANGED
@@ -239,3 +239,11 @@ export interface Verdict {
239
239
  evaluator: { name: string; version: string };
240
240
  network: "none";
241
241
  }
242
+
243
+ /**
244
+ * Size cap on candidate signature-evidence files (SPEC §9.4): a bundle
245
+ * artifact larger than this is simply not signature evidence. Declared
246
+ * here, dependency-free, because both play.ts (which imports node:fs) and
247
+ * check.ts (which is also built into the browser verifier) need it.
248
+ */
249
+ export const SIG_EVIDENCE_MAX_BYTES = 1_048_576;
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.5.1";
34
+ export const PLAYER_VERSION = "0.6.0";
35
35
 
36
36
  /** The player package's own version. */
37
37
  export function playerVersion(): string {