protect-mcp 0.9.2 → 0.9.4

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,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.4: agent payments get receipts, records get heartbeats, anchors get names
4
+
5
+ The provenance layer reaches the agentic economy's payment rails (x402), the
6
+ record gains continuous completeness, and anchoring can carry an identity.
7
+
8
+ - **Payment receipts (x402 interop).** The gate now tags agent payments with a
9
+ signed `payment` capability, detected broadly across x402 wire shapes
10
+ (`paymentRequirements`, `X-PAYMENT`, EIP-3009 `transferWithAuthorization`)
11
+ and payment-shaped tools. Receipts carry minimum-disclosure payment facts:
12
+ amount (only when clearly readable in human units), asset, a HASHED recipient,
13
+ and the x402 scheme. `claim --no payment` proves no agent paid anything;
14
+ `claim --payment-under <cap>` proves every payment stayed under a cap, and an
15
+ amount the gate could not read counts as OVER (you cannot prove an amount you
16
+ could not read, so the claim cannot lie).
17
+ - **Record checkpoints (`anchor-record`).** Anchors the record's CURRENT
18
+ commitment (the same Merkle root a claim commits to, plus count and time
19
+ range) into the public log. Run it on a heartbeat and the record grows an
20
+ anchored history: a later claim whose root matches a checkpoint is provably
21
+ over the complete set as of that checkpoint. Skips when the record is
22
+ unchanged; writes a local `.protect-mcp-anchors.jsonl` history; only the
23
+ root, count, and time range leave the machine.
24
+ - **Pinned identity in the anchor loop.** `claim --anchor`, `anchor-record`,
25
+ and `verify-claim` now resolve the anchoring key against the public ScopeBlind
26
+ key directory: an enrolled key shows "anchored as <Org> (key pinned)", a
27
+ revoked key fails verification, an anonymous key points at enrollment. The
28
+ free anchor stays anonymous; the named identity is the paid upgrade.
29
+
30
+ ## 0.9.3: the skeptic's tools sharpen
31
+
32
+ Two verification upgrades: the anchor check moves into the verifier, and the
33
+ record viewer proves signatures instead of just labelling them.
34
+
35
+ - `verify-claim` now verifies the anchor automatically: it finds the
36
+ `<claim>.anchor.json` sidecar, checks offline that the anchored envelope binds
37
+ this exact claim (digest, record root, and the same issuer key), and confirms
38
+ against the public log that the digest sits at the recorded entry. A failed
39
+ binding makes the attestation INVALID; an unreachable log does not (the local
40
+ checks stand alone). `--check-anchor` makes a missing anchor fatal,
41
+ `--anchor-file <p>` overrides the path, `--offline` skips the network hop.
42
+ - The `record` viewer now verifies Ed25519 signatures in the browser, locally:
43
+ each row shows `✓ verified`, `✗ invalid signature`, or `signed · unpinned
44
+ key`, and the stat strip counts how many verified. The CLI injects your
45
+ gateway's PUBLIC key so rows verify against your own key (the private key
46
+ never reaches the page).
47
+ - Receipts now embed the signer's public key inside the signed payload
48
+ (`payload.public_key`), so a receipt is self-contained: any verifier can check
49
+ it without a side channel, and the key cannot be swapped without breaking the
50
+ signature. Older receipts still verify against a pinned or pasted key.
51
+
3
52
  ## 0.9.2: anchor a claim to the public log
4
53
 
5
54
  Closes the one honest gap in a bare claim: that the disclosed set is complete.
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
@@ -335,6 +372,10 @@ 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>` incl. `--no payment`, `--only <c1,c2>`, `--no-verdict <verdict>`, `--count <verdict>`, `--payment-under <cap>`), disclosing only decision categories. Add `--anchor` to record the claim digest in the public transparency log; enrolled keys anchor as a named org. |
377
+ | `anchor-record` | Checkpoint the record's Merkle root + count + time range into the public log (heartbeat-friendly: skips when unchanged). A later claim whose commitment matches an anchored checkpoint is provably over the complete record as of that checkpoint. |
378
+ | `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
379
  | `killer-demo` | Generate a complete shadow-mode to policy to approval to signed-receipt demo pack. |
