protect-mcp 0.9.7 → 0.11.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 (37) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/{chunk-AUUAVWQM.mjs → chunk-3FSKOGGK.mjs} +28 -8
  3. package/dist/{chunk-MOXINIMB.mjs → chunk-BXZUS6HS.mjs} +2 -2
  4. package/dist/{chunk-G6X763MH.mjs → chunk-CIWIK6BT.mjs} +5 -251
  5. package/dist/{chunk-MWXDXYWH.mjs → chunk-FGCNKEEW.mjs} +86 -9
  6. package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
  7. package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
  8. package/dist/{chunk-YM6SOJBR.mjs → chunk-NQOKPBSQ.mjs} +19 -6
  9. package/dist/{chunk-JQDVKZBN.mjs → chunk-SOJBLPPX.mjs} +7 -2
  10. package/dist/chunk-WMIWQ6TS.mjs +266 -0
  11. package/dist/chunk-XOP3PEBM.mjs +114 -0
  12. package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
  13. package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
  14. package/dist/cli.js +1166 -3391
  15. package/dist/cli.mjs +131 -37
  16. package/dist/coverage-NB55FTYL.mjs +237 -0
  17. package/dist/demo-server.js +17 -14
  18. package/dist/demo-server.mjs +1 -1
  19. package/dist/hook-server.js +177 -436
  20. package/dist/hook-server.mjs +6 -7
  21. package/dist/{http-transport-VHD3YBT5.mjs → http-transport-BHHLPYL2.mjs} +8 -3
  22. package/dist/index.d.mts +98 -7
  23. package/dist/index.d.ts +98 -7
  24. package/dist/index.js +777 -4007
  25. package/dist/index.mjs +39 -1131
  26. package/dist/mcp-server.js +154 -14
  27. package/dist/mcp-server.mjs +24 -14
  28. package/dist/{report-5XCNW6FB.mjs → report-5B7EO3QB.mjs} +2 -1
  29. package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
  30. package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
  31. package/package.json +5 -5
  32. package/dist/chunk-AYNQIEN7.mjs +0 -10
  33. package/dist/chunk-D733KAPG.mjs +0 -252
  34. package/dist/chunk-FFVJL3KQ.mjs +0 -2039
  35. package/dist/chunk-JIDDQUSQ.mjs +0 -568
  36. package/dist/ed25519-SQA3S2RV.mjs +0 -39
  37. package/dist/utils-6AYZFE5A.mjs +0 -77
@@ -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,16 +1,21 @@
1
1
  import {
2
2
  ReceiptBuffer,
3
3
  buildActionReadback,
4
+ startStatusServer
5
+ } from "./chunk-CIWIK6BT.mjs";
6
+ import {
4
7
  checkRateLimit,
5
8
  getToolPolicy,
6
9
  isSigningEnabled,
7
10
  parseRateLimit,
8
- signDecision,
9
- startStatusServer
10
- } from "./chunk-G6X763MH.mjs";
11
+ signDecision
12
+ } from "./chunk-WMIWQ6TS.mjs";
11
13
  import {
12
14
  evaluateCedar
13
- } from "./chunk-MWXDXYWH.mjs";
15
+ } from "./chunk-FGCNKEEW.mjs";
16
+ import {
17
+ receiptHash
18
+ } from "./chunk-XOP3PEBM.mjs";
14
19
 
15
20
  // src/evidence-store.ts
16
21
  import { readFileSync, writeFileSync, existsSync } from "fs";
