protect-mcp 0.9.7 → 0.10.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 (34) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/chunk-3AVIMUNP.mjs +113 -0
  3. package/dist/{chunk-YM6SOJBR.mjs → chunk-622KQ4KW.mjs} +1 -1
  4. package/dist/{chunk-JQDVKZBN.mjs → chunk-HULPW5WR.mjs} +7 -2
  5. package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
  6. package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
  7. package/dist/{chunk-MOXINIMB.mjs → chunk-SPDIFFGP.mjs} +2 -2
  8. package/dist/{chunk-G6X763MH.mjs → chunk-VWGRAUWI.mjs} +28 -30
  9. package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
  10. package/dist/{chunk-AUUAVWQM.mjs → chunk-ZLZPSUCY.mjs} +24 -6
  11. package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
  12. package/dist/cli.js +289 -2936
  13. package/dist/cli.mjs +37 -31
  14. package/dist/demo-server.js +17 -14
  15. package/dist/demo-server.mjs +1 -1
  16. package/dist/hook-server.js +99 -374
  17. package/dist/hook-server.mjs +4 -6
  18. package/dist/{http-transport-VHD3YBT5.mjs → http-transport-JHGRFCYY.mjs} +3 -2
  19. package/dist/index.d.mts +95 -6
  20. package/dist/index.d.ts +95 -6
  21. package/dist/index.js +615 -3872
  22. package/dist/index.mjs +37 -1130
  23. package/dist/mcp-server.js +123 -13
  24. package/dist/mcp-server.mjs +23 -13
  25. package/dist/{report-5XCNW6FB.mjs → report-N5LPPSPF.mjs} +2 -1
  26. package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
  27. package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
  28. package/package.json +5 -5
  29. package/dist/chunk-AYNQIEN7.mjs +0 -10
  30. package/dist/chunk-D733KAPG.mjs +0 -252
  31. package/dist/chunk-FFVJL3KQ.mjs +0 -2039
  32. package/dist/chunk-JIDDQUSQ.mjs +0 -568
  33. package/dist/ed25519-SQA3S2RV.mjs +0 -39
  34. package/dist/utils-6AYZFE5A.mjs +0 -77
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0: receipts migrate to the draft-02 Acta envelope
4
+
5
+ Receipts are now emitted in the envelope the IETF draft actually specifies
6
+ (draft-farley-acta-signed-receipts-02): a two-field
7
+ `{ payload, signature: { alg: "EdDSA", kid, sig } }` envelope, with the
8
+ access-decision fields from section 3.1 (`type: "protectmcp:decision"`,
9
+ `tool_name`, `decision`, `reason`, `policy_digest`), `issuer_id` equal to
10
+ `signature.kid` (section 2.2), the signature computed as PureEdDSA directly
11
+ over the JCS bytes of the payload with no pre-hash (section 5.6), and new
12
+ keys defaulting to the section 2.1.1 recommended
13
+ `sb:issuer:<base58-fingerprint>` kid format (existing key files keep their
14
+ explicit kid). Previously the package emitted an internal envelope shape
15
+ that predated the published draft.
16
+
17
+ Receipt logs are now hash-chained per section 5.7: each receipt's
18
+ `previousReceiptHash` is the bare-hex SHA-256 of the JCS bytes of the
19
+ previous log line exactly as written, signature included. The hook server
20
+ resumes the chain across restarts from the tail of the existing log, and
21
+ signing-failure tombstones participate in the chain so an unsigned gap
22
+ cannot be silently dropped.
23
+
24
+ Verification is dual-shape everywhere (the `verify_receipt` MCP tool, the
25
+ `serve --enforce` self-test, the embedded record viewer, and the exported
26
+ `verifyReceipt` API): receipts written by protect-mcp 0.9.x and earlier
27
+ (flat v1 artifacts and structured v2 envelopes with a top-level signature
28
+ string) continue to verify, and a mixed pre/post-migration log replays
29
+ cleanly, including the chain link that spans the boundary. Verified against
30
+ 3,352 real receipts from a production gate log: all verify. The published
31
+ `@veritasacta/verify` CLI (0.9.2) verifies the new envelopes as-is via its
32
+ passport path; no verifier upgrade is required.
33
+
34
+ New module `acta-envelope` (exported from the package root):
35
+ `createReceiptEnvelope`, `verifyReceipt`, `receiptHash`, `receiptIdentity`,
36
+ `computeSbIssuerKid`. `signDecision` accepts an optional previous-chain-hash
37
+ argument and returns the emitted receipt's chain hash. `@noble/curves` and
38
+ `@noble/hashes` moved from optional to regular dependencies.
39
+
40
+ Fixed: importing the package as a library no longer attaches the demo
41
+ server's stdin JSON-RPC listener (it now only starts when demo-server is
42
+ the process entry point). Previously any `require("protect-mcp")` kept the
43
+ host process's event loop alive and answered JSON-RPC on stdout.
44
+
45
+ Note for strict draft conformance: `decision` may be "require_approval" for
46
+ held-for-co-sign flows, an extension value beyond the section 3.1 set
47
+ (allow, deny, rate_limit), pending a revision of the draft.
48
+
49
+
50
+ ## 0.9.7: the gate as an MCP server
51
+
52
+ `protect-mcp mcp` boots a stdio MCP server exposing the gate itself as four
53
+ tools, so an agent can call the gate directly instead of only through the
54
+ PreToolUse/PostToolUse hooks: `evaluate_action` (Cedar decision, fail-closed),
55
+ `sign_decision` (Ed25519 receipt, byte-compatible with the runtime gate's),
56
+ `verify_receipt` (offline check), and `self_test` (proves a known-forbidden
57
+ action is denied and a signed receipt round-trips). All four are read-only
58
+ and annotated with what they return. A bare-spawnable bin,
59
+ `protect-mcp-mcp`, is included alongside the `mcp` subcommand so any MCP
60
+ host or registry can point at either.
61
+
3
62
  ## 0.9.6: policy you can see and change
4
63
 
5
64
  The gate is default-deny and fail-closed, but a deny used to be a dead end: an
@@ -0,0 +1,113 @@
1
+ // src/acta-envelope.ts
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { sha256 } from "@noble/hashes/sha256";
4
+ import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
5
+ function canonicalize(obj) {
6
+ return JSON.stringify(obj, (_key, value) => {
7
+ if (value && typeof value === "object" && !Array.isArray(value)) {
8
+ const sorted = {};
9
+ for (const k of Object.keys(value).sort()) {
10
+ if (!/^[\x20-\x7E]*$/.test(k)) {
11
+ throw new Error(`Non-ASCII key "${k}" in receipt payload. Only ASCII keys are permitted.`);
12
+ }
13
+ sorted[k] = value[k];
14
+ }
15
+ return sorted;
16
+ }
17
+ return value;
18
+ });
19
+ }
20
+ function receiptHash(obj) {
21
+ return bytesToHex(sha256(utf8ToBytes(canonicalize(obj))));
22
+ }
23
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
24
+ function base58(bytes) {
25
+ let n = BigInt("0x" + bytesToHex(bytes));
26
+ let out = "";
27
+ while (n > 0n) {
28
+ out = B58_ALPHABET[Number(n % 58n)] + out;
29
+ n /= 58n;
30
+ }
31
+ for (const b of bytes) {
32
+ if (b === 0) out = "1" + out;
33
+ else break;
34
+ }
35
+ return out;
36
+ }
37
+ function computeSbIssuerKid(publicKeyHex) {
38
+ return `sb:issuer:${base58(hexToBytes(publicKeyHex)).slice(0, 12)}`;
39
+ }
40
+ function createReceiptEnvelope(fields, privateKeyHex, kid, issuedAt) {
41
+ if (!fields.type) throw new Error("receipt payload requires a type");
42
+ if (!kid) throw new Error("kid is required");
43
+ const payload = {
44
+ ...fields,
45
+ issued_at: fields.issued_at || issuedAt || (/* @__PURE__ */ new Date()).toISOString(),
46
+ issuer_id: kid
47
+ };
48
+ const sig = bytesToHex(ed25519.sign(utf8ToBytes(canonicalize(payload)), hexToBytes(privateKeyHex)));
49
+ const envelope = { payload, signature: { alg: "EdDSA", kid, sig } };
50
+ return { envelope, hash: receiptHash(envelope) };
51
+ }
52
+ function verifyReceipt(envelope, publicKeyHex) {
53
+ try {
54
+ if (!envelope || typeof envelope !== "object") {
55
+ return { valid: false, shape: null, error: "not_an_object" };
56
+ }
57
+ const env = envelope;
58
+ const signature = env.signature;
59
+ if (signature && typeof signature === "object" && !Array.isArray(signature)) {
60
+ const sigObj = signature;
61
+ if (sigObj.alg !== "EdDSA") {
62
+ return { valid: false, shape: "acta-02", error: `unsupported_alg:${String(sigObj.alg)}` };
63
+ }
64
+ if (typeof sigObj.sig !== "string" || !env.payload || typeof env.payload !== "object") {
65
+ return { valid: false, shape: "acta-02", error: "malformed_envelope" };
66
+ }
67
+ const message = utf8ToBytes(canonicalize(env.payload));
68
+ const valid = ed25519.verify(hexToBytes(sigObj.sig), message, hexToBytes(publicKeyHex));
69
+ return valid ? { valid: true, shape: "acta-02", hash: receiptHash(env) } : { valid: false, shape: "acta-02", error: "invalid_signature" };
70
+ }
71
+ if (typeof signature === "string") {
72
+ const rest = {};
73
+ for (const k of Object.keys(env)) if (k !== "signature") rest[k] = env[k];
74
+ const message = utf8ToBytes(canonicalize(rest));
75
+ const valid = ed25519.verify(hexToBytes(signature), message, hexToBytes(publicKeyHex));
76
+ const shape = env.v === 2 ? "legacy-v2" : "legacy-v1";
77
+ return valid ? { valid: true, shape, hash: receiptHash(env) } : { valid: false, shape, error: "invalid_signature" };
78
+ }
79
+ return { valid: false, shape: null, error: "missing_signature" };
80
+ } catch (err) {
81
+ return {
82
+ valid: false,
83
+ shape: null,
84
+ error: `verification_error:${err instanceof Error ? err.message : "unknown"}`
85
+ };
86
+ }
87
+ }
88
+ function receiptIdentity(envelope) {
89
+ if (!envelope || typeof envelope !== "object") return { kid: null, issuer: null, type: null };
90
+ const env = envelope;
91
+ if (env.signature && typeof env.signature === "object") {
92
+ const payload = env.payload || {};
93
+ const sig = env.signature;
94
+ return {
95
+ kid: typeof sig.kid === "string" ? sig.kid : null,
96
+ issuer: typeof payload.issuer_id === "string" ? payload.issuer_id : typeof payload.issuer_name === "string" ? payload.issuer_name : null,
97
+ type: typeof payload.type === "string" ? payload.type : null
98
+ };
99
+ }
100
+ return {
101
+ kid: typeof env.kid === "string" ? env.kid : null,
102
+ issuer: typeof env.issuer === "string" ? env.issuer : null,
103
+ type: typeof env.type === "string" ? env.type : null
104
+ };
105
+ }
106
+
107
+ export {
108
+ receiptHash,
109
+ computeSbIssuerKid,
110
+ createReceiptEnvelope,
111
+ verifyReceipt,
112
+ receiptIdentity
113
+ };
@@ -7,7 +7,7 @@ import {
7
7
  parseRateLimit,
8
8
  signDecision,
9
9
  startStatusServer
10
- } from "./chunk-G6X763MH.mjs";
10
+ } from "./chunk-VWGRAUWI.mjs";
11
11
  import {
12
12
  evaluateCedar
13
13
  } from "./chunk-MWXDXYWH.mjs";