339
380
  | `verify-disclosure` | Verify a `scopeblind.selective_disclosure.v0` package and explain disclosed versus hidden fields. |
340
381
  | `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-WWPQNIVF.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";
@@ -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";
@@ -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) => {
@@ -43,7 +43,11 @@ var RULES = [
43
43
  { cap: "secret.adjacent", text: /\.env\b|secret|credential|passwd|password|api[_-]?key|private[_-]?key|\.pem\b|\.key\b|id_rsa|bearer\s|aws_(access|secret)|authorization/ },
44
44
  { cap: "destructive", text: /rm\s+-[a-z]*[rf]|\brmdir\b|drop\s+table|truncate\s+table|delete\s+from|reset\s+--hard|--force\b|\bmkfs\b|\bdd\s+if=|shutdown|reboot|kill\s+-9|>\s*\/dev\/sd/ },
45
45
  { cap: "financial", text: /\b(order|trade|buy|sell|transfer|wire|payment|withdraw|deposit|swap|invoice|charge|refund|settle)\b/ },
46
- { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ }
46
+ { cap: "data.query", text: /\bselect\s+[\s\S]+\bfrom\b|\binsert\s+into\b|\bupdate\s+[\s\S]+\bset\b|\bdelete\s+from\b/ },
47
+ // Agent payments (x402 / value transfer). Deliberately BROAD: a false positive
48
+ // only makes a `claim --no payment` harder to assert (conservative); a false
49
+ // negative would let a real payment escape the record's payment claims.
50
+ { cap: "payment", tool: /(^|[_.-])(pay|payment|x402|checkout)($|[_.-])|wallet.*send|send.*payment/, text: /x402|x-payment|paymentrequirements|maxamountrequired|payto|"pay_to"|eip-3009|transferwithauthorization|payment_intent|send_payment|create_payment/ }
47
51
  ];
48
52
  function deriveCapabilities(tool, input) {
49
53
  const t = String(tool || "").toLowerCase();
@@ -77,6 +81,33 @@ function deriveResource(input) {
77
81
  }
78
82
  return void 0;
79
83
  }
84
+ function findField(input, names, depth = 0) {
85
+ if (depth > 4 || input === null || typeof input !== "object") return void 0;
86
+ const o = input;
87
+ const keys = Object.keys(o).sort();
88
+ for (const k of keys) {
89
+ if (names.indexOf(k.toLowerCase()) >= 0 && o[k] !== void 0 && o[k] !== null) return o[k];
90
+ }
91
+ for (const k of keys) {
92
+ const v = findField(o[k], names, depth + 1);
93
+ if (v !== void 0) return v;
94
+ }
95
+ return void 0;
96
+ }
97
+ function derivePayment(tool, input) {
98
+ if (deriveCapabilities(tool, input).indexOf("payment") < 0) return void 0;
99
+ const p = { amount: null, asset: null, recipient_digest: null };
100
+ const amt = findField(input, ["amount"]);
101
+ if (typeof amt === "number" && Number.isFinite(amt) && amt >= 0) p.amount = amt;
102
+ else if (typeof amt === "string" && /^\d{1,15}(\.\d{1,18})?$/.test(amt.trim()) && amt.indexOf(".") >= 0) p.amount = parseFloat(amt);
103
+ const asset = findField(input, ["asset", "currency", "token"]);
104
+ if (typeof asset === "string" && asset.trim()) p.asset = asset.trim().slice(0, 64);
105
+ const to = findField(input, ["payto", "pay_to", "recipient", "destination", "to"]);
106
+ if (typeof to === "string" && to.trim()) p.recipient_digest = sha256Hex(to.trim().toLowerCase());
107
+ const scheme = findField(input, ["scheme"]);
108
+ if (typeof scheme === "string" && scheme.trim()) p.scheme = scheme.trim().slice(0, 32);
109
+ return p;
110
+ }
80
111
  function buildEnrichment(tool, input) {
81
112
  const e = {
82
113
  v: ENRICHMENT_VERSION,
@@ -85,6 +116,8 @@ function buildEnrichment(tool, input) {
85
116
  };
86
117
  const resource = deriveResource(input);
87
118
  if (resource) e.resource = resource;
119
+ const payment = derivePayment(tool, input);
120
+ if (payment) e.payment = payment;
88
121
  return e;
89
122
  }
90
123
 
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalJson
3
- } from "./chunk-KMNXHGGT.mjs";
3
+ } from "./chunk-WWPQNIVF.mjs";
4
4
  import {
5
5
  sha256
6
6
  } from "./chunk-AYNQIEN7.mjs";
@@ -30,7 +30,13 @@ function receiptToLeaf(e) {
30
30
  const ms = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
31
31
  const t = isFinite(ms) ? new Date(ms).toISOString() : "";
32
32
  const d = sha256Hex(canonicalJson(e));
33
- return { d, v, c, t };
33
+ const leaf = { d, v, c, t };
34
+ const pay = enr && enr.payment;
35
+ if (pay && typeof pay === "object") {
36
+ const amt = pay.amount;
37
+ leaf.p = typeof amt === "number" && Number.isFinite(amt) ? amt : null;
38
+ }
39
+ return leaf;
34
40
  }
35
41
  function leafHash(leaf) {
36
42
  return sha256Hex(canonicalJson(leaf));
@@ -63,6 +69,10 @@ function evaluate(pred, leaves) {
63
69
  const matched2 = leaves.filter((l) => l.v === pred.verdict).length;
64
70
  return { statement: `No action was ${pred.verdict}`, holds: matched2 === 0, matched: matched2 };
65
71
  }
72
+ if (pred.kind === "payment_under") {
73
+ const matched2 = leaves.filter((l) => "p" in l && (l.p === null || l.p >= pred.cap)).length;
74
+ return { statement: `Every payment stayed under ${pred.cap} (unknown amounts count as over)`, holds: matched2 === 0, matched: matched2 };
75
+ }
66
76
  const matched = leaves.filter((l) => l.v === pred.verdict).length;
67
77
  return { statement: `${matched} action${matched === 1 ? " was" : "s were"} ${pred.verdict}`, holds: true, matched };
68
78
  }
@@ -156,11 +166,82 @@ function buildAnchorEnvelope(pack, key, issuedAt) {
156
166
  const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
157
167
  return { ...signed, signature, digest };
158
168
  }
159
- async function anchorClaim(pack, key, opts) {
160
- const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
161
- const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
162
- const doFetch = opts.fetchImpl || globalThis.fetch;
163
- if (!doFetch) return { ok: false, claim_digest: envelope.claim_digest, error: "fetch_unavailable", envelope };
169
+ function verifyAnchorEnvelope(pack, envelope) {
170
+ const reasons = [];
171
+ if (!envelope || envelope.type !== "evidence_pack" || envelope.anchors !== "protect-mcp-claim") {
172
+ return { ok: false, reasons: ["sidecar does not contain a protect-mcp claim anchor envelope"] };
173
+ }
174
+ const expected = claimDigest(pack);
175
+ if (envelope.claim_digest !== expected) {
176
+ reasons.push("anchored envelope binds a DIFFERENT claim (claim_digest mismatch)");
177
+ }
178
+ if (envelope.record_root !== pack.record.root) {
179
+ reasons.push("anchored envelope commits to a different record root");
180
+ }
181
+ if (pack.issuer && envelope.verification_key !== pack.issuer.publicKey) {
182
+ reasons.push("anchor was signed by a different key than the claim issuer");
183
+ }
184
+ try {
185
+ const { signature, digest, ...signed } = envelope;
186
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
187
+ if (bytesToHex(hash) !== String(digest).toLowerCase()) {
188
+ reasons.push("envelope digest does not match its contents");
189
+ } else if (!ed25519.verify(hexToBytes(String(signature)), hash, hexToBytes(envelope.verification_key))) {
190
+ reasons.push("envelope signature does not verify");
191
+ }
192
+ } catch {
193
+ reasons.push("envelope signature does not verify");
194
+ }
195
+ return { ok: reasons.length === 0, reasons };
196
+ }
197
+ async function checkClaimAnchor(pack, sidecar, opts) {
198
+ const reasons = [];
199
+ const envelope = sidecar && sidecar.envelope;
200
+ if (!envelope) {
201
+ return { local_ok: false, log_ok: null, reasons: ["sidecar has no anchor envelope"] };
202
+ }
203
+ const local = verifyAnchorEnvelope(pack, envelope);
204
+ reasons.push(...local.reasons);
205
+ const base = (sidecar.log || DEFAULT_LOG).replace(/\/+$/, "");
206
+ const out = {
207
+ local_ok: local.ok,
208
+ log_ok: null,
209
+ seq: sidecar.seq,
210
+ anchored_at: sidecar.anchored_at,
211
+ entry_url: sidecar.entry_url || (typeof sidecar.seq === "number" ? `${base}/fn/log/${sidecar.seq}` : void 0),
212
+ reasons
213
+ };
214
+ if (opts?.offline) return out;
215
+ const doFetch = opts?.fetchImpl || globalThis.fetch;
216
+ if (!doFetch) return out;
217
+ try {
218
+ const resp = await doFetch(`${base}/fn/log/digest/sha256:${envelope.digest}`, { headers: { accept: "application/json" } });
219
+ const data = await resp.json().catch(() => null);
220
+ if (!resp.ok || !data) {
221
+ out.log_ok = null;
222
+ return out;
223
+ }
224
+ if (data.anchored !== true) {
225
+ out.log_ok = false;
226
+ out.reasons.push("the public log does not contain this anchor digest");
227
+ return out;
228
+ }
229
+ if (typeof sidecar.seq === "number" && typeof data.seq === "number" && data.seq !== sidecar.seq) {
230
+ out.log_ok = false;
231
+ out.reasons.push(`log holds the digest at entry #${data.seq}, sidecar says #${sidecar.seq}`);
232
+ return out;
233
+ }
234
+ out.log_ok = true;
235
+ if (typeof data.seq === "number") out.seq = data.seq;
236
+ return out;
237
+ } catch {
238
+ out.log_ok = null;
239
+ return out;
240
+ }
241
+ }
242
+ async function submitEnvelope(envelope, base, fetchImpl) {
243
+ const doFetch = fetchImpl || globalThis.fetch;
244
+ if (!doFetch) return { ok: false, error: "fetch_unavailable" };
164
245
  const encoded = toBase64(new TextEncoder().encode(JSON.stringify(envelope)));
165
246
  try {
166
247
  const resp = await doFetch(`${base}/fn/log/anchor-pack`, {
@@ -170,32 +251,103 @@ async function anchorClaim(pack, key, opts) {
170
251
  });
171
252
  const data = await resp.json().catch(() => null);
172
253
  if (!resp.ok || !data || !data.ok || typeof data.seq !== "number") {
173
- return { ok: false, claim_digest: envelope.claim_digest, error: data && data.error || `http_${resp.status}`, envelope };
254
+ return { ok: false, error: data && data.error || `http_${resp.status}` };
174
255
  }
256
+ return { ok: true, seq: data.seq, anchored_at: data.anchored_at, already_anchored: !!data.already_anchored };
257
+ } catch {
258
+ return { ok: false, error: "network_error" };
259
+ }
260
+ }
261
+ async function anchorClaim(pack, key, opts) {
262
+ const envelope = buildAnchorEnvelope(pack, key, opts.issuedAt);
263
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
264
+ const out = await submitEnvelope(envelope, base, opts.fetchImpl);
265
+ if (!out.ok) return { ok: false, claim_digest: envelope.claim_digest, error: out.error, envelope };
266
+ return {
267
+ ok: true,
268
+ claim_digest: envelope.claim_digest,
269
+ seq: out.seq,
270
+ entry_url: `${base}/fn/log/${out.seq}`,
271
+ anchored_at: out.anchored_at,
272
+ already_anchored: out.already_anchored,
273
+ envelope
274
+ };
275
+ }
276
+ var CHECKPOINT_SCHEMA = "scopeblind.protect-mcp.record-checkpoint.v1";
277
+ function buildRecordCheckpoint(receipts, key, issuedAt) {
278
+ const leaves = receipts.map(receiptToLeaf);
279
+ const times = leaves.map((l) => l.t).filter(Boolean).sort();
280
+ const signed = {
281
+ type: "evidence_pack",
282
+ schema: CHECKPOINT_SCHEMA,
283
+ anchors: "protect-mcp-record",
284
+ record_root: merkleRoot(leaves.map(leafHash)),
285
+ total: leaves.length,
286
+ from: times[0] || "",
287
+ to: times[times.length - 1] || "",
288
+ issued_at: issuedAt,
289
+ verification_key: key.publicKey,
290
+ disclosure: "internal"
291
+ };
292
+ const hash = sha256(new TextEncoder().encode(JSON.stringify(anchorDeepSort(signed))));
293
+ const digest = bytesToHex(hash);
294
+ const signature = bytesToHex(ed25519.sign(hash, hexToBytes(key.privateKey)));
295
+ return { ...signed, signature, digest };
296
+ }
297
+ async function anchorRecordCheckpoint(receipts, key, opts) {
298
+ const checkpoint = buildRecordCheckpoint(receipts, key, opts.issuedAt);
299
+ const base = (opts.log || DEFAULT_LOG).replace(/\/+$/, "");
300
+ const out = await submitEnvelope(checkpoint, base, opts.fetchImpl);
301
+ if (!out.ok) return { ok: false, record_root: checkpoint.record_root, total: checkpoint.total, checkpoint, error: out.error };
302
+ return {
303
+ ok: true,
304
+ record_root: checkpoint.record_root,
305
+ total: checkpoint.total,
306
+ seq: out.seq,
307
+ entry_url: `${base}/fn/log/${out.seq}`,
308
+ anchored_at: out.anchored_at,
309
+ already_anchored: out.already_anchored,
310
+ checkpoint
311
+ };
312
+ }
313
+ async function lookupPinnedIdentity(publicKey, opts) {
314
+ const base = (opts && opts.log || DEFAULT_LOG).replace(/\/+$/, "");
315
+ const doFetch = opts && opts.fetchImpl || globalThis.fetch;
316
+ if (!doFetch || !/^[0-9a-f]{64}$/i.test(publicKey)) return null;
317
+ try {
318
+ const resp = await doFetch(`${base}/fn/log/keys/lookup/${publicKey.toLowerCase()}`, { headers: { accept: "application/json" } });
319
+ const data = await resp.json().catch(() => null);
320
+ if (!data || data.ok !== true) return null;
321
+ if (!data.found) return { found: false };
175
322
  return {
176
- ok: true,
177
- claim_digest: envelope.claim_digest,
178
- seq: data.seq,
179
- entry_url: `${base}/fn/log/${data.seq}`,
180
- anchored_at: data.anchored_at,
181
- already_anchored: !!data.already_anchored,
182
- envelope
323
+ found: true,
324
+ name: data.name,
325
+ slug: data.slug,
326
+ kid: data.kid,
327
+ enrolled_at: data.enrolled_at,
328
+ revoked: !!(data.revoked || data.revoked_at)
183
329
  };
184
330
  } catch {
185
- return { ok: false, claim_digest: envelope.claim_digest, error: "network_error", envelope };
331
+ return null;
186
332
  }
187
333
  }
188
334
  export {
189
335
  ANCHOR_SCHEMA,
336
+ CHECKPOINT_SCHEMA,
190
337
  CLAIM_TYPE,
191
338
  DEFAULT_LOG,
192
339
  anchorClaim,
340
+ anchorRecordCheckpoint,
193
341
  buildAnchorEnvelope,
194
342
  buildClaim,
343
+ buildRecordCheckpoint,
344
+ checkClaimAnchor,
195
345
  claimDigest,
196
346
  evaluate,
197
347
  leafHash,
348
+ lookupPinnedIdentity,
198
349
  merkleRoot,
199
350
  receiptToLeaf,
351
+ verifyAnchorEnvelope,
200
352
  verifyClaim
201
353
  };