@@ -589,7 +594,7 @@ function parseNotificationConfigFromEnv() {
589
594
  import { spawn } from "child_process";
590
595
  import { randomUUID, randomBytes } from "crypto";
591
596
  import { createInterface } from "readline";
592
- import { appendFileSync } from "fs";
597
+ import { appendFileSync, readFileSync as readFileSync2 } from "fs";
593
598
  import { join as join2 } from "path";
594
599
  var LOG_FILE = ".protect-mcp-log.jsonl";
595
600
  var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
@@ -600,6 +605,8 @@ var ProtectGateway = class {
600
605
  clientReader = null;
601
606
  logFilePath;
602
607
  receiptFilePath;
608
+ /** s5.7 hash of the last line appended to the receipt file (chain link) */
609
+ lastReceiptHash = null;
603
610
  evidenceStore;
604
611
  receiptBuffer;
605
612
  /** Approval grants keyed by request_id (scoped to the specific action that was requested) */
@@ -619,6 +626,11 @@ var ProtectGateway = class {
619
626
  this.config = config;
620
627
  this.logFilePath = join2(process.cwd(), LOG_FILE);
621
628
  this.receiptFilePath = join2(process.cwd(), RECEIPTS_FILE);
629
+ try {
630
+ const existing = readFileSync2(this.receiptFilePath, "utf-8").split("\n").filter((l) => l.trim());
631
+ if (existing.length > 0) this.lastReceiptHash = receiptHash(JSON.parse(existing[existing.length - 1]));
632
+ } catch {
633
+ }
622
634
  this.evidenceStore = new EvidenceStore();
623
635
  this.receiptBuffer = new ReceiptBuffer();
624
636
  this.notificationConfig = parseNotificationConfigFromEnv();
@@ -965,12 +977,13 @@ var ProtectGateway = class {
965
977
  } catch {
966
978
  }
967
979
  if (isSigningEnabled()) {
968
- const signed = signDecision(log);
980
+ const signed = signDecision(log, this.lastReceiptHash || void 0);
969
981
  if (signed.signed) {
970
982
  process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
971
983
  `);
972
984
  try {
973
985
  appendFileSync(this.receiptFilePath, signed.signed + "\n");
986
+ if (signed.receipt_hash) this.lastReceiptHash = signed.receipt_hash;
974
987
  } catch {
975
988
  }
976
989
  this.receiptBuffer.add(log.request_id, signed.signed);
@@ -1,3 +1,7 @@
1
+ import {
2
+ receiptIdentity
3
+ } from "./chunk-XOP3PEBM.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
  }
@@ -0,0 +1,266 @@
1
+ import {
2
+ digestBuiltinPolicy
3
+ } from "./chunk-FGCNKEEW.mjs";
4
+ import {
5
+ computeSbIssuerKid,
6
+ createReceiptEnvelope
7
+ } from "./chunk-XOP3PEBM.mjs";
8
+
9
+ // src/policy.ts
10
+ import { readFileSync } from "fs";
11
+ function loadPolicy(path) {
12
+ const raw = readFileSync(path, "utf-8");
13
+ const parsed = JSON.parse(raw);
14
+ if (!parsed.tools || typeof parsed.tools !== "object") {
15
+ throw new Error(`Invalid policy file: missing "tools" object in ${path}`);
16
+ }
17
+ const policy = {
18
+ tools: parsed.tools,
19
+ default_tier: parsed.default_tier || "unknown",
20
+ policy_engine: parsed.policy_engine || "built-in",
21
+ ...parsed.external ? { external: parsed.external } : {}
22
+ };
23
+ const digest = computePolicyDigest(policy);
24
+ return {
25
+ policy,
26
+ digest,
27
+ credentials: parsed.credentials,
28
+ signing: parsed.signing
29
+ };
30
+ }
31
+ function computePolicyDigest(policy) {
32
+ return digestBuiltinPolicy(policy).policy_digest;
33
+ }
34
+ function getToolPolicy(toolName, policy) {
35
+ if (!policy) {
36
+ return { require: "any" };
37
+ }
38
+ if (policy.tools[toolName]) {
39
+ return policy.tools[toolName];
40
+ }
41
+ if (policy.tools["*"]) {
42
+ return policy.tools["*"];
43
+ }
44
+ return { require: "any" };
45
+ }
46
+ function parseRateLimit(spec) {
47
+ const match = spec.match(/^(\d+)\/(second|minute|hour|day)$/);
48
+ if (!match) {
49
+ throw new Error(`Invalid rate limit format: "${spec}". Expected "N/unit" (e.g. "5/hour")`);
50
+ }
51
+ const count = parseInt(match[1], 10);
52
+ const unit = match[2];
53
+ const windowMs = {
54
+ second: 1e3,
55
+ minute: 6e4,
56
+ hour: 36e5,
57
+ day: 864e5
58
+ };
59
+ return { count, windowMs: windowMs[unit] };
60
+ }
61
+ function checkRateLimit(key, limit, store) {
62
+ const now = Date.now();
63
+ const windowStart = now - limit.windowMs;
64
+ const timestamps = (store.get(key) || []).filter((t) => t > windowStart);
65
+ if (timestamps.length >= limit.count) {
66
+ store.set(key, timestamps);
67
+ return { allowed: false, remaining: 0 };
68
+ }
69
+ timestamps.push(now);
70
+ store.set(key, timestamps);
71
+ return { allowed: true, remaining: limit.count - timestamps.length };
72
+ }
73
+
74
+ // src/signing.ts
75
+ import { readFileSync as readFileSync2, existsSync } from "fs";
76
+ var signerState = null;
77
+ var signingConfigured = false;
78
+ var signingInitError = null;
79
+ async function initSigning(config) {
80
+ const warnings = [];
81
+ signerState = null;
82
+ signingConfigured = Boolean(config && config.enabled !== false);
83
+ signingInitError = null;
84
+ if (!config || config.enabled === false) {
85
+ return warnings;
86
+ }
87
+ if (!config.key_path) {
88
+ signingInitError = "signing enabled but key_path is not configured";
89
+ warnings.push(`signing: ${signingInitError}`);
90
+ return warnings;
91
+ }
92
+ if (!existsSync(config.key_path)) {
93
+ signingInitError = `key file not found at ${config.key_path}`;
94
+ warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
95
+ return warnings;
96
+ }
97
+ let keyData;
98
+ try {
99
+ keyData = JSON.parse(readFileSync2(config.key_path, "utf-8"));
100
+ if (!keyData.privateKey || !keyData.publicKey) {
101
+ signingInitError = "key file missing privateKey or publicKey fields";
102
+ warnings.push(`signing: ${signingInitError}`);
103
+ return warnings;
104
+ }
105
+ } catch (err) {
106
+ signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
107
+ warnings.push(`signing: ${signingInitError}`);
108
+ return warnings;
109
+ }
110
+ try {
111
+ signerState = {
112
+ privateKey: keyData.privateKey,
113
+ publicKey: keyData.publicKey,
114
+ // kid is opaque per draft-02; existing key files keep their explicit kid,
115
+ // and keys without one get the s2.1.1 RECOMMENDED sb:issuer format.
116
+ kid: keyData.kid || computeSbIssuerKid(keyData.publicKey),
117
+ issuer: config.issuer || keyData.issuer || "protect-mcp"
118
+ };
119
+ } catch (err) {
120
+ signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
121
+ warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
122
+ }
123
+ return warnings;
124
+ }
125
+ function signDecision(entry, prevReceiptHash) {
126
+ const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
127
+ if (signingConfigured && signingInitError) {
128
+ return {
129
+ ok: false,
130
+ signed: null,
131
+ artifact_type: artifactType,
132
+ warning: `signing initialization failed: ${signingInitError}`,
133
+ error: signingInitError
134
+ };
135
+ }
136
+ if (signingConfigured && !signerState) {
137
+ const error = "signing was configured but no signer is ready";
138
+ return {
139
+ ok: false,
140
+ signed: null,
141
+ artifact_type: artifactType,
142
+ warning: error,
143
+ error
144
+ };
145
+ }
146
+ if (!signerState) {
147
+ return { ok: false, signed: null, artifact_type: "none" };
148
+ }
149
+ try {
150
+ const payload = {
151
+ // draft-02 s3.1 access-decision fields
152
+ type: "protectmcp:decision",
153
+ tool_name: entry.tool,
154
+ decision: entry.decision,
155
+ reason: entry.reason_code,
156
+ policy_digest: entry.policy_digest,
157
+ // Extension fields (signed alongside the s3.1 core)
158
+ scope: entry.request_id,
159
+ // request scope
160
+ mode: entry.mode,
161
+ request_id: entry.request_id,
162
+ // Spec version: ties every receipt to the IETF standard
163
+ spec: "draft-farley-acta-signed-receipts-02",
164
+ // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
165
+ // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
166
+ // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
167
+ // - uncertified = unsigned receipt (shadow mode, no signing configured)
168
+ issuer_certification: signerState ? "self-signed" : "uncertified",
169
+ // The signer's PUBLIC key, inside the signed payload, so a receipt is
170
+ // self-contained: any verifier (including the record viewer, in-browser)
171
+ // can check the signature without a side channel. Binding the key inside
172
+ // the signature means it cannot be swapped without breaking the signature;
173
+ // authenticity (that the key is YOUR gate's) still comes from pinning it.
174
+ public_key: signerState.publicKey
175
+ };
176
+ if (signerState.issuer && signerState.issuer !== signerState.kid) {
177
+ payload.issuer_name = signerState.issuer;
178
+ }
179
+ if (prevReceiptHash) payload.previousReceiptHash = prevReceiptHash;
180
+ if (entry.tier) payload.tier = entry.tier;
181
+ if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
182
+ if (entry.rate_limit_remaining !== void 0) {
183
+ payload.rate_limit_remaining = entry.rate_limit_remaining;
184
+ }
185
+ if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
186
+ if (entry.hook_event) payload.hook_event = entry.hook_event;
187
+ if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
188
+ if (entry.timing) payload.timing = entry.timing;
189
+ if (entry.swarm) payload.swarm = entry.swarm;
190
+ if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
191
+ if (entry.enrichment) payload.enrichment = entry.enrichment;
192
+ if (entry.action_readback) payload.action_readback = entry.action_readback;
193
+ if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
194
+ const result = createReceiptEnvelope(
195
+ payload,
196
+ signerState.privateKey,
197
+ signerState.kid,
198
+ Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : void 0
199
+ );
200
+ return {
201
+ ok: true,
202
+ signed: JSON.stringify(result.envelope),
203
+ artifact_type: artifactType,
204
+ receipt_hash: result.hash
205
+ };
206
+ } catch (err) {
207
+ const message = err instanceof Error ? err.message : "unknown error";
208
+ return {
209
+ ok: false,
210
+ signed: null,
211
+ artifact_type: artifactType,
212
+ warning: `signing failed: ${message}`,
213
+ error: message
214
+ };
215
+ }
216
+ }
217
+ function signGenericArtifact(_artifactType, payload) {
218
+ if (signingConfigured && signingInitError) {
219
+ return { ok: false, signed: null, warning: `signing initialization failed: ${signingInitError}`, error: signingInitError };
220
+ }
221
+ if (signingConfigured && !signerState) {
222
+ const error = "signing was configured but no signer is ready";
223
+ return { ok: false, signed: null, warning: error, error };
224
+ }
225
+ if (!signerState) {
226
+ return { ok: false, signed: null };
227
+ }
228
+ try {
229
+ const full = {
230
+ ...payload,
231
+ type: String(payload.type || "protectmcp:artifact"),
232
+ public_key: signerState.publicKey
233
+ };
234
+ if (signerState.issuer && signerState.issuer !== signerState.kid) {
235
+ full.issuer_name = signerState.issuer;
236
+ }
237
+ const result = createReceiptEnvelope(full, signerState.privateKey, signerState.kid);
238
+ return { ok: true, signed: JSON.stringify(result.envelope) };
239
+ } catch (err) {
240
+ const message = err instanceof Error ? err.message : "unknown error";
241
+ return { ok: false, signed: null, warning: `signing failed: ${message}`, error: message };
242
+ }
243
+ }
244
+ function getSignerInfo() {
245
+ if (!signerState) return null;
246
+ return {
247
+ publicKey: signerState.publicKey,
248
+ kid: signerState.kid,
249
+ issuer: signerState.issuer
250
+ };
251
+ }
252
+ function isSigningEnabled() {
253
+ return signingConfigured && signingInitError === null && signerState !== null;
254
+ }
255
+
256
+ export {
257
+ loadPolicy,
258
+ getToolPolicy,
259
+ parseRateLimit,
260
+ checkRateLimit,
261
+ initSigning,
262
+ signDecision,
263
+ signGenericArtifact,
264
+ getSignerInfo,
265
+ isSigningEnabled
266
+ };
@@ -0,0 +1,114 @@
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
+ canonicalize,
109
+ receiptHash,
110
+ computeSbIssuerKid,
111
+ createReceiptEnvelope,
112
+ verifyReceipt,
113
+ receiptIdentity
114
+ };