@@ -1,3 +1,7 @@
1
+ import {
2
+ receiptIdentity
3
+ } from "./chunk-3AVIMUNP.mjs";
4
+
1
5
  // src/report.ts
2
6
  import { readFileSync, existsSync } from "fs";
3
7
  function generateReport(logPath, receiptPath, periodDays) {
@@ -34,8 +38,9 @@ function generateReport(logPath, receiptPath, periodDays) {
34
38
  const parsed = JSON.parse(trimmed);
35
39
  if (parsed.signature) {
36
40
  receiptsSigned++;
37
- if (parsed.kid && !signerKid) signerKid = parsed.kid;
38
- if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
41
+ const identity = receiptIdentity(parsed);
42
+ if (identity.kid && !signerKid) signerKid = identity.kid;
43
+ if (identity.issuer && !signerIssuer) signerIssuer = identity.issuer;
39
44
  }
40
45
  } catch {
41
46
  }
@@ -1,22 +1,11 @@
1
- import {
2
- sha256
3
- } from "./chunk-AYNQIEN7.mjs";
4
- import {
5
- ed25519
6
- } from "./chunk-FFVJL3KQ.mjs";
7
- import {
8
- Hash,
9
- abytes,
10
- aexists,
11
- ahash,
12
- bytesToHex,
13
- clean,
14
- hexToBytes,
15
- randomBytes,
16
- toBytes
17
- } from "./chunk-D733KAPG.mjs";
1
+ // src/signing-committed.ts
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { sha256 as sha2563 } from "@noble/hashes/sha256";
4
+ import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3, randomBytes as randomBytes2 } from "@noble/hashes/utils";
18
5
 
