protect-mcp 0.9.1 → 0.9.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3: the skeptic's tools sharpen
4
+
5
+ Two verification upgrades: the anchor check moves into the verifier, and the
6
+ record viewer proves signatures instead of just labelling them.
7
+
8
+ - `verify-claim` now verifies the anchor automatically: it finds the
9
+ `<claim>.anchor.json` sidecar, checks offline that the anchored envelope binds
10
+ this exact claim (digest, record root, and the same issuer key), and confirms
11
+ against the public log that the digest sits at the recorded entry. A failed
12
+ binding makes the attestation INVALID; an unreachable log does not (the local
13
+ checks stand alone). `--check-anchor` makes a missing anchor fatal,
14
+ `--anchor-file <p>` overrides the path, `--offline` skips the network hop.
15
+ - The `record` viewer now verifies Ed25519 signatures in the browser, locally:
16
+ each row shows `✓ verified`, `✗ invalid signature`, or `signed · unpinned
17
+ key`, and the stat strip counts how many verified. The CLI injects your
18
+ gateway's PUBLIC key so rows verify against your own key (the private key
19
+ never reaches the page).
20
+ - Receipts now embed the signer's public key inside the signed payload
21
+ (`payload.public_key`), so a receipt is self-contained: any verifier can check
22
+ it without a side channel, and the key cannot be swapped without breaking the
23
+ signature. Older receipts still verify against a pinned or pasted key.
24
+
25
+ ## 0.9.2: anchor a claim to the public log
26
+
27
+ Closes the one honest gap in a bare claim: that the disclosed set is complete.
28
+
29
+ - Added `protect-mcp claim --anchor`: records the claim's digest in the public,
30
+ append-only ScopeBlind transparency log, so a counterparty who does not trust
31
+ you can confirm the exact claim existed at a time and was not quietly re-cut.
32
+ Only the hash is sent; the claim, its leaves, and every receipt stay on your
33
+ machine. Writes a `<claim>.anchor.json` sidecar with the log entry and a
34
+ verify URL. Anonymous and free; enrolling an org identity a counterparty can
35
+ pin is the paid upgrade (scopeblind.com/enroll).
36
+
37
+ ## 0.9.1: prove a claim without revealing the record
38
+
39
+ The record viewer closes the loop from a decision to a portable proof.
40
+
41
+ - The `record` viewer surfaces the exact `claim` command when you filter by a
42
+ capability, so a decision becomes a signed, position-blind attestation in one
43
+ step, with copy-to-clipboard and an offline `verify-claim` hint.
44
+ - The browser drop-viewer (legate.scopeblind.com/record) reached parity with
45
+ the CLI: signed capability tags, a capability facet, and a provenance tree.
46
+
47
+ ## 0.9.0: signed, position-blind claim attestations
48
+
49
+ Prove a predicate over the record without revealing it.
50
+
51
+ - Added `protect-mcp claim`: mints a signed attestation of a predicate over the
52
+ record (`--no <cap>` for "no action carried this capability", `--only`,
53
+ `--count`/`--no-verdict <verdict>`), disclosing only per-decision categories
54
+ (receipt digest, verdict, capability tags), never inputs, outputs, or data.
55
+ - Added `protect-mcp verify-claim`: checks it offline via Ed25519 over the pack,
56
+ a recomputed Merkle root over the disclosed set, and the predicate recomputed
57
+ independently. Honest trust model: an accountable position-blind attestation,
58
+ not zero-knowledge; set-completeness is issuer-attested.
59
+
60
+ ## 0.8.0: deterministic receipt enrichment
61
+
62
+ Each signed decision now carries minimum-disclosure, rule-derived metadata.
63
+
64
+ - `input_digest`: SHA-256 of the canonicalized tool input, so you can prove what
65
+ was requested without storing it.
66
+ - Signed `capabilities`: deterministic, rule-derived tags (fs.read, fs.write,
67
+ net.egress, secret.adjacent, destructive, financial, exec.shell, ...) so the
68
+ record is queryable by what an action touched.
69
+ - `resource`: the hashed target (path, host, or command). The gate sees tool
70
+ calls, not the model's prompt or reasoning, and does not claim to.
71
+
72
+ ## 0.7.6: the live record viewer
73
+
74
+ - Added `protect-mcp record --live`: a local, streaming view of the receipt
75
+ file that turns decisions into a searchable, auto-labelled record as the agent
76
+ runs (stat strip, per-row signed badge and digest, a List/Tree provenance
77
+ view), all local, nothing uploaded.
78
+ - One-click export: `.jsonl` carrying the real signatures (so a recipient
79
+ verifies offline with `npx @veritasacta/verify`) plus a Markdown report.
80
+
81
+ ## 0.7.5: honest version strings and the record viewer
82
+
83
+ - Added `protect-mcp record`, a local browser viewer over the receipts file
84
+ (the answer to "the receipts are a hidden dotfile I cannot find").
85
+ - Fixed hard-coded version strings in the banner and health/serverInfo: they
86
+ read the resolved package version now.
87
+ - WebAuthn co-sign hardening: verify the rpIdHash and enforce challenge expiry.
88
+
3
89
  ## 0.7.4: the self-serve trial path
