protect-mcp 0.5.5 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -3
- package/dist/{chunk-SPHLVRJ2.mjs → chunk-3YCKR72H.mjs} +223 -4
- package/dist/{ed25519-V7HDL2WC.mjs → chunk-LYKNULYU.mjs} +166 -10
- package/dist/{chunk-BYYWYSHM.mjs → chunk-PLKRTBDR.mjs} +15 -3
- package/dist/{chunk-GQWJCHQV.mjs → chunk-S4ICHNSP.mjs} +2 -2
- package/dist/{chunk-YTBC72JJ.mjs → chunk-UV53U6D4.mjs} +69 -25
- package/dist/cli.js +305 -31
- package/dist/cli.mjs +7 -7
- package/dist/ed25519-DZMMNNVE.mjs +38 -0
- package/dist/hook-server.js +283 -28
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-LNBENGXD.mjs → http-transport-MO32ESHZ.mjs} +2 -2
- package/dist/index.d.mts +223 -4
- package/dist/index.d.ts +223 -4
- package/dist/index.js +3057 -420
- package/dist/index.mjs +250 -11
- package/package.json +5 -4
|
@@ -78,11 +78,40 @@ function checkRateLimit(key, limit, store) {
|
|
|
78
78
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
79
79
|
var signerState = null;
|
|
80
80
|
var artifactsModule = null;
|
|
81
|
+
var signingConfigured = false;
|
|
82
|
+
var signingInitError = null;
|
|
81
83
|
async function initSigning(config) {
|
|
82
84
|
const warnings = [];
|
|
85
|
+
signerState = null;
|
|
86
|
+
artifactsModule = null;
|
|
87
|
+
signingConfigured = Boolean(config && config.enabled !== false);
|
|
88
|
+
signingInitError = null;
|
|
83
89
|
if (!config || config.enabled === false) {
|
|
84
90
|
return warnings;
|
|
85
91
|
}
|
|
92
|
+
if (!config.key_path) {
|
|
93
|
+
signingInitError = "signing enabled but key_path is not configured";
|
|
94
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
95
|
+
return warnings;
|
|
96
|
+
}
|
|
97
|
+
if (!existsSync(config.key_path)) {
|
|
98
|
+
signingInitError = `key file not found at ${config.key_path}`;
|
|
99
|
+
warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
|
|
100
|
+
return warnings;
|
|
101
|
+
}
|
|
102
|
+
let keyData;
|
|
103
|
+
try {
|
|
104
|
+
keyData = JSON.parse(readFileSync2(config.key_path, "utf-8"));
|
|
105
|
+
if (!keyData.privateKey || !keyData.publicKey) {
|
|
106
|
+
signingInitError = "key file missing privateKey or publicKey fields";
|
|
107
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
108
|
+
return warnings;
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
|
|
112
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
113
|
+
return warnings;
|
|
114
|
+
}
|
|
86
115
|
try {
|
|
87
116
|
const moduleName = "@veritasacta/artifacts";
|
|
88
117
|
artifactsModule = await import(
|
|
@@ -90,37 +119,48 @@ async function initSigning(config) {
|
|
|
90
119
|
moduleName
|
|
91
120
|
);
|
|
92
121
|
} catch {
|
|
93
|
-
|
|
122
|
+
signingInitError = "@veritasacta/artifacts not available";
|
|
123
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
94
124
|
return warnings;
|
|
95
125
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
signerState = {
|
|
108
|
-
privateKey: keyData.privateKey,
|
|
109
|
-
publicKey: keyData.publicKey,
|
|
110
|
-
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
111
|
-
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
112
|
-
};
|
|
113
|
-
} catch (err) {
|
|
114
|
-
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
115
|
-
}
|
|
126
|
+
try {
|
|
127
|
+
signerState = {
|
|
128
|
+
privateKey: keyData.privateKey,
|
|
129
|
+
publicKey: keyData.publicKey,
|
|
130
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
131
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
|
|
135
|
+
artifactsModule = null;
|
|
136
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
116
137
|
}
|
|
117
138
|
return warnings;
|
|
118
139
|
}
|
|
119
140
|
function signDecision(entry) {
|
|
141
|
+
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
142
|
+
if (signingConfigured && signingInitError) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
signed: null,
|
|
146
|
+
artifact_type: artifactType,
|
|
147
|
+
warning: `signing initialization failed: ${signingInitError}`,
|
|
148
|
+
error: signingInitError
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (signingConfigured && (!signerState || !artifactsModule)) {
|
|
152
|
+
const error = "signing was configured but no signer is ready";
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
signed: null,
|
|
156
|
+
artifact_type: artifactType,
|
|
157
|
+
warning: error,
|
|
158
|
+
error
|
|
159
|
+
};
|
|
160
|
+
}
|
|
120
161
|
if (!signerState || !artifactsModule) {
|
|
121
|
-
return { signed: null, artifact_type: "none" };
|
|
162
|
+
return { ok: false, signed: null, artifact_type: "none" };
|
|
122
163
|
}
|
|
123
|
-
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
124
164
|
try {
|
|
125
165
|
const payload = {
|
|
126
166
|
tool: entry.tool,
|
|
@@ -161,14 +201,18 @@ function signDecision(entry) {
|
|
|
161
201
|
}
|
|
162
202
|
);
|
|
163
203
|
return {
|
|
204
|
+
ok: true,
|
|
164
205
|
signed: JSON.stringify(result.artifact),
|
|
165
206
|
artifact_type: artifactType
|
|
166
207
|
};
|
|
167
208
|
} catch (err) {
|
|
209
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
168
210
|
return {
|
|
211
|
+
ok: false,
|
|
169
212
|
signed: null,
|
|
170
213
|
artifact_type: artifactType,
|
|
171
|
-
warning: `signing failed: ${
|
|
214
|
+
warning: `signing failed: ${message}`,
|
|
215
|
+
error: message
|
|
172
216
|
};
|
|
173
217
|
}
|
|
174
218
|
}
|
|
@@ -181,7 +225,7 @@ function getSignerInfo() {
|
|
|
181
225
|
};
|
|
182
226
|
}
|
|
183
227
|
function isSigningEnabled() {
|
|
184
|
-
return signerState !== null && artifactsModule !== null;
|
|
228
|
+
return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
// src/cedar-evaluator.ts
|
package/dist/cli.js
CHANGED
|
@@ -361,9 +361,36 @@ var init_credentials = __esm({
|
|
|
361
361
|
// src/signing.ts
|
|
362
362
|
async function initSigning(config) {
|
|
363
363
|
const warnings = [];
|
|
364
|
+
signerState = null;
|
|
365
|
+
artifactsModule = null;
|
|
366
|
+
signingConfigured = Boolean(config && config.enabled !== false);
|
|
367
|
+
signingInitError = null;
|
|
364
368
|
if (!config || config.enabled === false) {
|
|
365
369
|
return warnings;
|
|
366
370
|
}
|
|
371
|
+
if (!config.key_path) {
|
|
372
|
+
signingInitError = "signing enabled but key_path is not configured";
|
|
373
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
374
|
+
return warnings;
|
|
375
|
+
}
|
|
376
|
+
if (!(0, import_node_fs3.existsSync)(config.key_path)) {
|
|
377
|
+
signingInitError = `key file not found at ${config.key_path}`;
|
|
378
|
+
warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
|
|
379
|
+
return warnings;
|
|
380
|
+
}
|
|
381
|
+
let keyData;
|
|
382
|
+
try {
|
|
383
|
+
keyData = JSON.parse((0, import_node_fs3.readFileSync)(config.key_path, "utf-8"));
|
|
384
|
+
if (!keyData.privateKey || !keyData.publicKey) {
|
|
385
|
+
signingInitError = "key file missing privateKey or publicKey fields";
|
|
386
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
387
|
+
return warnings;
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
|
|
391
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
392
|
+
return warnings;
|
|
393
|
+
}
|
|
367
394
|
try {
|
|
368
395
|
const moduleName = "@veritasacta/artifacts";
|
|
369
396
|
artifactsModule = await import(
|
|
@@ -371,37 +398,48 @@ async function initSigning(config) {
|
|
|
371
398
|
moduleName
|
|
372
399
|
);
|
|
373
400
|
} catch {
|
|
374
|
-
|
|
401
|
+
signingInitError = "@veritasacta/artifacts not available";
|
|
402
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
375
403
|
return warnings;
|
|
376
404
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
signerState = {
|
|
389
|
-
privateKey: keyData.privateKey,
|
|
390
|
-
publicKey: keyData.publicKey,
|
|
391
|
-
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
392
|
-
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
393
|
-
};
|
|
394
|
-
} catch (err) {
|
|
395
|
-
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
396
|
-
}
|
|
405
|
+
try {
|
|
406
|
+
signerState = {
|
|
407
|
+
privateKey: keyData.privateKey,
|
|
408
|
+
publicKey: keyData.publicKey,
|
|
409
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
410
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
411
|
+
};
|
|
412
|
+
} catch (err) {
|
|
413
|
+
signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
|
|
414
|
+
artifactsModule = null;
|
|
415
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
397
416
|
}
|
|
398
417
|
return warnings;
|
|
399
418
|
}
|
|
400
419
|
function signDecision(entry) {
|
|
420
|
+
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
421
|
+
if (signingConfigured && signingInitError) {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
signed: null,
|
|
425
|
+
artifact_type: artifactType,
|
|
426
|
+
warning: `signing initialization failed: ${signingInitError}`,
|
|
427
|
+
error: signingInitError
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (signingConfigured && (!signerState || !artifactsModule)) {
|
|
431
|
+
const error = "signing was configured but no signer is ready";
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
signed: null,
|
|
435
|
+
artifact_type: artifactType,
|
|
436
|
+
warning: error,
|
|
437
|
+
error
|
|
438
|
+
};
|
|
439
|
+
}
|
|
401
440
|
if (!signerState || !artifactsModule) {
|
|
402
|
-
return { signed: null, artifact_type: "none" };
|
|
441
|
+
return { ok: false, signed: null, artifact_type: "none" };
|
|
403
442
|
}
|
|
404
|
-
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
405
443
|
try {
|
|
406
444
|
const payload = {
|
|
407
445
|
tool: entry.tool,
|
|
@@ -442,14 +480,18 @@ function signDecision(entry) {
|
|
|
442
480
|
}
|
|
443
481
|
);
|
|
444
482
|
return {
|
|
483
|
+
ok: true,
|
|
445
484
|
signed: JSON.stringify(result.artifact),
|
|
446
485
|
artifact_type: artifactType
|
|
447
486
|
};
|
|
448
487
|
} catch (err) {
|
|
488
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
449
489
|
return {
|
|
490
|
+
ok: false,
|
|
450
491
|
signed: null,
|
|
451
492
|
artifact_type: artifactType,
|
|
452
|
-
warning: `signing failed: ${
|
|
493
|
+
warning: `signing failed: ${message}`,
|
|
494
|
+
error: message
|
|
453
495
|
};
|
|
454
496
|
}
|
|
455
497
|
}
|
|
@@ -462,15 +504,17 @@ function getSignerInfo() {
|
|
|
462
504
|
};
|
|
463
505
|
}
|
|
464
506
|
function isSigningEnabled() {
|
|
465
|
-
return signerState !== null && artifactsModule !== null;
|
|
507
|
+
return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
|
|
466
508
|
}
|
|
467
|
-
var import_node_fs3, signerState, artifactsModule;
|
|
509
|
+
var import_node_fs3, signerState, artifactsModule, signingConfigured, signingInitError;
|
|
468
510
|
var init_signing = __esm({
|
|
469
511
|
"src/signing.ts"() {
|
|
470
512
|
"use strict";
|
|
471
513
|
import_node_fs3 = require("fs");
|
|
472
514
|
signerState = null;
|
|
473
515
|
artifactsModule = null;
|
|
516
|
+
signingConfigured = false;
|
|
517
|
+
signingInitError = null;
|
|
474
518
|
}
|
|
475
519
|
});
|
|
476
520
|
|
|
@@ -1639,8 +1683,20 @@ var init_gateway = __esm({
|
|
|
1639
1683
|
this.evidenceStore.save();
|
|
1640
1684
|
}
|
|
1641
1685
|
}
|
|
1642
|
-
} else if (signed.
|
|
1643
|
-
|
|
1686
|
+
} else if (signed.error) {
|
|
1687
|
+
const tombstone = JSON.stringify({
|
|
1688
|
+
type: "scopeblind.signing_failure.v1",
|
|
1689
|
+
request_id: log.request_id,
|
|
1690
|
+
tool: log.tool,
|
|
1691
|
+
decision: log.decision,
|
|
1692
|
+
error: signed.error,
|
|
1693
|
+
at: new Date(log.timestamp).toISOString()
|
|
1694
|
+
});
|
|
1695
|
+
try {
|
|
1696
|
+
(0, import_node_fs6.appendFileSync)(this.receiptFilePath, tombstone + "\n");
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1699
|
+
process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
|
|
1644
1700
|
`);
|
|
1645
1701
|
}
|
|
1646
1702
|
}
|
|
@@ -4768,6 +4824,160 @@ var init_hook_patterns = __esm({
|
|
|
4768
4824
|
}
|
|
4769
4825
|
});
|
|
4770
4826
|
|
|
4827
|
+
// src/scopeblind-bridge.ts
|
|
4828
|
+
function getScopeBlindBridge() {
|
|
4829
|
+
if (!singleton) singleton = new ScopeBlindBridge();
|
|
4830
|
+
return singleton;
|
|
4831
|
+
}
|
|
4832
|
+
var DEFAULT_BASE, FLUSH_INTERVAL_MS, BATCH_MAX, BRASS_REFRESH_MARGIN_MS, ScopeBlindBridge, singleton;
|
|
4833
|
+
var init_scopeblind_bridge = __esm({
|
|
4834
|
+
"src/scopeblind-bridge.ts"() {
|
|
4835
|
+
"use strict";
|
|
4836
|
+
DEFAULT_BASE = "https://scopeblind.com";
|
|
4837
|
+
FLUSH_INTERVAL_MS = 5e3;
|
|
4838
|
+
BATCH_MAX = 128;
|
|
4839
|
+
BRASS_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
4840
|
+
ScopeBlindBridge = class {
|
|
4841
|
+
token;
|
|
4842
|
+
base;
|
|
4843
|
+
tenantOverride;
|
|
4844
|
+
cachedProof = null;
|
|
4845
|
+
queue = [];
|
|
4846
|
+
flushTimer = null;
|
|
4847
|
+
stats;
|
|
4848
|
+
shuttingDown = false;
|
|
4849
|
+
constructor(env = process.env) {
|
|
4850
|
+
this.token = env.SCOPEBLIND_TOKEN || null;
|
|
4851
|
+
this.base = (env.SCOPEBLIND_BASE || DEFAULT_BASE).replace(/\/$/, "");
|
|
4852
|
+
this.tenantOverride = env.SCOPEBLIND_TENANT || null;
|
|
4853
|
+
this.stats = {
|
|
4854
|
+
enabled: Boolean(this.token),
|
|
4855
|
+
tenant_slug: this.tenantOverride,
|
|
4856
|
+
forwarded_total: 0,
|
|
4857
|
+
rejected_total: 0,
|
|
4858
|
+
last_flush_at: null,
|
|
4859
|
+
last_error: null
|
|
4860
|
+
};
|
|
4861
|
+
if (this.enabled()) {
|
|
4862
|
+
this.flushTimer = setInterval(() => {
|
|
4863
|
+
void this.flush();
|
|
4864
|
+
}, FLUSH_INTERVAL_MS);
|
|
4865
|
+
if (typeof this.flushTimer === "object" && this.flushTimer && "unref" in this.flushTimer) {
|
|
4866
|
+
this.flushTimer.unref?.();
|
|
4867
|
+
}
|
|
4868
|
+
process.on("beforeExit", () => {
|
|
4869
|
+
void this.shutdown();
|
|
4870
|
+
});
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
enabled() {
|
|
4874
|
+
return Boolean(this.token);
|
|
4875
|
+
}
|
|
4876
|
+
/** Push a signed receipt into the queue. Non-blocking. */
|
|
4877
|
+
forward(signedReceipt) {
|
|
4878
|
+
if (!this.enabled() || this.shuttingDown) return;
|
|
4879
|
+
this.queue.push(signedReceipt);
|
|
4880
|
+
if (this.queue.length >= BATCH_MAX) void this.flush();
|
|
4881
|
+
}
|
|
4882
|
+
/** Flush the queue. Safe to call concurrently. */
|
|
4883
|
+
async flush() {
|
|
4884
|
+
if (!this.enabled() || this.queue.length === 0) return;
|
|
4885
|
+
const batch = this.queue.splice(0, BATCH_MAX);
|
|
4886
|
+
try {
|
|
4887
|
+
const proof = await this.ensureBrassProof();
|
|
4888
|
+
const slug = this.tenantOverride || proof?.tenant_id;
|
|
4889
|
+
if (!slug) {
|
|
4890
|
+
this.queue.unshift(...batch);
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
this.stats.tenant_slug = slug;
|
|
4894
|
+
const res = await fetch(`${this.base}/fn/console/${slug}/receipts`, {
|
|
4895
|
+
method: "POST",
|
|
4896
|
+
headers: {
|
|
4897
|
+
"content-type": "application/json",
|
|
4898
|
+
authorization: `Bearer ${this.token}`,
|
|
4899
|
+
"user-agent": "protect-mcp/scopeblind-bridge"
|
|
4900
|
+
},
|
|
4901
|
+
body: JSON.stringify({ receipts: batch })
|
|
4902
|
+
});
|
|
4903
|
+
if (!res.ok) {
|
|
4904
|
+
const errBody = await res.text().catch(() => "");
|
|
4905
|
+
this.stats.last_error = `HTTP ${res.status} ${errBody.slice(0, 160)}`;
|
|
4906
|
+
this.stats.rejected_total += batch.length;
|
|
4907
|
+
if (res.status >= 500 && res.status !== 503) {
|
|
4908
|
+
this.queue.unshift(...batch);
|
|
4909
|
+
}
|
|
4910
|
+
return;
|
|
4911
|
+
}
|
|
4912
|
+
const body = await res.json().catch(() => ({}));
|
|
4913
|
+
this.stats.forwarded_total += body?.accepted ?? batch.length;
|
|
4914
|
+
this.stats.rejected_total += body?.rejected ?? 0;
|
|
4915
|
+
this.stats.last_flush_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4916
|
+
this.stats.last_error = null;
|
|
4917
|
+
} catch (err) {
|
|
4918
|
+
this.stats.last_error = String(err?.message || err);
|
|
4919
|
+
this.queue.unshift(...batch);
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
/** Exchange SCOPEBLIND_TOKEN for a BRASS-v2 proof; refresh near expiry. */
|
|
4923
|
+
async ensureBrassProof() {
|
|
4924
|
+
if (!this.token) return null;
|
|
4925
|
+
const now = Date.now();
|
|
4926
|
+
if (this.cachedProof && Date.parse(this.cachedProof.expires_at) - now > BRASS_REFRESH_MARGIN_MS) {
|
|
4927
|
+
return this.cachedProof;
|
|
4928
|
+
}
|
|
4929
|
+
try {
|
|
4930
|
+
const res = await fetch(`${this.base}/fn/brass/issue`, {
|
|
4931
|
+
method: "POST",
|
|
4932
|
+
headers: {
|
|
4933
|
+
"content-type": "application/json",
|
|
4934
|
+
"user-agent": "protect-mcp/scopeblind-bridge"
|
|
4935
|
+
},
|
|
4936
|
+
body: JSON.stringify({
|
|
4937
|
+
token: this.token,
|
|
4938
|
+
scope: "protect-mcp-receipt-emit",
|
|
4939
|
+
ttl_seconds: 3600
|
|
4940
|
+
})
|
|
4941
|
+
});
|
|
4942
|
+
if (!res.ok) {
|
|
4943
|
+
const text = await res.text().catch(() => "");
|
|
4944
|
+
this.stats.last_error = `brass-issue: HTTP ${res.status} ${text.slice(0, 160)}`;
|
|
4945
|
+
return null;
|
|
4946
|
+
}
|
|
4947
|
+
const body = await res.json();
|
|
4948
|
+
if (!body?.auth_proof) {
|
|
4949
|
+
this.stats.last_error = "brass-issue: missing auth_proof in response";
|
|
4950
|
+
return null;
|
|
4951
|
+
}
|
|
4952
|
+
this.cachedProof = body.auth_proof;
|
|
4953
|
+
return this.cachedProof;
|
|
4954
|
+
} catch (err) {
|
|
4955
|
+
this.stats.last_error = `brass-issue: ${err?.message || err}`;
|
|
4956
|
+
return null;
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Return a snapshot of bridge stats. Useful for `protect-mcp scopeblind status`.
|
|
4961
|
+
*/
|
|
4962
|
+
getStats() {
|
|
4963
|
+
return {
|
|
4964
|
+
...this.stats,
|
|
4965
|
+
queued: this.queue.length,
|
|
4966
|
+
brass_proof_expires_at: this.cachedProof?.expires_at || null
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
/** Flush remaining receipts and stop the interval. Called on process exit. */
|
|
4970
|
+
async shutdown() {
|
|
4971
|
+
if (this.shuttingDown) return;
|
|
4972
|
+
this.shuttingDown = true;
|
|
4973
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
4974
|
+
if (this.queue.length > 0) await this.flush();
|
|
4975
|
+
}
|
|
4976
|
+
};
|
|
4977
|
+
singleton = null;
|
|
4978
|
+
}
|
|
4979
|
+
});
|
|
4980
|
+
|
|
4771
4981
|
// src/hook-server.ts
|
|
4772
4982
|
var hook_server_exports = {};
|
|
4773
4983
|
__export(hook_server_exports, {
|
|
@@ -4976,7 +5186,7 @@ async function handlePreToolUse(input, state) {
|
|
|
4976
5186
|
const hookLatency = Date.now() - hookStart;
|
|
4977
5187
|
const denyKey = `${toolName}:${input.sessionId || "default"}`;
|
|
4978
5188
|
state.denyCounter.delete(denyKey);
|
|
4979
|
-
emitDecisionLog(state, {
|
|
5189
|
+
const emit = emitDecisionLog(state, {
|
|
4980
5190
|
tool: toolName,
|
|
4981
5191
|
decision: "allow",
|
|
4982
5192
|
reason_code: state.cedarPolicies ? "cedar_allow" : state.jsonPolicy ? "policy_allow" : "observe_mode",
|
|
@@ -4988,6 +5198,15 @@ async function handlePreToolUse(input, state) {
|
|
|
4988
5198
|
sandbox_state: detectSandboxState(),
|
|
4989
5199
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
4990
5200
|
});
|
|
5201
|
+
if (state.enforce && emit.signingFailed) {
|
|
5202
|
+
return {
|
|
5203
|
+
hookSpecificOutput: {
|
|
5204
|
+
hookEventName: "PreToolUse",
|
|
5205
|
+
permissionDecision: "deny",
|
|
5206
|
+
permissionDecisionReason: `[ScopeBlind] "${toolName}" was blocked because its receipt could not be signed. Failing closed: a governed action that cannot be proven is not allowed.`
|
|
5207
|
+
}
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
4991
5210
|
return {};
|
|
4992
5211
|
}
|
|
4993
5212
|
async function handlePostToolUse(input, state) {
|
|
@@ -5235,11 +5454,35 @@ function emitDecisionLog(state, entry) {
|
|
|
5235
5454
|
} catch {
|
|
5236
5455
|
}
|
|
5237
5456
|
state.receiptBuffer.add(log.request_id, signed.signed);
|
|
5238
|
-
|
|
5239
|
-
|
|
5457
|
+
try {
|
|
5458
|
+
const bridge = getScopeBlindBridge();
|
|
5459
|
+
if (bridge.enabled()) {
|
|
5460
|
+
const parsed = typeof signed.signed === "string" ? JSON.parse(signed.signed) : signed.signed;
|
|
5461
|
+
bridge.forward(parsed);
|
|
5462
|
+
}
|
|
5463
|
+
} catch (err) {
|
|
5464
|
+
process.stderr.write(`[PROTECT_MCP] ScopeBlind forward error: ${err instanceof Error ? err.message : err}
|
|
5465
|
+
`);
|
|
5466
|
+
}
|
|
5467
|
+
} else if (signed.error) {
|
|
5468
|
+
const tombstone = JSON.stringify({
|
|
5469
|
+
type: "scopeblind.signing_failure.v1",
|
|
5470
|
+
request_id: log.request_id,
|
|
5471
|
+
tool: log.tool,
|
|
5472
|
+
decision: log.decision,
|
|
5473
|
+
error: signed.error,
|
|
5474
|
+
at: new Date(log.timestamp).toISOString()
|
|
5475
|
+
});
|
|
5476
|
+
try {
|
|
5477
|
+
(0, import_node_fs8.appendFileSync)(state.receiptFilePath, tombstone + "\n");
|
|
5478
|
+
} catch {
|
|
5479
|
+
}
|
|
5480
|
+
process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
|
|
5240
5481
|
`);
|
|
5482
|
+
return { signingFailed: true };
|
|
5241
5483
|
}
|
|
5242
5484
|
}
|
|
5485
|
+
return { signingFailed: false };
|
|
5243
5486
|
}
|
|
5244
5487
|
async function routeHookEvent(input, state) {
|
|
5245
5488
|
switch (input.hookEventName) {
|
|
@@ -5550,6 +5793,7 @@ var init_hook_server = __esm({
|
|
|
5550
5793
|
init_signing();
|
|
5551
5794
|
init_policy();
|
|
5552
5795
|
init_http_server();
|
|
5796
|
+
init_scopeblind_bridge();
|
|
5553
5797
|
DEFAULT_PORT = 9377;
|
|
5554
5798
|
LOG_FILE3 = ".protect-mcp-log.jsonl";
|
|
5555
5799
|
RECEIPTS_FILE2 = ".protect-mcp-receipts.jsonl";
|
|
@@ -7712,6 +7956,36 @@ main().catch((err) => {
|
|
|
7712
7956
|
`);
|
|
7713
7957
|
process.exit(1);
|
|
7714
7958
|
});
|
|
7959
|
+
/**
|
|
7960
|
+
* scopeblind-bridge.ts
|
|
7961
|
+
*
|
|
7962
|
+
* Optional bridge between protect-mcp (local, MIT) and a paid ScopeBlind
|
|
7963
|
+
* tenant. When SCOPEBLIND_TOKEN is set in the environment, every signed
|
|
7964
|
+
* receipt that protect-mcp emits also gets forwarded to the tenant's
|
|
7965
|
+
* dashboard at https://scopeblind.com/console/<slug>.
|
|
7966
|
+
*
|
|
7967
|
+
* Lifecycle:
|
|
7968
|
+
* 1. On first use, exchange SCOPEBLIND_TOKEN for a short-lived BRASS-v2
|
|
7969
|
+
* auth proof from /fn/brass/issue. Cache the proof in memory until
|
|
7970
|
+
* ~5 minutes before expiry, then refresh.
|
|
7971
|
+
* 2. As receipts are emitted by hook-server.ts, push them into an
|
|
7972
|
+
* in-memory batch queue.
|
|
7973
|
+
* 3. Flush the queue every 5s (or when it reaches 128 receipts) by POSTing
|
|
7974
|
+
* to /fn/console/<slug>/receipts with Bearer SCOPEBLIND_TOKEN.
|
|
7975
|
+
*
|
|
7976
|
+
* Failure mode: forward errors NEVER throw upstream. protect-mcp continues
|
|
7977
|
+
* to mint and persist receipts locally regardless of dashboard availability.
|
|
7978
|
+
* The bridge logs failures to stderr (best-effort) and retries on the next
|
|
7979
|
+
* flush.
|
|
7980
|
+
*
|
|
7981
|
+
* Configuration:
|
|
7982
|
+
* SCOPEBLIND_TOKEN Tenant bearer token (from welcome email).
|
|
7983
|
+
* SCOPEBLIND_TENANT Optional slug override. By default we discover
|
|
7984
|
+
* the slug from the BRASS proof's tenant_id.
|
|
7985
|
+
* SCOPEBLIND_BASE Defaults to https://scopeblind.com.
|
|
7986
|
+
*
|
|
7987
|
+
* @license MIT
|
|
7988
|
+
*/
|
|
7715
7989
|
/*! Bundled license information:
|
|
7716
7990
|
|
|
7717
7991
|
@noble/hashes/esm/utils.js:
|
package/dist/cli.mjs
CHANGED
|
@@ -3,17 +3,17 @@ import {
|
|
|
3
3
|
formatSimulation,
|
|
4
4
|
parseLogFile,
|
|
5
5
|
simulate
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-S4ICHNSP.mjs";
|
|
7
7
|
import {
|
|
8
8
|
ProtectGateway,
|
|
9
9
|
validateCredentials
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-PLKRTBDR.mjs";
|
|
11
11
|
import {
|
|
12
12
|
initSigning,
|
|
13
13
|
isCedarAvailable,
|
|
14
14
|
loadCedarPolicies,
|
|
15
15
|
loadPolicy
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-UV53U6D4.mjs";
|
|
17
17
|
import "./chunk-PQJP2ZCI.mjs";
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
@@ -145,7 +145,7 @@ async function handleInit(argv) {
|
|
|
145
145
|
let keypair;
|
|
146
146
|
{
|
|
147
147
|
const { randomBytes } = await import("crypto");
|
|
148
|
-
const { ed25519 } = await import("./ed25519-
|
|
148
|
+
const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
|
|
149
149
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
150
150
|
const privateKey = randomBytes(32);
|
|
151
151
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -765,7 +765,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
765
765
|
const { randomBytes } = await import("crypto");
|
|
766
766
|
let keypair;
|
|
767
767
|
try {
|
|
768
|
-
const { ed25519 } = await import("./ed25519-
|
|
768
|
+
const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
|
|
769
769
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
770
770
|
const privateKey = randomBytes(32);
|
|
771
771
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -1111,7 +1111,7 @@ ${bold("protect-mcp init-hooks")}
|
|
|
1111
1111
|
if (!existsSync(keysDir)) mkdirSync(keysDir, { recursive: true });
|
|
1112
1112
|
const { randomBytes: rb } = await import("crypto");
|
|
1113
1113
|
try {
|
|
1114
|
-
const { ed25519 } = await import("./ed25519-
|
|
1114
|
+
const { ed25519 } = await import("./ed25519-DZMMNNVE.mjs");
|
|
1115
1115
|
const { bytesToHex } = await import("./utils-6AYZFE5A.mjs");
|
|
1116
1116
|
const privateKey = rb(32);
|
|
1117
1117
|
const publicKey = ed25519.getPublicKey(privateKey);
|
|
@@ -1442,7 +1442,7 @@ async function main() {
|
|
|
1442
1442
|
if (useHttp) {
|
|
1443
1443
|
const portIdx = args.indexOf("--port");
|
|
1444
1444
|
const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
|
|
1445
|
-
const { startHttpTransport } = await import("./http-transport-
|
|
1445
|
+
const { startHttpTransport } = await import("./http-transport-MO32ESHZ.mjs");
|
|
1446
1446
|
startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
|
|
1447
1447
|
return;
|
|
1448
1448
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ED25519_TORSION_SUBGROUP,
|
|
3
|
+
RistrettoPoint,
|
|
4
|
+
ed25519,
|
|
5
|
+
ed25519_hasher,
|
|
6
|
+
ed25519ctx,
|
|
7
|
+
ed25519ph,
|
|
8
|
+
edwardsToMontgomery,
|
|
9
|
+
edwardsToMontgomeryPriv,
|
|
10
|
+
edwardsToMontgomeryPub,
|
|
11
|
+
encodeToCurve,
|
|
12
|
+
hashToCurve,
|
|
13
|
+
hashToRistretto255,
|
|
14
|
+
hash_to_ristretto255,
|
|
15
|
+
ristretto255,
|
|
16
|
+
ristretto255_hasher,
|
|
17
|
+
x25519
|
|
18
|
+
} from "./chunk-LYKNULYU.mjs";
|
|
19
|
+
import "./chunk-D733KAPG.mjs";
|
|
20
|
+
import "./chunk-PQJP2ZCI.mjs";
|
|
21
|
+
export {
|
|
22
|
+
ED25519_TORSION_SUBGROUP,
|
|
23
|
+
RistrettoPoint,
|
|
24
|
+
ed25519,
|
|
25
|
+
ed25519_hasher,
|
|
26
|
+
ed25519ctx,
|
|
27
|
+
ed25519ph,
|
|
28
|
+
edwardsToMontgomery,
|
|
29
|
+
edwardsToMontgomeryPriv,
|
|
30
|
+
edwardsToMontgomeryPub,
|
|
31
|
+
encodeToCurve,
|
|
32
|
+
hashToCurve,
|
|
33
|
+
hashToRistretto255,
|
|
34
|
+
hash_to_ristretto255,
|
|
35
|
+
ristretto255,
|
|
36
|
+
ristretto255_hasher,
|
|
37
|
+
x25519
|
|
38
|
+
};
|