@suveren/gateway 0.9.0 → 0.10.1
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 +33 -1
- package/bin/suveren-gateway.js +203 -5
- package/content/integrations/crm.json +47 -3
- package/content/integrations/erp.json +231 -0
- package/content/integrations/index.json +3 -1
- package/content/integrations/mail.json +147 -0
- package/dist/control-plane/index.mjs +688 -179
- package/dist/mcp-server/http.mjs +483 -259
- package/dist/ui/assets/index-CV9Mbbkd.css +1 -0
- package/dist/ui/assets/{index-CIiOCgjJ.js → index-sq564t5H.js} +23 -23
- package/dist/ui/index.html +2 -2
- package/lib/as-pairing.mjs +77 -0
- package/lib/config.mjs +48 -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/node_modules/undici/LICENSE +21 -0
- package/node_modules/undici/README.md +741 -0
- package/node_modules/undici/docs/docs/api/Agent.md +84 -0
- package/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
- package/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
- package/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
- package/node_modules/undici/docs/docs/api/Client.md +288 -0
- package/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
- package/node_modules/undici/docs/docs/api/Connector.md +115 -0
- package/node_modules/undici/docs/docs/api/ContentType.md +57 -0
- package/node_modules/undici/docs/docs/api/Cookies.md +128 -0
- package/node_modules/undici/docs/docs/api/Debug.md +62 -0
- package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
- package/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
- package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
- package/node_modules/undici/docs/docs/api/Errors.md +49 -0
- package/node_modules/undici/docs/docs/api/EventSource.md +45 -0
- package/node_modules/undici/docs/docs/api/Fetch.md +60 -0
- package/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
- package/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
- package/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
- package/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
- package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
- package/node_modules/undici/docs/docs/api/MockClient.md +81 -0
- package/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
- package/node_modules/undici/docs/docs/api/MockPool.md +555 -0
- package/node_modules/undici/docs/docs/api/Pool.md +84 -0
- package/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
- package/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
- package/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
- package/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
- package/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
- package/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
- package/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
- package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
- package/node_modules/undici/docs/docs/api/Util.md +25 -0
- package/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
- package/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
- package/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
- package/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
- package/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
- package/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
- package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
- package/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
- package/node_modules/undici/index-fetch.js +65 -0
- package/node_modules/undici/index.d.ts +3 -0
- package/node_modules/undici/index.js +234 -0
- package/node_modules/undici/lib/api/abort-signal.js +59 -0
- package/node_modules/undici/lib/api/api-connect.js +110 -0
- package/node_modules/undici/lib/api/api-pipeline.js +252 -0
- package/node_modules/undici/lib/api/api-request.js +214 -0
- package/node_modules/undici/lib/api/api-stream.js +209 -0
- package/node_modules/undici/lib/api/api-upgrade.js +111 -0
- package/node_modules/undici/lib/api/index.js +7 -0
- package/node_modules/undici/lib/api/readable.js +580 -0
- package/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
- package/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
- package/node_modules/undici/lib/core/connect.js +153 -0
- package/node_modules/undici/lib/core/constants.js +143 -0
- package/node_modules/undici/lib/core/diagnostics.js +227 -0
- package/node_modules/undici/lib/core/errors.js +477 -0
- package/node_modules/undici/lib/core/request.js +485 -0
- package/node_modules/undici/lib/core/socks5-client.js +422 -0
- package/node_modules/undici/lib/core/socks5-utils.js +212 -0
- package/node_modules/undici/lib/core/symbols.js +75 -0
- package/node_modules/undici/lib/core/tree.js +160 -0
- package/node_modules/undici/lib/core/util.js +992 -0
- package/node_modules/undici/lib/dispatcher/agent.js +158 -0
- package/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
- package/node_modules/undici/lib/dispatcher/client-h1.js +1746 -0
- package/node_modules/undici/lib/dispatcher/client-h2.js +1060 -0
- package/node_modules/undici/lib/dispatcher/client.js +668 -0
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +184 -0
- package/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
- package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
- package/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
- package/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
- package/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
- package/node_modules/undici/lib/dispatcher/pool.js +118 -0
- package/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
- package/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
- package/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
- package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
- package/node_modules/undici/lib/encoding/index.js +33 -0
- package/node_modules/undici/lib/global.js +59 -0
- package/node_modules/undici/lib/handler/cache-handler.js +776 -0
- package/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
- package/node_modules/undici/lib/handler/decorator-handler.js +67 -0
- package/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
- package/node_modules/undici/lib/handler/redirect-handler.js +238 -0
- package/node_modules/undici/lib/handler/retry-handler.js +504 -0
- package/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
- package/node_modules/undici/lib/handler/wrap-handler.js +105 -0
- package/node_modules/undici/lib/interceptor/cache.js +621 -0
- package/node_modules/undici/lib/interceptor/decompress.js +551 -0
- package/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
- package/node_modules/undici/lib/interceptor/dns.js +571 -0
- package/node_modules/undici/lib/interceptor/dump.js +99 -0
- package/node_modules/undici/lib/interceptor/redirect.js +21 -0
- package/node_modules/undici/lib/interceptor/response-error.js +95 -0
- package/node_modules/undici/lib/interceptor/retry.js +19 -0
- package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
- package/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
- package/node_modules/undici/lib/llhttp/constants.js +531 -0
- package/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
- package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
- package/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
- package/node_modules/undici/lib/llhttp/utils.js +12 -0
- package/node_modules/undici/lib/mock/mock-agent.js +232 -0
- package/node_modules/undici/lib/mock/mock-call-history.js +248 -0
- package/node_modules/undici/lib/mock/mock-client.js +68 -0
- package/node_modules/undici/lib/mock/mock-errors.js +29 -0
- package/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
- package/node_modules/undici/lib/mock/mock-pool.js +68 -0
- package/node_modules/undici/lib/mock/mock-symbols.js +32 -0
- package/node_modules/undici/lib/mock/mock-utils.js +486 -0
- package/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
- package/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
- package/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
- package/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
- package/node_modules/undici/lib/util/cache.js +676 -0
- package/node_modules/undici/lib/util/date.js +670 -0
- package/node_modules/undici/lib/util/promise.js +28 -0
- package/node_modules/undici/lib/util/runtime-features.js +124 -0
- package/node_modules/undici/lib/util/stats.js +32 -0
- package/node_modules/undici/lib/util/timers.js +425 -0
- package/node_modules/undici/lib/web/cache/cache.js +864 -0
- package/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
- package/node_modules/undici/lib/web/cache/util.js +45 -0
- package/node_modules/undici/lib/web/cookies/constants.js +12 -0
- package/node_modules/undici/lib/web/cookies/index.js +199 -0
- package/node_modules/undici/lib/web/cookies/parse.js +314 -0
- package/node_modules/undici/lib/web/cookies/util.js +352 -0
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +494 -0
- package/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
- package/node_modules/undici/lib/web/eventsource/util.js +29 -0
- package/node_modules/undici/lib/web/fetch/LICENSE +21 -0
- package/node_modules/undici/lib/web/fetch/body.js +509 -0
- package/node_modules/undici/lib/web/fetch/constants.js +131 -0
- package/node_modules/undici/lib/web/fetch/data-url.js +596 -0
- package/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
- package/node_modules/undici/lib/web/fetch/formdata.js +259 -0
- package/node_modules/undici/lib/web/fetch/global.js +40 -0
- package/node_modules/undici/lib/web/fetch/headers.js +719 -0
- package/node_modules/undici/lib/web/fetch/index.js +2413 -0
- package/node_modules/undici/lib/web/fetch/request.js +1115 -0
- package/node_modules/undici/lib/web/fetch/response.js +641 -0
- package/node_modules/undici/lib/web/fetch/util.js +1522 -0
- package/node_modules/undici/lib/web/infra/index.js +229 -0
- package/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
- package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
- package/node_modules/undici/lib/web/webidl/index.js +1006 -0
- package/node_modules/undici/lib/web/websocket/connection.js +339 -0
- package/node_modules/undici/lib/web/websocket/constants.js +126 -0
- package/node_modules/undici/lib/web/websocket/events.js +331 -0
- package/node_modules/undici/lib/web/websocket/frame.js +133 -0
- package/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
- package/node_modules/undici/lib/web/websocket/receiver.js +507 -0
- package/node_modules/undici/lib/web/websocket/sender.js +109 -0
- package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
- package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
- package/node_modules/undici/lib/web/websocket/util.js +347 -0
- package/node_modules/undici/lib/web/websocket/websocket.js +758 -0
- package/node_modules/undici/package.json +152 -0
- package/node_modules/undici/scripts/strip-comments.js +10 -0
- package/node_modules/undici/types/README.md +6 -0
- package/node_modules/undici/types/agent.d.ts +32 -0
- package/node_modules/undici/types/api.d.ts +43 -0
- package/node_modules/undici/types/balanced-pool.d.ts +30 -0
- package/node_modules/undici/types/cache-interceptor.d.ts +179 -0
- package/node_modules/undici/types/cache.d.ts +36 -0
- package/node_modules/undici/types/client-stats.d.ts +15 -0
- package/node_modules/undici/types/client.d.ts +139 -0
- package/node_modules/undici/types/connector.d.ts +36 -0
- package/node_modules/undici/types/content-type.d.ts +21 -0
- package/node_modules/undici/types/cookies.d.ts +30 -0
- package/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
- package/node_modules/undici/types/dispatcher.d.ts +275 -0
- package/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
- package/node_modules/undici/types/errors.d.ts +177 -0
- package/node_modules/undici/types/eventsource.d.ts +66 -0
- package/node_modules/undici/types/fetch.d.ts +231 -0
- package/node_modules/undici/types/formdata.d.ts +114 -0
- package/node_modules/undici/types/global-dispatcher.d.ts +9 -0
- package/node_modules/undici/types/global-origin.d.ts +7 -0
- package/node_modules/undici/types/h2c-client.d.ts +73 -0
- package/node_modules/undici/types/handlers.d.ts +14 -0
- package/node_modules/undici/types/header.d.ts +160 -0
- package/node_modules/undici/types/index.d.ts +91 -0
- package/node_modules/undici/types/interceptors.d.ts +82 -0
- package/node_modules/undici/types/mock-agent.d.ts +68 -0
- package/node_modules/undici/types/mock-call-history.d.ts +111 -0
- package/node_modules/undici/types/mock-client.d.ts +27 -0
- package/node_modules/undici/types/mock-errors.d.ts +12 -0
- package/node_modules/undici/types/mock-interceptor.d.ts +94 -0
- package/node_modules/undici/types/mock-pool.d.ts +27 -0
- package/node_modules/undici/types/patch.d.ts +29 -0
- package/node_modules/undici/types/pool-stats.d.ts +19 -0
- package/node_modules/undici/types/pool.d.ts +41 -0
- package/node_modules/undici/types/proxy-agent.d.ts +29 -0
- package/node_modules/undici/types/readable.d.ts +68 -0
- package/node_modules/undici/types/retry-agent.d.ts +8 -0
- package/node_modules/undici/types/retry-handler.d.ts +125 -0
- package/node_modules/undici/types/round-robin-pool.d.ts +41 -0
- package/node_modules/undici/types/snapshot-agent.d.ts +109 -0
- package/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
- package/node_modules/undici/types/util.d.ts +18 -0
- package/node_modules/undici/types/utility.d.ts +7 -0
- package/node_modules/undici/types/webidl.d.ts +347 -0
- package/node_modules/undici/types/websocket.d.ts +188 -0
- package/package.json +4 -2
- 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/dist/ui/assets/index-BJqF5rmx.css +0 -1
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -112,6 +112,279 @@ function clientVersionHeaders() {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// src/lib/as-pairing.ts
|
|
116
|
+
import { createHash } from "crypto";
|
|
117
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
118
|
+
import { dirname, join as join2 } from "path";
|
|
119
|
+
function pairingPath(dataDir2) {
|
|
120
|
+
return join2(dataDir2, "as-pairing.json");
|
|
121
|
+
}
|
|
122
|
+
function readPairing(dataDir2) {
|
|
123
|
+
const path = pairingPath(dataDir2);
|
|
124
|
+
if (!existsSync2(path)) return null;
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(readFileSync2(path, "utf-8"));
|
|
127
|
+
if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
|
|
128
|
+
return {
|
|
129
|
+
asUrl: data.asUrl,
|
|
130
|
+
publicKeyHex: data.publicKeyHex,
|
|
131
|
+
pairedAt: data.pairedAt ?? "",
|
|
132
|
+
...typeof data.tlsSpkiPinHex === "string" ? { tlsSpkiPinHex: data.tlsSpkiPinHex } : {}
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function fingerprintOf(publicKeyHex) {
|
|
139
|
+
const raw = Buffer.from(publicKeyHex, "hex");
|
|
140
|
+
const digest = createHash("sha256").update(raw).digest("hex").toUpperCase();
|
|
141
|
+
return digest.match(/.{1,4}/g)?.join(":") ?? digest;
|
|
142
|
+
}
|
|
143
|
+
|
|
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";
|
|
147
|
+
var DEFAULT_AS_URL = "https://www.suveren.ai";
|
|
148
|
+
function configPath(dataDir2) {
|
|
149
|
+
return join3(dataDir2, "config.json");
|
|
150
|
+
}
|
|
151
|
+
function readAsConfigStrict(dataDir2) {
|
|
152
|
+
const path = configPath(dataDir2);
|
|
153
|
+
if (!existsSync3(path)) return {};
|
|
154
|
+
let data;
|
|
155
|
+
try {
|
|
156
|
+
data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const out = {};
|
|
163
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
164
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
165
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
166
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function readAsConfig(dataDir2) {
|
|
170
|
+
const path = configPath(dataDir2);
|
|
171
|
+
if (!existsSync3(path)) return {};
|
|
172
|
+
try {
|
|
173
|
+
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
174
|
+
const out = {};
|
|
175
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
176
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
177
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
178
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
179
|
+
return out;
|
|
180
|
+
} catch {
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function validateAsUrl(candidate) {
|
|
185
|
+
const trimmed = candidate.trim();
|
|
186
|
+
if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = new URL(trimmed);
|
|
190
|
+
} catch {
|
|
191
|
+
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
192
|
+
}
|
|
193
|
+
const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
194
|
+
if (parsed.protocol !== "https:" && !isLocalHttp) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
201
|
+
if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
|
|
202
|
+
normalized = normalized.slice(0, -1);
|
|
203
|
+
} else if (normalized === `${parsed.origin}/`) {
|
|
204
|
+
normalized = parsed.origin;
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, url: normalized };
|
|
207
|
+
}
|
|
208
|
+
function resolveAsUrl(dataDir2, flag) {
|
|
209
|
+
if (flag) {
|
|
210
|
+
const v = validateAsUrl(flag);
|
|
211
|
+
if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
|
|
212
|
+
return v.url;
|
|
213
|
+
}
|
|
214
|
+
const envUrl = process.env.SUVEREN_AS_URL;
|
|
215
|
+
if (envUrl) {
|
|
216
|
+
const v = validateAsUrl(envUrl);
|
|
217
|
+
if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
|
|
218
|
+
return v.url;
|
|
219
|
+
}
|
|
220
|
+
const saved = readAsConfigStrict(dataDir2).asUrl;
|
|
221
|
+
if (saved) {
|
|
222
|
+
const v = validateAsUrl(saved);
|
|
223
|
+
if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
|
|
224
|
+
return v.url;
|
|
225
|
+
}
|
|
226
|
+
return DEFAULT_AS_URL;
|
|
227
|
+
}
|
|
228
|
+
function resolvePinTls(dataDir2) {
|
|
229
|
+
return readAsConfig(dataDir2).pinTls === true;
|
|
230
|
+
}
|
|
231
|
+
function validatePinTlsForUrl(pinTls, asUrl) {
|
|
232
|
+
if (!pinTls) return;
|
|
233
|
+
if (!asUrl.startsWith("https:")) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`config.json sets "pinTls": true, but the Authority Server URL (${asUrl}) is not https:// \u2014 there is no TLS certificate to pin. Fix the saved as-url, or turn pin-tls off: \`suveren-gateway config set pin-tls off\`.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/lib/as-tls-pin.ts
|
|
241
|
+
import { createHash as createHash2 } from "crypto";
|
|
242
|
+
import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
|
|
243
|
+
import { Agent as HttpsAgent } from "https";
|
|
244
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
245
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
246
|
+
function effectiveCa() {
|
|
247
|
+
const path = process.env.NODE_EXTRA_CA_CERTS;
|
|
248
|
+
if (!path) return void 0;
|
|
249
|
+
try {
|
|
250
|
+
return [...rootCertificates, readFileSync4(path, "utf-8")];
|
|
251
|
+
} catch {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
var PIN_CHECK_ERROR_CODE = "EAS_TLS_PIN_CHECK";
|
|
256
|
+
function taggedCheckError(err) {
|
|
257
|
+
err.code = PIN_CHECK_ERROR_CODE;
|
|
258
|
+
return err;
|
|
259
|
+
}
|
|
260
|
+
function isPinCheckError(err) {
|
|
261
|
+
let cur = err;
|
|
262
|
+
for (let i = 0; i < 5 && cur; i++) {
|
|
263
|
+
if (cur instanceof Error && cur.code === PIN_CHECK_ERROR_CODE) return true;
|
|
264
|
+
cur = cur instanceof Error ? cur.cause : void 0;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
var AsTlsMismatchError = class extends Error {
|
|
269
|
+
constructor(message) {
|
|
270
|
+
super(message);
|
|
271
|
+
this.name = "AsTlsMismatchError";
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
function spkiSha256Hex(cert) {
|
|
275
|
+
if (!cert.pubkey) {
|
|
276
|
+
throw new Error("peer certificate carries no pubkey (expected the detailed certificate object)");
|
|
277
|
+
}
|
|
278
|
+
return createHash2("sha256").update(cert.pubkey).digest("hex");
|
|
279
|
+
}
|
|
280
|
+
function formatPinFingerprint(hex) {
|
|
281
|
+
const upper = hex.toUpperCase();
|
|
282
|
+
return upper.match(/.{1,4}/g)?.join(":") ?? upper;
|
|
283
|
+
}
|
|
284
|
+
function pinnedCheckServerIdentity(pinnedSpkiHex) {
|
|
285
|
+
const wantHex = pinnedSpkiHex.toLowerCase();
|
|
286
|
+
return (hostname, cert) => {
|
|
287
|
+
const defaultErr = defaultCheckServerIdentity(hostname, cert);
|
|
288
|
+
if (defaultErr) return taggedCheckError(defaultErr);
|
|
289
|
+
let actualHex;
|
|
290
|
+
try {
|
|
291
|
+
actualHex = spkiSha256Hex(cert);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
return taggedCheckError(new Error(
|
|
294
|
+
`could not read the server certificate's public key to check the TLS pin \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
295
|
+
));
|
|
296
|
+
}
|
|
297
|
+
if (actualHex !== wantHex) {
|
|
298
|
+
return taggedCheckError(new Error(
|
|
299
|
+
`TLS certificate pin mismatch for ${hostname}: expected SPKI sha256 ${formatPinFingerprint(wantHex)}, got ${formatPinFingerprint(actualHex)}.`
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
return void 0;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function buildPinnedDispatcher(pinnedSpkiHex) {
|
|
306
|
+
return new Agent({
|
|
307
|
+
connect: {
|
|
308
|
+
ca: effectiveCa(),
|
|
309
|
+
checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function buildCapturingDispatcher() {
|
|
314
|
+
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
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return { dispatcher, getCapturedCert: () => captured };
|
|
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
|
+
}
|
|
340
|
+
async function fetchAs(url, init, pinning) {
|
|
341
|
+
if (!pinning.enforce && !pinning.capture) {
|
|
342
|
+
const res2 = await fetch(url, withBoundedSignal(init));
|
|
343
|
+
return { res: res2 };
|
|
344
|
+
}
|
|
345
|
+
if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
|
|
346
|
+
throw new AsTlsMismatchError(
|
|
347
|
+
`pin-tls is on but no certificate is pinned yet for ${url} \u2014 sign in again to establish it (re-pairing required).`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
|
|
351
|
+
const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
|
|
352
|
+
const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
|
|
353
|
+
closeAfter(dispatcher2, pending2);
|
|
354
|
+
try {
|
|
355
|
+
const res2 = await pending2;
|
|
356
|
+
return { res: res2 };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (!isPinCheckError(err)) throw err;
|
|
359
|
+
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.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
|
|
365
|
+
const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
|
|
366
|
+
closeAfter(dispatcher, pending);
|
|
367
|
+
let res;
|
|
368
|
+
try {
|
|
369
|
+
res = await pending;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (!isPinCheckError(err)) throw err;
|
|
372
|
+
throw new AsTlsMismatchError(
|
|
373
|
+
`could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const cert = getCapturedCert();
|
|
377
|
+
return { res, capturedSpkiHex: cert ? spkiSha256Hex(cert) : void 0 };
|
|
378
|
+
}
|
|
379
|
+
function describeFetchError(err) {
|
|
380
|
+
if (err instanceof Error) {
|
|
381
|
+
const cause = err.cause;
|
|
382
|
+
if (cause instanceof Error) return cause.message;
|
|
383
|
+
return err.message;
|
|
384
|
+
}
|
|
385
|
+
return String(err);
|
|
386
|
+
}
|
|
387
|
+
|
|
115
388
|
// src/lib/sp-client.ts
|
|
116
389
|
var SPReceiptError = class extends Error {
|
|
117
390
|
constructor(message, statusCode, body) {
|
|
@@ -127,8 +400,9 @@ var DEFAULT_RECEIPT_RETRY = {
|
|
|
127
400
|
};
|
|
128
401
|
var sleep = (ms) => ms > 0 ? new Promise((resolve3) => setTimeout(resolve3, ms)) : Promise.resolve();
|
|
129
402
|
var SPClient = class {
|
|
130
|
-
constructor(baseUrl, receiptRetry = {}) {
|
|
403
|
+
constructor(baseUrl, receiptRetry = {}, dataDir2) {
|
|
131
404
|
this.baseUrl = baseUrl;
|
|
405
|
+
this.dataDir = dataDir2;
|
|
132
406
|
this.receiptRetry = { ...DEFAULT_RECEIPT_RETRY, ...receiptRetry };
|
|
133
407
|
}
|
|
134
408
|
sessionCookie = "";
|
|
@@ -141,6 +415,22 @@ var SPClient = class {
|
|
|
141
415
|
* was active tells the control plane. Concurrent in-flight calls that were
|
|
142
416
|
* all sent under the same now-dead cookie would otherwise all report it. */
|
|
143
417
|
sessionExpiredNotified = false;
|
|
418
|
+
/** Same single-flight shape, for a TLS pin mismatch (as-tls-pin.ts). */
|
|
419
|
+
tlsMismatchNotified = false;
|
|
420
|
+
/** Opt-in TLS pinning (as-tls-pin.ts) for every call THIS client makes.
|
|
421
|
+
* Re-read live on each call rather than cached — same reasoning as
|
|
422
|
+
* AttestationCache.getPublicKey's pin check: a pin or setting change
|
|
423
|
+
* must take effect on the very next call, not the next restart. Never
|
|
424
|
+
* allowed to CAPTURE a new pin here — only the control plane's sign-in
|
|
425
|
+
* exchange (as-challenge.ts) does that; this client only enforces. */
|
|
426
|
+
tlsPinning() {
|
|
427
|
+
if (!this.dataDir) return { enforce: false };
|
|
428
|
+
const pin = readPairing(this.dataDir);
|
|
429
|
+
return {
|
|
430
|
+
enforce: resolvePinTls(this.dataDir),
|
|
431
|
+
pinnedSpkiHex: pin && pin.asUrl === this.baseUrl ? pin.tlsSpkiPinHex : void 0
|
|
432
|
+
};
|
|
433
|
+
}
|
|
144
434
|
/** The AS base URL this client talks to (for provenance records). */
|
|
145
435
|
get url() {
|
|
146
436
|
return this.baseUrl;
|
|
@@ -152,6 +442,7 @@ var SPClient = class {
|
|
|
152
442
|
this.sessionCookie = cookie;
|
|
153
443
|
this.lockReason = null;
|
|
154
444
|
this.sessionExpiredNotified = false;
|
|
445
|
+
this.tlsMismatchNotified = false;
|
|
155
446
|
}
|
|
156
447
|
/**
|
|
157
448
|
* Drop the session because the control plane learned (by whatever path —
|
|
@@ -205,10 +496,21 @@ var SPClient = class {
|
|
|
205
496
|
headers["X-API-Key"] = this.apiKey;
|
|
206
497
|
}
|
|
207
498
|
const hadSession = this.sessionCookie !== "";
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
headers
|
|
211
|
-
|
|
499
|
+
let res;
|
|
500
|
+
try {
|
|
501
|
+
const result = await fetchAs(url, { ...init, headers }, this.tlsPinning());
|
|
502
|
+
res = result.res;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (err instanceof AsTlsMismatchError) {
|
|
505
|
+
this.sessionCookie = "";
|
|
506
|
+
this.lockReason = "as-tls-mismatch";
|
|
507
|
+
if (!this.tlsMismatchNotified) {
|
|
508
|
+
this.tlsMismatchNotified = true;
|
|
509
|
+
void notifyControlPlane("as-tls-mismatch");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
212
514
|
if (res.status === 401 && hadSession) {
|
|
213
515
|
this.sessionCookie = "";
|
|
214
516
|
this.lockReason = "expired";
|
|
@@ -371,32 +673,6 @@ var SPClient = class {
|
|
|
371
673
|
|
|
372
674
|
// src/lib/attestation-cache.ts
|
|
373
675
|
import { decodeAttestationBlob } from "@hap/core";
|
|
374
|
-
|
|
375
|
-
// src/lib/as-pairing.ts
|
|
376
|
-
import { createHash } from "crypto";
|
|
377
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
378
|
-
import { dirname, join as join2 } from "path";
|
|
379
|
-
function pairingPath(dataDir2) {
|
|
380
|
-
return join2(dataDir2, "as-pairing.json");
|
|
381
|
-
}
|
|
382
|
-
function readPairing(dataDir2) {
|
|
383
|
-
const path = pairingPath(dataDir2);
|
|
384
|
-
if (!existsSync2(path)) return null;
|
|
385
|
-
try {
|
|
386
|
-
const data = JSON.parse(readFileSync2(path, "utf-8"));
|
|
387
|
-
if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
|
|
388
|
-
return { asUrl: data.asUrl, publicKeyHex: data.publicKeyHex, pairedAt: data.pairedAt ?? "" };
|
|
389
|
-
} catch {
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
function fingerprintOf(publicKeyHex) {
|
|
394
|
-
const raw = Buffer.from(publicKeyHex, "hex");
|
|
395
|
-
const digest = createHash("sha256").update(raw).digest("hex").toUpperCase();
|
|
396
|
-
return digest.match(/.{1,4}/g)?.join(":") ?? digest;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/lib/attestation-cache.ts
|
|
400
676
|
var AsKeyMismatchError = class extends Error {
|
|
401
677
|
constructor(message) {
|
|
402
678
|
super(message);
|
|
@@ -558,18 +834,18 @@ var AttestationCache = class {
|
|
|
558
834
|
};
|
|
559
835
|
|
|
560
836
|
// src/lib/gate-store.ts
|
|
561
|
-
import { mkdirSync as
|
|
837
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
562
838
|
import { homedir } from "os";
|
|
563
|
-
import { dirname as
|
|
839
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
564
840
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
565
|
-
var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
841
|
+
var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir(), ".suveren");
|
|
566
842
|
var GateStore = class {
|
|
567
843
|
entries = /* @__PURE__ */ new Map();
|
|
568
844
|
baseDir;
|
|
569
845
|
vaultKey = null;
|
|
570
846
|
constructor(filePath) {
|
|
571
847
|
if (filePath && filePath.endsWith(".json")) {
|
|
572
|
-
this.baseDir =
|
|
848
|
+
this.baseDir = dirname3(filePath);
|
|
573
849
|
} else {
|
|
574
850
|
this.baseDir = filePath ?? DEFAULT_DIR;
|
|
575
851
|
}
|
|
@@ -580,11 +856,11 @@ var GateStore = class {
|
|
|
580
856
|
*/
|
|
581
857
|
setVaultKey(key) {
|
|
582
858
|
this.vaultKey = key;
|
|
583
|
-
if (
|
|
859
|
+
if (existsSync4(this.encryptedFilePath)) {
|
|
584
860
|
this.loadEncrypted();
|
|
585
861
|
} else if (this.entries.size > 0) {
|
|
586
862
|
this.persistEncrypted();
|
|
587
|
-
if (
|
|
863
|
+
if (existsSync4(this.plaintextFilePath)) {
|
|
588
864
|
try {
|
|
589
865
|
unlinkSync2(this.plaintextFilePath);
|
|
590
866
|
} catch {
|
|
@@ -622,11 +898,11 @@ var GateStore = class {
|
|
|
622
898
|
clearAll() {
|
|
623
899
|
this.entries = /* @__PURE__ */ new Map();
|
|
624
900
|
try {
|
|
625
|
-
if (
|
|
901
|
+
if (existsSync4(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
|
|
626
902
|
} catch {
|
|
627
903
|
}
|
|
628
904
|
try {
|
|
629
|
-
if (
|
|
905
|
+
if (existsSync4(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
|
|
630
906
|
} catch {
|
|
631
907
|
}
|
|
632
908
|
}
|
|
@@ -659,19 +935,19 @@ var GateStore = class {
|
|
|
659
935
|
}
|
|
660
936
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
661
937
|
get plaintextFilePath() {
|
|
662
|
-
return
|
|
938
|
+
return join4(this.baseDir, "gates.json");
|
|
663
939
|
}
|
|
664
940
|
get encryptedFilePath() {
|
|
665
|
-
return
|
|
941
|
+
return join4(this.baseDir, "gates.enc.json");
|
|
666
942
|
}
|
|
667
943
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
668
944
|
loadPlaintext() {
|
|
669
|
-
if (!
|
|
670
|
-
|
|
945
|
+
if (!existsSync4(this.plaintextFilePath)) {
|
|
946
|
+
mkdirSync3(this.baseDir, { recursive: true });
|
|
671
947
|
return;
|
|
672
948
|
}
|
|
673
949
|
try {
|
|
674
|
-
const raw =
|
|
950
|
+
const raw = readFileSync5(this.plaintextFilePath, "utf-8");
|
|
675
951
|
const data = JSON.parse(raw);
|
|
676
952
|
this.entries = new Map(Object.entries(data.entries));
|
|
677
953
|
} catch {
|
|
@@ -680,9 +956,9 @@ var GateStore = class {
|
|
|
680
956
|
}
|
|
681
957
|
}
|
|
682
958
|
loadEncrypted() {
|
|
683
|
-
if (!
|
|
959
|
+
if (!existsSync4(this.encryptedFilePath)) return;
|
|
684
960
|
try {
|
|
685
|
-
const raw =
|
|
961
|
+
const raw = readFileSync5(this.encryptedFilePath, "utf-8");
|
|
686
962
|
const data = JSON.parse(raw);
|
|
687
963
|
this.entries = /* @__PURE__ */ new Map();
|
|
688
964
|
for (const [key, blob] of Object.entries(data.entries)) {
|
|
@@ -706,8 +982,8 @@ var GateStore = class {
|
|
|
706
982
|
version: 1,
|
|
707
983
|
entries: Object.fromEntries(this.entries)
|
|
708
984
|
};
|
|
709
|
-
|
|
710
|
-
|
|
985
|
+
mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
|
|
986
|
+
writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
711
987
|
}
|
|
712
988
|
persistEncrypted() {
|
|
713
989
|
const data = {
|
|
@@ -717,17 +993,17 @@ var GateStore = class {
|
|
|
717
993
|
for (const [key, entry] of this.entries) {
|
|
718
994
|
data.entries[key] = this.encrypt(JSON.stringify(entry));
|
|
719
995
|
}
|
|
720
|
-
|
|
721
|
-
|
|
996
|
+
mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
|
|
997
|
+
writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
722
998
|
}
|
|
723
999
|
};
|
|
724
1000
|
|
|
725
1001
|
// src/lib/execution-log.ts
|
|
726
|
-
import { mkdirSync as
|
|
1002
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
727
1003
|
import { homedir as homedir2 } from "os";
|
|
728
|
-
import { dirname as
|
|
1004
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
729
1005
|
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
|
|
730
|
-
var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
1006
|
+
var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
|
|
731
1007
|
var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
|
|
732
1008
|
var ExecutionLog = class {
|
|
733
1009
|
entries = [];
|
|
@@ -735,7 +1011,7 @@ var ExecutionLog = class {
|
|
|
735
1011
|
vaultKey = null;
|
|
736
1012
|
constructor(filePath) {
|
|
737
1013
|
if (filePath && filePath.endsWith(".json")) {
|
|
738
|
-
this.baseDir =
|
|
1014
|
+
this.baseDir = dirname4(filePath);
|
|
739
1015
|
} else {
|
|
740
1016
|
this.baseDir = filePath ?? DEFAULT_DIR2;
|
|
741
1017
|
}
|
|
@@ -746,11 +1022,11 @@ var ExecutionLog = class {
|
|
|
746
1022
|
*/
|
|
747
1023
|
setVaultKey(key) {
|
|
748
1024
|
this.vaultKey = key;
|
|
749
|
-
if (
|
|
1025
|
+
if (existsSync5(this.encryptedFilePath)) {
|
|
750
1026
|
this.loadEncrypted();
|
|
751
1027
|
} else if (this.entries.length > 0) {
|
|
752
1028
|
this.persistEncrypted();
|
|
753
|
-
if (
|
|
1029
|
+
if (existsSync5(this.plaintextFilePath)) {
|
|
754
1030
|
try {
|
|
755
1031
|
unlinkSync3(this.plaintextFilePath);
|
|
756
1032
|
} catch {
|
|
@@ -854,19 +1130,19 @@ var ExecutionLog = class {
|
|
|
854
1130
|
}
|
|
855
1131
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
856
1132
|
get plaintextFilePath() {
|
|
857
|
-
return
|
|
1133
|
+
return join5(this.baseDir, "execution-log.json");
|
|
858
1134
|
}
|
|
859
1135
|
get encryptedFilePath() {
|
|
860
|
-
return
|
|
1136
|
+
return join5(this.baseDir, "execution-log.enc.json");
|
|
861
1137
|
}
|
|
862
1138
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
863
1139
|
loadPlaintext() {
|
|
864
|
-
if (!
|
|
865
|
-
|
|
1140
|
+
if (!existsSync5(this.plaintextFilePath)) {
|
|
1141
|
+
mkdirSync4(this.baseDir, { recursive: true });
|
|
866
1142
|
return;
|
|
867
1143
|
}
|
|
868
1144
|
try {
|
|
869
|
-
const raw =
|
|
1145
|
+
const raw = readFileSync6(this.plaintextFilePath, "utf-8");
|
|
870
1146
|
const data = JSON.parse(raw);
|
|
871
1147
|
this.entries = data.entries;
|
|
872
1148
|
this.prune();
|
|
@@ -876,9 +1152,9 @@ var ExecutionLog = class {
|
|
|
876
1152
|
}
|
|
877
1153
|
}
|
|
878
1154
|
loadEncrypted() {
|
|
879
|
-
if (!
|
|
1155
|
+
if (!existsSync5(this.encryptedFilePath)) return;
|
|
880
1156
|
try {
|
|
881
|
-
const raw =
|
|
1157
|
+
const raw = readFileSync6(this.encryptedFilePath, "utf-8");
|
|
882
1158
|
const data = JSON.parse(raw);
|
|
883
1159
|
const decrypted = this.decrypt(data.blob);
|
|
884
1160
|
const logFile = JSON.parse(decrypted);
|
|
@@ -898,8 +1174,8 @@ var ExecutionLog = class {
|
|
|
898
1174
|
}
|
|
899
1175
|
persistPlaintext() {
|
|
900
1176
|
const data = { version: 1, entries: this.entries };
|
|
901
|
-
|
|
902
|
-
|
|
1177
|
+
mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
|
|
1178
|
+
writeFileSync4(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
903
1179
|
}
|
|
904
1180
|
persistEncrypted() {
|
|
905
1181
|
const logFile = { version: 1, entries: this.entries };
|
|
@@ -907,8 +1183,8 @@ var ExecutionLog = class {
|
|
|
907
1183
|
version: 1,
|
|
908
1184
|
blob: this.encrypt(JSON.stringify(logFile))
|
|
909
1185
|
};
|
|
910
|
-
|
|
911
|
-
|
|
1186
|
+
mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
|
|
1187
|
+
writeFileSync4(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
912
1188
|
}
|
|
913
1189
|
};
|
|
914
1190
|
function windowCutoff(window, now) {
|
|
@@ -925,11 +1201,11 @@ function windowCutoff(window, now) {
|
|
|
925
1201
|
}
|
|
926
1202
|
|
|
927
1203
|
// src/lib/denial-log.ts
|
|
928
|
-
import { mkdirSync as
|
|
1204
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
929
1205
|
import { homedir as homedir3 } from "os";
|
|
930
|
-
import { dirname as
|
|
1206
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
931
1207
|
import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
|
|
932
|
-
var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ??
|
|
1208
|
+
var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
|
|
933
1209
|
var MAX_RECORDS = 200;
|
|
934
1210
|
var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
935
1211
|
var DenialLog = class {
|
|
@@ -937,16 +1213,16 @@ var DenialLog = class {
|
|
|
937
1213
|
baseDir;
|
|
938
1214
|
vaultKey = null;
|
|
939
1215
|
constructor(filePath) {
|
|
940
|
-
this.baseDir = filePath && filePath.endsWith(".json") ?
|
|
1216
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname5(filePath) : filePath ?? DEFAULT_DIR3;
|
|
941
1217
|
this.loadPlaintext();
|
|
942
1218
|
}
|
|
943
1219
|
setVaultKey(key) {
|
|
944
1220
|
this.vaultKey = key;
|
|
945
|
-
if (
|
|
1221
|
+
if (existsSync6(this.encryptedFilePath)) {
|
|
946
1222
|
this.loadEncrypted();
|
|
947
1223
|
} else if (this.records.length > 0) {
|
|
948
1224
|
this.persistEncrypted();
|
|
949
|
-
if (
|
|
1225
|
+
if (existsSync6(this.plaintextFilePath)) {
|
|
950
1226
|
try {
|
|
951
1227
|
unlinkSync4(this.plaintextFilePath);
|
|
952
1228
|
} catch {
|
|
@@ -996,18 +1272,18 @@ var DenialLog = class {
|
|
|
996
1272
|
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
997
1273
|
}
|
|
998
1274
|
get plaintextFilePath() {
|
|
999
|
-
return
|
|
1275
|
+
return join6(this.baseDir, "denials.json");
|
|
1000
1276
|
}
|
|
1001
1277
|
get encryptedFilePath() {
|
|
1002
|
-
return
|
|
1278
|
+
return join6(this.baseDir, "denials.enc.json");
|
|
1003
1279
|
}
|
|
1004
1280
|
loadPlaintext() {
|
|
1005
|
-
if (!
|
|
1006
|
-
|
|
1281
|
+
if (!existsSync6(this.plaintextFilePath)) {
|
|
1282
|
+
mkdirSync5(this.baseDir, { recursive: true });
|
|
1007
1283
|
return;
|
|
1008
1284
|
}
|
|
1009
1285
|
try {
|
|
1010
|
-
const data = JSON.parse(
|
|
1286
|
+
const data = JSON.parse(readFileSync7(this.plaintextFilePath, "utf-8"));
|
|
1011
1287
|
this.records = data.records ?? [];
|
|
1012
1288
|
} catch {
|
|
1013
1289
|
console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
|
|
@@ -1015,9 +1291,9 @@ var DenialLog = class {
|
|
|
1015
1291
|
}
|
|
1016
1292
|
}
|
|
1017
1293
|
loadEncrypted() {
|
|
1018
|
-
if (!
|
|
1294
|
+
if (!existsSync6(this.encryptedFilePath)) return;
|
|
1019
1295
|
try {
|
|
1020
|
-
const data = JSON.parse(
|
|
1296
|
+
const data = JSON.parse(readFileSync7(this.encryptedFilePath, "utf-8"));
|
|
1021
1297
|
const logFile = JSON.parse(this.decrypt(data.blob));
|
|
1022
1298
|
this.records = logFile.records ?? [];
|
|
1023
1299
|
} catch (err) {
|
|
@@ -1031,23 +1307,23 @@ var DenialLog = class {
|
|
|
1031
1307
|
}
|
|
1032
1308
|
persistPlaintext() {
|
|
1033
1309
|
const data = { version: 1, records: this.records };
|
|
1034
|
-
|
|
1035
|
-
|
|
1310
|
+
mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
|
|
1311
|
+
writeFileSync5(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1036
1312
|
}
|
|
1037
1313
|
persistEncrypted() {
|
|
1038
1314
|
const logFile = { version: 1, records: this.records };
|
|
1039
1315
|
const data = { version: 1, blob: this.encrypt(JSON.stringify(logFile)) };
|
|
1040
|
-
|
|
1041
|
-
|
|
1316
|
+
mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
|
|
1317
|
+
writeFileSync5(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1042
1318
|
}
|
|
1043
1319
|
};
|
|
1044
1320
|
|
|
1045
1321
|
// src/lib/receipt-archive.ts
|
|
1046
|
-
import { mkdirSync as
|
|
1322
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync5 } from "fs";
|
|
1047
1323
|
import { homedir as homedir4 } from "os";
|
|
1048
|
-
import { dirname as
|
|
1324
|
+
import { dirname as dirname6, join as join7 } from "path";
|
|
1049
1325
|
import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
|
|
1050
|
-
var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ??
|
|
1326
|
+
var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
|
|
1051
1327
|
function stripPreviews(proposal) {
|
|
1052
1328
|
const args = proposal.toolArgs;
|
|
1053
1329
|
if (!args || typeof args !== "object") return proposal;
|
|
@@ -1061,7 +1337,7 @@ var ReceiptArchive = class {
|
|
|
1061
1337
|
vaultKey = null;
|
|
1062
1338
|
constructor(filePath) {
|
|
1063
1339
|
if (filePath && filePath.endsWith(".json")) {
|
|
1064
|
-
this.baseDir =
|
|
1340
|
+
this.baseDir = dirname6(filePath);
|
|
1065
1341
|
} else {
|
|
1066
1342
|
this.baseDir = filePath ?? DEFAULT_DIR4;
|
|
1067
1343
|
}
|
|
@@ -1072,11 +1348,11 @@ var ReceiptArchive = class {
|
|
|
1072
1348
|
*/
|
|
1073
1349
|
setVaultKey(key) {
|
|
1074
1350
|
this.vaultKey = key;
|
|
1075
|
-
if (
|
|
1351
|
+
if (existsSync7(this.encryptedFilePath)) {
|
|
1076
1352
|
this.loadEncrypted();
|
|
1077
1353
|
} else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
|
|
1078
1354
|
this.persistEncrypted();
|
|
1079
|
-
if (
|
|
1355
|
+
if (existsSync7(this.plaintextFilePath)) {
|
|
1080
1356
|
try {
|
|
1081
1357
|
unlinkSync5(this.plaintextFilePath);
|
|
1082
1358
|
} catch {
|
|
@@ -1090,7 +1366,7 @@ var ReceiptArchive = class {
|
|
|
1090
1366
|
* this state — that would report a hollow success over unreadable evidence.
|
|
1091
1367
|
*/
|
|
1092
1368
|
isLocked() {
|
|
1093
|
-
return this.vaultKey === null &&
|
|
1369
|
+
return this.vaultKey === null && existsSync7(this.encryptedFilePath);
|
|
1094
1370
|
}
|
|
1095
1371
|
/**
|
|
1096
1372
|
* Archive a signed receipt (and the grant it was issued under).
|
|
@@ -1191,19 +1467,19 @@ var ReceiptArchive = class {
|
|
|
1191
1467
|
}
|
|
1192
1468
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
1193
1469
|
get plaintextFilePath() {
|
|
1194
|
-
return
|
|
1470
|
+
return join7(this.baseDir, "receipt-archive.json");
|
|
1195
1471
|
}
|
|
1196
1472
|
get encryptedFilePath() {
|
|
1197
|
-
return
|
|
1473
|
+
return join7(this.baseDir, "receipt-archive.enc.json");
|
|
1198
1474
|
}
|
|
1199
1475
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
1200
1476
|
loadPlaintext() {
|
|
1201
|
-
if (!
|
|
1202
|
-
|
|
1477
|
+
if (!existsSync7(this.plaintextFilePath)) {
|
|
1478
|
+
mkdirSync6(this.baseDir, { recursive: true });
|
|
1203
1479
|
return;
|
|
1204
1480
|
}
|
|
1205
1481
|
try {
|
|
1206
|
-
const raw =
|
|
1482
|
+
const raw = readFileSync8(this.plaintextFilePath, "utf-8");
|
|
1207
1483
|
const parsed = JSON.parse(raw);
|
|
1208
1484
|
this.data = {
|
|
1209
1485
|
version: 1,
|
|
@@ -1213,18 +1489,18 @@ var ReceiptArchive = class {
|
|
|
1213
1489
|
} catch {
|
|
1214
1490
|
console.error(`[ReceiptArchive] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
1215
1491
|
try {
|
|
1216
|
-
|
|
1492
|
+
writeFileSync6(
|
|
1217
1493
|
`${this.plaintextFilePath}.corrupt-${Date.now()}`,
|
|
1218
|
-
|
|
1494
|
+
readFileSync8(this.plaintextFilePath)
|
|
1219
1495
|
);
|
|
1220
1496
|
} catch {
|
|
1221
1497
|
}
|
|
1222
1498
|
}
|
|
1223
1499
|
}
|
|
1224
1500
|
loadEncrypted() {
|
|
1225
|
-
if (!
|
|
1501
|
+
if (!existsSync7(this.encryptedFilePath)) return;
|
|
1226
1502
|
try {
|
|
1227
|
-
const raw =
|
|
1503
|
+
const raw = readFileSync8(this.encryptedFilePath, "utf-8");
|
|
1228
1504
|
const data = JSON.parse(raw);
|
|
1229
1505
|
const decrypted = this.decrypt(data.blob);
|
|
1230
1506
|
const parsed = JSON.parse(decrypted);
|
|
@@ -1236,9 +1512,9 @@ var ReceiptArchive = class {
|
|
|
1236
1512
|
} catch (err) {
|
|
1237
1513
|
console.error(`[ReceiptArchive] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
1238
1514
|
try {
|
|
1239
|
-
|
|
1515
|
+
writeFileSync6(
|
|
1240
1516
|
`${this.encryptedFilePath}.corrupt-${Date.now()}`,
|
|
1241
|
-
|
|
1517
|
+
readFileSync8(this.encryptedFilePath)
|
|
1242
1518
|
);
|
|
1243
1519
|
} catch {
|
|
1244
1520
|
}
|
|
@@ -1252,29 +1528,29 @@ var ReceiptArchive = class {
|
|
|
1252
1528
|
}
|
|
1253
1529
|
}
|
|
1254
1530
|
persistPlaintext() {
|
|
1255
|
-
|
|
1256
|
-
|
|
1531
|
+
mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
|
|
1532
|
+
writeFileSync6(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1257
1533
|
}
|
|
1258
1534
|
persistEncrypted() {
|
|
1259
1535
|
const data = {
|
|
1260
1536
|
version: 1,
|
|
1261
1537
|
blob: this.encrypt(JSON.stringify(this.data))
|
|
1262
1538
|
};
|
|
1263
|
-
|
|
1264
|
-
|
|
1539
|
+
mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
|
|
1540
|
+
writeFileSync6(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1265
1541
|
}
|
|
1266
1542
|
};
|
|
1267
1543
|
|
|
1268
1544
|
// src/lib/execution-journal.ts
|
|
1269
|
-
import { mkdirSync as
|
|
1545
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync } from "fs";
|
|
1270
1546
|
import { homedir as homedir5 } from "os";
|
|
1271
|
-
import { dirname as
|
|
1272
|
-
import { createHash as
|
|
1273
|
-
var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ??
|
|
1547
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
1548
|
+
import { createHash as createHash3 } from "crypto";
|
|
1549
|
+
var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
|
|
1274
1550
|
var FILE_NAME = "execution-journal.json";
|
|
1275
1551
|
var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
|
|
1276
1552
|
function hashToolArgs(args) {
|
|
1277
|
-
return "sha256:" +
|
|
1553
|
+
return "sha256:" + createHash3("sha256").update(stableStringify(args)).digest("hex");
|
|
1278
1554
|
}
|
|
1279
1555
|
function stableStringify(value) {
|
|
1280
1556
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
@@ -1286,8 +1562,8 @@ function stableStringify(value) {
|
|
|
1286
1562
|
var ExecutionJournal = class {
|
|
1287
1563
|
filePath;
|
|
1288
1564
|
constructor(filePath) {
|
|
1289
|
-
const baseDir = filePath && filePath.endsWith(".json") ?
|
|
1290
|
-
this.filePath =
|
|
1565
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR5;
|
|
1566
|
+
this.filePath = join8(baseDir, FILE_NAME);
|
|
1291
1567
|
}
|
|
1292
1568
|
/** Look a ticket up. Always reads from disk — another process may have written. */
|
|
1293
1569
|
get(ticketId) {
|
|
@@ -1326,9 +1602,9 @@ var ExecutionJournal = class {
|
|
|
1326
1602
|
}
|
|
1327
1603
|
// ─── Persistence ────────────────────────────────────────────────────────
|
|
1328
1604
|
load() {
|
|
1329
|
-
if (!
|
|
1605
|
+
if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
|
|
1330
1606
|
try {
|
|
1331
|
-
const parsed = JSON.parse(
|
|
1607
|
+
const parsed = JSON.parse(readFileSync9(this.filePath, "utf8"));
|
|
1332
1608
|
if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
1333
1609
|
throw new Error("unexpected shape");
|
|
1334
1610
|
}
|
|
@@ -1342,9 +1618,9 @@ var ExecutionJournal = class {
|
|
|
1342
1618
|
persist(file) {
|
|
1343
1619
|
const cutoff = nowSec() - MAX_AGE_SECONDS2;
|
|
1344
1620
|
file.entries = file.entries.filter((e) => e.startedAt >= cutoff);
|
|
1345
|
-
|
|
1621
|
+
mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
|
|
1346
1622
|
const tmp = this.filePath + ".tmp";
|
|
1347
|
-
|
|
1623
|
+
writeFileSync7(tmp, JSON.stringify(file, null, 2), { mode: 384 });
|
|
1348
1624
|
renameSync(tmp, this.filePath);
|
|
1349
1625
|
}
|
|
1350
1626
|
};
|
|
@@ -1353,10 +1629,10 @@ function nowSec() {
|
|
|
1353
1629
|
}
|
|
1354
1630
|
|
|
1355
1631
|
// src/lib/proposal-submission-store.ts
|
|
1356
|
-
import { mkdirSync as
|
|
1632
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
|
|
1357
1633
|
import { homedir as homedir6 } from "os";
|
|
1358
|
-
import { dirname as
|
|
1359
|
-
var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ??
|
|
1634
|
+
import { dirname as dirname8, join as join9 } from "path";
|
|
1635
|
+
var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
|
|
1360
1636
|
var FILE_NAME2 = "proposal-submissions.json";
|
|
1361
1637
|
var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
|
|
1362
1638
|
function nowSec2() {
|
|
@@ -1365,8 +1641,8 @@ function nowSec2() {
|
|
|
1365
1641
|
var ProposalSubmissionStore = class {
|
|
1366
1642
|
filePath;
|
|
1367
1643
|
constructor(filePath) {
|
|
1368
|
-
const baseDir = filePath && filePath.endsWith(".json") ?
|
|
1369
|
-
this.filePath =
|
|
1644
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR6;
|
|
1645
|
+
this.filePath = join9(baseDir, FILE_NAME2);
|
|
1370
1646
|
}
|
|
1371
1647
|
/** Record what was submitted, right after a successful submitProposal(). */
|
|
1372
1648
|
record(entry) {
|
|
@@ -1389,9 +1665,9 @@ var ProposalSubmissionStore = class {
|
|
|
1389
1665
|
return this.load().entries.find((e) => e.proposalId === proposalId);
|
|
1390
1666
|
}
|
|
1391
1667
|
load() {
|
|
1392
|
-
if (!
|
|
1668
|
+
if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
|
|
1393
1669
|
try {
|
|
1394
|
-
const parsed = JSON.parse(
|
|
1670
|
+
const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
|
|
1395
1671
|
const cutoff = nowSec2() - MAX_AGE_SECONDS3;
|
|
1396
1672
|
parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
|
|
1397
1673
|
return parsed;
|
|
@@ -1400,9 +1676,9 @@ var ProposalSubmissionStore = class {
|
|
|
1400
1676
|
}
|
|
1401
1677
|
}
|
|
1402
1678
|
persist(file) {
|
|
1403
|
-
|
|
1679
|
+
mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
|
|
1404
1680
|
const tmpPath = `${this.filePath}.tmp`;
|
|
1405
|
-
|
|
1681
|
+
writeFileSync8(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1406
1682
|
renameSync2(tmpPath, this.filePath);
|
|
1407
1683
|
}
|
|
1408
1684
|
};
|
|
@@ -1514,12 +1790,13 @@ var SharedState = class {
|
|
|
1514
1790
|
gatekeeper;
|
|
1515
1791
|
/**
|
|
1516
1792
|
* @param dataDir Passed to the AttestationCache so it can enforce AS key
|
|
1517
|
-
* pinning (as-pairing.ts)
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1793
|
+
* pinning (as-pairing.ts), and to the SPClient so it can enforce opt-in
|
|
1794
|
+
* TLS pinning (as-tls-pin.ts). Optional for backward compat with
|
|
1795
|
+
* existing constructions/tests that pass only a URL — those get
|
|
1796
|
+
* unpinned trust-on-first-use behavior, same as before pinning existed.
|
|
1520
1797
|
*/
|
|
1521
1798
|
constructor(spUrl2, gateStorePath, dataDir2) {
|
|
1522
|
-
this.spClient = new SPClient(spUrl2);
|
|
1799
|
+
this.spClient = new SPClient(spUrl2, void 0, dataDir2);
|
|
1523
1800
|
this.cache = new AttestationCache(this.spClient, dataDir2);
|
|
1524
1801
|
this.gateStore = new GateStore(gateStorePath);
|
|
1525
1802
|
this.executionLog = new ExecutionLog(gateStorePath);
|
|
@@ -1679,17 +1956,17 @@ function formatConsumptionFull(entries) {
|
|
|
1679
1956
|
}
|
|
1680
1957
|
|
|
1681
1958
|
// src/lib/context-loader.ts
|
|
1682
|
-
import { readFileSync as
|
|
1959
|
+
import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
|
|
1683
1960
|
import { homedir as homedir7 } from "os";
|
|
1684
|
-
import { join as
|
|
1685
|
-
var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ??
|
|
1961
|
+
import { join as join10 } from "path";
|
|
1962
|
+
var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
|
|
1686
1963
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
1687
1964
|
function readContextFile(dataDir2) {
|
|
1688
1965
|
const dir = dataDir2 ?? DEFAULT_DIR7;
|
|
1689
|
-
const filePath =
|
|
1690
|
-
if (!
|
|
1966
|
+
const filePath = join10(dir, "context.md");
|
|
1967
|
+
if (!existsSync10(filePath)) return null;
|
|
1691
1968
|
try {
|
|
1692
|
-
const content =
|
|
1969
|
+
const content = readFileSync11(filePath, "utf-8").trim();
|
|
1693
1970
|
return content || null;
|
|
1694
1971
|
} catch {
|
|
1695
1972
|
return null;
|
|
@@ -1809,6 +2086,11 @@ function asKeyMismatchNotice(what) {
|
|
|
1809
2086
|
|
|
1810
2087
|
TELL THE USER: this needs an operator to check whether the Authority Server's key changed intentionally (e.g. a reinstall) or something else is answering at that address, at ${uiUrl()}. Nothing ran under the unrecognized key.`;
|
|
1811
2088
|
}
|
|
2089
|
+
function asTlsMismatchNotice(what) {
|
|
2090
|
+
return `${what} Suveren gateway is LOCKED because a connection to the Authority Server presented a TLS certificate that does not match the one pinned when this gateway last signed in (opt-in pin-tls). This is NOT an ended sign-in \u2014 signing in again will be refused until the mismatch is resolved.
|
|
2091
|
+
|
|
2092
|
+
TELL THE USER: this needs an operator to check whether the Authority Server's certificate changed intentionally (e.g. a new TLS key, not just a renewal) or something else is answering at that address, at ${uiUrl()}. Nothing was sent under the unrecognized certificate.`;
|
|
2093
|
+
}
|
|
1812
2094
|
function asUrlChangedNotice(what) {
|
|
1813
2095
|
return `${what} Suveren gateway is LOCKED because the Authority Server it points at has changed since it last signed in. Cached mandates from the old server were cleared.
|
|
1814
2096
|
|
|
@@ -1821,6 +2103,8 @@ function lockedNotice(action, reason = "restart") {
|
|
|
1821
2103
|
return expiredSessionNotice(what);
|
|
1822
2104
|
case "as-key-mismatch":
|
|
1823
2105
|
return asKeyMismatchNotice(what);
|
|
2106
|
+
case "as-tls-mismatch":
|
|
2107
|
+
return asTlsMismatchNotice(what);
|
|
1824
2108
|
case "as-url-changed":
|
|
1825
2109
|
return asUrlChangedNotice(what);
|
|
1826
2110
|
default:
|
|
@@ -1989,6 +2273,11 @@ async function verifyTicket(cache, receipt, expected) {
|
|
|
1989
2273
|
`Ticket proposalId "${String(receipt.proposalId)}" does not match the proposal being executed "${expected.proposalId}" \u2014 refusing to execute.`
|
|
1990
2274
|
);
|
|
1991
2275
|
}
|
|
2276
|
+
if (expected.idempotencyKey !== void 0 && receipt.idempotencyKey !== expected.idempotencyKey) {
|
|
2277
|
+
throw new TicketBindingMismatchError(
|
|
2278
|
+
`Ticket idempotencyKey "${String(receipt.idempotencyKey)}" does not match the one this call sent "${expected.idempotencyKey}" \u2014 refusing to execute.`
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
1992
2281
|
if (expected.contentHash !== void 0) {
|
|
1993
2282
|
if (receipt.contentHash !== expected.contentHash) {
|
|
1994
2283
|
throw new TicketBindingMismatchError(
|
|
@@ -2766,6 +3055,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
2766
3055
|
try {
|
|
2767
3056
|
const actionType = declaredActionType;
|
|
2768
3057
|
const binding = computeContentBinding(auth.profileId, tool, args, actionType);
|
|
3058
|
+
const idempotencyKey = randomUUID();
|
|
2769
3059
|
const { receipt } = await state2.spClient.postReceipt({
|
|
2770
3060
|
authorizationId: authzId,
|
|
2771
3061
|
// Optional cross-check — the AS fails closed on a mismatch.
|
|
@@ -2775,7 +3065,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
2775
3065
|
actionType,
|
|
2776
3066
|
executionContext: { ...execution },
|
|
2777
3067
|
amount: typeof execution.amount === "number" ? execution.amount : void 0,
|
|
2778
|
-
idempotencyKey
|
|
3068
|
+
idempotencyKey,
|
|
2779
3069
|
// Privacy: send the hash and how to reproduce it — never the
|
|
2780
3070
|
// preimage. `binding` also carries `boundContent`, the plaintext
|
|
2781
3071
|
// that was hashed (an email's to/cc/subject/body); it stays on
|
|
@@ -2789,6 +3079,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
2789
3079
|
executionContext: { ...execution },
|
|
2790
3080
|
authorizationId: authzId,
|
|
2791
3081
|
profileId: auth.profileId,
|
|
3082
|
+
idempotencyKey,
|
|
2792
3083
|
contentHash: binding?.contentHash,
|
|
2793
3084
|
contentBinding: binding?.contentBinding
|
|
2794
3085
|
});
|
|
@@ -3750,16 +4041,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
3750
4041
|
}
|
|
3751
4042
|
|
|
3752
4043
|
// src/lib/integration-registry.ts
|
|
3753
|
-
import { mkdirSync as
|
|
4044
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11 } from "fs";
|
|
3754
4045
|
import { homedir as homedir8 } from "os";
|
|
3755
|
-
import { dirname as
|
|
3756
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
4046
|
+
import { dirname as dirname9, join as join11 } from "path";
|
|
4047
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
3757
4048
|
var IntegrationRegistry = class {
|
|
3758
4049
|
integrations = /* @__PURE__ */ new Map();
|
|
3759
4050
|
filePath;
|
|
3760
4051
|
constructor(dataDir2) {
|
|
3761
4052
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
3762
|
-
this.filePath =
|
|
4053
|
+
this.filePath = join11(dir, "integrations.json");
|
|
3763
4054
|
this.load();
|
|
3764
4055
|
}
|
|
3765
4056
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -3790,12 +4081,12 @@ var IntegrationRegistry = class {
|
|
|
3790
4081
|
}
|
|
3791
4082
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
3792
4083
|
load() {
|
|
3793
|
-
if (!
|
|
3794
|
-
|
|
4084
|
+
if (!existsSync11(this.filePath)) {
|
|
4085
|
+
mkdirSync9(dirname9(this.filePath), { recursive: true });
|
|
3795
4086
|
return;
|
|
3796
4087
|
}
|
|
3797
4088
|
try {
|
|
3798
|
-
const raw =
|
|
4089
|
+
const raw = readFileSync12(this.filePath, "utf-8");
|
|
3799
4090
|
const data = JSON.parse(raw);
|
|
3800
4091
|
const migrated = data.integrations.map((i) => {
|
|
3801
4092
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -3824,45 +4115,45 @@ var IntegrationRegistry = class {
|
|
|
3824
4115
|
version: 1,
|
|
3825
4116
|
integrations: Array.from(this.integrations.values())
|
|
3826
4117
|
};
|
|
3827
|
-
|
|
3828
|
-
|
|
4118
|
+
mkdirSync9(dirname9(this.filePath), { recursive: true });
|
|
4119
|
+
writeFileSync9(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
3829
4120
|
}
|
|
3830
4121
|
};
|
|
3831
4122
|
|
|
3832
4123
|
// src/lib/integration-manager.ts
|
|
3833
4124
|
import { homedir as homedir9 } from "os";
|
|
3834
|
-
import { join as
|
|
3835
|
-
import { existsSync as
|
|
4125
|
+
import { join as join13, delimiter } from "path";
|
|
4126
|
+
import { existsSync as existsSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync } from "fs";
|
|
3836
4127
|
import { execFile } from "child_process";
|
|
3837
4128
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3838
4129
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3839
4130
|
import { getProfile as getProfile5 } from "@hap/core";
|
|
3840
4131
|
|
|
3841
4132
|
// src/lib/manifest-loader.ts
|
|
3842
|
-
import { readFileSync as
|
|
3843
|
-
import { join as
|
|
4133
|
+
import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
|
|
4134
|
+
import { join as join12, resolve } from "path";
|
|
3844
4135
|
var manifests = /* @__PURE__ */ new Map();
|
|
3845
4136
|
function loadManifests(integrationsDir) {
|
|
3846
4137
|
const dir = resolve(
|
|
3847
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
4138
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3848
4139
|
);
|
|
3849
|
-
const indexPath =
|
|
3850
|
-
if (!
|
|
4140
|
+
const indexPath = join12(dir, "index.json");
|
|
4141
|
+
if (!existsSync12(indexPath)) {
|
|
3851
4142
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3852
4143
|
return 0;
|
|
3853
4144
|
}
|
|
3854
4145
|
let index;
|
|
3855
4146
|
try {
|
|
3856
|
-
index = JSON.parse(
|
|
4147
|
+
index = JSON.parse(readFileSync13(indexPath, "utf-8"));
|
|
3857
4148
|
} catch (err) {
|
|
3858
4149
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3859
4150
|
return 0;
|
|
3860
4151
|
}
|
|
3861
4152
|
let loaded = 0;
|
|
3862
4153
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3863
|
-
const manifestPath =
|
|
4154
|
+
const manifestPath = join12(dir, relativePath);
|
|
3864
4155
|
try {
|
|
3865
|
-
const manifest = JSON.parse(
|
|
4156
|
+
const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
|
|
3866
4157
|
manifests.set(id, manifest);
|
|
3867
4158
|
loaded++;
|
|
3868
4159
|
} catch (err) {
|
|
@@ -3950,26 +4241,26 @@ function safeHost(url) {
|
|
|
3950
4241
|
}
|
|
3951
4242
|
|
|
3952
4243
|
// src/lib/integration-manager.ts
|
|
3953
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
3954
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
4244
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
|
|
4245
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join13(DEFAULT_DATA_DIR2, "integrations");
|
|
3955
4246
|
var installLock = Promise.resolve();
|
|
3956
4247
|
function withInstallLock(task) {
|
|
3957
4248
|
const run = installLock.then(task, task);
|
|
3958
4249
|
installLock = run.then(() => void 0, () => void 0);
|
|
3959
4250
|
return run;
|
|
3960
4251
|
}
|
|
3961
|
-
var INTEGRATIONS_BIN =
|
|
4252
|
+
var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
3962
4253
|
function buildPath() {
|
|
3963
4254
|
const base = process.env.PATH ?? "";
|
|
3964
4255
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
3965
4256
|
}
|
|
3966
4257
|
function ensureIntegrationsDir() {
|
|
3967
|
-
if (!
|
|
3968
|
-
|
|
4258
|
+
if (!existsSync13(INTEGRATIONS_DIR)) {
|
|
4259
|
+
mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
|
|
3969
4260
|
}
|
|
3970
|
-
const pkgPath =
|
|
3971
|
-
if (!
|
|
3972
|
-
|
|
4261
|
+
const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
|
|
4262
|
+
if (!existsSync13(pkgPath)) {
|
|
4263
|
+
writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
3973
4264
|
}
|
|
3974
4265
|
}
|
|
3975
4266
|
function readAgeOf(config) {
|
|
@@ -4035,19 +4326,19 @@ var IntegrationManager = class {
|
|
|
4035
4326
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
4036
4327
|
*/
|
|
4037
4328
|
isUsableInstall(npmPackage) {
|
|
4038
|
-
const pkgDir =
|
|
4039
|
-
const pkgJson =
|
|
4040
|
-
if (!
|
|
4329
|
+
const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
4330
|
+
const pkgJson = join13(pkgDir, "package.json");
|
|
4331
|
+
if (!existsSync13(pkgDir) || !existsSync13(pkgJson)) return false;
|
|
4041
4332
|
try {
|
|
4042
|
-
const pkg = JSON.parse(
|
|
4333
|
+
const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
|
|
4043
4334
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
4044
4335
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
4045
4336
|
const binEntries = Object.entries(binMap);
|
|
4046
4337
|
for (const [binName, target] of binEntries) {
|
|
4047
|
-
if (!
|
|
4338
|
+
if (!existsSync13(join13(pkgDir, target))) return false;
|
|
4048
4339
|
if (!this.binShimExists(binName)) return false;
|
|
4049
4340
|
}
|
|
4050
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
4341
|
+
if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
|
|
4051
4342
|
return true;
|
|
4052
4343
|
} catch {
|
|
4053
4344
|
return false;
|
|
@@ -4061,7 +4352,7 @@ var IntegrationManager = class {
|
|
|
4061
4352
|
binShimExists(binName) {
|
|
4062
4353
|
if (!binName) return false;
|
|
4063
4354
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
4064
|
-
return candidates.some((c) =>
|
|
4355
|
+
return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
|
|
4065
4356
|
}
|
|
4066
4357
|
/**
|
|
4067
4358
|
* Install an npm package into the managed integrations directory if not
|
|
@@ -4085,8 +4376,8 @@ var IntegrationManager = class {
|
|
|
4085
4376
|
}
|
|
4086
4377
|
/** The actual install. Callers MUST hold the install lock. */
|
|
4087
4378
|
async installNow(npmPackage) {
|
|
4088
|
-
const pkgDir =
|
|
4089
|
-
if (
|
|
4379
|
+
const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
4380
|
+
if (existsSync13(pkgDir)) {
|
|
4090
4381
|
console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
|
|
4091
4382
|
try {
|
|
4092
4383
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -4521,28 +4812,28 @@ var IntegrationManager = class {
|
|
|
4521
4812
|
};
|
|
4522
4813
|
|
|
4523
4814
|
// src/lib/profile-loader.ts
|
|
4524
|
-
import { readFileSync as
|
|
4525
|
-
import { join as
|
|
4815
|
+
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
4816
|
+
import { join as join14, resolve as resolve2 } from "path";
|
|
4526
4817
|
import { registerProfile } from "@hap/core";
|
|
4527
4818
|
function loadProfiles(profilesDir) {
|
|
4528
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
4529
|
-
const indexPath =
|
|
4530
|
-
if (!
|
|
4819
|
+
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
4820
|
+
const indexPath = join14(dir, "index.json");
|
|
4821
|
+
if (!existsSync14(indexPath)) {
|
|
4531
4822
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
4532
4823
|
return 0;
|
|
4533
4824
|
}
|
|
4534
4825
|
let index;
|
|
4535
4826
|
try {
|
|
4536
|
-
index = JSON.parse(
|
|
4827
|
+
index = JSON.parse(readFileSync15(indexPath, "utf-8"));
|
|
4537
4828
|
} catch (err) {
|
|
4538
4829
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
4539
4830
|
return 0;
|
|
4540
4831
|
}
|
|
4541
4832
|
let loaded = 0;
|
|
4542
4833
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
4543
|
-
const profilePath =
|
|
4834
|
+
const profilePath = join14(dir, relativePath);
|
|
4544
4835
|
try {
|
|
4545
|
-
const profile = JSON.parse(
|
|
4836
|
+
const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
|
|
4546
4837
|
registerProfile(profileId, profile);
|
|
4547
4838
|
const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
4548
4839
|
if (shortName && shortName !== profileId) {
|
|
@@ -4578,8 +4869,8 @@ function warnNoProfiles(reason) {
|
|
|
4578
4869
|
import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
|
|
4579
4870
|
|
|
4580
4871
|
// src/lib/committed-executor.ts
|
|
4581
|
-
import { existsSync as
|
|
4582
|
-
import { dirname as
|
|
4872
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
4873
|
+
import { dirname as dirname10, join as join15 } from "path";
|
|
4583
4874
|
var CommittedExecutor = class {
|
|
4584
4875
|
constructor(run) {
|
|
4585
4876
|
this.run = run;
|
|
@@ -4607,7 +4898,7 @@ var ExecutorLock = class {
|
|
|
4607
4898
|
path;
|
|
4608
4899
|
held = false;
|
|
4609
4900
|
constructor(dataDir2) {
|
|
4610
|
-
this.path =
|
|
4901
|
+
this.path = join15(dataDir2, "executor.lock");
|
|
4611
4902
|
}
|
|
4612
4903
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
4613
4904
|
acquire() {
|
|
@@ -4615,9 +4906,9 @@ var ExecutorLock = class {
|
|
|
4615
4906
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
4616
4907
|
return { held: false, holderPid: current.pid };
|
|
4617
4908
|
}
|
|
4618
|
-
|
|
4909
|
+
mkdirSync11(dirname10(this.path), { recursive: true, mode: 448 });
|
|
4619
4910
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
4620
|
-
|
|
4911
|
+
writeFileSync11(this.path, JSON.stringify(lock), { mode: 384 });
|
|
4621
4912
|
this.held = true;
|
|
4622
4913
|
return { held: true };
|
|
4623
4914
|
}
|
|
@@ -4639,9 +4930,9 @@ var ExecutorLock = class {
|
|
|
4639
4930
|
}
|
|
4640
4931
|
}
|
|
4641
4932
|
read() {
|
|
4642
|
-
if (!
|
|
4933
|
+
if (!existsSync15(this.path)) return null;
|
|
4643
4934
|
try {
|
|
4644
|
-
const parsed = JSON.parse(
|
|
4935
|
+
const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
|
|
4645
4936
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
4646
4937
|
} catch {
|
|
4647
4938
|
return null;
|
|
@@ -4661,76 +4952,8 @@ function isAlive(pid) {
|
|
|
4661
4952
|
import { homedir as homedir10 } from "os";
|
|
4662
4953
|
import { join as join17 } from "path";
|
|
4663
4954
|
|
|
4664
|
-
// src/lib/as-config.ts
|
|
4665
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
4666
|
-
import { dirname as dirname10, join as join15 } from "path";
|
|
4667
|
-
var DEFAULT_AS_URL = "https://www.suveren.ai";
|
|
4668
|
-
function configPath(dataDir2) {
|
|
4669
|
-
return join15(dataDir2, "config.json");
|
|
4670
|
-
}
|
|
4671
|
-
function readAsConfigStrict(dataDir2) {
|
|
4672
|
-
const path = configPath(dataDir2);
|
|
4673
|
-
if (!existsSync15(path)) return {};
|
|
4674
|
-
let data;
|
|
4675
|
-
try {
|
|
4676
|
-
data = JSON.parse(readFileSync15(path, "utf-8"));
|
|
4677
|
-
} catch (err) {
|
|
4678
|
-
throw new Error(
|
|
4679
|
-
`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
4680
|
-
);
|
|
4681
|
-
}
|
|
4682
|
-
const out = {};
|
|
4683
|
-
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
4684
|
-
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
4685
|
-
return out;
|
|
4686
|
-
}
|
|
4687
|
-
function validateAsUrl(candidate) {
|
|
4688
|
-
const trimmed = candidate.trim();
|
|
4689
|
-
if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
|
|
4690
|
-
let parsed;
|
|
4691
|
-
try {
|
|
4692
|
-
parsed = new URL(trimmed);
|
|
4693
|
-
} catch {
|
|
4694
|
-
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
4695
|
-
}
|
|
4696
|
-
const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
4697
|
-
if (parsed.protocol !== "https:" && !isLocalHttp) {
|
|
4698
|
-
return {
|
|
4699
|
-
ok: false,
|
|
4700
|
-
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
|
|
4701
|
-
};
|
|
4702
|
-
}
|
|
4703
|
-
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
4704
|
-
if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
|
|
4705
|
-
normalized = normalized.slice(0, -1);
|
|
4706
|
-
} else if (normalized === `${parsed.origin}/`) {
|
|
4707
|
-
normalized = parsed.origin;
|
|
4708
|
-
}
|
|
4709
|
-
return { ok: true, url: normalized };
|
|
4710
|
-
}
|
|
4711
|
-
function resolveAsUrl(dataDir2, flag) {
|
|
4712
|
-
if (flag) {
|
|
4713
|
-
const v = validateAsUrl(flag);
|
|
4714
|
-
if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
|
|
4715
|
-
return v.url;
|
|
4716
|
-
}
|
|
4717
|
-
const envUrl = process.env.SUVEREN_AS_URL;
|
|
4718
|
-
if (envUrl) {
|
|
4719
|
-
const v = validateAsUrl(envUrl);
|
|
4720
|
-
if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
|
|
4721
|
-
return v.url;
|
|
4722
|
-
}
|
|
4723
|
-
const saved = readAsConfigStrict(dataDir2).asUrl;
|
|
4724
|
-
if (saved) {
|
|
4725
|
-
const v = validateAsUrl(saved);
|
|
4726
|
-
if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
|
|
4727
|
-
return v.url;
|
|
4728
|
-
}
|
|
4729
|
-
return DEFAULT_AS_URL;
|
|
4730
|
-
}
|
|
4731
|
-
|
|
4732
4955
|
// src/lib/mcp-as-tracker.ts
|
|
4733
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as
|
|
4956
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "fs";
|
|
4734
4957
|
import { dirname as dirname11, join as join16 } from "path";
|
|
4735
4958
|
function trackerPath(dataDir2) {
|
|
4736
4959
|
return join16(dataDir2, "mcp-as-pairing.json");
|
|
@@ -4739,7 +4962,7 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
4739
4962
|
const path = trackerPath(dataDir2);
|
|
4740
4963
|
if (!existsSync16(path)) return null;
|
|
4741
4964
|
try {
|
|
4742
|
-
const data = JSON.parse(
|
|
4965
|
+
const data = JSON.parse(readFileSync17(path, "utf-8"));
|
|
4743
4966
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
4744
4967
|
} catch {
|
|
4745
4968
|
return null;
|
|
@@ -4754,6 +4977,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
|
4754
4977
|
// bin/http.ts
|
|
4755
4978
|
var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
|
|
4756
4979
|
var spUrl = resolveAsUrl(dataDir);
|
|
4980
|
+
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
4757
4981
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
4758
4982
|
setAsBaseUrl(spUrl);
|
|
4759
4983
|
var state = new SharedState(spUrl, void 0, dataDir);
|
|
@@ -4838,7 +5062,7 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
4838
5062
|
});
|
|
4839
5063
|
app.post("/internal/clear-session", internalOnly, (req, res) => {
|
|
4840
5064
|
const reason = req.body?.reason;
|
|
4841
|
-
const validReason = reason === "as-key-mismatch" || reason === "as-url-changed" ? reason : void 0;
|
|
5065
|
+
const validReason = reason === "as-key-mismatch" || reason === "as-tls-mismatch" || reason === "as-url-changed" ? reason : void 0;
|
|
4842
5066
|
state.spClient.clearSession(validReason);
|
|
4843
5067
|
console.error(`[Suveren MCP] Session cleared by control-plane (${validReason ?? "expired"})`);
|
|
4844
5068
|
res.json({ ok: true });
|