4
90
 
5
91
  Everything the scopeblind.com trial and docs pages describe now ships in the
package/README.md CHANGED
@@ -148,6 +148,43 @@ disclosure, not full zero-knowledge, but it makes the privacy claim concrete:
148
148
  auditors can verify selected facts without receiving the full tool payload or
149
149
  sensitive desk context.
150
150
 
151
+ ### Prove a claim over the record (position-blind attestations)
152
+
153
+ You can prove a CLAIM over your record without revealing it. Mint a signed,
154
+ position-blind attestation over the whole record that discloses only per-decision
155
+ categories (a receipt digest, the verdict, capability tags), never your tool
156
+ inputs, outputs, or data:
157
+
158
+ ```bash
159
+ # "No action reached the network across the record":
160
+ npx protect-mcp claim --no net.egress
161
+
162
+ # other predicates:
163
+ # --only fs.read,fs.write all actions were confined to these capabilities
164
+ # --no-verdict blocked no action was blocked
165
+ # --count blocked how many were blocked
166
+ ```
167
+
168
+ Anyone verifies it offline, seeing only the categories, never the content:
169
+
170
+ ```bash
171
+ npx protect-mcp verify-claim claim-<id>.json
172
+ ```
173
+
174
+ The verifier recomputes a Merkle root over the disclosed set and recomputes the
175
+ predicate independently, so the issuer cannot lie about the claim given the
176
+ disclosure. Add `--anchor` to record the claim's digest in the public,
177
+ append-only ScopeBlind transparency log, so a counterparty who does not trust you
178
+ can confirm the disclosed set is complete and was not quietly re-cut (only the
179
+ hash is sent; the record stays local):
180
+
181
+ ```bash
182
+ npx protect-mcp claim --no net.egress --anchor
183
+ ```
184
+
185
+ This is an accountable, position-blind attestation, not full zero-knowledge: it
186
+ reveals the shape, not the content.
187
+
151
188
  ## Claude Code hook quickstart
152
189
 
