@suveren/gateway 0.11.2 → 0.13.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.
Files changed (127) hide show
  1. package/README.md +35 -1
  2. package/bin/suveren-gateway.js +137 -12
  3. package/content/integrations/calendar.json +1 -0
  4. package/content/integrations/crm.json +1 -67
  5. package/content/integrations/deploy-github.json +3 -5
  6. package/content/integrations/erp.json +1 -80
  7. package/content/integrations/gmail.json +1 -0
  8. package/content/integrations/linkedin.json +1 -0
  9. package/content/integrations/mail.json +1 -48
  10. package/content/integrations/mollie.json +1 -0
  11. package/content/integrations/records.json +1 -0
  12. package/dist/control-plane/index.mjs +555 -122
  13. package/dist/mcp-server/http.mjs +643 -168
  14. package/dist/ui/assets/{index-BzcgeHlq.js → index-CmLqzgqA.js} +21 -21
  15. package/dist/ui/assets/{index-BFOprnFZ.css → index-y1OFeYnS.css} +1 -1
  16. package/dist/ui/index.html +2 -2
  17. package/lib/autostart-templates.d.mts +6 -0
  18. package/lib/autostart-templates.mjs +21 -4
  19. package/lib/config.d.mts +10 -0
  20. package/lib/config.mjs +79 -18
  21. package/lib/node-version.d.mts +7 -0
  22. package/lib/node-version.mjs +28 -0
  23. package/lib/policy.d.mts +34 -0
  24. package/lib/policy.mjs +369 -0
  25. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.d.ts.map +1 -1
  26. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js +9 -7
  27. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js.map +1 -1
  28. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/sse.d.ts +13 -0
  29. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/sse.d.ts.map +1 -1
  30. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/sse.js +17 -4
  31. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/sse.js.map +1 -1
  32. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.d.ts +13 -0
  33. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.d.ts.map +1 -1
  34. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js +23 -10
  35. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js.map +1 -1
  36. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/elicitationFormExample.js +63 -20
  37. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/elicitationFormExample.js.map +1 -1
  38. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/elicitationUrlExample.js +71 -29
  39. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/elicitationUrlExample.js.map +1 -1
  40. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/jsonResponseStreamableHttp.js +45 -6
  41. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/jsonResponseStreamableHttp.js.map +1 -1
  42. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/simpleStreamableHttp.js +71 -31
  43. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/simpleStreamableHttp.js.map +1 -1
  44. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/simpleTaskInteractive.js +61 -18
  45. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/simpleTaskInteractive.js.map +1 -1
  46. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.js +57 -19
  47. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.js.map +1 -1
  48. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/ssePollingExample.js +64 -17
  49. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/ssePollingExample.js.map +1 -1
  50. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.js +61 -13
  51. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.js.map +1 -1
  52. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/interfaces.d.ts +4 -0
  53. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/interfaces.d.ts.map +1 -1
  54. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/interfaces.js.map +1 -1
  55. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/stores/in-memory.d.ts +17 -8
  56. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/stores/in-memory.d.ts.map +1 -1
  57. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/stores/in-memory.js +31 -14
  58. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/experimental/tasks/stores/in-memory.js.map +1 -1
  59. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/auth/middleware/bearerAuth.d.ts +9 -1
  60. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/auth/middleware/bearerAuth.d.ts.map +1 -1
  61. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/auth/middleware/bearerAuth.js +17 -1
  62. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/auth/middleware/bearerAuth.js.map +1 -1
  63. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.d.ts +12 -1
  64. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.d.ts.map +1 -1
  65. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js +46 -2
  66. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js.map +1 -1
  67. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/protocol.d.ts +1 -0
  68. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/protocol.d.ts.map +1 -1
  69. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/protocol.js +39 -9
  70. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/protocol.js.map +1 -1
  71. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/transport.d.ts +10 -0
  72. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/transport.d.ts.map +1 -1
  73. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/transport.js +77 -0
  74. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/transport.js.map +1 -1
  75. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.d.ts.map +1 -1
  76. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +9 -7
  77. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js.map +1 -1
  78. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/sse.d.ts +13 -0
  79. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/sse.d.ts.map +1 -1
  80. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/sse.js +18 -5
  81. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/sse.js.map +1 -1
  82. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.d.ts +13 -0
  83. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.d.ts.map +1 -1
  84. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +24 -11
  85. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js.map +1 -1
  86. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/elicitationFormExample.js +63 -20
  87. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/elicitationFormExample.js.map +1 -1
  88. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/elicitationUrlExample.js +71 -29
  89. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/elicitationUrlExample.js.map +1 -1
  90. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/jsonResponseStreamableHttp.js +45 -6
  91. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/jsonResponseStreamableHttp.js.map +1 -1
  92. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/simpleStreamableHttp.js +71 -31
  93. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/simpleStreamableHttp.js.map +1 -1
  94. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/simpleTaskInteractive.js +61 -18
  95. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/simpleTaskInteractive.js.map +1 -1
  96. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.js +57 -19
  97. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.js.map +1 -1
  98. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/ssePollingExample.js +64 -17
  99. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/ssePollingExample.js.map +1 -1
  100. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.js +61 -13
  101. package/node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.js.map +1 -1
  102. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.d.ts +4 -0
  103. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.d.ts.map +1 -1
  104. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js.map +1 -1
  105. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/stores/in-memory.d.ts +17 -8
  106. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/stores/in-memory.d.ts.map +1 -1
  107. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/stores/in-memory.js +31 -14
  108. package/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/stores/in-memory.js.map +1 -1
  109. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/middleware/bearerAuth.d.ts +9 -1
  110. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/middleware/bearerAuth.d.ts.map +1 -1
  111. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/middleware/bearerAuth.js +17 -1
  112. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/middleware/bearerAuth.js.map +1 -1
  113. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts +12 -1
  114. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts.map +1 -1
  115. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js +46 -2
  116. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js.map +1 -1
  117. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.d.ts +1 -0
  118. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.d.ts.map +1 -1
  119. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js +39 -9
  120. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js.map +1 -1
  121. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.d.ts +10 -0
  122. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.d.ts.map +1 -1
  123. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.js +72 -0
  124. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.js.map +1 -1
  125. package/node_modules/@modelcontextprotocol/sdk/package.json +1 -1
  126. package/package.json +2 -2
  127. package/server.js +60 -8
@@ -142,18 +142,237 @@ function fingerprintOf(publicKeyHex) {
142
142
  }
143
143
 
144
144
  // src/lib/as-config.ts
