@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/src/pin.ts DELETED
@@ -1,155 +0,0 @@
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
- }