@s-gw/s-gw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +35 -0
- package/.mcp.json +16 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +197 -0
- package/TRADEMARKS.md +9 -0
- package/assets/icons/aws-ec2.png +0 -0
- package/assets/icons/lucide/bot.svg +8 -0
- package/assets/icons/lucide/monitor.svg +5 -0
- package/assets/icons/lucide/server.svg +6 -0
- package/assets/icons/lucide/terminal.svg +4 -0
- package/assets/icons/s-gw-128.png +0 -0
- package/assets/icons/s-gw-16.png +0 -0
- package/assets/icons/s-gw-180.png +0 -0
- package/assets/icons/s-gw-192.png +0 -0
- package/assets/icons/s-gw-32.png +0 -0
- package/assets/icons/s-gw-64.png +0 -0
- package/assets/icons/s-gw-menu-bar-template.png +0 -0
- package/dist/agent-context.d.ts +17 -0
- package/dist/agent-context.js +207 -0
- package/dist/agents.d.ts +64 -0
- package/dist/agents.js +763 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1385 -0
- package/dist/command-suggest.d.ts +3 -0
- package/dist/command-suggest.js +131 -0
- package/dist/console-server.d.ts +16 -0
- package/dist/console-server.js +978 -0
- package/dist/console-ui/assets/codex-DYTPdPxi.png +0 -0
- package/dist/console-ui/assets/cursor-CBrUTJD-.png +0 -0
- package/dist/console-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/console-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/console-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/console-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/console-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/console-ui/assets/hermes-B8hNbJPm.png +0 -0
- package/dist/console-ui/assets/index-BxUf0Sye.js +96 -0
- package/dist/console-ui/assets/index-CmTiBR_w.css +2 -0
- package/dist/console-ui/assets/omnigent-Cxa4p2Mq.png +0 -0
- package/dist/console-ui/assets/openclaw-C5wL4ZVW.png +0 -0
- package/dist/console-ui/assets/opencode-D_wFATSC.png +0 -0
- package/dist/console-ui/assets/openhands-DnrlGgev.svg +9 -0
- package/dist/console-ui/assets/s-gw-64-ByMUGQ3K.png +0 -0
- package/dist/console-ui/assets/vscode-Bdtr9eyf.png +0 -0
- package/dist/console-ui/assets/zeptoclaw-DztQW8Sw.png +0 -0
- package/dist/console-ui/index.html +13 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.js +53 -0
- package/dist/executor.d.ts +7 -0
- package/dist/executor.js +297 -0
- package/dist/gateway.d.ts +31 -0
- package/dist/gateway.js +114 -0
- package/dist/guard.d.ts +61 -0
- package/dist/guard.js +247 -0
- package/dist/install.d.ts +146 -0
- package/dist/install.js +629 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +119 -0
- package/dist/native/s-gw-core +0 -0
- package/dist/native/s-gw-keychain-helper +0 -0
- package/dist/onepassword.d.ts +48 -0
- package/dist/onepassword.js +412 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +22 -0
- package/dist/s-gw Menu Bar.app/Contents/Info.plist +28 -0
- package/dist/s-gw Menu Bar.app/Contents/MacOS/s-gw-menu-bar-helper +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/AwsEc2.png +0 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-bot.svg +8 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-monitor.svg +5 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-server.svg +6 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-terminal.svg +4 -0
- package/dist/s-gw Menu Bar.app/Contents/Resources/MenuBarTemplate.png +0 -0
- package/dist/s-gw Menu Bar.app/Contents/_CodeSignature/CodeResources +194 -0
- package/dist/s-gw.app/Contents/Info.plist +28 -0
- package/dist/s-gw.app/Contents/MacOS/s-gw +0 -0
- package/dist/s-gw.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/s-gw.app/Contents/Resources/MenuBarTemplate.png +0 -0
- package/dist/s-gw.app/Contents/_CodeSignature/CodeResources +139 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.js +437 -0
- package/dist/ssh.d.ts +31 -0
- package/dist/ssh.js +286 -0
- package/dist/store.d.ts +131 -0
- package/dist/store.js +1611 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +2 -0
- package/dist/unlock.d.ts +29 -0
- package/dist/unlock.js +274 -0
- package/dist/windows/VERSION.txt +1 -0
- package/dist/windows/s-gw-client.cmd +4 -0
- package/dist/windows/s-gw-client.ps1 +106 -0
- package/dist/windows/s-gw-credential.cmd +4 -0
- package/dist/windows/s-gw-credential.ps1 +167 -0
- package/dist/windows/s-gw-helper.cmd +4 -0
- package/dist/windows/s-gw-helper.ps1 +180 -0
- package/docs/README.md +23 -0
- package/docs/agents.md +160 -0
- package/docs/architecture.md +72 -0
- package/docs/deployment.md +447 -0
- package/docs/detection.md +44 -0
- package/docs/images/s-gw-overview.png +0 -0
- package/docs/integrations.md +195 -0
- package/docs/keychain.md +39 -0
- package/docs/onepassword.md +84 -0
- package/docs/quickstart.md +104 -0
- package/docs/threat-model.md +100 -0
- package/docs/ui/THIRD_PARTY_NOTICES.md +111 -0
- package/docs/ui/apple-touch-icon.png +0 -0
- package/docs/ui/favicon-32.png +0 -0
- package/docs/ui/local-console.html +4477 -0
- package/docs/ui/vendor/d3-sankey/d3-array.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-array.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-path.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-path.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-sankey.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-sankey.min.js +2 -0
- package/docs/ui/vendor/d3-sankey/d3-shape.LICENSE.txt +27 -0
- package/docs/ui/vendor/d3-sankey/d3-shape.min.js +2 -0
- package/docs/ui/vendor/sankeymatic/LICENSE.txt +17 -0
- package/docs/ui/vendor/sankeymatic/sankey.js +897 -0
- package/package.json +117 -0
- package/skills/s-gw/SKILL.md +19 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { executeApprovedRequest } from "./executor.js";
|
|
8
|
+
import { requestAgentName } from "./agent-context.js";
|
|
9
|
+
import { addLocalSecret, buildEnvCommandAction, preferredLocalSecretBackend, scanLocalText } from "./gateway.js";
|
|
10
|
+
import { getAgentCodeGuardPlan, listAgentProfiles, renderAgentMcpSnippet, resolveAgentProfile } from "./agents.js";
|
|
11
|
+
import { readinessForUnlock } from "./install.js";
|
|
12
|
+
import { SecretStore } from "./store.js";
|
|
13
|
+
import { unlockStatus } from "./unlock.js";
|
|
14
|
+
const version = "0.1.0";
|
|
15
|
+
const maxBodyBytes = 1024 * 1024;
|
|
16
|
+
export async function startConsoleServer(options = {}) {
|
|
17
|
+
const host = options.host || "127.0.0.1";
|
|
18
|
+
const port = options.port ?? 8718;
|
|
19
|
+
const token = options.token || randomBytes(24).toString("base64url");
|
|
20
|
+
const store = options.store || new SecretStore();
|
|
21
|
+
const uiDir = options.uiDir || defaultUiDir();
|
|
22
|
+
await store.init();
|
|
23
|
+
const server = createServer((req, res) => {
|
|
24
|
+
handleRequest(req, res, store, token, uiDir).catch((error) => {
|
|
25
|
+
sendError(res, error);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
server.once("error", reject);
|
|
30
|
+
server.listen(port, host, () => {
|
|
31
|
+
server.off("error", reject);
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
const address = server.address();
|
|
36
|
+
const shownHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
37
|
+
const url = `http://${shownHost}:${address.port}/`;
|
|
38
|
+
return {
|
|
39
|
+
url,
|
|
40
|
+
token,
|
|
41
|
+
server,
|
|
42
|
+
close: () => closeServer(server)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function closeServer(server) {
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
48
|
+
server.closeIdleConnections();
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
server.closeAllConnections();
|
|
51
|
+
}, 250).unref();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function handleRequest(req, res, store, token, uiDir) {
|
|
55
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
56
|
+
if (url.pathname.startsWith("/api/")) {
|
|
57
|
+
if (url.pathname !== "/api/health" && !validConsoleToken(req, token)) {
|
|
58
|
+
throw new HttpError(403, "Missing or invalid local console token.");
|
|
59
|
+
}
|
|
60
|
+
await handleApi(req, res, url, store);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (url.pathname === "/favicon.ico") {
|
|
64
|
+
res.writeHead(204, { "Cache-Control": "no-store" });
|
|
65
|
+
res.end();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await serveUi(req, res, url.pathname, uiDir, token);
|
|
69
|
+
}
|
|
70
|
+
async function handleApi(req, res, url, store) {
|
|
71
|
+
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
72
|
+
sendJson(res, 200, { ok: true, name: "s-gw", version });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (req.method === "GET" && url.pathname === "/api/state") {
|
|
76
|
+
sendJson(res, 200, await buildState(store));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (req.method === "GET" && url.pathname === "/api/audit.csv") {
|
|
80
|
+
sendCsv(res, await auditCsv(store));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (req.method === "GET" && url.pathname === "/api/approval") {
|
|
84
|
+
sendJson(res, 200, await store.getApprovalSettings());
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (req.method === "GET" && url.pathname === "/api/approval/grants") {
|
|
88
|
+
sendJson(res, 200, await store.listApprovalGrants());
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (req.method === "GET" && url.pathname === "/api/approval/policies") {
|
|
92
|
+
sendJson(res, 200, await store.listApprovalPolicyRules());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (req.method === "POST" && url.pathname === "/api/approval/policies") {
|
|
96
|
+
const body = await readJson(req);
|
|
97
|
+
sendJson(res, 200, await store.addApprovalPolicyRule({
|
|
98
|
+
name: optionalString(body.name),
|
|
99
|
+
enabled: body.enabled !== false,
|
|
100
|
+
priority: optionalNumberValue(body.priority),
|
|
101
|
+
decision: approvalPolicyDecision(body.decision),
|
|
102
|
+
expiresAt: optionalString(body.expiresAt),
|
|
103
|
+
durationMs: optionalNumberValue(body.durationMs),
|
|
104
|
+
conditions: {
|
|
105
|
+
handles: stringArray(body.handles),
|
|
106
|
+
secretTypes: stringArray(body.secretTypes).map(secretType),
|
|
107
|
+
providers: stringArray(body.providers),
|
|
108
|
+
minSeverity: optionalSecretSeverity(body.minSeverity),
|
|
109
|
+
agents: stringArray(body.agents),
|
|
110
|
+
actionKinds: stringArray(body.actionKinds).map(approvalPolicyActionKind),
|
|
111
|
+
commands: stringArray(body.commands),
|
|
112
|
+
injectEnvs: stringArray(body.injectEnvs),
|
|
113
|
+
workingDirs: stringArray(body.workingDirs),
|
|
114
|
+
sshTargets: stringArray(body.sshTargets),
|
|
115
|
+
sshPorts: stringArray(body.sshPorts).map((item) => Number(item))
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (req.method === "DELETE" && url.pathname === "/api/approval/grants") {
|
|
121
|
+
sendJson(res, 200, await store.clearApprovalGrants());
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const approvalGrantMatch = url.pathname.match(/^\/api\/approval\/grants\/([^/]+)$/);
|
|
125
|
+
if (req.method === "DELETE" && approvalGrantMatch) {
|
|
126
|
+
sendJson(res, 200, await store.revokeApprovalGrant(decodeURIComponent(approvalGrantMatch[1])));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const approvalPolicyMatch = url.pathname.match(/^\/api\/approval\/policies\/([^/]+)$/);
|
|
130
|
+
if (approvalPolicyMatch) {
|
|
131
|
+
const id = decodeURIComponent(approvalPolicyMatch[1]);
|
|
132
|
+
if (req.method === "DELETE") {
|
|
133
|
+
sendJson(res, 200, await store.deleteApprovalPolicyRule(id));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (req.method === "PATCH") {
|
|
137
|
+
const body = await readJson(req);
|
|
138
|
+
if (typeof body.enabled !== "boolean") {
|
|
139
|
+
throw new HttpError(400, "enabled must be a boolean.");
|
|
140
|
+
}
|
|
141
|
+
sendJson(res, 200, await store.setApprovalPolicyRuleEnabled(id, body.enabled));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (req.method === "POST" && url.pathname === "/api/approval") {
|
|
146
|
+
const body = await readJson(req);
|
|
147
|
+
sendJson(res, 200, await store.setApprovalSettings({
|
|
148
|
+
mode: approvalMode(body.mode),
|
|
149
|
+
durationMs: numberValue(body.durationMs, 15 * 60 * 1000)
|
|
150
|
+
}));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (req.method === "POST" && url.pathname === "/api/scan") {
|
|
154
|
+
const body = await readJson(req);
|
|
155
|
+
const text = stringValue(body.text, "text");
|
|
156
|
+
const persist = body.persist === true;
|
|
157
|
+
const source = optionalString(body.source);
|
|
158
|
+
sendJson(res, 200, await scanLocalText(store, text, { persist, source, backend: localBackendValue(body.backend) }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (req.method === "POST" && url.pathname === "/api/secrets") {
|
|
162
|
+
const body = await readJson(req);
|
|
163
|
+
const record = await addLocalSecret(store, {
|
|
164
|
+
name: stringValue(body.name, "name"),
|
|
165
|
+
type: secretType(body.type),
|
|
166
|
+
provider: optionalString(body.provider),
|
|
167
|
+
value: stringValue(body.value, "value"),
|
|
168
|
+
source: optionalString(body.source),
|
|
169
|
+
service: optionalString(body.service),
|
|
170
|
+
policy: {
|
|
171
|
+
injectEnv: optionalString(body.injectEnv),
|
|
172
|
+
allowedCommands: stringList(body.allowedCommands),
|
|
173
|
+
maxOutputBytes: numberValue(body.maxOutputBytes, 16_384)
|
|
174
|
+
}
|
|
175
|
+
}, localBackendValue(body.backend));
|
|
176
|
+
sendJson(res, 201, {
|
|
177
|
+
handle: record.handle,
|
|
178
|
+
name: record.name,
|
|
179
|
+
type: record.type,
|
|
180
|
+
provider: record.provider,
|
|
181
|
+
backend: record.backend,
|
|
182
|
+
policy: record.policy
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const secretMatch = url.pathname.match(/^\/api\/secrets\/([^/]+)$/);
|
|
187
|
+
if (req.method === "DELETE" && secretMatch) {
|
|
188
|
+
const handle = decodeURIComponent(secretMatch[1]);
|
|
189
|
+
sendJson(res, 200, await store.deleteSecret(handle));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (req.method === "POST" && url.pathname === "/api/requests") {
|
|
193
|
+
const body = await readJson(req);
|
|
194
|
+
const action = buildEnvCommandAction({
|
|
195
|
+
command: stringValue(body.command, "command"),
|
|
196
|
+
args: stringList(body.args),
|
|
197
|
+
injectEnv: stringValue(body.injectEnv, "injectEnv"),
|
|
198
|
+
env: envBindingList(body.env),
|
|
199
|
+
workingDir: optionalString(body.workingDir),
|
|
200
|
+
timeoutMs: numberValue(body.timeoutMs, 30_000)
|
|
201
|
+
});
|
|
202
|
+
const request = await store.createRequest(stringValue(body.handle, "handle"), action, optionalString(body.reason) || "Local console request", {
|
|
203
|
+
agentName: optionalString(body.agentName),
|
|
204
|
+
env: {}
|
|
205
|
+
});
|
|
206
|
+
sendJson(res, 201, request);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const match = url.pathname.match(/^\/api\/requests\/([^/]+)\/(approve|deny|execute|recover)$/);
|
|
210
|
+
if (req.method === "POST" && match) {
|
|
211
|
+
const id = decodeURIComponent(match[1]);
|
|
212
|
+
const action = match[2];
|
|
213
|
+
if (action === "approve") {
|
|
214
|
+
const body = await readJson(req);
|
|
215
|
+
sendJson(res, 200, await store.approveRequest(id, {
|
|
216
|
+
mode: optionalApprovalMode(body.mode),
|
|
217
|
+
durationMs: optionalNumberValue(body.durationMs),
|
|
218
|
+
agentScope: optionalApprovalAgentScope(body.agentScope)
|
|
219
|
+
}));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (action === "deny") {
|
|
223
|
+
sendJson(res, 200, await store.denyRequest(id));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (action === "recover") {
|
|
227
|
+
const recovered = await store.forceRecoverExecutions(id);
|
|
228
|
+
sendJson(res, 200, recovered[0]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
sendJson(res, 200, await executeApprovedRequest(store, id));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
throw new HttpError(404, "Unknown console API route.");
|
|
235
|
+
}
|
|
236
|
+
async function buildState(store) {
|
|
237
|
+
const handles = await store.listHandles();
|
|
238
|
+
const requests = await store.listRequests();
|
|
239
|
+
const audit = await store.auditLog();
|
|
240
|
+
const approvalSettings = await store.getApprovalSettings();
|
|
241
|
+
const approvalGrants = await store.listApprovalGrants();
|
|
242
|
+
const approvalPolicyRules = await store.listApprovalPolicyRules();
|
|
243
|
+
const pending = requests.filter((request) => request.state === "pending");
|
|
244
|
+
const highRisk = handles.filter((handle) => handle.severity === "high" || handle.severity === "critical");
|
|
245
|
+
const agents = listAgentProfiles();
|
|
246
|
+
const unlock = unlockStatus();
|
|
247
|
+
const readiness = readinessForUnlock(unlock.activeSource !== "none");
|
|
248
|
+
const usageFlow = buildUsageFlow(requests, handles);
|
|
249
|
+
return {
|
|
250
|
+
version,
|
|
251
|
+
ready: readiness.ok,
|
|
252
|
+
readiness,
|
|
253
|
+
status: {
|
|
254
|
+
daemonRunning: true,
|
|
255
|
+
storePath: store.storePath,
|
|
256
|
+
unlock
|
|
257
|
+
},
|
|
258
|
+
metrics: {
|
|
259
|
+
localSecrets: handles.length,
|
|
260
|
+
pendingApprovals: pending.length,
|
|
261
|
+
activeAgents: agents.length,
|
|
262
|
+
highRiskFindings: highRisk.length
|
|
263
|
+
},
|
|
264
|
+
handles,
|
|
265
|
+
approvalSettings,
|
|
266
|
+
approvalGrants,
|
|
267
|
+
approvalPolicyRules,
|
|
268
|
+
usageFlow,
|
|
269
|
+
credentials: groupHandles(handles),
|
|
270
|
+
requests: sortRequests(requests),
|
|
271
|
+
pendingRequests: sortRequests(pending),
|
|
272
|
+
audit: [...audit].reverse(),
|
|
273
|
+
agents: agents.map((agent) => {
|
|
274
|
+
const profile = resolveAgentProfile(agent.id);
|
|
275
|
+
return {
|
|
276
|
+
id: agent.id,
|
|
277
|
+
name: agent.displayName,
|
|
278
|
+
status: agent.mcpStatus,
|
|
279
|
+
aliases: profile.aliases,
|
|
280
|
+
mcp: {
|
|
281
|
+
supported: profile.mcp.supported,
|
|
282
|
+
format: profile.mcp.snippet,
|
|
283
|
+
writeMode: profile.mcp.writeMode,
|
|
284
|
+
configPaths: profile.mcp.configPaths,
|
|
285
|
+
notes: profile.mcp.notes,
|
|
286
|
+
snippet: profile.mcp.supported ? renderAgentMcpSnippet(profile.id) : null
|
|
287
|
+
},
|
|
288
|
+
skills: profile.skills,
|
|
289
|
+
plugins: profile.plugins,
|
|
290
|
+
hooks: {
|
|
291
|
+
supported: profile.hooks?.supported || false,
|
|
292
|
+
kind: profile.hooks?.kind || "none",
|
|
293
|
+
configPaths: profile.hooks?.configPaths || [],
|
|
294
|
+
events: profile.hooks?.events || [],
|
|
295
|
+
notes: profile.hooks?.notes || []
|
|
296
|
+
},
|
|
297
|
+
limitations: profile.limitations,
|
|
298
|
+
codeGuard: getAgentCodeGuardPlan(profile.id),
|
|
299
|
+
snippetCommand: `s-gw agent mcp-snippet ${profile.id}`,
|
|
300
|
+
guardCommand: `s-gw run ${profile.id}`
|
|
301
|
+
};
|
|
302
|
+
})
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function groupHandles(handles) {
|
|
306
|
+
const grouped = new Map();
|
|
307
|
+
for (const handle of handles) {
|
|
308
|
+
const provider = handle.provider || providerForType(handle.type);
|
|
309
|
+
const existing = grouped.get(provider);
|
|
310
|
+
if (!existing) {
|
|
311
|
+
grouped.set(provider, {
|
|
312
|
+
provider,
|
|
313
|
+
label: providerLabel(provider),
|
|
314
|
+
prefix: handlePrefix(provider, handle.type),
|
|
315
|
+
secrets: 1,
|
|
316
|
+
severity: handle.severity || "low",
|
|
317
|
+
lastUsed: handle.updatedAt
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
existing.secrets += 1;
|
|
322
|
+
existing.severity = higherSeverity(existing.severity, handle.severity || "low");
|
|
323
|
+
if (!existing.lastUsed || handle.updatedAt > existing.lastUsed) {
|
|
324
|
+
existing.lastUsed = handle.updatedAt;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return [...grouped.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
328
|
+
}
|
|
329
|
+
function sortRequests(requests) {
|
|
330
|
+
return [...requests].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
331
|
+
}
|
|
332
|
+
function buildUsageFlow(requests, handles) {
|
|
333
|
+
const handlesById = new Map(handles.map((handle) => [handle.handle, handle]));
|
|
334
|
+
const nodeCounts = new Map();
|
|
335
|
+
const linkCounts = new Map();
|
|
336
|
+
const rowCounts = new Map();
|
|
337
|
+
const entries = [];
|
|
338
|
+
for (const request of requests) {
|
|
339
|
+
const handle = handlesById.get(request.handle);
|
|
340
|
+
const agent = request.agentName || requestAgentName(request.reason);
|
|
341
|
+
const credential = handle?.name || request.handle;
|
|
342
|
+
const authType = authTypeLabel(handle);
|
|
343
|
+
const authTypeId = `auth:${authTypeKey(handle)}`;
|
|
344
|
+
const targetType = targetTypeLabel(request.action);
|
|
345
|
+
const targetTypeId = `target:${targetTypeKey(request.action)}`;
|
|
346
|
+
const action = actionLabel(request.action);
|
|
347
|
+
const command = commandBase(request.action.command);
|
|
348
|
+
const target = actionTarget(request.action);
|
|
349
|
+
const agentId = `agent:${agent}`;
|
|
350
|
+
bumpNode(nodeCounts, agentId, "agent", agent, "Requesting agent");
|
|
351
|
+
bumpNode(nodeCounts, authTypeId, "auth", authType, authTypeDetail(handle));
|
|
352
|
+
bumpNode(nodeCounts, targetTypeId, "target", targetType, targetTypeDetail(request.action));
|
|
353
|
+
bumpLink(linkCounts, agentId, authTypeId);
|
|
354
|
+
bumpLink(linkCounts, authTypeId, targetTypeId);
|
|
355
|
+
entries.push({
|
|
356
|
+
requestId: request.id,
|
|
357
|
+
agentId,
|
|
358
|
+
agent,
|
|
359
|
+
authTypeId,
|
|
360
|
+
authType,
|
|
361
|
+
targetTypeId,
|
|
362
|
+
targetType,
|
|
363
|
+
credential,
|
|
364
|
+
action,
|
|
365
|
+
command,
|
|
366
|
+
target,
|
|
367
|
+
state: request.state,
|
|
368
|
+
lastSeen: request.updatedAt
|
|
369
|
+
});
|
|
370
|
+
const rowKey = `${agent}\n${authType}\n${targetType}`;
|
|
371
|
+
let row = rowCounts.get(rowKey);
|
|
372
|
+
if (!row) {
|
|
373
|
+
row = {
|
|
374
|
+
agentId,
|
|
375
|
+
agent,
|
|
376
|
+
authTypeId,
|
|
377
|
+
authType,
|
|
378
|
+
targetTypeId,
|
|
379
|
+
targetType,
|
|
380
|
+
handle: request.handle,
|
|
381
|
+
credential,
|
|
382
|
+
action,
|
|
383
|
+
command,
|
|
384
|
+
target,
|
|
385
|
+
handles: [],
|
|
386
|
+
credentials: [],
|
|
387
|
+
actions: [],
|
|
388
|
+
targets: [],
|
|
389
|
+
count: 0,
|
|
390
|
+
lastSeen: request.updatedAt,
|
|
391
|
+
states: emptyStateCounts()
|
|
392
|
+
};
|
|
393
|
+
rowCounts.set(rowKey, row);
|
|
394
|
+
}
|
|
395
|
+
row.count += 1;
|
|
396
|
+
row.states[request.state] += 1;
|
|
397
|
+
addFlowExample(row.handles, shortHandle(request.handle));
|
|
398
|
+
addFlowExample(row.credentials, credential);
|
|
399
|
+
addFlowExample(row.actions, action);
|
|
400
|
+
addFlowExample(row.targets, target);
|
|
401
|
+
if (request.updatedAt > row.lastSeen) {
|
|
402
|
+
row.lastSeen = request.updatedAt;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
generatedAt: new Date().toISOString(),
|
|
407
|
+
totalRequests: requests.length,
|
|
408
|
+
nodes: [...nodeCounts.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)),
|
|
409
|
+
links: [...linkCounts.values()].sort((a, b) => b.value - a.value || a.source.localeCompare(b.source)),
|
|
410
|
+
rows: [...rowCounts.values()].sort((a, b) => b.count - a.count || b.lastSeen.localeCompare(a.lastSeen)),
|
|
411
|
+
entries: entries.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen))
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function emptyStateCounts() {
|
|
415
|
+
return {
|
|
416
|
+
pending: 0,
|
|
417
|
+
approved: 0,
|
|
418
|
+
executing: 0,
|
|
419
|
+
denied: 0,
|
|
420
|
+
executed: 0,
|
|
421
|
+
failed: 0
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function bumpNode(nodes, id, kind, label, detail) {
|
|
425
|
+
const existing = nodes.get(id);
|
|
426
|
+
if (existing) {
|
|
427
|
+
existing.count += 1;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
nodes.set(id, { id, kind, label, detail, count: 1 });
|
|
431
|
+
}
|
|
432
|
+
function bumpLink(links, source, target) {
|
|
433
|
+
const key = `${source}\n${target}`;
|
|
434
|
+
const existing = links.get(key);
|
|
435
|
+
if (existing) {
|
|
436
|
+
existing.value += 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
links.set(key, { source, target, value: 1 });
|
|
440
|
+
}
|
|
441
|
+
function addFlowExample(values, value) {
|
|
442
|
+
const cleaned = String(value || "").trim();
|
|
443
|
+
if (!cleaned || values.includes(cleaned) || values.length >= 4) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
values.push(cleaned);
|
|
447
|
+
}
|
|
448
|
+
function authTypeKey(handle) {
|
|
449
|
+
return normalizeFlowKey(authTypeLabel(handle));
|
|
450
|
+
}
|
|
451
|
+
function authTypeLabel(handle) {
|
|
452
|
+
const provider = String(handle?.provider || "").toLowerCase();
|
|
453
|
+
const name = String(handle?.name || "").toLowerCase();
|
|
454
|
+
const source = String(handle?.source || "").toLowerCase();
|
|
455
|
+
const envName = String(handle?.policy.injectEnv || "").toLowerCase();
|
|
456
|
+
const haystack = `${provider} ${name} ${source} ${envName}`;
|
|
457
|
+
if (provider === "aws" || handle?.type === "access-key" || /\baws\b|amazon|access[_ -]?key|secret[_ -]?access/.test(haystack)) {
|
|
458
|
+
return "AWS access key";
|
|
459
|
+
}
|
|
460
|
+
if (provider === "github" || /\bgithub\b|\bgh\b|ghp_|github[_ -]?token/.test(haystack)) {
|
|
461
|
+
return "GitHub token";
|
|
462
|
+
}
|
|
463
|
+
if (provider === "openai" || /openai|sk-/.test(haystack)) {
|
|
464
|
+
return "OpenAI API key";
|
|
465
|
+
}
|
|
466
|
+
if (provider === "ssh" || /ssh|bastion|private[_ -]?key|rsa|ed25519/.test(haystack)) {
|
|
467
|
+
return handle?.type === "password" ? "SSH password" : "SSH private key";
|
|
468
|
+
}
|
|
469
|
+
if (handle?.type === "private-key" || handle?.type === "ssh-key") {
|
|
470
|
+
return "Private key";
|
|
471
|
+
}
|
|
472
|
+
if (handle?.type === "password") {
|
|
473
|
+
return "Password";
|
|
474
|
+
}
|
|
475
|
+
if (handle?.type === "api-token") {
|
|
476
|
+
return "API token";
|
|
477
|
+
}
|
|
478
|
+
if (handle?.type === "credential") {
|
|
479
|
+
return "Credential pair";
|
|
480
|
+
}
|
|
481
|
+
return "Unknown credential";
|
|
482
|
+
}
|
|
483
|
+
function authTypeDetail(handle) {
|
|
484
|
+
const parts = [];
|
|
485
|
+
if (handle?.severity) {
|
|
486
|
+
parts.push(`${handle.severity} risk`);
|
|
487
|
+
}
|
|
488
|
+
if (handle?.backend) {
|
|
489
|
+
parts.push(`stored in ${flowProviderLabel(handle.backend)}`);
|
|
490
|
+
}
|
|
491
|
+
return parts.join(" / ") || "Authentication type";
|
|
492
|
+
}
|
|
493
|
+
function targetTypeKey(action) {
|
|
494
|
+
return normalizeFlowKey(targetTypeLabel(action));
|
|
495
|
+
}
|
|
496
|
+
function targetTypeLabel(action) {
|
|
497
|
+
if (sshDestination(action)) {
|
|
498
|
+
return "SSH server";
|
|
499
|
+
}
|
|
500
|
+
const command = commandBase(action.command).toLowerCase();
|
|
501
|
+
const target = actionTarget(action).toLowerCase();
|
|
502
|
+
const args = action.args.join(" ").toLowerCase();
|
|
503
|
+
const envName = action.injectEnv.toLowerCase();
|
|
504
|
+
const haystack = `${command} ${target} ${args} ${envName}`;
|
|
505
|
+
if (command === "aws" || /\baws\b|\bec2\b|\bs3\b|\bsts\b|cloudformation|securityhub|iam\b/.test(haystack)) {
|
|
506
|
+
return "AWS API";
|
|
507
|
+
}
|
|
508
|
+
if (command === "gh" || command === "github" || /github|pull request|\brepo\b/.test(haystack)) {
|
|
509
|
+
return "GitHub repository";
|
|
510
|
+
}
|
|
511
|
+
if (command === "kubectl" || /kubernetes|kubeconfig|namespace/.test(haystack)) {
|
|
512
|
+
return "Kubernetes cluster";
|
|
513
|
+
}
|
|
514
|
+
if (command === "docker" || command === "podman" || /container|image|compose/.test(haystack)) {
|
|
515
|
+
return "Container runtime";
|
|
516
|
+
}
|
|
517
|
+
if (command === "curl" || command === "wget" || target.startsWith("http://") || target.startsWith("https://") || args.includes("http://") || args.includes("https://")) {
|
|
518
|
+
return "Web API";
|
|
519
|
+
}
|
|
520
|
+
if (command === "psql" || command === "mysql" || command === "redis-cli" || /database|postgres|mysql|redis/.test(haystack)) {
|
|
521
|
+
return "Database";
|
|
522
|
+
}
|
|
523
|
+
if (/\bnas\b|network.?attached.?storage|storage.?appliance|file.?server/.test(haystack)) {
|
|
524
|
+
return "NAS / appliance";
|
|
525
|
+
}
|
|
526
|
+
if (action.kind === "env_command") {
|
|
527
|
+
return "Local command";
|
|
528
|
+
}
|
|
529
|
+
return "Other target";
|
|
530
|
+
}
|
|
531
|
+
function targetTypeDetail(action) {
|
|
532
|
+
const target = actionTarget(action);
|
|
533
|
+
if (target && target !== "local command") {
|
|
534
|
+
return target;
|
|
535
|
+
}
|
|
536
|
+
return commandBase(action.command);
|
|
537
|
+
}
|
|
538
|
+
function flowProviderLabel(provider) {
|
|
539
|
+
const value = provider || "generic";
|
|
540
|
+
if (value === "aws")
|
|
541
|
+
return "AWS";
|
|
542
|
+
if (value === "github")
|
|
543
|
+
return "GitHub";
|
|
544
|
+
if (value === "openai")
|
|
545
|
+
return "OpenAI";
|
|
546
|
+
if (value === "ssh")
|
|
547
|
+
return "SSH";
|
|
548
|
+
if (value === "onepassword")
|
|
549
|
+
return "1Password";
|
|
550
|
+
if (value === "keychain" || value === "macos-keychain")
|
|
551
|
+
return "macOS Keychain";
|
|
552
|
+
if (value === "windows-credential-manager")
|
|
553
|
+
return "Windows Credential Manager";
|
|
554
|
+
return titleCase(value);
|
|
555
|
+
}
|
|
556
|
+
function normalizeFlowKey(value) {
|
|
557
|
+
return String(value || "unknown").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
558
|
+
}
|
|
559
|
+
function titleCase(value) {
|
|
560
|
+
return String(value || "unknown")
|
|
561
|
+
.split(/[-_\s]+/)
|
|
562
|
+
.filter(Boolean)
|
|
563
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
564
|
+
.join(" ");
|
|
565
|
+
}
|
|
566
|
+
function actionLabel(action) {
|
|
567
|
+
const command = action.kind === "ssh_session" ? "ssh" : commandBase(action.command);
|
|
568
|
+
const target = actionTarget(action);
|
|
569
|
+
if (!target || target === "local command") {
|
|
570
|
+
return command;
|
|
571
|
+
}
|
|
572
|
+
return `${command} -> ${target}`;
|
|
573
|
+
}
|
|
574
|
+
function actionTarget(action) {
|
|
575
|
+
const sshTarget = sshDestination(action);
|
|
576
|
+
if (sshTarget)
|
|
577
|
+
return sshTarget;
|
|
578
|
+
if (action.workingDir) {
|
|
579
|
+
return action.workingDir;
|
|
580
|
+
}
|
|
581
|
+
if (action.args[0] === "-e") {
|
|
582
|
+
return `${commandBase(action.command)} inline script`;
|
|
583
|
+
}
|
|
584
|
+
for (const arg of action.args) {
|
|
585
|
+
if (arg && !arg.startsWith("-")) {
|
|
586
|
+
return arg;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return action.injectEnv || "local command";
|
|
590
|
+
}
|
|
591
|
+
const sshOptionsWithValue = new Set([
|
|
592
|
+
"-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J", "-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S", "-W", "-w"
|
|
593
|
+
]);
|
|
594
|
+
function sshDestination(action) {
|
|
595
|
+
if (action.kind === "ssh_session" && action.ssh?.target) {
|
|
596
|
+
return action.ssh.port && action.ssh.port !== 22 ? `${action.ssh.target}:${action.ssh.port}` : action.ssh.target;
|
|
597
|
+
}
|
|
598
|
+
if (commandBase(action.command).toLowerCase() !== "ssh")
|
|
599
|
+
return undefined;
|
|
600
|
+
let skipNext = false;
|
|
601
|
+
for (const arg of action.args) {
|
|
602
|
+
if (skipNext) {
|
|
603
|
+
skipNext = false;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (sshOptionsWithValue.has(arg)) {
|
|
607
|
+
skipNext = true;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (arg === "--" || arg.startsWith("-"))
|
|
611
|
+
continue;
|
|
612
|
+
return arg;
|
|
613
|
+
}
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
function commandBase(command) {
|
|
617
|
+
const normalized = String(command || "").replace(/\\/g, "/");
|
|
618
|
+
const parts = normalized.split("/");
|
|
619
|
+
return parts[parts.length - 1] || normalized || "local command";
|
|
620
|
+
}
|
|
621
|
+
function shortHandle(handle) {
|
|
622
|
+
if (handle.length <= 34) {
|
|
623
|
+
return handle;
|
|
624
|
+
}
|
|
625
|
+
return `${handle.slice(0, 18)}...${handle.slice(-8)}`;
|
|
626
|
+
}
|
|
627
|
+
async function auditCsv(store) {
|
|
628
|
+
const audit = await store.auditLog();
|
|
629
|
+
const rows = [["time", "event", "handle", "request", "message"]];
|
|
630
|
+
for (const event of audit) {
|
|
631
|
+
rows.push([event.ts, event.type, event.handle || "", event.requestId || "", event.message]);
|
|
632
|
+
}
|
|
633
|
+
return rows.map((row) => row.map(csvCell).join(",")).join("\n") + "\n";
|
|
634
|
+
}
|
|
635
|
+
async function serveUi(req, res, pathname, uiDir, token) {
|
|
636
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
637
|
+
throw new HttpError(405, "Only GET is supported for local console assets.");
|
|
638
|
+
}
|
|
639
|
+
const safeRoot = path.resolve(uiDir);
|
|
640
|
+
let relative = uiRelativePath(pathname, uiDir);
|
|
641
|
+
let target = path.resolve(uiDir, relative);
|
|
642
|
+
if (target !== safeRoot && !target.startsWith(`${safeRoot}${path.sep}`)) {
|
|
643
|
+
throw new HttpError(403, "Path is outside the local console directory.");
|
|
644
|
+
}
|
|
645
|
+
if (!existsSync(target) && shouldServeSpaFallback(pathname, uiDir)) {
|
|
646
|
+
relative = "index.html";
|
|
647
|
+
target = path.resolve(uiDir, relative);
|
|
648
|
+
}
|
|
649
|
+
let body = await readFile(target);
|
|
650
|
+
if (path.basename(target) === "local-console.html" || path.basename(target) === "index.html") {
|
|
651
|
+
body = Buffer.from(injectConsoleToken(body.toString("utf8"), token));
|
|
652
|
+
}
|
|
653
|
+
res.writeHead(200, {
|
|
654
|
+
"Content-Type": contentType(target),
|
|
655
|
+
"Cache-Control": "no-store"
|
|
656
|
+
});
|
|
657
|
+
if (req.method === "HEAD") {
|
|
658
|
+
res.end();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
res.end(body);
|
|
662
|
+
}
|
|
663
|
+
function uiRelativePath(pathname, uiDir) {
|
|
664
|
+
if (pathname === "/" || pathname === "") {
|
|
665
|
+
return existsSync(path.join(uiDir, "index.html")) ? "index.html" : "local-console.html";
|
|
666
|
+
}
|
|
667
|
+
let cleaned = decodeURIComponent(pathname).replace(/^\/+/, "");
|
|
668
|
+
cleaned = cleaned.replace(/^docs\/ui\//, "");
|
|
669
|
+
return cleaned || (existsSync(path.join(uiDir, "index.html")) ? "index.html" : "local-console.html");
|
|
670
|
+
}
|
|
671
|
+
function shouldServeSpaFallback(pathname, uiDir) {
|
|
672
|
+
if (!existsSync(path.join(uiDir, "index.html"))) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
const cleaned = decodeURIComponent(pathname || "").replace(/^\/+/, "");
|
|
676
|
+
if (!cleaned) {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
const ext = path.extname(cleaned);
|
|
680
|
+
return ext === "";
|
|
681
|
+
}
|
|
682
|
+
function injectConsoleToken(html, token) {
|
|
683
|
+
const script = `<script>window.SGW_CONSOLE_TOKEN=${JSON.stringify(token)};window.SGW_CONSOLE_LIVE=true;</script>`;
|
|
684
|
+
return html.includes("</head>") ? html.replace("</head>", `${script}\n</head>`) : `${script}\n${html}`;
|
|
685
|
+
}
|
|
686
|
+
async function readJson(req) {
|
|
687
|
+
const chunks = [];
|
|
688
|
+
let size = 0;
|
|
689
|
+
for await (const chunk of req) {
|
|
690
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
691
|
+
size += buffer.byteLength;
|
|
692
|
+
if (size > maxBodyBytes) {
|
|
693
|
+
throw new HttpError(413, "Request body is too large.");
|
|
694
|
+
}
|
|
695
|
+
chunks.push(buffer);
|
|
696
|
+
}
|
|
697
|
+
if (chunks.length === 0) {
|
|
698
|
+
return {};
|
|
699
|
+
}
|
|
700
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
701
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
702
|
+
throw new HttpError(400, "JSON body must be an object.");
|
|
703
|
+
}
|
|
704
|
+
return parsed;
|
|
705
|
+
}
|
|
706
|
+
function validConsoleToken(req, expected) {
|
|
707
|
+
const header = req.headers["x-sgw-console-token"];
|
|
708
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
709
|
+
if (!token || token.length !== expected.length) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
|
|
713
|
+
}
|
|
714
|
+
function stringValue(value, name) {
|
|
715
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
716
|
+
throw new HttpError(400, `${name} is required.`);
|
|
717
|
+
}
|
|
718
|
+
return value;
|
|
719
|
+
}
|
|
720
|
+
function optionalString(value) {
|
|
721
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
722
|
+
}
|
|
723
|
+
function localBackendValue(value) {
|
|
724
|
+
if (value === undefined || value === null || value === "") {
|
|
725
|
+
return preferredLocalSecretBackend();
|
|
726
|
+
}
|
|
727
|
+
if (value === "local" || value === "keychain") {
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
throw new HttpError(400, "backend must be either local or keychain.");
|
|
731
|
+
}
|
|
732
|
+
function stringList(value) {
|
|
733
|
+
if (value === undefined) {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
if (!Array.isArray(value)) {
|
|
737
|
+
throw new HttpError(400, "Expected an array of strings.");
|
|
738
|
+
}
|
|
739
|
+
const out = [];
|
|
740
|
+
for (const item of value) {
|
|
741
|
+
if (typeof item !== "string") {
|
|
742
|
+
throw new HttpError(400, "Expected an array of strings.");
|
|
743
|
+
}
|
|
744
|
+
out.push(item);
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
function envBindingList(value) {
|
|
749
|
+
if (value === undefined) {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
if (!Array.isArray(value)) {
|
|
753
|
+
throw new HttpError(400, "env must be an array of {handle, injectEnv} objects.");
|
|
754
|
+
}
|
|
755
|
+
return value.map((item) => {
|
|
756
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
757
|
+
throw new HttpError(400, "env must be an array of {handle, injectEnv} objects.");
|
|
758
|
+
}
|
|
759
|
+
const record = item;
|
|
760
|
+
return {
|
|
761
|
+
handle: stringValue(record.handle, "env.handle"),
|
|
762
|
+
injectEnv: stringValue(record.injectEnv, "env.injectEnv")
|
|
763
|
+
};
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function numberValue(value, fallback) {
|
|
767
|
+
if (value === undefined || value === null || value === "") {
|
|
768
|
+
return fallback;
|
|
769
|
+
}
|
|
770
|
+
const parsed = Number(value);
|
|
771
|
+
if (!Number.isFinite(parsed)) {
|
|
772
|
+
throw new HttpError(400, "Expected a finite number.");
|
|
773
|
+
}
|
|
774
|
+
return parsed;
|
|
775
|
+
}
|
|
776
|
+
function secretType(input) {
|
|
777
|
+
const allowed = [
|
|
778
|
+
"api-token",
|
|
779
|
+
"ssh-key",
|
|
780
|
+
"private-key",
|
|
781
|
+
"password",
|
|
782
|
+
"credential",
|
|
783
|
+
"access-key",
|
|
784
|
+
"unknown"
|
|
785
|
+
];
|
|
786
|
+
return allowed.includes(input) ? input : "unknown";
|
|
787
|
+
}
|
|
788
|
+
function approvalMode(input) {
|
|
789
|
+
if (input === "per-transaction" || input === "timed-session" || input === "login-session" || input === "always") {
|
|
790
|
+
return input;
|
|
791
|
+
}
|
|
792
|
+
throw new HttpError(400, "mode must be per-transaction, timed-session, login-session, or always.");
|
|
793
|
+
}
|
|
794
|
+
function approvalPolicyDecision(input) {
|
|
795
|
+
if (input === "ask" || input === "allow" || input === "deny") {
|
|
796
|
+
return input;
|
|
797
|
+
}
|
|
798
|
+
throw new HttpError(400, "decision must be ask, allow, or deny.");
|
|
799
|
+
}
|
|
800
|
+
function approvalPolicyActionKind(input) {
|
|
801
|
+
if (input === "env_command" || input === "env-command" || input === "command") {
|
|
802
|
+
return "env_command";
|
|
803
|
+
}
|
|
804
|
+
if (input === "ssh_session" || input === "ssh-session" || input === "ssh") {
|
|
805
|
+
return "ssh_session";
|
|
806
|
+
}
|
|
807
|
+
throw new HttpError(400, "actionKinds must contain env_command or ssh_session.");
|
|
808
|
+
}
|
|
809
|
+
function optionalApprovalMode(input) {
|
|
810
|
+
if (input === undefined || input === null || input === "") {
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
return approvalMode(input);
|
|
814
|
+
}
|
|
815
|
+
function optionalApprovalAgentScope(input) {
|
|
816
|
+
if (input === undefined || input === null || input === "") {
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
if (input === "same-agent" || input === "any-agent") {
|
|
820
|
+
return input;
|
|
821
|
+
}
|
|
822
|
+
throw new HttpError(400, "agentScope must be same-agent or any-agent.");
|
|
823
|
+
}
|
|
824
|
+
function optionalSecretSeverity(input) {
|
|
825
|
+
if (input === undefined || input === null || input === "") {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
if (input === "low" || input === "medium" || input === "high" || input === "critical") {
|
|
829
|
+
return input;
|
|
830
|
+
}
|
|
831
|
+
throw new HttpError(400, "minSeverity must be low, medium, high, or critical.");
|
|
832
|
+
}
|
|
833
|
+
function stringArray(input) {
|
|
834
|
+
if (input === undefined || input === null || input === "") {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
if (Array.isArray(input) && input.every((item) => typeof item === "string")) {
|
|
838
|
+
return input;
|
|
839
|
+
}
|
|
840
|
+
throw new HttpError(400, "Expected an array of strings.");
|
|
841
|
+
}
|
|
842
|
+
function optionalNumberValue(value) {
|
|
843
|
+
if (value === undefined || value === null || value === "") {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
const parsed = Number(value);
|
|
847
|
+
if (!Number.isFinite(parsed)) {
|
|
848
|
+
throw new HttpError(400, "Expected a finite number.");
|
|
849
|
+
}
|
|
850
|
+
return parsed;
|
|
851
|
+
}
|
|
852
|
+
function providerForType(type) {
|
|
853
|
+
if (type === "private-key" || type === "ssh-key") {
|
|
854
|
+
return "ssh";
|
|
855
|
+
}
|
|
856
|
+
if (type === "unknown") {
|
|
857
|
+
return "generic";
|
|
858
|
+
}
|
|
859
|
+
return type;
|
|
860
|
+
}
|
|
861
|
+
function providerLabel(provider) {
|
|
862
|
+
const known = {
|
|
863
|
+
aws: "AWS",
|
|
864
|
+
github: "GitHub",
|
|
865
|
+
openai: "OpenAI",
|
|
866
|
+
ssh: "SSH",
|
|
867
|
+
"1password": "1Password",
|
|
868
|
+
onepassword: "1Password",
|
|
869
|
+
generic: "Generic",
|
|
870
|
+
keychain: "macOS Keychain",
|
|
871
|
+
"macos-keychain": "macOS Keychain",
|
|
872
|
+
"windows-credential-manager": "Windows Credential Manager",
|
|
873
|
+
"api-token": "API Tokens",
|
|
874
|
+
credential: "Credentials"
|
|
875
|
+
};
|
|
876
|
+
return known[provider] || provider.replace(/(^|-)([a-z])/g, (_match, dash, char) => {
|
|
877
|
+
return `${dash ? " " : ""}${char.toUpperCase()}`;
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function handlePrefix(provider, type) {
|
|
881
|
+
if (provider && provider !== "generic") {
|
|
882
|
+
return `s-gw:${provider}`;
|
|
883
|
+
}
|
|
884
|
+
return `s-gw:${type}`;
|
|
885
|
+
}
|
|
886
|
+
function higherSeverity(a, b) {
|
|
887
|
+
const rank = {
|
|
888
|
+
low: 0,
|
|
889
|
+
medium: 1,
|
|
890
|
+
high: 2,
|
|
891
|
+
critical: 3
|
|
892
|
+
};
|
|
893
|
+
return rank[b] > rank[a] ? b : a;
|
|
894
|
+
}
|
|
895
|
+
function csvCell(value) {
|
|
896
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
897
|
+
}
|
|
898
|
+
function contentType(filePath) {
|
|
899
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
900
|
+
if (ext === ".html") {
|
|
901
|
+
return "text/html; charset=utf-8";
|
|
902
|
+
}
|
|
903
|
+
if (ext === ".css") {
|
|
904
|
+
return "text/css; charset=utf-8";
|
|
905
|
+
}
|
|
906
|
+
if (ext === ".js") {
|
|
907
|
+
return "text/javascript; charset=utf-8";
|
|
908
|
+
}
|
|
909
|
+
if (ext === ".png") {
|
|
910
|
+
return "image/png";
|
|
911
|
+
}
|
|
912
|
+
if (ext === ".svg") {
|
|
913
|
+
return "image/svg+xml";
|
|
914
|
+
}
|
|
915
|
+
if (ext === ".woff2") {
|
|
916
|
+
return "font/woff2";
|
|
917
|
+
}
|
|
918
|
+
if (ext === ".md") {
|
|
919
|
+
return "text/markdown; charset=utf-8";
|
|
920
|
+
}
|
|
921
|
+
return "application/octet-stream";
|
|
922
|
+
}
|
|
923
|
+
function defaultUiDir() {
|
|
924
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
925
|
+
const builtReactCandidates = [
|
|
926
|
+
path.resolve(here, "console-ui"),
|
|
927
|
+
path.resolve(here, "..", "dist", "console-ui")
|
|
928
|
+
];
|
|
929
|
+
for (const candidate of builtReactCandidates) {
|
|
930
|
+
if (isBuiltReactUi(candidate)) {
|
|
931
|
+
return candidate;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return path.resolve(here, "..", "docs", "ui");
|
|
935
|
+
}
|
|
936
|
+
function isBuiltReactUi(candidate) {
|
|
937
|
+
return existsSync(path.join(candidate, "index.html")) && existsSync(path.join(candidate, "assets"));
|
|
938
|
+
}
|
|
939
|
+
function sendJson(res, status, value) {
|
|
940
|
+
res.writeHead(status, {
|
|
941
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
942
|
+
"Cache-Control": "no-store"
|
|
943
|
+
});
|
|
944
|
+
res.end(`${JSON.stringify(value, null, 2)}\n`);
|
|
945
|
+
}
|
|
946
|
+
function sendCsv(res, csv) {
|
|
947
|
+
res.writeHead(200, {
|
|
948
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
949
|
+
"Content-Disposition": 'attachment; filename="s-gw-audit.csv"',
|
|
950
|
+
"Cache-Control": "no-store"
|
|
951
|
+
});
|
|
952
|
+
res.end(csv);
|
|
953
|
+
}
|
|
954
|
+
function sendError(res, error) {
|
|
955
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
956
|
+
const status = error instanceof HttpError ? error.status : statusForErrorMessage(message);
|
|
957
|
+
sendJson(res, status, { error: message });
|
|
958
|
+
}
|
|
959
|
+
function statusForErrorMessage(message) {
|
|
960
|
+
if (/unknown (secret handle|request)/i.test(message)) {
|
|
961
|
+
return 404;
|
|
962
|
+
}
|
|
963
|
+
if (/approval|approved|pending|denied|executed|failed/i.test(message)) {
|
|
964
|
+
return 409;
|
|
965
|
+
}
|
|
966
|
+
if (/not allowed|invalid|required|missing|empty/i.test(message)) {
|
|
967
|
+
return 400;
|
|
968
|
+
}
|
|
969
|
+
return 500;
|
|
970
|
+
}
|
|
971
|
+
class HttpError extends Error {
|
|
972
|
+
status;
|
|
973
|
+
constructor(status, message) {
|
|
974
|
+
super(message);
|
|
975
|
+
this.status = status;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
//# sourceMappingURL=console-server.js.map
|