145
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
146
- import { dirname as dirname2, join as join3 } from "path";
145
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
146
+ import { dirname as dirname2, join as join4 } from "path";
147
+
148
+ // src/lib/policy.ts
149
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
150
+ import { join as join3 } from "path";
151
+ import { spawnSync } from "child_process";
152
+ var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod"];
153
+ var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
154
+ function registryKeyPath(hive) {
155
+ const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
156
+ return `${hive}\\${baseKey}`;
157
+ }
158
+ function validateAsUrl(candidate) {
159
+ const trimmed = candidate.trim();
160
+ if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
161
+ let parsed;
162
+ try {
163
+ parsed = new URL(trimmed);
164
+ } catch {
165
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
166
+ }
167
+ const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
168
+ if (parsed.protocol !== "https:" && !isLocalHttp) {
169
+ return {
170
+ ok: false,
171
+ error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
172
+ };
173
+ }
174
+ let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
175
+ if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
176
+ normalized = normalized.slice(0, -1);
177
+ } else if (normalized === `${parsed.origin}/`) {
178
+ normalized = parsed.origin;
179
+ }
180
+ return { ok: true, url: normalized };
181
+ }
182
+ function validateCaFile(candidate) {
183
+ const trimmed = candidate.trim();
184
+ if (!trimmed) return { ok: false, error: "The CA file path is empty." };
185
+ if (!existsSync3(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
186
+ return { ok: true, path: trimmed };
187
+ }
188
+ function validateProxyUrl(candidate) {
189
+ const trimmed = candidate.trim();
190
+ if (!trimmed) return { ok: false, error: "The proxy URL is empty." };
191
+ let parsed;
192
+ try {
193
+ parsed = new URL(trimmed);
194
+ } catch {
195
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
196
+ }
197
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
198
+ return { ok: false, error: `"${trimmed}" must use http:// or https://.` };
199
+ }
200
+ return { ok: true, url: trimmed };
201
+ }
202
+ function coerceBoolean(value) {
203
+ if (typeof value === "boolean") return value;
204
+ if (typeof value === "number") {
205
+ if (value === 0) return false;
206
+ if (value === 1) return true;
207
+ return void 0;
208
+ }
209
+ if (typeof value === "string") {
210
+ const v = value.trim().toLowerCase();
211
+ if (v === "true" || v === "1") return true;
212
+ if (v === "false" || v === "0") return false;
213
+ }
214
+ return void 0;
215
+ }
216
+ function parseRegQueryOutput(stdout) {
217
+ const out = {};
218
+ const lineRe = /^ {4}(\S+)\s+(REG_SZ|REG_EXPAND_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_MULTI_SZ)\s+(.*)$/;
219
+ for (const line of stdout.split(/\r?\n/)) {
220
+ const m = lineRe.exec(line);
221
+ if (!m) continue;
222
+ const [, name, type, rawValue] = m;
223
+ if (!POLICY_KEYS.includes(name)) continue;
224
+ const key = name;
225
+ if (type === "REG_DWORD" || type === "REG_QWORD") {
226
+ out[key] = parseInt(rawValue, 16);
227
+ } else {
228
+ out[key] = rawValue;
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+ function readRegistryHive(hive) {
234
+ const path = registryKeyPath(hive);
235
+ const source = `the registry policy (${path})`;
236
+ if (process.env.SUVEREN_POLICY_REGISTRY === "off") return { values: {}, source };
237
+ if (process.platform !== "win32") return { values: {}, source };
238
+ let result;
239
+ try {
240
+ result = spawnSync("reg", ["query", path], { encoding: "utf8" });
241
+ } catch {
242
+ return { values: {}, source };
243
+ }
244
+ if (!result || result.status !== 0 || !result.stdout) return { values: {}, source };
245
+ return { values: parseRegQueryOutput(result.stdout), source };
246
+ }
247
+ function policyFilePath() {
248
+ if (process.env.SUVEREN_POLICY_FILE) return process.env.SUVEREN_POLICY_FILE;
249
+ if (process.platform === "win32") {
250
+ const programData = process.env.ProgramData || process.env.PROGRAMDATA || "C:\\ProgramData";
251
+ return join3(programData, "Suveren", "gateway-policy.json");
252
+ }
253
+ if (process.platform === "darwin") {
254
+ return "/Library/Application Support/Suveren/gateway-policy.json";
255
+ }
256
+ return "/etc/suveren/gateway-policy.json";
257
+ }
258
+ function readPolicyFile() {
259
+ const path = policyFilePath();
260
+ const source = `the policy file (${path})`;
261
+ if (!existsSync3(path)) return { values: {}, source };
262
+ let data;
263
+ try {
264
+ data = JSON.parse(readFileSync3(path, "utf8"));
265
+ } catch (err) {
266
+ throw new Error(`Could not parse policy file ${path}: ${err instanceof Error ? err.message : String(err)}`);
267
+ }
268
+ const values = {};
269
+ for (const key of POLICY_KEYS) {
270
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
271
+ values[key] = data[key];
272
+ }
273
+ }
274
+ return { values, source };
275
+ }
276
+ function mergeBySource(entries) {
277
+ const merged = {};
278
+ const sourceOf = {};
279
+ for (const key of POLICY_KEYS) {
280
+ for (const entry of entries) {
281
+ if (Object.prototype.hasOwnProperty.call(entry.values, key) && entry.values[key] !== void 0) {
282
+ merged[key] = entry.values[key];
283
+ sourceOf[key] = entry.source;
284
+ break;
285
+ }
286
+ }
287
+ }
288
+ return { merged, sourceOf };
289
+ }
290
+ var cached = null;
291
+ function readPolicy() {
292
+ if (cached) return cached;
293
+ const hklm = readRegistryHive("HKLM");
294
+ const hkcu = readRegistryHive("HKCU");
295
+ const file = readPolicyFile();
296
+ const { merged, sourceOf } = mergeBySource([hklm, hkcu, file]);
297
+ const policy = {};
298
+ const locked = /* @__PURE__ */ new Set();
299
+ if (merged.AsUrl !== void 0) {
300
+ const v = validateAsUrl(String(merged.AsUrl));
301
+ if (!v.ok) {
302
+ throw new Error(`Invalid policy AsUrl ("${merged.AsUrl}") from ${sourceOf.AsUrl}: ${v.error}`);
303
+ }
304
+ policy.asUrl = v.url;
305
+ locked.add("asUrl");
306
+ }
307
+ if (merged.CaFile !== void 0) {
308
+ const v = validateCaFile(String(merged.CaFile));
309
+ if (!v.ok) {
310
+ throw new Error(`Invalid policy CaFile ("${merged.CaFile}") from ${sourceOf.CaFile}: ${v.error}`);
311
+ }
312
+ policy.caFile = v.path;
313
+ locked.add("caFile");
314
+ }
315
+ if (merged.PinTls !== void 0) {
316
+ const b = coerceBoolean(merged.PinTls);
317
+ if (b === void 0) {
318
+ throw new Error(
319
+ `Invalid policy PinTls ("${String(merged.PinTls)}") from ${sourceOf.PinTls}: must be a boolean or 0/1.`
320
+ );
321
+ }
322
+ policy.pinTls = b;
323
+ locked.add("pinTls");
324
+ }
325
+ if (merged.Proxy !== void 0) {
326
+ const v = validateProxyUrl(String(merged.Proxy));
327
+ if (!v.ok) {
328
+ throw new Error(`Invalid policy Proxy ("${String(merged.Proxy)}") from ${sourceOf.Proxy}: ${v.error}`);
329
+ }
330
+ policy.proxy = v.url;
331
+ locked.add("proxy");
332
+ }
333
+ if (merged.NoProxy !== void 0) {
334
+ const raw = String(merged.NoProxy).trim();
335
+ if (!raw) {
336
+ throw new Error(`Invalid policy NoProxy ("${String(merged.NoProxy)}") from ${sourceOf.NoProxy}: must not be empty.`);
337
+ }
338
+ policy.noProxy = raw;
339
+ locked.add("noProxy");
340
+ }
341
+ if (merged.Simulation !== void 0) {
342
+ const b = coerceBoolean(merged.Simulation);
343
+ if (b === void 0) {
344
+ throw new Error(
345
+ `Invalid policy Simulation ("${String(merged.Simulation)}") from ${sourceOf.Simulation}: must be a boolean or 0/1.`
346
+ );
347
+ }
348
+ policy.simulation = b;
349
+ locked.add("simulation");
350
+ }
351
+ if (merged.InstallMethod !== void 0) {
352
+ const raw = String(merged.InstallMethod).trim().toLowerCase();
353
+ if (raw !== "managed") {
354
+ throw new Error(
355
+ `Invalid policy InstallMethod ("${String(merged.InstallMethod)}") from ${sourceOf.InstallMethod}: only "managed" is supported.`
356
+ );
357
+ }
358
+ policy.installMethod = "managed";
359
+ locked.add("installMethod");
360
+ }
361
+ cached = { policy, locked };
362
+ return cached;
363
+ }
364
+
365
+ // src/lib/as-config.ts
147
366
  var DEFAULT_AS_URL = "https://www.suveren.ai";
148
367
  function configPath(dataDir2) {
149
- return join3(dataDir2, "config.json");
368
+ return join4(dataDir2, "config.json");
150
369
  }
151
370
  function readAsConfigStrict(dataDir2) {
152
371
  const path = configPath(dataDir2);
153
- if (!existsSync3(path)) return {};
372
+ if (!existsSync4(path)) return {};
154
373
  let data;
155
374
  try {
156
- data = JSON.parse(readFileSync3(path, "utf-8"));
375
+ data = JSON.parse(readFileSync4(path, "utf-8"));
157
376
  } catch (err) {
158
377
  throw new Error(
159
378
  `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -168,9 +387,9 @@ function readAsConfigStrict(dataDir2) {
168
387
  }
169
388
  function readAsConfig(dataDir2) {
170
389
  const path = configPath(dataDir2);
171
- if (!existsSync3(path)) return {};
390
+ if (!existsSync4(path)) return {};
172
391
  try {
173
- const data = JSON.parse(readFileSync3(path, "utf-8"));
392
+ const data = JSON.parse(readFileSync4(path, "utf-8"));
174
393
  const out = {};
175
394
  if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
176
395
  if (typeof data.caFile === "string") out.caFile = data.caFile;
@@ -181,7 +400,7 @@ function readAsConfig(dataDir2) {
181
400
  return {};
182
401
  }
183
402
  }
184
- function validateAsUrl(candidate) {
403
+ function validateAsUrl2(candidate) {
185
404
  const trimmed = candidate.trim();
186
405
  if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
187
406
  let parsed;
@@ -206,26 +425,30 @@ function validateAsUrl(candidate) {
206
425
  return { ok: true, url: normalized };
207
426
  }
208
427
  function resolveAsUrl(dataDir2, flag) {
428
+ const policyUrl = readPolicy().policy.asUrl;
429
+ if (policyUrl) return policyUrl;
209
430
  if (flag) {
210
- const v = validateAsUrl(flag);
431
+ const v = validateAsUrl2(flag);
211
432
  if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
212
433
  return v.url;
213
434
  }
214
435
  const envUrl = process.env.SUVEREN_AS_URL;
215
436
  if (envUrl) {
216
- const v = validateAsUrl(envUrl);
437
+ const v = validateAsUrl2(envUrl);
217
438
  if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
218
439
  return v.url;
219
440
  }
220
441
  const saved = readAsConfigStrict(dataDir2).asUrl;
221
442
  if (saved) {
222
- const v = validateAsUrl(saved);
443
+ const v = validateAsUrl2(saved);
223
444
  if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
224
445
  return v.url;
225
446
  }
226
447
  return DEFAULT_AS_URL;
227
448
  }
228
449
  function resolvePinTls(dataDir2) {
450
+ const policyPinTls = readPolicy().policy.pinTls;
451
+ if (policyPinTls !== void 0) return policyPinTls;
229
452
  return readAsConfig(dataDir2).pinTls === true;
230
453
  }
231
454
  function validatePinTlsForUrl(pinTls, asUrl) {
@@ -240,14 +463,58 @@ function validatePinTlsForUrl(pinTls, asUrl) {
240
463
  // src/lib/as-tls-pin.ts
241
464
  import { createHash as createHash2 } from "crypto";
242
465
  import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
466
+ import { Agent as HttpsAgent2 } from "https";
467
+ import { readFileSync as readFileSync5 } from "fs";
468
+ import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
469
+
470
+ // src/lib/proxy-env.ts
471
+ function isLoopbackHostname(hostname) {
472
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
473
+ if (h === "localhost") return true;
474
+ if (h === "::1" || h === "0:0:0:0:0:0:0:1") return true;
475
+ return /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(h);
476
+ }
477
+ function readProxyEnv() {
478
+ const http = process.env.http_proxy || process.env.HTTP_PROXY || void 0;
479
+ const https = process.env.https_proxy || process.env.HTTPS_PROXY || void 0;
480
+ const noProxy = process.env.no_proxy ?? process.env.NO_PROXY ?? "";
481
+ return { http, https, noProxy };
482
+ }
483
+ function matchesNoProxy(hostname, port2, noProxy) {
484
+ const entries = noProxy.split(/[,\s]+/).filter(Boolean);
485
+ if (entries.length === 0) return false;
486
+ if (entries.includes("*")) return true;
487
+ const h = hostname.toLowerCase();
488
+ for (const raw of entries) {
489
+ const m = raw.match(/^(.+):(\d+)$/);
490
+ const entryHost = (m ? m[1] : raw).replace(/^\*?\./, "").toLowerCase();
491
+ const entryPort = m ? Number(m[2]) : 0;
492
+ if (entryPort && entryPort !== port2) continue;
493
+ if (h === entryHost || h.endsWith(`.${entryHost}`)) return true;
494
+ }
495
+ return false;
496
+ }
497
+ function selectProxyUrl(targetUrl) {
498
+ const url = typeof targetUrl === "string" ? new URL(targetUrl) : targetUrl;
499
+ if (isLoopbackHostname(url.hostname)) return void 0;
500
+ const { http, https, noProxy } = readProxyEnv();
501
+ const port2 = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
502
+ if (matchesNoProxy(url.hostname, port2, noProxy)) return void 0;
503
+ return url.protocol === "https:" ? https ?? http : http;
504
+ }
505
+
506
+ // src/lib/proxy-https-agent.ts
243
507
  import { Agent as HttpsAgent } from "https";
244
- import { readFileSync as readFileSync4 } from "fs";
245
- import { Agent, fetch as undiciFetch } from "undici";
508
+ import { connect as netConnect } from "net";
509
+ import { connect as tlsConnect } from "tls";
510
+ var MAX_CONNECT_RESPONSE_BYTES = 64 * 1024;
511
+
512
+ // src/lib/as-tls-pin.ts
246
513
  function effectiveCa() {
247
514
  const path = process.env.NODE_EXTRA_CA_CERTS;
248
515
  if (!path) return void 0;
249
516
  try {
250
- return [...rootCertificates, readFileSync4(path, "utf-8")];
517
+ return [...rootCertificates, readFileSync5(path, "utf-8")];
251
518
  } catch {
252
519
  return void 0;
253
520
  }
@@ -302,24 +569,27 @@ function pinnedCheckServerIdentity(pinnedSpkiHex) {
302
569
  return void 0;
303
570
  };
304
571
  }
305
- function buildPinnedDispatcher(pinnedSpkiHex) {
306
- return new Agent({
307
- connect: {
308
- ca: effectiveCa(),
309
- checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
310
- }
572
+ function buildTlsDispatcher(url, connectOpts) {
573
+ const proxyUrl = selectProxyUrl(url);
574
+ if (proxyUrl) {
575
+ return new ProxyAgent({ uri: proxyUrl, requestTls: connectOpts });
576
+ }
577
+ return new Agent({ connect: connectOpts });
578
+ }
579
+ function buildPinnedDispatcher(pinnedSpkiHex, url) {
580
+ return buildTlsDispatcher(url, {
581
+ ca: effectiveCa(),
582
+ checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
311
583
  });
312
584
  }
313
- function buildCapturingDispatcher() {
585
+ function buildCapturingDispatcher(url) {
314
586
  let captured;
315
- const dispatcher = new Agent({
316
- connect: {
317
- ca: effectiveCa(),
318
- checkServerIdentity: (hostname, cert) => {
319
- captured = cert;
320
- const defaultErr = defaultCheckServerIdentity(hostname, cert);
321
- return defaultErr ? taggedCheckError(defaultErr) : void 0;
322
- }
587
+ const dispatcher = buildTlsDispatcher(url, {
588
+ ca: effectiveCa(),
589
+ checkServerIdentity: (hostname, cert) => {
590
+ captured = cert;
591
+ const defaultErr = defaultCheckServerIdentity(hostname, cert);
592
+ return defaultErr ? taggedCheckError(defaultErr) : void 0;
323
593
  }
324
594
  });
325
595
  return { dispatcher, getCapturedCert: () => captured };
@@ -330,6 +600,34 @@ function withBoundedSignal(init) {
330
600
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
331
601
  return { ...init, signal };
332
602
  }
603
+ var UNKNOWN_ISSUER_CODES = /* @__PURE__ */ new Set([
604
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
605
+ "UNABLE_TO_GET_ISSUER_CERT",
606
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
607
+ "SELF_SIGNED_CERT_IN_CHAIN",
608
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
609
+ "CERT_UNTRUSTED"
610
+ ]);
611
+ function tlsErrorCode(err) {
612
+ if (!(err instanceof Error)) return void 0;
613
+ const cause = err.cause;
614
+ const withCode = cause instanceof Error ? cause : err;
615
+ return withCode.code;
616
+ }
617
+ function isUnknownIssuerError(err) {
618
+ const code = tlsErrorCode(err);
619
+ return !!code && UNKNOWN_ISSUER_CODES.has(code);
620
+ }
621
+ function rethrowWithIssuerHint(err, url) {
622
+ if (isUnknownIssuerError(err)) {
623
+ const hinted = new Error(
624
+ `Could not verify the certificate presented for ${url} \u2014 its issuer is not trusted (${tlsErrorCode(err)}). This is the typical signature of a company network that inspects HTTPS traffic with its own root certificate: trust it with \`suveren-gateway config set ca-file <path-to-company-root.pem>\` (or --ca-file / NODE_EXTRA_CA_CERTS), then try again. Original error: ${describeFetchError(err)}`
625
+ );
626
+ hinted.cause = err;
627
+ throw hinted;
628
+ }
629
+ throw err;
630
+ }
333
631
  function closeAfter(dispatcher, pending) {
334
632
  pending.catch(() => {
335
633
  }).finally(() => {
@@ -339,8 +637,24 @@ function closeAfter(dispatcher, pending) {
339
637
  }
340
638
  async function fetchAs(url, init, pinning) {
341
639
  if (!pinning.enforce && !pinning.capture) {
342
- const res2 = await fetch(url, withBoundedSignal(init));
343
- return { res: res2 };
640
+ const proxyUrl = selectProxyUrl(url);
641
+ if (!proxyUrl) {
642
+ try {
643
+ const res2 = await fetch(url, withBoundedSignal(init));
644
+ return { res: res2 };
645
+ } catch (err) {
646
+ rethrowWithIssuerHint(err, url);
647
+ }
648
+ }
649
+ const dispatcher2 = new ProxyAgent({ uri: proxyUrl, requestTls: { ca: effectiveCa() } });
650
+ const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
651
+ closeAfter(dispatcher2, pending2);
652
+ try {
653
+ const res2 = await pending2;
654
+ return { res: res2 };
655
+ } catch (err) {
656
+ rethrowWithIssuerHint(err, url);
657
+ }
344
658
  }
345
659
  if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
346
660
  throw new AsTlsMismatchError(
@@ -348,27 +662,27 @@ async function fetchAs(url, init, pinning) {
348
662
  );
349
663
  }
350
664
  if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
351
- const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
665
+ const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex, url);
352
666
  const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
353
667
  closeAfter(dispatcher2, pending2);
354
668
  try {
355
669
  const res2 = await pending2;
356
670
  return { res: res2 };
357
671
  } catch (err) {
358
- if (!isPinCheckError(err)) throw err;
672
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
359
673
  throw new AsTlsMismatchError(
360
- `TLS pin check failed for ${url} \u2014 ${describeFetchError(err)}. Refusing the connection; the API key / session cookie were never sent. If the Authority Server's certificate changed intentionally (a new key, not just renewal), an operator must clear the pairing and sign in again to re-pin it.`
674
+ `TLS pin check failed for ${url} \u2014 ${describeFetchError(err)}. Refusing the connection; the API key / session cookie were never sent. Two things produce this: (1) the Authority Server's TLS certificate genuinely changed (a new signing key, not just a renewal) \u2014 clear the pairing and sign in again to re-pin it; or (2) a TLS-inspecting proxy on this network is re-signing traffic with its own certificate \u2014 ask network/IT to exempt the Authority Server's host from TLS inspection (pin-tls refusing this, even with a trusted --ca-file, is working as intended).`
361
675
  );
362
676
  }
363
677
  }
364
- const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
678
+ const { dispatcher, getCapturedCert } = buildCapturingDispatcher(url);
365
679
  const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
366
680
  closeAfter(dispatcher, pending);
367
681
  let res;
368
682
  try {
369
683
  res = await pending;
370
684
  } catch (err) {
371
- if (!isPinCheckError(err)) throw err;
685
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
372
686
  throw new AsTlsMismatchError(
373
687
  `could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
374
688
  );
@@ -834,11 +1148,11 @@ var AttestationCache = class {
834
1148
  };
835
1149
 
836
1150
  // src/lib/gate-store.ts
837
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
1151
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
838
1152
  import { homedir } from "os";
839
- import { dirname as dirname3, join as join4 } from "path";
1153
+ import { dirname as dirname3, join as join5 } from "path";
840
1154
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
841
- var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir(), ".suveren");
1155
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir(), ".suveren");
842
1156
  var GateStore = class {
843
1157
  entries = /* @__PURE__ */ new Map();
844
1158
  baseDir;
@@ -856,11 +1170,11 @@ var GateStore = class {
856
1170
  */
857
1171
  setVaultKey(key) {
858
1172
  this.vaultKey = key;
859
- if (existsSync4(this.encryptedFilePath)) {
1173
+ if (existsSync5(this.encryptedFilePath)) {
860
1174
  this.loadEncrypted();
861
1175
  } else if (this.entries.size > 0) {
862
1176
  this.persistEncrypted();
863
- if (existsSync4(this.plaintextFilePath)) {
1177
+ if (existsSync5(this.plaintextFilePath)) {
864
1178
  try {
865
1179
  unlinkSync2(this.plaintextFilePath);
866
1180
  } catch {
@@ -898,11 +1212,11 @@ var GateStore = class {
898
1212
  clearAll() {
899
1213
  this.entries = /* @__PURE__ */ new Map();
900
1214
  try {
901
- if (existsSync4(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
1215
+ if (existsSync5(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
902
1216
  } catch {
903
1217
  }
904
1218
  try {
905
- if (existsSync4(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
1219
+ if (existsSync5(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
906
1220
  } catch {
907
1221
  }
908
1222
  }
@@ -935,19 +1249,19 @@ var GateStore = class {
935
1249
  }
936
1250
  // ─── File paths ─────────────────────────────────────────────────────────
937
1251
  get plaintextFilePath() {
938
- return join4(this.baseDir, "gates.json");
1252
+ return join5(this.baseDir, "gates.json");
939
1253
  }
940
1254
  get encryptedFilePath() {
941
- return join4(this.baseDir, "gates.enc.json");
1255
+ return join5(this.baseDir, "gates.enc.json");
942
1256
  }
943
1257
  // ─── Load / Persist ─────────────────────────────────────────────────────
944
1258
  loadPlaintext() {
945
- if (!existsSync4(this.plaintextFilePath)) {
1259
+ if (!existsSync5(this.plaintextFilePath)) {
946
1260
  mkdirSync3(this.baseDir, { recursive: true });
947
1261
  return;
948
1262
  }
949
1263
  try {
950
- const raw = readFileSync5(this.plaintextFilePath, "utf-8");
1264
+ const raw = readFileSync6(this.plaintextFilePath, "utf-8");
951
1265
  const data = JSON.parse(raw);
952
1266
  this.entries = new Map(Object.entries(data.entries));
953
1267
  } catch {
@@ -956,9 +1270,9 @@ var GateStore = class {
956
1270
  }
957
1271
  }
958
1272
  loadEncrypted() {
959
- if (!existsSync4(this.encryptedFilePath)) return;
1273
+ if (!existsSync5(this.encryptedFilePath)) return;
960
1274
  try {
961
- const raw = readFileSync5(this.encryptedFilePath, "utf-8");
1275
+ const raw = readFileSync6(this.encryptedFilePath, "utf-8");
962
1276
  const data = JSON.parse(raw);
963
1277
  this.entries = /* @__PURE__ */ new Map();
964
1278
  for (const [key, blob] of Object.entries(data.entries)) {
@@ -999,11 +1313,11 @@ var GateStore = class {
999
1313
  };
1000
1314
 
1001
1315
  // src/lib/execution-log.ts
1002
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
1316
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync6, unlinkSync as unlinkSync3 } from "fs";
1003
1317
  import { homedir as homedir2 } from "os";
1004
- import { dirname as dirname4, join as join5 } from "path";
1318
+ import { dirname as dirname4, join as join6 } from "path";
1005
1319
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
1006
- var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
1320
+ var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join6(homedir2(), ".suveren");
1007
1321
  var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
1008
1322
  var ExecutionLog = class {
1009
1323
  entries = [];
@@ -1022,11 +1336,11 @@ var ExecutionLog = class {
1022
1336
  */
1023
1337
  setVaultKey(key) {
1024
1338
  this.vaultKey = key;
1025
- if (existsSync5(this.encryptedFilePath)) {
1339
+ if (existsSync6(this.encryptedFilePath)) {
1026
1340
  this.loadEncrypted();
1027
1341
  } else if (this.entries.length > 0) {
1028
1342
  this.persistEncrypted();
1029
- if (existsSync5(this.plaintextFilePath)) {
1343
+ if (existsSync6(this.plaintextFilePath)) {
1030
1344
  try {
1031
1345
  unlinkSync3(this.plaintextFilePath);
1032
1346
  } catch {
@@ -1130,19 +1444,19 @@ var ExecutionLog = class {
1130
1444
  }
1131
1445
  // ─── File paths ─────────────────────────────────────────────────────────
1132
1446
  get plaintextFilePath() {
1133
- return join5(this.baseDir, "execution-log.json");
1447
+ return join6(this.baseDir, "execution-log.json");
1134
1448
  }
1135
1449
  get encryptedFilePath() {
1136
- return join5(this.baseDir, "execution-log.enc.json");
1450
+ return join6(this.baseDir, "execution-log.enc.json");
1137
1451
  }
1138
1452
  // ─── Load / Persist ─────────────────────────────────────────────────────
1139
1453
  loadPlaintext() {
1140
- if (!existsSync5(this.plaintextFilePath)) {
1454
+ if (!existsSync6(this.plaintextFilePath)) {
1141
1455
  mkdirSync4(this.baseDir, { recursive: true });
1142
1456
  return;
1143
1457
  }
1144
1458
  try {
1145
- const raw = readFileSync6(this.plaintextFilePath, "utf-8");
1459
+ const raw = readFileSync7(this.plaintextFilePath, "utf-8");
1146
1460
  const data = JSON.parse(raw);
1147
1461
  this.entries = data.entries;
1148
1462
  this.prune();
@@ -1152,9 +1466,9 @@ var ExecutionLog = class {
1152
1466
  }
1153
1467
  }
1154
1468
  loadEncrypted() {
1155
- if (!existsSync5(this.encryptedFilePath)) return;
1469
+ if (!existsSync6(this.encryptedFilePath)) return;
1156
1470
  try {
1157
- const raw = readFileSync6(this.encryptedFilePath, "utf-8");
1471
+ const raw = readFileSync7(this.encryptedFilePath, "utf-8");
1158
1472
  const data = JSON.parse(raw);
1159
1473
  const decrypted = this.decrypt(data.blob);
1160
1474
  const logFile = JSON.parse(decrypted);
@@ -1201,11 +1515,11 @@ function windowCutoff(window, now) {
1201
1515
  }
1202
1516
 
1203
1517
  // src/lib/denial-log.ts
1204
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "fs";
1518
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync7, unlinkSync as unlinkSync4 } from "fs";
1205
1519
  import { homedir as homedir3 } from "os";
1206
- import { dirname as dirname5, join as join6 } from "path";
1520
+ import { dirname as dirname5, join as join7 } from "path";
1207
1521
  import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
1208
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
1522
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join7(homedir3(), ".suveren");
1209
1523
  var MAX_RECORDS = 200;
1210
1524
  var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1211
1525
  var DenialLog = class {
@@ -1218,11 +1532,11 @@ var DenialLog = class {
1218
1532
  }
1219
1533
  setVaultKey(key) {
1220
1534
  this.vaultKey = key;
1221
- if (existsSync6(this.encryptedFilePath)) {
1535
+ if (existsSync7(this.encryptedFilePath)) {
1222
1536
  this.loadEncrypted();
1223
1537
  } else if (this.records.length > 0) {
1224
1538
  this.persistEncrypted();
1225
- if (existsSync6(this.plaintextFilePath)) {
1539
+ if (existsSync7(this.plaintextFilePath)) {
1226
1540
  try {
1227
1541
  unlinkSync4(this.plaintextFilePath);
1228
1542
  } catch {
@@ -1272,18 +1586,18 @@ var DenialLog = class {
1272
1586
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
1273
1587
  }
1274
1588
  get plaintextFilePath() {
1275
- return join6(this.baseDir, "denials.json");
1589
+ return join7(this.baseDir, "denials.json");
1276
1590
  }
1277
1591
  get encryptedFilePath() {
1278
- return join6(this.baseDir, "denials.enc.json");
1592
+ return join7(this.baseDir, "denials.enc.json");
1279
1593
  }
1280
1594
  loadPlaintext() {
1281
- if (!existsSync6(this.plaintextFilePath)) {
1595
+ if (!existsSync7(this.plaintextFilePath)) {
1282
1596
  mkdirSync5(this.baseDir, { recursive: true });
1283
1597
  return;
1284
1598
  }
1285
1599
  try {
1286
- const data = JSON.parse(readFileSync7(this.plaintextFilePath, "utf-8"));
1600
+ const data = JSON.parse(readFileSync8(this.plaintextFilePath, "utf-8"));
1287
1601
  this.records = data.records ?? [];
1288
1602
  } catch {
1289
1603
  console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
@@ -1291,9 +1605,9 @@ var DenialLog = class {
1291
1605
  }
1292
1606
  }
1293
1607
  loadEncrypted() {
1294
- if (!existsSync6(this.encryptedFilePath)) return;
1608
+ if (!existsSync7(this.encryptedFilePath)) return;
1295
1609
  try {
1296
- const data = JSON.parse(readFileSync7(this.encryptedFilePath, "utf-8"));
1610
+ const data = JSON.parse(readFileSync8(this.encryptedFilePath, "utf-8"));
1297
1611
  const logFile = JSON.parse(this.decrypt(data.blob));
1298
1612
  this.records = logFile.records ?? [];
1299
1613
  } catch (err) {
@@ -1319,11 +1633,11 @@ var DenialLog = class {
1319
1633
  };
1320
1634
 
1321
1635
  // src/lib/receipt-archive.ts
1322
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync5 } from "fs";
1636
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync5 } from "fs";
1323
1637
  import { homedir as homedir4 } from "os";
1324
- import { dirname as dirname6, join as join7 } from "path";
1638
+ import { dirname as dirname6, join as join8 } from "path";
1325
1639
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
1326
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
1640
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join8(homedir4(), ".suveren");
1327
1641
  function stripPreviews(proposal) {
1328
1642
  const args = proposal.toolArgs;
1329
1643
  if (!args || typeof args !== "object") return proposal;
@@ -1348,11 +1662,11 @@ var ReceiptArchive = class {
1348
1662
  */
1349
1663
  setVaultKey(key) {
1350
1664
  this.vaultKey = key;
1351
- if (existsSync7(this.encryptedFilePath)) {
1665
+ if (existsSync8(this.encryptedFilePath)) {
1352
1666
  this.loadEncrypted();
1353
1667
  } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
1354
1668
  this.persistEncrypted();
1355
- if (existsSync7(this.plaintextFilePath)) {
1669
+ if (existsSync8(this.plaintextFilePath)) {
1356
1670
  try {
1357
1671
  unlinkSync5(this.plaintextFilePath);
1358
1672
  } catch {
@@ -1366,7 +1680,7 @@ var ReceiptArchive = class {
1366
1680
  * this state — that would report a hollow success over unreadable evidence.
1367
1681
  */
1368
1682
  isLocked() {
1369
- return this.vaultKey === null && existsSync7(this.encryptedFilePath);
1683
+ return this.vaultKey === null && existsSync8(this.encryptedFilePath);
1370
1684
  }
1371
1685
  /**
1372
1686
  * Archive a signed receipt (and the grant it was issued under).
@@ -1467,19 +1781,19 @@ var ReceiptArchive = class {
1467
1781
  }
1468
1782
  // ─── File paths ─────────────────────────────────────────────────────────
1469
1783
  get plaintextFilePath() {
1470
- return join7(this.baseDir, "receipt-archive.json");
1784
+ return join8(this.baseDir, "receipt-archive.json");
1471
1785
  }
1472
1786
  get encryptedFilePath() {
1473
- return join7(this.baseDir, "receipt-archive.enc.json");
1787
+ return join8(this.baseDir, "receipt-archive.enc.json");
1474
1788
  }
1475
1789
  // ─── Load / Persist ─────────────────────────────────────────────────────
1476
1790
  loadPlaintext() {
1477
- if (!existsSync7(this.plaintextFilePath)) {
1791
+ if (!existsSync8(this.plaintextFilePath)) {
1478
1792
  mkdirSync6(this.baseDir, { recursive: true });
1479
1793
  return;
1480
1794
  }
1481
1795
  try {
1482
- const raw = readFileSync8(this.plaintextFilePath, "utf-8");
1796
+ const raw = readFileSync9(this.plaintextFilePath, "utf-8");
1483
1797
  const parsed = JSON.parse(raw);
1484
1798
  this.data = {
1485
1799
  version: 1,
@@ -1491,16 +1805,16 @@ var ReceiptArchive = class {
1491
1805
  try {
1492
1806
  writeFileSync6(
1493
1807
  `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1494
- readFileSync8(this.plaintextFilePath)
1808
+ readFileSync9(this.plaintextFilePath)
1495
1809
  );
1496
1810
  } catch {
1497
1811
  }
1498
1812
  }
1499
1813
  }
1500
1814
  loadEncrypted() {
1501
- if (!existsSync7(this.encryptedFilePath)) return;
1815
+ if (!existsSync8(this.encryptedFilePath)) return;
1502
1816
  try {
1503
- const raw = readFileSync8(this.encryptedFilePath, "utf-8");
1817
+ const raw = readFileSync9(this.encryptedFilePath, "utf-8");
1504
1818
  const data = JSON.parse(raw);
1505
1819
  const decrypted = this.decrypt(data.blob);
1506
1820
  const parsed = JSON.parse(decrypted);
@@ -1514,7 +1828,7 @@ var ReceiptArchive = class {
1514
1828
  try {
1515
1829
  writeFileSync6(
1516
1830
  `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1517
- readFileSync8(this.encryptedFilePath)
1831
+ readFileSync9(this.encryptedFilePath)
1518
1832
  );
1519
1833
  } catch {
1520
1834
  }
@@ -1542,11 +1856,11 @@ var ReceiptArchive = class {
1542
1856
  };
1543
1857
 
1544
1858
  // src/lib/execution-journal.ts
1545
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync } from "fs";
1859
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, renameSync } from "fs";
1546
1860
  import { homedir as homedir5 } from "os";
1547
- import { dirname as dirname7, join as join8 } from "path";
1861
+ import { dirname as dirname7, join as join9 } from "path";
1548
1862
  import { createHash as createHash3 } from "crypto";
1549
- var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
1863
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join9(homedir5(), ".suveren");
1550
1864
  var FILE_NAME = "execution-journal.json";
1551
1865
  var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1552
1866
  function hashToolArgs(args) {
@@ -1563,7 +1877,7 @@ var ExecutionJournal = class {
1563
1877
  filePath;
1564
1878
  constructor(filePath) {
1565
1879
  const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR5;
1566
- this.filePath = join8(baseDir, FILE_NAME);
1880
+ this.filePath = join9(baseDir, FILE_NAME);
1567
1881
  }
1568
1882
  /** Look a ticket up. Always reads from disk — another process may have written. */
1569
1883
  get(ticketId) {
@@ -1602,9 +1916,9 @@ var ExecutionJournal = class {
1602
1916
  }
1603
1917
  // ─── Persistence ────────────────────────────────────────────────────────
1604
1918
  load() {
1605
- if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1919
+ if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1606
1920
  try {
1607
- const parsed = JSON.parse(readFileSync9(this.filePath, "utf8"));
1921
+ const parsed = JSON.parse(readFileSync10(this.filePath, "utf8"));
1608
1922
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
1609
1923
  throw new Error("unexpected shape");
1610
1924
  }
@@ -1629,10 +1943,10 @@ function nowSec() {
1629
1943
  }
1630
1944
 
1631
1945
  // src/lib/proposal-submission-store.ts
1632
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
1946
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync2 } from "fs";
1633
1947
  import { homedir as homedir6 } from "os";
1634
- import { dirname as dirname8, join as join9 } from "path";
1635
- var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
1948
+ import { dirname as dirname8, join as join10 } from "path";
1949
+ var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join10(homedir6(), ".suveren");
1636
1950
  var FILE_NAME2 = "proposal-submissions.json";
1637
1951
  var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
1638
1952
  function nowSec2() {
@@ -1642,7 +1956,7 @@ var ProposalSubmissionStore = class {
1642
1956
  filePath;
1643
1957
  constructor(filePath) {
1644
1958
  const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR6;
1645
- this.filePath = join9(baseDir, FILE_NAME2);
1959
+ this.filePath = join10(baseDir, FILE_NAME2);
1646
1960
  }
1647
1961
  /** Record what was submitted, right after a successful submitProposal(). */
1648
1962
  record(entry) {
@@ -1665,9 +1979,9 @@ var ProposalSubmissionStore = class {
1665
1979
  return this.load().entries.find((e) => e.proposalId === proposalId);
1666
1980
  }
1667
1981
  load() {
1668
- if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1982
+ if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
1669
1983
  try {
1670
- const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
1984
+ const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
1671
1985
  const cutoff = nowSec2() - MAX_AGE_SECONDS3;
1672
1986
  parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
1673
1987
  return parsed;
@@ -1956,17 +2270,17 @@ function formatConsumptionFull(entries) {
1956
2270
  }
1957
2271
 
1958
2272
  // src/lib/context-loader.ts
1959
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
2273
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
1960
2274
  import { homedir as homedir7 } from "os";
1961
- import { join as join10 } from "path";
1962
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2275
+ import { join as join11 } from "path";
2276
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
1963
2277
  var BRIEF_MAX_CHARS = 16 * 1024;
1964
2278
  function readContextFile(dataDir2) {
1965
2279
  const dir = dataDir2 ?? DEFAULT_DIR7;
1966
- const filePath = join10(dir, "context.md");
1967
- if (!existsSync10(filePath)) return null;
2280
+ const filePath = join11(dir, "context.md");
2281
+ if (!existsSync11(filePath)) return null;
1968
2282
  try {
1969
- const content = readFileSync11(filePath, "utf-8").trim();
2283
+ const content = readFileSync12(filePath, "utf-8").trim();
1970
2284
  return content || null;
1971
2285
  } catch {
1972
2286
  return null;
@@ -2605,30 +2919,49 @@ import { extname } from "path";
2605
2919
  import { randomUUID } from "crypto";
2606
2920
 
2607
2921
  // src/lib/manifest-loader.ts
2608
- import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
2609
- import { join as join11, resolve } from "path";
2922
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
2923
+ import { join as join12, resolve } from "path";
2924
+ var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
2925
+ function isExactSemver(value) {
2926
+ return typeof value === "string" && EXACT_SEMVER_RE.test(value);
2927
+ }
2928
+ function invalidNpmPinReason(manifest) {
2929
+ if (!manifest.npmPackage) return null;
2930
+ if (manifest.npmVersion === void 0) {
2931
+ return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
2932
+ }
2933
+ if (!isExactSemver(manifest.npmVersion)) {
2934
+ return `npmVersion ${JSON.stringify(manifest.npmVersion)} for "${manifest.npmPackage}" is not an exact semver (e.g. "1.2.3") \u2014 ranges, dist-tags like "latest", git specs, URLs, and file paths are not allowed`;
2935
+ }
2936
+ return null;
2937
+ }
2610
2938
  var manifests = /* @__PURE__ */ new Map();
2611
2939
  function loadManifests(integrationsDir) {
2612
2940
  const dir = resolve(
2613
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2941
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2614
2942
  );
2615
- const indexPath = join11(dir, "index.json");
2616
- if (!existsSync11(indexPath)) {
2943
+ const indexPath = join12(dir, "index.json");
2944
+ if (!existsSync12(indexPath)) {
2617
2945
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
2618
2946
  return 0;
2619
2947
  }
2620
2948
  let index;
2621
2949
  try {
2622
- index = JSON.parse(readFileSync12(indexPath, "utf-8"));
2950
+ index = JSON.parse(readFileSync13(indexPath, "utf-8"));
2623
2951
  } catch (err) {
2624
2952
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
2625
2953
  return 0;
2626
2954
  }
2627
2955
  let loaded = 0;
2628
2956
  for (const [id, relativePath] of Object.entries(index.integrations)) {
2629
- const manifestPath = join11(dir, relativePath);
2957
+ const manifestPath = join12(dir, relativePath);
2630
2958
  try {
2631
- const manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
2959
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
2960
+ const pinReason = invalidNpmPinReason(manifest);
2961
+ if (pinReason) {
2962
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
2963
+ continue;
2964
+ }
2632
2965
  manifests.set(id, manifest);
2633
2966
  loaded++;
2634
2967
  } catch (err) {
@@ -2647,6 +2980,8 @@ function getAllManifests() {
2647
2980
 
2648
2981
  // src/lib/simulation-mode.ts
2649
2982
  function isSimulationMode() {
2983
+ const policySimulation = readPolicy().policy.simulation;
2984
+ if (policySimulation !== void 0) return policySimulation;
2650
2985
  return process.env.SUVEREN_SIMULATION === "1";
2651
2986
  }
2652
2987
  function manifestIsSimulated(manifest) {
@@ -4176,16 +4511,16 @@ function verifyGateContentHashes(content, auth) {
4176
4511
  }
4177
4512
 
4178
4513
  // src/lib/integration-registry.ts
4179
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync12 } from "fs";
4514
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "fs";
4180
4515
  import { homedir as homedir8 } from "os";
4181
- import { dirname as dirname9, join as join12 } from "path";
4182
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join12(homedir8(), ".suveren");
4516
+ import { dirname as dirname9, join as join13 } from "path";
4517
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join13(homedir8(), ".suveren");
4183
4518
  var IntegrationRegistry = class {
4184
4519
  integrations = /* @__PURE__ */ new Map();
4185
4520
  filePath;
4186
4521
  constructor(dataDir2) {
4187
4522
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
4188
- this.filePath = join12(dir, "integrations.json");
4523
+ this.filePath = join13(dir, "integrations.json");
4189
4524
  this.load();
4190
4525
  }
4191
4526
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -4216,12 +4551,12 @@ var IntegrationRegistry = class {
4216
4551
  }
4217
4552
  // ─── Load / Save ────────────────────────────────────────────────────────
4218
4553
  load() {
4219
- if (!existsSync12(this.filePath)) {
4554
+ if (!existsSync13(this.filePath)) {
4220
4555
  mkdirSync9(dirname9(this.filePath), { recursive: true });
4221
4556
  return;
4222
4557
  }
4223
4558
  try {
4224
- const raw = readFileSync13(this.filePath, "utf-8");
4559
+ const raw = readFileSync14(this.filePath, "utf-8");
4225
4560
  const data = JSON.parse(raw);
4226
4561
  const migrated = data.integrations.map((i) => {
4227
4562
  if ("toolGating" in i && !("profile" in i)) {
@@ -4257,8 +4592,8 @@ var IntegrationRegistry = class {
4257
4592
 
4258
4593
  // src/lib/integration-manager.ts
4259
4594
  import { homedir as homedir9 } from "os";
4260
- import { join as join13, delimiter } from "path";
4261
- import { existsSync as existsSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync } from "fs";
4595
+ import { join as join14, delimiter } from "path";
4596
+ import { existsSync as existsSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync } from "fs";
4262
4597
  import { execFile } from "child_process";
4263
4598
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4264
4599
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -4335,25 +4670,26 @@ function safeHost(url) {
4335
4670
  }
4336
4671
 
4337
4672
  // src/lib/integration-manager.ts
4338
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
4339
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join13(DEFAULT_DATA_DIR2, "integrations");
4673
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join14(homedir9(), ".suveren");
4674
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join14(DEFAULT_DATA_DIR2, "integrations");
4675
+ var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
4340
4676
  var installLock = Promise.resolve();
4341
4677
  function withInstallLock(task) {
4342
4678
  const run = installLock.then(task, task);
4343
4679
  installLock = run.then(() => void 0, () => void 0);
4344
4680
  return run;
4345
4681
  }
4346
- var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
4682
+ var INTEGRATIONS_BIN = join14(INTEGRATIONS_DIR, "node_modules", ".bin");
4347
4683
  function buildPath() {
4348
4684
  const base = process.env.PATH ?? "";
4349
4685
  return [INTEGRATIONS_BIN, base].join(delimiter);
4350
4686
  }
4351
4687
  function ensureIntegrationsDir() {
4352
- if (!existsSync13(INTEGRATIONS_DIR)) {
4688
+ if (!existsSync14(INTEGRATIONS_DIR)) {
4353
4689
  mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
4354
4690
  }
4355
- const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
4356
- if (!existsSync13(pkgPath)) {
4691
+ const pkgPath = join14(INTEGRATIONS_DIR, "package.json");
4692
+ if (!existsSync14(pkgPath)) {
4357
4693
  writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
4358
4694
  }
4359
4695
  }
@@ -4383,6 +4719,17 @@ var IntegrationManager = class {
4383
4719
  }
4384
4720
  running = /* @__PURE__ */ new Map();
4385
4721
  onToolsChanged = null;
4722
+ /**
4723
+ * Called when a persisted config was migrated to the manifest's command/args
4724
+ * (see `migrateNpxConfig`) so the caller can write the fix back to
4725
+ * `integrations.json` — the manager itself has no registry/persistence
4726
+ * handle, by design (mirrors `onToolsChanged`). Without a subscriber the
4727
+ * migration still takes effect for THIS start (the in-memory config is
4728
+ * still corrected before spawn); only the one-time persistence is skipped,
4729
+ * so every later start keeps re-migrating in memory, which is safe, if
4730
+ * redundant.
4731
+ */
4732
+ onConfigMigrated = null;
4386
4733
  /**
4387
4734
  * Per-integration operation queue. Start/stop for one id are serialized
4388
4735
  * through here; without it, a second start issued while the first is still
@@ -4408,6 +4755,16 @@ var IntegrationManager = class {
4408
4755
  setOnToolsChanged(cb) {
4409
4756
  this.onToolsChanged = cb;
4410
4757
  }
4758
+ /**
4759
+ * Register a callback invoked when a persisted config is migrated off a
4760
+ * stale `npx` command to the current manifest's command/args (see
4761
+ * `migrateNpxConfig`). The caller is expected to persist `updates` against
4762
+ * `id` in `integrations.json` so the fix survives the next boot instead of
4763
+ * silently reapplying on every start.
4764
+ */
4765
+ setOnConfigMigrated(cb) {
4766
+ this.onConfigMigrated = cb;
4767
+ }
4411
4768
  /**
4412
4769
  * Is `npmPackage` present AND usable?
4413
4770
  *
@@ -4420,24 +4777,41 @@ var IntegrationManager = class {
4420
4777
  * exist, and treat anything else as not-installed (and reinstallable).
4421
4778
  */
4422
4779
  isUsableInstall(npmPackage) {
4423
- const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4424
- const pkgJson = join13(pkgDir, "package.json");
4425
- if (!existsSync13(pkgDir) || !existsSync13(pkgJson)) return false;
4780
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4781
+ const pkgJson = join14(pkgDir, "package.json");
4782
+ if (!existsSync14(pkgDir) || !existsSync14(pkgJson)) return false;
4426
4783
  try {
4427
- const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4784
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4428
4785
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
4429
4786
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
4430
4787
  const binEntries = Object.entries(binMap);
4431
4788
  for (const [binName, target] of binEntries) {
4432
- if (!existsSync13(join13(pkgDir, target))) return false;
4789
+ if (!existsSync14(join14(pkgDir, target))) return false;
4433
4790
  if (!this.binShimExists(binName)) return false;
4434
4791
  }
4435
- if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
4792
+ if (binEntries.length === 0 && pkg.main && !existsSync14(join14(pkgDir, pkg.main))) return false;
4436
4793
  return true;
4437
4794
  } catch {
4438
4795
  return false;
4439
4796
  }
4440
4797
  }
4798
+ /**
4799
+ * The installed version of `npmPackage`, or null if it isn't usably
4800
+ * installed at all (see `isUsableInstall`) or its package.json has no
4801
+ * parseable `version`. Never throws — an unreadable version is treated the
4802
+ * same as "not installed", which is the safe direction: it drives a
4803
+ * (re)install rather than a false "already pinned".
4804
+ */
4805
+ readInstalledVersion(npmPackage) {
4806
+ if (!this.isUsableInstall(npmPackage)) return null;
4807
+ const pkgJson = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
4808
+ try {
4809
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4810
+ return typeof pkg.version === "string" ? pkg.version : null;
4811
+ } catch {
4812
+ return null;
4813
+ }
4814
+ }
4441
4815
  /**
4442
4816
  * Does the node_modules/.bin shim for `binName` exist? On Windows npm writes
4443
4817
  * `<name>`, `<name>.cmd`, and `<name>.ps1`; the .cmd variant is what actually
@@ -4446,11 +4820,34 @@ var IntegrationManager = class {
4446
4820
  binShimExists(binName) {
4447
4821
  if (!binName) return false;
4448
4822
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
4449
- return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
4823
+ return candidates.some((c) => existsSync14(join14(INTEGRATIONS_BIN, c)));
4824
+ }
4825
+ /**
4826
+ * The exact version THIS gateway's CURRENTLY LOADED manifest pins
4827
+ * `config.npmPackage` to — or undefined when there's no npmPackage, no
4828
+ * manifest for this id, or the manifest's `npmPackage` no longer names the
4829
+ * same package as `config` (a manually re-pointed integration: nothing in
4830
+ * the manifest vouches for a version of config's actual package).
4831
+ *
4832
+ * Read fresh from `getManifest(config.id)` on every call, never from
4833
+ * anything persisted on `config` itself — manifests are reloaded fresh on
4834
+ * every boot, `integrations.json` is not, so this is what keeps the pin
4835
+ * from going stale the day after an integration was first activated. Used
4836
+ * both to decide the npm install/update (`ensureInstalled`) and to decide
4837
+ * whether a persisted `npx …` command must be refused/migrated
4838
+ * (`migrateNpxConfig`) — one definition of "pinned", so the two can't
4839
+ * disagree about which connectors it applies to.
4840
+ */
4841
+ pinnedVersionFor(config) {
4842
+ if (!config.npmPackage) return void 0;
4843
+ const manifest = getManifest(config.id);
4844
+ return manifest?.npmPackage === config.npmPackage ? manifest.npmVersion : void 0;
4450
4845
  }
4451
4846
  /**
4452
4847
  * Install an npm package into the managed integrations directory if not
4453
- * already present. Called automatically before spawning when
4848
+ * already present, and — when `config`'s manifest pins an exact
4849
+ * `npmVersion` for this same package — bring the installed copy to EXACTLY
4850
+ * that version, older or newer. Called automatically before spawning when
4454
4851
  * config.npmPackage is set.
4455
4852
  *
4456
4853
  * Asynchronous ON PURPOSE. This used to be execSync, which blocked the MCP
@@ -4460,19 +4857,62 @@ var IntegrationManager = class {
4460
4857
  * CI runners: ~3.6s (Linux), ~5.4s (macOS), ~14s (Windows), and far worse
4461
4858
  * on machines with real-time antivirus.
4462
4859
  */
4463
- async ensureInstalled(npmPackage) {
4860
+ async ensureInstalled(config) {
4861
+ const npmPackage = config.npmPackage;
4862
+ if (!npmPackage) return;
4464
4863
  ensureIntegrationsDir();
4864
+ const pinnedVersion = this.pinnedVersionFor(config);
4865
+ if (OFFLINE_MODE) {
4866
+ const current = this.readInstalledVersion(npmPackage);
4867
+ if (pinnedVersion) {
4868
+ if (current === pinnedVersion) return;
4869
+ throw new Error(
4870
+ `${config.id}: connector ${npmPackage} version ${pinnedVersion} is not part of this installation (found ${current ?? "nothing installed"} at ${INTEGRATIONS_DIR}; SUVEREN_OFFLINE=1 so no npm install was attempted).`
4871
+ );
4872
+ }
4873
+ if (current !== null) return;
4874
+ throw new Error(
4875
+ `${config.id}: connector ${npmPackage} is not part of this installation (nothing installed at ${INTEGRATIONS_DIR}; SUVEREN_OFFLINE=1 so no npm install was attempted).`
4876
+ );
4877
+ }
4878
+ if (pinnedVersion) {
4879
+ if (this.readInstalledVersion(npmPackage) === pinnedVersion) return;
4880
+ return withInstallLock(async () => {
4881
+ const current = this.readInstalledVersion(npmPackage);
4882
+ if (current === pinnedVersion) return;
4883
+ console.error(
4884
+ `[IntegrationManager] ${npmPackage} ${current ?? "(not installed)"} \u2192 ${pinnedVersion} (pinned by manifest)`
4885
+ );
4886
+ const cleanFirst = !this.isUsableInstall(npmPackage);
4887
+ await this.installNow(npmPackage, pinnedVersion, cleanFirst);
4888
+ });
4889
+ }
4465
4890
  if (this.isUsableInstall(npmPackage)) return;
4466
4891
  return withInstallLock(async () => {
4467
4892
  if (this.isUsableInstall(npmPackage)) return;
4468
4893
  await this.installNow(npmPackage);
4469
4894
  });
4470
4895
  }
4471
- /** The actual install. Callers MUST hold the install lock. */
4472
- async installNow(npmPackage) {
4473
- const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4474
- if (existsSync13(pkgDir)) {
4475
- console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
4896
+ /**
4897
+ * The actual install. Callers MUST hold the install lock.
4898
+ *
4899
+ * `exactVersion`, when given, is a pin already validated as an exact semver
4900
+ * by the manifest loader (`isExactSemver`) — re-validated here anyway
4901
+ * because this method does not trust its caller's caller, and because the
4902
+ * version reaches a shell on Windows (see the package-name check below).
4903
+ *
4904
+ * `cleanFirst` (default true, i.e. today's behaviour for every caller
4905
+ * except a version-only pin correction): wipe any existing package
4906
+ * directory before installing. Needed for a genuinely broken/partial
4907
+ * install, where stale files can make the reinstall itself fail; skipped
4908
+ * for a USABLE install being corrected to a different pinned version, so a
4909
+ * failed update leaves the previously-working version on disk instead of
4910
+ * nothing at all.
4911
+ */
4912
+ async installNow(npmPackage, exactVersion, cleanFirst = true) {
4913
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4914
+ if (cleanFirst && existsSync14(pkgDir)) {
4915
+ console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
4476
4916
  try {
4477
4917
  rmSync(pkgDir, { recursive: true, force: true });
4478
4918
  } catch (err) {
@@ -4482,18 +4922,22 @@ var IntegrationManager = class {
4482
4922
  if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(npmPackage)) {
4483
4923
  throw new Error(`Refusing to install unsafe package name: ${JSON.stringify(npmPackage)}`);
4484
4924
  }
4485
- console.error(`[IntegrationManager] Installing ${npmPackage}...`);
4925
+ if (exactVersion !== void 0 && !isExactSemver(exactVersion)) {
4926
+ throw new Error(`Refusing to install ${npmPackage} at unsafe/non-exact version spec: ${JSON.stringify(exactVersion)}`);
4927
+ }
4928
+ const spec = exactVersion ? `${npmPackage}@${exactVersion}` : npmPackage;
4929
+ console.error(`[IntegrationManager] Installing ${spec}...`);
4486
4930
  const startedAt = Date.now();
4487
4931
  const isWin = process.platform === "win32";
4488
4932
  await new Promise((resolve3, reject) => {
4489
4933
  const child = execFile(
4490
4934
  "npm",
4491
- ["install", "--no-fund", "--no-audit", npmPackage],
4935
+ ["install", "--no-fund", "--no-audit", spec],
4492
4936
  {
4493
4937
  cwd: INTEGRATIONS_DIR,
4494
4938
  // Windows: a shell so cmd.exe finds npm.cmd (see EINVAL note above).
4495
4939
  // POSIX: no shell — npm is a normal executable and keeping shell off
4496
- // means the validated package name is still passed as a bare argv.
4940
+ // means the validated package spec is still passed as a bare argv.
4497
4941
  shell: isWin,
4498
4942
  // No timeout: a slow install on a machine with antivirus is not an
4499
4943
  // error, and killing it midway is what created broken installs in
@@ -4503,20 +4947,28 @@ var IntegrationManager = class {
4503
4947
  },
4504
4948
  (err, _stdout, stderr) => {
4505
4949
  if (err) {
4506
- reject(new Error(`Failed to install ${npmPackage}: ${err.message}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`));
4950
+ reject(new Error(`Failed to install ${spec}: ${err.message}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`));
4507
4951
  return;
4508
4952
  }
4509
4953
  resolve3();
4510
4954
  }
4511
4955
  );
4512
- child.on("error", (err) => reject(new Error(`Failed to run npm for ${npmPackage}: ${err.message}`)));
4956
+ child.on("error", (err) => reject(new Error(`Failed to run npm for ${spec}: ${err.message}`)));
4513
4957
  });
4514
4958
  if (!this.isUsableInstall(npmPackage)) {
4515
4959
  throw new Error(
4516
- `Installed ${npmPackage} but it is not usable \u2014 package.json or its bin target is missing. Check ${INTEGRATIONS_DIR} for a partial install.`
4960
+ `Installed ${spec} but it is not usable \u2014 package.json or its bin target is missing. Check ${INTEGRATIONS_DIR} for a partial install.`
4517
4961
  );
4518
4962
  }
4519
- console.error(`[IntegrationManager] Installed ${npmPackage} (${((Date.now() - startedAt) / 1e3).toFixed(1)}s)`);
4963
+ if (exactVersion !== void 0) {
4964
+ const installed = this.readInstalledVersion(npmPackage);
4965
+ if (installed !== exactVersion) {
4966
+ throw new Error(
4967
+ `Installed ${npmPackage} but got version ${installed ?? "unknown"}, expected the pinned ${exactVersion}.`
4968
+ );
4969
+ }
4970
+ }
4971
+ console.error(`[IntegrationManager] Installed ${spec} (${((Date.now() - startedAt) / 1e3).toFixed(1)}s)`);
4520
4972
  }
4521
4973
  /**
4522
4974
  * Connect a client to its transport, but never wait indefinitely. Resolves
@@ -4575,7 +5027,26 @@ var IntegrationManager = class {
4575
5027
  throw new Error(`${config.id}: ${SIMULATION_BLOCK_REASON}`);
4576
5028
  }
4577
5029
  if (config.npmPackage) {
4578
- await this.ensureInstalled(config.npmPackage);
5030
+ await this.ensureInstalled(config);
5031
+ }
5032
+ const pinnedVersion = this.pinnedVersionFor(config);
5033
+ if (pinnedVersion && config.command === "npx") {
5034
+ const manifest = getManifest(config.id);
5035
+ const migrated = {
5036
+ command: manifest.mcp.command,
5037
+ args: manifest.mcp.args,
5038
+ env: manifest.mcp.env ?? config.env
5039
+ };
5040
+ console.error(
5041
+ `[IntegrationManager] ${config.id}: command migrated to manifest (was npx ${config.args.join(" ")})`
5042
+ );
5043
+ config = { ...config, ...migrated };
5044
+ this.onConfigMigrated?.(config.id, migrated);
5045
+ }
5046
+ if (pinnedVersion && config.command === "npx") {
5047
+ throw new Error(
5048
+ `${config.id}: refusing to spawn a pinned connector ("${config.npmPackage}"@${pinnedVersion}) via npx \u2014 this would ignore the pinned local install and fetch the package fresh on every start. The manifest's mcp.command must name the installed binary, not "npx".`
5049
+ );
4579
5050
  }
4580
5051
  const env = this.resolveEnvKeys(config);
4581
5052
  if (isSimulationMode()) {
@@ -4929,28 +5400,28 @@ var IntegrationManager = class {
4929
5400
  };
4930
5401
 
4931
5402
  // src/lib/profile-loader.ts
4932
- import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
4933
- import { join as join14, resolve as resolve2 } from "path";
5403
+ import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
5404
+ import { join as join15, resolve as resolve2 } from "path";
4934
5405
  import { registerProfile } from "@hap/core";
4935
5406
  function loadProfiles(profilesDir) {
4936
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
4937
- const indexPath = join14(dir, "index.json");
4938
- if (!existsSync14(indexPath)) {
5407
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join15(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
5408
+ const indexPath = join15(dir, "index.json");
5409
+ if (!existsSync15(indexPath)) {
4939
5410
  warnNoProfiles(`No index.json at ${indexPath}`);
4940
5411
  return 0;
4941
5412
  }
4942
5413
  let index;
4943
5414
  try {
4944
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
5415
+ index = JSON.parse(readFileSync16(indexPath, "utf-8"));
4945
5416
  } catch (err) {
4946
5417
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
4947
5418
  return 0;
4948
5419
  }
4949
5420
  let loaded = 0;
4950
5421
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
4951
- const profilePath = join14(dir, relativePath);
5422
+ const profilePath = join15(dir, relativePath);
4952
5423
  try {
4953
- const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
5424
+ const profile = JSON.parse(readFileSync16(profilePath, "utf-8"));
4954
5425
  registerProfile(profileId, profile);
4955
5426
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
4956
5427
  if (shortName && shortName !== profileId) {
@@ -4986,8 +5457,8 @@ function warnNoProfiles(reason) {
4986
5457
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
4987
5458
 
4988
5459
  // src/lib/committed-executor.ts
4989
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
4990
- import { dirname as dirname10, join as join15 } from "path";
5460
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync17, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
5461
+ import { dirname as dirname10, join as join16 } from "path";
4991
5462
  var CommittedExecutor = class {
4992
5463
  constructor(run) {
4993
5464
  this.run = run;
@@ -5015,7 +5486,7 @@ var ExecutorLock = class {
5015
5486
  path;
5016
5487
  held = false;
5017
5488
  constructor(dataDir2) {
5018
- this.path = join15(dataDir2, "executor.lock");
5489
+ this.path = join16(dataDir2, "executor.lock");
5019
5490
  }
5020
5491
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
5021
5492
  acquire() {
@@ -5047,9 +5518,9 @@ var ExecutorLock = class {
5047
5518
  }
5048
5519
  }
5049
5520
  read() {
5050
- if (!existsSync15(this.path)) return null;
5521
+ if (!existsSync16(this.path)) return null;
5051
5522
  try {
5052
- const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
5523
+ const parsed = JSON.parse(readFileSync17(this.path, "utf8"));
5053
5524
  return typeof parsed?.pid === "number" ? parsed : null;
5054
5525
  } catch {
5055
5526
  return null;
@@ -5067,19 +5538,19 @@ function isAlive(pid) {
5067
5538
 
5068
5539
  // bin/http.ts
5069
5540
  import { homedir as homedir10 } from "os";
5070
- import { join as join17 } from "path";
5541
+ import { join as join18 } from "path";
5071
5542
 
5072
5543
  // src/lib/mcp-as-tracker.ts
5073
- import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "fs";
5074
- import { dirname as dirname11, join as join16 } from "path";
5544
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
5545
+ import { dirname as dirname11, join as join17 } from "path";
5075
5546
  function trackerPath(dataDir2) {
5076
- return join16(dataDir2, "mcp-as-pairing.json");
5547
+ return join17(dataDir2, "mcp-as-pairing.json");
5077
5548
  }
5078
5549
  function readMcpPairedAsUrl(dataDir2) {
5079
5550
  const path = trackerPath(dataDir2);
5080
- if (!existsSync16(path)) return null;
5551
+ if (!existsSync17(path)) return null;
5081
5552
  try {
5082
- const data = JSON.parse(readFileSync17(path, "utf-8"));
5553
+ const data = JSON.parse(readFileSync18(path, "utf-8"));
5083
5554
  return typeof data.asUrl === "string" ? data.asUrl : null;
5084
5555
  } catch {
5085
5556
  return null;
@@ -5092,7 +5563,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
5092
5563
  }
5093
5564
 
5094
5565
  // bin/http.ts
5095
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
5566
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5096
5567
  var spUrl = resolveAsUrl(dataDir);
5097
5568
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
5098
5569
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -5129,6 +5600,10 @@ function refreshAllSessions() {
5129
5600
  integrationManager.setOnToolsChanged(() => {
5130
5601
  refreshAllSessions();
5131
5602
  });
5603
+ integrationManager.setOnConfigMigrated((id, updates) => {
5604
+ integrationRegistry.update(id, updates);
5605
+ console.error(`[Suveren MCP] ${id}: persisted config migrated (${Object.keys(updates).join(", ")})`);
5606
+ });
5132
5607
  var app = express();
5133
5608
  app.use(express.json());
5134
5609
  app.use((_req, res, next) => {
@@ -5615,7 +6090,7 @@ app.listen(port, "0.0.0.0", () => {
5615
6090
  (proposal) => executeCommitted(proposal, state, integrationManager)
5616
6091
  );
5617
6092
  installCommittedExecutor(committedExecutor);
5618
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
6093
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5619
6094
  const executorLock = new ExecutorLock(dataDir2);
5620
6095
  const lock = executorLock.acquire();
5621
6096
  if (!lock.held) {