@suveren/gateway 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/bin/suveren-gateway.js +117 -2
- package/content/integrations/crm.json +48 -3
- package/content/integrations/erp.json +51 -3
- package/content/integrations/index.json +2 -1
- package/content/integrations/mail.json +148 -0
- package/dist/control-plane/index.mjs +49 -6
- package/dist/mcp-server/http.mjs +150 -56
- package/dist/ui/assets/{index-B1qNl7zo.js → index-Bqb9azUL.js} +23 -23
- package/dist/ui/assets/index-DBdWDq4V.css +1 -0
- package/dist/ui/index.html +2 -2
- package/lib/config.d.mts +4 -0
- package/lib/config.mjs +16 -0
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/@types/node/zlib.d.ts +6 -0
- package/node_modules/follow-redirects/index.js +10 -1
- package/node_modules/follow-redirects/package.json +1 -1
- package/node_modules/ip-address/README.md +34 -34
- package/node_modules/ip-address/dist/ipv4.js +7 -0
- package/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/ip-address/package.json +1 -1
- package/package.json +1 -1
- package/profiles/customers/0.8.profile.json +187 -0
- package/profiles/email/0.7.profile.json +254 -0
- package/profiles/index.json +4 -1
- package/profiles/sales/0.2.profile.json +282 -0
- package/server.js +8 -1
- package/dist/ui/assets/index-BJqF5rmx.css +0 -1
|
@@ -831,9 +831,22 @@ function buildPinnedHttpsAgent(getPinning) {
|
|
|
831
831
|
ca: effectiveCa()
|
|
832
832
|
});
|
|
833
833
|
}
|
|
834
|
+
var DEFAULT_AS_FETCH_TIMEOUT_MS = 15e3;
|
|
835
|
+
function withBoundedSignal(init) {
|
|
836
|
+
const timeout = AbortSignal.timeout(DEFAULT_AS_FETCH_TIMEOUT_MS);
|
|
837
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
838
|
+
return { ...init, signal };
|
|
839
|
+
}
|
|
840
|
+
function closeAfter(dispatcher, pending) {
|
|
841
|
+
pending.catch(() => {
|
|
842
|
+
}).finally(() => {
|
|
843
|
+
void dispatcher.close().catch(() => {
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
}
|
|
834
847
|
async function fetchAs(url, init, pinning) {
|
|
835
848
|
if (!pinning.enforce && !pinning.capture) {
|
|
836
|
-
const res2 = await fetch(url, init);
|
|
849
|
+
const res2 = await fetch(url, withBoundedSignal(init));
|
|
837
850
|
return { res: res2 };
|
|
838
851
|
}
|
|
839
852
|
if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
|
|
@@ -843,8 +856,10 @@ async function fetchAs(url, init, pinning) {
|
|
|
843
856
|
}
|
|
844
857
|
if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
|
|
845
858
|
const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
|
|
859
|
+
const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
|
|
860
|
+
closeAfter(dispatcher2, pending2);
|
|
846
861
|
try {
|
|
847
|
-
const res2 = await
|
|
862
|
+
const res2 = await pending2;
|
|
848
863
|
return { res: res2 };
|
|
849
864
|
} catch (err) {
|
|
850
865
|
if (!isPinCheckError(err)) throw err;
|
|
@@ -854,9 +869,11 @@ async function fetchAs(url, init, pinning) {
|
|
|
854
869
|
}
|
|
855
870
|
}
|
|
856
871
|
const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
|
|
872
|
+
const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
|
|
873
|
+
closeAfter(dispatcher, pending);
|
|
857
874
|
let res;
|
|
858
875
|
try {
|
|
859
|
-
res = await
|
|
876
|
+
res = await pending;
|
|
860
877
|
} catch (err) {
|
|
861
878
|
if (!isPinCheckError(err)) throw err;
|
|
862
879
|
throw new AsTlsMismatchError(
|
|
@@ -1208,7 +1225,12 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
1208
1225
|
// identifies itself this way; without it, a 24-hour browser
|
|
1209
1226
|
// session (see doc at the top of this file).
|
|
1210
1227
|
...clientVersionHeaders()
|
|
1211
|
-
}
|
|
1228
|
+
},
|
|
1229
|
+
// Belt-and-suspenders on top of fetchAs's own default bound
|
|
1230
|
+
// (as-tls-pin.ts): this is the exact call that hung in
|
|
1231
|
+
// production (2026-10-02) with NO timeout at all — a dead
|
|
1232
|
+
// keep-alive socket left the whole login request stuck forever.
|
|
1233
|
+
signal: AbortSignal.timeout(1e4)
|
|
1212
1234
|
},
|
|
1213
1235
|
sessionPinning
|
|
1214
1236
|
);
|
|
@@ -1218,7 +1240,11 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
1218
1240
|
res.status(409).json({ error: "as_tls_mismatch", message: err.message });
|
|
1219
1241
|
return;
|
|
1220
1242
|
}
|
|
1221
|
-
|
|
1243
|
+
res.status(502).json({
|
|
1244
|
+
error: "as_unreachable",
|
|
1245
|
+
message: `Could not reach the Authority Server at ${SP_URL2} to sign in \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to sign in.`
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1222
1248
|
}
|
|
1223
1249
|
if (!spRes.ok) {
|
|
1224
1250
|
const err = await spRes.json().catch(() => ({ error: "Invalid API key" }));
|
|
@@ -1366,7 +1392,16 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
1366
1392
|
unconfigureSession(),
|
|
1367
1393
|
cookie ? fetchAs(
|
|
1368
1394
|
`${SP_URL2}/api/auth/logout`,
|
|
1369
|
-
{
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: { cookie, ...clientVersionHeaders() },
|
|
1398
|
+
redirect: "manual",
|
|
1399
|
+
// Same reasoning as the session call above — this sat with no
|
|
1400
|
+
// timeout of its own before, which would have hung the
|
|
1401
|
+
// `Promise.allSettled` below (and this handler's response)
|
|
1402
|
+
// forever on a dead connection.
|
|
1403
|
+
signal: AbortSignal.timeout(1e4)
|
|
1404
|
+
},
|
|
1370
1405
|
logoutPinning
|
|
1371
1406
|
).catch((err) => {
|
|
1372
1407
|
if (err instanceof AsTlsMismatchError) {
|
|
@@ -3510,6 +3545,11 @@ function selectDenials(records, opts = {}) {
|
|
|
3510
3545
|
return { count, records: out };
|
|
3511
3546
|
}
|
|
3512
3547
|
|
|
3548
|
+
// src/lib/simulation-mode.ts
|
|
3549
|
+
function isSimulationMode() {
|
|
3550
|
+
return process.env.SUVEREN_SIMULATION === "1";
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3513
3553
|
// src/index.ts
|
|
3514
3554
|
(function migrateDataDir() {
|
|
3515
3555
|
if (process.env.SUVEREN_DATA_DIR) return;
|
|
@@ -4124,6 +4164,9 @@ app.get("/health", async (req, res) => {
|
|
|
4124
4164
|
updateAvailable: update.updateAvailable,
|
|
4125
4165
|
installMethod: INSTALL_METHOD,
|
|
4126
4166
|
spUrl: SP_URL,
|
|
4167
|
+
// Purely local — the Authority Server never learns this. UI reads it to
|
|
4168
|
+
// show a gateway-wide banner ("real systems are blocked").
|
|
4169
|
+
simulation: isSimulationMode(),
|
|
4127
4170
|
session: buildSessionHealth(vault),
|
|
4128
4171
|
security: {
|
|
4129
4172
|
note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -324,9 +324,22 @@ function buildCapturingDispatcher() {
|
|
|
324
324
|
});
|
|
325
325
|
return { dispatcher, getCapturedCert: () => captured };
|
|
326
326
|
}
|
|
327
|
+
var DEFAULT_AS_FETCH_TIMEOUT_MS = 15e3;
|
|
328
|
+
function withBoundedSignal(init) {
|
|
329
|
+
const timeout = AbortSignal.timeout(DEFAULT_AS_FETCH_TIMEOUT_MS);
|
|
330
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
331
|
+
return { ...init, signal };
|
|
332
|
+
}
|
|
333
|
+
function closeAfter(dispatcher, pending) {
|
|
334
|
+
pending.catch(() => {
|
|
335
|
+
}).finally(() => {
|
|
336
|
+
void dispatcher.close().catch(() => {
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
327
340
|
async function fetchAs(url, init, pinning) {
|
|
328
341
|
if (!pinning.enforce && !pinning.capture) {
|
|
329
|
-
const res2 = await fetch(url, init);
|
|
342
|
+
const res2 = await fetch(url, withBoundedSignal(init));
|
|
330
343
|
return { res: res2 };
|
|
331
344
|
}
|
|
332
345
|
if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
|
|
@@ -336,8 +349,10 @@ async function fetchAs(url, init, pinning) {
|
|
|
336
349
|
}
|
|
337
350
|
if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
|
|
338
351
|
const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
|
|
352
|
+
const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
|
|
353
|
+
closeAfter(dispatcher2, pending2);
|
|
339
354
|
try {
|
|
340
|
-
const res2 = await
|
|
355
|
+
const res2 = await pending2;
|
|
341
356
|
return { res: res2 };
|
|
342
357
|
} catch (err) {
|
|
343
358
|
if (!isPinCheckError(err)) throw err;
|
|
@@ -347,9 +362,11 @@ async function fetchAs(url, init, pinning) {
|
|
|
347
362
|
}
|
|
348
363
|
}
|
|
349
364
|
const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
|
|
365
|
+
const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
|
|
366
|
+
closeAfter(dispatcher, pending);
|
|
350
367
|
let res;
|
|
351
368
|
try {
|
|
352
|
-
res = await
|
|
369
|
+
res = await pending;
|
|
353
370
|
} catch (err) {
|
|
354
371
|
if (!isPinCheckError(err)) throw err;
|
|
355
372
|
throw new AsTlsMismatchError(
|
|
@@ -2582,10 +2599,62 @@ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
|
|
|
2582
2599
|
}
|
|
2583
2600
|
|
|
2584
2601
|
// src/lib/tool-proxy.ts
|
|
2585
|
-
import { getProfile as getProfile3, ContentBindingError } from "@hap/core";
|
|
2602
|
+
import { getProfile as getProfile3, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
|
|
2586
2603
|
import { readFile } from "fs/promises";
|
|
2587
2604
|
import { extname } from "path";
|
|
2588
2605
|
import { randomUUID } from "crypto";
|
|
2606
|
+
|
|
2607
|
+
// src/lib/manifest-loader.ts
|
|
2608
|
+
import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
|
|
2609
|
+
import { join as join11, resolve } from "path";
|
|
2610
|
+
var manifests = /* @__PURE__ */ new Map();
|
|
2611
|
+
function loadManifests(integrationsDir) {
|
|
2612
|
+
const dir = resolve(
|
|
2613
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
2614
|
+
);
|
|
2615
|
+
const indexPath = join11(dir, "index.json");
|
|
2616
|
+
if (!existsSync11(indexPath)) {
|
|
2617
|
+
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
2618
|
+
return 0;
|
|
2619
|
+
}
|
|
2620
|
+
let index;
|
|
2621
|
+
try {
|
|
2622
|
+
index = JSON.parse(readFileSync12(indexPath, "utf-8"));
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
2625
|
+
return 0;
|
|
2626
|
+
}
|
|
2627
|
+
let loaded = 0;
|
|
2628
|
+
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
2629
|
+
const manifestPath = join11(dir, relativePath);
|
|
2630
|
+
try {
|
|
2631
|
+
const manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
|
|
2632
|
+
manifests.set(id, manifest);
|
|
2633
|
+
loaded++;
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
|
|
2639
|
+
return loaded;
|
|
2640
|
+
}
|
|
2641
|
+
function getManifest(id) {
|
|
2642
|
+
return manifests.get(id);
|
|
2643
|
+
}
|
|
2644
|
+
function getAllManifests() {
|
|
2645
|
+
return Array.from(manifests.values());
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/lib/simulation-mode.ts
|
|
2649
|
+
function isSimulationMode() {
|
|
2650
|
+
return process.env.SUVEREN_SIMULATION === "1";
|
|
2651
|
+
}
|
|
2652
|
+
function manifestIsSimulated(manifest) {
|
|
2653
|
+
return Boolean(manifest?.simulation);
|
|
2654
|
+
}
|
|
2655
|
+
var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
|
|
2656
|
+
|
|
2657
|
+
// src/lib/tool-proxy.ts
|
|
2589
2658
|
var IMAGE_MIME = {
|
|
2590
2659
|
".jpg": "image/jpeg",
|
|
2591
2660
|
".jpeg": "image/jpeg",
|
|
@@ -2645,6 +2714,38 @@ function applyMapping(m, value, execution) {
|
|
|
2645
2714
|
function profileMatches(profileId, shortName) {
|
|
2646
2715
|
return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
|
|
2647
2716
|
}
|
|
2717
|
+
function countBoundsFor(profileId, actionType) {
|
|
2718
|
+
const fields = getProfile3(profileId)?.boundsSchema?.fields;
|
|
2719
|
+
if (!fields) return null;
|
|
2720
|
+
return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
|
|
2721
|
+
const applies = boundActionTypes2(name, def);
|
|
2722
|
+
return applies === void 0 || applies.includes(actionType);
|
|
2723
|
+
}).map(([name]) => name);
|
|
2724
|
+
}
|
|
2725
|
+
function zeroCappedBound(profileId, bounds, actionType) {
|
|
2726
|
+
if (typeof actionType !== "string") return null;
|
|
2727
|
+
const names = countBoundsFor(profileId, actionType);
|
|
2728
|
+
if (!names) return null;
|
|
2729
|
+
for (const n of names) {
|
|
2730
|
+
const v = bounds?.[n];
|
|
2731
|
+
if (v !== void 0 && v !== "" && Number(v) <= 0) return n;
|
|
2732
|
+
}
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
function toolIsAuthorizedForDisplay(tool, matchingAuths) {
|
|
2736
|
+
if (!tool.gating?.hideUnlessAuthorized) return true;
|
|
2737
|
+
if (matchingAuths.length === 0) return false;
|
|
2738
|
+
const profileId = tool.gating.profile;
|
|
2739
|
+
const actionType = tool.gating.staticExecution?.action_type;
|
|
2740
|
+
if (!profileId || typeof actionType !== "string") return false;
|
|
2741
|
+
const requiredFields = countBoundsFor(profileId, actionType);
|
|
2742
|
+
if (!requiredFields) return false;
|
|
2743
|
+
if (requiredFields.length === 0) return true;
|
|
2744
|
+
return matchingAuths.some((a) => {
|
|
2745
|
+
const bounds = a.bounds ?? a.frame;
|
|
2746
|
+
return requiredFields.every((f) => Number(bounds?.[f] ?? 0) > 0);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2648
2749
|
function parseFirstJson(result) {
|
|
2649
2750
|
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
2650
2751
|
if (typeof text !== "string") return void 0;
|
|
@@ -2670,6 +2771,15 @@ function denyRead(state2, tool, reason, detail, target) {
|
|
|
2670
2771
|
return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
|
|
2671
2772
|
}
|
|
2672
2773
|
function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
2774
|
+
if (isSimulationMode() && !manifestIsSimulated(getManifest(tool.integrationId))) {
|
|
2775
|
+
return async () => ({
|
|
2776
|
+
content: [{
|
|
2777
|
+
type: "text",
|
|
2778
|
+
text: `Refused: "${tool.namespacedName}" talks to a real system, but this gateway is running in simulation mode (real systems are off). No ticket was requested.`
|
|
2779
|
+
}],
|
|
2780
|
+
isError: true
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2673
2783
|
const gated = createGatedToolHandlerInner(tool, integrationManager2, state2);
|
|
2674
2784
|
const inner = async (args) => gated(normalizeIncomingArgs(tool, args));
|
|
2675
2785
|
const blocked = tool.gating?.blockedArgs ?? [];
|
|
@@ -2945,7 +3055,10 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
2945
3055
|
bounds: candidate.bounds,
|
|
2946
3056
|
context: candidate.context
|
|
2947
3057
|
});
|
|
2948
|
-
|
|
3058
|
+
const capped = result.approved ? zeroCappedBound(candidate.profileId, candidate.bounds ?? candidate.frame, typeof execution.action_type === "string" ? execution.action_type : void 0) : null;
|
|
3059
|
+
if (result.approved && capped) {
|
|
3060
|
+
errors.push(`${candidate.path}: ${capped} is 0 \u2014 this mandate does not allow "${execution.action_type}"`);
|
|
3061
|
+
} else if (result.approved) {
|
|
2949
3062
|
passers.push(candidate);
|
|
2950
3063
|
} else {
|
|
2951
3064
|
const reasons = result.errors.map((e) => {
|
|
@@ -3981,9 +4094,10 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
3981
4094
|
const description = buildProxiedToolDescription(tool, state2);
|
|
3982
4095
|
registered.update({ description });
|
|
3983
4096
|
if (tool.gating?.profile) {
|
|
3984
|
-
const
|
|
4097
|
+
const matchingAuths = auths.filter(
|
|
3985
4098
|
(a) => a.complete && profileMatches(a.profileId, tool.gating.profile)
|
|
3986
4099
|
);
|
|
4100
|
+
const hasAuth = matchingAuths.length > 0 && toolIsAuthorizedForDisplay(tool, matchingAuths);
|
|
3987
4101
|
if (hasAuth) registered.enable();
|
|
3988
4102
|
else registered.disable();
|
|
3989
4103
|
} else {
|
|
@@ -4024,16 +4138,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
4024
4138
|
}
|
|
4025
4139
|
|
|
4026
4140
|
// src/lib/integration-registry.ts
|
|
4027
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
4141
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync12 } from "fs";
|
|
4028
4142
|
import { homedir as homedir8 } from "os";
|
|
4029
|
-
import { dirname as dirname9, join as
|
|
4030
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
4143
|
+
import { dirname as dirname9, join as join12 } from "path";
|
|
4144
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join12(homedir8(), ".suveren");
|
|
4031
4145
|
var IntegrationRegistry = class {
|
|
4032
4146
|
integrations = /* @__PURE__ */ new Map();
|
|
4033
4147
|
filePath;
|
|
4034
4148
|
constructor(dataDir2) {
|
|
4035
4149
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
4036
|
-
this.filePath =
|
|
4150
|
+
this.filePath = join12(dir, "integrations.json");
|
|
4037
4151
|
this.load();
|
|
4038
4152
|
}
|
|
4039
4153
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -4064,12 +4178,12 @@ var IntegrationRegistry = class {
|
|
|
4064
4178
|
}
|
|
4065
4179
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
4066
4180
|
load() {
|
|
4067
|
-
if (!
|
|
4181
|
+
if (!existsSync12(this.filePath)) {
|
|
4068
4182
|
mkdirSync9(dirname9(this.filePath), { recursive: true });
|
|
4069
4183
|
return;
|
|
4070
4184
|
}
|
|
4071
4185
|
try {
|
|
4072
|
-
const raw =
|
|
4186
|
+
const raw = readFileSync13(this.filePath, "utf-8");
|
|
4073
4187
|
const data = JSON.parse(raw);
|
|
4074
4188
|
const migrated = data.integrations.map((i) => {
|
|
4075
4189
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -4112,47 +4226,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
4112
4226
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4113
4227
|
import { getProfile as getProfile5 } from "@hap/core";
|
|
4114
4228
|
|
|
4115
|
-
// src/lib/manifest-loader.ts
|
|
4116
|
-
import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
|
|
4117
|
-
import { join as join12, resolve } from "path";
|
|
4118
|
-
var manifests = /* @__PURE__ */ new Map();
|
|
4119
|
-
function loadManifests(integrationsDir) {
|
|
4120
|
-
const dir = resolve(
|
|
4121
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
4122
|
-
);
|
|
4123
|
-
const indexPath = join12(dir, "index.json");
|
|
4124
|
-
if (!existsSync12(indexPath)) {
|
|
4125
|
-
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
4126
|
-
return 0;
|
|
4127
|
-
}
|
|
4128
|
-
let index;
|
|
4129
|
-
try {
|
|
4130
|
-
index = JSON.parse(readFileSync13(indexPath, "utf-8"));
|
|
4131
|
-
} catch (err) {
|
|
4132
|
-
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
4133
|
-
return 0;
|
|
4134
|
-
}
|
|
4135
|
-
let loaded = 0;
|
|
4136
|
-
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
4137
|
-
const manifestPath = join12(dir, relativePath);
|
|
4138
|
-
try {
|
|
4139
|
-
const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
|
|
4140
|
-
manifests.set(id, manifest);
|
|
4141
|
-
loaded++;
|
|
4142
|
-
} catch (err) {
|
|
4143
|
-
console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
|
|
4147
|
-
return loaded;
|
|
4148
|
-
}
|
|
4149
|
-
function getManifest(id) {
|
|
4150
|
-
return manifests.get(id);
|
|
4151
|
-
}
|
|
4152
|
-
function getAllManifests() {
|
|
4153
|
-
return Array.from(manifests.values());
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
4229
|
// src/lib/remote-auth-preflight.ts
|
|
4157
4230
|
var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
|
|
4158
4231
|
function remotePreflightTarget(command, args) {
|
|
@@ -4460,10 +4533,21 @@ var IntegrationManager = class {
|
|
|
4460
4533
|
if (opts.skipIfRunning) return existing.tools;
|
|
4461
4534
|
await this.stopIntegrationLocked(config.id);
|
|
4462
4535
|
}
|
|
4536
|
+
if (isSimulationMode() && !manifestIsSimulated(getManifest(config.id))) {
|
|
4537
|
+
throw new Error(`${config.id}: ${SIMULATION_BLOCK_REASON}`);
|
|
4538
|
+
}
|
|
4463
4539
|
if (config.npmPackage) {
|
|
4464
4540
|
await this.ensureInstalled(config.npmPackage);
|
|
4465
4541
|
}
|
|
4466
4542
|
const env = this.resolveEnvKeys(config);
|
|
4543
|
+
if (isSimulationMode()) {
|
|
4544
|
+
const manifest = getManifest(config.id);
|
|
4545
|
+
const simDecl = manifest?.simulation;
|
|
4546
|
+
if (simDecl) {
|
|
4547
|
+
const envVar = Object.entries(manifest.credentials.envMapping).find(([, credKey]) => credKey === simDecl.field)?.[0];
|
|
4548
|
+
if (envVar) env[envVar] = "simulation";
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4467
4551
|
const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? process.env[name] ?? "");
|
|
4468
4552
|
const args = config.args.map(interpolate);
|
|
4469
4553
|
const remote = remotePreflightTarget(config.command, args);
|
|
@@ -4638,7 +4722,15 @@ var IntegrationManager = class {
|
|
|
4638
4722
|
name: config.name,
|
|
4639
4723
|
running: false,
|
|
4640
4724
|
toolCount: 0,
|
|
4641
|
-
readAgeDays: readAgeOf(config)
|
|
4725
|
+
readAgeDays: readAgeOf(config),
|
|
4726
|
+
// Computed fresh, not cached: true whenever simulation mode is on
|
|
4727
|
+
// AND this connector has no manifest `simulation` marker, which is
|
|
4728
|
+
// exactly the condition startIntegrationLocked refuses under — so
|
|
4729
|
+
// the UI always reflects why a real connector never started,
|
|
4730
|
+
// without needing a separate error-tracking path for this one
|
|
4731
|
+
// reason. Unrelated start failures still surface via the
|
|
4732
|
+
// `warning` string returned by /internal/add-integration.
|
|
4733
|
+
error: isSimulationMode() && !manifestIsSimulated(getManifest(config.id)) ? SIMULATION_BLOCK_REASON : void 0
|
|
4642
4734
|
});
|
|
4643
4735
|
}
|
|
4644
4736
|
}
|
|
@@ -4732,7 +4824,8 @@ var IntegrationManager = class {
|
|
|
4732
4824
|
read: ext.read,
|
|
4733
4825
|
readGovernance: ext.readGovernance,
|
|
4734
4826
|
readGovernanceReason: ext.readGovernanceReason,
|
|
4735
|
-
blockedArgs: ext.blockedArgs
|
|
4827
|
+
blockedArgs: ext.blockedArgs,
|
|
4828
|
+
hideUnlessAuthorized: ext.hideUnlessAuthorized
|
|
4736
4829
|
};
|
|
4737
4830
|
}
|
|
4738
4831
|
return {
|
|
@@ -4746,7 +4839,8 @@ var IntegrationManager = class {
|
|
|
4746
4839
|
contentField: ext.contentField,
|
|
4747
4840
|
blockedArgs: ext.blockedArgs,
|
|
4748
4841
|
argEncoding: ext.argEncoding,
|
|
4749
|
-
argNormalization: ext.argNormalization
|
|
4842
|
+
argNormalization: ext.argNormalization,
|
|
4843
|
+
hideUnlessAuthorized: ext.hideUnlessAuthorized
|
|
4750
4844
|
};
|
|
4751
4845
|
}
|
|
4752
4846
|
return {
|