19
6
  // src/commitments/merkle.ts
7
+ import { sha256 } from "@noble/hashes/sha256";
8
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
20
9
  var DOMAIN_LEAF = 0;
21
10
  var DOMAIN_INTERNAL = 1;
22
11
  function hashLeaf(leafBytes) {
@@ -127,75 +116,10 @@ function largestPowerOfTwoLessThan(n) {
127
116
  return k;
128
117
  }
129
118
 
130
- // node_modules/@noble/hashes/esm/hmac.js
131
- var HMAC = class extends Hash {
132
- constructor(hash, _key) {
133
- super();
134
- this.finished = false;
135
- this.destroyed = false;
136
- ahash(hash);
137
- const key = toBytes(_key);
138
- this.iHash = hash.create();
139
- if (typeof this.iHash.update !== "function")
140
- throw new Error("Expected instance of class which extends utils.Hash");
141
- this.blockLen = this.iHash.blockLen;
142
- this.outputLen = this.iHash.outputLen;
143
- const blockLen = this.blockLen;
144
- const pad = new Uint8Array(blockLen);
145
- pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
146
- for (let i = 0; i < pad.length; i++)
147
- pad[i] ^= 54;
148
- this.iHash.update(pad);
149
- this.oHash = hash.create();
150
- for (let i = 0; i < pad.length; i++)
151
- pad[i] ^= 54 ^ 92;
152
- this.oHash.update(pad);
153
- clean(pad);
154
- }
155
- update(buf) {
156
- aexists(this);
157
- this.iHash.update(buf);
158
- return this;
159
- }
160
- digestInto(out) {
161
- aexists(this);
162
- abytes(out, this.outputLen);
163
- this.finished = true;
164
- this.iHash.digestInto(out);
165
- this.oHash.update(out);
166
- this.oHash.digestInto(out);
167
- this.destroy();
168
- }
169
- digest() {
170
- const out = new Uint8Array(this.oHash.outputLen);
171
- this.digestInto(out);
172
- return out;
173
- }
174
- _cloneInto(to) {
175
- to || (to = Object.create(Object.getPrototypeOf(this), {}));
176
- const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
177
- to = to;
178
- to.finished = finished;
179
- to.destroyed = destroyed;
180
- to.blockLen = blockLen;
181
- to.outputLen = outputLen;
182
- to.oHash = oHash._cloneInto(to.oHash);
183
- to.iHash = iHash._cloneInto(to.iHash);
184
- return to;
185
- }
186
- clone() {
187
- return this._cloneInto();
188
- }
189
- destroy() {
190
- this.destroyed = true;
191
- this.oHash.destroy();
192
- this.iHash.destroy();
193
- }
194
- };
195
- var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
196
- hmac.create = (hash, key) => new HMAC(hash, key);
197
-
198
119
  // src/commitments/primitives.ts
120
+ import { sha256 as sha2562 } from "@noble/hashes/sha256";
121
+ import { hmac } from "@noble/hashes/hmac";
122
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2, randomBytes } from "@noble/hashes/utils";
199
123
  function jcs(value) {
200
124
  if (value === null || value === void 0) return "null";
201
125
  if (typeof value === "boolean" || typeof value === "number")
@@ -257,7 +181,7 @@ function leavesFromFields(fields) {
257
181
 
258
182
  // src/signing-committed.ts
259
183
  function freshSalt() {
260
- return randomBytes(32);
184
+ return randomBytes2(32);
261
185
  }
262
186
  function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
263
187
  const allFields = {
@@ -297,7 +221,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
297
221
  const { sorted, leafBytes } = leavesFromFields(committedFields);
298
222
  const leafHashes = leafBytes.map(hashLeaf);
299
223
  const root = merkleRoot(leafHashes);
300
- committedFieldsRoot = bytesToHex(root);
224
+ committedFieldsRoot = bytesToHex3(root);
301
225
  sorted.forEach((f, i) => {
302
226
  openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
303
227
  });
@@ -314,8 +238,8 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
314
238
  payload.committed_field_names = committedFields.map((f) => f.name);
315
239
  }
316
240
  const canonical = jcs(payload);
317
- const messageHash = sha256(new TextEncoder().encode(canonical));
318
- const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
241
+ const messageHash = sha2563(new TextEncoder().encode(canonical));
242
+ const signatureBytes = ed25519.sign(messageHash, hexToBytes3(signingKey));
319
243
  const signedReceipt = {
320
244
  ...payload,
321
245
  signature: {
@@ -328,7 +252,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
328
252
  }
329
253
  };
330
254
  const signedJson = JSON.stringify(signedReceipt);
331
- const receiptHash = bytesToHex(sha256(new TextEncoder().encode(jcs(signedReceipt))));
255
+ const receiptHash = bytesToHex3(sha2563(new TextEncoder().encode(jcs(signedReceipt))));
332
256
  return {
333
257
  signed: signedJson,
334
258
  artifact_type: "decision_receipt_committed_v1",
@@ -457,7 +381,7 @@ function committedFieldNamesFromReceipt(receipt, openings) {
457
381
  return Array.from(new Set(names)).sort();
458
382
  }
459
383
  function receiptHashHex(receipt) {
460
- return bytesToHex(sha256(new TextEncoder().encode(jcs(receipt))));
384
+ return bytesToHex3(sha2563(new TextEncoder().encode(jcs(receipt))));
461
385
  }
462
386
  function verifyCommittedReceiptSignature(receipt) {
463
387
  const signature = receipt.signature;
@@ -467,16 +391,15 @@ function verifyCommittedReceiptSignature(receipt) {
467
391
  return null;
468
392
  }
469
393
  const { signature: _signature, ...payloadWithoutSig } = receipt;
470
- const messageHash = sha256(new TextEncoder().encode(jcs(payloadWithoutSig)));
394
+ const messageHash = sha2563(new TextEncoder().encode(jcs(payloadWithoutSig)));
471
395
  try {
472
- return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
396
+ return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes3(sig.public_key));
473
397
  } catch {
474
398
  return false;
475
399
  }
476
400
  }
477
401
 
478
402
  export {
479
- hmac,
480
403
  signCommittedDecision,
481
404
  discloseField,
482
405
  createSelectiveDisclosurePackage,
@@ -1,11 +1,6 @@
1
- import {
2
- sha256
3
- } from "./chunk-AYNQIEN7.mjs";
4
- import {
5
- bytesToHex
6
- } from "./chunk-D733KAPG.mjs";
7
-
8
1
  // src/receipt-enrichment.ts
2
+ import { sha256 } from "@noble/hashes/sha256";
3
+ import { bytesToHex } from "@noble/hashes/utils";
9
4
  var ENRICHMENT_VERSION = 2;
10
5
  function canonicalJson(value) {
11
6
  const seen = /* @__PURE__ */ new WeakSet();
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-YM6SOJBR.mjs";
3
+ } from "./chunk-622KQ4KW.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-G6X763MH.mjs";
8
+ } from "./chunk-VWGRAUWI.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";
@@ -1,3 +1,8 @@
1
+ import {
2
+ computeSbIssuerKid,
3
+ createReceiptEnvelope
4
+ } from "./chunk-3AVIMUNP.mjs";
5
+
1
6
  // src/policy.ts
2
7
  import { createHash } from "crypto";
3
8
  import { readFileSync } from "fs";
@@ -77,13 +82,11 @@ function checkRateLimit(key, limit, store) {
77
82
  // src/signing.ts
78
83
  import { readFileSync as readFileSync2, existsSync } from "fs";
79
84
  var signerState = null;
80
- var artifactsModule = null;
81
85
  var signingConfigured = false;
82
86
  var signingInitError = null;
83
87
  async function initSigning(config) {
84
88
  const warnings = [];
85
89
  signerState = null;
86
- artifactsModule = null;
87
90
  signingConfigured = Boolean(config && config.enabled !== false);
88
91
  signingInitError = null;
89
92
  if (!config || config.enabled === false) {
@@ -112,32 +115,22 @@ async function initSigning(config) {
112
115
  warnings.push(`signing: ${signingInitError}`);
113
116
  return warnings;
114
117
  }
115
- try {
116
- const moduleName = "@veritasacta/artifacts";
117
- artifactsModule = await import(
118
- /* @vite-ignore */
119
- moduleName
120
- );
121
- } catch {
122
- signingInitError = "@veritasacta/artifacts not available";
123
- warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
124
- return warnings;
125
- }
126
118
  try {
127
119
  signerState = {
128
120
  privateKey: keyData.privateKey,
129
121
  publicKey: keyData.publicKey,
130
- kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
122
+ // kid is opaque per draft-02; existing key files keep their explicit kid,
123
+ // and keys without one get the s2.1.1 RECOMMENDED sb:issuer format.
124
+ kid: keyData.kid || computeSbIssuerKid(keyData.publicKey),
131
125
  issuer: config.issuer || keyData.issuer || "protect-mcp"
132
126
  };
133
127
  } catch (err) {
134
128
  signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
135
- artifactsModule = null;
136
129
  warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
137
130
  }
138
131
  return warnings;
139
132
  }
140
- function signDecision(entry) {
133
+ function signDecision(entry, prevReceiptHash) {
141
134
  const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
142
135
  if (signingConfigured && signingInitError) {
143
136
  return {
@@ -148,7 +141,7 @@ function signDecision(entry) {
148
141
  error: signingInitError
149
142
  };
150
143
  }
151
- if (signingConfigured && (!signerState || !artifactsModule)) {
144
+ if (signingConfigured && !signerState) {
152
145
  const error = "signing was configured but no signer is ready";
153
146
  return {
154
147
  ok: false,
@@ -158,21 +151,24 @@ function signDecision(entry) {
158
151
  error
159
152
  };
160
153
  }
161
- if (!signerState || !artifactsModule) {
154
+ if (!signerState) {
162
155
  return { ok: false, signed: null, artifact_type: "none" };
163
156
  }
164
157
  try {
165
158
  const payload = {
166
- tool: entry.tool,
159
+ // draft-02 s3.1 access-decision fields
160
+ type: "protectmcp:decision",
161
+ tool_name: entry.tool,
167
162
  decision: entry.decision,
168
- reason_code: entry.reason_code,
163
+ reason: entry.reason_code,
169
164
  policy_digest: entry.policy_digest,
165
+ // Extension fields (signed alongside the s3.1 core)
170
166
  scope: entry.request_id,
171
167
  // request scope
172
168
  mode: entry.mode,
173
169
  request_id: entry.request_id,
174
170
  // Spec version: ties every receipt to the IETF standard
175
- spec: "draft-farley-acta-signed-receipts-01",
171
+ spec: "draft-farley-acta-signed-receipts-02",
176
172
  // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
177
173
  // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
178
174
  // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
@@ -185,6 +181,10 @@ function signDecision(entry) {
185
181
  // authenticity (that the key is YOUR gate's) still comes from pinning it.
186
182
  public_key: signerState.publicKey
187
183
  };
184
+ if (signerState.issuer && signerState.issuer !== signerState.kid) {
185
+ payload.issuer_name = signerState.issuer;
186
+ }
187
+ if (prevReceiptHash) payload.previousReceiptHash = prevReceiptHash;
188
188
  if (entry.tier) payload.tier = entry.tier;
189
189
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
190
190
  if (entry.rate_limit_remaining !== void 0) {
@@ -199,19 +199,17 @@ function signDecision(entry) {
199
199
  if (entry.enrichment) payload.enrichment = entry.enrichment;
200
200
  if (entry.action_readback) payload.action_readback = entry.action_readback;
201
201
  if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
202
- const result = artifactsModule.createSignedArtifact(
203
- artifactType,
202
+ const result = createReceiptEnvelope(
204
203
  payload,
205
204
  signerState.privateKey,
206
- {
207
- kid: signerState.kid,
208
- issuer: signerState.issuer
209
- }
205
+ signerState.kid,
206
+ Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : void 0
210
207
  );
211
208
  return {
212
209
  ok: true,
213
- signed: JSON.stringify(result.artifact),
214
- artifact_type: artifactType
210
+ signed: JSON.stringify(result.envelope),
211
+ artifact_type: artifactType,
212
+ receipt_hash: result.hash
215
213
  };
216
214
  } catch (err) {
217
215
  const message = err instanceof Error ? err.message : "unknown error";
@@ -233,7 +231,7 @@ function getSignerInfo() {
233
231
  };
234
232
  }
235
233
  function isSigningEnabled() {
236
- return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
234
+ return signingConfigured && signingInitError === null && signerState !== null;
237
235
  }
238
236
 
239
237
  // src/http-server.ts
@@ -35158,20 +35158,23 @@ Contents: Hello from protect-mcp demo server!`;
35158
35158
  }
35159
35159
  return "";
35160
35160
  }
35161
- var rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
35162
- rl.on("line", (line) => {
35163
- const trimmed = line.trim();
35164
- if (!trimmed) return;
35165
- try {
35166
- const request = JSON.parse(trimmed);
35167
- const response = handleRequest(request);
35168
- if (response) {
35169
- process.stdout.write(response + "\n");
35170
- }
35171
- } catch {
35172
- }
35173
- });
35174
- process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
35161
+ var isMainModule = /demo-server\.(js|cjs|mjs|ts)$/.test(process.argv[1] || "");
35162
+ if (isMainModule) {
35163
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
35164
+ rl.on("line", (line) => {
35165
+ const trimmed = line.trim();
35166
+ if (!trimmed) return;
35167
+ try {
35168
+ const request = JSON.parse(trimmed);
35169
+ const response = handleRequest(request);
35170
+ if (response) {
35171
+ process.stdout.write(response + "\n");
35172
+ }
35173
+ } catch {
35174
+ }
35175
+ });
35176
+ process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
35177
+ }
35175
35178
  function createSandboxServer() {
35176
35179
  const { McpServer } = require_mcp();
35177
35180
  const { z } = require_zod();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildEnrichment
3
- } from "./chunk-WWPQNIVF.mjs";
3
+ } from "./chunk-KRKZ2YX7.mjs";
4
4
  import {
5
5
  ReceiptBuffer,
6
6
  buildActionReadback,
@@ -12,12 +12,15 @@ import {
12
12
  loadPolicy,
13
13
  parseRateLimit,
14
14
  signDecision
15
- } from "./chunk-G6X763MH.mjs";
15
+ } from "./chunk-VWGRAUWI.mjs";
16
16
  import {
17
17
  evaluateCedar,
18
18
  isCedarAvailable,
19
19
  loadCedarPolicies
20
20
  } from "./chunk-MWXDXYWH.mjs";
21
+ import {
22
+ receiptHash
23
+ } from "./chunk-3AVIMUNP.mjs";
21
24
 
22
25
  // src/hook-server.ts
23
26
  import { createServer } from "http";
@@ -181,6 +184,16 @@ var DEFAULT_PORT = 9377;
181
184
  var LOG_FILE = ".protect-mcp-log.jsonl";
182
185
  var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
183
186
  var PAYLOAD_HASH_THRESHOLD = 1024;
187
+ function resumeReceiptChain(receiptFilePath) {
188
+ try {
189
+ if (!existsSync(receiptFilePath)) return null;
190
+ const lines = readFileSync(receiptFilePath, "utf-8").split("\n").filter((l) => l.trim());
191
+ if (lines.length === 0) return null;
192
+ return receiptHash(JSON.parse(lines[lines.length - 1]));
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
184
197
  function detectSwarmContext() {
185
198
  const teamName = process.env.CLAUDE_CODE_TEAM_NAME;
186
199
  const agentId = process.env.CLAUDE_CODE_AGENT_ID;
@@ -683,10 +696,11 @@ function emitDecisionLog(state, entry) {
683
696
  } catch {
684
697
  }
685
698
  if (isSigningEnabled()) {
686
- const signed = signDecision(log);
699
+ const signed = signDecision(log, state.lastReceiptHash || void 0);
687
700
  if (signed.signed) {
688
701
  try {
689
702
  appendFileSync(state.receiptFilePath, signed.signed + "\n");
703
+ if (signed.receipt_hash) state.lastReceiptHash = signed.receipt_hash;
690
704
  } catch {
691
705
  }
692
706
  state.receiptBuffer.add(log.request_id, signed.signed);
@@ -701,16 +715,19 @@ function emitDecisionLog(state, entry) {
701
715
  `);
702
716
  }
703
717
  } else if (signed.error) {
704
- const tombstone = JSON.stringify({
718
+ const tombstoneObj = {
705
719
  type: "scopeblind.signing_failure.v1",
706
720
  request_id: log.request_id,
707
721
  tool: log.tool,
708
722
  decision: log.decision,
709
723
  error: signed.error,
710
- at: new Date(log.timestamp).toISOString()
711
- });
724
+ at: new Date(log.timestamp).toISOString(),
725
+ ...state.lastReceiptHash ? { previousReceiptHash: state.lastReceiptHash } : {}
726
+ };
727
+ const tombstone = JSON.stringify(tombstoneObj);
712
728
  try {
713
729
  appendFileSync(state.receiptFilePath, tombstone + "\n");
730
+ state.lastReceiptHash = receiptHash(tombstoneObj);
714
731
  } catch {
715
732
  }
716
733
  process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
@@ -826,6 +843,7 @@ async function startHookServer(options = {}) {
826
843
  policyDigest,
827
844
  logFilePath: join(process.cwd(), LOG_FILE),
828
845
  receiptFilePath: join(process.cwd(), RECEIPTS_FILE),
846
+ lastReceiptHash: resumeReceiptChain(join(process.cwd(), RECEIPTS_FILE)),
829
847
  permissionSuggestions: /* @__PURE__ */ new Map(),
830
848
  configAlerts: []
831
849
  };