153
190
  ```bash
@@ -189,7 +226,7 @@ session always runs the gate you tested:
189
226
  "hooks": [
190
227
  {
191
228
  "type": "command",
192
- "command": "npx protect-mcp@0.7.3 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
229
+ "command": "npx protect-mcp@0.9.1 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
193
230
  }
194
231
  ]
195
232
  }
@@ -200,7 +237,7 @@ session always runs the gate you tested:
200
237
  "hooks": [
201
238
  {
202
239
  "type": "command",
203
- "command": "npx protect-mcp@0.7.3 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
240
+ "command": "npx protect-mcp@0.9.1 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
204
241
  }
205
242
  ]
206
243
  }
@@ -335,6 +372,9 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
335
372
  | `dashboard` | Start a local-only dashboard on `127.0.0.1` showing tool inventory, risk, policy coverage, exact-action approvals, receipt chains, and audit export. |
336
373
  | `recommend` | Draft a reviewable JSON policy from observed local calls. Dry-run by default; use `--write` to create `protect-mcp.recommended.json`. |
337
374
  | `registry` | Create an org identity, anchor receipt digests, and write a static verifier page. Hosted mode uploads digests only. |
375
+ | `record` | Open a local, searchable viewer over your receipts (`--live` streams as the agent runs): Ed25519 signatures verified in your browser against your gateway key, capability tags, a provenance tree, and one-click signed export. All local, nothing uploaded. |
376
+ | `claim` | Mint a signed, position-blind attestation of a predicate over the record (`--no <cap>`, `--only <c1,c2>`, `--no-verdict <verdict>`, `--count <verdict>`), disclosing only decision categories. Add `--anchor` to record the claim digest in the public transparency log. |
377
+ | `verify-claim` | Verify a claim pack offline: signature, recomputed Merkle root, independently recomputed predicate, and the anchor sidecar when present (binds the anchored envelope to this exact claim, then confirms the public log holds it). `--check-anchor` requires the anchor; `--offline` skips the log hop. |
338
378
  | `killer-demo` | Generate a complete shadow-mode to policy to approval to signed-receipt demo pack. |
339
379
  | `verify-disclosure` | Verify a `scopeblind.selective_disclosure.v0` package and explain disclosed versus hidden fields. |
340
380
  | `policy-packs` | List, inspect, and install starter Cedar policy packs. |
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-GHR65WVD.mjs";
3
+ } from "./chunk-PB3TC7E3.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-IDUH2O4Q.mjs";
8
+ } from "./chunk-XLJUZ4WO.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildEnrichment
3
- } from "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-HPKHMVZX.mjs";
4
4
  import {
5
5
  ReceiptBuffer,
6
6
  buildActionReadback,
@@ -15,7 +15,7 @@ import {
15
15
  loadPolicy,
16
16
  parseRateLimit,
17
17
  signDecision
18
- } from "./chunk-IDUH2O4Q.mjs";
18
+ } from "./chunk-XLJUZ4WO.mjs";
19
19
 
20
20
  // src/hook-server.ts
21
21
  import { createServer } from "http";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-D733KAPG.mjs";
7
7
 
8
8
  // src/receipt-enrichment.ts
9
- var ENRICHMENT_VERSION = 1;
9
+ var ENRICHMENT_VERSION = 2;
10
10
  function canonicalJson(value) {
11
11
  const seen = /* @__PURE__ */ new WeakSet();
12
12
  const enc = (v) => {
@@ -8,7 +8,7 @@ import {
8
8
  parseRateLimit,
9
9
  signDecision,
10
10
  startStatusServer
11
- } from "./chunk-IDUH2O4Q.mjs";
11
+ } from "./chunk-XLJUZ4WO.mjs";
12
12
 
13
13
  // src/evidence-store.ts
14
14
  import { readFileSync, writeFileSync, existsSync } from "fs";
@@ -177,7 +177,13 @@ function signDecision(entry) {
177
177
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
178
178
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
179
179
  // - uncertified = unsigned receipt (shadow mode, no signing configured)
180
- issuer_certification: signerState ? "self-signed" : "uncertified"
180
+ issuer_certification: signerState ? "self-signed" : "uncertified",
181
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
182
+ // self-contained: any verifier (including the record viewer, in-browser)
183
+ // can check the signature without a side channel. Binding the key inside
184
+ // the signature means it cannot be swapped without breaking the signature;
185
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
186
+ public_key: signerState.publicKey
181
187
  };
182
188
  if (entry.tier) payload.tier = entry.tier;
183
189
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -0,0 +1,276 @@
1
+ import {
2
+ canonicalJson
3
+ } from "./chunk-HPKHMVZX.mjs";
4
+ import {
5
+ sha256
6
+ } from "./chunk-AYNQIEN7.mjs";
7
+ import {
8
+ ed25519
9
+ } from "./chunk-FFVJL3KQ.mjs";
10
+ import "./chunk-JIDDQUSQ.mjs";
11
+ import {
12
+ bytesToHex,
13
+ hexToBytes
14
+ } from "./chunk-D733KAPG.mjs";
15
+ import "./chunk-PQJP2ZCI.mjs";
16
+
17
+ // src/claim.ts
18
+ var CLAIM_TYPE = "scopeblind.claim.v1";
19
+ function sha256Hex(input) {
20
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
21
+ return bytesToHex(sha256(bytes));
22
+ }
23
+ function receiptToLeaf(e) {
24
+ const p = e && typeof e.payload === "object" && e.payload || e;
25
+ const dec = String(p.decision || e.decision || "").toLowerCase();
26
+ const v = /den|block|reject|refus/.test(dec) ? "blocked" : /ask|approv|hold|escal|review|pending/.test(dec) ? "held" : "allowed";
27
+ const enr = p.enrichment;
28
+ const c = enr && Array.isArray(enr.capabilities) ? enr.capabilities.map(String).sort() : [];
29
+ const tsRaw = e.issued_at || p.timestamp || p.issued_at;
30
+ const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
31
+ const t = isFinite(ms) ? new Date(ms).toISOString() : "";
32
+ const d = sha256Hex(canonicalJson(e));
33
+ return { d, v, c, t };
34
+ }
35
+ function leafHash(leaf) {
36
+ return sha256Hex(canonicalJson(leaf));
37
+ }
38
+ function merkleRoot(leafHashes) {
39
+ if (leafHashes.length === 0) return sha256Hex("scopeblind.claim.empty");
40
+ let level = [...leafHashes].sort();
41
+ while (level.length > 1) {
42
+ const next = [];
43
+ for (let i = 0; i < level.length; i += 2) {
44
+ const a = level[i];
45
+ const b = i + 1 < level.length ? level[i + 1] : level[i];
46
+ next.push(sha256Hex(a + b));
47
+ }
48
+ level = next;
49
+ }
50
+ return level[0];
51
+ }
52
+ function evaluate(pred, leaves) {
53
+ if (pred.kind === "no_capability") {
54
+ const matched2 = leaves.filter((l) => l.c.indexOf(pred.capability) >= 0).length;
55
+ return { statement: `No action used capability "${pred.capability}"`, holds: matched2 === 0, matched: matched2 };
56
+ }
57
+ if (pred.kind === "only_capabilities") {
58
+ const allow = new Set(pred.capabilities);
59
+ const matched2 = leaves.filter((l) => !l.c.every((c) => allow.has(c))).length;
60
+ return { statement: `All actions were confined to capabilities {${pred.capabilities.join(", ")}}`, holds: matched2 === 0, matched: matched2 };
61
+ }
62
+ if (pred.kind === "no_verdict") {
63
+ const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
64
+ return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
65
+ }
66
+ const matched = leaves.filter((l) => l.v === pred.verdict).length;
67
+ return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
68
+ }
69
+ function messageHash(unsigned) {
70
+ return sha256(new TextEncoder().encode(canonicalJson(unsigned)));
71
+ }
72
+ function buildClaim(receipts, predicate, key, issuedAt) {
73
+ const leaves = receipts.map(receiptToLeaf);
74
+ const root = merkleRoot(leaves.map(leafHash));
75
+ const claim = evaluate(predicate, leaves);
76
+ const times = leaves.map((l) => l.t).filter(Boolean).sort();
77
+ const unsigned = {
78
+ type: CLAIM_TYPE,
79
+ predicate,
80
+ claim,
81
+ scope: { total: leaves.length, from: times[0] || "", to: times[times.length - 1] || "" },
82
+ record: { root },
83
+ leaves,
84
+ issuer: { kid: key.kid, publicKey: key.publicKey, issuer: key.issuer || "protect-mcp" },
85
+ issued_at: issuedAt
86
+ };
87
+ const signature = bytesToHex(ed25519.sign(messageHash(unsigned), hexToBytes(key.privateKey)));
88
+ return { ...unsigned, signature };
89
+ }
90
+ function verifyClaim(pack, overridePublicKey) {
91
+ const reasons = [];
92
+ const leaves = Array.isArray(pack.leaves) ? pack.leaves : [];
93
+ const recomputedRoot = merkleRoot(leaves.map(leafHash));
94
+ const root_ok = !!pack.record && recomputedRoot === pack.record.root;
95
+ if (!root_ok) reasons.push("record commitment (Merkle root) does not match the disclosed decisions");
96
+ const recomputed = evaluate(pack.predicate, leaves);
97
+ const predicate_ok = !!pack.claim && recomputed.holds === pack.claim.holds && recomputed.matched === pack.claim.matched;
98
+ if (!predicate_ok) reasons.push("claim result does not match the predicate recomputed over the disclosed decisions");
99
+ let authentic = false;
100
+ try {
101
+ const { signature, ...unsigned } = pack;
102
+ const pub = overridePublicKey || pack.issuer && pack.issuer.publicKey;
103
+ if (pub && signature) {
104
+ authentic = ed25519.verify(hexToBytes(signature), messageHash(unsigned), hexToBytes(pub));
105
+ }
106
+ } catch {
107
+ }
108
+ if (!authentic) reasons.push("signature does not verify against the issuer public key");
109
+ return {
110
+ valid: authentic && root_ok && predicate_ok,
111
+ authentic,
112
+ root_ok,
113
+ predicate_ok,
114
+ holds: !!(pack.claim && pack.claim.holds),
115
+ matched: pack.claim ? pack.claim.matched : recomputed.matched,
116
+ total: leaves.length,
117
+ statement: pack.claim ? pack.claim.statement : recomputed.statement,
118
+ reasons
119
+ };
120
+ }
121
+ var ANCHOR_SCHEMA = "scopeblind.protect-mcp.anchor.v1";
122
+ var DEFAULT_LOG = "https://scopeblind.com";
123
+ function anchorDeepSort(o) {
124
+ if (o === null || typeof o !== "object") return o;
125
+ if (Array.isArray(o)) return o.map(anchorDeepSort);
126
+ const src = o;
127
+ const out = {};
128
+ for (const k of Object.keys(src).sort()) out[k] = anchorDeepSort(src[k]);
129
+ return out;
130
+ }
131
+ function toBase64(bytes) {
132
+ let bin = "";
133
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
134
+ return btoa(bin);
135
+ }
136
+ function claimDigest(pack) {
137
+ return sha256Hex(canonicalJson(pack));
138
+ }
139
+ function buildAnchorEnvelope(pack, key, issuedAt) {
140
+ const signed = {
141
+ type: "evidence_pack",
142
+ schema: ANCHOR_SCHEMA,
143
+ anchors: "protect-mcp-claim",
144
+ claim_digest: claimDigest(pack),
145
+ record_root: pack.record.root,
146
+ statement: pack.claim.statement,
147
+ holds: pack.claim.holds,
148
+ matched: pack.claim.matched,
149
+ total: pack.scope.total,
150
+ issued_at: issuedAt,
151
+ verification_key: key.publicKey,
152
+ disclosure: "internal"
153
+ };
154
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
155
+ const digest = bytesToHex(hash);
156
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
157
+ return { ...signed, signature, digest };
158
+ }
159
+ function verifyAnchorEnvelope(pack, envelope) {
160
+ const reasons = [];
161
+ if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
162
+ return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
163
+ }
164
+ const expected = claimDigest(pack);
165
+ if (envelope.claim_digest !== expected) {
166
+ reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
167
+ }
168
+ if (envelope.record_root !== pack.record.root) {
169
+ reasons.push("anchored envelope commits to a different record root");
170
+ }
171
+ if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
172
+ reasons.push("anchor was signed by a different key than the claim issuer");
173
+ }
174
+ try {
175
+ const { signature, digest, ...signed } = envelope;
176
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
177
+ if (bytesToHex(hash) !== String(digest).toLowerCase()) {
178
+ reasons.push("envelope digest does not match its contents");
179
+ } else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
180
+ reasons.push("envelope signature does not verify");
181
+ }
182
+ } catch {
183
+ reasons.push("envelope signature does not verify");
184
+ }
185
+ return { ok: reasons.length === 0, reasons };
186
+ }
187
+ async function checkClaimAnchor(pack, sidecar, opts) {
188
+ const reasons = [];
189
+ const envelope = sidecar && sidecar.envelope;
190
+ if (!envelope) {
191
+ return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
192
+ }
193
+ const local = verifyAnchorEnvelope(pack, envelope);
194
+ reasons.push(...local.reasons);
195
+ const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
196
+ const out = {
197
+ local_ok: local.ok,
198
+ log_ok: null,
199
+ seq: sidecar.seq,
200
+ anchored_at: sidecar.anchored_at,
201
+ entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
202
+ reasons
203
+ };
204
+ if (opts?.offline) return out;
205
+ const doFetch = opts?.fetchImpl || globalThis.fetch;
206
+ if (!doFetch) return out;
207
+ try {
208
+ const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
209
+ const data = await resp.json().catch(() => null);
210
+ if (!resp.ok || !data) {
211
+ out.log_ok = null;
212
+ return out;
213
+ }
214
+ if (data.anchored !== true) {
215
+ out.log_ok = false;
216
+ out.reasons.push("the public log does not contain this anchor digest");
217
+ return out;
218
+ }
219
+ if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
220
+ out.log_ok = false;
221
+ out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
222
+ return out;
223
+ }
224
+ out.log_ok = true;
225
+ if (typeof data.seq === "number") out.seq = data.seq;
226
+ return out;
227
+ } catch {
228
+ out.log_ok = null;
229
+ return out;
230
+ }
231
+ }
232
+ async function anchorClaim(pack, key, opts) {
233
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
234
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
235
+ const doFetch = opts.fetchImpl || globalThis.fetch;
236
+ if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
237
+ const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
238
+ try {
239
+ const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
240
+ method: "POST",
241
+ headers: { "content-type": "application/json" },
242
+ body: JSON.stringify({ encoded })
243
+ });
244
+ const data = await resp.json().catch(() => null);
245
+ if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
246
+ return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
247
+ }
248
+ return {
249
+ ok: true,
250
+ claim_digest: envelope.claim_digest,
251
+ seq: data.seq,
252
+ entry_url: `${base}/fn/log/${data.seq}`,
253
+ anchored_at: data.anchored_at,
254
+ already_anchored: !!data.already_anchored,
255
+ envelope
256
+ };
257
+ } catch {
258
+ return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
259
+ }
260
+ }
261
+ export {
262
+ ANCHOR_SCHEMA,
263
+ CLAIM_TYPE,
264
+ DEFAULT_LOG,
265
+ anchorClaim,
266
+ buildAnchorEnvelope,
267
+ buildClaim,
268
+ checkClaimAnchor,
269
+ claimDigest,
270
+ evaluate,
271
+ leafHash,
272
+ merkleRoot,
273
+ receiptToLeaf,
274
+ verifyAnchorEnvelope,
275
+ verifyClaim
276
+ };