@securecode-ai/mcp 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -12
- package/dist/api/types.d.ts +7 -0
- package/dist/approval/auditLog.d.ts +1 -1
- package/dist/approval/auditLog.js +11 -3
- package/dist/approval/auditLog.js.map +1 -1
- package/dist/approval/broker.d.ts +7 -2
- package/dist/approval/broker.js +239 -110
- package/dist/approval/broker.js.map +1 -1
- package/dist/approval/policy.d.ts +10 -0
- package/dist/approval/policy.js +104 -0
- package/dist/approval/policy.js.map +1 -0
- package/dist/approval/types.d.ts +11 -2
- package/dist/approval/types.js +10 -1
- package/dist/approval/types.js.map +1 -1
- package/dist/attack/agentScanExecutor.d.ts +35 -1
- package/dist/attack/agentScanExecutor.js +405 -76
- package/dist/attack/agentScanExecutor.js.map +1 -1
- package/dist/attack/agentScanLoop.d.ts +20 -0
- package/dist/attack/agentScanLoop.js +1264 -60
- package/dist/attack/agentScanLoop.js.map +1 -1
- package/dist/attack/agentScanProtocol.d.ts +382 -6
- package/dist/attack/agentScanProtocol.js +110 -4
- package/dist/attack/agentScanProtocol.js.map +1 -1
- package/dist/attack/agentTrace.d.ts +73 -0
- package/dist/attack/agentTrace.js +228 -0
- package/dist/attack/agentTrace.js.map +1 -0
- package/dist/attack/architectureScoutExecutor.d.ts +18 -0
- package/dist/attack/architectureScoutExecutor.js +211 -0
- package/dist/attack/architectureScoutExecutor.js.map +1 -0
- package/dist/attack/architectureScoutLoop.d.ts +36 -0
- package/dist/attack/architectureScoutLoop.js +403 -0
- package/dist/attack/architectureScoutLoop.js.map +1 -0
- package/dist/attack/architectureScoutProtocol.d.ts +209 -0
- package/dist/attack/architectureScoutProtocol.js +47 -0
- package/dist/attack/architectureScoutProtocol.js.map +1 -0
- package/dist/attack/candidateStore.d.ts +95 -0
- package/dist/attack/candidateStore.js +231 -0
- package/dist/attack/candidateStore.js.map +1 -0
- package/dist/attack/evidenceLedger.d.ts +67 -0
- package/dist/attack/evidenceLedger.js +192 -0
- package/dist/attack/evidenceLedger.js.map +1 -0
- package/dist/attack/finishGate.d.ts +62 -0
- package/dist/attack/finishGate.js +209 -0
- package/dist/attack/finishGate.js.map +1 -0
- package/dist/attack/fixCodeMerge.d.ts +27 -0
- package/dist/attack/fixCodeMerge.js +42 -0
- package/dist/attack/fixCodeMerge.js.map +1 -0
- package/dist/attack/fixVerifyLoop.d.ts +55 -0
- package/dist/attack/fixVerifyLoop.js +187 -0
- package/dist/attack/fixVerifyLoop.js.map +1 -0
- package/dist/attack/investigationProfiles.d.ts +36 -0
- package/dist/attack/investigationProfiles.js +144 -0
- package/dist/attack/investigationProfiles.js.map +1 -0
- package/dist/attack/investigationState.d.ts +227 -0
- package/dist/attack/investigationState.js +666 -0
- package/dist/attack/investigationState.js.map +1 -0
- package/dist/attack/mutationOperators.d.ts +22 -0
- package/dist/attack/mutationOperators.js +170 -0
- package/dist/attack/mutationOperators.js.map +1 -0
- package/dist/attack/mutationTest.d.ts +33 -0
- package/dist/attack/mutationTest.js +110 -0
- package/dist/attack/mutationTest.js.map +1 -0
- package/dist/attack/proofGate.d.ts +20 -0
- package/dist/attack/proofGate.js +104 -0
- package/dist/attack/proofGate.js.map +1 -0
- package/dist/attack/proofTypes.d.ts +57 -0
- package/dist/attack/proofTypes.js +32 -0
- package/dist/attack/proofTypes.js.map +1 -0
- package/dist/attack/protocolValidator.d.ts +50 -0
- package/dist/attack/protocolValidator.js +427 -0
- package/dist/attack/protocolValidator.js.map +1 -0
- package/dist/attack/qualityMetrics.d.ts +133 -0
- package/dist/attack/qualityMetrics.js +226 -0
- package/dist/attack/qualityMetrics.js.map +1 -0
- package/dist/attack/scanScheduler.d.ts +52 -0
- package/dist/attack/scanScheduler.js +265 -0
- package/dist/attack/scanScheduler.js.map +1 -0
- package/dist/attack/scanState.d.ts +58 -0
- package/dist/attack/scanState.js +102 -0
- package/dist/attack/scanState.js.map +1 -0
- package/dist/attack/searchIntent.d.ts +16 -0
- package/dist/attack/searchIntent.js +57 -0
- package/dist/attack/searchIntent.js.map +1 -0
- package/dist/attack/verifyLoop.d.ts +43 -0
- package/dist/attack/verifyLoop.js +314 -14
- package/dist/attack/verifyLoop.js.map +1 -1
- package/dist/attack/workItem.d.ts +57 -0
- package/dist/attack/workItem.js +225 -0
- package/dist/attack/workItem.js.map +1 -0
- package/dist/audit/findingReviewQueue.d.ts +109 -0
- package/dist/audit/findingReviewQueue.js +335 -0
- package/dist/audit/findingReviewQueue.js.map +1 -0
- package/dist/audit/scanAuditLog.d.ts +160 -0
- package/dist/audit/scanAuditLog.js +404 -0
- package/dist/audit/scanAuditLog.js.map +1 -0
- package/dist/dependency/dependencyChecker.js +35 -9
- package/dist/dependency/dependencyChecker.js.map +1 -1
- package/dist/dependency/exploitPriority.d.ts +33 -0
- package/dist/dependency/exploitPriority.js +78 -0
- package/dist/dependency/exploitPriority.js.map +1 -0
- package/dist/dependency/finding.d.ts +16 -0
- package/dist/dependency/osvClient.js +2 -0
- package/dist/dependency/osvClient.js.map +1 -1
- package/dist/dependency/types.d.ts +12 -1
- package/dist/mcp/server.js +13 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +127 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-map/agentMemory.d.ts +83 -1
- package/dist/project-map/agentMemory.js +197 -7
- package/dist/project-map/agentMemory.js.map +1 -1
- package/dist/project-map/architectureContext.d.ts +202 -0
- package/dist/project-map/architectureContext.js +421 -0
- package/dist/project-map/architectureContext.js.map +1 -0
- package/dist/project-map/blastRadius.d.ts +54 -0
- package/dist/project-map/blastRadius.js +196 -0
- package/dist/project-map/blastRadius.js.map +1 -0
- package/dist/project-map/callGraphExtractor.d.ts +13 -0
- package/dist/project-map/callGraphExtractor.js +222 -0
- package/dist/project-map/callGraphExtractor.js.map +1 -0
- package/dist/project-map/capabilityRegistry.d.ts +46 -0
- package/dist/project-map/capabilityRegistry.js +91 -0
- package/dist/project-map/capabilityRegistry.js.map +1 -0
- package/dist/project-map/findTests.d.ts +29 -0
- package/dist/project-map/findTests.js +236 -0
- package/dist/project-map/findTests.js.map +1 -0
- package/dist/project-map/handlerInventory.d.ts +112 -0
- package/dist/project-map/handlerInventory.js +184 -0
- package/dist/project-map/handlerInventory.js.map +1 -0
- package/dist/project-map/implementationResolver.d.ts +43 -0
- package/dist/project-map/implementationResolver.js +217 -0
- package/dist/project-map/implementationResolver.js.map +1 -0
- package/dist/project-map/mapContext.d.ts +6 -1
- package/dist/project-map/mapContext.js +1 -0
- package/dist/project-map/mapContext.js.map +1 -1
- package/dist/project-map/scanCache.d.ts +57 -3
- package/dist/project-map/scanCache.js +61 -2
- package/dist/project-map/scanCache.js.map +1 -1
- package/dist/project-map/symbolIndex.d.ts +39 -0
- package/dist/project-map/symbolIndex.js +385 -0
- package/dist/project-map/symbolIndex.js.map +1 -0
- package/dist/tooling/agentEvalScoring.d.ts +77 -0
- package/dist/tooling/agentEvalScoring.js +140 -0
- package/dist/tooling/agentEvalScoring.js.map +1 -0
- package/dist/tooling/agentRegression.d.ts +53 -0
- package/dist/tooling/agentRegression.js +99 -0
- package/dist/tooling/agentRegression.js.map +1 -0
- package/dist/tools/agentScan.d.ts +69 -0
- package/dist/tools/agentScan.js +702 -43
- package/dist/tools/agentScan.js.map +1 -1
- package/dist/tools/findingReviewTools.d.ts +22 -0
- package/dist/tools/findingReviewTools.js +121 -0
- package/dist/tools/findingReviewTools.js.map +1 -0
- package/dist/tools/fix.js +1 -1
- package/dist/tools/fix.js.map +1 -1
- package/dist/tools/map.js +191 -1
- package/dist/tools/map.js.map +1 -1
- package/dist/tools/runTests.d.ts +2 -0
- package/dist/tools/runTests.js +29 -0
- package/dist/tools/runTests.js.map +1 -0
- package/dist/utils/effectMock.d.ts +1 -0
- package/dist/utils/effectMock.js +162 -0
- package/dist/utils/effectMock.js.map +1 -0
- package/dist/utils/gitContext.d.ts +26 -0
- package/dist/utils/gitContext.js +317 -0
- package/dist/utils/gitContext.js.map +1 -0
- package/dist/utils/localTestRunner.d.ts +57 -2
- package/dist/utils/localTestRunner.js +122 -115
- package/dist/utils/localTestRunner.js.map +1 -1
- package/dist/utils/securityConfig.d.ts +17 -0
- package/dist/utils/securityConfig.js +192 -0
- package/dist/utils/securityConfig.js.map +1 -0
- package/dist/utils/testCommandPolicy.d.ts +36 -0
- package/dist/utils/testCommandPolicy.js +252 -0
- package/dist/utils/testCommandPolicy.js.map +1 -0
- package/dist/utils/testRunner.d.ts +56 -0
- package/dist/utils/testRunner.js +246 -0
- package/dist/utils/testRunner.js.map +1 -0
- package/dist/utils/testSafety.d.ts +40 -2
- package/dist/utils/testSafety.js +205 -26
- package/dist/utils/testSafety.js.map +1 -1
- package/dist/utils/verificationSandbox.d.ts +113 -0
- package/dist/utils/verificationSandbox.js +656 -0
- package/dist/utils/verificationSandbox.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@ Standalone MCP (Model Context Protocol) server for SecureCode AI. Provides secur
|
|
|
8
8
|
npm install -g @securecode-ai/mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Or use directly with npx (no install needed):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @securecode-ai/mcp scan src/app.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## Quick Start
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -61,6 +67,22 @@ securecode-mcp scan src/app.ts --json
|
|
|
61
67
|
|
|
62
68
|
### Cursor / Windsurf
|
|
63
69
|
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"securecode": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "@securecode-ai/mcp@latest", "serve", "--workspace", "/path/to/your/project"],
|
|
76
|
+
"env": {
|
|
77
|
+
"SECURECODE_API_TOKEN": "your-api-token-here"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or if installed globally:
|
|
85
|
+
|
|
64
86
|
```json
|
|
65
87
|
{
|
|
66
88
|
"mcpServers": {
|
|
@@ -78,14 +100,14 @@ securecode-mcp scan src/app.ts --json
|
|
|
78
100
|
claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/your/project
|
|
79
101
|
```
|
|
80
102
|
|
|
81
|
-
## Tools (
|
|
103
|
+
## Tools (16)
|
|
82
104
|
|
|
83
105
|
### Scanning
|
|
84
106
|
|
|
85
107
|
| Tool | Description | Approval |
|
|
86
108
|
|------|-------------|----------|
|
|
87
109
|
| `securecode.scan` | Scan code for vulnerabilities (AI pipeline) | No |
|
|
88
|
-
| `securecode.agent-scan` | Agent-mode deep scan with
|
|
110
|
+
| `securecode.agent-scan` | Agent-mode deep scan with 20+ tools, structured proof, and sandbox verification | No |
|
|
89
111
|
| `securecode.scan-batch` | Scan multiple files in one call | No |
|
|
90
112
|
| `securecode.scan-secrets` | Scan for hardcoded secrets and PII (local, no AI) | No |
|
|
91
113
|
| `securecode.scan-dependencies` | Scan lockfiles for known vulnerabilities (OSV/NVD) | No |
|
|
@@ -94,7 +116,7 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
|
|
|
94
116
|
|
|
95
117
|
| Tool | Description | Approval |
|
|
96
118
|
|------|-------------|----------|
|
|
97
|
-
| `securecode.map` |
|
|
119
|
+
| `securecode.map` | Build project map: endpoints, middleware, auth, architecture context | No |
|
|
98
120
|
|
|
99
121
|
### Fixes & Testing
|
|
100
122
|
|
|
@@ -102,6 +124,7 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
|
|
|
102
124
|
|------|-------------|----------|
|
|
103
125
|
| `securecode.fix` | Generate a patch for a specific finding | Yes |
|
|
104
126
|
| `securecode.attack` | Endpoint red-team testing (beta) | Yes |
|
|
127
|
+
| `securecode.run-tests` | Run tests in sandbox for verification | Yes |
|
|
105
128
|
|
|
106
129
|
### Agent Memory (FP Learning)
|
|
107
130
|
|
|
@@ -112,6 +135,14 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
|
|
|
112
135
|
| `securecode.clear-agent-memory` | Clear all agent memory (or one FP by ID) | No |
|
|
113
136
|
| `securecode.add-known-fact` | Add a project fact for faster investigations | No |
|
|
114
137
|
|
|
138
|
+
### Finding Review
|
|
139
|
+
|
|
140
|
+
| Tool | Description | Approval |
|
|
141
|
+
|------|-------------|----------|
|
|
142
|
+
| `securecode.review-findings` | Review the finding queue for a workspace | No |
|
|
143
|
+
| `securecode.decide-finding` | Accept or reject a finding in the review queue | No |
|
|
144
|
+
| `securecode.clear-finding-reviews` | Clear all finding reviews for a workspace | No |
|
|
145
|
+
|
|
115
146
|
### How Agent Memory Works
|
|
116
147
|
|
|
117
148
|
When the agent reports a false positive, dismiss it with `record-false-positive`. The agent stores the pattern in `.securecode/agent-memory.json` and will not report similar patterns in future scans of that workspace.
|
|
@@ -125,17 +156,25 @@ Memory is per-workspace, user-owned, and deletable. No cross-tenant leakage.
|
|
|
125
156
|
|
|
126
157
|
## Agent Scan Architecture
|
|
127
158
|
|
|
128
|
-
The agent scan (`securecode.agent-scan`) is an AI investigator that:
|
|
159
|
+
The agent scan (`securecode.agent-scan`) is an AI security investigator that:
|
|
160
|
+
|
|
161
|
+
1. **Maps** the project architecture (architecture scout with trust boundaries, security controls, risks)
|
|
162
|
+
2. **Reads** the target file and related files across the codebase
|
|
163
|
+
3. **Traces** data flows (taint tracking, cross-file flow with structured source→sink→hop chains)
|
|
164
|
+
4. **Checks** guards, endpoint policies, and configuration
|
|
165
|
+
5. **Verifies** threat model applicability and capability reachability
|
|
166
|
+
6. **Self-critiques** before reporting (selfCritique field)
|
|
167
|
+
7. **Gets reviewed** by an independent critique LLM
|
|
168
|
+
8. **Proves** findings in a sandbox (PROVEN/UNPROVEN)
|
|
169
|
+
9. **Generates fixes** for proven findings
|
|
129
170
|
|
|
130
|
-
|
|
131
|
-
2. **Traces** data flows (taint tracking, cross-file)
|
|
132
|
-
3. **Checks** guards and endpoint policies
|
|
133
|
-
4. **Self-critiques** before reporting (selfCritique field)
|
|
134
|
-
5. **Gets reviewed** by an independent critique LLM
|
|
135
|
-
6. **Proves** findings in a sandbox (PROVEN/UNPROVEN)
|
|
136
|
-
7. **Generates fixes** for proven findings
|
|
171
|
+
Agent tools (20+): `read_file`, `search_code`, `trace_flow`, `trace_flow_cross_file`, `check_guard`, `check_policy`, `get_endpoints`, `list_imports`, `list_files`, `call_graph`, `git_blame`, `git_history`, `git_diff`, `check_dependencies`, `read_config`, `find_definition`, `find_references`, `find_tests`, `run_tests`, `finish`.
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
The deterministic control plane enforces proof quality:
|
|
174
|
+
- Every finding requires source, reachability, control, threat-model, and impact evidence
|
|
175
|
+
- Unproven concerns become investigation notes, not findings
|
|
176
|
+
- Architecture risks expand across related files (callers, implementations, sinks)
|
|
177
|
+
- The finish gate rejects finish while proof requirements remain unsatisfied
|
|
139
178
|
|
|
140
179
|
Languages: JavaScript, TypeScript, Python (partial).
|
|
141
180
|
|
|
@@ -145,6 +184,7 @@ Languages: JavaScript, TypeScript, Python (partial).
|
|
|
145
184
|
|----------|---------|-------------|
|
|
146
185
|
| `SECURECODE_API_TOKEN` | — | API token (alternative to login) |
|
|
147
186
|
| `SECURECODE_API_URL` | `https://api.usesecurecode.tech` | API base URL |
|
|
187
|
+
| `SECURECODE_ATTACK_ENABLED` | — | Set to `1` to enable the attack tool |
|
|
148
188
|
|
|
149
189
|
## Security
|
|
150
190
|
|
package/dist/api/types.d.ts
CHANGED
|
@@ -184,18 +184,25 @@ export interface SandboxProveResponse {
|
|
|
184
184
|
exploitStatus?: number;
|
|
185
185
|
};
|
|
186
186
|
scanCredits?: number;
|
|
187
|
+
proofEvidence?: import('../attack/proofTypes').ProofEvidence;
|
|
188
|
+
proofGateResult?: import('../attack/proofTypes').ProofGateResult;
|
|
187
189
|
}
|
|
188
190
|
export interface VerifyGenerateResponse {
|
|
189
191
|
canTest: boolean;
|
|
190
192
|
testScript?: string;
|
|
193
|
+
setupScript?: string | null;
|
|
191
194
|
runner?: string;
|
|
192
195
|
description?: string;
|
|
193
196
|
skipReason?: string;
|
|
194
197
|
scanCredits?: number;
|
|
198
|
+
/** Dollar cost the API charged for this generate call (credits → USD). */
|
|
199
|
+
costUsd?: number;
|
|
195
200
|
}
|
|
196
201
|
export interface VerifyAnalyzeResponse {
|
|
197
202
|
verdict: 'PROVEN' | 'UNPROVEN' | 'INCONCLUSIVE';
|
|
198
203
|
reason: string;
|
|
199
204
|
shouldRetry: boolean;
|
|
200
205
|
scanCredits?: number;
|
|
206
|
+
/** Dollar cost the API charged for this analyze call (credits → USD). */
|
|
207
|
+
costUsd?: number;
|
|
201
208
|
}
|
|
@@ -40,6 +40,8 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
41
|
const AUDIT_DIR = path.join(os.homedir(), '.securecode');
|
|
42
42
|
const AUDIT_FILE = path.join(AUDIT_DIR, 'approval-audit.log');
|
|
43
|
+
const MAX_ENTRIES = 200;
|
|
44
|
+
let auditWriteFailed = false;
|
|
43
45
|
function appendAudit(entry) {
|
|
44
46
|
try {
|
|
45
47
|
if (!fs.existsSync(AUDIT_DIR)) {
|
|
@@ -47,12 +49,18 @@ function appendAudit(entry) {
|
|
|
47
49
|
}
|
|
48
50
|
const line = JSON.stringify(entry) + '\n';
|
|
49
51
|
fs.appendFileSync(AUDIT_FILE, line, { mode: 0o600 });
|
|
52
|
+
auditWriteFailed = false;
|
|
53
|
+
return true;
|
|
50
54
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (!auditWriteFailed) {
|
|
57
|
+
auditWriteFailed = true;
|
|
58
|
+
console.warn(`[securecode] Audit write failed: ${err?.message || err}. Further audit failures will be silent.`);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
|
-
function readAudit(limit =
|
|
63
|
+
function readAudit(limit = MAX_ENTRIES) {
|
|
56
64
|
try {
|
|
57
65
|
if (!fs.existsSync(AUDIT_FILE))
|
|
58
66
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auditLog.js","sourceRoot":"","sources":["../../src/approval/auditLog.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"auditLog.js","sourceRoot":"","sources":["../../src/approval/auditLog.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,kCAgBC;AAED,8BASC;AAtCD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAGzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;AAC9D,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAgB,WAAW,CAAC,KAAiB;IACzC,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;QAC1C,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,gBAAgB,GAAG,KAAK,CAAC;QACzB,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAChB,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACpB,gBAAgB,GAAG,IAAI,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,oCAAoC,GAAG,EAAE,OAAO,IAAI,GAAG,0CAA0C,CAAC,CAAC;QACpH,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED,SAAgB,SAAS,CAAC,QAAgB,WAAW;IACjD,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ApprovalResult } from './types';
|
|
1
|
+
import { type ApprovalResult, type OperationCategory } from './types';
|
|
2
2
|
export declare class ApprovalBroker {
|
|
3
3
|
private server;
|
|
4
4
|
private port;
|
|
@@ -6,6 +6,11 @@ export declare class ApprovalBroker {
|
|
|
6
6
|
start(): Promise<number>;
|
|
7
7
|
stop(): Promise<void>;
|
|
8
8
|
getPort(): number;
|
|
9
|
-
requestApproval(tool: string, summary: string, operationParts: unknown[], timeoutMs?: number): Promise<ApprovalResult>;
|
|
9
|
+
requestApproval(tool: string, summary: string, operationParts: unknown[], timeoutMs?: number, category?: OperationCategory, workspaceRoot?: string | null): Promise<ApprovalResult>;
|
|
10
|
+
private settle;
|
|
11
|
+
private isLocalOrigin;
|
|
12
|
+
private isLocalHost;
|
|
10
13
|
private handle;
|
|
14
|
+
private handleDetails;
|
|
15
|
+
private handleDecide;
|
|
11
16
|
}
|
package/dist/approval/broker.js
CHANGED
|
@@ -38,6 +38,9 @@ const http = __importStar(require("http"));
|
|
|
38
38
|
const child_process_1 = require("child_process");
|
|
39
39
|
const types_1 = require("./types");
|
|
40
40
|
const auditLog_1 = require("./auditLog");
|
|
41
|
+
const MAX_PENDING = 16;
|
|
42
|
+
const MAX_BODY_BYTES = 4096;
|
|
43
|
+
const LOOPBACK = '127.0.0.1';
|
|
41
44
|
const APPROVAL_PAGE_HTML = `<!DOCTYPE html>
|
|
42
45
|
<html>
|
|
43
46
|
<head>
|
|
@@ -55,6 +58,7 @@ button { padding: 10px 24px; border: none; border-radius: 6px; font-size: 14px;
|
|
|
55
58
|
.result { margin-top: 24px; font-size: 16px; font-weight: 600; }
|
|
56
59
|
.timeout { color: #d97706; }
|
|
57
60
|
.expired { display: none; color: #dc2626; }
|
|
61
|
+
.error { color: #dc2626; }
|
|
58
62
|
</style>
|
|
59
63
|
</head>
|
|
60
64
|
<body>
|
|
@@ -70,42 +74,65 @@ button { padding: 10px 24px; border: none; border-radius: 6px; font-size: 14px;
|
|
|
70
74
|
<script>
|
|
71
75
|
const params = new URLSearchParams(window.location.search);
|
|
72
76
|
const reqId = params.get('id');
|
|
73
|
-
|
|
74
|
-
document.getElementById('hash').textContent = params.get('hash') || '';
|
|
75
|
-
document.getElementById('summary').textContent = decodeURIComponent(params.get('summary') || '');
|
|
77
|
+
let decisionToken = '';
|
|
76
78
|
|
|
77
|
-
async function
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
79
|
+
async function loadDetails() {
|
|
80
|
+
try {
|
|
81
|
+
const r = await fetch('/details?id=' + encodeURIComponent(reqId || ''));
|
|
82
|
+
const data = await r.json();
|
|
83
|
+
if (!data.ok) {
|
|
84
|
+
document.getElementById('buttons').style.display = 'none';
|
|
85
|
+
const el = document.getElementById('result');
|
|
86
|
+
el.textContent = 'Error: ' + (data.error || 'unknown');
|
|
87
|
+
el.style.color = '#dc2626';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
document.getElementById('tool').textContent = data.tool || '';
|
|
91
|
+
document.getElementById('hash').textContent = data.operationHash || '';
|
|
92
|
+
document.getElementById('summary').textContent = data.summary || '';
|
|
93
|
+
decisionToken = data.decisionToken || '';
|
|
94
|
+
if (data.expiresAt && Date.now() > data.expiresAt) {
|
|
95
|
+
document.getElementById('buttons').style.display = 'none';
|
|
96
|
+
document.getElementById('expired').style.display = 'block';
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
document.getElementById('buttons').style.display = 'none';
|
|
100
|
+
const el = document.getElementById('result');
|
|
101
|
+
el.textContent = 'Error loading details: ' + e.message;
|
|
102
|
+
el.style.color = '#dc2626';
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
document.getElementById('
|
|
103
|
-
|
|
106
|
+
async function respond(approved) {
|
|
107
|
+
document.getElementById('buttons').style.display = 'none';
|
|
108
|
+
const res = document.getElementById('result');
|
|
109
|
+
try {
|
|
110
|
+
const r = await fetch('/decide', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {'Content-Type': 'application/json'},
|
|
113
|
+
body: JSON.stringify({ id: reqId, decisionToken, approved })
|
|
114
|
+
});
|
|
115
|
+
const data = await r.json();
|
|
116
|
+
if (data.ok) {
|
|
117
|
+
res.textContent = approved ? 'Approved. You can close this page.' : 'Denied. You can close this page.';
|
|
118
|
+
res.style.color = approved ? '#16a34a' : '#dc2626';
|
|
119
|
+
} else {
|
|
120
|
+
res.textContent = 'Error: ' + (data.error || 'unknown');
|
|
121
|
+
res.style.color = '#dc2626';
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
res.textContent = 'Error: ' + e.message;
|
|
125
|
+
res.style.color = '#dc2626';
|
|
126
|
+
}
|
|
104
127
|
}
|
|
128
|
+
|
|
129
|
+
loadDetails();
|
|
105
130
|
</script>
|
|
106
131
|
</body>
|
|
107
132
|
</html>`;
|
|
108
133
|
function openBrowser(url) {
|
|
134
|
+
if (process.env.SECURECODE_TEST_MODE === '1')
|
|
135
|
+
return;
|
|
109
136
|
const platform = process.platform;
|
|
110
137
|
try {
|
|
111
138
|
if (platform === 'darwin') {
|
|
@@ -122,14 +149,23 @@ function openBrowser(url) {
|
|
|
122
149
|
// If browser open fails, the user can copy the URL from stdout
|
|
123
150
|
}
|
|
124
151
|
}
|
|
152
|
+
const SECURITY_HEADERS = {
|
|
153
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'",
|
|
154
|
+
'X-Content-Type-Options': 'nosniff',
|
|
155
|
+
'Cache-Control': 'no-store',
|
|
156
|
+
'Referrer-Policy': 'no-referrer',
|
|
157
|
+
};
|
|
125
158
|
class ApprovalBroker {
|
|
126
159
|
server = null;
|
|
127
160
|
port = 0;
|
|
128
161
|
pending = new Map();
|
|
129
162
|
async start() {
|
|
130
|
-
return new Promise((resolve) => {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
131
164
|
this.server = http.createServer((req, res) => this.handle(req, res));
|
|
132
|
-
this.server.
|
|
165
|
+
this.server.once('error', (err) => {
|
|
166
|
+
reject(err);
|
|
167
|
+
});
|
|
168
|
+
this.server.listen(0, LOOPBACK, () => {
|
|
133
169
|
const addr = this.server.address();
|
|
134
170
|
this.port = addr.port;
|
|
135
171
|
resolve(this.port);
|
|
@@ -137,6 +173,9 @@ class ApprovalBroker {
|
|
|
137
173
|
});
|
|
138
174
|
}
|
|
139
175
|
async stop() {
|
|
176
|
+
for (const [, entry] of this.pending) {
|
|
177
|
+
this.settle(entry, false, 'shutdown', 'Broker shutting down');
|
|
178
|
+
}
|
|
140
179
|
return new Promise((resolve) => {
|
|
141
180
|
if (this.server) {
|
|
142
181
|
this.server.close(() => resolve());
|
|
@@ -149,104 +188,194 @@ class ApprovalBroker {
|
|
|
149
188
|
getPort() {
|
|
150
189
|
return this.port;
|
|
151
190
|
}
|
|
152
|
-
requestApproval(tool, summary, operationParts, timeoutMs = 60_000) {
|
|
191
|
+
requestApproval(tool, summary, operationParts, timeoutMs = 60_000, category = 'paid-generation', workspaceRoot = null) {
|
|
153
192
|
return new Promise((resolve) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
193
|
+
if (this.pending.size >= MAX_PENDING) {
|
|
194
|
+
resolve({
|
|
195
|
+
approved: false,
|
|
196
|
+
reason: 'Too many pending approval requests',
|
|
197
|
+
requestId: '',
|
|
198
|
+
duration: 0,
|
|
199
|
+
category,
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const req = (0, types_1.createApprovalRequest)(tool, summary, operationParts, timeoutMs, category, workspaceRoot);
|
|
204
|
+
const startTime = req.createdAt;
|
|
160
205
|
const timer = setTimeout(() => {
|
|
161
206
|
const entry = this.pending.get(req.id);
|
|
162
|
-
if (entry) {
|
|
163
|
-
this.
|
|
164
|
-
const result = {
|
|
165
|
-
approved: false,
|
|
166
|
-
reason: 'Request timed out',
|
|
167
|
-
requestId: req.id,
|
|
168
|
-
duration: Date.now() - startTime,
|
|
169
|
-
};
|
|
170
|
-
(0, auditLog_1.appendAudit)({
|
|
171
|
-
timestamp: new Date().toISOString(),
|
|
172
|
-
requestId: req.id,
|
|
173
|
-
tool: req.tool,
|
|
174
|
-
operationHash: req.operationHash,
|
|
175
|
-
approved: false,
|
|
176
|
-
reason: 'timeout',
|
|
177
|
-
durationMs: result.duration,
|
|
178
|
-
});
|
|
179
|
-
resolve(result);
|
|
207
|
+
if (entry && !entry.settled) {
|
|
208
|
+
this.settle(entry, false, 'timeout', 'Request timed out');
|
|
180
209
|
}
|
|
181
210
|
}, timeoutMs + 5000);
|
|
182
|
-
const
|
|
183
|
-
this.pending.set(req.id,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
origResolve(r);
|
|
188
|
-
},
|
|
189
|
-
});
|
|
211
|
+
const entry = { req, resolve, timer, settled: false };
|
|
212
|
+
this.pending.set(req.id, entry);
|
|
213
|
+
const url = `http://${LOOPBACK}:${this.port}/?id=${req.id}`;
|
|
214
|
+
console.error(`[securecode] Approval required for ${tool}. Open: ${url}`);
|
|
215
|
+
openBrowser(url);
|
|
190
216
|
});
|
|
191
217
|
}
|
|
218
|
+
settle(entry, approved, reason, humanReason) {
|
|
219
|
+
if (entry.settled)
|
|
220
|
+
return;
|
|
221
|
+
entry.settled = true;
|
|
222
|
+
clearTimeout(entry.timer);
|
|
223
|
+
this.pending.delete(entry.req.id);
|
|
224
|
+
const duration = Date.now() - entry.req.createdAt;
|
|
225
|
+
const result = {
|
|
226
|
+
approved,
|
|
227
|
+
reason: humanReason,
|
|
228
|
+
requestId: entry.req.id,
|
|
229
|
+
duration,
|
|
230
|
+
category: entry.req.category,
|
|
231
|
+
};
|
|
232
|
+
const audit = {
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
requestId: entry.req.id,
|
|
235
|
+
tool: entry.req.tool,
|
|
236
|
+
operationHash: entry.req.operationHash,
|
|
237
|
+
category: entry.req.category,
|
|
238
|
+
workspaceId: entry.req.workspaceId,
|
|
239
|
+
approved,
|
|
240
|
+
reason,
|
|
241
|
+
durationMs: duration,
|
|
242
|
+
};
|
|
243
|
+
(0, auditLog_1.appendAudit)(audit);
|
|
244
|
+
entry.resolve(result);
|
|
245
|
+
}
|
|
246
|
+
isLocalOrigin(req) {
|
|
247
|
+
const origin = req.headers.origin;
|
|
248
|
+
if (!origin)
|
|
249
|
+
return true; // non-browser client
|
|
250
|
+
try {
|
|
251
|
+
const u = new URL(origin);
|
|
252
|
+
return u.hostname === LOOPBACK;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
isLocalHost(req) {
|
|
259
|
+
const host = req.headers.host || '';
|
|
260
|
+
try {
|
|
261
|
+
const { hostname } = new URL(`http://${host}`);
|
|
262
|
+
return hostname === LOOPBACK || hostname === 'localhost';
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
192
268
|
handle(req, res) {
|
|
269
|
+
if (!this.isLocalHost(req)) {
|
|
270
|
+
res.writeHead(403, SECURITY_HEADERS);
|
|
271
|
+
res.end('Forbidden');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
193
274
|
const url = req.url || '/';
|
|
194
275
|
if (req.method === 'GET' && (url === '/' || url.startsWith('/?'))) {
|
|
195
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
276
|
+
res.writeHead(200, { 'Content-Type': 'text/html', ...SECURITY_HEADERS });
|
|
196
277
|
res.end(APPROVAL_PAGE_HTML);
|
|
197
278
|
return;
|
|
198
279
|
}
|
|
280
|
+
if (req.method === 'GET' && url.startsWith('/details')) {
|
|
281
|
+
this.handleDetails(req, res);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
199
284
|
if (req.method === 'POST' && req.url === '/decide') {
|
|
200
|
-
|
|
201
|
-
req.on('data', (c) => { body += c; });
|
|
202
|
-
req.on('end', () => {
|
|
203
|
-
try {
|
|
204
|
-
const data = JSON.parse(body);
|
|
205
|
-
const requestId = data.id;
|
|
206
|
-
const entry = requestId ? this.pending.get(requestId) : undefined;
|
|
207
|
-
if (!entry || !requestId) {
|
|
208
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
209
|
-
res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if ((0, types_1.isExpired)(entry.req)) {
|
|
213
|
-
this.pending.delete(requestId);
|
|
214
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
215
|
-
res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
this.pending.delete(requestId);
|
|
219
|
-
const approved = data.approved === true;
|
|
220
|
-
const startTime = entry.req.createdAt;
|
|
221
|
-
const result = {
|
|
222
|
-
approved,
|
|
223
|
-
reason: approved ? 'User approved' : 'User denied',
|
|
224
|
-
requestId: entry.req.id,
|
|
225
|
-
duration: Date.now() - startTime,
|
|
226
|
-
};
|
|
227
|
-
(0, auditLog_1.appendAudit)({
|
|
228
|
-
timestamp: new Date().toISOString(),
|
|
229
|
-
requestId: entry.req.id,
|
|
230
|
-
tool: entry.req.tool,
|
|
231
|
-
operationHash: entry.req.operationHash,
|
|
232
|
-
approved,
|
|
233
|
-
reason: approved ? 'approved' : 'denied',
|
|
234
|
-
durationMs: result.duration,
|
|
235
|
-
});
|
|
236
|
-
entry.resolve(result);
|
|
237
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
238
|
-
res.end(JSON.stringify({ ok: true }));
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
242
|
-
res.end(JSON.stringify({ ok: false, error: 'Invalid request body' }));
|
|
243
|
-
}
|
|
244
|
-
});
|
|
285
|
+
this.handleDecide(req, res);
|
|
245
286
|
return;
|
|
246
287
|
}
|
|
247
|
-
res.writeHead(404);
|
|
288
|
+
res.writeHead(404, SECURITY_HEADERS);
|
|
248
289
|
res.end('Not found');
|
|
249
290
|
}
|
|
291
|
+
handleDetails(req, res) {
|
|
292
|
+
const parsed = new URL(req.url || '', `http://${LOOPBACK}`);
|
|
293
|
+
const id = parsed.searchParams.get('id');
|
|
294
|
+
const entry = id ? this.pending.get(id) : undefined;
|
|
295
|
+
if (!entry || !id) {
|
|
296
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
297
|
+
res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if ((0, types_1.isExpired)(entry.req)) {
|
|
301
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
302
|
+
res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
306
|
+
res.end(JSON.stringify({
|
|
307
|
+
ok: true,
|
|
308
|
+
tool: entry.req.tool,
|
|
309
|
+
summary: entry.req.summary,
|
|
310
|
+
operationHash: entry.req.operationHash,
|
|
311
|
+
expiresAt: entry.req.expiresAt,
|
|
312
|
+
decisionToken: entry.req.decisionToken,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
handleDecide(req, res) {
|
|
316
|
+
if (!this.isLocalOrigin(req)) {
|
|
317
|
+
res.writeHead(403, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
318
|
+
res.end(JSON.stringify({ ok: false, error: 'Forbidden origin' }));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const ct = req.headers['content-type'] || '';
|
|
322
|
+
if (!ct.includes('application/json')) {
|
|
323
|
+
res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
324
|
+
res.end(JSON.stringify({ ok: false, error: 'Content-Type must be application/json' }));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let body = '';
|
|
328
|
+
let tooLarge = false;
|
|
329
|
+
req.on('data', (c) => {
|
|
330
|
+
body += c;
|
|
331
|
+
if (body.length > MAX_BODY_BYTES) {
|
|
332
|
+
tooLarge = true;
|
|
333
|
+
req.destroy();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
req.on('end', () => {
|
|
337
|
+
if (tooLarge) {
|
|
338
|
+
res.writeHead(413, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
339
|
+
res.end(JSON.stringify({ ok: false, error: 'Body too large' }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const data = JSON.parse(body);
|
|
344
|
+
const requestId = data.id;
|
|
345
|
+
const entry = requestId ? this.pending.get(requestId) : undefined;
|
|
346
|
+
if (!entry || !requestId) {
|
|
347
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
348
|
+
res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (entry.settled) {
|
|
352
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
353
|
+
res.end(JSON.stringify({ ok: false, error: 'Request already answered' }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!data.decisionToken || data.decisionToken !== entry.req.decisionToken) {
|
|
357
|
+
this.settle(entry, false, 'invalid-token', 'Invalid decision token');
|
|
358
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
359
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid decision token' }));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if ((0, types_1.isExpired)(entry.req)) {
|
|
363
|
+
this.settle(entry, false, 'expired', 'Request expired');
|
|
364
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
365
|
+
res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const approved = data.approved === true;
|
|
369
|
+
this.settle(entry, approved, approved ? 'approved' : 'denied', approved ? 'User approved' : 'User denied');
|
|
370
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
371
|
+
res.end(JSON.stringify({ ok: true }));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
|
|
375
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid request body' }));
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
250
379
|
}
|
|
251
380
|
exports.ApprovalBroker = ApprovalBroker;
|
|
252
381
|
//# sourceMappingURL=broker.js.map
|