protect-mcp 0.7.3 → 0.7.5
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 +48 -0
- package/README.md +155 -7
- package/dist/{bundle-XTR3YMPO.mjs → bundle-YUUHAG7O.mjs} +1 -1
- package/dist/{chunk-G67NUBML.mjs → chunk-6E2DHBAR.mjs} +21 -6
- package/dist/chunk-JCMDLN5I.mjs +1053 -0
- package/dist/{chunk-LYKNULYU.mjs → chunk-LJQOALYR.mjs} +152 -0
- package/dist/{chunk-5JXFV37Y.mjs → chunk-PM2ZO57M.mjs} +10 -2
- package/dist/{chunk-J6L4XCTE.mjs → chunk-SETXVE2K.mjs} +77 -2
- package/dist/{chunk-OHUTUFTC.mjs → chunk-VTPZ4G5I.mjs} +19 -15
- package/dist/{chunk-546U3A7R.mjs → chunk-WIPWNWMJ.mjs} +93 -2
- package/dist/chunk-WV4DKYE4.mjs +486 -0
- package/dist/cli.js +4609 -546
- package/dist/cli.mjs +2215 -40
- package/dist/demo-server.d.mts +3 -0
- package/dist/demo-server.d.ts +3 -0
- package/dist/demo-server.js +77 -2
- package/dist/demo-server.mjs +1 -1
- package/dist/{ed25519-DZMMNNVE.mjs → ed25519-BSHMMVNX.mjs} +1 -1
- package/dist/hook-server.js +128 -24
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-R5AO7X6D.mjs → http-transport-JBORN27G.mjs} +3 -3
- package/dist/index.d.mts +158 -10
- package/dist/index.d.ts +158 -10
- package/dist/index.js +2704 -108
- package/dist/index.mjs +1208 -261
- package/dist/receipt-registry-6CAOY6RP.mjs +279 -0
- package/dist/signing-committed-QXCW24RF.mjs +15 -0
- package/package.json +2 -2
- package/dist/chunk-ZBKJANP7.mjs +0 -145
package/dist/index.js
CHANGED
|
@@ -1002,10 +1002,10 @@ var require_types = __commonJS({
|
|
|
1002
1002
|
if (!jwtRegex.test(jwt))
|
|
1003
1003
|
return false;
|
|
1004
1004
|
try {
|
|
1005
|
-
const [
|
|
1006
|
-
if (!
|
|
1005
|
+
const [header2] = jwt.split(".");
|
|
1006
|
+
if (!header2)
|
|
1007
1007
|
return false;
|
|
1008
|
-
const base64 =
|
|
1008
|
+
const base64 = header2.replace(/-/g, "+").replace(/_/g, "/").padEnd(header2.length + (4 - header2.length % 4) % 4, "=");
|
|
1009
1009
|
const decoded = JSON.parse(atob(base64));
|
|
1010
1010
|
if (typeof decoded !== "object" || decoded === null)
|
|
1011
1011
|
return false;
|
|
@@ -6486,10 +6486,10 @@ var require_schemas = __commonJS({
|
|
|
6486
6486
|
const tokensParts = token.split(".");
|
|
6487
6487
|
if (tokensParts.length !== 3)
|
|
6488
6488
|
return false;
|
|
6489
|
-
const [
|
|
6490
|
-
if (!
|
|
6489
|
+
const [header2] = tokensParts;
|
|
6490
|
+
if (!header2)
|
|
6491
6491
|
return false;
|
|
6492
|
-
const parsedHeader = JSON.parse(atob(
|
|
6492
|
+
const parsedHeader = JSON.parse(atob(header2));
|
|
6493
6493
|
if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT")
|
|
6494
6494
|
return false;
|
|
6495
6495
|
if (!parsedHeader.alg)
|
|
@@ -28476,8 +28476,8 @@ var require_resolve = __commonJS({
|
|
|
28476
28476
|
}
|
|
28477
28477
|
return count;
|
|
28478
28478
|
}
|
|
28479
|
-
function getFullPath(resolver, id = "",
|
|
28480
|
-
if (
|
|
28479
|
+
function getFullPath(resolver, id = "", normalize2) {
|
|
28480
|
+
if (normalize2 !== false)
|
|
28481
28481
|
id = normalizeId(id);
|
|
28482
28482
|
const p = resolver.parse(id);
|
|
28483
28483
|
return _getFullPath(resolver, p);
|
|
@@ -29817,7 +29817,7 @@ var require_fast_uri = __commonJS({
|
|
|
29817
29817
|
"use strict";
|
|
29818
29818
|
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
|
|
29819
29819
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
29820
|
-
function
|
|
29820
|
+
function normalize2(uri, options) {
|
|
29821
29821
|
if (typeof uri === "string") {
|
|
29822
29822
|
uri = /** @type {T} */
|
|
29823
29823
|
serialize(parse(uri, options), options);
|
|
@@ -30053,7 +30053,7 @@ var require_fast_uri = __commonJS({
|
|
|
30053
30053
|
}
|
|
30054
30054
|
var fastUri = {
|
|
30055
30055
|
SCHEMES,
|
|
30056
|
-
normalize,
|
|
30056
|
+
normalize: normalize2,
|
|
30057
30057
|
resolve,
|
|
30058
30058
|
resolveComponent,
|
|
30059
30059
|
equal,
|
|
@@ -35019,7 +35019,9 @@ var require_mcp = __commonJS({
|
|
|
35019
35019
|
var index_exports = {};
|
|
35020
35020
|
__export(index_exports, {
|
|
35021
35021
|
BUILTIN_PATTERNS: () => BUILTIN_PATTERNS,
|
|
35022
|
+
CONNECTOR_PILOTS: () => CONNECTOR_PILOTS,
|
|
35022
35023
|
ConfidentialGate: () => ConfidentialGate,
|
|
35024
|
+
POLICY_PACKS: () => POLICY_PACKS,
|
|
35023
35025
|
ProtectGateway: () => ProtectGateway,
|
|
35024
35026
|
ReceiptPropagator: () => ReceiptPropagator,
|
|
35025
35027
|
ScopeBlindBridge: () => ScopeBlindBridge,
|
|
@@ -35029,6 +35031,9 @@ __export(index_exports, {
|
|
|
35029
35031
|
collectSignedReceipts: () => collectSignedReceipts,
|
|
35030
35032
|
computeCalibration: () => computeCalibration,
|
|
35031
35033
|
confidentialInference: () => confidentialInference,
|
|
35034
|
+
connectorDirectory: () => connectorDirectory,
|
|
35035
|
+
connectorDoctor: () => connectorDoctor,
|
|
35036
|
+
connectorPilotIds: () => connectorPilotIds,
|
|
35032
35037
|
createApprovalChallenge: () => createApprovalChallenge,
|
|
35033
35038
|
createApprovalReceiptPayload: () => createApprovalReceiptPayload,
|
|
35034
35039
|
createAttestationField: () => createAttestationField,
|
|
@@ -35040,6 +35045,7 @@ __export(index_exports, {
|
|
|
35040
35045
|
createReceiptChannel: () => createReceiptChannel,
|
|
35041
35046
|
createSandbox: () => createSandbox,
|
|
35042
35047
|
createSandboxServer: () => createSandboxServer,
|
|
35048
|
+
createSelectiveDisclosurePackage: () => createSelectiveDisclosurePackage,
|
|
35043
35049
|
destroySandbox: () => destroySandbox,
|
|
35044
35050
|
discloseField: () => discloseField,
|
|
35045
35051
|
ed25519ToDIDKey: () => ed25519ToDIDKey,
|
|
@@ -35060,6 +35066,8 @@ __export(index_exports, {
|
|
|
35060
35066
|
generateSampleCedarPolicy: () => generateSampleCedarPolicy,
|
|
35061
35067
|
generateSchemaStub: () => generateSchemaStub,
|
|
35062
35068
|
generateVerifyReceiptSkill: () => generateVerifyReceiptSkill,
|
|
35069
|
+
getConnectorPilot: () => getConnectorPilot,
|
|
35070
|
+
getPolicyPack: () => getPolicyPack,
|
|
35063
35071
|
getScopeBlindBridge: () => getScopeBlindBridge,
|
|
35064
35072
|
getSignerInfo: () => getSignerInfo,
|
|
35065
35073
|
getToolPolicy: () => getToolPolicy,
|
|
@@ -35080,8 +35088,10 @@ __export(index_exports, {
|
|
|
35080
35088
|
parseLogFile: () => parseLogFile,
|
|
35081
35089
|
parseNotificationConfigFromEnv: () => parseNotificationConfigFromEnv,
|
|
35082
35090
|
parseRateLimit: () => parseRateLimit,
|
|
35091
|
+
policyPackIds: () => policyPackIds,
|
|
35083
35092
|
policySetFromSource: () => policySetFromSource,
|
|
35084
35093
|
queryExternalPDP: () => queryExternalPDP,
|
|
35094
|
+
readInstalledConnectorPilots: () => readInstalledConnectorPilots,
|
|
35085
35095
|
receiptToVP: () => receiptToVP,
|
|
35086
35096
|
receiptsToHFRows: () => receiptsToHFRows,
|
|
35087
35097
|
redactFields: () => redactFields,
|
|
@@ -35105,13 +35115,15 @@ __export(index_exports, {
|
|
|
35105
35115
|
verifyApprovalAssertion: () => verifyApprovalAssertion,
|
|
35106
35116
|
verifyCommitment: () => verifyCommitment,
|
|
35107
35117
|
verifyEvidenceAttestation: () => verifyEvidenceAttestation,
|
|
35108
|
-
verifyRekorAnchor: () => verifyRekorAnchor
|
|
35118
|
+
verifyRekorAnchor: () => verifyRekorAnchor,
|
|
35119
|
+
verifySelectiveDisclosurePackage: () => verifySelectiveDisclosurePackage,
|
|
35120
|
+
writeConnectorPilots: () => writeConnectorPilots
|
|
35109
35121
|
});
|
|
35110
35122
|
module.exports = __toCommonJS(index_exports);
|
|
35111
35123
|
|
|
35112
35124
|
// src/gateway.ts
|
|
35113
35125
|
var import_node_child_process = require("child_process");
|
|
35114
|
-
var
|
|
35126
|
+
var import_node_crypto4 = require("crypto");
|
|
35115
35127
|
var import_node_readline = require("readline");
|
|
35116
35128
|
var import_node_fs6 = require("fs");
|
|
35117
35129
|
var import_node_path4 = require("path");
|
|
@@ -35541,6 +35553,7 @@ function signDecision(entry) {
|
|
|
35541
35553
|
if (entry.timing) payload.timing = entry.timing;
|
|
35542
35554
|
if (entry.swarm) payload.swarm = entry.swarm;
|
|
35543
35555
|
if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
|
|
35556
|
+
if (entry.action_readback) payload.action_readback = entry.action_readback;
|
|
35544
35557
|
if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
|
|
35545
35558
|
const result = artifactsModule.createSignedArtifact(
|
|
35546
35559
|
artifactType,
|
|
@@ -36214,7 +36227,7 @@ function handleHealth(res, startTime, config) {
|
|
|
36214
36227
|
status: "ok",
|
|
36215
36228
|
uptime_ms: Date.now() - startTime,
|
|
36216
36229
|
mode: config.mode,
|
|
36217
|
-
version:
|
|
36230
|
+
version: process.env.PROTECT_MCP_VERSION || "unknown"
|
|
36218
36231
|
}));
|
|
36219
36232
|
}
|
|
36220
36233
|
function handleStatus(res, logDir) {
|
|
@@ -36347,6 +36360,95 @@ function handleListApprovals(res, approvalStore) {
|
|
|
36347
36360
|
res.end(JSON.stringify({ grants }));
|
|
36348
36361
|
}
|
|
36349
36362
|
|
|
36363
|
+
// src/action-readback.ts
|
|
36364
|
+
var import_node_crypto3 = require("crypto");
|
|
36365
|
+
var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|session|token|private[_-]?key)/i;
|
|
36366
|
+
var DESTINATION_KEYS = [
|
|
36367
|
+
"path",
|
|
36368
|
+
"file_path",
|
|
36369
|
+
"filePath",
|
|
36370
|
+
"url",
|
|
36371
|
+
"uri",
|
|
36372
|
+
"endpoint",
|
|
36373
|
+
"host",
|
|
36374
|
+
"hostname",
|
|
36375
|
+
"repo",
|
|
36376
|
+
"repository",
|
|
36377
|
+
"branch",
|
|
36378
|
+
"channel",
|
|
36379
|
+
"to",
|
|
36380
|
+
"recipient",
|
|
36381
|
+
"symbol",
|
|
36382
|
+
"account",
|
|
36383
|
+
"bucket",
|
|
36384
|
+
"database",
|
|
36385
|
+
"table",
|
|
36386
|
+
"service"
|
|
36387
|
+
];
|
|
36388
|
+
function stableStringify(value) {
|
|
36389
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
36390
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
36391
|
+
const obj = value;
|
|
36392
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
36393
|
+
}
|
|
36394
|
+
function redact(value, path = [], redacted = [], disclosed = [], depth = 0) {
|
|
36395
|
+
if (depth > 4) return "[truncated-depth]";
|
|
36396
|
+
if (value === null || value === void 0) return value;
|
|
36397
|
+
if (typeof value !== "object") {
|
|
36398
|
+
if (path.length > 0) disclosed.push(path.join("."));
|
|
36399
|
+
if (typeof value === "string" && value.length > 240) return `${value.slice(0, 240)}...`;
|
|
36400
|
+
return value;
|
|
36401
|
+
}
|
|
36402
|
+
if (Array.isArray(value)) {
|
|
36403
|
+
return value.slice(0, 20).map((item, idx) => redact(item, [...path, String(idx)], redacted, disclosed, depth + 1));
|
|
36404
|
+
}
|
|
36405
|
+
const out = {};
|
|
36406
|
+
for (const [key, child] of Object.entries(value)) {
|
|
36407
|
+
const childPath = [...path, key];
|
|
36408
|
+
if (SECRET_KEY_RE.test(key)) {
|
|
36409
|
+
redacted.push(childPath.join("."));
|
|
36410
|
+
out[key] = "[redacted]";
|
|
36411
|
+
continue;
|
|
36412
|
+
}
|
|
36413
|
+
out[key] = redact(child, childPath, redacted, disclosed, depth + 1);
|
|
36414
|
+
}
|
|
36415
|
+
return out;
|
|
36416
|
+
}
|
|
36417
|
+
function firstStringValue(input, keys) {
|
|
36418
|
+
for (const key of keys) {
|
|
36419
|
+
const value = input[key];
|
|
36420
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
36421
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
36422
|
+
}
|
|
36423
|
+
return void 0;
|
|
36424
|
+
}
|
|
36425
|
+
function actionFor(tool, input) {
|
|
36426
|
+
const explicit = firstStringValue(input, ["action", "operation", "method", "verb", "command"]);
|
|
36427
|
+
if (explicit) return explicit.length > 90 ? `${explicit.slice(0, 90)}...` : explicit;
|
|
36428
|
+
return tool;
|
|
36429
|
+
}
|
|
36430
|
+
function buildActionReadback(tool, input) {
|
|
36431
|
+
const normalized = input && typeof input === "object" && !Array.isArray(input) ? input : { value: input };
|
|
36432
|
+
const canonical = stableStringify(normalized);
|
|
36433
|
+
const redactedFields = [];
|
|
36434
|
+
const disclosedFields = [];
|
|
36435
|
+
const payloadPreview = redact(normalized, [], redactedFields, disclosedFields);
|
|
36436
|
+
const action = actionFor(tool, normalized);
|
|
36437
|
+
const destination = firstStringValue(normalized, DESTINATION_KEYS);
|
|
36438
|
+
const summary = destination ? `${tool} -> ${destination}` : `${tool} request`;
|
|
36439
|
+
return {
|
|
36440
|
+
tool,
|
|
36441
|
+
action,
|
|
36442
|
+
destination,
|
|
36443
|
+
payload_preview: payloadPreview,
|
|
36444
|
+
payload_hash: (0, import_node_crypto3.createHash)("sha256").update(canonical).digest("hex"),
|
|
36445
|
+
payload_bytes: Buffer.byteLength(canonical, "utf-8"),
|
|
36446
|
+
disclosed_fields: [...new Set(disclosedFields)].slice(0, 80),
|
|
36447
|
+
redacted_fields: [...new Set(redactedFields)].slice(0, 80),
|
|
36448
|
+
summary
|
|
36449
|
+
};
|
|
36450
|
+
}
|
|
36451
|
+
|
|
36350
36452
|
// src/gateway.ts
|
|
36351
36453
|
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
36352
36454
|
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
@@ -36362,7 +36464,7 @@ var ProtectGateway = class {
|
|
|
36362
36464
|
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
36363
36465
|
approvalStore = /* @__PURE__ */ new Map();
|
|
36364
36466
|
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
36365
|
-
approvalNonce = (0,
|
|
36467
|
+
approvalNonce = (0, import_node_crypto4.randomBytes)(16).toString("hex");
|
|
36366
36468
|
currentTier = "unknown";
|
|
36367
36469
|
admissionResult = null;
|
|
36368
36470
|
/** Notification config for approval gates (SMS, webhook, email) */
|
|
@@ -36516,8 +36618,10 @@ var ProtectGateway = class {
|
|
|
36516
36618
|
}
|
|
36517
36619
|
async interceptToolCall(request) {
|
|
36518
36620
|
const toolName = request.params?.name || "unknown";
|
|
36519
|
-
const requestId = (0,
|
|
36621
|
+
const requestId = (0, import_node_crypto4.randomUUID)().slice(0, 12);
|
|
36520
36622
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
36623
|
+
const toolInput = request.params?.arguments && typeof request.params.arguments === "object" ? request.params.arguments : request.params || {};
|
|
36624
|
+
const actionReadback = buildActionReadback(toolName, toolInput);
|
|
36521
36625
|
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
36522
36626
|
let effectiveToolPolicy;
|
|
36523
36627
|
if (this.config.multiAgent?.enabled) {
|
|
@@ -36527,7 +36631,7 @@ var ProtectGateway = class {
|
|
|
36527
36631
|
if (agentOverrides && agentOverrides[toolName]) {
|
|
36528
36632
|
effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
|
|
36529
36633
|
} else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
|
|
36530
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
|
|
36634
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier, action_readback: actionReadback });
|
|
36531
36635
|
if (this.config.enforce) {
|
|
36532
36636
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
|
|
36533
36637
|
}
|
|
@@ -36548,7 +36652,7 @@ var ProtectGateway = class {
|
|
|
36548
36652
|
if (cred.resolved) {
|
|
36549
36653
|
credentialRef = cred.label;
|
|
36550
36654
|
} else if (cred.error && !cred.error.includes("not configured")) {
|
|
36551
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName });
|
|
36655
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName, action_readback: actionReadback });
|
|
36552
36656
|
if (this.config.enforce) {
|
|
36553
36657
|
return this.makeErrorResponse(request.id, -32600, `Credential error for tool "${toolName}"`);
|
|
36554
36658
|
}
|
|
@@ -36563,13 +36667,13 @@ var ProtectGateway = class {
|
|
|
36563
36667
|
});
|
|
36564
36668
|
if (!cedarDecision.allowed) {
|
|
36565
36669
|
const reason = cedarDecision.reason || "cedar_deny";
|
|
36566
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36670
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36567
36671
|
if (this.config.enforce) {
|
|
36568
36672
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by Cedar policy`);
|
|
36569
36673
|
}
|
|
36570
36674
|
return null;
|
|
36571
36675
|
}
|
|
36572
|
-
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36676
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36573
36677
|
return null;
|
|
36574
36678
|
} catch (err) {
|
|
36575
36679
|
if (this.config.verbose) this.log(`Cedar evaluation error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -36587,7 +36691,7 @@ var ProtectGateway = class {
|
|
|
36587
36691
|
const externalDecision = await queryExternalPDP(ctx, this.config.policy.external);
|
|
36588
36692
|
if (!externalDecision.allowed) {
|
|
36589
36693
|
const reason = `external_pdp_deny${externalDecision.reason ? ": " + externalDecision.reason : ""}`;
|
|
36590
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36694
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36591
36695
|
if (this.config.enforce) {
|
|
36592
36696
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by external policy engine`);
|
|
36593
36697
|
}
|
|
@@ -36599,7 +36703,7 @@ var ProtectGateway = class {
|
|
|
36599
36703
|
}
|
|
36600
36704
|
if (toolPolicy.min_tier) {
|
|
36601
36705
|
if (!meetsMinTier(this.currentTier, toolPolicy.min_tier)) {
|
|
36602
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36706
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36603
36707
|
if (this.config.enforce) {
|
|
36604
36708
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" requires tier "${toolPolicy.min_tier}"`);
|
|
36605
36709
|
}
|
|
@@ -36607,7 +36711,7 @@ var ProtectGateway = class {
|
|
|
36607
36711
|
}
|
|
36608
36712
|
}
|
|
36609
36713
|
if (toolPolicy.block) {
|
|
36610
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36714
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36611
36715
|
if (this.config.enforce) {
|
|
36612
36716
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" is blocked by policy`);
|
|
36613
36717
|
}
|
|
@@ -36618,10 +36722,10 @@ var ProtectGateway = class {
|
|
|
36618
36722
|
const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
|
|
36619
36723
|
if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
|
|
36620
36724
|
if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
|
|
36621
|
-
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36725
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36622
36726
|
return null;
|
|
36623
36727
|
}
|
|
36624
|
-
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36728
|
+
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36625
36729
|
if (this.notificationConfig) {
|
|
36626
36730
|
sendApprovalNotification(this.notificationConfig, {
|
|
36627
36731
|
requestId,
|
|
@@ -36644,7 +36748,7 @@ var ProtectGateway = class {
|
|
|
36644
36748
|
content: [
|
|
36645
36749
|
{
|
|
36646
36750
|
type: "text",
|
|
36647
|
-
text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
|
|
36751
|
+
text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Exact action: ${actionReadback.summary}. Payload hash: ${actionReadback.payload_hash.slice(0, 16)}\u2026 Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
|
|
36648
36752
|
}
|
|
36649
36753
|
],
|
|
36650
36754
|
isError: true
|
|
@@ -36660,19 +36764,19 @@ var ProtectGateway = class {
|
|
|
36660
36764
|
const key = `tool:${toolName}:${this.currentTier}`;
|
|
36661
36765
|
const { allowed, remaining } = checkRateLimit(key, limit, this.rateLimitStore);
|
|
36662
36766
|
if (!allowed) {
|
|
36663
|
-
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef });
|
|
36767
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36664
36768
|
if (this.config.enforce) {
|
|
36665
36769
|
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" rate limit exceeded (${rateSpec})`);
|
|
36666
36770
|
}
|
|
36667
36771
|
return null;
|
|
36668
36772
|
}
|
|
36669
|
-
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef });
|
|
36773
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36670
36774
|
} catch {
|
|
36671
|
-
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36775
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36672
36776
|
}
|
|
36673
36777
|
} else {
|
|
36674
36778
|
const reasonCode = this.config.enforce ? "policy_allow" : "observe_mode";
|
|
36675
|
-
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
36779
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
|
|
36676
36780
|
}
|
|
36677
36781
|
return null;
|
|
36678
36782
|
}
|
|
@@ -36694,8 +36798,8 @@ var ProtectGateway = class {
|
|
|
36694
36798
|
*/
|
|
36695
36799
|
emitDecisionLog(entry) {
|
|
36696
36800
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
36697
|
-
const otelTraceId = entry.otel_trace_id || (0,
|
|
36698
|
-
const otelSpanId = entry.otel_span_id || (0,
|
|
36801
|
+
const otelTraceId = entry.otel_trace_id || (0, import_node_crypto4.randomBytes)(16).toString("hex");
|
|
36802
|
+
const otelSpanId = entry.otel_span_id || (0, import_node_crypto4.randomBytes)(8).toString("hex");
|
|
36699
36803
|
const log = {
|
|
36700
36804
|
v: 2,
|
|
36701
36805
|
tool: entry.tool || "unknown",
|
|
@@ -36703,12 +36807,13 @@ var ProtectGateway = class {
|
|
|
36703
36807
|
reason_code: entry.reason_code || "default_allow",
|
|
36704
36808
|
policy_digest: this.config.policyDigest,
|
|
36705
36809
|
policy_engine: this.config.policy?.policy_engine || "built-in",
|
|
36706
|
-
request_id: entry.request_id || (0,
|
|
36810
|
+
request_id: entry.request_id || (0, import_node_crypto4.randomUUID)().slice(0, 12),
|
|
36707
36811
|
timestamp: Date.now(),
|
|
36708
36812
|
mode,
|
|
36709
36813
|
...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining },
|
|
36710
36814
|
...entry.tier && { tier: entry.tier },
|
|
36711
36815
|
...entry.credential_ref && { credential_ref: entry.credential_ref },
|
|
36816
|
+
...entry.action_readback && { action_readback: entry.action_readback },
|
|
36712
36817
|
otel_trace_id: otelTraceId,
|
|
36713
36818
|
otel_span_id: otelSpanId
|
|
36714
36819
|
};
|
|
@@ -36894,6 +36999,12 @@ function abytes(b, ...lengths) {
|
|
|
36894
36999
|
if (lengths.length > 0 && !lengths.includes(b.length))
|
|
36895
37000
|
throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
|
|
36896
37001
|
}
|
|
37002
|
+
function ahash(h) {
|
|
37003
|
+
if (typeof h !== "function" || typeof h.create !== "function")
|
|
37004
|
+
throw new Error("Hash should be wrapped by utils.createHasher");
|
|
37005
|
+
anumber(h.outputLen);
|
|
37006
|
+
anumber(h.blockLen);
|
|
37007
|
+
}
|
|
36897
37008
|
function aexists(instance, checkFinished = true) {
|
|
36898
37009
|
if (instance.destroyed)
|
|
36899
37010
|
throw new Error("Hash instance has been destroyed");
|
|
@@ -37129,6 +37240,24 @@ var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
|
37129
37240
|
528734635,
|
|
37130
37241
|
1541459225
|
|
37131
37242
|
]);
|
|
37243
|
+
var SHA384_IV = /* @__PURE__ */ Uint32Array.from([
|
|
37244
|
+
3418070365,
|
|
37245
|
+
3238371032,
|
|
37246
|
+
1654270250,
|
|
37247
|
+
914150663,
|
|
37248
|
+
2438529370,
|
|
37249
|
+
812702999,
|
|
37250
|
+
355462360,
|
|
37251
|
+
4144912697,
|
|
37252
|
+
1731405415,
|
|
37253
|
+
4290775857,
|
|
37254
|
+
2394180231,
|
|
37255
|
+
1750603025,
|
|
37256
|
+
3675008525,
|
|
37257
|
+
1694076839,
|
|
37258
|
+
1203062813,
|
|
37259
|
+
3204075428
|
|
37260
|
+
]);
|
|
37132
37261
|
var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
37133
37262
|
1779033703,
|
|
37134
37263
|
4089235720,
|
|
@@ -37518,8 +37647,30 @@ var SHA512 = class extends HashMD {
|
|
|
37518
37647
|
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
37519
37648
|
}
|
|
37520
37649
|
};
|
|
37650
|
+
var SHA384 = class extends SHA512 {
|
|
37651
|
+
constructor() {
|
|
37652
|
+
super(48);
|
|
37653
|
+
this.Ah = SHA384_IV[0] | 0;
|
|
37654
|
+
this.Al = SHA384_IV[1] | 0;
|
|
37655
|
+
this.Bh = SHA384_IV[2] | 0;
|
|
37656
|
+
this.Bl = SHA384_IV[3] | 0;
|
|
37657
|
+
this.Ch = SHA384_IV[4] | 0;
|
|
37658
|
+
this.Cl = SHA384_IV[5] | 0;
|
|
37659
|
+
this.Dh = SHA384_IV[6] | 0;
|
|
37660
|
+
this.Dl = SHA384_IV[7] | 0;
|
|
37661
|
+
this.Eh = SHA384_IV[8] | 0;
|
|
37662
|
+
this.El = SHA384_IV[9] | 0;
|
|
37663
|
+
this.Fh = SHA384_IV[10] | 0;
|
|
37664
|
+
this.Fl = SHA384_IV[11] | 0;
|
|
37665
|
+
this.Gh = SHA384_IV[12] | 0;
|
|
37666
|
+
this.Gl = SHA384_IV[13] | 0;
|
|
37667
|
+
this.Hh = SHA384_IV[14] | 0;
|
|
37668
|
+
this.Hl = SHA384_IV[15] | 0;
|
|
37669
|
+
}
|
|
37670
|
+
};
|
|
37521
37671
|
var sha256 = /* @__PURE__ */ createHasher(() => new SHA256());
|
|
37522
37672
|
var sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
|
|
37673
|
+
var sha384 = /* @__PURE__ */ createHasher(() => new SHA384());
|
|
37523
37674
|
|
|
37524
37675
|
// node_modules/@noble/curves/esm/utils.js
|
|
37525
37676
|
var _0n = /* @__PURE__ */ BigInt(0);
|
|
@@ -37543,6 +37694,10 @@ function _abytes2(value, length, title = "") {
|
|
|
37543
37694
|
}
|
|
37544
37695
|
return value;
|
|
37545
37696
|
}
|
|
37697
|
+
function numberToHexUnpadded(num) {
|
|
37698
|
+
const hex = num.toString(16);
|
|
37699
|
+
return hex.length & 1 ? "0" + hex : hex;
|
|
37700
|
+
}
|
|
37546
37701
|
function hexToNumber(hex) {
|
|
37547
37702
|
if (typeof hex !== "string")
|
|
37548
37703
|
throw new Error("hex string expected, got " + typeof hex);
|
|
@@ -37605,6 +37760,56 @@ function bitLen(n) {
|
|
|
37605
37760
|
return len;
|
|
37606
37761
|
}
|
|
37607
37762
|
var bitMask = (n) => (_1n << BigInt(n)) - _1n;
|
|
37763
|
+
function createHmacDrbg(hashLen, qByteLen, hmacFn) {
|
|
37764
|
+
if (typeof hashLen !== "number" || hashLen < 2)
|
|
37765
|
+
throw new Error("hashLen must be a number");
|
|
37766
|
+
if (typeof qByteLen !== "number" || qByteLen < 2)
|
|
37767
|
+
throw new Error("qByteLen must be a number");
|
|
37768
|
+
if (typeof hmacFn !== "function")
|
|
37769
|
+
throw new Error("hmacFn must be a function");
|
|
37770
|
+
const u8n = (len) => new Uint8Array(len);
|
|
37771
|
+
const u8of = (byte) => Uint8Array.of(byte);
|
|
37772
|
+
let v = u8n(hashLen);
|
|
37773
|
+
let k = u8n(hashLen);
|
|
37774
|
+
let i = 0;
|
|
37775
|
+
const reset = () => {
|
|
37776
|
+
v.fill(1);
|
|
37777
|
+
k.fill(0);
|
|
37778
|
+
i = 0;
|
|
37779
|
+
};
|
|
37780
|
+
const h = (...b) => hmacFn(k, v, ...b);
|
|
37781
|
+
const reseed = (seed = u8n(0)) => {
|
|
37782
|
+
k = h(u8of(0), seed);
|
|
37783
|
+
v = h();
|
|
37784
|
+
if (seed.length === 0)
|
|
37785
|
+
return;
|
|
37786
|
+
k = h(u8of(1), seed);
|
|
37787
|
+
v = h();
|
|
37788
|
+
};
|
|
37789
|
+
const gen = () => {
|
|
37790
|
+
if (i++ >= 1e3)
|
|
37791
|
+
throw new Error("drbg: tried 1000 values");
|
|
37792
|
+
let len = 0;
|
|
37793
|
+
const out = [];
|
|
37794
|
+
while (len < qByteLen) {
|
|
37795
|
+
v = h();
|
|
37796
|
+
const sl = v.slice();
|
|
37797
|
+
out.push(sl);
|
|
37798
|
+
len += v.length;
|
|
37799
|
+
}
|
|
37800
|
+
return concatBytes(...out);
|
|
37801
|
+
};
|
|
37802
|
+
const genUntil = (seed, pred) => {
|
|
37803
|
+
reset();
|
|
37804
|
+
reseed(seed);
|
|
37805
|
+
let res = void 0;
|
|
37806
|
+
while (!(res = pred(gen())))
|
|
37807
|
+
reseed();
|
|
37808
|
+
reset();
|
|
37809
|
+
return res;
|
|
37810
|
+
};
|
|
37811
|
+
return genUntil;
|
|
37812
|
+
}
|
|
37608
37813
|
function _validateObject(object, fields, optFields = {}) {
|
|
37609
37814
|
if (!object || typeof object !== "object")
|
|
37610
37815
|
throw new Error("expected valid options object");
|
|
@@ -37959,6 +38164,26 @@ function Field(ORDER, bitLenOrOpts, isLE = false, opts = {}) {
|
|
|
37959
38164
|
});
|
|
37960
38165
|
return Object.freeze(f);
|
|
37961
38166
|
}
|
|
38167
|
+
function getFieldBytesLength(fieldOrder) {
|
|
38168
|
+
if (typeof fieldOrder !== "bigint")
|
|
38169
|
+
throw new Error("field order must be bigint");
|
|
38170
|
+
const bitLength = fieldOrder.toString(2).length;
|
|
38171
|
+
return Math.ceil(bitLength / 8);
|
|
38172
|
+
}
|
|
38173
|
+
function getMinHashLength(fieldOrder) {
|
|
38174
|
+
const length = getFieldBytesLength(fieldOrder);
|
|
38175
|
+
return length + Math.ceil(length / 2);
|
|
38176
|
+
}
|
|
38177
|
+
function mapHashToField(key, fieldOrder, isLE = false) {
|
|
38178
|
+
const len = key.length;
|
|
38179
|
+
const fieldLen = getFieldBytesLength(fieldOrder);
|
|
38180
|
+
const minLen = getMinHashLength(fieldOrder);
|
|
38181
|
+
if (len < 16 || len < minLen || len > 1024)
|
|
38182
|
+
throw new Error("expected " + minLen + "-1024 bytes of input, got " + len);
|
|
38183
|
+
const num = isLE ? bytesToNumberLE(key) : bytesToNumberBE(key);
|
|
38184
|
+
const reduced = mod(num, fieldOrder - _1n2) + _1n2;
|
|
38185
|
+
return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
|
|
38186
|
+
}
|
|
37962
38187
|
|
|
37963
38188
|
// node_modules/@noble/curves/esm/abstract/curve.js
|
|
37964
38189
|
var _0n3 = BigInt(0);
|
|
@@ -38152,6 +38377,21 @@ var wNAF = class {
|
|
|
38152
38377
|
return getW(elm) !== 1;
|
|
38153
38378
|
}
|
|
38154
38379
|
};
|
|
38380
|
+
function mulEndoUnsafe(Point, point, k1, k2) {
|
|
38381
|
+
let acc = point;
|
|
38382
|
+
let p1 = Point.ZERO;
|
|
38383
|
+
let p2 = Point.ZERO;
|
|
38384
|
+
while (k1 > _0n3 || k2 > _0n3) {
|
|
38385
|
+
if (k1 & _1n3)
|
|
38386
|
+
p1 = p1.add(acc);
|
|
38387
|
+
if (k2 & _1n3)
|
|
38388
|
+
p2 = p2.add(acc);
|
|
38389
|
+
acc = acc.double();
|
|
38390
|
+
k1 >>= _1n3;
|
|
38391
|
+
k2 >>= _1n3;
|
|
38392
|
+
}
|
|
38393
|
+
return { p1, p2 };
|
|
38394
|
+
}
|
|
38155
38395
|
function pippenger(c, fieldN, points, scalars) {
|
|
38156
38396
|
validateMSMPoints(points, c);
|
|
38157
38397
|
validateMSMScalars(scalars, fieldN);
|
|
@@ -39058,6 +39298,50 @@ function collectPath(leaves, index, out) {
|
|
|
39058
39298
|
out.push(merkleRoot(leaves.slice(0, k)));
|
|
39059
39299
|
}
|
|
39060
39300
|
}
|
|
39301
|
+
function verifyProof(expectedRootHex, leafHash, proof) {
|
|
39302
|
+
if (proof.index < 0 || proof.index >= proof.treeSize) return false;
|
|
39303
|
+
if (proof.treeSize === 1) {
|
|
39304
|
+
return proof.siblings.length === 0 && bytesToHex(leafHash).toLowerCase() === expectedRootHex.toLowerCase();
|
|
39305
|
+
}
|
|
39306
|
+
let result;
|
|
39307
|
+
try {
|
|
39308
|
+
result = reconstructRoot(
|
|
39309
|
+
leafHash,
|
|
39310
|
+
proof.index,
|
|
39311
|
+
proof.treeSize,
|
|
39312
|
+
proof.siblings
|
|
39313
|
+
);
|
|
39314
|
+
} catch {
|
|
39315
|
+
return false;
|
|
39316
|
+
}
|
|
39317
|
+
return bytesToHex(result).toLowerCase() === expectedRootHex.toLowerCase();
|
|
39318
|
+
}
|
|
39319
|
+
function reconstructRoot(leafHash, index, treeSize, siblings) {
|
|
39320
|
+
if (treeSize === 1) {
|
|
39321
|
+
if (siblings.length !== 0) {
|
|
39322
|
+
throw new Error("reconstructRoot: extra siblings at single-leaf level");
|
|
39323
|
+
}
|
|
39324
|
+
return leafHash;
|
|
39325
|
+
}
|
|
39326
|
+
if (siblings.length === 0) {
|
|
39327
|
+
throw new Error("reconstructRoot: ran out of siblings before single-leaf");
|
|
39328
|
+
}
|
|
39329
|
+
const k = largestPowerOfTwoLessThan(treeSize);
|
|
39330
|
+
const outermostSibling = hexToBytes(siblings[siblings.length - 1]);
|
|
39331
|
+
const innerSiblings = siblings.slice(0, -1);
|
|
39332
|
+
if (index < k) {
|
|
39333
|
+
const leftHash = reconstructRoot(leafHash, index, k, innerSiblings);
|
|
39334
|
+
return hashInternal(leftHash, outermostSibling);
|
|
39335
|
+
} else {
|
|
39336
|
+
const rightHash = reconstructRoot(
|
|
39337
|
+
leafHash,
|
|
39338
|
+
index - k,
|
|
39339
|
+
treeSize - k,
|
|
39340
|
+
innerSiblings
|
|
39341
|
+
);
|
|
39342
|
+
return hashInternal(outermostSibling, rightHash);
|
|
39343
|
+
}
|
|
39344
|
+
}
|
|
39061
39345
|
function largestPowerOfTwoLessThan(n) {
|
|
39062
39346
|
if (n < 2) {
|
|
39063
39347
|
throw new Error(`largestPowerOfTwoLessThan: n must be >= 2 (got ${n})`);
|
|
@@ -39067,6 +39351,74 @@ function largestPowerOfTwoLessThan(n) {
|
|
|
39067
39351
|
return k;
|
|
39068
39352
|
}
|
|
39069
39353
|
|
|
39354
|
+
// node_modules/@noble/hashes/esm/hmac.js
|
|
39355
|
+
var HMAC = class extends Hash {
|
|
39356
|
+
constructor(hash, _key) {
|
|
39357
|
+
super();
|
|
39358
|
+
this.finished = false;
|
|
39359
|
+
this.destroyed = false;
|
|
39360
|
+
ahash(hash);
|
|
39361
|
+
const key = toBytes(_key);
|
|
39362
|
+
this.iHash = hash.create();
|
|
39363
|
+
if (typeof this.iHash.update !== "function")
|
|
39364
|
+
throw new Error("Expected instance of class which extends utils.Hash");
|
|
39365
|
+
this.blockLen = this.iHash.blockLen;
|
|
39366
|
+
this.outputLen = this.iHash.outputLen;
|
|
39367
|
+
const blockLen = this.blockLen;
|
|
39368
|
+
const pad = new Uint8Array(blockLen);
|
|
39369
|
+
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
39370
|
+
for (let i = 0; i < pad.length; i++)
|
|
39371
|
+
pad[i] ^= 54;
|
|
39372
|
+
this.iHash.update(pad);
|
|
39373
|
+
this.oHash = hash.create();
|
|
39374
|
+
for (let i = 0; i < pad.length; i++)
|
|
39375
|
+
pad[i] ^= 54 ^ 92;
|
|
39376
|
+
this.oHash.update(pad);
|
|
39377
|
+
clean(pad);
|
|
39378
|
+
}
|
|
39379
|
+
update(buf) {
|
|
39380
|
+
aexists(this);
|
|
39381
|
+
this.iHash.update(buf);
|
|
39382
|
+
return this;
|
|
39383
|
+
}
|
|
39384
|
+
digestInto(out) {
|
|
39385
|
+
aexists(this);
|
|
39386
|
+
abytes(out, this.outputLen);
|
|
39387
|
+
this.finished = true;
|
|
39388
|
+
this.iHash.digestInto(out);
|
|
39389
|
+
this.oHash.update(out);
|
|
39390
|
+
this.oHash.digestInto(out);
|
|
39391
|
+
this.destroy();
|
|
39392
|
+
}
|
|
39393
|
+
digest() {
|
|
39394
|
+
const out = new Uint8Array(this.oHash.outputLen);
|
|
39395
|
+
this.digestInto(out);
|
|
39396
|
+
return out;
|
|
39397
|
+
}
|
|
39398
|
+
_cloneInto(to) {
|
|
39399
|
+
to || (to = Object.create(Object.getPrototypeOf(this), {}));
|
|
39400
|
+
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
|
39401
|
+
to = to;
|
|
39402
|
+
to.finished = finished;
|
|
39403
|
+
to.destroyed = destroyed;
|
|
39404
|
+
to.blockLen = blockLen;
|
|
39405
|
+
to.outputLen = outputLen;
|
|
39406
|
+
to.oHash = oHash._cloneInto(to.oHash);
|
|
39407
|
+
to.iHash = iHash._cloneInto(to.iHash);
|
|
39408
|
+
return to;
|
|
39409
|
+
}
|
|
39410
|
+
clone() {
|
|
39411
|
+
return this._cloneInto();
|
|
39412
|
+
}
|
|
39413
|
+
destroy() {
|
|
39414
|
+
this.destroyed = true;
|
|
39415
|
+
this.oHash.destroy();
|
|
39416
|
+
this.iHash.destroy();
|
|
39417
|
+
}
|
|
39418
|
+
};
|
|
39419
|
+
var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
|
|
39420
|
+
hmac.create = (hash, key) => new HMAC(hash, key);
|
|
39421
|
+
|
|
39070
39422
|
// src/commitments/primitives.ts
|
|
39071
39423
|
function jcs(value) {
|
|
39072
39424
|
if (value === null || value === void 0) return "null";
|
|
@@ -39085,6 +39437,17 @@ function base64urlNoPad(bytes) {
|
|
|
39085
39437
|
const std = typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : btoa(String.fromCharCode(...bytes));
|
|
39086
39438
|
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
39087
39439
|
}
|
|
39440
|
+
function base64urlDecode(s) {
|
|
39441
|
+
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
39442
|
+
const padded = std + "=".repeat((4 - std.length % 4) % 4);
|
|
39443
|
+
if (typeof Buffer !== "undefined") {
|
|
39444
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
39445
|
+
}
|
|
39446
|
+
const bin = atob(padded);
|
|
39447
|
+
const out = new Uint8Array(bin.length);
|
|
39448
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
39449
|
+
return out;
|
|
39450
|
+
}
|
|
39088
39451
|
function encodeLeaf(field) {
|
|
39089
39452
|
const obj = {
|
|
39090
39453
|
name: field.name,
|
|
@@ -39218,11 +39581,128 @@ function discloseField(receiptHash, fieldName, openings) {
|
|
|
39218
39581
|
proof
|
|
39219
39582
|
};
|
|
39220
39583
|
}
|
|
39584
|
+
function createSelectiveDisclosurePackage(receipt, fieldNames, openings) {
|
|
39585
|
+
const receiptHash = receiptHashHex(receipt);
|
|
39586
|
+
const committedFieldsRoot = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
39587
|
+
if (!committedFieldsRoot) {
|
|
39588
|
+
throw new Error("selective disclosure requires a committed receipt with committed_fields_root");
|
|
39589
|
+
}
|
|
39590
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, openings);
|
|
39591
|
+
const uniqueFields = Array.from(new Set(fieldNames));
|
|
39592
|
+
for (const fieldName of uniqueFields) {
|
|
39593
|
+
if (!committedFieldNames.includes(fieldName)) {
|
|
39594
|
+
throw new Error(`selective disclosure: field "${fieldName}" is not committed by this receipt`);
|
|
39595
|
+
}
|
|
39596
|
+
}
|
|
39597
|
+
const disclosures = uniqueFields.map((fieldName) => discloseField(receiptHash, fieldName, openings));
|
|
39598
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !uniqueFields.includes(fieldName));
|
|
39599
|
+
return {
|
|
39600
|
+
type: "scopeblind.selective_disclosure.v0",
|
|
39601
|
+
version: 0,
|
|
39602
|
+
parent_receipt_hash: receiptHash,
|
|
39603
|
+
committed_fields_root: committedFieldsRoot,
|
|
39604
|
+
disclosed_fields: uniqueFields,
|
|
39605
|
+
hidden_fields: hiddenFields,
|
|
39606
|
+
disclosures,
|
|
39607
|
+
verifier_explanation: {
|
|
39608
|
+
summary: "This package opens selected committed receipt fields and leaves the rest hidden.",
|
|
39609
|
+
disclosed: uniqueFields.length ? `Disclosed fields: ${uniqueFields.join(", ")}.` : "No fields were disclosed.",
|
|
39610
|
+
hidden: hiddenFields.length ? `Hidden committed fields: ${hiddenFields.join(", ")}. Their salted commitments remain bound to the signed receipt root.` : "No committed fields remain hidden.",
|
|
39611
|
+
limitation: "Selective Disclosure v0 uses salted SHA-256 commitments and Merkle proofs. It is not a full zero-knowledge proof."
|
|
39612
|
+
}
|
|
39613
|
+
};
|
|
39614
|
+
}
|
|
39615
|
+
function verifySelectiveDisclosurePackage(receipt, disclosure) {
|
|
39616
|
+
const errors = [];
|
|
39617
|
+
if (disclosure.type !== "scopeblind.selective_disclosure.v0") {
|
|
39618
|
+
errors.push("disclosure.type is not scopeblind.selective_disclosure.v0");
|
|
39619
|
+
}
|
|
39620
|
+
const actualReceiptHash = receiptHashHex(receipt);
|
|
39621
|
+
const receiptHashValid = disclosure.parent_receipt_hash === actualReceiptHash;
|
|
39622
|
+
if (!receiptHashValid) {
|
|
39623
|
+
errors.push("parent_receipt_hash does not match the supplied receipt");
|
|
39624
|
+
}
|
|
39625
|
+
const root = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
39626
|
+
const commitmentRootValid = Boolean(root) && disclosure.committed_fields_root === root;
|
|
39627
|
+
if (!commitmentRootValid) {
|
|
39628
|
+
errors.push("committed_fields_root does not match the supplied receipt");
|
|
39629
|
+
}
|
|
39630
|
+
const signatureValid = verifyCommittedReceiptSignature(receipt);
|
|
39631
|
+
if (signatureValid === false) {
|
|
39632
|
+
errors.push("receipt signature failed verification");
|
|
39633
|
+
}
|
|
39634
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, {});
|
|
39635
|
+
const disclosed = /* @__PURE__ */ new Set();
|
|
39636
|
+
for (const item of disclosure.disclosures || []) {
|
|
39637
|
+
if (item.parent_receipt_hash !== disclosure.parent_receipt_hash) {
|
|
39638
|
+
errors.push(`disclosure for "${item.name}" targets a different receipt hash`);
|
|
39639
|
+
continue;
|
|
39640
|
+
}
|
|
39641
|
+
if (!committedFieldNames.includes(item.name)) {
|
|
39642
|
+
errors.push(`field "${item.name}" is not listed in committed_field_names`);
|
|
39643
|
+
continue;
|
|
39644
|
+
}
|
|
39645
|
+
const leafBytes = encodeLeaf({
|
|
39646
|
+
name: item.name,
|
|
39647
|
+
salt: base64urlDecode(item.salt),
|
|
39648
|
+
value: item.value
|
|
39649
|
+
});
|
|
39650
|
+
const ok = root ? verifyProof(root, hashLeaf(leafBytes), item.proof) : false;
|
|
39651
|
+
if (!ok) {
|
|
39652
|
+
errors.push(`field "${item.name}" failed Merkle inclusion verification`);
|
|
39653
|
+
} else {
|
|
39654
|
+
disclosed.add(item.name);
|
|
39655
|
+
}
|
|
39656
|
+
}
|
|
39657
|
+
const disclosedFields = Array.from(disclosed);
|
|
39658
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !disclosed.has(fieldName));
|
|
39659
|
+
const valid = errors.length === 0 && receiptHashValid && commitmentRootValid && signatureValid !== false;
|
|
39660
|
+
const explanation = [
|
|
39661
|
+
valid ? "Selective disclosure verified: the disclosed fields open to the signed receipt commitment root." : "Selective disclosure failed verification.",
|
|
39662
|
+
signatureValid === true ? "Receipt signature verified against the embedded Ed25519 public key." : signatureValid === null ? "Receipt signature was not checked because the committed receipt did not carry an embedded Ed25519 signature object." : "Receipt signature did not verify.",
|
|
39663
|
+
disclosedFields.length ? `Disclosed fields: ${disclosedFields.join(", ")}.` : "No fields were disclosed.",
|
|
39664
|
+
hiddenFields.length ? `Hidden fields: ${hiddenFields.join(", ")}. These remain private but bound to the same commitment root.` : "No committed fields remain hidden.",
|
|
39665
|
+
"Limitation: this is salted commitment disclosure, not full zero-knowledge."
|
|
39666
|
+
];
|
|
39667
|
+
return {
|
|
39668
|
+
valid,
|
|
39669
|
+
receipt_hash_valid: receiptHashValid,
|
|
39670
|
+
signature_valid: signatureValid,
|
|
39671
|
+
commitment_root_valid: commitmentRootValid,
|
|
39672
|
+
disclosed_fields: disclosedFields,
|
|
39673
|
+
hidden_fields: hiddenFields,
|
|
39674
|
+
errors,
|
|
39675
|
+
explanation
|
|
39676
|
+
};
|
|
39677
|
+
}
|
|
39678
|
+
function committedFieldNamesFromReceipt(receipt, openings) {
|
|
39679
|
+
const fromReceipt = Array.isArray(receipt.committed_field_names) ? receipt.committed_field_names.filter((fieldName) => typeof fieldName === "string") : [];
|
|
39680
|
+
const names = fromReceipt.length ? fromReceipt : Object.keys(openings);
|
|
39681
|
+
return Array.from(new Set(names)).sort();
|
|
39682
|
+
}
|
|
39683
|
+
function receiptHashHex(receipt) {
|
|
39684
|
+
return bytesToHex(sha2562(new TextEncoder().encode(jcs(receipt))));
|
|
39685
|
+
}
|
|
39686
|
+
function verifyCommittedReceiptSignature(receipt) {
|
|
39687
|
+
const signature = receipt.signature;
|
|
39688
|
+
if (!signature || typeof signature !== "object") return null;
|
|
39689
|
+
const sig = signature;
|
|
39690
|
+
if (sig.alg !== "EdDSA" || typeof sig.sig !== "string" || typeof sig.public_key !== "string") {
|
|
39691
|
+
return null;
|
|
39692
|
+
}
|
|
39693
|
+
const { signature: _signature, ...payloadWithoutSig } = receipt;
|
|
39694
|
+
const messageHash = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
|
|
39695
|
+
try {
|
|
39696
|
+
return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
|
|
39697
|
+
} catch {
|
|
39698
|
+
return false;
|
|
39699
|
+
}
|
|
39700
|
+
}
|
|
39221
39701
|
|
|
39222
39702
|
// src/bundle.ts
|
|
39223
39703
|
function createAuditBundle(opts) {
|
|
39224
39704
|
const receipts = opts.receipts.filter(
|
|
39225
|
-
(r) => r && typeof r === "object" && typeof r.signature === "string"
|
|
39705
|
+
(r) => r && typeof r === "object" && (typeof r.signature === "string" || r.signature !== null && typeof r.signature === "object")
|
|
39226
39706
|
);
|
|
39227
39707
|
if (receipts.length === 0) {
|
|
39228
39708
|
throw new Error("Audit bundle requires at least one signed receipt");
|
|
@@ -39251,10 +39731,18 @@ function createAuditBundle(opts) {
|
|
|
39251
39731
|
time_range: timeRange,
|
|
39252
39732
|
receipts,
|
|
39253
39733
|
anchors: opts.anchors || [],
|
|
39734
|
+
selective_disclosures: opts.selectiveDisclosures || [],
|
|
39735
|
+
privacy: {
|
|
39736
|
+
selective_disclosure: {
|
|
39737
|
+
supported: true,
|
|
39738
|
+
model: "salted_commitments_merkle_v0",
|
|
39739
|
+
statement: "Committed receipts may disclose selected fields with salted Merkle openings. Undisclosed committed fields remain hidden while staying bound to the signed commitment root."
|
|
39740
|
+
}
|
|
39741
|
+
},
|
|
39254
39742
|
verification: {
|
|
39255
39743
|
algorithm: "ed25519",
|
|
39256
39744
|
signing_keys: Array.from(keyMap.values()),
|
|
39257
|
-
instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. CLI: npx @veritasacta/verify bundle.json --bundle`
|
|
39745
|
+
instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. For scopeblind.selective_disclosure.v0 packages, recompute each disclosed leaf and verify it against the receipt committed_fields_root; fields not disclosed remain hidden. CLI: npx @veritasacta/verify bundle.json --bundle`
|
|
39258
39746
|
}
|
|
39259
39747
|
};
|
|
39260
39748
|
}
|
|
@@ -39717,7 +40205,7 @@ function validateEvidenceReceipt(receipt) {
|
|
|
39717
40205
|
|
|
39718
40206
|
// src/hook-server.ts
|
|
39719
40207
|
var import_node_http2 = require("http");
|
|
39720
|
-
var
|
|
40208
|
+
var import_node_crypto5 = require("crypto");
|
|
39721
40209
|
var import_node_fs9 = require("fs");
|
|
39722
40210
|
var import_node_path5 = require("path");
|
|
39723
40211
|
|
|
@@ -39900,7 +40388,7 @@ function computePayloadDigest(input) {
|
|
|
39900
40388
|
return void 0;
|
|
39901
40389
|
}
|
|
39902
40390
|
return {
|
|
39903
|
-
input_hash: (0,
|
|
40391
|
+
input_hash: (0, import_node_crypto5.createHash)("sha256").update(content).digest("hex"),
|
|
39904
40392
|
input_size: size,
|
|
39905
40393
|
truncated: true,
|
|
39906
40394
|
preview: content.slice(0, 256)
|
|
@@ -39913,7 +40401,7 @@ function computeOutputDigest(output) {
|
|
|
39913
40401
|
return void 0;
|
|
39914
40402
|
}
|
|
39915
40403
|
return {
|
|
39916
|
-
output_hash: (0,
|
|
40404
|
+
output_hash: (0, import_node_crypto5.createHash)("sha256").update(content).digest("hex"),
|
|
39917
40405
|
output_size: size
|
|
39918
40406
|
};
|
|
39919
40407
|
}
|
|
@@ -39936,13 +40424,14 @@ function detectSandboxState() {
|
|
|
39936
40424
|
async function handlePreToolUse(input, state) {
|
|
39937
40425
|
const hookStart = Date.now();
|
|
39938
40426
|
const toolName = input.toolName || "unknown";
|
|
39939
|
-
const requestId = input.toolUseId || (0,
|
|
40427
|
+
const requestId = input.toolUseId || (0, import_node_crypto5.randomUUID)().slice(0, 12);
|
|
39940
40428
|
state.inflightTools.set(requestId, {
|
|
39941
40429
|
tool: toolName,
|
|
39942
40430
|
startedAt: hookStart,
|
|
39943
40431
|
requestId
|
|
39944
40432
|
});
|
|
39945
40433
|
const payloadDigest = computePayloadDigest(input.toolInput);
|
|
40434
|
+
const actionReadback = buildActionReadback(toolName, input.toolInput || {});
|
|
39946
40435
|
const swarm = {
|
|
39947
40436
|
...state.swarmContext,
|
|
39948
40437
|
...input.agentId && { agent_id: input.agentId },
|
|
@@ -39982,6 +40471,7 @@ async function handlePreToolUse(input, state) {
|
|
|
39982
40471
|
swarm: swarm.team_name ? swarm : void 0,
|
|
39983
40472
|
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
39984
40473
|
payload_digest: payloadDigest,
|
|
40474
|
+
action_readback: actionReadback,
|
|
39985
40475
|
deny_iteration: denyCount,
|
|
39986
40476
|
sandbox_state: detectSandboxState(),
|
|
39987
40477
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
@@ -40016,6 +40506,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40016
40506
|
swarm: swarm.team_name ? swarm : void 0,
|
|
40017
40507
|
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
40018
40508
|
payload_digest: payloadDigest,
|
|
40509
|
+
action_readback: actionReadback,
|
|
40019
40510
|
sandbox_state: detectSandboxState()
|
|
40020
40511
|
});
|
|
40021
40512
|
return {
|
|
@@ -40040,6 +40531,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40040
40531
|
swarm: swarm.team_name ? swarm : void 0,
|
|
40041
40532
|
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
40042
40533
|
payload_digest: payloadDigest,
|
|
40534
|
+
action_readback: actionReadback,
|
|
40043
40535
|
sandbox_state: detectSandboxState()
|
|
40044
40536
|
});
|
|
40045
40537
|
return {
|
|
@@ -40060,13 +40552,15 @@ async function handlePreToolUse(input, state) {
|
|
|
40060
40552
|
hook_event: "PreToolUse",
|
|
40061
40553
|
swarm: swarm.team_name ? swarm : void 0,
|
|
40062
40554
|
timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
|
|
40555
|
+
payload_digest: payloadDigest,
|
|
40556
|
+
action_readback: actionReadback,
|
|
40063
40557
|
sandbox_state: detectSandboxState()
|
|
40064
40558
|
});
|
|
40065
40559
|
return {
|
|
40066
40560
|
hookSpecificOutput: {
|
|
40067
40561
|
hookEventName: "PreToolUse",
|
|
40068
40562
|
permissionDecision: "ask",
|
|
40069
|
-
permissionDecisionReason: `[ScopeBlind]
|
|
40563
|
+
permissionDecisionReason: `[ScopeBlind] Approval required for exactly this action: ${actionReadback.summary}. Payload hash: ${actionReadback.payload_hash.slice(0, 16)}\u2026 Policy: ${state.policyDigest}`
|
|
40070
40564
|
}
|
|
40071
40565
|
};
|
|
40072
40566
|
}
|
|
@@ -40111,6 +40605,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40111
40605
|
swarm: swarm.team_name ? swarm : void 0,
|
|
40112
40606
|
timing: { hook_latency_ms: hookLatency, started_at: hookStart },
|
|
40113
40607
|
payload_digest: payloadDigest,
|
|
40608
|
+
action_readback: actionReadback,
|
|
40114
40609
|
sandbox_state: detectSandboxState(),
|
|
40115
40610
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
40116
40611
|
});
|
|
@@ -40127,7 +40622,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40127
40622
|
}
|
|
40128
40623
|
async function handlePostToolUse(input, state) {
|
|
40129
40624
|
const toolName = input.toolName || "unknown";
|
|
40130
|
-
const requestId = input.toolUseId || (0,
|
|
40625
|
+
const requestId = input.toolUseId || (0, import_node_crypto5.randomUUID)().slice(0, 12);
|
|
40131
40626
|
const now = Date.now();
|
|
40132
40627
|
const inflight = state.inflightTools.get(requestId);
|
|
40133
40628
|
const timing = {
|
|
@@ -40139,7 +40634,7 @@ async function handlePostToolUse(input, state) {
|
|
|
40139
40634
|
state.inflightTools.delete(requestId);
|
|
40140
40635
|
}
|
|
40141
40636
|
const outputDigest = computeOutputDigest(input.toolResult);
|
|
40142
|
-
const receiptId = (0,
|
|
40637
|
+
const receiptId = (0, import_node_crypto5.randomUUID)().slice(0, 8);
|
|
40143
40638
|
const policyName = state.cedarPolicies ? `cedar:${state.policyDigest}` : state.policyDigest;
|
|
40144
40639
|
const additionalContext = `[ScopeBlind] Tool call receipted. Policy: ${policyName}. Decision: allow. Receipt: #${receiptId}.` + (timing.tool_duration_ms !== void 0 ? ` Duration: ${timing.tool_duration_ms}ms.` : "") + (timing.hook_latency_ms !== void 0 ? ` Overhead: ${timing.hook_latency_ms}ms.` : "");
|
|
40145
40640
|
emitDecisionLog(state, {
|
|
@@ -40171,7 +40666,7 @@ function handleSubagentStart(input, state) {
|
|
|
40171
40666
|
tool: `subagent:${agentId}`,
|
|
40172
40667
|
decision: "allow",
|
|
40173
40668
|
reason_code: "subagent_started",
|
|
40174
|
-
request_id: (0,
|
|
40669
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40175
40670
|
hook_event: "SubagentStart",
|
|
40176
40671
|
swarm: {
|
|
40177
40672
|
...state.swarmContext,
|
|
@@ -40192,7 +40687,7 @@ function handleSubagentStop(input, state) {
|
|
|
40192
40687
|
tool: `subagent:${agentId}`,
|
|
40193
40688
|
decision: "allow",
|
|
40194
40689
|
reason_code: "subagent_stopped",
|
|
40195
|
-
request_id: (0,
|
|
40690
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40196
40691
|
hook_event: "SubagentStop",
|
|
40197
40692
|
swarm: {
|
|
40198
40693
|
...state.swarmContext,
|
|
@@ -40207,7 +40702,7 @@ function handleTaskCreated(input, state) {
|
|
|
40207
40702
|
tool: `task:${input.taskId || "unknown"}`,
|
|
40208
40703
|
decision: "allow",
|
|
40209
40704
|
reason_code: "task_created",
|
|
40210
|
-
request_id: (0,
|
|
40705
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40211
40706
|
hook_event: "TaskCreated",
|
|
40212
40707
|
swarm: {
|
|
40213
40708
|
...state.swarmContext,
|
|
@@ -40221,7 +40716,7 @@ function handleTaskCompleted(input, state) {
|
|
|
40221
40716
|
tool: `task:${input.taskId || "unknown"}`,
|
|
40222
40717
|
decision: "allow",
|
|
40223
40718
|
reason_code: "task_completed",
|
|
40224
|
-
request_id: (0,
|
|
40719
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40225
40720
|
hook_event: "TaskCompleted",
|
|
40226
40721
|
swarm: state.swarmContext
|
|
40227
40722
|
});
|
|
@@ -40232,7 +40727,7 @@ function handleSessionStart(input, state) {
|
|
|
40232
40727
|
tool: "session",
|
|
40233
40728
|
decision: "allow",
|
|
40234
40729
|
reason_code: "session_started",
|
|
40235
|
-
request_id: input.sessionId || (0,
|
|
40730
|
+
request_id: input.sessionId || (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40236
40731
|
hook_event: "SessionStart",
|
|
40237
40732
|
swarm: state.swarmContext,
|
|
40238
40733
|
sandbox_state: detectSandboxState()
|
|
@@ -40255,7 +40750,7 @@ function handleSessionEnd(input, state) {
|
|
|
40255
40750
|
tool: "session",
|
|
40256
40751
|
decision: "allow",
|
|
40257
40752
|
reason_code: "session_ended",
|
|
40258
|
-
request_id: input.sessionId || (0,
|
|
40753
|
+
request_id: input.sessionId || (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40259
40754
|
hook_event: "SessionEnd",
|
|
40260
40755
|
swarm: state.swarmContext
|
|
40261
40756
|
});
|
|
@@ -40266,7 +40761,7 @@ function handleTeammateIdle(input, state) {
|
|
|
40266
40761
|
tool: `teammate:${input.agentId || "unknown"}`,
|
|
40267
40762
|
decision: "allow",
|
|
40268
40763
|
reason_code: "teammate_idle",
|
|
40269
|
-
request_id: (0,
|
|
40764
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40270
40765
|
hook_event: "TeammateIdle",
|
|
40271
40766
|
swarm: {
|
|
40272
40767
|
...state.swarmContext,
|
|
@@ -40294,7 +40789,7 @@ function handleConfigChange(input, state) {
|
|
|
40294
40789
|
tool: "config",
|
|
40295
40790
|
decision: "deny",
|
|
40296
40791
|
reason_code: "config_tamper_detected",
|
|
40297
|
-
request_id: (0,
|
|
40792
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40298
40793
|
hook_event: "ConfigChange",
|
|
40299
40794
|
swarm: state.swarmContext
|
|
40300
40795
|
});
|
|
@@ -40303,7 +40798,7 @@ function handleConfigChange(input, state) {
|
|
|
40303
40798
|
tool: "config",
|
|
40304
40799
|
decision: "allow",
|
|
40305
40800
|
reason_code: "config_changed",
|
|
40306
|
-
request_id: (0,
|
|
40801
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40307
40802
|
hook_event: "ConfigChange"
|
|
40308
40803
|
});
|
|
40309
40804
|
}
|
|
@@ -40325,7 +40820,7 @@ function handleStop(input, state) {
|
|
|
40325
40820
|
tool: "session",
|
|
40326
40821
|
decision: "allow",
|
|
40327
40822
|
reason_code: "agent_stopped",
|
|
40328
|
-
request_id: (0,
|
|
40823
|
+
request_id: (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40329
40824
|
hook_event: "Stop",
|
|
40330
40825
|
swarm: state.swarmContext
|
|
40331
40826
|
});
|
|
@@ -40333,8 +40828,8 @@ function handleStop(input, state) {
|
|
|
40333
40828
|
}
|
|
40334
40829
|
function emitDecisionLog(state, entry) {
|
|
40335
40830
|
const mode = state.enforce ? "enforce" : "shadow";
|
|
40336
|
-
const otelTraceId = (0,
|
|
40337
|
-
const otelSpanId = (0,
|
|
40831
|
+
const otelTraceId = (0, import_node_crypto5.randomBytes)(16).toString("hex");
|
|
40832
|
+
const otelSpanId = (0, import_node_crypto5.randomBytes)(8).toString("hex");
|
|
40338
40833
|
const log = {
|
|
40339
40834
|
v: 2,
|
|
40340
40835
|
tool: entry.tool || "unknown",
|
|
@@ -40342,7 +40837,7 @@ function emitDecisionLog(state, entry) {
|
|
|
40342
40837
|
reason_code: entry.reason_code || "default_allow",
|
|
40343
40838
|
policy_digest: state.policyDigest,
|
|
40344
40839
|
policy_engine: state.cedarPolicies ? "cedar" : "built-in",
|
|
40345
|
-
request_id: entry.request_id || (0,
|
|
40840
|
+
request_id: entry.request_id || (0, import_node_crypto5.randomUUID)().slice(0, 12),
|
|
40346
40841
|
timestamp: Date.now(),
|
|
40347
40842
|
mode,
|
|
40348
40843
|
otel_trace_id: otelTraceId,
|
|
@@ -40523,7 +41018,7 @@ async function startHookServer(options = {}) {
|
|
|
40523
41018
|
res.end(JSON.stringify({
|
|
40524
41019
|
status: "ok",
|
|
40525
41020
|
server: "protect-mcp-hooks",
|
|
40526
|
-
version:
|
|
41021
|
+
version: process.env.PROTECT_MCP_VERSION || "unknown",
|
|
40527
41022
|
uptime_ms: Date.now() - state.startTime,
|
|
40528
41023
|
mode: enforce ? "enforce" : "shadow",
|
|
40529
41024
|
policy_digest: policyDigest,
|
|
@@ -40610,9 +41105,10 @@ async function startHookServer(options = {}) {
|
|
|
40610
41105
|
const pad = (s, n = 46) => s.padEnd(n);
|
|
40611
41106
|
w(`
|
|
40612
41107
|
`);
|
|
40613
|
-
w(` protect-mcp
|
|
41108
|
+
w(process.env.PROTECT_MCP_VERSION ? ` protect-mcp v${process.env.PROTECT_MCP_VERSION}
|
|
41109
|
+
` : ` protect-mcp
|
|
40614
41110
|
`);
|
|
40615
|
-
w(` ScopeBlind \
|
|
41111
|
+
w(` ScopeBlind \xB7 https://scopeblind.com
|
|
40616
41112
|
`);
|
|
40617
41113
|
w(`
|
|
40618
41114
|
`);
|
|
@@ -40640,7 +41136,13 @@ async function startHookServer(options = {}) {
|
|
|
40640
41136
|
`);
|
|
40641
41137
|
w(`
|
|
40642
41138
|
`);
|
|
40643
|
-
w(` deny is authoritative
|
|
41139
|
+
w(` deny is authoritative: it cannot be overridden.
|
|
41140
|
+
`);
|
|
41141
|
+
w(`
|
|
41142
|
+
`);
|
|
41143
|
+
w(` See your record npx protect-mcp record
|
|
41144
|
+
`);
|
|
41145
|
+
w(` a searchable view of every decision, all on this machine
|
|
40644
41146
|
`);
|
|
40645
41147
|
w(`
|
|
40646
41148
|
`);
|
|
@@ -41321,8 +41823,906 @@ function generateSchemaStub(namespace = "ScopeBlind") {
|
|
|
41321
41823
|
].join("\n");
|
|
41322
41824
|
}
|
|
41323
41825
|
|
|
41826
|
+
// src/policy-packs.ts
|
|
41827
|
+
var header = (id, description) => `// ScopeBlind protect-mcp policy pack: ${id}
|
|
41828
|
+
// ${description}
|
|
41829
|
+
// Start in shadow mode, review receipts, then run with --enforce.
|
|
41830
|
+
|
|
41831
|
+
`;
|
|
41832
|
+
var defaultPermit = `
|
|
41833
|
+
// Default posture: allow non-matching calls so teams can start in shadow mode.
|
|
41834
|
+
// Tighten this after reviewing your local action dashboard.
|
|
41835
|
+
permit(principal, action == Action::"MCP::Tool::call", resource);
|
|
41836
|
+
`;
|
|
41837
|
+
var filesystemSafe = `${header("filesystem-safe", "Block common destructive filesystem and secret-file access patterns.")}// Destructive file tools are never safe as an unattended default.
|
|
41838
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"delete_file");
|
|
41839
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"remove_file");
|
|
41840
|
+
|
|
41841
|
+
// Secret-like reads by path.
|
|
41842
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41843
|
+
context has "input" && context.input has "path" && (
|
|
41844
|
+
context.input.path like "*/.env*" ||
|
|
41845
|
+
context.input.path like "*/id_rsa*" ||
|
|
41846
|
+
context.input.path like "*/.ssh/*" ||
|
|
41847
|
+
context.input.path like "*secret*" ||
|
|
41848
|
+
context.input.path like "*credential*"
|
|
41849
|
+
)
|
|
41850
|
+
};
|
|
41851
|
+
|
|
41852
|
+
// Dangerous shell operations that mutate or destroy local state.
|
|
41853
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash") when {
|
|
41854
|
+
context has "command" && (
|
|
41855
|
+
context.command like "*rm -rf*" ||
|
|
41856
|
+
context.command like "*mkfs*" ||
|
|
41857
|
+
context.command like "*dd if=*" ||
|
|
41858
|
+
context.command like "*chmod -R 777*" ||
|
|
41859
|
+
context.command like "*chown -R*"
|
|
41860
|
+
)
|
|
41861
|
+
};
|
|
41862
|
+
${defaultPermit}`;
|
|
41863
|
+
var gitSafe = `${header("git-safe", "Prevent unattended history rewrites, force pushes, and destructive repo cleanup.")}forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash") when {
|
|
41864
|
+
context has "command" && (
|
|
41865
|
+
context.command like "*git push --force*" ||
|
|
41866
|
+
context.command like "*git push -f*" ||
|
|
41867
|
+
context.command like "*git reset --hard*" ||
|
|
41868
|
+
context.command like "*git clean -fd*" ||
|
|
41869
|
+
context.command like "*git checkout --*" ||
|
|
41870
|
+
context.command like "*git branch -D*" ||
|
|
41871
|
+
context.command like "*gh repo delete*"
|
|
41872
|
+
)
|
|
41873
|
+
};
|
|
41874
|
+
${defaultPermit}`;
|
|
41875
|
+
var emailSafe = `${header("email-safe", "Permit drafting but block unattended external sends.")}forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"mail.send");
|
|
41876
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"email.send");
|
|
41877
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"send_email");
|
|
41878
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"gmail.send");
|
|
41879
|
+
|
|
41880
|
+
// Shell fallbacks that send mail are blocked too.
|
|
41881
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash") when {
|
|
41882
|
+
context has "command" && (
|
|
41883
|
+
context.command like "*sendmail*" ||
|
|
41884
|
+
context.command like "*mailx*" ||
|
|
41885
|
+
context.command like "*smtp*"
|
|
41886
|
+
)
|
|
41887
|
+
};
|
|
41888
|
+
${defaultPermit}`;
|
|
41889
|
+
var databaseSafe = `${header("database-safe", "Allow reads, block write/admin SQL unless explicitly approved elsewhere.")}forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41890
|
+
context has "input" && context.input has "query" && (
|
|
41891
|
+
context.input.query like "*DROP *" ||
|
|
41892
|
+
context.input.query like "*TRUNCATE *" ||
|
|
41893
|
+
context.input.query like "*DELETE *" ||
|
|
41894
|
+
context.input.query like "*UPDATE *" ||
|
|
41895
|
+
context.input.query like "*INSERT *" ||
|
|
41896
|
+
context.input.query like "*ALTER *" ||
|
|
41897
|
+
context.input.query like "*GRANT *" ||
|
|
41898
|
+
context.input.query like "*REVOKE *"
|
|
41899
|
+
)
|
|
41900
|
+
};
|
|
41901
|
+
${defaultPermit}`;
|
|
41902
|
+
var cloudSpendSafe = `${header("cloud-spend-safe", "Block cloud actions that can create spend or destroy infrastructure.")}forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash") when {
|
|
41903
|
+
context has "command" && (
|
|
41904
|
+
context.command like "*terraform destroy*" ||
|
|
41905
|
+
context.command like "*terraform apply*" ||
|
|
41906
|
+
context.command like "*pulumi up*" ||
|
|
41907
|
+
context.command like "*pulumi destroy*" ||
|
|
41908
|
+
context.command like "*aws ec2 run-instances*" ||
|
|
41909
|
+
context.command like "*aws rds create*" ||
|
|
41910
|
+
context.command like "*gcloud compute instances create*" ||
|
|
41911
|
+
context.command like "*az vm create*" ||
|
|
41912
|
+
context.command like "*kubectl delete*"
|
|
41913
|
+
)
|
|
41914
|
+
};
|
|
41915
|
+
${defaultPermit}`;
|
|
41916
|
+
var secretsSafe = `${header("secrets-safe", "Block secret exfiltration from files, env, shell, and common credential tools.")}forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41917
|
+
context has "input" && context.input has "path" && (
|
|
41918
|
+
context.input.path like "*/.env*" ||
|
|
41919
|
+
context.input.path like "*/.aws/credentials*" ||
|
|
41920
|
+
context.input.path like "*/.npmrc*" ||
|
|
41921
|
+
context.input.path like "*/.netrc*" ||
|
|
41922
|
+
context.input.path like "*/id_rsa*" ||
|
|
41923
|
+
context.input.path like "*secret*" ||
|
|
41924
|
+
context.input.path like "*token*"
|
|
41925
|
+
)
|
|
41926
|
+
};
|
|
41927
|
+
|
|
41928
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash") when {
|
|
41929
|
+
context has "command" && (
|
|
41930
|
+
context.command like "*printenv*" ||
|
|
41931
|
+
context.command like "*env |*" ||
|
|
41932
|
+
context.command like "*security find-generic-password*" ||
|
|
41933
|
+
context.command like "*aws secretsmanager get-secret-value*" ||
|
|
41934
|
+
context.command like "*gcloud secrets versions access*" ||
|
|
41935
|
+
context.command like "*op read*"
|
|
41936
|
+
)
|
|
41937
|
+
};
|
|
41938
|
+
${defaultPermit}`;
|
|
41939
|
+
var financeMandateSafe = `${header("finance-mandate-safe", "Block restricted-list and concentration-limit breaches in booking tools.")}forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"pms.book") when {
|
|
41940
|
+
context has "input" && context.input has "on_restricted_list" && context.input.on_restricted_list == true
|
|
41941
|
+
};
|
|
41942
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"booking.execute") when {
|
|
41943
|
+
context has "input" && context.input has "on_restricted_list" && context.input.on_restricted_list == true
|
|
41944
|
+
};
|
|
41945
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"booking.ticket") when {
|
|
41946
|
+
context has "input" && context.input has "on_restricted_list" && context.input.on_restricted_list == true
|
|
41947
|
+
};
|
|
41948
|
+
|
|
41949
|
+
// Default example caps: single-name > 10%, gross > 200%, net > 100%.
|
|
41950
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41951
|
+
context has "input" && context.input has "post_trade_weight_bps" && context.input.post_trade_weight_bps > 1000
|
|
41952
|
+
};
|
|
41953
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41954
|
+
context has "input" && context.input has "post_trade_gross_exposure_bps" && context.input.post_trade_gross_exposure_bps > 20000
|
|
41955
|
+
};
|
|
41956
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource) when {
|
|
41957
|
+
context has "input" && context.input has "post_trade_net_exposure_bps" && context.input.post_trade_net_exposure_bps > 10000
|
|
41958
|
+
};
|
|
41959
|
+
${defaultPermit}`;
|
|
41960
|
+
var POLICY_PACKS = [
|
|
41961
|
+
{
|
|
41962
|
+
id: "filesystem-safe",
|
|
41963
|
+
name: "Filesystem Safe",
|
|
41964
|
+
description: "Blocks destructive filesystem calls and secret-like path reads.",
|
|
41965
|
+
recommendedMode: "shadow-first",
|
|
41966
|
+
files: [{ path: "filesystem-safe.cedar", contents: filesystemSafe }]
|
|
41967
|
+
},
|
|
41968
|
+
{
|
|
41969
|
+
id: "git-safe",
|
|
41970
|
+
name: "Git Safe",
|
|
41971
|
+
description: "Blocks force pushes, hard resets, destructive cleanup, and repo deletion.",
|
|
41972
|
+
recommendedMode: "shadow-first",
|
|
41973
|
+
files: [{ path: "git-safe.cedar", contents: gitSafe }]
|
|
41974
|
+
},
|
|
41975
|
+
{
|
|
41976
|
+
id: "email-safe",
|
|
41977
|
+
name: "Email Safe",
|
|
41978
|
+
description: "Allows drafting workflows while blocking unattended sends.",
|
|
41979
|
+
recommendedMode: "shadow-first",
|
|
41980
|
+
files: [{ path: "email-safe.cedar", contents: emailSafe }]
|
|
41981
|
+
},
|
|
41982
|
+
{
|
|
41983
|
+
id: "database-safe",
|
|
41984
|
+
name: "Database Safe",
|
|
41985
|
+
description: "Allows read-oriented DB tools while blocking mutating/admin SQL.",
|
|
41986
|
+
recommendedMode: "shadow-first",
|
|
41987
|
+
files: [{ path: "database-safe.cedar", contents: databaseSafe }]
|
|
41988
|
+
},
|
|
41989
|
+
{
|
|
41990
|
+
id: "cloud-spend-safe",
|
|
41991
|
+
name: "Cloud Spend Safe",
|
|
41992
|
+
description: "Blocks obvious cloud spend creation and infrastructure destruction.",
|
|
41993
|
+
recommendedMode: "shadow-first",
|
|
41994
|
+
files: [{ path: "cloud-spend-safe.cedar", contents: cloudSpendSafe }]
|
|
41995
|
+
},
|
|
41996
|
+
{
|
|
41997
|
+
id: "secrets-safe",
|
|
41998
|
+
name: "Secrets Safe",
|
|
41999
|
+
description: "Blocks common file, env, shell, and cloud secret exfiltration paths.",
|
|
42000
|
+
recommendedMode: "enforce-ready",
|
|
42001
|
+
files: [{ path: "secrets-safe.cedar", contents: secretsSafe }]
|
|
42002
|
+
},
|
|
42003
|
+
{
|
|
42004
|
+
id: "finance-mandate-safe",
|
|
42005
|
+
name: "Finance Mandate Safe",
|
|
42006
|
+
description: "Blocks restricted-list and concentration breaches in booking flows.",
|
|
42007
|
+
recommendedMode: "shadow-first",
|
|
42008
|
+
files: [{ path: "finance-mandate-safe.cedar", contents: financeMandateSafe }]
|
|
42009
|
+
}
|
|
42010
|
+
];
|
|
42011
|
+
function getPolicyPack(id) {
|
|
42012
|
+
return POLICY_PACKS.find((pack) => pack.id === id);
|
|
42013
|
+
}
|
|
42014
|
+
function policyPackIds() {
|
|
42015
|
+
return POLICY_PACKS.map((pack) => pack.id);
|
|
42016
|
+
}
|
|
42017
|
+
|
|
42018
|
+
// src/connector-pilots.ts
|
|
42019
|
+
var import_node_fs10 = require("fs");
|
|
42020
|
+
var import_node_path6 = require("path");
|
|
42021
|
+
var defaultPermit2 = `
|
|
42022
|
+
// Default posture: observe all non-matching tools so the connector can be piloted in shadow mode.
|
|
42023
|
+
permit(principal, action == Action::"MCP::Tool::call", resource);
|
|
42024
|
+
`;
|
|
42025
|
+
var nautilusBridgePy = String.raw`#!/usr/bin/env python3
|
|
42026
|
+
"""
|
|
42027
|
+
ScopeBlind external bridge for NautilusTrader-compatible pilots.
|
|
42028
|
+
|
|
42029
|
+
This file is intentionally outside NautilusTrader. It gives protect-mcp a stable
|
|
42030
|
+
JSONL command boundary for staging, approval-gated submission, cancellation, and
|
|
42031
|
+
event export while keeping the trading engine customer-owned.
|
|
42032
|
+
|
|
42033
|
+
Mock mode runs without NautilusTrader installed. Real mode is enabled by setting
|
|
42034
|
+
NAUTILUS_BRIDGE_MODULE to "module.path:ClassName"; the class may implement:
|
|
42035
|
+
submit_order(order), modify_order(order), cancel_order(order), reconcile(order),
|
|
42036
|
+
export_events(since=None)
|
|
42037
|
+
"""
|
|
42038
|
+
|
|
42039
|
+
from __future__ import annotations
|
|
42040
|
+
|
|
42041
|
+
import hashlib
|
|
42042
|
+
import importlib
|
|
42043
|
+
import json
|
|
42044
|
+
import os
|
|
42045
|
+
import sys
|
|
42046
|
+
import time
|
|
42047
|
+
from dataclasses import dataclass, field
|
|
42048
|
+
from pathlib import Path
|
|
42049
|
+
from typing import Any, Callable
|
|
42050
|
+
|
|
42051
|
+
|
|
42052
|
+
def canonical_json(value: Any) -> str:
|
|
42053
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
42054
|
+
|
|
42055
|
+
|
|
42056
|
+
def sha256_json(value: Any) -> str:
|
|
42057
|
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
42058
|
+
|
|
42059
|
+
|
|
42060
|
+
def now_ms() -> int:
|
|
42061
|
+
return int(time.time() * 1000)
|
|
42062
|
+
|
|
42063
|
+
|
|
42064
|
+
@dataclass
|
|
42065
|
+
class BridgeState:
|
|
42066
|
+
root: Path = field(default_factory=lambda: Path(os.environ.get("SCOPEBLIND_NAUTILUS_STATE_DIR", ".protect-mcp/nautilus")))
|
|
42067
|
+
|
|
42068
|
+
def __post_init__(self) -> None:
|
|
42069
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
42070
|
+
self.orders_path.touch(exist_ok=True)
|
|
42071
|
+
self.events_path.touch(exist_ok=True)
|
|
42072
|
+
|
|
42073
|
+
@property
|
|
42074
|
+
def orders_path(self) -> Path:
|
|
42075
|
+
return self.root / "orders.jsonl"
|
|
42076
|
+
|
|
42077
|
+
@property
|
|
42078
|
+
def events_path(self) -> Path:
|
|
42079
|
+
return self.root / "events.jsonl"
|
|
42080
|
+
|
|
42081
|
+
def append_order(self, order: dict[str, Any]) -> None:
|
|
42082
|
+
with self.orders_path.open("a", encoding="utf-8") as handle:
|
|
42083
|
+
handle.write(canonical_json(order) + "\n")
|
|
42084
|
+
|
|
42085
|
+
def append_event(self, event: dict[str, Any]) -> dict[str, Any]:
|
|
42086
|
+
enriched = {
|
|
42087
|
+
"event_id": event.get("event_id") or f"nt-{now_ms()}-{len(event)}",
|
|
42088
|
+
"observed_at_ms": now_ms(),
|
|
42089
|
+
**event,
|
|
42090
|
+
}
|
|
42091
|
+
enriched["event_digest"] = sha256_json(enriched)
|
|
42092
|
+
with self.events_path.open("a", encoding="utf-8") as handle:
|
|
42093
|
+
handle.write(canonical_json(enriched) + "\n")
|
|
42094
|
+
return enriched
|
|
42095
|
+
|
|
42096
|
+
def events(self) -> list[dict[str, Any]]:
|
|
42097
|
+
rows: list[dict[str, Any]] = []
|
|
42098
|
+
with self.events_path.open("r", encoding="utf-8") as handle:
|
|
42099
|
+
for line in handle:
|
|
42100
|
+
if line.strip():
|
|
42101
|
+
rows.append(json.loads(line))
|
|
42102
|
+
return rows
|
|
42103
|
+
|
|
42104
|
+
|
|
42105
|
+
class ScopeBlindNautilusBridge:
|
|
42106
|
+
def __init__(self) -> None:
|
|
42107
|
+
self.state = BridgeState()
|
|
42108
|
+
self.real = self._load_real_bridge()
|
|
42109
|
+
|
|
42110
|
+
def _load_real_bridge(self) -> Any | None:
|
|
42111
|
+
target = os.environ.get("NAUTILUS_BRIDGE_MODULE")
|
|
42112
|
+
if not target:
|
|
42113
|
+
return None
|
|
42114
|
+
module_name, _, class_name = target.partition(":")
|
|
42115
|
+
if not module_name or not class_name:
|
|
42116
|
+
raise ValueError("NAUTILUS_BRIDGE_MODULE must be module.path:ClassName")
|
|
42117
|
+
module = importlib.import_module(module_name)
|
|
42118
|
+
return getattr(module, class_name)()
|
|
42119
|
+
|
|
42120
|
+
def handle(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42121
|
+
action = command.get("action")
|
|
42122
|
+
handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
42123
|
+
"stage_order": self.stage_order,
|
|
42124
|
+
"submit_order": self.submit_order,
|
|
42125
|
+
"modify_order": self.modify_order,
|
|
42126
|
+
"cancel_order": self.cancel_order,
|
|
42127
|
+
"reconcile": self.reconcile,
|
|
42128
|
+
"export_events": self.export_events,
|
|
42129
|
+
}
|
|
42130
|
+
if action not in handlers:
|
|
42131
|
+
return self.error(command, "unknown_action", f"Unsupported action: {action}")
|
|
42132
|
+
try:
|
|
42133
|
+
return handlers[action](command)
|
|
42134
|
+
except Exception as exc:
|
|
42135
|
+
return self.error(command, "bridge_error", str(exc))
|
|
42136
|
+
|
|
42137
|
+
def require(self, command: dict[str, Any], *fields: str) -> None:
|
|
42138
|
+
missing = [field for field in fields if command.get(field) in (None, "")]
|
|
42139
|
+
if missing:
|
|
42140
|
+
raise ValueError(f"missing required field(s): {', '.join(missing)}")
|
|
42141
|
+
|
|
42142
|
+
def require_approved(self, command: dict[str, Any]) -> None:
|
|
42143
|
+
self.require(command, "approval_receipt")
|
|
42144
|
+
if command.get("mandate_passed") is not True:
|
|
42145
|
+
raise ValueError("mandate_passed must be true before live order mutation")
|
|
42146
|
+
|
|
42147
|
+
def stage_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42148
|
+
self.require(command, "client_order_id", "instrument_id", "side", "quantity")
|
|
42149
|
+
order = self.order_projection(command, status="staged")
|
|
42150
|
+
self.state.append_order(order)
|
|
42151
|
+
event = self.state.append_event({
|
|
42152
|
+
"type": "scopeblind.nautilus.order_staged.v1",
|
|
42153
|
+
"client_order_id": order["client_order_id"],
|
|
42154
|
+
"order_digest": sha256_json(order),
|
|
42155
|
+
"disclosure": "position_blind",
|
|
42156
|
+
})
|
|
42157
|
+
return self.ok(command, {"status": "staged", "order": order, "event": event})
|
|
42158
|
+
|
|
42159
|
+
def submit_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42160
|
+
self.require_approved(command)
|
|
42161
|
+
order = self.order_projection(command, status="submitted")
|
|
42162
|
+
if self.real and hasattr(self.real, "submit_order"):
|
|
42163
|
+
external = self.real.submit_order(order)
|
|
42164
|
+
else:
|
|
42165
|
+
external = {"mode": "mock", "external_order_id": f"MOCK-{order['client_order_id']}"}
|
|
42166
|
+
event = self.state.append_event({
|
|
42167
|
+
"type": "scopeblind.nautilus.order_submitted.v1",
|
|
42168
|
+
"client_order_id": order["client_order_id"],
|
|
42169
|
+
"order_digest": sha256_json(order),
|
|
42170
|
+
"external_digest": sha256_json(external),
|
|
42171
|
+
"disclosure": "position_blind",
|
|
42172
|
+
})
|
|
42173
|
+
return self.ok(command, {"status": "submitted", "order": order, "external": external, "event": event})
|
|
42174
|
+
|
|
42175
|
+
def modify_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42176
|
+
self.require_approved(command)
|
|
42177
|
+
self.require(command, "client_order_id")
|
|
42178
|
+
if self.real and hasattr(self.real, "modify_order"):
|
|
42179
|
+
external = self.real.modify_order(command)
|
|
42180
|
+
else:
|
|
42181
|
+
external = {"mode": "mock", "modified": command["client_order_id"]}
|
|
42182
|
+
event = self.state.append_event({
|
|
42183
|
+
"type": "scopeblind.nautilus.order_modified.v1",
|
|
42184
|
+
"client_order_id": command["client_order_id"],
|
|
42185
|
+
"command_digest": sha256_json(command),
|
|
42186
|
+
"external_digest": sha256_json(external),
|
|
42187
|
+
"disclosure": "position_blind",
|
|
42188
|
+
})
|
|
42189
|
+
return self.ok(command, {"status": "modified", "external": external, "event": event})
|
|
42190
|
+
|
|
42191
|
+
def cancel_order(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42192
|
+
self.require_approved(command)
|
|
42193
|
+
self.require(command, "client_order_id")
|
|
42194
|
+
if self.real and hasattr(self.real, "cancel_order"):
|
|
42195
|
+
external = self.real.cancel_order(command)
|
|
42196
|
+
else:
|
|
42197
|
+
external = {"mode": "mock", "cancelled": command["client_order_id"]}
|
|
42198
|
+
event = self.state.append_event({
|
|
42199
|
+
"type": "scopeblind.nautilus.order_cancelled.v1",
|
|
42200
|
+
"client_order_id": command["client_order_id"],
|
|
42201
|
+
"command_digest": sha256_json(command),
|
|
42202
|
+
"external_digest": sha256_json(external),
|
|
42203
|
+
"disclosure": "position_blind",
|
|
42204
|
+
})
|
|
42205
|
+
return self.ok(command, {"status": "cancelled", "external": external, "event": event})
|
|
42206
|
+
|
|
42207
|
+
def reconcile(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42208
|
+
self.require(command, "client_order_id")
|
|
42209
|
+
if self.real and hasattr(self.real, "reconcile"):
|
|
42210
|
+
external = self.real.reconcile(command)
|
|
42211
|
+
else:
|
|
42212
|
+
external = {"mode": "mock", "client_order_id": command["client_order_id"], "state": "accepted"}
|
|
42213
|
+
event = self.state.append_event({
|
|
42214
|
+
"type": "scopeblind.nautilus.reconciled.v1",
|
|
42215
|
+
"client_order_id": command["client_order_id"],
|
|
42216
|
+
"external_digest": sha256_json(external),
|
|
42217
|
+
"disclosure": "position_blind",
|
|
42218
|
+
})
|
|
42219
|
+
return self.ok(command, {"status": "reconciled", "external": external, "event": event})
|
|
42220
|
+
|
|
42221
|
+
def export_events(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
42222
|
+
if self.real and hasattr(self.real, "export_events"):
|
|
42223
|
+
external_events = self.real.export_events(command.get("since"))
|
|
42224
|
+
else:
|
|
42225
|
+
external_events = self.state.events()
|
|
42226
|
+
return self.ok(command, {
|
|
42227
|
+
"status": "exported",
|
|
42228
|
+
"event_count": len(external_events),
|
|
42229
|
+
"commitment_root": sha256_json(external_events),
|
|
42230
|
+
"events": external_events,
|
|
42231
|
+
})
|
|
42232
|
+
|
|
42233
|
+
def order_projection(self, command: dict[str, Any], status: str) -> dict[str, Any]:
|
|
42234
|
+
return {
|
|
42235
|
+
"client_order_id": command["client_order_id"],
|
|
42236
|
+
"instrument_id": command["instrument_id"],
|
|
42237
|
+
"side": command["side"],
|
|
42238
|
+
"quantity": command["quantity"],
|
|
42239
|
+
"price": command.get("price"),
|
|
42240
|
+
"time_in_force": command.get("time_in_force", "GTC"),
|
|
42241
|
+
"strategy_id": command.get("strategy_id"),
|
|
42242
|
+
"mandate_digest": command.get("mandate_digest"),
|
|
42243
|
+
"approval_receipt": command.get("approval_receipt"),
|
|
42244
|
+
"status": status,
|
|
42245
|
+
"created_at_ms": now_ms(),
|
|
42246
|
+
}
|
|
42247
|
+
|
|
42248
|
+
def ok(self, command: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
|
|
42249
|
+
return {
|
|
42250
|
+
"ok": True,
|
|
42251
|
+
"bridge": "scopeblind.nautilus.external.v1",
|
|
42252
|
+
"mode": "real" if self.real else "mock",
|
|
42253
|
+
"request_digest": sha256_json(command),
|
|
42254
|
+
**result,
|
|
42255
|
+
}
|
|
42256
|
+
|
|
42257
|
+
def error(self, command: dict[str, Any], code: str, message: str) -> dict[str, Any]:
|
|
42258
|
+
return {
|
|
42259
|
+
"ok": False,
|
|
42260
|
+
"bridge": "scopeblind.nautilus.external.v1",
|
|
42261
|
+
"mode": "real" if self.real else "mock",
|
|
42262
|
+
"error": {"code": code, "message": message},
|
|
42263
|
+
"request_digest": sha256_json(command),
|
|
42264
|
+
}
|
|
42265
|
+
|
|
42266
|
+
|
|
42267
|
+
def main() -> int:
|
|
42268
|
+
bridge = ScopeBlindNautilusBridge()
|
|
42269
|
+
for line in sys.stdin:
|
|
42270
|
+
if not line.strip():
|
|
42271
|
+
continue
|
|
42272
|
+
command = json.loads(line)
|
|
42273
|
+
print(canonical_json(bridge.handle(command)), flush=True)
|
|
42274
|
+
return 0
|
|
42275
|
+
|
|
42276
|
+
|
|
42277
|
+
if __name__ == "__main__":
|
|
42278
|
+
raise SystemExit(main())
|
|
42279
|
+
`;
|
|
42280
|
+
var nautilusAdapterReadme = `# NautilusTrader-compatible external bridge
|
|
42281
|
+
|
|
42282
|
+
This connector is intentionally external to NautilusTrader. It lets protect-mcp
|
|
42283
|
+
control and receipt high-risk order actions while a customer-owned Nautilus
|
|
42284
|
+
process remains the trading engine.
|
|
42285
|
+
|
|
42286
|
+
## Local mock run
|
|
42287
|
+
|
|
42288
|
+
\`\`\`bash
|
|
42289
|
+
python3 .protect-mcp/connectors/nautilus-trader/bridge.py <<'JSONL'
|
|
42290
|
+
{"action":"stage_order","client_order_id":"SB-1","instrument_id":"AAPL.NASDAQ","side":"BUY","quantity":"50","price":"182.40","mandate_digest":"demo"}
|
|
42291
|
+
{"action":"submit_order","client_order_id":"SB-1","instrument_id":"AAPL.NASDAQ","side":"BUY","quantity":"50","price":"182.40","mandate_digest":"demo","mandate_passed":true,"approval_receipt":"receipt-demo"}
|
|
42292
|
+
{"action":"export_events"}
|
|
42293
|
+
JSONL
|
|
42294
|
+
\`\`\`
|
|
42295
|
+
|
|
42296
|
+
## Real mode
|
|
42297
|
+
|
|
42298
|
+
Set \`NAUTILUS_BRIDGE_MODULE=customer_module:BridgeClass\`. The class can
|
|
42299
|
+
implement \`submit_order\`, \`modify_order\`, \`cancel_order\`, \`reconcile\`,
|
|
42300
|
+
and \`export_events\`. Keep that glue in the customer's repository so Nautilus
|
|
42301
|
+
licensing, credentials, and trading logic stay outside ScopeBlind.
|
|
42302
|
+
|
|
42303
|
+
## Upstream contribution posture
|
|
42304
|
+
|
|
42305
|
+
The best NautilusTrader contribution is not this bridge or a UI. It is a small,
|
|
42306
|
+
vendor-neutral audit/event sink RFC: a documented way to export normalized order
|
|
42307
|
+
commands, execution reports, fills, cancels, and reconciliation events so
|
|
42308
|
+
external compliance wrappers can prove what happened without mutating the
|
|
42309
|
+
engine.
|
|
42310
|
+
`;
|
|
42311
|
+
var CONNECTOR_PILOTS = [
|
|
42312
|
+
{
|
|
42313
|
+
id: "github",
|
|
42314
|
+
category: "code",
|
|
42315
|
+
name: "GitHub pull-request control",
|
|
42316
|
+
status: "usable-pilot",
|
|
42317
|
+
description: "Controls GitHub REST/MCP calls for issue, PR, branch, and workflow actions.",
|
|
42318
|
+
value: "Useful when agents already have repo access through GitHub MCP, gh, or a GitHub-backed tool server.",
|
|
42319
|
+
env: [
|
|
42320
|
+
{ name: "GITHUB_TOKEN", required: true, description: "Fine-grained token scoped to the pilot repo." },
|
|
42321
|
+
{ name: "GITHUB_REPOSITORY", required: true, description: "owner/repo target for the pilot." }
|
|
42322
|
+
],
|
|
42323
|
+
tools: ["github.rest.request", "github.issue.create", "github.pull_request.merge", "github.workflow.dispatch"],
|
|
42324
|
+
actions: [
|
|
42325
|
+
{ name: "Read repo metadata", tool: "github.rest.request", risk: "low", mode: "observe", description: "GET-only repository and PR inspection." },
|
|
42326
|
+
{ name: "Create issue or comment", tool: "github.issue.create", risk: "medium", mode: "require_approval", description: "External write to the system of record." },
|
|
42327
|
+
{ name: "Merge PR / dispatch workflow", tool: "github.pull_request.merge", risk: "high", mode: "require_approval", description: "Code-changing or CI-triggering action." }
|
|
42328
|
+
],
|
|
42329
|
+
setup: [
|
|
42330
|
+
"Create a fine-grained GitHub token for one repository.",
|
|
42331
|
+
"Set GITHUB_TOKEN and GITHUB_REPOSITORY.",
|
|
42332
|
+
"Run the agent through protect-mcp and review GitHub tool calls in the dashboard."
|
|
42333
|
+
],
|
|
42334
|
+
config: {
|
|
42335
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42336
|
+
provider: "github",
|
|
42337
|
+
target_env: ["GITHUB_TOKEN", "GITHUB_REPOSITORY"],
|
|
42338
|
+
safe_read_probe: "GET /repos/{GITHUB_REPOSITORY}",
|
|
42339
|
+
controlled_tools: ["github.rest.request", "github.issue.create", "github.pull_request.merge", "github.workflow.dispatch"],
|
|
42340
|
+
approval_required_for: ["POST", "PATCH", "PUT", "DELETE", "merge", "workflow_dispatch"],
|
|
42341
|
+
receipt_fields: ["method", "path", "repo", "actor", "payload_hash", "approval_reason"]
|
|
42342
|
+
},
|
|
42343
|
+
cedar: `${defaultPermit2}
|
|
42344
|
+
// GitHub pilot: reads are observed; writes and merges need exact-action approval.
|
|
42345
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42346
|
+
when { context.tool == "github.pull_request.merge" };
|
|
42347
|
+
|
|
42348
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42349
|
+
when { context.tool == "github.workflow.dispatch" && !context.approved };
|
|
42350
|
+
|
|
42351
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42352
|
+
when { context.tool == "github.issue.create" && !context.approved };
|
|
42353
|
+
`
|
|
42354
|
+
},
|
|
42355
|
+
{
|
|
42356
|
+
id: "email-gmail",
|
|
42357
|
+
category: "communications",
|
|
42358
|
+
name: "Gmail self-send / draft approval",
|
|
42359
|
+
status: "usable-pilot",
|
|
42360
|
+
description: "Uses the existing Gmail OAuth connector path and restricts send mode to email.self for the first production pilot.",
|
|
42361
|
+
value: "Makes external communications reviewable before an agent can send mail.",
|
|
42362
|
+
env: [
|
|
42363
|
+
{ name: "GOOGLE_CLIENT_ID", required: true, description: "OAuth client for Gmail." },
|
|
42364
|
+
{ name: "GOOGLE_CLIENT_SECRET", required: true, description: "OAuth client secret." },
|
|
42365
|
+
{ name: "CONNECTOR_TOKEN_KEY", required: true, description: "AES-GCM key material for sealed connector tokens." }
|
|
42366
|
+
],
|
|
42367
|
+
tools: ["gmail.draft.create", "gmail.send.email_self", "email.send"],
|
|
42368
|
+
actions: [
|
|
42369
|
+
{ name: "Create draft", tool: "gmail.draft.create", risk: "medium", mode: "require_approval", description: "Draft content can leak sensitive information." },
|
|
42370
|
+
{ name: "Self-send test", tool: "gmail.send.email_self", risk: "medium", mode: "require_approval", description: "First release allows only sending to the account owner." },
|
|
42371
|
+
{ name: "External send", tool: "email.send", risk: "high", mode: "deny", description: "Direct external send stays blocked until a customer-specific allowlist exists." }
|
|
42372
|
+
],
|
|
42373
|
+
setup: [
|
|
42374
|
+
"Configure Google OAuth redirect /fn/connectors/gmail/callback.",
|
|
42375
|
+
"Connect Gmail through the hosted console or local connector flow.",
|
|
42376
|
+
"Keep send mode to email.self until the customer approves recipient allowlists."
|
|
42377
|
+
],
|
|
42378
|
+
config: {
|
|
42379
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42380
|
+
provider: "gmail",
|
|
42381
|
+
hosted_functions: ["/fn/connectors/gmail/start", "/fn/connectors/gmail/callback", "/fn/connectors/gmail/send", "/fn/connectors/gmail/status"],
|
|
42382
|
+
first_release_scope: "email.self",
|
|
42383
|
+
denied_until_configured: ["email.send.external", "email.bulk_send"],
|
|
42384
|
+
receipt_fields: ["to_hash", "subject_hash", "body_hash", "approval_reason", "gmail_message_id"]
|
|
42385
|
+
},
|
|
42386
|
+
cedar: `${defaultPermit2}
|
|
42387
|
+
// Email pilot: no direct external send. Draft/self-send require exact approval.
|
|
42388
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42389
|
+
when { context.tool == "email.send" };
|
|
42390
|
+
|
|
42391
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42392
|
+
when { context.tool == "gmail.draft.create" && !context.approved };
|
|
42393
|
+
|
|
42394
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42395
|
+
when { context.tool == "gmail.send.email_self" && !context.approved };
|
|
42396
|
+
`
|
|
42397
|
+
},
|
|
42398
|
+
{
|
|
42399
|
+
id: "filesystem-git",
|
|
42400
|
+
category: "local-computer",
|
|
42401
|
+
name: "Filesystem and Git control",
|
|
42402
|
+
status: "usable-pilot",
|
|
42403
|
+
description: "Controls reads, writes, shell commands, and Git mutation in the local project.",
|
|
42404
|
+
value: "Immediately useful with Claude Code, Codex, Cursor, and any agent that edits files or runs shell commands.",
|
|
42405
|
+
env: [],
|
|
42406
|
+
tools: ["Read", "Write", "Edit", "MultiEdit", "Bash", "git.commit", "git.push"],
|
|
42407
|
+
actions: [
|
|
42408
|
+
{ name: "Read files", tool: "Read", risk: "low", mode: "observe", description: "Observe file reads for audit context." },
|
|
42409
|
+
{ name: "Write/edit files", tool: "Write", risk: "medium", mode: "require_approval", description: "Require approval for sensitive paths or broad rewrites." },
|
|
42410
|
+
{ name: "Git push/reset", tool: "Bash", risk: "high", mode: "require_approval", description: "Commands that publish, reset, or delete require exact-action approval." }
|
|
42411
|
+
],
|
|
42412
|
+
setup: [
|
|
42413
|
+
"Run protect-mcp init-hooks in the project.",
|
|
42414
|
+
"Install filesystem-safe and Git-safe policy packs.",
|
|
42415
|
+
"Review the dashboard before turning on enforce mode."
|
|
42416
|
+
],
|
|
42417
|
+
config: {
|
|
42418
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42419
|
+
provider: "filesystem-git",
|
|
42420
|
+
local_only: true,
|
|
42421
|
+
protected_paths: [".env", ".ssh", "keys/", "secrets/", "node_modules/"],
|
|
42422
|
+
dangerous_command_patterns: ["rm -rf", "git push", "git reset --hard", "curl | sh", "chmod 777"],
|
|
42423
|
+
receipt_fields: ["tool", "path_hash", "command_hash", "diff_hash", "approval_reason"]
|
|
42424
|
+
},
|
|
42425
|
+
cedar: `${defaultPermit2}
|
|
42426
|
+
// Filesystem/Git pilot: dangerous shell and protected-path writes need approval.
|
|
42427
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42428
|
+
when { context.tool == "Bash" && context.command_pattern.contains("git reset --hard") && !context.approved };
|
|
42429
|
+
|
|
42430
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42431
|
+
when { context.tool == "Bash" && context.command_pattern.contains("git push") && !context.approved };
|
|
42432
|
+
|
|
42433
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42434
|
+
when { ["Write", "Edit", "MultiEdit"].contains(context.tool) && context.path.contains(".env") && !context.approved };
|
|
42435
|
+
`
|
|
42436
|
+
},
|
|
42437
|
+
{
|
|
42438
|
+
id: "slack-teams",
|
|
42439
|
+
category: "communications",
|
|
42440
|
+
name: "Slack or Teams outbound approval",
|
|
42441
|
+
status: "usable-pilot",
|
|
42442
|
+
description: "Controls messages to Slack channels or Microsoft Teams webhooks.",
|
|
42443
|
+
value: "Makes high-impact internal broadcasts and client channels approval-gated.",
|
|
42444
|
+
env: [
|
|
42445
|
+
{ name: "SLACK_BOT_TOKEN", required: false, description: "Slack bot token for chat.postMessage pilots." },
|
|
42446
|
+
{ name: "SLACK_CHANNEL_ID", required: false, description: "Default Slack channel for the pilot." },
|
|
42447
|
+
{ name: "TEAMS_WEBHOOK_URL", required: false, description: "Teams incoming webhook URL if Teams is preferred." }
|
|
42448
|
+
],
|
|
42449
|
+
tools: ["slack.chat.postMessage", "slack.files.upload", "teams.webhook.post"],
|
|
42450
|
+
actions: [
|
|
42451
|
+
{ name: "Post internal message", tool: "slack.chat.postMessage", risk: "medium", mode: "require_approval", description: "Message text and channel are read back before send." },
|
|
42452
|
+
{ name: "Upload file", tool: "slack.files.upload", risk: "high", mode: "require_approval", description: "Files can leak customer data and need explicit approval." },
|
|
42453
|
+
{ name: "Teams webhook post", tool: "teams.webhook.post", risk: "medium", mode: "require_approval", description: "Webhook destination and payload hash are receipted." }
|
|
42454
|
+
],
|
|
42455
|
+
setup: [
|
|
42456
|
+
"Choose Slack or Teams for the first pilot, not both.",
|
|
42457
|
+
"Set the relevant token/webhook environment variables.",
|
|
42458
|
+
"Start with a private test channel and exact-action approval for every send."
|
|
42459
|
+
],
|
|
42460
|
+
config: {
|
|
42461
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42462
|
+
provider: "slack-or-teams",
|
|
42463
|
+
supported_modes: ["slack.chat.postMessage", "teams.webhook.post"],
|
|
42464
|
+
require_channel_allowlist: true,
|
|
42465
|
+
receipt_fields: ["channel_hash", "message_hash", "file_hash", "approval_reason", "provider_message_id"]
|
|
42466
|
+
},
|
|
42467
|
+
cedar: `${defaultPermit2}
|
|
42468
|
+
// Slack/Teams pilot: all outbound posts and uploads require approval by default.
|
|
42469
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42470
|
+
when { ["slack.chat.postMessage", "slack.files.upload", "teams.webhook.post"].contains(context.tool) && !context.approved };
|
|
42471
|
+
`
|
|
42472
|
+
},
|
|
42473
|
+
{
|
|
42474
|
+
id: "finance-pms",
|
|
42475
|
+
category: "finance",
|
|
42476
|
+
name: "Finance PMS mock-to-real adapter",
|
|
42477
|
+
status: "usable-pilot",
|
|
42478
|
+
description: "Stages orders into a PMS adapter contract, with mock mode locally and real mode through PMS_ADAPTER_URL.",
|
|
42479
|
+
value: "Gives hedge funds the controlled booking path: parse, mandate-check, approve, book, corroborate, receipt.",
|
|
42480
|
+
env: [
|
|
42481
|
+
{ name: "PMS_ADAPTER_URL", required: false, description: "Customer-owned adapter endpoint. Omit for local mock mode." },
|
|
42482
|
+
{ name: "PMS_ADAPTER_TOKEN", required: false, description: "Bearer token for the customer-owned PMS adapter." }
|
|
42483
|
+
],
|
|
42484
|
+
tools: ["pms.order.stage", "pms.order.book", "pms.order.cancel", "pms.reconcile"],
|
|
42485
|
+
actions: [
|
|
42486
|
+
{ name: "Stage order", tool: "pms.order.stage", risk: "medium", mode: "require_approval", description: "Creates a booking ticket but does not execute." },
|
|
42487
|
+
{ name: "Book order", tool: "pms.order.book", risk: "high", mode: "require_approval", description: "Must pass mandate checks and human readback." },
|
|
42488
|
+
{ name: "Cancel/order correction", tool: "pms.order.cancel", risk: "high", mode: "require_approval", description: "Mutates book state and requires approval." }
|
|
42489
|
+
],
|
|
42490
|
+
setup: [
|
|
42491
|
+
"Run local mock mode first with the Legate finance pilot pack.",
|
|
42492
|
+
"Point PMS_ADAPTER_URL at a customer-owned bridge when ready.",
|
|
42493
|
+
"Require mandate checks and exact-action approval before pms.order.book."
|
|
42494
|
+
],
|
|
42495
|
+
config: {
|
|
42496
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42497
|
+
provider: "finance-pms",
|
|
42498
|
+
mode: "mock-first",
|
|
42499
|
+
adapter_contract: {
|
|
42500
|
+
stage: "POST /orders/stage",
|
|
42501
|
+
book: "POST /orders/book",
|
|
42502
|
+
cancel: "POST /orders/{client_order_id}/cancel",
|
|
42503
|
+
reconcile: "GET /orders/{client_order_id}"
|
|
42504
|
+
},
|
|
42505
|
+
receipt_fields: ["client_order_id", "side", "symbol_hash", "qty", "price", "mandate_digest", "approval_reason", "external_confirmation_hash"]
|
|
42506
|
+
},
|
|
42507
|
+
cedar: `${defaultPermit2}
|
|
42508
|
+
// Finance/PMS pilot: booking actions require mandate pass and exact approval.
|
|
42509
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42510
|
+
when { ["pms.order.stage", "pms.order.book", "pms.order.cancel"].contains(context.tool) && !context.approved };
|
|
42511
|
+
|
|
42512
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42513
|
+
when { context.tool == "pms.order.book" && context.mandate_passed != true };
|
|
42514
|
+
`
|
|
42515
|
+
},
|
|
42516
|
+
{
|
|
42517
|
+
id: "nautilus-trader",
|
|
42518
|
+
category: "finance",
|
|
42519
|
+
name: "NautilusTrader-compatible external bridge",
|
|
42520
|
+
status: "usable-pilot",
|
|
42521
|
+
description: "Controls NautilusTrader-compatible staged orders through an external JSONL bridge, with local mock mode and customer-owned real mode.",
|
|
42522
|
+
value: "Turns Nautilus into a strong Legate demo target: mandate-check, exact approval, external order event, position-blind audit bundle, and later reconciliation.",
|
|
42523
|
+
env: [
|
|
42524
|
+
{ name: "NAUTILUS_BRIDGE_MODULE", required: false, description: "Optional customer glue in module.path:ClassName form for real Nautilus submission." },
|
|
42525
|
+
{ name: "SCOPEBLIND_NAUTILUS_STATE_DIR", required: false, description: "Optional state directory for local mock events. Defaults to .protect-mcp/nautilus." },
|
|
42526
|
+
{ name: "NAUTILUS_TRADER_PROJECT", required: false, description: "Optional path to the customer Nautilus project when running real mode." }
|
|
42527
|
+
],
|
|
42528
|
+
tools: [
|
|
42529
|
+
"nautilus.order.stage",
|
|
42530
|
+
"nautilus.order.submit",
|
|
42531
|
+
"nautilus.order.modify",
|
|
42532
|
+
"nautilus.order.cancel",
|
|
42533
|
+
"nautilus.strategy.deploy",
|
|
42534
|
+
"nautilus.event.export",
|
|
42535
|
+
"nautilus.reconcile"
|
|
42536
|
+
],
|
|
42537
|
+
actions: [
|
|
42538
|
+
{ name: "Stage order", tool: "nautilus.order.stage", risk: "medium", mode: "require_approval", description: "Creates a position-blind booking intent and event commitment." },
|
|
42539
|
+
{ name: "Submit order", tool: "nautilus.order.submit", risk: "high", mode: "require_approval", description: "Requires mandate pass plus exact approval before live order mutation." },
|
|
42540
|
+
{ name: "Modify or cancel order", tool: "nautilus.order.modify", risk: "high", mode: "require_approval", description: "Mutates live order state and must carry a fresh approval receipt." },
|
|
42541
|
+
{ name: "Deploy strategy", tool: "nautilus.strategy.deploy", risk: "high", mode: "require_approval", description: "Requires signed strategy pack, mandate scope, and operator approval." },
|
|
42542
|
+
{ name: "Export event log", tool: "nautilus.event.export", risk: "low", mode: "observe", description: "Exports normalized event commitments for receipt corroboration." }
|
|
42543
|
+
],
|
|
42544
|
+
setup: [
|
|
42545
|
+
"Run mock mode first: protect-mcp connectors init nautilus-trader --force.",
|
|
42546
|
+
"Pipe stage/submit/reconcile JSONL through .protect-mcp/connectors/nautilus-trader/bridge.py.",
|
|
42547
|
+
"For real mode, set NAUTILUS_BRIDGE_MODULE to customer-owned glue that calls NautilusTrader APIs.",
|
|
42548
|
+
"Open an upstream NautilusTrader RFC for a neutral audit/event sink before proposing any PR."
|
|
42549
|
+
],
|
|
42550
|
+
config: {
|
|
42551
|
+
type: "scopeblind.connector_pilot.v1",
|
|
42552
|
+
provider: "nautilus-trader-compatible",
|
|
42553
|
+
mode: "external-bridge-mock-first",
|
|
42554
|
+
license_boundary: "No NautilusTrader code is bundled. Real mode calls a customer-owned process/module.",
|
|
42555
|
+
adapter_contract: {
|
|
42556
|
+
protocol: "stdin/stdout JSONL",
|
|
42557
|
+
bridge: ".protect-mcp/connectors/nautilus-trader/bridge.py",
|
|
42558
|
+
real_mode_env: "NAUTILUS_BRIDGE_MODULE=module.path:ClassName",
|
|
42559
|
+
actions: ["stage_order", "submit_order", "modify_order", "cancel_order", "reconcile", "export_events"]
|
|
42560
|
+
},
|
|
42561
|
+
controlled_tools: [
|
|
42562
|
+
"nautilus.order.stage",
|
|
42563
|
+
"nautilus.order.submit",
|
|
42564
|
+
"nautilus.order.modify",
|
|
42565
|
+
"nautilus.order.cancel",
|
|
42566
|
+
"nautilus.strategy.deploy",
|
|
42567
|
+
"nautilus.event.export",
|
|
42568
|
+
"nautilus.reconcile"
|
|
42569
|
+
],
|
|
42570
|
+
approval_required_for: ["submit_order", "modify_order", "cancel_order", "strategy_deploy"],
|
|
42571
|
+
receipt_fields: [
|
|
42572
|
+
"client_order_id",
|
|
42573
|
+
"instrument_id_hash",
|
|
42574
|
+
"side",
|
|
42575
|
+
"quantity",
|
|
42576
|
+
"price",
|
|
42577
|
+
"mandate_digest",
|
|
42578
|
+
"approval_receipt",
|
|
42579
|
+
"external_event_digest",
|
|
42580
|
+
"commitment_root"
|
|
42581
|
+
],
|
|
42582
|
+
upstream_rfc: {
|
|
42583
|
+
title: "[RFC] Add a vendor-neutral order/execution audit event sink",
|
|
42584
|
+
non_goals: ["ScopeBlind dependency", "UI dashboard", "AI tooling", "new venue adapter"]
|
|
42585
|
+
}
|
|
42586
|
+
},
|
|
42587
|
+
artifacts: [
|
|
42588
|
+
{ path: "nautilus-trader/bridge.py", contents: nautilusBridgePy, executable: true },
|
|
42589
|
+
{ path: "nautilus-trader/README.md", contents: nautilusAdapterReadme }
|
|
42590
|
+
],
|
|
42591
|
+
cedar: `${defaultPermit2}
|
|
42592
|
+
// NautilusTrader-compatible pilot: stage can be observed, but any live mutation requires exact approval.
|
|
42593
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42594
|
+
when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel", "nautilus.strategy.deploy"].contains(context.tool) && !context.approved };
|
|
42595
|
+
|
|
42596
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42597
|
+
when { ["nautilus.order.submit", "nautilus.order.modify", "nautilus.order.cancel"].contains(context.tool) && context.mandate_passed != true };
|
|
42598
|
+
|
|
42599
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource)
|
|
42600
|
+
when { context.tool == "nautilus.strategy.deploy" && context.strategy_pack_signed != true };
|
|
42601
|
+
`
|
|
42602
|
+
}
|
|
42603
|
+
];
|
|
42604
|
+
function connectorPilotIds() {
|
|
42605
|
+
return CONNECTOR_PILOTS.map((pilot) => pilot.id);
|
|
42606
|
+
}
|
|
42607
|
+
function getConnectorPilot(id) {
|
|
42608
|
+
return CONNECTOR_PILOTS.find((pilot) => pilot.id === id);
|
|
42609
|
+
}
|
|
42610
|
+
function connectorDirectory(dir) {
|
|
42611
|
+
return (0, import_node_path6.join)(dir, ".protect-mcp", "connectors");
|
|
42612
|
+
}
|
|
42613
|
+
function writeConnectorPilots(opts) {
|
|
42614
|
+
const directory = connectorDirectory(opts.dir);
|
|
42615
|
+
(0, import_node_fs10.mkdirSync)(directory, { recursive: true });
|
|
42616
|
+
const selected = opts.ids && opts.ids.length > 0 && !opts.ids.includes("all") ? opts.ids.map((id) => {
|
|
42617
|
+
const pilot = getConnectorPilot(id);
|
|
42618
|
+
if (!pilot) throw new Error(`Unknown connector pilot: ${id}`);
|
|
42619
|
+
return pilot;
|
|
42620
|
+
}) : CONNECTOR_PILOTS;
|
|
42621
|
+
const written = [];
|
|
42622
|
+
for (const pilot of selected) {
|
|
42623
|
+
const configPath = (0, import_node_path6.join)(directory, `${pilot.id}.json`);
|
|
42624
|
+
const policyPath = (0, import_node_path6.join)(directory, `${pilot.id}.cedar`);
|
|
42625
|
+
if (!opts.force && ((0, import_node_fs10.existsSync)(configPath) || (0, import_node_fs10.existsSync)(policyPath))) {
|
|
42626
|
+
throw new Error(`Refusing to overwrite ${pilot.id}. Re-run with --force if intentional.`);
|
|
42627
|
+
}
|
|
42628
|
+
(0, import_node_fs10.writeFileSync)(configPath, JSON.stringify({ ...pilot.config, id: pilot.id, name: pilot.name, category: pilot.category, tools: pilot.tools, actions: pilot.actions, setup: pilot.setup }, null, 2) + "\n");
|
|
42629
|
+
(0, import_node_fs10.writeFileSync)(policyPath, pilot.cedar.endsWith("\n") ? pilot.cedar : `${pilot.cedar}
|
|
42630
|
+
`);
|
|
42631
|
+
written.push(configPath, policyPath);
|
|
42632
|
+
for (const artifact of pilot.artifacts || []) {
|
|
42633
|
+
const artifactPath = connectorArtifactPath(directory, artifact.path);
|
|
42634
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path6.dirname)(artifactPath), { recursive: true });
|
|
42635
|
+
(0, import_node_fs10.writeFileSync)(artifactPath, artifact.contents.endsWith("\n") ? artifact.contents : `${artifact.contents}
|
|
42636
|
+
`);
|
|
42637
|
+
if (artifact.executable) (0, import_node_fs10.chmodSync)(artifactPath, 493);
|
|
42638
|
+
written.push(artifactPath);
|
|
42639
|
+
}
|
|
42640
|
+
}
|
|
42641
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path6.join)(directory, "README.md"), renderConnectorReadme(selected));
|
|
42642
|
+
written.push((0, import_node_path6.join)(directory, "README.md"));
|
|
42643
|
+
return { written, pilots: selected, directory };
|
|
42644
|
+
}
|
|
42645
|
+
function connectorArtifactPath(directory, relativePath) {
|
|
42646
|
+
const clean2 = (0, import_node_path6.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
42647
|
+
if (clean2.startsWith("/") || clean2.includes("..")) {
|
|
42648
|
+
throw new Error(`Unsafe connector artifact path: ${relativePath}`);
|
|
42649
|
+
}
|
|
42650
|
+
return (0, import_node_path6.join)(directory, clean2);
|
|
42651
|
+
}
|
|
42652
|
+
function readInstalledConnectorPilots(dir) {
|
|
42653
|
+
const directory = connectorDirectory(dir);
|
|
42654
|
+
if (!(0, import_node_fs10.existsSync)(directory)) return [];
|
|
42655
|
+
return (0, import_node_fs10.readdirSync)(directory).filter((name) => name.endsWith(".json")).map((name) => {
|
|
42656
|
+
const configPath = (0, import_node_path6.join)(directory, name);
|
|
42657
|
+
try {
|
|
42658
|
+
const parsed = JSON.parse((0, import_node_fs10.readFileSync)(configPath, "utf-8"));
|
|
42659
|
+
const id = String(parsed.id || name.replace(/\.json$/, ""));
|
|
42660
|
+
const pilot = getConnectorPilot(id);
|
|
42661
|
+
return {
|
|
42662
|
+
id,
|
|
42663
|
+
name: String(parsed.name || pilot?.name || id),
|
|
42664
|
+
category: String(parsed.category || pilot?.category || "unknown"),
|
|
42665
|
+
status: String(parsed.status || parsed.type || "installed"),
|
|
42666
|
+
config_path: configPath,
|
|
42667
|
+
policy_path: (0, import_node_path6.join)(directory, `${id}.cedar`)
|
|
42668
|
+
};
|
|
42669
|
+
} catch {
|
|
42670
|
+
return null;
|
|
42671
|
+
}
|
|
42672
|
+
}).filter(Boolean);
|
|
42673
|
+
}
|
|
42674
|
+
function connectorDoctor(dir, env = process.env) {
|
|
42675
|
+
const installed = new Set(readInstalledConnectorPilots(dir).map((pilot) => pilot.id));
|
|
42676
|
+
return CONNECTOR_PILOTS.map((pilot) => {
|
|
42677
|
+
const envRows = pilot.env.map((item) => ({
|
|
42678
|
+
name: item.name,
|
|
42679
|
+
required: item.required,
|
|
42680
|
+
present: Boolean(env[item.name]),
|
|
42681
|
+
description: item.description
|
|
42682
|
+
}));
|
|
42683
|
+
const missingRequired = envRows.filter((item) => item.required && !item.present).map((item) => item.name);
|
|
42684
|
+
const optionalPresent = envRows.filter((item) => !item.required && item.present).map((item) => item.name);
|
|
42685
|
+
const optionalProviderReady = pilot.id === "slack-teams" ? Boolean(env.SLACK_BOT_TOKEN || env.TEAMS_WEBHOOK_URL) : pilot.id === "finance-pms" ? Boolean(env.PMS_ADAPTER_URL) : pilot.id === "nautilus-trader" ? Boolean(env.NAUTILUS_BRIDGE_MODULE || env.NAUTILUS_TRADER_PROJECT) : false;
|
|
42686
|
+
const mockModeReady = pilot.id === "finance-pms" || pilot.id === "nautilus-trader";
|
|
42687
|
+
return {
|
|
42688
|
+
id: pilot.id,
|
|
42689
|
+
name: pilot.name,
|
|
42690
|
+
category: pilot.category,
|
|
42691
|
+
installed: installed.has(pilot.id),
|
|
42692
|
+
usable: missingRequired.length === 0 && (pilot.env.some((item) => item.required) || pilot.env.length === 0 || optionalProviderReady || mockModeReady),
|
|
42693
|
+
mode: pilot.id === "finance-pms" && !env.PMS_ADAPTER_URL ? "mock" : pilot.id === "nautilus-trader" && !env.NAUTILUS_BRIDGE_MODULE ? "mock_bridge" : pilot.id === "slack-teams" && !env.SLACK_BOT_TOKEN && !env.TEAMS_WEBHOOK_URL ? "needs_provider_choice" : "configured_or_local",
|
|
42694
|
+
missing_required: missingRequired,
|
|
42695
|
+
optional_present: optionalPresent,
|
|
42696
|
+
tools: pilot.tools,
|
|
42697
|
+
next: missingRequired.length > 0 ? `Set ${missingRequired.join(", ")}` : installed.has(pilot.id) ? "Run through protect-mcp and inspect the dashboard." : `Install with protect-mcp connectors init ${pilot.id}`
|
|
42698
|
+
};
|
|
42699
|
+
});
|
|
42700
|
+
}
|
|
42701
|
+
function renderConnectorReadme(pilots) {
|
|
42702
|
+
return `# protect-mcp connector pilots
|
|
42703
|
+
|
|
42704
|
+
These files make real tool classes visible and controllable without uploading raw prompts or payloads.
|
|
42705
|
+
|
|
42706
|
+
${pilots.map((pilot) => `## ${pilot.name}
|
|
42707
|
+
|
|
42708
|
+
${pilot.description}
|
|
42709
|
+
|
|
42710
|
+
Value: ${pilot.value}
|
|
42711
|
+
|
|
42712
|
+
Tools: ${pilot.tools.map((tool) => `\`${tool}\``).join(", ")}
|
|
42713
|
+
|
|
42714
|
+
Setup:
|
|
42715
|
+
${pilot.setup.map((step) => `- ${step}`).join("\n")}
|
|
42716
|
+
${pilot.artifacts?.length ? `
|
|
42717
|
+
Generated files:
|
|
42718
|
+
${pilot.artifacts.map((artifact) => `- \`${artifact.path}\``).join("\n")}
|
|
42719
|
+
` : ""}`).join("\n")}
|
|
42720
|
+
Next: run \`npx protect-mcp dashboard --open\` and review tool inventory, policy coverage, approvals, and receipts.
|
|
42721
|
+
`;
|
|
42722
|
+
}
|
|
42723
|
+
|
|
41324
42724
|
// src/rekor-anchor.ts
|
|
41325
|
-
var
|
|
42725
|
+
var import_node_crypto6 = require("crypto");
|
|
41326
42726
|
var REKOR_API = "https://rekor.sigstore.dev/api/v1";
|
|
41327
42727
|
async function anchorToRekor(receiptHash, signature, publicKeyPem) {
|
|
41328
42728
|
const entry = {
|
|
@@ -41391,7 +42791,7 @@ async function verifyRekorAnchor(logIndex, expectedHash) {
|
|
|
41391
42791
|
}
|
|
41392
42792
|
function hashReceipt(receipt) {
|
|
41393
42793
|
const canonical = JSON.stringify(receipt, Object.keys(receipt).sort());
|
|
41394
|
-
return (0,
|
|
42794
|
+
return (0, import_node_crypto6.createHash)("sha256").update(canonical).digest("hex");
|
|
41395
42795
|
}
|
|
41396
42796
|
function createLogAnchorField(anchor) {
|
|
41397
42797
|
return {
|
|
@@ -41404,7 +42804,7 @@ function createLogAnchorField(anchor) {
|
|
|
41404
42804
|
}
|
|
41405
42805
|
|
|
41406
42806
|
// src/selective-disclosure.ts
|
|
41407
|
-
var
|
|
42807
|
+
var import_node_crypto7 = require("crypto");
|
|
41408
42808
|
function redactFields(receipt, fieldsToRedact) {
|
|
41409
42809
|
const redacted = JSON.parse(JSON.stringify(receipt));
|
|
41410
42810
|
const salts = [];
|
|
@@ -41420,7 +42820,7 @@ function redactFields(receipt, fieldsToRedact) {
|
|
|
41420
42820
|
if (i === parts.length - 1) {
|
|
41421
42821
|
if (key in current) {
|
|
41422
42822
|
const originalValue = current[key];
|
|
41423
|
-
const salt = (0,
|
|
42823
|
+
const salt = (0, import_node_crypto7.randomBytes)(16).toString("hex");
|
|
41424
42824
|
const commitment = computeCommitment(salt, originalValue);
|
|
41425
42825
|
salts.push({ field: fieldPath, salt, originalValue });
|
|
41426
42826
|
current[key] = `sha256(salt + ${typeof originalValue === "string" ? "..." : JSON.stringify(originalValue).slice(0, 20) + "..."})`;
|
|
@@ -41492,11 +42892,11 @@ function createDisclosurePackage(allSalts, fieldsToDisclose) {
|
|
|
41492
42892
|
}
|
|
41493
42893
|
function computeCommitment(salt, value) {
|
|
41494
42894
|
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
41495
|
-
return (0,
|
|
42895
|
+
return (0, import_node_crypto7.createHash)("sha256").update(salt + serialized).digest("hex");
|
|
41496
42896
|
}
|
|
41497
42897
|
function hashObject(obj) {
|
|
41498
42898
|
const canonical = JSON.stringify(obj, Object.keys(obj).sort());
|
|
41499
|
-
return (0,
|
|
42899
|
+
return (0, import_node_crypto7.createHash)("sha256").update(canonical).digest("hex");
|
|
41500
42900
|
}
|
|
41501
42901
|
|
|
41502
42902
|
// src/huggingface-export.ts
|
|
@@ -41637,10 +43037,1070 @@ MIT
|
|
|
41637
43037
|
}
|
|
41638
43038
|
|
|
41639
43039
|
// src/webauthn-approval.ts
|
|
41640
|
-
var
|
|
43040
|
+
var import_node_crypto8 = require("crypto");
|
|
43041
|
+
|
|
43042
|
+
// node_modules/@noble/curves/esm/abstract/weierstrass.js
|
|
43043
|
+
var divNearest = (num, den) => (num + (num >= 0 ? den : -den) / _2n4) / den;
|
|
43044
|
+
function _splitEndoScalar(k, basis, n) {
|
|
43045
|
+
const [[a1, b1], [a2, b2]] = basis;
|
|
43046
|
+
const c1 = divNearest(b2 * k, n);
|
|
43047
|
+
const c2 = divNearest(-b1 * k, n);
|
|
43048
|
+
let k1 = k - c1 * a1 - c2 * a2;
|
|
43049
|
+
let k2 = -c1 * b1 - c2 * b2;
|
|
43050
|
+
const k1neg = k1 < _0n6;
|
|
43051
|
+
const k2neg = k2 < _0n6;
|
|
43052
|
+
if (k1neg)
|
|
43053
|
+
k1 = -k1;
|
|
43054
|
+
if (k2neg)
|
|
43055
|
+
k2 = -k2;
|
|
43056
|
+
const MAX_NUM = bitMask(Math.ceil(bitLen(n) / 2)) + _1n6;
|
|
43057
|
+
if (k1 < _0n6 || k1 >= MAX_NUM || k2 < _0n6 || k2 >= MAX_NUM) {
|
|
43058
|
+
throw new Error("splitScalar (endomorphism): failed, k=" + k);
|
|
43059
|
+
}
|
|
43060
|
+
return { k1neg, k1, k2neg, k2 };
|
|
43061
|
+
}
|
|
43062
|
+
function validateSigFormat(format) {
|
|
43063
|
+
if (!["compact", "recovered", "der"].includes(format))
|
|
43064
|
+
throw new Error('Signature format must be "compact", "recovered", or "der"');
|
|
43065
|
+
return format;
|
|
43066
|
+
}
|
|
43067
|
+
function validateSigOpts(opts, def) {
|
|
43068
|
+
const optsn = {};
|
|
43069
|
+
for (let optName of Object.keys(def)) {
|
|
43070
|
+
optsn[optName] = opts[optName] === void 0 ? def[optName] : opts[optName];
|
|
43071
|
+
}
|
|
43072
|
+
_abool2(optsn.lowS, "lowS");
|
|
43073
|
+
_abool2(optsn.prehash, "prehash");
|
|
43074
|
+
if (optsn.format !== void 0)
|
|
43075
|
+
validateSigFormat(optsn.format);
|
|
43076
|
+
return optsn;
|
|
43077
|
+
}
|
|
43078
|
+
var DERErr = class extends Error {
|
|
43079
|
+
constructor(m = "") {
|
|
43080
|
+
super(m);
|
|
43081
|
+
}
|
|
43082
|
+
};
|
|
43083
|
+
var DER = {
|
|
43084
|
+
// asn.1 DER encoding utils
|
|
43085
|
+
Err: DERErr,
|
|
43086
|
+
// Basic building block is TLV (Tag-Length-Value)
|
|
43087
|
+
_tlv: {
|
|
43088
|
+
encode: (tag, data) => {
|
|
43089
|
+
const { Err: E } = DER;
|
|
43090
|
+
if (tag < 0 || tag > 256)
|
|
43091
|
+
throw new E("tlv.encode: wrong tag");
|
|
43092
|
+
if (data.length & 1)
|
|
43093
|
+
throw new E("tlv.encode: unpadded data");
|
|
43094
|
+
const dataLen = data.length / 2;
|
|
43095
|
+
const len = numberToHexUnpadded(dataLen);
|
|
43096
|
+
if (len.length / 2 & 128)
|
|
43097
|
+
throw new E("tlv.encode: long form length too big");
|
|
43098
|
+
const lenLen = dataLen > 127 ? numberToHexUnpadded(len.length / 2 | 128) : "";
|
|
43099
|
+
const t = numberToHexUnpadded(tag);
|
|
43100
|
+
return t + lenLen + len + data;
|
|
43101
|
+
},
|
|
43102
|
+
// v - value, l - left bytes (unparsed)
|
|
43103
|
+
decode(tag, data) {
|
|
43104
|
+
const { Err: E } = DER;
|
|
43105
|
+
let pos = 0;
|
|
43106
|
+
if (tag < 0 || tag > 256)
|
|
43107
|
+
throw new E("tlv.encode: wrong tag");
|
|
43108
|
+
if (data.length < 2 || data[pos++] !== tag)
|
|
43109
|
+
throw new E("tlv.decode: wrong tlv");
|
|
43110
|
+
const first = data[pos++];
|
|
43111
|
+
const isLong = !!(first & 128);
|
|
43112
|
+
let length = 0;
|
|
43113
|
+
if (!isLong)
|
|
43114
|
+
length = first;
|
|
43115
|
+
else {
|
|
43116
|
+
const lenLen = first & 127;
|
|
43117
|
+
if (!lenLen)
|
|
43118
|
+
throw new E("tlv.decode(long): indefinite length not supported");
|
|
43119
|
+
if (lenLen > 4)
|
|
43120
|
+
throw new E("tlv.decode(long): byte length is too big");
|
|
43121
|
+
const lengthBytes = data.subarray(pos, pos + lenLen);
|
|
43122
|
+
if (lengthBytes.length !== lenLen)
|
|
43123
|
+
throw new E("tlv.decode: length bytes not complete");
|
|
43124
|
+
if (lengthBytes[0] === 0)
|
|
43125
|
+
throw new E("tlv.decode(long): zero leftmost byte");
|
|
43126
|
+
for (const b of lengthBytes)
|
|
43127
|
+
length = length << 8 | b;
|
|
43128
|
+
pos += lenLen;
|
|
43129
|
+
if (length < 128)
|
|
43130
|
+
throw new E("tlv.decode(long): not minimal encoding");
|
|
43131
|
+
}
|
|
43132
|
+
const v = data.subarray(pos, pos + length);
|
|
43133
|
+
if (v.length !== length)
|
|
43134
|
+
throw new E("tlv.decode: wrong value length");
|
|
43135
|
+
return { v, l: data.subarray(pos + length) };
|
|
43136
|
+
}
|
|
43137
|
+
},
|
|
43138
|
+
// https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag,
|
|
43139
|
+
// since we always use positive integers here. It must always be empty:
|
|
43140
|
+
// - add zero byte if exists
|
|
43141
|
+
// - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding)
|
|
43142
|
+
_int: {
|
|
43143
|
+
encode(num) {
|
|
43144
|
+
const { Err: E } = DER;
|
|
43145
|
+
if (num < _0n6)
|
|
43146
|
+
throw new E("integer: negative integers are not allowed");
|
|
43147
|
+
let hex = numberToHexUnpadded(num);
|
|
43148
|
+
if (Number.parseInt(hex[0], 16) & 8)
|
|
43149
|
+
hex = "00" + hex;
|
|
43150
|
+
if (hex.length & 1)
|
|
43151
|
+
throw new E("unexpected DER parsing assertion: unpadded hex");
|
|
43152
|
+
return hex;
|
|
43153
|
+
},
|
|
43154
|
+
decode(data) {
|
|
43155
|
+
const { Err: E } = DER;
|
|
43156
|
+
if (data[0] & 128)
|
|
43157
|
+
throw new E("invalid signature integer: negative");
|
|
43158
|
+
if (data[0] === 0 && !(data[1] & 128))
|
|
43159
|
+
throw new E("invalid signature integer: unnecessary leading zero");
|
|
43160
|
+
return bytesToNumberBE(data);
|
|
43161
|
+
}
|
|
43162
|
+
},
|
|
43163
|
+
toSig(hex) {
|
|
43164
|
+
const { Err: E, _int: int, _tlv: tlv } = DER;
|
|
43165
|
+
const data = ensureBytes("signature", hex);
|
|
43166
|
+
const { v: seqBytes, l: seqLeftBytes } = tlv.decode(48, data);
|
|
43167
|
+
if (seqLeftBytes.length)
|
|
43168
|
+
throw new E("invalid signature: left bytes after parsing");
|
|
43169
|
+
const { v: rBytes, l: rLeftBytes } = tlv.decode(2, seqBytes);
|
|
43170
|
+
const { v: sBytes, l: sLeftBytes } = tlv.decode(2, rLeftBytes);
|
|
43171
|
+
if (sLeftBytes.length)
|
|
43172
|
+
throw new E("invalid signature: left bytes after parsing");
|
|
43173
|
+
return { r: int.decode(rBytes), s: int.decode(sBytes) };
|
|
43174
|
+
},
|
|
43175
|
+
hexFromSig(sig) {
|
|
43176
|
+
const { _tlv: tlv, _int: int } = DER;
|
|
43177
|
+
const rs = tlv.encode(2, int.encode(sig.r));
|
|
43178
|
+
const ss = tlv.encode(2, int.encode(sig.s));
|
|
43179
|
+
const seq = rs + ss;
|
|
43180
|
+
return tlv.encode(48, seq);
|
|
43181
|
+
}
|
|
43182
|
+
};
|
|
43183
|
+
var _0n6 = BigInt(0);
|
|
43184
|
+
var _1n6 = BigInt(1);
|
|
43185
|
+
var _2n4 = BigInt(2);
|
|
43186
|
+
var _3n3 = BigInt(3);
|
|
43187
|
+
var _4n2 = BigInt(4);
|
|
43188
|
+
function _normFnElement(Fn2, key) {
|
|
43189
|
+
const { BYTES: expected } = Fn2;
|
|
43190
|
+
let num;
|
|
43191
|
+
if (typeof key === "bigint") {
|
|
43192
|
+
num = key;
|
|
43193
|
+
} else {
|
|
43194
|
+
let bytes = ensureBytes("private key", key);
|
|
43195
|
+
try {
|
|
43196
|
+
num = Fn2.fromBytes(bytes);
|
|
43197
|
+
} catch (error) {
|
|
43198
|
+
throw new Error(`invalid private key: expected ui8a of size ${expected}, got ${typeof key}`);
|
|
43199
|
+
}
|
|
43200
|
+
}
|
|
43201
|
+
if (!Fn2.isValidNot0(num))
|
|
43202
|
+
throw new Error("invalid private key: out of range [1..N-1]");
|
|
43203
|
+
return num;
|
|
43204
|
+
}
|
|
43205
|
+
function weierstrassN(params, extraOpts = {}) {
|
|
43206
|
+
const validated = _createCurveFields("weierstrass", params, extraOpts);
|
|
43207
|
+
const { Fp: Fp2, Fn: Fn2 } = validated;
|
|
43208
|
+
let CURVE = validated.CURVE;
|
|
43209
|
+
const { h: cofactor, n: CURVE_ORDER } = CURVE;
|
|
43210
|
+
_validateObject(extraOpts, {}, {
|
|
43211
|
+
allowInfinityPoint: "boolean",
|
|
43212
|
+
clearCofactor: "function",
|
|
43213
|
+
isTorsionFree: "function",
|
|
43214
|
+
fromBytes: "function",
|
|
43215
|
+
toBytes: "function",
|
|
43216
|
+
endo: "object",
|
|
43217
|
+
wrapPrivateKey: "boolean"
|
|
43218
|
+
});
|
|
43219
|
+
const { endo } = extraOpts;
|
|
43220
|
+
if (endo) {
|
|
43221
|
+
if (!Fp2.is0(CURVE.a) || typeof endo.beta !== "bigint" || !Array.isArray(endo.basises)) {
|
|
43222
|
+
throw new Error('invalid endo: expected "beta": bigint and "basises": array');
|
|
43223
|
+
}
|
|
43224
|
+
}
|
|
43225
|
+
const lengths = getWLengths(Fp2, Fn2);
|
|
43226
|
+
function assertCompressionIsSupported() {
|
|
43227
|
+
if (!Fp2.isOdd)
|
|
43228
|
+
throw new Error("compression is not supported: Field does not have .isOdd()");
|
|
43229
|
+
}
|
|
43230
|
+
function pointToBytes(_c, point, isCompressed) {
|
|
43231
|
+
const { x, y } = point.toAffine();
|
|
43232
|
+
const bx = Fp2.toBytes(x);
|
|
43233
|
+
_abool2(isCompressed, "isCompressed");
|
|
43234
|
+
if (isCompressed) {
|
|
43235
|
+
assertCompressionIsSupported();
|
|
43236
|
+
const hasEvenY = !Fp2.isOdd(y);
|
|
43237
|
+
return concatBytes(pprefix(hasEvenY), bx);
|
|
43238
|
+
} else {
|
|
43239
|
+
return concatBytes(Uint8Array.of(4), bx, Fp2.toBytes(y));
|
|
43240
|
+
}
|
|
43241
|
+
}
|
|
43242
|
+
function pointFromBytes(bytes) {
|
|
43243
|
+
_abytes2(bytes, void 0, "Point");
|
|
43244
|
+
const { publicKey: comp, publicKeyUncompressed: uncomp } = lengths;
|
|
43245
|
+
const length = bytes.length;
|
|
43246
|
+
const head = bytes[0];
|
|
43247
|
+
const tail = bytes.subarray(1);
|
|
43248
|
+
if (length === comp && (head === 2 || head === 3)) {
|
|
43249
|
+
const x = Fp2.fromBytes(tail);
|
|
43250
|
+
if (!Fp2.isValid(x))
|
|
43251
|
+
throw new Error("bad point: is not on curve, wrong x");
|
|
43252
|
+
const y2 = weierstrassEquation(x);
|
|
43253
|
+
let y;
|
|
43254
|
+
try {
|
|
43255
|
+
y = Fp2.sqrt(y2);
|
|
43256
|
+
} catch (sqrtError) {
|
|
43257
|
+
const err = sqrtError instanceof Error ? ": " + sqrtError.message : "";
|
|
43258
|
+
throw new Error("bad point: is not on curve, sqrt error" + err);
|
|
43259
|
+
}
|
|
43260
|
+
assertCompressionIsSupported();
|
|
43261
|
+
const isYOdd = Fp2.isOdd(y);
|
|
43262
|
+
const isHeadOdd = (head & 1) === 1;
|
|
43263
|
+
if (isHeadOdd !== isYOdd)
|
|
43264
|
+
y = Fp2.neg(y);
|
|
43265
|
+
return { x, y };
|
|
43266
|
+
} else if (length === uncomp && head === 4) {
|
|
43267
|
+
const L = Fp2.BYTES;
|
|
43268
|
+
const x = Fp2.fromBytes(tail.subarray(0, L));
|
|
43269
|
+
const y = Fp2.fromBytes(tail.subarray(L, L * 2));
|
|
43270
|
+
if (!isValidXY(x, y))
|
|
43271
|
+
throw new Error("bad point: is not on curve");
|
|
43272
|
+
return { x, y };
|
|
43273
|
+
} else {
|
|
43274
|
+
throw new Error(`bad point: got length ${length}, expected compressed=${comp} or uncompressed=${uncomp}`);
|
|
43275
|
+
}
|
|
43276
|
+
}
|
|
43277
|
+
const encodePoint = extraOpts.toBytes || pointToBytes;
|
|
43278
|
+
const decodePoint = extraOpts.fromBytes || pointFromBytes;
|
|
43279
|
+
function weierstrassEquation(x) {
|
|
43280
|
+
const x2 = Fp2.sqr(x);
|
|
43281
|
+
const x3 = Fp2.mul(x2, x);
|
|
43282
|
+
return Fp2.add(Fp2.add(x3, Fp2.mul(x, CURVE.a)), CURVE.b);
|
|
43283
|
+
}
|
|
43284
|
+
function isValidXY(x, y) {
|
|
43285
|
+
const left = Fp2.sqr(y);
|
|
43286
|
+
const right = weierstrassEquation(x);
|
|
43287
|
+
return Fp2.eql(left, right);
|
|
43288
|
+
}
|
|
43289
|
+
if (!isValidXY(CURVE.Gx, CURVE.Gy))
|
|
43290
|
+
throw new Error("bad curve params: generator point");
|
|
43291
|
+
const _4a3 = Fp2.mul(Fp2.pow(CURVE.a, _3n3), _4n2);
|
|
43292
|
+
const _27b2 = Fp2.mul(Fp2.sqr(CURVE.b), BigInt(27));
|
|
43293
|
+
if (Fp2.is0(Fp2.add(_4a3, _27b2)))
|
|
43294
|
+
throw new Error("bad curve params: a or b");
|
|
43295
|
+
function acoord(title, n, banZero = false) {
|
|
43296
|
+
if (!Fp2.isValid(n) || banZero && Fp2.is0(n))
|
|
43297
|
+
throw new Error(`bad point coordinate ${title}`);
|
|
43298
|
+
return n;
|
|
43299
|
+
}
|
|
43300
|
+
function aprjpoint(other) {
|
|
43301
|
+
if (!(other instanceof Point))
|
|
43302
|
+
throw new Error("ProjectivePoint expected");
|
|
43303
|
+
}
|
|
43304
|
+
function splitEndoScalarN(k) {
|
|
43305
|
+
if (!endo || !endo.basises)
|
|
43306
|
+
throw new Error("no endo");
|
|
43307
|
+
return _splitEndoScalar(k, endo.basises, Fn2.ORDER);
|
|
43308
|
+
}
|
|
43309
|
+
const toAffineMemo = memoized((p, iz) => {
|
|
43310
|
+
const { X, Y, Z } = p;
|
|
43311
|
+
if (Fp2.eql(Z, Fp2.ONE))
|
|
43312
|
+
return { x: X, y: Y };
|
|
43313
|
+
const is0 = p.is0();
|
|
43314
|
+
if (iz == null)
|
|
43315
|
+
iz = is0 ? Fp2.ONE : Fp2.inv(Z);
|
|
43316
|
+
const x = Fp2.mul(X, iz);
|
|
43317
|
+
const y = Fp2.mul(Y, iz);
|
|
43318
|
+
const zz = Fp2.mul(Z, iz);
|
|
43319
|
+
if (is0)
|
|
43320
|
+
return { x: Fp2.ZERO, y: Fp2.ZERO };
|
|
43321
|
+
if (!Fp2.eql(zz, Fp2.ONE))
|
|
43322
|
+
throw new Error("invZ was invalid");
|
|
43323
|
+
return { x, y };
|
|
43324
|
+
});
|
|
43325
|
+
const assertValidMemo = memoized((p) => {
|
|
43326
|
+
if (p.is0()) {
|
|
43327
|
+
if (extraOpts.allowInfinityPoint && !Fp2.is0(p.Y))
|
|
43328
|
+
return;
|
|
43329
|
+
throw new Error("bad point: ZERO");
|
|
43330
|
+
}
|
|
43331
|
+
const { x, y } = p.toAffine();
|
|
43332
|
+
if (!Fp2.isValid(x) || !Fp2.isValid(y))
|
|
43333
|
+
throw new Error("bad point: x or y not field elements");
|
|
43334
|
+
if (!isValidXY(x, y))
|
|
43335
|
+
throw new Error("bad point: equation left != right");
|
|
43336
|
+
if (!p.isTorsionFree())
|
|
43337
|
+
throw new Error("bad point: not in prime-order subgroup");
|
|
43338
|
+
return true;
|
|
43339
|
+
});
|
|
43340
|
+
function finishEndo(endoBeta, k1p, k2p, k1neg, k2neg) {
|
|
43341
|
+
k2p = new Point(Fp2.mul(k2p.X, endoBeta), k2p.Y, k2p.Z);
|
|
43342
|
+
k1p = negateCt(k1neg, k1p);
|
|
43343
|
+
k2p = negateCt(k2neg, k2p);
|
|
43344
|
+
return k1p.add(k2p);
|
|
43345
|
+
}
|
|
43346
|
+
class Point {
|
|
43347
|
+
/** Does NOT validate if the point is valid. Use `.assertValidity()`. */
|
|
43348
|
+
constructor(X, Y, Z) {
|
|
43349
|
+
this.X = acoord("x", X);
|
|
43350
|
+
this.Y = acoord("y", Y, true);
|
|
43351
|
+
this.Z = acoord("z", Z);
|
|
43352
|
+
Object.freeze(this);
|
|
43353
|
+
}
|
|
43354
|
+
static CURVE() {
|
|
43355
|
+
return CURVE;
|
|
43356
|
+
}
|
|
43357
|
+
/** Does NOT validate if the point is valid. Use `.assertValidity()`. */
|
|
43358
|
+
static fromAffine(p) {
|
|
43359
|
+
const { x, y } = p || {};
|
|
43360
|
+
if (!p || !Fp2.isValid(x) || !Fp2.isValid(y))
|
|
43361
|
+
throw new Error("invalid affine point");
|
|
43362
|
+
if (p instanceof Point)
|
|
43363
|
+
throw new Error("projective point not allowed");
|
|
43364
|
+
if (Fp2.is0(x) && Fp2.is0(y))
|
|
43365
|
+
return Point.ZERO;
|
|
43366
|
+
return new Point(x, y, Fp2.ONE);
|
|
43367
|
+
}
|
|
43368
|
+
static fromBytes(bytes) {
|
|
43369
|
+
const P = Point.fromAffine(decodePoint(_abytes2(bytes, void 0, "point")));
|
|
43370
|
+
P.assertValidity();
|
|
43371
|
+
return P;
|
|
43372
|
+
}
|
|
43373
|
+
static fromHex(hex) {
|
|
43374
|
+
return Point.fromBytes(ensureBytes("pointHex", hex));
|
|
43375
|
+
}
|
|
43376
|
+
get x() {
|
|
43377
|
+
return this.toAffine().x;
|
|
43378
|
+
}
|
|
43379
|
+
get y() {
|
|
43380
|
+
return this.toAffine().y;
|
|
43381
|
+
}
|
|
43382
|
+
/**
|
|
43383
|
+
*
|
|
43384
|
+
* @param windowSize
|
|
43385
|
+
* @param isLazy true will defer table computation until the first multiplication
|
|
43386
|
+
* @returns
|
|
43387
|
+
*/
|
|
43388
|
+
precompute(windowSize = 8, isLazy = true) {
|
|
43389
|
+
wnaf.createCache(this, windowSize);
|
|
43390
|
+
if (!isLazy)
|
|
43391
|
+
this.multiply(_3n3);
|
|
43392
|
+
return this;
|
|
43393
|
+
}
|
|
43394
|
+
// TODO: return `this`
|
|
43395
|
+
/** A point on curve is valid if it conforms to equation. */
|
|
43396
|
+
assertValidity() {
|
|
43397
|
+
assertValidMemo(this);
|
|
43398
|
+
}
|
|
43399
|
+
hasEvenY() {
|
|
43400
|
+
const { y } = this.toAffine();
|
|
43401
|
+
if (!Fp2.isOdd)
|
|
43402
|
+
throw new Error("Field doesn't support isOdd");
|
|
43403
|
+
return !Fp2.isOdd(y);
|
|
43404
|
+
}
|
|
43405
|
+
/** Compare one point to another. */
|
|
43406
|
+
equals(other) {
|
|
43407
|
+
aprjpoint(other);
|
|
43408
|
+
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
43409
|
+
const { X: X2, Y: Y2, Z: Z2 } = other;
|
|
43410
|
+
const U1 = Fp2.eql(Fp2.mul(X1, Z2), Fp2.mul(X2, Z1));
|
|
43411
|
+
const U2 = Fp2.eql(Fp2.mul(Y1, Z2), Fp2.mul(Y2, Z1));
|
|
43412
|
+
return U1 && U2;
|
|
43413
|
+
}
|
|
43414
|
+
/** Flips point to one corresponding to (x, -y) in Affine coordinates. */
|
|
43415
|
+
negate() {
|
|
43416
|
+
return new Point(this.X, Fp2.neg(this.Y), this.Z);
|
|
43417
|
+
}
|
|
43418
|
+
// Renes-Costello-Batina exception-free doubling formula.
|
|
43419
|
+
// There is 30% faster Jacobian formula, but it is not complete.
|
|
43420
|
+
// https://eprint.iacr.org/2015/1060, algorithm 3
|
|
43421
|
+
// Cost: 8M + 3S + 3*a + 2*b3 + 15add.
|
|
43422
|
+
double() {
|
|
43423
|
+
const { a, b } = CURVE;
|
|
43424
|
+
const b3 = Fp2.mul(b, _3n3);
|
|
43425
|
+
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
43426
|
+
let X3 = Fp2.ZERO, Y3 = Fp2.ZERO, Z3 = Fp2.ZERO;
|
|
43427
|
+
let t0 = Fp2.mul(X1, X1);
|
|
43428
|
+
let t1 = Fp2.mul(Y1, Y1);
|
|
43429
|
+
let t2 = Fp2.mul(Z1, Z1);
|
|
43430
|
+
let t3 = Fp2.mul(X1, Y1);
|
|
43431
|
+
t3 = Fp2.add(t3, t3);
|
|
43432
|
+
Z3 = Fp2.mul(X1, Z1);
|
|
43433
|
+
Z3 = Fp2.add(Z3, Z3);
|
|
43434
|
+
X3 = Fp2.mul(a, Z3);
|
|
43435
|
+
Y3 = Fp2.mul(b3, t2);
|
|
43436
|
+
Y3 = Fp2.add(X3, Y3);
|
|
43437
|
+
X3 = Fp2.sub(t1, Y3);
|
|
43438
|
+
Y3 = Fp2.add(t1, Y3);
|
|
43439
|
+
Y3 = Fp2.mul(X3, Y3);
|
|
43440
|
+
X3 = Fp2.mul(t3, X3);
|
|
43441
|
+
Z3 = Fp2.mul(b3, Z3);
|
|
43442
|
+
t2 = Fp2.mul(a, t2);
|
|
43443
|
+
t3 = Fp2.sub(t0, t2);
|
|
43444
|
+
t3 = Fp2.mul(a, t3);
|
|
43445
|
+
t3 = Fp2.add(t3, Z3);
|
|
43446
|
+
Z3 = Fp2.add(t0, t0);
|
|
43447
|
+
t0 = Fp2.add(Z3, t0);
|
|
43448
|
+
t0 = Fp2.add(t0, t2);
|
|
43449
|
+
t0 = Fp2.mul(t0, t3);
|
|
43450
|
+
Y3 = Fp2.add(Y3, t0);
|
|
43451
|
+
t2 = Fp2.mul(Y1, Z1);
|
|
43452
|
+
t2 = Fp2.add(t2, t2);
|
|
43453
|
+
t0 = Fp2.mul(t2, t3);
|
|
43454
|
+
X3 = Fp2.sub(X3, t0);
|
|
43455
|
+
Z3 = Fp2.mul(t2, t1);
|
|
43456
|
+
Z3 = Fp2.add(Z3, Z3);
|
|
43457
|
+
Z3 = Fp2.add(Z3, Z3);
|
|
43458
|
+
return new Point(X3, Y3, Z3);
|
|
43459
|
+
}
|
|
43460
|
+
// Renes-Costello-Batina exception-free addition formula.
|
|
43461
|
+
// There is 30% faster Jacobian formula, but it is not complete.
|
|
43462
|
+
// https://eprint.iacr.org/2015/1060, algorithm 1
|
|
43463
|
+
// Cost: 12M + 0S + 3*a + 3*b3 + 23add.
|
|
43464
|
+
add(other) {
|
|
43465
|
+
aprjpoint(other);
|
|
43466
|
+
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
43467
|
+
const { X: X2, Y: Y2, Z: Z2 } = other;
|
|
43468
|
+
let X3 = Fp2.ZERO, Y3 = Fp2.ZERO, Z3 = Fp2.ZERO;
|
|
43469
|
+
const a = CURVE.a;
|
|
43470
|
+
const b3 = Fp2.mul(CURVE.b, _3n3);
|
|
43471
|
+
let t0 = Fp2.mul(X1, X2);
|
|
43472
|
+
let t1 = Fp2.mul(Y1, Y2);
|
|
43473
|
+
let t2 = Fp2.mul(Z1, Z2);
|
|
43474
|
+
let t3 = Fp2.add(X1, Y1);
|
|
43475
|
+
let t4 = Fp2.add(X2, Y2);
|
|
43476
|
+
t3 = Fp2.mul(t3, t4);
|
|
43477
|
+
t4 = Fp2.add(t0, t1);
|
|
43478
|
+
t3 = Fp2.sub(t3, t4);
|
|
43479
|
+
t4 = Fp2.add(X1, Z1);
|
|
43480
|
+
let t5 = Fp2.add(X2, Z2);
|
|
43481
|
+
t4 = Fp2.mul(t4, t5);
|
|
43482
|
+
t5 = Fp2.add(t0, t2);
|
|
43483
|
+
t4 = Fp2.sub(t4, t5);
|
|
43484
|
+
t5 = Fp2.add(Y1, Z1);
|
|
43485
|
+
X3 = Fp2.add(Y2, Z2);
|
|
43486
|
+
t5 = Fp2.mul(t5, X3);
|
|
43487
|
+
X3 = Fp2.add(t1, t2);
|
|
43488
|
+
t5 = Fp2.sub(t5, X3);
|
|
43489
|
+
Z3 = Fp2.mul(a, t4);
|
|
43490
|
+
X3 = Fp2.mul(b3, t2);
|
|
43491
|
+
Z3 = Fp2.add(X3, Z3);
|
|
43492
|
+
X3 = Fp2.sub(t1, Z3);
|
|
43493
|
+
Z3 = Fp2.add(t1, Z3);
|
|
43494
|
+
Y3 = Fp2.mul(X3, Z3);
|
|
43495
|
+
t1 = Fp2.add(t0, t0);
|
|
43496
|
+
t1 = Fp2.add(t1, t0);
|
|
43497
|
+
t2 = Fp2.mul(a, t2);
|
|
43498
|
+
t4 = Fp2.mul(b3, t4);
|
|
43499
|
+
t1 = Fp2.add(t1, t2);
|
|
43500
|
+
t2 = Fp2.sub(t0, t2);
|
|
43501
|
+
t2 = Fp2.mul(a, t2);
|
|
43502
|
+
t4 = Fp2.add(t4, t2);
|
|
43503
|
+
t0 = Fp2.mul(t1, t4);
|
|
43504
|
+
Y3 = Fp2.add(Y3, t0);
|
|
43505
|
+
t0 = Fp2.mul(t5, t4);
|
|
43506
|
+
X3 = Fp2.mul(t3, X3);
|
|
43507
|
+
X3 = Fp2.sub(X3, t0);
|
|
43508
|
+
t0 = Fp2.mul(t3, t1);
|
|
43509
|
+
Z3 = Fp2.mul(t5, Z3);
|
|
43510
|
+
Z3 = Fp2.add(Z3, t0);
|
|
43511
|
+
return new Point(X3, Y3, Z3);
|
|
43512
|
+
}
|
|
43513
|
+
subtract(other) {
|
|
43514
|
+
return this.add(other.negate());
|
|
43515
|
+
}
|
|
43516
|
+
is0() {
|
|
43517
|
+
return this.equals(Point.ZERO);
|
|
43518
|
+
}
|
|
43519
|
+
/**
|
|
43520
|
+
* Constant time multiplication.
|
|
43521
|
+
* Uses wNAF method. Windowed method may be 10% faster,
|
|
43522
|
+
* but takes 2x longer to generate and consumes 2x memory.
|
|
43523
|
+
* Uses precomputes when available.
|
|
43524
|
+
* Uses endomorphism for Koblitz curves.
|
|
43525
|
+
* @param scalar by which the point would be multiplied
|
|
43526
|
+
* @returns New point
|
|
43527
|
+
*/
|
|
43528
|
+
multiply(scalar) {
|
|
43529
|
+
const { endo: endo2 } = extraOpts;
|
|
43530
|
+
if (!Fn2.isValidNot0(scalar))
|
|
43531
|
+
throw new Error("invalid scalar: out of range");
|
|
43532
|
+
let point, fake;
|
|
43533
|
+
const mul = (n) => wnaf.cached(this, n, (p) => normalizeZ(Point, p));
|
|
43534
|
+
if (endo2) {
|
|
43535
|
+
const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(scalar);
|
|
43536
|
+
const { p: k1p, f: k1f } = mul(k1);
|
|
43537
|
+
const { p: k2p, f: k2f } = mul(k2);
|
|
43538
|
+
fake = k1f.add(k2f);
|
|
43539
|
+
point = finishEndo(endo2.beta, k1p, k2p, k1neg, k2neg);
|
|
43540
|
+
} else {
|
|
43541
|
+
const { p, f } = mul(scalar);
|
|
43542
|
+
point = p;
|
|
43543
|
+
fake = f;
|
|
43544
|
+
}
|
|
43545
|
+
return normalizeZ(Point, [point, fake])[0];
|
|
43546
|
+
}
|
|
43547
|
+
/**
|
|
43548
|
+
* Non-constant-time multiplication. Uses double-and-add algorithm.
|
|
43549
|
+
* It's faster, but should only be used when you don't care about
|
|
43550
|
+
* an exposed secret key e.g. sig verification, which works over *public* keys.
|
|
43551
|
+
*/
|
|
43552
|
+
multiplyUnsafe(sc) {
|
|
43553
|
+
const { endo: endo2 } = extraOpts;
|
|
43554
|
+
const p = this;
|
|
43555
|
+
if (!Fn2.isValid(sc))
|
|
43556
|
+
throw new Error("invalid scalar: out of range");
|
|
43557
|
+
if (sc === _0n6 || p.is0())
|
|
43558
|
+
return Point.ZERO;
|
|
43559
|
+
if (sc === _1n6)
|
|
43560
|
+
return p;
|
|
43561
|
+
if (wnaf.hasCache(this))
|
|
43562
|
+
return this.multiply(sc);
|
|
43563
|
+
if (endo2) {
|
|
43564
|
+
const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(sc);
|
|
43565
|
+
const { p1, p2 } = mulEndoUnsafe(Point, p, k1, k2);
|
|
43566
|
+
return finishEndo(endo2.beta, p1, p2, k1neg, k2neg);
|
|
43567
|
+
} else {
|
|
43568
|
+
return wnaf.unsafe(p, sc);
|
|
43569
|
+
}
|
|
43570
|
+
}
|
|
43571
|
+
multiplyAndAddUnsafe(Q, a, b) {
|
|
43572
|
+
const sum = this.multiplyUnsafe(a).add(Q.multiplyUnsafe(b));
|
|
43573
|
+
return sum.is0() ? void 0 : sum;
|
|
43574
|
+
}
|
|
43575
|
+
/**
|
|
43576
|
+
* Converts Projective point to affine (x, y) coordinates.
|
|
43577
|
+
* @param invertedZ Z^-1 (inverted zero) - optional, precomputation is useful for invertBatch
|
|
43578
|
+
*/
|
|
43579
|
+
toAffine(invertedZ) {
|
|
43580
|
+
return toAffineMemo(this, invertedZ);
|
|
43581
|
+
}
|
|
43582
|
+
/**
|
|
43583
|
+
* Checks whether Point is free of torsion elements (is in prime subgroup).
|
|
43584
|
+
* Always torsion-free for cofactor=1 curves.
|
|
43585
|
+
*/
|
|
43586
|
+
isTorsionFree() {
|
|
43587
|
+
const { isTorsionFree } = extraOpts;
|
|
43588
|
+
if (cofactor === _1n6)
|
|
43589
|
+
return true;
|
|
43590
|
+
if (isTorsionFree)
|
|
43591
|
+
return isTorsionFree(Point, this);
|
|
43592
|
+
return wnaf.unsafe(this, CURVE_ORDER).is0();
|
|
43593
|
+
}
|
|
43594
|
+
clearCofactor() {
|
|
43595
|
+
const { clearCofactor } = extraOpts;
|
|
43596
|
+
if (cofactor === _1n6)
|
|
43597
|
+
return this;
|
|
43598
|
+
if (clearCofactor)
|
|
43599
|
+
return clearCofactor(Point, this);
|
|
43600
|
+
return this.multiplyUnsafe(cofactor);
|
|
43601
|
+
}
|
|
43602
|
+
isSmallOrder() {
|
|
43603
|
+
return this.multiplyUnsafe(cofactor).is0();
|
|
43604
|
+
}
|
|
43605
|
+
toBytes(isCompressed = true) {
|
|
43606
|
+
_abool2(isCompressed, "isCompressed");
|
|
43607
|
+
this.assertValidity();
|
|
43608
|
+
return encodePoint(Point, this, isCompressed);
|
|
43609
|
+
}
|
|
43610
|
+
toHex(isCompressed = true) {
|
|
43611
|
+
return bytesToHex(this.toBytes(isCompressed));
|
|
43612
|
+
}
|
|
43613
|
+
toString() {
|
|
43614
|
+
return `<Point ${this.is0() ? "ZERO" : this.toHex()}>`;
|
|
43615
|
+
}
|
|
43616
|
+
// TODO: remove
|
|
43617
|
+
get px() {
|
|
43618
|
+
return this.X;
|
|
43619
|
+
}
|
|
43620
|
+
get py() {
|
|
43621
|
+
return this.X;
|
|
43622
|
+
}
|
|
43623
|
+
get pz() {
|
|
43624
|
+
return this.Z;
|
|
43625
|
+
}
|
|
43626
|
+
toRawBytes(isCompressed = true) {
|
|
43627
|
+
return this.toBytes(isCompressed);
|
|
43628
|
+
}
|
|
43629
|
+
_setWindowSize(windowSize) {
|
|
43630
|
+
this.precompute(windowSize);
|
|
43631
|
+
}
|
|
43632
|
+
static normalizeZ(points) {
|
|
43633
|
+
return normalizeZ(Point, points);
|
|
43634
|
+
}
|
|
43635
|
+
static msm(points, scalars) {
|
|
43636
|
+
return pippenger(Point, Fn2, points, scalars);
|
|
43637
|
+
}
|
|
43638
|
+
static fromPrivateKey(privateKey) {
|
|
43639
|
+
return Point.BASE.multiply(_normFnElement(Fn2, privateKey));
|
|
43640
|
+
}
|
|
43641
|
+
}
|
|
43642
|
+
Point.BASE = new Point(CURVE.Gx, CURVE.Gy, Fp2.ONE);
|
|
43643
|
+
Point.ZERO = new Point(Fp2.ZERO, Fp2.ONE, Fp2.ZERO);
|
|
43644
|
+
Point.Fp = Fp2;
|
|
43645
|
+
Point.Fn = Fn2;
|
|
43646
|
+
const bits = Fn2.BITS;
|
|
43647
|
+
const wnaf = new wNAF(Point, extraOpts.endo ? Math.ceil(bits / 2) : bits);
|
|
43648
|
+
Point.BASE.precompute(8);
|
|
43649
|
+
return Point;
|
|
43650
|
+
}
|
|
43651
|
+
function pprefix(hasEvenY) {
|
|
43652
|
+
return Uint8Array.of(hasEvenY ? 2 : 3);
|
|
43653
|
+
}
|
|
43654
|
+
function getWLengths(Fp2, Fn2) {
|
|
43655
|
+
return {
|
|
43656
|
+
secretKey: Fn2.BYTES,
|
|
43657
|
+
publicKey: 1 + Fp2.BYTES,
|
|
43658
|
+
publicKeyUncompressed: 1 + 2 * Fp2.BYTES,
|
|
43659
|
+
publicKeyHasPrefix: true,
|
|
43660
|
+
signature: 2 * Fn2.BYTES
|
|
43661
|
+
};
|
|
43662
|
+
}
|
|
43663
|
+
function ecdh(Point, ecdhOpts = {}) {
|
|
43664
|
+
const { Fn: Fn2 } = Point;
|
|
43665
|
+
const randomBytes_ = ecdhOpts.randomBytes || randomBytes2;
|
|
43666
|
+
const lengths = Object.assign(getWLengths(Point.Fp, Fn2), { seed: getMinHashLength(Fn2.ORDER) });
|
|
43667
|
+
function isValidSecretKey(secretKey) {
|
|
43668
|
+
try {
|
|
43669
|
+
return !!_normFnElement(Fn2, secretKey);
|
|
43670
|
+
} catch (error) {
|
|
43671
|
+
return false;
|
|
43672
|
+
}
|
|
43673
|
+
}
|
|
43674
|
+
function isValidPublicKey(publicKey, isCompressed) {
|
|
43675
|
+
const { publicKey: comp, publicKeyUncompressed } = lengths;
|
|
43676
|
+
try {
|
|
43677
|
+
const l = publicKey.length;
|
|
43678
|
+
if (isCompressed === true && l !== comp)
|
|
43679
|
+
return false;
|
|
43680
|
+
if (isCompressed === false && l !== publicKeyUncompressed)
|
|
43681
|
+
return false;
|
|
43682
|
+
return !!Point.fromBytes(publicKey);
|
|
43683
|
+
} catch (error) {
|
|
43684
|
+
return false;
|
|
43685
|
+
}
|
|
43686
|
+
}
|
|
43687
|
+
function randomSecretKey(seed = randomBytes_(lengths.seed)) {
|
|
43688
|
+
return mapHashToField(_abytes2(seed, lengths.seed, "seed"), Fn2.ORDER);
|
|
43689
|
+
}
|
|
43690
|
+
function getPublicKey(secretKey, isCompressed = true) {
|
|
43691
|
+
return Point.BASE.multiply(_normFnElement(Fn2, secretKey)).toBytes(isCompressed);
|
|
43692
|
+
}
|
|
43693
|
+
function keygen(seed) {
|
|
43694
|
+
const secretKey = randomSecretKey(seed);
|
|
43695
|
+
return { secretKey, publicKey: getPublicKey(secretKey) };
|
|
43696
|
+
}
|
|
43697
|
+
function isProbPub(item) {
|
|
43698
|
+
if (typeof item === "bigint")
|
|
43699
|
+
return false;
|
|
43700
|
+
if (item instanceof Point)
|
|
43701
|
+
return true;
|
|
43702
|
+
const { secretKey, publicKey, publicKeyUncompressed } = lengths;
|
|
43703
|
+
if (Fn2.allowedLengths || secretKey === publicKey)
|
|
43704
|
+
return void 0;
|
|
43705
|
+
const l = ensureBytes("key", item).length;
|
|
43706
|
+
return l === publicKey || l === publicKeyUncompressed;
|
|
43707
|
+
}
|
|
43708
|
+
function getSharedSecret(secretKeyA, publicKeyB, isCompressed = true) {
|
|
43709
|
+
if (isProbPub(secretKeyA) === true)
|
|
43710
|
+
throw new Error("first arg must be private key");
|
|
43711
|
+
if (isProbPub(publicKeyB) === false)
|
|
43712
|
+
throw new Error("second arg must be public key");
|
|
43713
|
+
const s = _normFnElement(Fn2, secretKeyA);
|
|
43714
|
+
const b = Point.fromHex(publicKeyB);
|
|
43715
|
+
return b.multiply(s).toBytes(isCompressed);
|
|
43716
|
+
}
|
|
43717
|
+
const utils = {
|
|
43718
|
+
isValidSecretKey,
|
|
43719
|
+
isValidPublicKey,
|
|
43720
|
+
randomSecretKey,
|
|
43721
|
+
// TODO: remove
|
|
43722
|
+
isValidPrivateKey: isValidSecretKey,
|
|
43723
|
+
randomPrivateKey: randomSecretKey,
|
|
43724
|
+
normPrivateKeyToScalar: (key) => _normFnElement(Fn2, key),
|
|
43725
|
+
precompute(windowSize = 8, point = Point.BASE) {
|
|
43726
|
+
return point.precompute(windowSize, false);
|
|
43727
|
+
}
|
|
43728
|
+
};
|
|
43729
|
+
return Object.freeze({ getPublicKey, getSharedSecret, keygen, Point, utils, lengths });
|
|
43730
|
+
}
|
|
43731
|
+
function ecdsa(Point, hash, ecdsaOpts = {}) {
|
|
43732
|
+
ahash(hash);
|
|
43733
|
+
_validateObject(ecdsaOpts, {}, {
|
|
43734
|
+
hmac: "function",
|
|
43735
|
+
lowS: "boolean",
|
|
43736
|
+
randomBytes: "function",
|
|
43737
|
+
bits2int: "function",
|
|
43738
|
+
bits2int_modN: "function"
|
|
43739
|
+
});
|
|
43740
|
+
const randomBytes6 = ecdsaOpts.randomBytes || randomBytes2;
|
|
43741
|
+
const hmac2 = ecdsaOpts.hmac || ((key, ...msgs) => hmac(hash, key, concatBytes(...msgs)));
|
|
43742
|
+
const { Fp: Fp2, Fn: Fn2 } = Point;
|
|
43743
|
+
const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn2;
|
|
43744
|
+
const { keygen, getPublicKey, getSharedSecret, utils, lengths } = ecdh(Point, ecdsaOpts);
|
|
43745
|
+
const defaultSigOpts = {
|
|
43746
|
+
prehash: false,
|
|
43747
|
+
lowS: typeof ecdsaOpts.lowS === "boolean" ? ecdsaOpts.lowS : false,
|
|
43748
|
+
format: void 0,
|
|
43749
|
+
//'compact' as ECDSASigFormat,
|
|
43750
|
+
extraEntropy: false
|
|
43751
|
+
};
|
|
43752
|
+
const defaultSigOpts_format = "compact";
|
|
43753
|
+
function isBiggerThanHalfOrder(number) {
|
|
43754
|
+
const HALF = CURVE_ORDER >> _1n6;
|
|
43755
|
+
return number > HALF;
|
|
43756
|
+
}
|
|
43757
|
+
function validateRS(title, num) {
|
|
43758
|
+
if (!Fn2.isValidNot0(num))
|
|
43759
|
+
throw new Error(`invalid signature ${title}: out of range 1..Point.Fn.ORDER`);
|
|
43760
|
+
return num;
|
|
43761
|
+
}
|
|
43762
|
+
function validateSigLength(bytes, format) {
|
|
43763
|
+
validateSigFormat(format);
|
|
43764
|
+
const size = lengths.signature;
|
|
43765
|
+
const sizer = format === "compact" ? size : format === "recovered" ? size + 1 : void 0;
|
|
43766
|
+
return _abytes2(bytes, sizer, `${format} signature`);
|
|
43767
|
+
}
|
|
43768
|
+
class Signature {
|
|
43769
|
+
constructor(r, s, recovery) {
|
|
43770
|
+
this.r = validateRS("r", r);
|
|
43771
|
+
this.s = validateRS("s", s);
|
|
43772
|
+
if (recovery != null)
|
|
43773
|
+
this.recovery = recovery;
|
|
43774
|
+
Object.freeze(this);
|
|
43775
|
+
}
|
|
43776
|
+
static fromBytes(bytes, format = defaultSigOpts_format) {
|
|
43777
|
+
validateSigLength(bytes, format);
|
|
43778
|
+
let recid;
|
|
43779
|
+
if (format === "der") {
|
|
43780
|
+
const { r: r2, s: s2 } = DER.toSig(_abytes2(bytes));
|
|
43781
|
+
return new Signature(r2, s2);
|
|
43782
|
+
}
|
|
43783
|
+
if (format === "recovered") {
|
|
43784
|
+
recid = bytes[0];
|
|
43785
|
+
format = "compact";
|
|
43786
|
+
bytes = bytes.subarray(1);
|
|
43787
|
+
}
|
|
43788
|
+
const L = Fn2.BYTES;
|
|
43789
|
+
const r = bytes.subarray(0, L);
|
|
43790
|
+
const s = bytes.subarray(L, L * 2);
|
|
43791
|
+
return new Signature(Fn2.fromBytes(r), Fn2.fromBytes(s), recid);
|
|
43792
|
+
}
|
|
43793
|
+
static fromHex(hex, format) {
|
|
43794
|
+
return this.fromBytes(hexToBytes(hex), format);
|
|
43795
|
+
}
|
|
43796
|
+
addRecoveryBit(recovery) {
|
|
43797
|
+
return new Signature(this.r, this.s, recovery);
|
|
43798
|
+
}
|
|
43799
|
+
recoverPublicKey(messageHash) {
|
|
43800
|
+
const FIELD_ORDER = Fp2.ORDER;
|
|
43801
|
+
const { r, s, recovery: rec } = this;
|
|
43802
|
+
if (rec == null || ![0, 1, 2, 3].includes(rec))
|
|
43803
|
+
throw new Error("recovery id invalid");
|
|
43804
|
+
const hasCofactor = CURVE_ORDER * _2n4 < FIELD_ORDER;
|
|
43805
|
+
if (hasCofactor && rec > 1)
|
|
43806
|
+
throw new Error("recovery id is ambiguous for h>1 curve");
|
|
43807
|
+
const radj = rec === 2 || rec === 3 ? r + CURVE_ORDER : r;
|
|
43808
|
+
if (!Fp2.isValid(radj))
|
|
43809
|
+
throw new Error("recovery id 2 or 3 invalid");
|
|
43810
|
+
const x = Fp2.toBytes(radj);
|
|
43811
|
+
const R = Point.fromBytes(concatBytes(pprefix((rec & 1) === 0), x));
|
|
43812
|
+
const ir = Fn2.inv(radj);
|
|
43813
|
+
const h = bits2int_modN(ensureBytes("msgHash", messageHash));
|
|
43814
|
+
const u1 = Fn2.create(-h * ir);
|
|
43815
|
+
const u2 = Fn2.create(s * ir);
|
|
43816
|
+
const Q = Point.BASE.multiplyUnsafe(u1).add(R.multiplyUnsafe(u2));
|
|
43817
|
+
if (Q.is0())
|
|
43818
|
+
throw new Error("point at infinify");
|
|
43819
|
+
Q.assertValidity();
|
|
43820
|
+
return Q;
|
|
43821
|
+
}
|
|
43822
|
+
// Signatures should be low-s, to prevent malleability.
|
|
43823
|
+
hasHighS() {
|
|
43824
|
+
return isBiggerThanHalfOrder(this.s);
|
|
43825
|
+
}
|
|
43826
|
+
toBytes(format = defaultSigOpts_format) {
|
|
43827
|
+
validateSigFormat(format);
|
|
43828
|
+
if (format === "der")
|
|
43829
|
+
return hexToBytes(DER.hexFromSig(this));
|
|
43830
|
+
const r = Fn2.toBytes(this.r);
|
|
43831
|
+
const s = Fn2.toBytes(this.s);
|
|
43832
|
+
if (format === "recovered") {
|
|
43833
|
+
if (this.recovery == null)
|
|
43834
|
+
throw new Error("recovery bit must be present");
|
|
43835
|
+
return concatBytes(Uint8Array.of(this.recovery), r, s);
|
|
43836
|
+
}
|
|
43837
|
+
return concatBytes(r, s);
|
|
43838
|
+
}
|
|
43839
|
+
toHex(format) {
|
|
43840
|
+
return bytesToHex(this.toBytes(format));
|
|
43841
|
+
}
|
|
43842
|
+
// TODO: remove
|
|
43843
|
+
assertValidity() {
|
|
43844
|
+
}
|
|
43845
|
+
static fromCompact(hex) {
|
|
43846
|
+
return Signature.fromBytes(ensureBytes("sig", hex), "compact");
|
|
43847
|
+
}
|
|
43848
|
+
static fromDER(hex) {
|
|
43849
|
+
return Signature.fromBytes(ensureBytes("sig", hex), "der");
|
|
43850
|
+
}
|
|
43851
|
+
normalizeS() {
|
|
43852
|
+
return this.hasHighS() ? new Signature(this.r, Fn2.neg(this.s), this.recovery) : this;
|
|
43853
|
+
}
|
|
43854
|
+
toDERRawBytes() {
|
|
43855
|
+
return this.toBytes("der");
|
|
43856
|
+
}
|
|
43857
|
+
toDERHex() {
|
|
43858
|
+
return bytesToHex(this.toBytes("der"));
|
|
43859
|
+
}
|
|
43860
|
+
toCompactRawBytes() {
|
|
43861
|
+
return this.toBytes("compact");
|
|
43862
|
+
}
|
|
43863
|
+
toCompactHex() {
|
|
43864
|
+
return bytesToHex(this.toBytes("compact"));
|
|
43865
|
+
}
|
|
43866
|
+
}
|
|
43867
|
+
const bits2int = ecdsaOpts.bits2int || function bits2int_def(bytes) {
|
|
43868
|
+
if (bytes.length > 8192)
|
|
43869
|
+
throw new Error("input is too large");
|
|
43870
|
+
const num = bytesToNumberBE(bytes);
|
|
43871
|
+
const delta = bytes.length * 8 - fnBits;
|
|
43872
|
+
return delta > 0 ? num >> BigInt(delta) : num;
|
|
43873
|
+
};
|
|
43874
|
+
const bits2int_modN = ecdsaOpts.bits2int_modN || function bits2int_modN_def(bytes) {
|
|
43875
|
+
return Fn2.create(bits2int(bytes));
|
|
43876
|
+
};
|
|
43877
|
+
const ORDER_MASK = bitMask(fnBits);
|
|
43878
|
+
function int2octets(num) {
|
|
43879
|
+
aInRange("num < 2^" + fnBits, num, _0n6, ORDER_MASK);
|
|
43880
|
+
return Fn2.toBytes(num);
|
|
43881
|
+
}
|
|
43882
|
+
function validateMsgAndHash(message, prehash) {
|
|
43883
|
+
_abytes2(message, void 0, "message");
|
|
43884
|
+
return prehash ? _abytes2(hash(message), void 0, "prehashed message") : message;
|
|
43885
|
+
}
|
|
43886
|
+
function prepSig(message, privateKey, opts) {
|
|
43887
|
+
if (["recovered", "canonical"].some((k) => k in opts))
|
|
43888
|
+
throw new Error("sign() legacy options not supported");
|
|
43889
|
+
const { lowS, prehash, extraEntropy } = validateSigOpts(opts, defaultSigOpts);
|
|
43890
|
+
message = validateMsgAndHash(message, prehash);
|
|
43891
|
+
const h1int = bits2int_modN(message);
|
|
43892
|
+
const d = _normFnElement(Fn2, privateKey);
|
|
43893
|
+
const seedArgs = [int2octets(d), int2octets(h1int)];
|
|
43894
|
+
if (extraEntropy != null && extraEntropy !== false) {
|
|
43895
|
+
const e = extraEntropy === true ? randomBytes6(lengths.secretKey) : extraEntropy;
|
|
43896
|
+
seedArgs.push(ensureBytes("extraEntropy", e));
|
|
43897
|
+
}
|
|
43898
|
+
const seed = concatBytes(...seedArgs);
|
|
43899
|
+
const m = h1int;
|
|
43900
|
+
function k2sig(kBytes) {
|
|
43901
|
+
const k = bits2int(kBytes);
|
|
43902
|
+
if (!Fn2.isValidNot0(k))
|
|
43903
|
+
return;
|
|
43904
|
+
const ik = Fn2.inv(k);
|
|
43905
|
+
const q = Point.BASE.multiply(k).toAffine();
|
|
43906
|
+
const r = Fn2.create(q.x);
|
|
43907
|
+
if (r === _0n6)
|
|
43908
|
+
return;
|
|
43909
|
+
const s = Fn2.create(ik * Fn2.create(m + r * d));
|
|
43910
|
+
if (s === _0n6)
|
|
43911
|
+
return;
|
|
43912
|
+
let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n6);
|
|
43913
|
+
let normS = s;
|
|
43914
|
+
if (lowS && isBiggerThanHalfOrder(s)) {
|
|
43915
|
+
normS = Fn2.neg(s);
|
|
43916
|
+
recovery ^= 1;
|
|
43917
|
+
}
|
|
43918
|
+
return new Signature(r, normS, recovery);
|
|
43919
|
+
}
|
|
43920
|
+
return { seed, k2sig };
|
|
43921
|
+
}
|
|
43922
|
+
function sign(message, secretKey, opts = {}) {
|
|
43923
|
+
message = ensureBytes("message", message);
|
|
43924
|
+
const { seed, k2sig } = prepSig(message, secretKey, opts);
|
|
43925
|
+
const drbg = createHmacDrbg(hash.outputLen, Fn2.BYTES, hmac2);
|
|
43926
|
+
const sig = drbg(seed, k2sig);
|
|
43927
|
+
return sig;
|
|
43928
|
+
}
|
|
43929
|
+
function tryParsingSig(sg) {
|
|
43930
|
+
let sig = void 0;
|
|
43931
|
+
const isHex = typeof sg === "string" || isBytes(sg);
|
|
43932
|
+
const isObj = !isHex && sg !== null && typeof sg === "object" && typeof sg.r === "bigint" && typeof sg.s === "bigint";
|
|
43933
|
+
if (!isHex && !isObj)
|
|
43934
|
+
throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");
|
|
43935
|
+
if (isObj) {
|
|
43936
|
+
sig = new Signature(sg.r, sg.s);
|
|
43937
|
+
} else if (isHex) {
|
|
43938
|
+
try {
|
|
43939
|
+
sig = Signature.fromBytes(ensureBytes("sig", sg), "der");
|
|
43940
|
+
} catch (derError) {
|
|
43941
|
+
if (!(derError instanceof DER.Err))
|
|
43942
|
+
throw derError;
|
|
43943
|
+
}
|
|
43944
|
+
if (!sig) {
|
|
43945
|
+
try {
|
|
43946
|
+
sig = Signature.fromBytes(ensureBytes("sig", sg), "compact");
|
|
43947
|
+
} catch (error) {
|
|
43948
|
+
return false;
|
|
43949
|
+
}
|
|
43950
|
+
}
|
|
43951
|
+
}
|
|
43952
|
+
if (!sig)
|
|
43953
|
+
return false;
|
|
43954
|
+
return sig;
|
|
43955
|
+
}
|
|
43956
|
+
function verify(signature, message, publicKey, opts = {}) {
|
|
43957
|
+
const { lowS, prehash, format } = validateSigOpts(opts, defaultSigOpts);
|
|
43958
|
+
publicKey = ensureBytes("publicKey", publicKey);
|
|
43959
|
+
message = validateMsgAndHash(ensureBytes("message", message), prehash);
|
|
43960
|
+
if ("strict" in opts)
|
|
43961
|
+
throw new Error("options.strict was renamed to lowS");
|
|
43962
|
+
const sig = format === void 0 ? tryParsingSig(signature) : Signature.fromBytes(ensureBytes("sig", signature), format);
|
|
43963
|
+
if (sig === false)
|
|
43964
|
+
return false;
|
|
43965
|
+
try {
|
|
43966
|
+
const P = Point.fromBytes(publicKey);
|
|
43967
|
+
if (lowS && sig.hasHighS())
|
|
43968
|
+
return false;
|
|
43969
|
+
const { r, s } = sig;
|
|
43970
|
+
const h = bits2int_modN(message);
|
|
43971
|
+
const is = Fn2.inv(s);
|
|
43972
|
+
const u1 = Fn2.create(h * is);
|
|
43973
|
+
const u2 = Fn2.create(r * is);
|
|
43974
|
+
const R = Point.BASE.multiplyUnsafe(u1).add(P.multiplyUnsafe(u2));
|
|
43975
|
+
if (R.is0())
|
|
43976
|
+
return false;
|
|
43977
|
+
const v = Fn2.create(R.x);
|
|
43978
|
+
return v === r;
|
|
43979
|
+
} catch (e) {
|
|
43980
|
+
return false;
|
|
43981
|
+
}
|
|
43982
|
+
}
|
|
43983
|
+
function recoverPublicKey(signature, message, opts = {}) {
|
|
43984
|
+
const { prehash } = validateSigOpts(opts, defaultSigOpts);
|
|
43985
|
+
message = validateMsgAndHash(message, prehash);
|
|
43986
|
+
return Signature.fromBytes(signature, "recovered").recoverPublicKey(message).toBytes();
|
|
43987
|
+
}
|
|
43988
|
+
return Object.freeze({
|
|
43989
|
+
keygen,
|
|
43990
|
+
getPublicKey,
|
|
43991
|
+
getSharedSecret,
|
|
43992
|
+
utils,
|
|
43993
|
+
lengths,
|
|
43994
|
+
Point,
|
|
43995
|
+
sign,
|
|
43996
|
+
verify,
|
|
43997
|
+
recoverPublicKey,
|
|
43998
|
+
Signature,
|
|
43999
|
+
hash
|
|
44000
|
+
});
|
|
44001
|
+
}
|
|
44002
|
+
function _weierstrass_legacy_opts_to_new(c) {
|
|
44003
|
+
const CURVE = {
|
|
44004
|
+
a: c.a,
|
|
44005
|
+
b: c.b,
|
|
44006
|
+
p: c.Fp.ORDER,
|
|
44007
|
+
n: c.n,
|
|
44008
|
+
h: c.h,
|
|
44009
|
+
Gx: c.Gx,
|
|
44010
|
+
Gy: c.Gy
|
|
44011
|
+
};
|
|
44012
|
+
const Fp2 = c.Fp;
|
|
44013
|
+
let allowedLengths = c.allowedPrivateKeyLengths ? Array.from(new Set(c.allowedPrivateKeyLengths.map((l) => Math.ceil(l / 2)))) : void 0;
|
|
44014
|
+
const Fn2 = Field(CURVE.n, {
|
|
44015
|
+
BITS: c.nBitLength,
|
|
44016
|
+
allowedLengths,
|
|
44017
|
+
modFromBytes: c.wrapPrivateKey
|
|
44018
|
+
});
|
|
44019
|
+
const curveOpts = {
|
|
44020
|
+
Fp: Fp2,
|
|
44021
|
+
Fn: Fn2,
|
|
44022
|
+
allowInfinityPoint: c.allowInfinityPoint,
|
|
44023
|
+
endo: c.endo,
|
|
44024
|
+
isTorsionFree: c.isTorsionFree,
|
|
44025
|
+
clearCofactor: c.clearCofactor,
|
|
44026
|
+
fromBytes: c.fromBytes,
|
|
44027
|
+
toBytes: c.toBytes
|
|
44028
|
+
};
|
|
44029
|
+
return { CURVE, curveOpts };
|
|
44030
|
+
}
|
|
44031
|
+
function _ecdsa_legacy_opts_to_new(c) {
|
|
44032
|
+
const { CURVE, curveOpts } = _weierstrass_legacy_opts_to_new(c);
|
|
44033
|
+
const ecdsaOpts = {
|
|
44034
|
+
hmac: c.hmac,
|
|
44035
|
+
randomBytes: c.randomBytes,
|
|
44036
|
+
lowS: c.lowS,
|
|
44037
|
+
bits2int: c.bits2int,
|
|
44038
|
+
bits2int_modN: c.bits2int_modN
|
|
44039
|
+
};
|
|
44040
|
+
return { CURVE, curveOpts, hash: c.hash, ecdsaOpts };
|
|
44041
|
+
}
|
|
44042
|
+
function _ecdsa_new_output_to_legacy(c, _ecdsa) {
|
|
44043
|
+
const Point = _ecdsa.Point;
|
|
44044
|
+
return Object.assign({}, _ecdsa, {
|
|
44045
|
+
ProjectivePoint: Point,
|
|
44046
|
+
CURVE: Object.assign({}, c, nLength(Point.Fn.ORDER, Point.Fn.BITS))
|
|
44047
|
+
});
|
|
44048
|
+
}
|
|
44049
|
+
function weierstrass(c) {
|
|
44050
|
+
const { CURVE, curveOpts, hash, ecdsaOpts } = _ecdsa_legacy_opts_to_new(c);
|
|
44051
|
+
const Point = weierstrassN(CURVE, curveOpts);
|
|
44052
|
+
const signs = ecdsa(Point, hash, ecdsaOpts);
|
|
44053
|
+
return _ecdsa_new_output_to_legacy(c, signs);
|
|
44054
|
+
}
|
|
44055
|
+
|
|
44056
|
+
// node_modules/@noble/curves/esm/_shortw_utils.js
|
|
44057
|
+
function createCurve(curveDef, defHash) {
|
|
44058
|
+
const create = (hash) => weierstrass({ ...curveDef, hash });
|
|
44059
|
+
return { ...create(defHash), create };
|
|
44060
|
+
}
|
|
44061
|
+
|
|
44062
|
+
// node_modules/@noble/curves/esm/nist.js
|
|
44063
|
+
var p256_CURVE = {
|
|
44064
|
+
p: BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),
|
|
44065
|
+
n: BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),
|
|
44066
|
+
h: BigInt(1),
|
|
44067
|
+
a: BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc"),
|
|
44068
|
+
b: BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b"),
|
|
44069
|
+
Gx: BigInt("0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),
|
|
44070
|
+
Gy: BigInt("0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")
|
|
44071
|
+
};
|
|
44072
|
+
var p384_CURVE = {
|
|
44073
|
+
p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"),
|
|
44074
|
+
n: BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"),
|
|
44075
|
+
h: BigInt(1),
|
|
44076
|
+
a: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"),
|
|
44077
|
+
b: BigInt("0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"),
|
|
44078
|
+
Gx: BigInt("0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"),
|
|
44079
|
+
Gy: BigInt("0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f")
|
|
44080
|
+
};
|
|
44081
|
+
var p521_CURVE = {
|
|
44082
|
+
p: BigInt("0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
|
|
44083
|
+
n: BigInt("0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"),
|
|
44084
|
+
h: BigInt(1),
|
|
44085
|
+
a: BigInt("0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"),
|
|
44086
|
+
b: BigInt("0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"),
|
|
44087
|
+
Gx: BigInt("0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"),
|
|
44088
|
+
Gy: BigInt("0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650")
|
|
44089
|
+
};
|
|
44090
|
+
var Fp256 = Field(p256_CURVE.p);
|
|
44091
|
+
var Fp384 = Field(p384_CURVE.p);
|
|
44092
|
+
var Fp521 = Field(p521_CURVE.p);
|
|
44093
|
+
var p256 = createCurve({ ...p256_CURVE, Fp: Fp256, lowS: false }, sha256);
|
|
44094
|
+
var p384 = createCurve({ ...p384_CURVE, Fp: Fp384, lowS: false }, sha384);
|
|
44095
|
+
var p521 = createCurve({ ...p521_CURVE, Fp: Fp521, lowS: false, allowedPrivateKeyLengths: [130, 131, 132] }, sha512);
|
|
44096
|
+
|
|
44097
|
+
// node_modules/@noble/curves/esm/p256.js
|
|
44098
|
+
var p2562 = p256;
|
|
44099
|
+
|
|
44100
|
+
// src/webauthn-approval.ts
|
|
41641
44101
|
function createApprovalChallenge(requestId, toolName, agentId, rpId = "scopeblind.com", timeoutSeconds = 300) {
|
|
41642
|
-
const challengeBytes = (0,
|
|
41643
|
-
const contextHash = (0,
|
|
44102
|
+
const challengeBytes = (0, import_node_crypto8.randomBytes)(32);
|
|
44103
|
+
const contextHash = (0, import_node_crypto8.createHash)("sha256").update(JSON.stringify({ requestId, toolName, agentId, timestamp: Date.now() })).digest("hex");
|
|
41644
44104
|
return {
|
|
41645
44105
|
challenge: base64urlEncode(challengeBytes),
|
|
41646
44106
|
requestId,
|
|
@@ -41655,57 +44115,86 @@ function createApprovalChallenge(requestId, toolName, agentId, rpId = "scopeblin
|
|
|
41655
44115
|
function toCredentialRequestOptions(challenge, allowCredentials) {
|
|
41656
44116
|
return {
|
|
41657
44117
|
publicKey: {
|
|
41658
|
-
challenge:
|
|
44118
|
+
challenge: base64urlDecode2(challenge.challenge).buffer,
|
|
41659
44119
|
rpId: challenge.rpId,
|
|
41660
44120
|
timeout: challenge.timeoutSeconds * 1e3,
|
|
41661
44121
|
userVerification: "required",
|
|
41662
44122
|
// Always require biometric
|
|
41663
44123
|
...allowCredentials ? {
|
|
41664
44124
|
allowCredentials: allowCredentials.map((c) => ({
|
|
41665
|
-
id:
|
|
44125
|
+
id: base64urlDecode2(c.id).buffer,
|
|
41666
44126
|
type: "public-key"
|
|
41667
44127
|
}))
|
|
41668
44128
|
} : {}
|
|
41669
44129
|
}
|
|
41670
44130
|
};
|
|
41671
44131
|
}
|
|
41672
|
-
function verifyApprovalAssertion(challenge, assertion) {
|
|
44132
|
+
function verifyApprovalAssertion(challenge, assertion, credentialPublicKey, opts = {}) {
|
|
44133
|
+
const now = opts.now ?? Date.now();
|
|
44134
|
+
const fail = (reason, partial = {}) => ({
|
|
44135
|
+
valid: false,
|
|
44136
|
+
reason,
|
|
44137
|
+
credentialId: assertion.credentialId,
|
|
44138
|
+
authenticatorType: "unknown",
|
|
44139
|
+
userVerified: false,
|
|
44140
|
+
signCount: 0,
|
|
44141
|
+
contextHash: challenge.contextHash,
|
|
44142
|
+
approvedAt: new Date(now).toISOString(),
|
|
44143
|
+
...partial
|
|
44144
|
+
});
|
|
41673
44145
|
const createdAt = new Date(challenge.createdAt).getTime();
|
|
41674
|
-
|
|
41675
|
-
if (
|
|
41676
|
-
|
|
41677
|
-
|
|
41678
|
-
|
|
41679
|
-
|
|
41680
|
-
|
|
41681
|
-
|
|
41682
|
-
|
|
41683
|
-
|
|
41684
|
-
|
|
41685
|
-
}
|
|
41686
|
-
|
|
44146
|
+
if (now - createdAt > challenge.timeoutSeconds * 1e3) return fail("challenge_expired");
|
|
44147
|
+
if (!credentialPublicKey?.publicKeyHex) return fail("missing_credential_public_key");
|
|
44148
|
+
const clientDataBytes = base64urlDecode2(assertion.clientDataJSON);
|
|
44149
|
+
let clientData;
|
|
44150
|
+
try {
|
|
44151
|
+
clientData = JSON.parse(Buffer.from(clientDataBytes).toString("utf8"));
|
|
44152
|
+
} catch {
|
|
44153
|
+
return fail("client_data_parse_error");
|
|
44154
|
+
}
|
|
44155
|
+
if (clientData.type !== "webauthn.get") return fail("wrong_client_data_type");
|
|
44156
|
+
if (!constantTimeStrEqual(clientData.challenge ?? "", challenge.challenge)) return fail("challenge_mismatch");
|
|
44157
|
+
const allowedOrigins = opts.expectedOrigin ? Array.isArray(opts.expectedOrigin) ? opts.expectedOrigin : [opts.expectedOrigin] : [`https://${challenge.rpId}`];
|
|
44158
|
+
if (!clientData.origin || !allowedOrigins.includes(clientData.origin)) return fail("origin_mismatch");
|
|
44159
|
+
const authData = base64urlDecode2(assertion.authenticatorData);
|
|
44160
|
+
if (authData.length < 37) return fail("authenticator_data_too_short");
|
|
44161
|
+
const rpIdHash = authData.slice(0, 32);
|
|
44162
|
+
const expectedRpIdHash = sha2562(new TextEncoder().encode(challenge.rpId));
|
|
44163
|
+
if (!bytesEqual(rpIdHash, expectedRpIdHash)) return fail("rp_id_hash_mismatch");
|
|
41687
44164
|
const flags = authData[32];
|
|
41688
44165
|
const userPresent = !!(flags & 1);
|
|
41689
44166
|
const userVerified = !!(flags & 4);
|
|
41690
|
-
|
|
41691
|
-
|
|
41692
|
-
|
|
44167
|
+
if (!userPresent) return fail("user_not_present");
|
|
44168
|
+
if ((opts.requireUserVerification ?? true) && !userVerified) return fail("user_verification_required", { userVerified });
|
|
44169
|
+
const signCount = authData[33] << 24 | authData[34] << 16 | authData[35] << 8 | authData[36];
|
|
44170
|
+
if (typeof opts.prevSignCount === "number" && signCount !== 0 && signCount <= opts.prevSignCount) {
|
|
44171
|
+
return fail("sign_count_regression", { userVerified, signCount });
|
|
44172
|
+
}
|
|
44173
|
+
const signedData = concatBytes2(authData, sha2562(clientDataBytes));
|
|
44174
|
+
const sigBytes = base64urlDecode2(assertion.signature);
|
|
44175
|
+
let sigOk = false;
|
|
41693
44176
|
try {
|
|
41694
|
-
|
|
41695
|
-
|
|
41696
|
-
|
|
44177
|
+
if (credentialPublicKey.alg === -7) {
|
|
44178
|
+
sigOk = p2562.verify(sigBytes, sha2562(signedData), hexToBytes(credentialPublicKey.publicKeyHex), { format: "der" });
|
|
44179
|
+
} else if (credentialPublicKey.alg === -8) {
|
|
44180
|
+
sigOk = ed25519.verify(sigBytes, signedData, hexToBytes(credentialPublicKey.publicKeyHex));
|
|
44181
|
+
} else {
|
|
44182
|
+
return fail("unsupported_algorithm", { userVerified, signCount });
|
|
41697
44183
|
}
|
|
41698
44184
|
} catch {
|
|
44185
|
+
sigOk = false;
|
|
41699
44186
|
}
|
|
44187
|
+
if (!sigOk) return fail("invalid_signature", { userVerified, signCount });
|
|
41700
44188
|
return {
|
|
41701
|
-
valid:
|
|
41702
|
-
// At minimum, user must be present
|
|
44189
|
+
valid: true,
|
|
41703
44190
|
credentialId: assertion.credentialId,
|
|
41704
|
-
|
|
44191
|
+
// Heuristic: platform authenticators (TouchID/FaceID/Hello) report UV; roaming
|
|
44192
|
+
// keys without a PIN are UP-only. Attachment is authoritative only at registration.
|
|
44193
|
+
authenticatorType: userVerified ? "platform" : "cross-platform",
|
|
41705
44194
|
userVerified,
|
|
41706
44195
|
signCount,
|
|
41707
44196
|
contextHash: challenge.contextHash,
|
|
41708
|
-
approvedAt:
|
|
44197
|
+
approvedAt: new Date(now).toISOString()
|
|
41709
44198
|
};
|
|
41710
44199
|
}
|
|
41711
44200
|
function createApprovalReceiptPayload(challenge, result) {
|
|
@@ -41720,17 +44209,33 @@ function createApprovalReceiptPayload(challenge, result) {
|
|
|
41720
44209
|
context_hash: result.contextHash,
|
|
41721
44210
|
approved_at: result.approvedAt,
|
|
41722
44211
|
// Hash the credential ID for privacy — don't store the raw ID
|
|
41723
|
-
credential_id_hash: (0,
|
|
44212
|
+
credential_id_hash: (0, import_node_crypto8.createHash)("sha256").update(result.credentialId).digest("hex").slice(0, 16)
|
|
41724
44213
|
};
|
|
41725
44214
|
}
|
|
41726
44215
|
function base64urlEncode(buffer) {
|
|
41727
44216
|
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
41728
44217
|
}
|
|
41729
|
-
function
|
|
44218
|
+
function base64urlDecode2(str) {
|
|
41730
44219
|
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
41731
44220
|
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
41732
44221
|
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
41733
44222
|
}
|
|
44223
|
+
function concatBytes2(a, b) {
|
|
44224
|
+
const out = new Uint8Array(a.length + b.length);
|
|
44225
|
+
out.set(a, 0);
|
|
44226
|
+
out.set(b, a.length);
|
|
44227
|
+
return out;
|
|
44228
|
+
}
|
|
44229
|
+
function bytesEqual(a, b) {
|
|
44230
|
+
if (a.length !== b.length) return false;
|
|
44231
|
+
return (0, import_node_crypto8.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
|
|
44232
|
+
}
|
|
44233
|
+
function constantTimeStrEqual(a, b) {
|
|
44234
|
+
const ab = Buffer.from(a, "utf8");
|
|
44235
|
+
const bb = Buffer.from(b, "utf8");
|
|
44236
|
+
if (ab.length !== bb.length) return false;
|
|
44237
|
+
return (0, import_node_crypto8.timingSafeEqual)(ab, bb);
|
|
44238
|
+
}
|
|
41734
44239
|
|
|
41735
44240
|
// src/did-vc.ts
|
|
41736
44241
|
function ed25519ToDIDKey(publicKeyHex) {
|
|
@@ -41974,7 +44479,7 @@ function evaluatePolicy(tool, policy) {
|
|
|
41974
44479
|
}
|
|
41975
44480
|
|
|
41976
44481
|
// src/evidence-authenticity.ts
|
|
41977
|
-
var
|
|
44482
|
+
var import_node_crypto9 = require("crypto");
|
|
41978
44483
|
async function createEvidenceAttestation(input) {
|
|
41979
44484
|
const tlsNotaryAvailable = await isTLSNotaryAvailable();
|
|
41980
44485
|
if (tlsNotaryAvailable) {
|
|
@@ -42034,14 +44539,14 @@ async function verifyEvidenceAttestation(attestation) {
|
|
|
42034
44539
|
}
|
|
42035
44540
|
}
|
|
42036
44541
|
function hashResponseBody(body) {
|
|
42037
|
-
return (0,
|
|
44542
|
+
return (0, import_node_crypto9.createHash)("sha256").update(typeof body === "string" ? body : body).digest("hex");
|
|
42038
44543
|
}
|
|
42039
44544
|
function createAttestationField(attestation) {
|
|
42040
44545
|
return {
|
|
42041
44546
|
evidence_authenticity: {
|
|
42042
44547
|
version: attestation.version,
|
|
42043
44548
|
method: attestation.method,
|
|
42044
|
-
url_hash: (0,
|
|
44549
|
+
url_hash: (0, import_node_crypto9.createHash)("sha256").update(attestation.url).digest("hex").slice(0, 16),
|
|
42045
44550
|
response_hash: attestation.responseHash,
|
|
42046
44551
|
fetched_at: attestation.fetchedAt,
|
|
42047
44552
|
verified: attestation.verified,
|
|
@@ -42072,7 +44577,7 @@ async function createTLSNotaryAttestation(input) {
|
|
|
42072
44577
|
}
|
|
42073
44578
|
|
|
42074
44579
|
// src/c2pa-credentials.ts
|
|
42075
|
-
var
|
|
44580
|
+
var import_node_crypto10 = require("crypto");
|
|
42076
44581
|
function createC2PAManifest(receipts, options) {
|
|
42077
44582
|
const generator = options.generator || "protect-mcp";
|
|
42078
44583
|
const version = options.version || "0.3.3";
|
|
@@ -42086,7 +44591,7 @@ function createC2PAManifest(receipts, options) {
|
|
|
42086
44591
|
(r) => r.payload?.decision === "deny"
|
|
42087
44592
|
);
|
|
42088
44593
|
const receiptHashes = receipts.map(
|
|
42089
|
-
(r) => (0,
|
|
44594
|
+
(r) => (0, import_node_crypto10.createHash)("sha256").update(JSON.stringify(r)).digest("hex")
|
|
42090
44595
|
);
|
|
42091
44596
|
const merkleRoot2 = computeMerkleRoot(receiptHashes);
|
|
42092
44597
|
const assertions = [
|
|
@@ -42228,7 +44733,7 @@ function computeMerkleRoot(hashes) {
|
|
|
42228
44733
|
const left = hashes[i];
|
|
42229
44734
|
const right = i + 1 < hashes.length ? hashes[i + 1] : left;
|
|
42230
44735
|
nextLevel.push(
|
|
42231
|
-
(0,
|
|
44736
|
+
(0, import_node_crypto10.createHash)("sha256").update(left + right).digest("hex")
|
|
42232
44737
|
);
|
|
42233
44738
|
}
|
|
42234
44739
|
return computeMerkleRoot(nextLevel);
|
|
@@ -42281,7 +44786,7 @@ function toManifoldFormat(prediction) {
|
|
|
42281
44786
|
}
|
|
42282
44787
|
|
|
42283
44788
|
// src/agent-exchange.ts
|
|
42284
|
-
var
|
|
44789
|
+
var import_node_crypto11 = require("crypto");
|
|
42285
44790
|
var ReceiptPropagator = class {
|
|
42286
44791
|
issuer;
|
|
42287
44792
|
signer;
|
|
@@ -42302,7 +44807,7 @@ var ReceiptPropagator = class {
|
|
|
42302
44807
|
delegate(delegateId, options) {
|
|
42303
44808
|
const now = /* @__PURE__ */ new Date();
|
|
42304
44809
|
const receipt = {
|
|
42305
|
-
receipt_id: `del_${(0,
|
|
44810
|
+
receipt_id: `del_${(0, import_node_crypto11.randomUUID)().slice(0, 12)}`,
|
|
42306
44811
|
receipt_type: "delegation",
|
|
42307
44812
|
issuer_id: this.issuer,
|
|
42308
44813
|
event_time: now.toISOString(),
|
|
@@ -42359,7 +44864,7 @@ var ReceiptPropagator = class {
|
|
|
42359
44864
|
const currentCount = this.delegationCallCounts.get(options.delegation_receipt) || 0;
|
|
42360
44865
|
this.delegationCallCounts.set(options.delegation_receipt, currentCount + 1);
|
|
42361
44866
|
const receipt = {
|
|
42362
|
-
receipt_id: `act_${(0,
|
|
44867
|
+
receipt_id: `act_${(0, import_node_crypto11.randomUUID)().slice(0, 12)}`,
|
|
42363
44868
|
receipt_type: "execution",
|
|
42364
44869
|
issuer_id: this.issuer,
|
|
42365
44870
|
event_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -42451,7 +44956,7 @@ function createReceiptChannel(orchestratorId) {
|
|
|
42451
44956
|
async withDelegation(delegateId, tools, fn, options) {
|
|
42452
44957
|
const delegation = propagator.delegate(delegateId, {
|
|
42453
44958
|
tools,
|
|
42454
|
-
scope: options?.scope || `task-${(0,
|
|
44959
|
+
scope: options?.scope || `task-${(0, import_node_crypto11.randomUUID)().slice(0, 8)}`,
|
|
42455
44960
|
ttl: options?.ttl || 3600,
|
|
42456
44961
|
maxCalls: options?.maxCalls
|
|
42457
44962
|
});
|
|
@@ -42601,6 +45106,48 @@ var TOOLS = [
|
|
|
42601
45106
|
},
|
|
42602
45107
|
required: ["environment"]
|
|
42603
45108
|
}
|
|
45109
|
+
},
|
|
45110
|
+
{
|
|
45111
|
+
name: "github_create_pr",
|
|
45112
|
+
description: "Create a GitHub pull request",
|
|
45113
|
+
inputSchema: {
|
|
45114
|
+
type: "object",
|
|
45115
|
+
properties: {
|
|
45116
|
+
repo: { type: "string", description: "Repository name" },
|
|
45117
|
+
branch: { type: "string", description: "Source branch" },
|
|
45118
|
+
title: { type: "string", description: "Pull request title" }
|
|
45119
|
+
},
|
|
45120
|
+
required: ["repo", "branch", "title"]
|
|
45121
|
+
}
|
|
45122
|
+
},
|
|
45123
|
+
{
|
|
45124
|
+
name: "send_email",
|
|
45125
|
+
description: "Send an email",
|
|
45126
|
+
inputSchema: {
|
|
45127
|
+
type: "object",
|
|
45128
|
+
properties: {
|
|
45129
|
+
to: { type: "string", description: "Recipient email address" },
|
|
45130
|
+
subject: { type: "string", description: "Subject line" },
|
|
45131
|
+
body: { type: "string", description: "Message body" }
|
|
45132
|
+
},
|
|
45133
|
+
required: ["to", "subject"]
|
|
45134
|
+
}
|
|
45135
|
+
},
|
|
45136
|
+
{
|
|
45137
|
+
name: "pms_book_fill",
|
|
45138
|
+
description: "Book a fill into the mock portfolio management system",
|
|
45139
|
+
inputSchema: {
|
|
45140
|
+
type: "object",
|
|
45141
|
+
properties: {
|
|
45142
|
+
account: { type: "string", description: "Portfolio or fund account" },
|
|
45143
|
+
symbol: { type: "string", description: "Instrument symbol" },
|
|
45144
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
45145
|
+
quantity: { type: "number" },
|
|
45146
|
+
price: { type: "number" },
|
|
45147
|
+
strategy: { type: "string" }
|
|
45148
|
+
},
|
|
45149
|
+
required: ["account", "symbol", "side", "quantity", "price"]
|
|
45150
|
+
}
|
|
42604
45151
|
}
|
|
42605
45152
|
];
|
|
42606
45153
|
function handleRequest(request) {
|
|
@@ -42610,7 +45157,7 @@ function handleRequest(request) {
|
|
|
42610
45157
|
id: request.id,
|
|
42611
45158
|
result: {
|
|
42612
45159
|
protocolVersion: "2024-11-05",
|
|
42613
|
-
serverInfo: { name: "protect-mcp-demo", version: "0.5.3" },
|
|
45160
|
+
serverInfo: { name: "protect-mcp-demo", version: process.env.PROTECT_MCP_VERSION || "0.5.3" },
|
|
42614
45161
|
capabilities: { tools: {} }
|
|
42615
45162
|
}
|
|
42616
45163
|
});
|
|
@@ -42648,6 +45195,15 @@ Contents: Hello from protect-mcp demo server!`;
|
|
|
42648
45195
|
case "deploy":
|
|
42649
45196
|
resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
|
|
42650
45197
|
break;
|
|
45198
|
+
case "github_create_pr":
|
|
45199
|
+
resultText = `[demo] Created PR in ${args.repo || "scopeblind/demo"} from ${args.branch || "agent/demo"}: ${args.title || "Agent change"}`;
|
|
45200
|
+
break;
|
|
45201
|
+
case "send_email":
|
|
45202
|
+
resultText = `[demo] Drafted email to ${args.to || "pm@example.com"}: ${args.subject || "Agent update"}`;
|
|
45203
|
+
break;
|
|
45204
|
+
case "pms_book_fill":
|
|
45205
|
+
resultText = `[demo] Booked ${args.side || "BUY"} ${args.quantity || 0} ${args.symbol || "AAPL"} @ ${args.price || 0} into ${args.account || "Demo Fund"} (${args.strategy || "Unassigned"})`;
|
|
45206
|
+
break;
|
|
42651
45207
|
default:
|
|
42652
45208
|
resultText = `[demo] Unknown tool: ${toolName}`;
|
|
42653
45209
|
}
|
|
@@ -42687,7 +45243,7 @@ function createSandboxServer() {
|
|
|
42687
45243
|
const { z } = require_zod();
|
|
42688
45244
|
const server = new McpServer({
|
|
42689
45245
|
name: "protect-mcp",
|
|
42690
|
-
version: "0.4.5",
|
|
45246
|
+
version: process.env.PROTECT_MCP_VERSION || "0.4.5",
|
|
42691
45247
|
description: "Security gateway for MCP servers. Per-tool policies, Ed25519-signed receipts, human approval gates, trust tiers."
|
|
42692
45248
|
});
|
|
42693
45249
|
server.tool(
|
|
@@ -42720,12 +45276,38 @@ function createSandboxServer() {
|
|
|
42720
45276
|
{ environment: z.enum(["staging", "production"]).describe("Target environment"), reason: z.string().optional().describe("Deployment reason") },
|
|
42721
45277
|
async (args) => ({ content: [{ type: "text", text: `[demo] Deployed to ${args.environment}` }] })
|
|
42722
45278
|
);
|
|
45279
|
+
server.tool("github_create_pr", "Create a GitHub pull request", {
|
|
45280
|
+
repo: z.string(),
|
|
45281
|
+
branch: z.string(),
|
|
45282
|
+
title: z.string()
|
|
45283
|
+
}, async (args) => ({
|
|
45284
|
+
content: [{ type: "text", text: `[demo] PR created in ${args.repo} from ${args.branch}: ${args.title}` }]
|
|
45285
|
+
}));
|
|
45286
|
+
server.tool("send_email", "Send an email", {
|
|
45287
|
+
to: z.string(),
|
|
45288
|
+
subject: z.string(),
|
|
45289
|
+
body: z.string().optional()
|
|
45290
|
+
}, async (args) => ({
|
|
45291
|
+
content: [{ type: "text", text: `[demo] Email prepared for ${args.to}: ${args.subject}` }]
|
|
45292
|
+
}));
|
|
45293
|
+
server.tool("pms_book_fill", "Book a fill into a mock PMS", {
|
|
45294
|
+
account: z.string(),
|
|
45295
|
+
symbol: z.string(),
|
|
45296
|
+
side: z.enum(["BUY", "SELL"]),
|
|
45297
|
+
quantity: z.number(),
|
|
45298
|
+
price: z.number(),
|
|
45299
|
+
strategy: z.string().optional()
|
|
45300
|
+
}, async (args) => ({
|
|
45301
|
+
content: [{ type: "text", text: `[demo] Booked ${args.side} ${args.quantity} ${args.symbol} @ ${args.price} into ${args.account}` }]
|
|
45302
|
+
}));
|
|
42723
45303
|
return server;
|
|
42724
45304
|
}
|
|
42725
45305
|
// Annotate the CommonJS export names for ESM import in node:
|
|
42726
45306
|
0 && (module.exports = {
|
|
42727
45307
|
BUILTIN_PATTERNS,
|
|
45308
|
+
CONNECTOR_PILOTS,
|
|
42728
45309
|
ConfidentialGate,
|
|
45310
|
+
POLICY_PACKS,
|
|
42729
45311
|
ProtectGateway,
|
|
42730
45312
|
ReceiptPropagator,
|
|
42731
45313
|
ScopeBlindBridge,
|
|
@@ -42735,6 +45317,9 @@ function createSandboxServer() {
|
|
|
42735
45317
|
collectSignedReceipts,
|
|
42736
45318
|
computeCalibration,
|
|
42737
45319
|
confidentialInference,
|
|
45320
|
+
connectorDirectory,
|
|
45321
|
+
connectorDoctor,
|
|
45322
|
+
connectorPilotIds,
|
|
42738
45323
|
createApprovalChallenge,
|
|
42739
45324
|
createApprovalReceiptPayload,
|
|
42740
45325
|
createAttestationField,
|
|
@@ -42746,6 +45331,7 @@ function createSandboxServer() {
|
|
|
42746
45331
|
createReceiptChannel,
|
|
42747
45332
|
createSandbox,
|
|
42748
45333
|
createSandboxServer,
|
|
45334
|
+
createSelectiveDisclosurePackage,
|
|
42749
45335
|
destroySandbox,
|
|
42750
45336
|
discloseField,
|
|
42751
45337
|
ed25519ToDIDKey,
|
|
@@ -42766,6 +45352,8 @@ function createSandboxServer() {
|
|
|
42766
45352
|
generateSampleCedarPolicy,
|
|
42767
45353
|
generateSchemaStub,
|
|
42768
45354
|
generateVerifyReceiptSkill,
|
|
45355
|
+
getConnectorPilot,
|
|
45356
|
+
getPolicyPack,
|
|
42769
45357
|
getScopeBlindBridge,
|
|
42770
45358
|
getSignerInfo,
|
|
42771
45359
|
getToolPolicy,
|
|
@@ -42786,8 +45374,10 @@ function createSandboxServer() {
|
|
|
42786
45374
|
parseLogFile,
|
|
42787
45375
|
parseNotificationConfigFromEnv,
|
|
42788
45376
|
parseRateLimit,
|
|
45377
|
+
policyPackIds,
|
|
42789
45378
|
policySetFromSource,
|
|
42790
45379
|
queryExternalPDP,
|
|
45380
|
+
readInstalledConnectorPilots,
|
|
42791
45381
|
receiptToVP,
|
|
42792
45382
|
receiptsToHFRows,
|
|
42793
45383
|
redactFields,
|
|
@@ -42811,7 +45401,9 @@ function createSandboxServer() {
|
|
|
42811
45401
|
verifyApprovalAssertion,
|
|
42812
45402
|
verifyCommitment,
|
|
42813
45403
|
verifyEvidenceAttestation,
|
|
42814
|
-
verifyRekorAnchor
|
|
45404
|
+
verifyRekorAnchor,
|
|
45405
|
+
verifySelectiveDisclosurePackage,
|
|
45406
|
+
writeConnectorPilots
|
|
42815
45407
|
});
|
|
42816
45408
|
/**
|
|
42817
45409
|
* scopeblind-bridge.ts
|
|
@@ -42853,5 +45445,9 @@ function createSandboxServer() {
|
|
|
42853
45445
|
@noble/curves/esm/abstract/curve.js:
|
|
42854
45446
|
@noble/curves/esm/abstract/edwards.js:
|
|
42855
45447
|
@noble/curves/esm/ed25519.js:
|
|
45448
|
+
@noble/curves/esm/abstract/weierstrass.js:
|
|
45449
|
+
@noble/curves/esm/_shortw_utils.js:
|
|
45450
|
+
@noble/curves/esm/nist.js:
|
|
45451
|
+
@noble/curves/esm/p256.js:
|
|
42856
45452
|
(*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
|
|
42857
45453
|
*/
|