@bluefields/cli 0.2.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 ADDED
@@ -0,0 +1,181 @@
1
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
2
+
3
+ # Bluefield — git for web data
4
+
5
+ Snapshot web pages into a **content-addressed store on your own disk**, and
6
+ sign every capture with an **offline-verifiable Ed25519 provenance manifest**
7
+ (the *Bluefield Provenance Manifest / BPM*). Track changes over time, diff any
8
+ two snapshots, and prove — cryptographically, months later, with no network —
9
+ exactly what a URL served and when.
10
+
11
+ - 🗄️ **Local-first.** Snapshots live in `~/.bluefield` (or a project-local
12
+ `./.bf`). No server. No account. No hosted database.
13
+ - 🔏 **Signed & tamper-evident.** Each capture is content-addressed by SHA-256
14
+ and signed with a local key. `bf verify` re-hashes the bytes and checks the
15
+ signature — entirely offline.
16
+ - 🕰️ **History & diffs.** `bf log`, `bf diff`, and `bf watch` give you a
17
+ git-like history of any page.
18
+ - 🤝 **BYO everything.** Proxy and LLM are yours, configured by env. There is
19
+ **no telemetry and no phone-home** — grep the source.
20
+ - 🧰 **Library + CLI + MCP.** Use `bf` on the command line, `import` the
21
+ library, or expose it to an AI agent over MCP.
22
+
23
+ It is the same audited engine that powers the hosted Bluefield service
24
+ (`@bluefields/fetcher` / `extractor` / `differ` / `attest`), wired to a local
25
+ embedded database (PGlite) and a filesystem object store — so nothing leaves
26
+ your machine.
27
+
28
+ ---
29
+
30
+ ## 60-second quickstart
31
+
32
+ Install from a checkout (the CLI is not yet published on npm):
33
+
34
+ ```console
35
+ $ git clone <repository-url> && cd bluefield
36
+ $ pnpm install && pnpm -r build
37
+ $ node packages/cli/dist/cli.js --help # or link the `bf` bin onto your PATH
38
+ ```
39
+
40
+ > Once it is published, `npm install -g @bluefields/cli` will provide the `bf`
41
+ > command directly. The examples below use `bf` for brevity.
42
+
43
+ ```console
44
+ # 1. Capture a page. Needs network, and the URL must be https://
45
+ # (the fetcher is https-only and blocks localhost/file:// by design).
46
+ # First run generates your signing key in ~/.bluefield/keys.
47
+ $ bf scrape https://example.com/
48
+ captured https://example.com/
49
+ snapshot 3f9a2b18-...-a7c1
50
+ sha256 e722a1f6...b952df
51
+ signed yes changed: yes
52
+
53
+ # 2. Prove it — recomputes the hash and checks the signature, fully offline.
54
+ $ bf verify 3f9a2b
55
+ ✓ VERIFIED 3f9a2b18
56
+ content hash matches : yes
57
+ signature valid : yes
58
+
59
+ # 3. Capture again later; Bluefield tells you if it changed and shows the diff.
60
+ $ bf scrape https://example.com/
61
+ $ bf diff https://example.com/
62
+ --- 3f9a2b18 2026-07-06T10:00:00.000Z
63
+ +++ 9c04 e1af 2026-07-06T18:00:00.000Z
64
+ (content) +2 -1
65
+ - <p>old price: $9</p>
66
+ + <p>new price: $12</p>
67
+
68
+ # 4. History, and a portable proof bundle anyone can re-verify.
69
+ $ bf log https://example.com/
70
+ $ bf export 3f9a2b --out ./proof # writes <id>.content + <id>.bundle.json
71
+ ```
72
+
73
+ No API key. The only network is fetching the page itself in step 1 — once a
74
+ capture exists, `verify`, `log`, `diff`, and `export` all run fully offline.
75
+
76
+ ---
77
+
78
+ ## Commands
79
+
80
+ | Command | Does |
81
+ | --- | --- |
82
+ | `bf scrape <url>` | Capture a URL: fetch → content-address on disk → sign → record |
83
+ | `bf log [url]` | Snapshot history, newest first (all, or one URL) |
84
+ | `bf diff <a> [b]` | Line-diff two snapshots (or the last two of a URL) |
85
+ | `bf watch <url>` | Poll a URL on an interval and report changes |
86
+ | `bf verify <ref>` | Recompute the content hash + verify the signature (offline) |
87
+ | `bf export <ref>` | Write raw content + a portable proof bundle |
88
+ | `bf key` | Show this machine's public signing identity (safe to share) |
89
+ | `bf where` | Print the resolved store + keys locations |
90
+ | `bf mcp` | Run the local MCP server over stdio |
91
+
92
+ A `<ref>` is a **snapshot-id prefix** (e.g. `3f9a2b`) or a **URL** (resolves to
93
+ its latest snapshot). Run `bf help` for all flags.
94
+
95
+ ### Structured extraction (optional, BYO LLM)
96
+
97
+ `bf scrape <url> --extract --schema schema.json` runs the extractor to pull
98
+ structured JSON. This is the one feature that calls out to an LLM — set
99
+ `ANTHROPIC_API_KEY` yourself. Everything else is fully offline.
100
+
101
+ ---
102
+
103
+ ## Where things live
104
+
105
+ ```
106
+ ~/.bluefield/keys/ed25519.key.jwk # PRIVATE signing key, chmod 0600 — never printed
107
+ ~/.bluefield/keys/ed25519.pub.jwk # public key + keyId (shareable)
108
+
109
+ <store>/objects/<ab>/<sha256> # content-addressed raw bodies (the "git objects")
110
+ <store>/manifests/<snapshotId>.json # portable proof bundle per snapshot
111
+ <store>/db/ # embedded PGlite metadata (history)
112
+ <store>/trusted/*.jwk # extra public keys you trust for `verify`
113
+ ```
114
+
115
+ `<store>` resolves like git: `$BF_HOME` if set, else the nearest `./.bf`
116
+ walking up from the cwd, else the global `~/.bluefield`. Your **signing key
117
+ always stays in `~/.bluefield/keys`** (relocatable with `$BF_KEYS_DIR`), so a
118
+ project-local `.bf/` never contains private material and is safe to inspect or
119
+ share.
120
+
121
+ ## Verifying someone else's capture
122
+
123
+ `bf verify` fails **closed**: a signature is only accepted if its key is one you
124
+ trust — your own local key, anything in `<store>/trusted/`, or (with explicit
125
+ `--trust-embedded`) the key carried in the bundle. An unknown key is rejected,
126
+ so an exported bundle can't self-authorize.
127
+
128
+ ## Transparency anchoring (optional)
129
+
130
+ `bf scrape --anchor` will submit the bundle to a public transparency log when
131
+ the optional anchoring module is present. It is strictly additive: if the
132
+ module isn't in your build, anchoring is skipped with a note and your capture is
133
+ still fully signed and locally verifiable.
134
+
135
+ ## Configuration (all optional)
136
+
137
+ | Env | Effect |
138
+ | --- | --- |
139
+ | `BF_HOME` | Override the store root outright |
140
+ | `BF_KEYS_DIR` | Relocate the signing-keys directory |
141
+ | `ANTHROPIC_API_KEY` | Enable `--extract` structured extraction |
142
+ | `IPROYAL_*` | Route fetches through your proxy (Patchright tier) |
143
+ | `LOG_LEVEL` | `info`/`debug` to surface the engine's structured logs (default: silent) |
144
+
145
+ ## Library
146
+
147
+ ```ts
148
+ import { openBluefield } from '@bluefields/cli';
149
+
150
+ const bf = await openBluefield(); // opens ~/.bluefield
151
+ const r = await bf.scrape('https://example.com/');
152
+ if (r.ok) {
153
+ const v = await bf.verify(r.snapshotId);
154
+ console.log(v.ok ? 'authentic' : 'FAILED');
155
+ }
156
+ await bf.close();
157
+ ```
158
+
159
+ ## MCP
160
+
161
+ Expose the local store to an AI agent (Claude Desktop, etc.):
162
+
163
+ ```json
164
+ { "mcpServers": { "bluefield": { "command": "bf", "args": ["mcp"] } } }
165
+ ```
166
+
167
+ Tools: `scrape`, `log`, `diff`, `verify`, `export`, `identity` — all local, no
168
+ API key.
169
+
170
+ ## Development (from the monorepo)
171
+
172
+ ```console
173
+ pnpm install
174
+ pnpm --filter @bluefields/cli test # offline fixture tests (PGlite + fake fetcher)
175
+ pnpm --filter @bluefields/cli typecheck
176
+ pnpm --filter @bluefields/cli build
177
+ ```
178
+
179
+ ## License
180
+
181
+ AGPL-3.0-or-later. See [LICENSE](./LICENSE).
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Optional transparency anchoring (`--anchor`).
3
+ *
4
+ * The Rekor/OpenTimestamps anchoring lives in BF-rekor's `transparency.ts`,
5
+ * which ships on a separate branch and is NOT present in every build. This
6
+ * module feature-detects it at runtime via a computed import specifier (so the
7
+ * type checker doesn't hard-require the module) and degrades gracefully to a
8
+ * clear "unavailable" result when it is absent — anchoring is strictly
9
+ * additive and never blocks a capture.
10
+ */
11
+ import type { AttestationBundle } from '@bluefields/attest';
12
+ export interface AnchorResult {
13
+ anchored: boolean;
14
+ /** Present when anchoring succeeded — the transparency proof/receipt. */
15
+ proof?: unknown;
16
+ /** Human-readable reason, especially when `anchored` is false. */
17
+ detail: string;
18
+ }
19
+ /**
20
+ * Attempt to anchor a bundle in a transparency log. Returns
21
+ * `{ anchored: false }` (never throws) when the optional module is absent or
22
+ * anchoring fails — the caller keeps its offline, locally-verifiable capture.
23
+ */
24
+ export declare function tryAnchor(bundle: AttestationBundle): Promise<AnchorResult>;
package/dist/anchor.js ADDED
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Optional transparency anchoring (`--anchor`).
4
+ *
5
+ * The Rekor/OpenTimestamps anchoring lives in BF-rekor's `transparency.ts`,
6
+ * which ships on a separate branch and is NOT present in every build. This
7
+ * module feature-detects it at runtime via a computed import specifier (so the
8
+ * type checker doesn't hard-require the module) and degrades gracefully to a
9
+ * clear "unavailable" result when it is absent — anchoring is strictly
10
+ * additive and never blocks a capture.
11
+ */
12
+ /**
13
+ * Attempt to anchor a bundle in a transparency log. Returns
14
+ * `{ anchored: false }` (never throws) when the optional module is absent or
15
+ * anchoring fails — the caller keeps its offline, locally-verifiable capture.
16
+ */
17
+ export async function tryAnchor(bundle) {
18
+ const mod = await loadTransparency();
19
+ if (!mod || typeof mod.anchorBundle !== 'function') {
20
+ return {
21
+ anchored: false,
22
+ detail: 'transparency anchoring is not available in this build (BF-rekor absent)',
23
+ };
24
+ }
25
+ try {
26
+ const proof = await mod.anchorBundle(bundle);
27
+ return { anchored: true, proof, detail: 'anchored in transparency log' };
28
+ }
29
+ catch (err) {
30
+ return {
31
+ anchored: false,
32
+ detail: `anchoring failed: ${err instanceof Error ? err.message : String(err)}`,
33
+ };
34
+ }
35
+ }
36
+ /** Dynamically resolve the optional transparency module; null if absent. */
37
+ async function loadTransparency() {
38
+ // Computed specifier: keeps NodeNext from statically requiring a module that
39
+ // is not part of this build's dependency graph.
40
+ const spec = `@bluefields/${'attest'}/transparency`;
41
+ try {
42
+ return (await import(spec));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ //# sourceMappingURL=anchor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor.js","sourceRoot":"","sources":["../src/anchor.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C;;;;;;;;;GASG;AAYH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAyB;IACvD,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACnD,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,yEAAyE;SAClF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAC7C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;IAC3E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,qBAAqB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SAChF,CAAC;IACJ,CAAC;AACH,CAAC;AAMD,4EAA4E;AAC5E,KAAK,UAAU,gBAAgB;IAC7B,6EAA6E;IAC7E,gDAAgD;IAChD,MAAM,IAAI,GAAG,eAAe,QAAQ,eAAe,CAAC;IACpD,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAuB,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Provenance glue between the snapshot store and `@bluefields/attest`.
3
+ *
4
+ * The engine signs a JCS-canonical v1 manifest and persists the detached
5
+ * signature + keyId on the snapshot row (not the manifest itself). This module
6
+ * rebuilds the exact signed manifest from a row so a capture can be verified
7
+ * offline, packages it as a portable `AttestationBundle` (manifest + signature
8
+ * + public JWK) for `export`, and builds a LOCAL trust store so `verifyBundle`
9
+ * stays fail-closed against local keys instead of the production trust store.
10
+ *
11
+ * Reconstruction is byte-exact because the manifest never stores the headers
12
+ * themselves — only `sha256(canonicalize(headers))`, and JCS canonicalization
13
+ * sorts keys, so a Postgres `jsonb` round-trip (which may reorder keys) yields
14
+ * the same hash. The signed bytes therefore reproduce identically.
15
+ */
16
+ import { type AttestationBundle, type Ed25519Jwk, type Signature, type SignedManifest, type TrustStore } from '@bluefields/attest';
17
+ import type { Snapshot } from '@bluefields/db';
18
+ /** Rebuild the exact `ManifestInput` the engine signed, from a snapshot row. */
19
+ export declare function manifestInputFromRow(row: Snapshot): {
20
+ urlCanonical: string;
21
+ canonVersion: number;
22
+ contentHash: string;
23
+ rawHtmlSizeBytes: number;
24
+ fetchedAt: string;
25
+ fetchDurationMs: number;
26
+ fetcherId: string;
27
+ proxyEgressIp: string;
28
+ userAgent: string;
29
+ responseStatus: number;
30
+ responseHeadersHash: string;
31
+ };
32
+ /** The detached signature as stored on the row. */
33
+ export declare function signatureFromRow(row: Snapshot): Signature;
34
+ /** True when the row was actually signed (vs the no-attestation placeholder). */
35
+ export declare function isSigned(row: Snapshot): boolean;
36
+ /** Reconstruct the `SignedManifest` (manifest + signature) from a row. */
37
+ export declare function signedManifestFromRow(row: Snapshot): SignedManifest;
38
+ /** Package a signed manifest + public key as a portable, self-contained bundle. */
39
+ export declare function toBundle(signed: SignedManifest, jwk: Ed25519Jwk): AttestationBundle;
40
+ /** Freeze a bundle to `<store>/manifests/<snapshotId>.json`. */
41
+ export declare function writeBundleSidecar(manifestsDir: string, snapshotId: string, bundle: AttestationBundle): void;
42
+ /** Read a frozen bundle sidecar; null if none exists. */
43
+ export declare function readBundleSidecar(manifestsDir: string, snapshotId: string): AttestationBundle | null;
44
+ /**
45
+ * Build a trust store from a set of public JWKs (keyed by their `kid`). This is
46
+ * the root of trust for local `verify`: only keys explicitly present here are
47
+ * accepted, so an exported bundle's embedded key still can't self-authorize —
48
+ * it must match a key the verifier already trusts.
49
+ */
50
+ export declare function localTrustStore(jwks: Ed25519Jwk[]): TrustStore;
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Provenance glue between the snapshot store and `@bluefields/attest`.
4
+ *
5
+ * The engine signs a JCS-canonical v1 manifest and persists the detached
6
+ * signature + keyId on the snapshot row (not the manifest itself). This module
7
+ * rebuilds the exact signed manifest from a row so a capture can be verified
8
+ * offline, packages it as a portable `AttestationBundle` (manifest + signature
9
+ * + public JWK) for `export`, and builds a LOCAL trust store so `verifyBundle`
10
+ * stays fail-closed against local keys instead of the production trust store.
11
+ *
12
+ * Reconstruction is byte-exact because the manifest never stores the headers
13
+ * themselves — only `sha256(canonicalize(headers))`, and JCS canonicalization
14
+ * sorts keys, so a Postgres `jsonb` round-trip (which may reorder keys) yields
15
+ * the same hash. The signed bytes therefore reproduce identically.
16
+ */
17
+ import { createHash } from 'node:crypto';
18
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { buildManifest, canonicalize, } from '@bluefields/attest';
21
+ /** Rebuild the exact `ManifestInput` the engine signed, from a snapshot row. */
22
+ export function manifestInputFromRow(row) {
23
+ const headers = (row.responseHeaders ?? {});
24
+ return {
25
+ urlCanonical: row.urlCanonical,
26
+ canonVersion: row.canonVersion,
27
+ contentHash: row.contentHash,
28
+ rawHtmlSizeBytes: row.rawHtmlSizeBytes,
29
+ fetchedAt: row.fetchedAt.toISOString(),
30
+ fetchDurationMs: row.fetchDurationMs,
31
+ fetcherId: row.fetcherId,
32
+ proxyEgressIp: String(row.proxyEgressIp),
33
+ userAgent: row.userAgent,
34
+ responseStatus: row.responseStatus,
35
+ responseHeadersHash: createHash('sha256').update(canonicalize(headers)).digest('hex'),
36
+ };
37
+ }
38
+ /** The detached signature as stored on the row. */
39
+ export function signatureFromRow(row) {
40
+ return {
41
+ signature: row.attestationSignature,
42
+ keyId: row.attestationKeyId,
43
+ algorithm: row.attestationAlgorithm,
44
+ };
45
+ }
46
+ /** True when the row was actually signed (vs the no-attestation placeholder). */
47
+ export function isSigned(row) {
48
+ return row.attestationSignature.length > 0 && row.attestationKeyId !== 'v0-no-attestation';
49
+ }
50
+ /** Reconstruct the `SignedManifest` (manifest + signature) from a row. */
51
+ export function signedManifestFromRow(row) {
52
+ return { manifest: buildManifest(manifestInputFromRow(row)), signature: signatureFromRow(row) };
53
+ }
54
+ /** Package a signed manifest + public key as a portable, self-contained bundle. */
55
+ export function toBundle(signed, jwk) {
56
+ return { manifest: signed.manifest, signature: signed.signature, jwk };
57
+ }
58
+ /** Sidecar path for a snapshot's bundle. */
59
+ function sidecarPath(manifestsDir, snapshotId) {
60
+ return join(manifestsDir, `${snapshotId}.json`);
61
+ }
62
+ /** Freeze a bundle to `<store>/manifests/<snapshotId>.json`. */
63
+ export function writeBundleSidecar(manifestsDir, snapshotId, bundle) {
64
+ mkdirSync(manifestsDir, { recursive: true });
65
+ writeFileSync(sidecarPath(manifestsDir, snapshotId), `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
66
+ }
67
+ /** Read a frozen bundle sidecar; null if none exists. */
68
+ export function readBundleSidecar(manifestsDir, snapshotId) {
69
+ try {
70
+ return JSON.parse(readFileSync(sidecarPath(manifestsDir, snapshotId), 'utf8'));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Build a trust store from a set of public JWKs (keyed by their `kid`). This is
78
+ * the root of trust for local `verify`: only keys explicitly present here are
79
+ * accepted, so an exported bundle's embedded key still can't self-authorize —
80
+ * it must match a key the verifier already trusts.
81
+ */
82
+ export function localTrustStore(jwks) {
83
+ const store = {};
84
+ for (const jwk of jwks) {
85
+ if (jwk.kid)
86
+ store[jwk.kid] = jwk;
87
+ }
88
+ return store;
89
+ }
90
+ //# sourceMappingURL=attestation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation.js","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The Bluefield local library — "git for web data".
3
+ *
4
+ * A thin orchestration layer over the reused engine packages:
5
+ * - `@bluefields/fetcher` → fetch + content-address + sign (fetchAndStore)
6
+ * - `@bluefields/extractor` → optional structured extraction (BYO LLM key)
7
+ * - `@bluefields/differ` → line/structural diffs between snapshots
8
+ * - `@bluefields/attest` → offline signature verification
9
+ *
10
+ * It injects a LOCAL `DbClient` (PGlite) and a LOCAL filesystem
11
+ * `StorageAdapter` so the whole engine runs with no server, no Postgres, no
12
+ * KMS, and no network beyond the fetch itself. There is NO phone-home and NO
13
+ * telemetry anywhere in this package.
14
+ *
15
+ * Everything the CLI and the MCP server do goes through this class, so both
16
+ * surfaces stay thin.
17
+ */
18
+ import { type Extraction } from '@bluefields/db';
19
+ import { type FetchOptions, type FetcherAdapter } from '@bluefields/fetcher';
20
+ import { type AnchorResult } from './anchor.js';
21
+ import { createFsStorageAdapter } from './fs-storage.js';
22
+ import { type LocalIdentity } from './keystore.js';
23
+ import { type StorePaths } from './paths.js';
24
+ import { type Store } from './store.js';
25
+ /** Options for opening a Bluefield store. */
26
+ export interface OpenOptions {
27
+ /** Override the resolved store root (default: `$BF_HOME` / nearest `.bf` / `~/.bluefield`). */
28
+ storeRoot?: string;
29
+ /** Override the keys directory (default: `~/.bluefield/keys`). Tests pass a temp dir. */
30
+ keysDir?: string;
31
+ /** Environment for fetcher/proxy/LLM config (default: `process.env`). */
32
+ env?: NodeJS.ProcessEnv;
33
+ /** Inject a fetcher adapter (test seam / BYO). Default: `getDefaultFetcher(env)`. */
34
+ fetcher?: FetcherAdapter;
35
+ /** Respect robots.txt on capture (default: true). */
36
+ respectRobotsTxt?: boolean;
37
+ }
38
+ /** A capture request. */
39
+ export interface ScrapeOptions {
40
+ /** Sign the manifest with the local key (default: true). */
41
+ sign?: boolean;
42
+ /** Run structured extraction after capture (needs `ANTHROPIC_API_KEY` unless schema is null → markdown-only). */
43
+ extract?: boolean;
44
+ /** JSON schema for structured extraction (null/omitted → markdown-only). */
45
+ schema?: Record<string, unknown> | null;
46
+ /** One-sentence extraction intent. */
47
+ intent?: string | null;
48
+ /** Attempt transparency anchoring (best-effort; no-op if BF-rekor absent). */
49
+ anchor?: boolean;
50
+ /** Per-fetch options forwarded to the fetcher adapter. */
51
+ fetchOptions?: FetchOptions;
52
+ }
53
+ /** Outcome of a capture. */
54
+ export type ScrapeResult = {
55
+ ok: true;
56
+ snapshotId: string;
57
+ contentHash: string;
58
+ url: string;
59
+ signed: boolean;
60
+ /** True when this capture's content hash differs from the previous snapshot of the URL. */
61
+ changed: boolean;
62
+ /** Present when `extract` was requested. */
63
+ extraction?: Extraction;
64
+ /** Present when `anchor` was requested. */
65
+ anchor?: AnchorResult;
66
+ } | {
67
+ ok: false;
68
+ reason: 'robots_disallowed';
69
+ url: string;
70
+ };
71
+ /** A compact snapshot summary for `log`. */
72
+ export interface SnapshotSummary {
73
+ snapshotId: string;
74
+ url: string;
75
+ contentHash: string;
76
+ fetchedAt: Date;
77
+ responseStatus: number;
78
+ sizeBytes: number;
79
+ signed: boolean;
80
+ keyId: string;
81
+ }
82
+ /** Result of a diff between two snapshots. */
83
+ export interface DiffResult {
84
+ from: SnapshotSummary;
85
+ to: SnapshotSummary;
86
+ mode: 'content' | 'structured';
87
+ /** Unified-diff text (content mode) or a JSON string of the patch ops (structured mode). */
88
+ text: string;
89
+ added: number;
90
+ removed: number;
91
+ /** Structured-mode JSON Patch ops (empty in content mode). */
92
+ ops: unknown[];
93
+ identical: boolean;
94
+ }
95
+ /** Result of verifying a snapshot or bundle. */
96
+ export interface VerifyResult {
97
+ ok: boolean;
98
+ snapshotId?: string;
99
+ keyId: string;
100
+ /** True when the on-disk content re-hashes to the manifest's content hash. */
101
+ contentHashMatches: boolean;
102
+ /** True when the detached signature verifies against a trusted key. */
103
+ signatureValid: boolean;
104
+ detail: string;
105
+ }
106
+ /** Result of exporting a snapshot to a directory. */
107
+ export interface ExportResult {
108
+ snapshotId: string;
109
+ dir: string;
110
+ contentFile: string;
111
+ bundleFile: string;
112
+ }
113
+ /** Open (or create) a local Bluefield store. Call `close()` when done. */
114
+ export declare function openBluefield(opts?: OpenOptions): Promise<Bluefield>;
115
+ export declare class Bluefield {
116
+ readonly paths: StorePaths;
117
+ readonly identity: LocalIdentity;
118
+ private readonly store;
119
+ private readonly storage;
120
+ private readonly fetcher;
121
+ private readonly env;
122
+ private readonly respectRobotsTxt;
123
+ constructor(paths: StorePaths, identity: LocalIdentity, store: Store, storage: ReturnType<typeof createFsStorageAdapter>, fetcher: FetcherAdapter, env: NodeJS.ProcessEnv, respectRobotsTxt: boolean);
124
+ private get db();
125
+ /** Capture a URL: fetch → content-address on disk → sign → record → sidecar. */
126
+ scrape(url: string, opts?: ScrapeOptions): Promise<ScrapeResult>;
127
+ /** Run the extractor over a snapshot's HTML (BYO LLM key via env). */
128
+ private runExtract;
129
+ /** History: newest-first snapshots, optionally filtered to one URL. */
130
+ log(url?: string | null, opts?: {
131
+ limit?: number;
132
+ }): Promise<SnapshotSummary[]>;
133
+ /**
134
+ * Diff two snapshots. With one URL and no explicit `to`, diffs the two most
135
+ * recent snapshots of that URL. `content` mode line-diffs the raw bodies;
136
+ * `structured` mode JSON-patch-diffs their extractions' structured data.
137
+ */
138
+ diff(fromRef: string, toRef?: string, opts?: {
139
+ mode?: 'content' | 'structured';
140
+ }): Promise<DiffResult>;
141
+ /**
142
+ * Poll a URL on an interval, capturing each tick and reporting whether the
143
+ * content changed. Runs `times` iterations (default: forever) or until the
144
+ * `signal` aborts. `onTick` is invoked after every capture.
145
+ */
146
+ watch(url: string, opts?: {
147
+ intervalMs?: number;
148
+ times?: number;
149
+ signal?: AbortSignal;
150
+ onTick?: (r: ScrapeResult, iteration: number) => void;
151
+ scrape?: ScrapeOptions;
152
+ }): Promise<ScrapeResult[]>;
153
+ /**
154
+ * Verify a snapshot (by ref) or a previously-exported bundle. Recomputes the
155
+ * content hash from the stored object AND checks the signature against a
156
+ * trusted key (the local identity plus any keys under `<store>/trusted`, plus
157
+ * `extraKeys`). Fails closed on an unknown key unless `trustEmbedded` is set.
158
+ */
159
+ verify(ref: string, opts?: {
160
+ trustEmbedded?: boolean;
161
+ extraKeys?: import('@bluefields/attest').Ed25519Jwk[];
162
+ }): Promise<VerifyResult>;
163
+ /** Write a snapshot's raw content + portable bundle into `destDir`. */
164
+ export(ref: string, destDir: string): Promise<ExportResult>;
165
+ /** Retrieve the raw stored content for a snapshot ref. */
166
+ getContent(ref: string): Promise<string>;
167
+ /** The public identity (safe to share). */
168
+ publicIdentity(): {
169
+ keyId: string;
170
+ publicJwk: import("@bluefields/attest").Ed25519Jwk;
171
+ };
172
+ close(): Promise<void>;
173
+ private loadTrustedKeys;
174
+ private snapshotById;
175
+ private latestForUrl;
176
+ /** Resolve a ref (snapshot-id prefix, or URL → latest) to a row, or null. */
177
+ private resolve;
178
+ private mustResolve;
179
+ private extractionFor;
180
+ }