@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
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
} from "./chunk-3BNWN6VP.mjs";
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
|
-
import { randomBytes as
|
|
8
|
-
import { existsSync as existsSync9, readFileSync as
|
|
7
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
8
|
+
import { existsSync as existsSync9, readFileSync as readFileSync11, renameSync as renameSync2 } from "fs";
|
|
9
9
|
import { dirname as dirname8, join as join11 } from "path";
|
|
10
10
|
import { homedir as homedir7 } from "os";
|
|
11
11
|
import express from "express";
|
|
@@ -117,6 +117,13 @@ var Vault = class {
|
|
|
117
117
|
this.clearKey();
|
|
118
118
|
this.lockedReason = "as-key-mismatch";
|
|
119
119
|
}
|
|
120
|
+
/** Lock because a pin-tls-enforced connection to the Authority Server
|
|
121
|
+
* presented a certificate whose public key doesn't match the one pinned
|
|
122
|
+
* at pairing — see as-tls-pin.ts. Mirrors lockAsKeyMismatch() exactly. */
|
|
123
|
+
lockAsTlsMismatch() {
|
|
124
|
+
this.clearKey();
|
|
125
|
+
this.lockedReason = "as-tls-mismatch";
|
|
126
|
+
}
|
|
120
127
|
/**
|
|
121
128
|
* Boot-time only: record that the gateway starts locked because the
|
|
122
129
|
* resolved Authority Server URL differs from the one it was last paired
|
|
@@ -673,18 +680,38 @@ function readPairing(dataDir) {
|
|
|
673
680
|
try {
|
|
674
681
|
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
675
682
|
if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
|
|
676
|
-
return {
|
|
683
|
+
return {
|
|
684
|
+
asUrl: data.asUrl,
|
|
685
|
+
publicKeyHex: data.publicKeyHex,
|
|
686
|
+
pairedAt: data.pairedAt ?? "",
|
|
687
|
+
...typeof data.tlsSpkiPinHex === "string" ? { tlsSpkiPinHex: data.tlsSpkiPinHex } : {}
|
|
688
|
+
};
|
|
677
689
|
} catch {
|
|
678
690
|
return null;
|
|
679
691
|
}
|
|
680
692
|
}
|
|
681
|
-
function writePairing(dataDir, asUrl, publicKeyHex) {
|
|
682
|
-
const pairing = {
|
|
693
|
+
function writePairing(dataDir, asUrl, publicKeyHex, opts = {}) {
|
|
694
|
+
const pairing = {
|
|
695
|
+
asUrl,
|
|
696
|
+
publicKeyHex,
|
|
697
|
+
pairedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
698
|
+
...opts.tlsSpkiPinHex ? { tlsSpkiPinHex: opts.tlsSpkiPinHex } : {}
|
|
699
|
+
};
|
|
683
700
|
const path = pairingPath(dataDir);
|
|
684
701
|
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
685
702
|
writeFileSync2(path, JSON.stringify(pairing, null, 2), { encoding: "utf-8", mode: 384 });
|
|
686
703
|
return pairing;
|
|
687
704
|
}
|
|
705
|
+
function recordTlsPin(dataDir, asUrl, tlsSpkiPinHex) {
|
|
706
|
+
const existing = readPairing(dataDir);
|
|
707
|
+
if (!existing || existing.asUrl !== asUrl) return;
|
|
708
|
+
const path = pairingPath(dataDir);
|
|
709
|
+
writeFileSync2(
|
|
710
|
+
path,
|
|
711
|
+
JSON.stringify({ ...existing, tlsSpkiPinHex }, null, 2),
|
|
712
|
+
{ encoding: "utf-8", mode: 384 }
|
|
713
|
+
);
|
|
714
|
+
}
|
|
688
715
|
function clearPairing(dataDir) {
|
|
689
716
|
const path = pairingPath(dataDir);
|
|
690
717
|
try {
|
|
@@ -698,46 +725,467 @@ function fingerprintOf(publicKeyHex) {
|
|
|
698
725
|
return digest.match(/.{1,4}/g)?.join(":") ?? digest;
|
|
699
726
|
}
|
|
700
727
|
|
|
728
|
+
// src/lib/as-challenge.ts
|
|
729
|
+
import { createPublicKey, randomBytes as randomBytes2, verify as cryptoVerify } from "crypto";
|
|
730
|
+
import { canonicalize } from "@hap/core";
|
|
731
|
+
|
|
732
|
+
// src/lib/as-tls-pin.ts
|
|
733
|
+
import { createHash as createHash3 } from "crypto";
|
|
734
|
+
import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
|
|
735
|
+
import { Agent as HttpsAgent } from "https";
|
|
736
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
737
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
738
|
+
function effectiveCa() {
|
|
739
|
+
const path = process.env.NODE_EXTRA_CA_CERTS;
|
|
740
|
+
if (!path) return void 0;
|
|
741
|
+
try {
|
|
742
|
+
return [...rootCertificates, readFileSync4(path, "utf-8")];
|
|
743
|
+
} catch {
|
|
744
|
+
return void 0;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
var PIN_CHECK_ERROR_CODE = "EAS_TLS_PIN_CHECK";
|
|
748
|
+
function taggedCheckError(err) {
|
|
749
|
+
err.code = PIN_CHECK_ERROR_CODE;
|
|
750
|
+
return err;
|
|
751
|
+
}
|
|
752
|
+
function isPinCheckError(err) {
|
|
753
|
+
let cur = err;
|
|
754
|
+
for (let i = 0; i < 5 && cur; i++) {
|
|
755
|
+
if (cur instanceof Error && cur.code === PIN_CHECK_ERROR_CODE) return true;
|
|
756
|
+
cur = cur instanceof Error ? cur.cause : void 0;
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
var AsTlsMismatchError = class extends Error {
|
|
761
|
+
constructor(message) {
|
|
762
|
+
super(message);
|
|
763
|
+
this.name = "AsTlsMismatchError";
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function spkiSha256Hex(cert) {
|
|
767
|
+
if (!cert.pubkey) {
|
|
768
|
+
throw new Error("peer certificate carries no pubkey (expected the detailed certificate object)");
|
|
769
|
+
}
|
|
770
|
+
return createHash3("sha256").update(cert.pubkey).digest("hex");
|
|
771
|
+
}
|
|
772
|
+
function formatPinFingerprint(hex) {
|
|
773
|
+
const upper = hex.toUpperCase();
|
|
774
|
+
return upper.match(/.{1,4}/g)?.join(":") ?? upper;
|
|
775
|
+
}
|
|
776
|
+
function pinnedCheckServerIdentity(pinnedSpkiHex) {
|
|
777
|
+
const wantHex = pinnedSpkiHex.toLowerCase();
|
|
778
|
+
return (hostname, cert) => {
|
|
779
|
+
const defaultErr = defaultCheckServerIdentity(hostname, cert);
|
|
780
|
+
if (defaultErr) return taggedCheckError(defaultErr);
|
|
781
|
+
let actualHex;
|
|
782
|
+
try {
|
|
783
|
+
actualHex = spkiSha256Hex(cert);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return taggedCheckError(new Error(
|
|
786
|
+
`could not read the server certificate's public key to check the TLS pin \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
787
|
+
));
|
|
788
|
+
}
|
|
789
|
+
if (actualHex !== wantHex) {
|
|
790
|
+
return taggedCheckError(new Error(
|
|
791
|
+
`TLS certificate pin mismatch for ${hostname}: expected SPKI sha256 ${formatPinFingerprint(wantHex)}, got ${formatPinFingerprint(actualHex)}.`
|
|
792
|
+
));
|
|
793
|
+
}
|
|
794
|
+
return void 0;
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function buildPinnedDispatcher(pinnedSpkiHex) {
|
|
798
|
+
return new Agent({
|
|
799
|
+
connect: {
|
|
800
|
+
ca: effectiveCa(),
|
|
801
|
+
checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function buildCapturingDispatcher() {
|
|
806
|
+
let captured;
|
|
807
|
+
const dispatcher = new Agent({
|
|
808
|
+
connect: {
|
|
809
|
+
ca: effectiveCa(),
|
|
810
|
+
checkServerIdentity: (hostname, cert) => {
|
|
811
|
+
captured = cert;
|
|
812
|
+
const defaultErr = defaultCheckServerIdentity(hostname, cert);
|
|
813
|
+
return defaultErr ? taggedCheckError(defaultErr) : void 0;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
return { dispatcher, getCapturedCert: () => captured };
|
|
818
|
+
}
|
|
819
|
+
function buildPinnedHttpsAgent(getPinning) {
|
|
820
|
+
return new HttpsAgent({
|
|
821
|
+
checkServerIdentity: (hostname, cert) => {
|
|
822
|
+
const pinning = getPinning();
|
|
823
|
+
if (!pinning.enforce) return defaultCheckServerIdentity(hostname, cert);
|
|
824
|
+
if (!pinning.pinnedSpkiHex) {
|
|
825
|
+
return taggedCheckError(new Error(
|
|
826
|
+
`pin-tls is on but no certificate is pinned yet for ${hostname} \u2014 sign in again to establish it (re-pairing required).`
|
|
827
|
+
));
|
|
828
|
+
}
|
|
829
|
+
return pinnedCheckServerIdentity(pinning.pinnedSpkiHex)(hostname, cert);
|
|
830
|
+
},
|
|
831
|
+
ca: effectiveCa()
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
var DEFAULT_AS_FETCH_TIMEOUT_MS = 15e3;
|
|
835
|
+
function withBoundedSignal(init) {
|
|
836
|
+
const timeout = AbortSignal.timeout(DEFAULT_AS_FETCH_TIMEOUT_MS);
|
|
837
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
838
|
+
return { ...init, signal };
|
|
839
|
+
}
|
|
840
|
+
function closeAfter(dispatcher, pending) {
|
|
841
|
+
pending.catch(() => {
|
|
842
|
+
}).finally(() => {
|
|
843
|
+
void dispatcher.close().catch(() => {
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
async function fetchAs(url, init, pinning) {
|
|
848
|
+
if (!pinning.enforce && !pinning.capture) {
|
|
849
|
+
const res2 = await fetch(url, withBoundedSignal(init));
|
|
850
|
+
return { res: res2 };
|
|
851
|
+
}
|
|
852
|
+
if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
|
|
853
|
+
throw new AsTlsMismatchError(
|
|
854
|
+
`pin-tls is on but no certificate is pinned yet for ${url} \u2014 sign in again to establish it (re-pairing required).`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
|
|
858
|
+
const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
|
|
859
|
+
const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
|
|
860
|
+
closeAfter(dispatcher2, pending2);
|
|
861
|
+
try {
|
|
862
|
+
const res2 = await pending2;
|
|
863
|
+
return { res: res2 };
|
|
864
|
+
} catch (err) {
|
|
865
|
+
if (!isPinCheckError(err)) throw err;
|
|
866
|
+
throw new AsTlsMismatchError(
|
|
867
|
+
`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.`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
|
|
872
|
+
const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
|
|
873
|
+
closeAfter(dispatcher, pending);
|
|
874
|
+
let res;
|
|
875
|
+
try {
|
|
876
|
+
res = await pending;
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (!isPinCheckError(err)) throw err;
|
|
879
|
+
throw new AsTlsMismatchError(
|
|
880
|
+
`could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const cert = getCapturedCert();
|
|
884
|
+
return { res, capturedSpkiHex: cert ? spkiSha256Hex(cert) : void 0 };
|
|
885
|
+
}
|
|
886
|
+
function describeFetchError(err) {
|
|
887
|
+
if (err instanceof Error) {
|
|
888
|
+
const cause = err.cause;
|
|
889
|
+
if (cause instanceof Error) return cause.message;
|
|
890
|
+
return err.message;
|
|
891
|
+
}
|
|
892
|
+
return String(err);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/lib/as-challenge.ts
|
|
896
|
+
var AsChallengeUnreachableError = class extends Error {
|
|
897
|
+
constructor(message) {
|
|
898
|
+
super(message);
|
|
899
|
+
this.name = "AsChallengeUnreachableError";
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
var AsChallengeInvalidError = class extends Error {
|
|
903
|
+
constructor(message) {
|
|
904
|
+
super(message);
|
|
905
|
+
this.name = "AsChallengeInvalidError";
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var CHALLENGE_MAX_SKEW_SECONDS = 300;
|
|
909
|
+
var CHALLENGE_TYPE = "hap-as-challenge";
|
|
910
|
+
var CHALLENGE_DOMAIN_PREFIX = "hap-as-challenge\0";
|
|
911
|
+
function challengeSigningBytes(unsigned) {
|
|
912
|
+
return Buffer.concat([
|
|
913
|
+
Buffer.from(CHALLENGE_DOMAIN_PREFIX, "utf-8"),
|
|
914
|
+
Buffer.from(canonicalize({ typ: unsigned.typ, nonce: unsigned.nonce, issuedAt: unsigned.issuedAt }), "utf-8")
|
|
915
|
+
]);
|
|
916
|
+
}
|
|
917
|
+
function publicKeyFromHex(publicKeyHex) {
|
|
918
|
+
const raw = Buffer.from(publicKeyHex, "hex");
|
|
919
|
+
return createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") }, format: "jwk" });
|
|
920
|
+
}
|
|
921
|
+
function verifyChallengeSignature(publicKeyHex, unsigned, signature) {
|
|
922
|
+
try {
|
|
923
|
+
const base64 = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
924
|
+
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
925
|
+
const sigBytes = Buffer.from(base64 + padding, "base64");
|
|
926
|
+
const key = publicKeyFromHex(publicKeyHex);
|
|
927
|
+
return cryptoVerify(null, challengeSigningBytes(unsigned), key, sigBytes);
|
|
928
|
+
} catch {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async function verifyAsHoldsKey(asUrl, publicKeyHex, pinning = { enforce: false, capture: true }) {
|
|
933
|
+
const nonce = randomBytes2(32).toString("base64url");
|
|
934
|
+
let res;
|
|
935
|
+
let capturedSpkiHex;
|
|
936
|
+
try {
|
|
937
|
+
const result = await fetchAs(
|
|
938
|
+
`${asUrl}/api/as/challenge`,
|
|
939
|
+
{
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: { "Content-Type": "application/json" },
|
|
942
|
+
body: JSON.stringify({ nonce }),
|
|
943
|
+
signal: AbortSignal.timeout(5e3)
|
|
944
|
+
},
|
|
945
|
+
pinning
|
|
946
|
+
);
|
|
947
|
+
res = result.res;
|
|
948
|
+
capturedSpkiHex = result.capturedSpkiHex;
|
|
949
|
+
} catch (err) {
|
|
950
|
+
if (err instanceof AsTlsMismatchError) throw err;
|
|
951
|
+
throw new AsChallengeUnreachableError(
|
|
952
|
+
`could not reach the Authority Server at ${asUrl} to verify it holds its signing key \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (!res.ok) {
|
|
956
|
+
throw new AsChallengeUnreachableError(
|
|
957
|
+
`the Authority Server at ${asUrl} answered its signing-key challenge with HTTP ${res.status}`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const body = await res.json().catch(() => null);
|
|
961
|
+
if (!body) {
|
|
962
|
+
throw new AsChallengeUnreachableError(
|
|
963
|
+
`the Authority Server at ${asUrl} returned a malformed response to its signing-key challenge`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (body.typ !== CHALLENGE_TYPE) {
|
|
967
|
+
throw new AsChallengeInvalidError(
|
|
968
|
+
`the challenge response carries the wrong type ("${String(body.typ)}", expected "${CHALLENGE_TYPE}")`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
if (body.nonce !== nonce) {
|
|
972
|
+
throw new AsChallengeInvalidError(
|
|
973
|
+
"the challenge response echoes a different nonce than the one sent"
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (typeof body.issuedAt !== "number" || !Number.isFinite(body.issuedAt)) {
|
|
977
|
+
throw new AsChallengeInvalidError("the challenge response carries no valid issuedAt");
|
|
978
|
+
}
|
|
979
|
+
const skew = Math.abs(Math.floor(Date.now() / 1e3) - body.issuedAt);
|
|
980
|
+
if (skew > CHALLENGE_MAX_SKEW_SECONDS) {
|
|
981
|
+
throw new AsChallengeInvalidError(
|
|
982
|
+
`the challenge response is timestamped ${skew}s away from now, beyond the ${CHALLENGE_MAX_SKEW_SECONDS}s tolerance`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (typeof body.signature !== "string" || !body.signature) {
|
|
986
|
+
throw new AsChallengeInvalidError("the challenge response carries no signature");
|
|
987
|
+
}
|
|
988
|
+
if (!verifyChallengeSignature(publicKeyHex, { typ: body.typ, nonce: body.nonce, issuedAt: body.issuedAt }, body.signature)) {
|
|
989
|
+
throw new AsChallengeInvalidError(
|
|
990
|
+
"the challenge signature does not verify under the key being checked \u2014 this is exactly what the challenge exists to catch: something answered the request but does not hold the matching private key."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return { capturedTlsSpkiHex: capturedSpkiHex };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/lib/as-config.ts
|
|
997
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
998
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
999
|
+
var DEFAULT_AS_URL = "https://www.suveren.ai";
|
|
1000
|
+
function configPath(dataDir) {
|
|
1001
|
+
return join4(dataDir, "config.json");
|
|
1002
|
+
}
|
|
1003
|
+
function readAsConfigStrict(dataDir) {
|
|
1004
|
+
const path = configPath(dataDir);
|
|
1005
|
+
if (!existsSync3(path)) return {};
|
|
1006
|
+
let data;
|
|
1007
|
+
try {
|
|
1008
|
+
data = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const out = {};
|
|
1015
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
1016
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
1017
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
1018
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
function readAsConfig(dataDir) {
|
|
1022
|
+
const path = configPath(dataDir);
|
|
1023
|
+
if (!existsSync3(path)) return {};
|
|
1024
|
+
try {
|
|
1025
|
+
const data = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1026
|
+
const out = {};
|
|
1027
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
1028
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
1029
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
1030
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
1031
|
+
return out;
|
|
1032
|
+
} catch {
|
|
1033
|
+
return {};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function writeAsConfig(dataDir, patch) {
|
|
1037
|
+
const merged = { ...readAsConfig(dataDir), ...patch };
|
|
1038
|
+
const path = configPath(dataDir);
|
|
1039
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
|
|
1040
|
+
writeFileSync3(path, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1041
|
+
return merged;
|
|
1042
|
+
}
|
|
1043
|
+
function validateAsUrl(candidate) {
|
|
1044
|
+
const trimmed = candidate.trim();
|
|
1045
|
+
if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
|
|
1046
|
+
let parsed;
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = new URL(trimmed);
|
|
1049
|
+
} catch {
|
|
1050
|
+
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
1051
|
+
}
|
|
1052
|
+
const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
1053
|
+
if (parsed.protocol !== "https:" && !isLocalHttp) {
|
|
1054
|
+
return {
|
|
1055
|
+
ok: false,
|
|
1056
|
+
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
1060
|
+
if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
|
|
1061
|
+
normalized = normalized.slice(0, -1);
|
|
1062
|
+
} else if (normalized === `${parsed.origin}/`) {
|
|
1063
|
+
normalized = parsed.origin;
|
|
1064
|
+
}
|
|
1065
|
+
return { ok: true, url: normalized };
|
|
1066
|
+
}
|
|
1067
|
+
function resolveAsUrl(dataDir, flag) {
|
|
1068
|
+
if (flag) {
|
|
1069
|
+
const v = validateAsUrl(flag);
|
|
1070
|
+
if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
|
|
1071
|
+
return v.url;
|
|
1072
|
+
}
|
|
1073
|
+
const envUrl = process.env.SUVEREN_AS_URL;
|
|
1074
|
+
if (envUrl) {
|
|
1075
|
+
const v = validateAsUrl(envUrl);
|
|
1076
|
+
if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
|
|
1077
|
+
return v.url;
|
|
1078
|
+
}
|
|
1079
|
+
const saved = readAsConfigStrict(dataDir).asUrl;
|
|
1080
|
+
if (saved) {
|
|
1081
|
+
const v = validateAsUrl(saved);
|
|
1082
|
+
if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir}/config.json: ${v.error}`);
|
|
1083
|
+
return v.url;
|
|
1084
|
+
}
|
|
1085
|
+
return DEFAULT_AS_URL;
|
|
1086
|
+
}
|
|
1087
|
+
function resolvePinTls(dataDir) {
|
|
1088
|
+
return readAsConfig(dataDir).pinTls === true;
|
|
1089
|
+
}
|
|
1090
|
+
function validatePinTlsForUrl(pinTls, asUrl) {
|
|
1091
|
+
if (!pinTls) return;
|
|
1092
|
+
if (!asUrl.startsWith("https:")) {
|
|
1093
|
+
throw new Error(
|
|
1094
|
+
`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\`.`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function resolvePinTlsExpectedFingerprint(dataDir) {
|
|
1099
|
+
return readAsConfig(dataDir).pinTlsExpectedFingerprint;
|
|
1100
|
+
}
|
|
1101
|
+
function clearPinTlsExpectedFingerprint(dataDir) {
|
|
1102
|
+
if (readAsConfig(dataDir).pinTlsExpectedFingerprint === void 0) return;
|
|
1103
|
+
writeAsConfig(dataDir, { pinTlsExpectedFingerprint: void 0 });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
701
1106
|
// src/routes/auth.ts
|
|
702
1107
|
var DEFAULT_SP_URL = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
|
|
703
|
-
async function
|
|
704
|
-
let publicKeyHex;
|
|
1108
|
+
async function fetchAsPublicKey(asUrl) {
|
|
705
1109
|
try {
|
|
706
1110
|
const res = await fetch(`${asUrl}/api/as/pubkey`, { signal: AbortSignal.timeout(5e3) });
|
|
707
1111
|
if (!res.ok) {
|
|
708
1112
|
return {
|
|
709
1113
|
ok: false,
|
|
710
|
-
|
|
711
|
-
message: `Could not reach the Authority Server at ${asUrl} to verify its signing key (HTTP ${res.status}). Refusing to sign in.`
|
|
1114
|
+
message: `Could not reach the Authority Server at ${asUrl} to read its signing key (HTTP ${res.status}). Refusing to sign in.`
|
|
712
1115
|
};
|
|
713
1116
|
}
|
|
714
1117
|
const body = await res.json().catch(() => null);
|
|
715
1118
|
if (!body || typeof body.publicKey !== "string" || !body.publicKey) {
|
|
716
1119
|
return {
|
|
717
1120
|
ok: false,
|
|
718
|
-
error: "as_unreachable",
|
|
719
1121
|
message: `The Authority Server at ${asUrl} returned a malformed signing key. Refusing to sign in.`
|
|
720
1122
|
};
|
|
721
1123
|
}
|
|
722
|
-
publicKeyHex
|
|
1124
|
+
return { ok: true, publicKeyHex: body.publicKey };
|
|
723
1125
|
} catch (err) {
|
|
724
1126
|
return {
|
|
725
1127
|
ok: false,
|
|
726
|
-
|
|
727
|
-
message: `Could not reach the Authority Server at ${asUrl} to verify its signing key \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to sign in.`
|
|
1128
|
+
message: `Could not reach the Authority Server at ${asUrl} to read its signing key \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to sign in.`
|
|
728
1129
|
};
|
|
729
1130
|
}
|
|
1131
|
+
}
|
|
1132
|
+
async function checkAsKeyBeforeLogin(asUrl, dataDir) {
|
|
730
1133
|
const existing = readPairing(dataDir);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1134
|
+
const pinnedKey = existing && existing.asUrl === asUrl ? existing.publicKeyHex : void 0;
|
|
1135
|
+
const existingTlsPin = (existing && existing.asUrl === asUrl ? existing.tlsSpkiPinHex : void 0) ?? resolvePinTlsExpectedFingerprint(dataDir);
|
|
1136
|
+
let candidateKey;
|
|
1137
|
+
if (pinnedKey) {
|
|
1138
|
+
candidateKey = pinnedKey;
|
|
1139
|
+
} else {
|
|
1140
|
+
const fetched = await fetchAsPublicKey(asUrl);
|
|
1141
|
+
if (!fetched.ok) {
|
|
1142
|
+
return { ok: false, error: "as_unreachable", message: fetched.message };
|
|
1143
|
+
}
|
|
1144
|
+
candidateKey = fetched.publicKeyHex;
|
|
1145
|
+
}
|
|
1146
|
+
const pinning = {
|
|
1147
|
+
enforce: resolvePinTls(dataDir),
|
|
1148
|
+
pinnedSpkiHex: existingTlsPin,
|
|
1149
|
+
capture: true
|
|
1150
|
+
};
|
|
1151
|
+
let capturedTlsSpkiHex;
|
|
1152
|
+
try {
|
|
1153
|
+
const result = await verifyAsHoldsKey(asUrl, candidateKey, pinning);
|
|
1154
|
+
capturedTlsSpkiHex = result.capturedTlsSpkiHex;
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1157
|
+
return { ok: false, error: "as_tls_mismatch", message: err.message };
|
|
1158
|
+
}
|
|
1159
|
+
if (err instanceof AsChallengeUnreachableError) {
|
|
1160
|
+
return { ok: false, error: "as_unverified", message: `${err.message}. Refusing to sign in.` };
|
|
1161
|
+
}
|
|
1162
|
+
if (err instanceof AsChallengeInvalidError) {
|
|
1163
|
+
if (pinnedKey) {
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false,
|
|
1166
|
+
error: "as_key_mismatch",
|
|
1167
|
+
message: `The Authority Server at ${asUrl} does not match the one pinned when this gateway last signed in (pinned fingerprint ${fingerprintOf(pinnedKey)}) \u2014 its signing-key challenge did not verify (${err.message}). Refusing to sign in. If the AS's key changed intentionally, an operator must clear the old pairing before signing in again.`
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
ok: false,
|
|
1172
|
+
error: "as_unverified",
|
|
1173
|
+
message: `The Authority Server at ${asUrl} could not prove it holds the signing key it presented \u2014 ${err.message}. Refusing to sign in.`
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
throw err;
|
|
737
1177
|
}
|
|
738
|
-
return {
|
|
1178
|
+
return {
|
|
1179
|
+
ok: true,
|
|
1180
|
+
publicKeyHex: candidateKey,
|
|
1181
|
+
// A fresh capture (always present for https, unless pin-tls is on and
|
|
1182
|
+
// an existing pin was enforced instead — see fetchAs) wins; otherwise
|
|
1183
|
+
// whatever was already on file carries through unchanged.
|
|
1184
|
+
tlsSpkiPinHex: capturedTlsSpkiHex ?? existingTlsPin
|
|
1185
|
+
};
|
|
739
1186
|
}
|
|
740
|
-
function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished, pairing) {
|
|
1187
|
+
function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished, pairing, lockAsTlsMismatch2 = () => {
|
|
1188
|
+
}) {
|
|
741
1189
|
const router = Router();
|
|
742
1190
|
const SP_URL2 = pairing?.asUrl ?? DEFAULT_SP_URL;
|
|
743
1191
|
router.post("/login", loginRateLimit2, async (req, res) => {
|
|
@@ -752,24 +1200,52 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
752
1200
|
if (pairing) {
|
|
753
1201
|
keyCheck = await checkAsKeyBeforeLogin(pairing.asUrl, pairing.dataDir);
|
|
754
1202
|
if (!keyCheck.ok) {
|
|
755
|
-
|
|
1203
|
+
const status = keyCheck.error === "as_key_mismatch" || keyCheck.error === "as_tls_mismatch" ? 409 : 502;
|
|
1204
|
+
res.status(status).json({
|
|
756
1205
|
error: keyCheck.error,
|
|
757
1206
|
message: keyCheck.message
|
|
758
1207
|
});
|
|
759
1208
|
return;
|
|
760
1209
|
}
|
|
761
1210
|
}
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1211
|
+
const sessionPinning = {
|
|
1212
|
+
enforce: Boolean(pairing && resolvePinTls(pairing.dataDir)),
|
|
1213
|
+
pinnedSpkiHex: keyCheck?.tlsSpkiPinHex
|
|
1214
|
+
};
|
|
1215
|
+
let spRes;
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await fetchAs(
|
|
1218
|
+
`${SP_URL2}/api/auth/session`,
|
|
1219
|
+
{
|
|
1220
|
+
method: "POST",
|
|
1221
|
+
headers: {
|
|
1222
|
+
"Content-Type": "application/json",
|
|
1223
|
+
"X-API-Key": apiKey,
|
|
1224
|
+
// The AS grants a 30-day gateway session ONLY to a login that
|
|
1225
|
+
// identifies itself this way; without it, a 24-hour browser
|
|
1226
|
+
// session (see doc at the top of this file).
|
|
1227
|
+
...clientVersionHeaders()
|
|
1228
|
+
},
|
|
1229
|
+
// Belt-and-suspenders on top of fetchAs's own default bound
|
|
1230
|
+
// (as-tls-pin.ts): this is the exact call that hung in
|
|
1231
|
+
// production (2026-10-02) with NO timeout at all — a dead
|
|
1232
|
+
// keep-alive socket left the whole login request stuck forever.
|
|
1233
|
+
signal: AbortSignal.timeout(1e4)
|
|
1234
|
+
},
|
|
1235
|
+
sessionPinning
|
|
1236
|
+
);
|
|
1237
|
+
spRes = result.res;
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1240
|
+
res.status(409).json({ error: "as_tls_mismatch", message: err.message });
|
|
1241
|
+
return;
|
|
771
1242
|
}
|
|
772
|
-
|
|
1243
|
+
res.status(502).json({
|
|
1244
|
+
error: "as_unreachable",
|
|
1245
|
+
message: `Could not reach the Authority Server at ${SP_URL2} to sign in \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to sign in.`
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
773
1249
|
if (!spRes.ok) {
|
|
774
1250
|
const err = await spRes.json().catch(() => ({ error: "Invalid API key" }));
|
|
775
1251
|
res.status(spRes.status).json(err);
|
|
@@ -778,9 +1254,15 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
778
1254
|
if (pairing && keyCheck?.publicKeyHex) {
|
|
779
1255
|
const existing = readPairing(pairing.dataDir);
|
|
780
1256
|
if (!existing || existing.asUrl !== pairing.asUrl) {
|
|
781
|
-
writePairing(pairing.dataDir, pairing.asUrl, keyCheck.publicKeyHex
|
|
1257
|
+
writePairing(pairing.dataDir, pairing.asUrl, keyCheck.publicKeyHex, {
|
|
1258
|
+
tlsSpkiPinHex: keyCheck.tlsSpkiPinHex
|
|
1259
|
+
});
|
|
782
1260
|
console.error(`[Control Plane] Paired with Authority Server ${pairing.asUrl} (fingerprint ${fingerprintOf(keyCheck.publicKeyHex)})`);
|
|
1261
|
+
} else if (keyCheck.tlsSpkiPinHex && keyCheck.tlsSpkiPinHex !== existing.tlsSpkiPinHex) {
|
|
1262
|
+
recordTlsPin(pairing.dataDir, pairing.asUrl, keyCheck.tlsSpkiPinHex);
|
|
1263
|
+
console.error(`[Control Plane] TLS pin for Authority Server ${pairing.asUrl} captured/updated.`);
|
|
783
1264
|
}
|
|
1265
|
+
clearPinTlsExpectedFingerprint(pairing.dataDir);
|
|
784
1266
|
}
|
|
785
1267
|
const data = await spRes.json();
|
|
786
1268
|
const setCookieHeaders = spRes.headers.getSetCookie?.() ?? [];
|
|
@@ -828,10 +1310,11 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
828
1310
|
const kp = await loadOrGenerateKeyPair(vault2);
|
|
829
1311
|
const localPubkeyB64 = Buffer.from(kp.publicKey).toString("base64");
|
|
830
1312
|
const spCookie = vault2.getSpCookie();
|
|
831
|
-
const meKeyRes = await
|
|
832
|
-
|
|
833
|
-
signal: AbortSignal.timeout(5e3)
|
|
834
|
-
|
|
1313
|
+
const { res: meKeyRes } = await fetchAs(
|
|
1314
|
+
`${SP_URL2}/api/users/me/pubkey`,
|
|
1315
|
+
{ headers: spCookie ? { Cookie: spCookie } : {}, signal: AbortSignal.timeout(5e3) },
|
|
1316
|
+
sessionPinning
|
|
1317
|
+
);
|
|
835
1318
|
let needsUpdate = false;
|
|
836
1319
|
if (meKeyRes.status === 404) {
|
|
837
1320
|
needsUpdate = true;
|
|
@@ -840,15 +1323,19 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
840
1323
|
needsUpdate = meKeyData.pubkey !== localPubkeyB64;
|
|
841
1324
|
}
|
|
842
1325
|
if (needsUpdate) {
|
|
843
|
-
const putRes = await
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1326
|
+
const { res: putRes } = await fetchAs(
|
|
1327
|
+
`${SP_URL2}/api/users/me/pubkey`,
|
|
1328
|
+
{
|
|
1329
|
+
method: "PUT",
|
|
1330
|
+
headers: {
|
|
1331
|
+
"Content-Type": "application/json",
|
|
1332
|
+
...spCookie ? { Cookie: spCookie } : {}
|
|
1333
|
+
},
|
|
1334
|
+
body: JSON.stringify({ pubkey: localPubkeyB64 }),
|
|
1335
|
+
signal: AbortSignal.timeout(5e3)
|
|
848
1336
|
},
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
});
|
|
1337
|
+
sessionPinning
|
|
1338
|
+
);
|
|
852
1339
|
if (!putRes.ok) {
|
|
853
1340
|
console.error(`[Control Plane] E2EE pubkey registration failed: ${putRes.status}`);
|
|
854
1341
|
} else {
|
|
@@ -858,7 +1345,12 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
858
1345
|
console.error("[Control Plane] E2EE pubkey already up to date");
|
|
859
1346
|
}
|
|
860
1347
|
} catch (err) {
|
|
861
|
-
|
|
1348
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1349
|
+
console.error("[Control Plane] E2EE pubkey auto-register aborted \u2014 TLS pin mismatch:", err.message);
|
|
1350
|
+
lockAsTlsMismatch2();
|
|
1351
|
+
} else {
|
|
1352
|
+
console.error("[Control Plane] E2EE pubkey auto-register failed:", err);
|
|
1353
|
+
}
|
|
862
1354
|
}
|
|
863
1355
|
for (const credId of vault2.listCredentials()) {
|
|
864
1356
|
try {
|
|
@@ -895,12 +1387,29 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
895
1387
|
router.post("/logout", logoutAuth, async (_req, res) => {
|
|
896
1388
|
const cookie = vault2.getSpCookie();
|
|
897
1389
|
vault2.clearKey();
|
|
1390
|
+
const logoutPinning = pairing ? { enforce: resolvePinTls(pairing.dataDir), pinnedSpkiHex: readPairing(pairing.dataDir)?.tlsSpkiPinHex } : { enforce: false };
|
|
898
1391
|
await Promise.allSettled([
|
|
899
1392
|
unconfigureSession(),
|
|
900
|
-
cookie ?
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1393
|
+
cookie ? fetchAs(
|
|
1394
|
+
`${SP_URL2}/api/auth/logout`,
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: { cookie, ...clientVersionHeaders() },
|
|
1398
|
+
redirect: "manual",
|
|
1399
|
+
// Same reasoning as the session call above — this sat with no
|
|
1400
|
+
// timeout of its own before, which would have hung the
|
|
1401
|
+
// `Promise.allSettled` below (and this handler's response)
|
|
1402
|
+
// forever on a dead connection.
|
|
1403
|
+
signal: AbortSignal.timeout(1e4)
|
|
1404
|
+
},
|
|
1405
|
+
logoutPinning
|
|
1406
|
+
).catch((err) => {
|
|
1407
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1408
|
+
console.error("[Control Plane] Logout: TLS pin mismatch talking to the Authority Server \u2014 local sign-out still applied:", err.message);
|
|
1409
|
+
lockAsTlsMismatch2();
|
|
1410
|
+
} else {
|
|
1411
|
+
throw err;
|
|
1412
|
+
}
|
|
904
1413
|
}) : Promise.resolve()
|
|
905
1414
|
]);
|
|
906
1415
|
res.json({ ok: true });
|
|
@@ -1512,18 +2021,18 @@ async function testAIConnectivity(config) {
|
|
|
1512
2021
|
}
|
|
1513
2022
|
|
|
1514
2023
|
// src/lib/agent-brief-store.ts
|
|
1515
|
-
import { existsSync as
|
|
2024
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
1516
2025
|
import { homedir as homedir2 } from "os";
|
|
1517
|
-
import { join as
|
|
2026
|
+
import { join as join5 } from "path";
|
|
1518
2027
|
var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
|
|
1519
2028
|
function agentBriefPath() {
|
|
1520
|
-
const dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
1521
|
-
return
|
|
2029
|
+
const dataDir = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
|
|
2030
|
+
return join5(dataDir, "context.md");
|
|
1522
2031
|
}
|
|
1523
2032
|
function readAgentBrief() {
|
|
1524
2033
|
const filePath = agentBriefPath();
|
|
1525
|
-
if (!
|
|
1526
|
-
return
|
|
2034
|
+
if (!existsSync4(filePath)) return "";
|
|
2035
|
+
return readFileSync6(filePath, "utf-8");
|
|
1527
2036
|
}
|
|
1528
2037
|
|
|
1529
2038
|
// src/routes/ai.ts
|
|
@@ -1887,7 +2396,7 @@ function createMCPRouter() {
|
|
|
1887
2396
|
// src/routes/autostart.ts
|
|
1888
2397
|
import { Router as Router6 } from "express";
|
|
1889
2398
|
import { execFile } from "child_process";
|
|
1890
|
-
import { existsSync as
|
|
2399
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1891
2400
|
import { dirname as pathDirname, resolve } from "path";
|
|
1892
2401
|
import { fileURLToPath } from "url";
|
|
1893
2402
|
import { promisify } from "util";
|
|
@@ -1908,7 +2417,7 @@ function findCli(dirname9 = MODULE_DIR) {
|
|
|
1908
2417
|
// resolves differently between a shell and a login service — and it would
|
|
1909
2418
|
// mask a genuinely missing CLI whenever the cwd happened to be the repo.
|
|
1910
2419
|
];
|
|
1911
|
-
return candidates.find(
|
|
2420
|
+
return candidates.find(existsSync5) ?? null;
|
|
1912
2421
|
}
|
|
1913
2422
|
async function cli(args) {
|
|
1914
2423
|
const bin = findCli();
|
|
@@ -1989,17 +2498,17 @@ function createAutostartRouter() {
|
|
|
1989
2498
|
import { spawn, spawnSync } from "child_process";
|
|
1990
2499
|
|
|
1991
2500
|
// src/lib/notifier-icon.ts
|
|
1992
|
-
import { writeFileSync as
|
|
2501
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
1993
2502
|
import { homedir as homedir3 } from "os";
|
|
1994
|
-
import { join as
|
|
2503
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
1995
2504
|
var ICON_BASE64 = "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";
|
|
1996
2505
|
function ensureNotifierIcon(dataDir) {
|
|
1997
2506
|
try {
|
|
1998
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
1999
|
-
const path =
|
|
2000
|
-
if (!
|
|
2001
|
-
|
|
2002
|
-
|
|
2507
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
|
|
2508
|
+
const path = join6(dir, "notifier-icon.png");
|
|
2509
|
+
if (!existsSync6(path)) {
|
|
2510
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
2511
|
+
writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
|
|
2003
2512
|
}
|
|
2004
2513
|
return path;
|
|
2005
2514
|
} catch {
|
|
@@ -2201,24 +2710,24 @@ import { Router as Router9 } from "express";
|
|
|
2201
2710
|
|
|
2202
2711
|
// src/routes/approved-intents.ts
|
|
2203
2712
|
import { Router as Router8 } from "express";
|
|
2204
|
-
import { readFileSync as
|
|
2713
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
2205
2714
|
import { homedir as homedir4 } from "os";
|
|
2206
|
-
import { join as
|
|
2207
|
-
var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
2208
|
-
var FILE_PATH =
|
|
2715
|
+
import { join as join7 } from "path";
|
|
2716
|
+
var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
|
|
2717
|
+
var FILE_PATH = join7(SUVEREN_DATA_DIR, "approved-intents.enc.json");
|
|
2209
2718
|
function readFile() {
|
|
2210
|
-
if (!
|
|
2719
|
+
if (!existsSync7(FILE_PATH)) {
|
|
2211
2720
|
return { version: 1, entries: {} };
|
|
2212
2721
|
}
|
|
2213
2722
|
try {
|
|
2214
|
-
return JSON.parse(
|
|
2723
|
+
return JSON.parse(readFileSync7(FILE_PATH, "utf-8"));
|
|
2215
2724
|
} catch {
|
|
2216
2725
|
return { version: 1, entries: {} };
|
|
2217
2726
|
}
|
|
2218
2727
|
}
|
|
2219
2728
|
function writeFile(data) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2729
|
+
mkdirSync5(SUVEREN_DATA_DIR, { recursive: true });
|
|
2730
|
+
writeFileSync5(FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
2222
2731
|
}
|
|
2223
2732
|
function readApprovedIntents(vault2) {
|
|
2224
2733
|
const file = readFile();
|
|
@@ -2267,7 +2776,7 @@ function createApprovedIntentsRouter(vault2) {
|
|
|
2267
2776
|
|
|
2268
2777
|
// src/routes/evidence-export.ts
|
|
2269
2778
|
var AS_FETCH_TIMEOUT_MS = 2e4;
|
|
2270
|
-
function createEvidenceExportRouter(spUrl, vault2) {
|
|
2779
|
+
function createEvidenceExportRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2) {
|
|
2271
2780
|
const router = Router9();
|
|
2272
2781
|
const getSpCookie = () => vault2.getSpCookie();
|
|
2273
2782
|
router.get("/", async (_req, res) => {
|
|
@@ -2287,17 +2796,29 @@ function createEvidenceExportRouter(spUrl, vault2) {
|
|
|
2287
2796
|
const controller = new AbortController();
|
|
2288
2797
|
const timeout = setTimeout(() => controller.abort(), AS_FETCH_TIMEOUT_MS);
|
|
2289
2798
|
try {
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2799
|
+
const pin = readPairing(dataDir);
|
|
2800
|
+
const { res: asRes } = await fetchAs(
|
|
2801
|
+
`${spUrl}/api/receipts/export`,
|
|
2802
|
+
{ headers: { Cookie: cookie }, signal: controller.signal },
|
|
2803
|
+
{
|
|
2804
|
+
enforce: resolvePinTls(dataDir),
|
|
2805
|
+
pinnedSpkiHex: pin && pin.asUrl === spUrl ? pin.tlsSpkiPinHex : void 0
|
|
2806
|
+
// Never capture here — only the sign-in challenge establishes or
|
|
2807
|
+
// refreshes a pin (as-challenge.ts).
|
|
2808
|
+
}
|
|
2809
|
+
);
|
|
2294
2810
|
if (asRes.ok) {
|
|
2295
2811
|
authorityServer = await asRes.json();
|
|
2296
2812
|
} else {
|
|
2297
2813
|
asError = `Authority Server export failed: ${asRes.status}`;
|
|
2298
2814
|
}
|
|
2299
2815
|
} catch (err) {
|
|
2300
|
-
|
|
2816
|
+
if (err instanceof AsTlsMismatchError) {
|
|
2817
|
+
lockAsTlsMismatch2();
|
|
2818
|
+
asError = err.message;
|
|
2819
|
+
} else {
|
|
2820
|
+
asError = `Authority Server unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
2821
|
+
}
|
|
2301
2822
|
} finally {
|
|
2302
2823
|
clearTimeout(timeout);
|
|
2303
2824
|
}
|
|
@@ -2483,7 +3004,7 @@ function createDecryptIntentRouter(vault2) {
|
|
|
2483
3004
|
|
|
2484
3005
|
// src/lib/update-checker.ts
|
|
2485
3006
|
import { execSync } from "child_process";
|
|
2486
|
-
import { dirname as
|
|
3007
|
+
import { dirname as dirname5 } from "path";
|
|
2487
3008
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2488
3009
|
var GHCR_IMAGE = "suverenai/suveren-gateway";
|
|
2489
3010
|
var NPM_PACKAGE = "@suveren/gateway";
|
|
@@ -2549,7 +3070,7 @@ function compareSemver(a, b) {
|
|
|
2549
3070
|
return 0;
|
|
2550
3071
|
}
|
|
2551
3072
|
function checkDev() {
|
|
2552
|
-
const cwd =
|
|
3073
|
+
const cwd = dirname5(fileURLToPath2(import.meta.url));
|
|
2553
3074
|
try {
|
|
2554
3075
|
execSync("git fetch origin --quiet", { cwd, stdio: "ignore", timeout: 15e3 });
|
|
2555
3076
|
const out = execSync("git rev-list HEAD..origin/main --count", {
|
|
@@ -2622,19 +3143,19 @@ data: ${data}
|
|
|
2622
3143
|
import { Router as Router12 } from "express";
|
|
2623
3144
|
|
|
2624
3145
|
// src/lib/gateway-settings.ts
|
|
2625
|
-
import { readFileSync as
|
|
3146
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
2626
3147
|
import { homedir as homedir5 } from "os";
|
|
2627
|
-
import { join as
|
|
3148
|
+
import { join as join8, dirname as dirname6 } from "path";
|
|
2628
3149
|
var DEFAULT_SETTINGS = {
|
|
2629
3150
|
desktopNotifications: true
|
|
2630
3151
|
};
|
|
2631
3152
|
function settingsPath(dataDir) {
|
|
2632
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
2633
|
-
return
|
|
3153
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
|
|
3154
|
+
return join8(dir, "settings.json");
|
|
2634
3155
|
}
|
|
2635
3156
|
function readSettings(dataDir) {
|
|
2636
3157
|
try {
|
|
2637
|
-
const raw = JSON.parse(
|
|
3158
|
+
const raw = JSON.parse(readFileSync8(settingsPath(dataDir), "utf8"));
|
|
2638
3159
|
return {
|
|
2639
3160
|
desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
|
|
2640
3161
|
};
|
|
@@ -2648,8 +3169,8 @@ function writeSettings(patch, dataDir) {
|
|
|
2648
3169
|
...typeof patch.desktopNotifications === "boolean" ? { desktopNotifications: patch.desktopNotifications } : {}
|
|
2649
3170
|
};
|
|
2650
3171
|
const path = settingsPath(dataDir);
|
|
2651
|
-
|
|
2652
|
-
|
|
3172
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
3173
|
+
writeFileSync6(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2653
3174
|
return next;
|
|
2654
3175
|
}
|
|
2655
3176
|
|
|
@@ -2681,20 +3202,20 @@ function createGatewaySettingsRouter() {
|
|
|
2681
3202
|
import { Router as Router13 } from "express";
|
|
2682
3203
|
|
|
2683
3204
|
// src/lib/archived-mandates-store.ts
|
|
2684
|
-
import { readFileSync as
|
|
3205
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
2685
3206
|
import { homedir as homedir6 } from "os";
|
|
2686
|
-
import { join as
|
|
3207
|
+
import { join as join9, dirname as dirname7 } from "path";
|
|
2687
3208
|
var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
2688
3209
|
function isValidMandateId(id) {
|
|
2689
3210
|
return typeof id === "string" && ID_PATTERN.test(id);
|
|
2690
3211
|
}
|
|
2691
3212
|
function storePath(dataDir) {
|
|
2692
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
2693
|
-
return
|
|
3213
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
|
|
3214
|
+
return join9(dir, "archived-mandates.json");
|
|
2694
3215
|
}
|
|
2695
3216
|
function readArchived(dataDir) {
|
|
2696
3217
|
try {
|
|
2697
|
-
const raw = JSON.parse(
|
|
3218
|
+
const raw = JSON.parse(readFileSync9(storePath(dataDir), "utf8"));
|
|
2698
3219
|
if (!Array.isArray(raw.archived)) return [];
|
|
2699
3220
|
return [...new Set(raw.archived.filter(isValidMandateId))];
|
|
2700
3221
|
} catch {
|
|
@@ -2704,9 +3225,9 @@ function readArchived(dataDir) {
|
|
|
2704
3225
|
function writeArchived(ids, dataDir) {
|
|
2705
3226
|
const next = [...new Set(ids.filter(isValidMandateId))].sort();
|
|
2706
3227
|
const path = storePath(dataDir);
|
|
2707
|
-
|
|
3228
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
2708
3229
|
const tmp = `${path}.tmp`;
|
|
2709
|
-
|
|
3230
|
+
writeFileSync7(tmp, JSON.stringify({ archived: next }, null, 2) + "\n", "utf8");
|
|
2710
3231
|
renameSync(tmp, path);
|
|
2711
3232
|
return next;
|
|
2712
3233
|
}
|
|
@@ -2748,6 +3269,7 @@ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
|
2748
3269
|
var ALLOWED = ["proposal-added", "action-approval-needed"];
|
|
2749
3270
|
var SESSION_EXPIRED = "session-expired";
|
|
2750
3271
|
var AS_KEY_MISMATCH = "as-key-mismatch";
|
|
3272
|
+
var AS_TLS_MISMATCH = "as-tls-mismatch";
|
|
2751
3273
|
function secretMatches(provided, expected) {
|
|
2752
3274
|
if (!expected) return false;
|
|
2753
3275
|
if (!provided) return false;
|
|
@@ -2756,7 +3278,7 @@ function secretMatches(provided, expected) {
|
|
|
2756
3278
|
if (a.length !== b.length) return false;
|
|
2757
3279
|
return timingSafeEqual2(a, b);
|
|
2758
3280
|
}
|
|
2759
|
-
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch) {
|
|
3281
|
+
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch, onAsTlsMismatch) {
|
|
2760
3282
|
const router = Router14();
|
|
2761
3283
|
router.post("/event", (req, res) => {
|
|
2762
3284
|
if (!secretMatches(req.headers["x-internal-secret"], getSecret())) {
|
|
@@ -2774,6 +3296,11 @@ function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch
|
|
|
2774
3296
|
res.json({ ok: true });
|
|
2775
3297
|
return;
|
|
2776
3298
|
}
|
|
3299
|
+
if (type === AS_TLS_MISMATCH) {
|
|
3300
|
+
onAsTlsMismatch?.();
|
|
3301
|
+
res.json({ ok: true });
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
2777
3304
|
if (typeof type !== "string" || !ALLOWED.includes(type)) {
|
|
2778
3305
|
res.status(400).json({ error: "Unsupported event type" });
|
|
2779
3306
|
return;
|
|
@@ -2873,6 +3400,7 @@ function startNotificationDispatcher(opts = {}) {
|
|
|
2873
3400
|
// src/lib/session-lock.ts
|
|
2874
3401
|
var SESSION_LOCKED_MESSAGE = "Your sign-in ended after 30 days or was revoked. Sign in again.";
|
|
2875
3402
|
var AS_KEY_MISMATCH_MESSAGE = "The Authority Server presented a signing key that does not match the one pinned at pairing. Sign in again to review.";
|
|
3403
|
+
var AS_TLS_MISMATCH_MESSAGE = "A connection to the Authority Server presented a TLS certificate that does not match the one pinned at pairing. The connection was refused before anything was sent. If the certificate changed intentionally (a new key, not just renewal), clear the pairing and sign in again to re-pin it.";
|
|
2876
3404
|
function createSessionLock(deps) {
|
|
2877
3405
|
const { vault: vault2, port: port2 } = deps;
|
|
2878
3406
|
const notifyFn = deps.notifyFn ?? notify;
|
|
@@ -2907,6 +3435,23 @@ function createAsKeyMismatchLock(deps) {
|
|
|
2907
3435
|
notifyFn(title, AS_KEY_MISMATCH_MESSAGE, process.platform, url);
|
|
2908
3436
|
};
|
|
2909
3437
|
}
|
|
3438
|
+
function createAsTlsMismatchLock(deps) {
|
|
3439
|
+
const { vault: vault2, port: port2 } = deps;
|
|
3440
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
3441
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
3442
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
3443
|
+
return function lockOnAsTlsMismatch() {
|
|
3444
|
+
if (!vault2.isUnlocked()) return;
|
|
3445
|
+
vault2.lockAsTlsMismatch();
|
|
3446
|
+
console.error("[Control Plane] Authority Server TLS pin mismatch \u2014 gateway LOCKED");
|
|
3447
|
+
void unconfigureSessionFn("as-tls-mismatch").catch((err) => {
|
|
3448
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
3449
|
+
});
|
|
3450
|
+
emit("session-locked", { reason: "as-tls-mismatch", message: AS_TLS_MISMATCH_MESSAGE });
|
|
3451
|
+
const { title, url } = sessionExpiredNotification(port2);
|
|
3452
|
+
notifyFn(title, AS_TLS_MISMATCH_MESSAGE, process.platform, url);
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
2910
3455
|
|
|
2911
3456
|
// src/lib/session-expiry-scheduler.ts
|
|
2912
3457
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
@@ -2976,17 +3521,17 @@ function buildSessionHealth(vault2) {
|
|
|
2976
3521
|
}
|
|
2977
3522
|
|
|
2978
3523
|
// src/lib/denials-reader.ts
|
|
2979
|
-
import { existsSync as
|
|
2980
|
-
import { join as
|
|
3524
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
|
|
3525
|
+
import { join as join10 } from "path";
|
|
2981
3526
|
function loadDenials(dataDir, decrypt) {
|
|
2982
|
-
const encPath =
|
|
2983
|
-
const plainPath =
|
|
2984
|
-
if (
|
|
2985
|
-
const { blob } = JSON.parse(
|
|
3527
|
+
const encPath = join10(dataDir, "denials.enc.json");
|
|
3528
|
+
const plainPath = join10(dataDir, "denials.json");
|
|
3529
|
+
if (existsSync8(encPath)) {
|
|
3530
|
+
const { blob } = JSON.parse(readFileSync10(encPath, "utf-8"));
|
|
2986
3531
|
return JSON.parse(decrypt(blob)).records ?? [];
|
|
2987
3532
|
}
|
|
2988
|
-
if (
|
|
2989
|
-
return JSON.parse(
|
|
3533
|
+
if (existsSync8(plainPath)) {
|
|
3534
|
+
return JSON.parse(readFileSync10(plainPath, "utf-8")).records ?? [];
|
|
2990
3535
|
}
|
|
2991
3536
|
return [];
|
|
2992
3537
|
}
|
|
@@ -3000,74 +3545,6 @@ function selectDenials(records, opts = {}) {
|
|
|
3000
3545
|
return { count, records: out };
|
|
3001
3546
|
}
|
|
3002
3547
|
|
|
3003
|
-
// src/lib/as-config.ts
|
|
3004
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
3005
|
-
import { dirname as dirname7, join as join10 } from "path";
|
|
3006
|
-
var DEFAULT_AS_URL = "https://www.suveren.ai";
|
|
3007
|
-
function configPath(dataDir) {
|
|
3008
|
-
return join10(dataDir, "config.json");
|
|
3009
|
-
}
|
|
3010
|
-
function readAsConfigStrict(dataDir) {
|
|
3011
|
-
const path = configPath(dataDir);
|
|
3012
|
-
if (!existsSync8(path)) return {};
|
|
3013
|
-
let data;
|
|
3014
|
-
try {
|
|
3015
|
-
data = JSON.parse(readFileSync9(path, "utf-8"));
|
|
3016
|
-
} catch (err) {
|
|
3017
|
-
throw new Error(
|
|
3018
|
-
`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
3019
|
-
);
|
|
3020
|
-
}
|
|
3021
|
-
const out = {};
|
|
3022
|
-
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
3023
|
-
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
3024
|
-
return out;
|
|
3025
|
-
}
|
|
3026
|
-
function validateAsUrl(candidate) {
|
|
3027
|
-
const trimmed = candidate.trim();
|
|
3028
|
-
if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
|
|
3029
|
-
let parsed;
|
|
3030
|
-
try {
|
|
3031
|
-
parsed = new URL(trimmed);
|
|
3032
|
-
} catch {
|
|
3033
|
-
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
3034
|
-
}
|
|
3035
|
-
const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
3036
|
-
if (parsed.protocol !== "https:" && !isLocalHttp) {
|
|
3037
|
-
return {
|
|
3038
|
-
ok: false,
|
|
3039
|
-
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
|
|
3040
|
-
};
|
|
3041
|
-
}
|
|
3042
|
-
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
3043
|
-
if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
|
|
3044
|
-
normalized = normalized.slice(0, -1);
|
|
3045
|
-
} else if (normalized === `${parsed.origin}/`) {
|
|
3046
|
-
normalized = parsed.origin;
|
|
3047
|
-
}
|
|
3048
|
-
return { ok: true, url: normalized };
|
|
3049
|
-
}
|
|
3050
|
-
function resolveAsUrl(dataDir, flag) {
|
|
3051
|
-
if (flag) {
|
|
3052
|
-
const v = validateAsUrl(flag);
|
|
3053
|
-
if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
|
|
3054
|
-
return v.url;
|
|
3055
|
-
}
|
|
3056
|
-
const envUrl = process.env.SUVEREN_AS_URL;
|
|
3057
|
-
if (envUrl) {
|
|
3058
|
-
const v = validateAsUrl(envUrl);
|
|
3059
|
-
if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
|
|
3060
|
-
return v.url;
|
|
3061
|
-
}
|
|
3062
|
-
const saved = readAsConfigStrict(dataDir).asUrl;
|
|
3063
|
-
if (saved) {
|
|
3064
|
-
const v = validateAsUrl(saved);
|
|
3065
|
-
if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir}/config.json: ${v.error}`);
|
|
3066
|
-
return v.url;
|
|
3067
|
-
}
|
|
3068
|
-
return DEFAULT_AS_URL;
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
3548
|
// src/index.ts
|
|
3072
3549
|
(function migrateDataDir() {
|
|
3073
3550
|
if (process.env.SUVEREN_DATA_DIR) return;
|
|
@@ -3085,6 +3562,7 @@ function resolveAsUrl(dataDir, flag) {
|
|
|
3085
3562
|
})();
|
|
3086
3563
|
var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
|
|
3087
3564
|
var SP_URL = resolveAsUrl(DATA_DIR);
|
|
3565
|
+
validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
|
|
3088
3566
|
var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
|
|
3089
3567
|
var HAP_MODE = process.env.HAP_MODE ?? "personal";
|
|
3090
3568
|
var UI_DIST = process.env.HAP_UI_DIST ?? join11(import.meta.dirname ?? __dirname, "../../ui/dist");
|
|
@@ -3099,7 +3577,7 @@ var vault = new Vault(DATA_DIR);
|
|
|
3099
3577
|
vault.markBootLockedAsUrlChanged();
|
|
3100
3578
|
}
|
|
3101
3579
|
})();
|
|
3102
|
-
var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ??
|
|
3580
|
+
var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ?? randomBytes3(32).toString("hex");
|
|
3103
3581
|
setInternalSecret(internalSecret2);
|
|
3104
3582
|
var app = express();
|
|
3105
3583
|
var jsonParser = express.json();
|
|
@@ -3123,6 +3601,7 @@ function loginRateLimit(req, res, next) {
|
|
|
3123
3601
|
}
|
|
3124
3602
|
var lockExpiredSession = createSessionLock({ vault, port });
|
|
3125
3603
|
var lockAsKeyMismatch = createAsKeyMismatchLock({ vault, port });
|
|
3604
|
+
var lockAsTlsMismatch = createAsTlsMismatchLock({ vault, port });
|
|
3126
3605
|
var sessionExpiryScheduler = new SessionExpiryScheduler(
|
|
3127
3606
|
() => vault.getSessionExpiresAt(),
|
|
3128
3607
|
lockExpiredSession
|
|
@@ -3339,7 +3818,7 @@ app.get("/auth/oauth/:integrationId/health", authGuard, async (req, res) => {
|
|
|
3339
3818
|
}
|
|
3340
3819
|
});
|
|
3341
3820
|
app.get("/events", requireAllowedHost, requireAuthQueryOrHeader(vault), createEventsHandler());
|
|
3342
|
-
app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession, lockAsKeyMismatch));
|
|
3821
|
+
app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession, lockAsKeyMismatch, lockAsTlsMismatch));
|
|
3343
3822
|
app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
|
|
3344
3823
|
app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
|
|
3345
3824
|
app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
|
|
@@ -3348,7 +3827,7 @@ app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
|
|
|
3348
3827
|
app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRouter());
|
|
3349
3828
|
app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
|
|
3350
3829
|
app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
|
|
3351
|
-
app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault));
|
|
3830
|
+
app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
|
|
3352
3831
|
app.use("/api/evidence", authGuard, createEvidenceRouter());
|
|
3353
3832
|
app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
|
|
3354
3833
|
app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
|
|
@@ -3489,11 +3968,18 @@ app.get("/skipped-commitments", authGuard, async (_req, res) => {
|
|
|
3489
3968
|
});
|
|
3490
3969
|
app.get("/as-pairing", authGuard, (_req, res) => {
|
|
3491
3970
|
const pairing = readPairing(DATA_DIR);
|
|
3971
|
+
const paired = pairing !== null && pairing.asUrl === SP_URL;
|
|
3492
3972
|
res.json({
|
|
3493
3973
|
asUrl: SP_URL,
|
|
3494
|
-
paired
|
|
3495
|
-
fingerprint:
|
|
3496
|
-
pairedAt:
|
|
3974
|
+
paired,
|
|
3975
|
+
fingerprint: paired ? fingerprintOf(pairing.publicKeyHex) : null,
|
|
3976
|
+
pairedAt: paired ? pairing.pairedAt : null,
|
|
3977
|
+
// Opt-in TLS pinning (`config set pin-tls on`) — see as-tls-pin.ts.
|
|
3978
|
+
// `tlsPinFingerprint` uses the SAME display convention as the signing
|
|
3979
|
+
// key (formatPinFingerprint mirrors fingerprintOf's grouping), applied
|
|
3980
|
+
// to the already-computed SPKI digest rather than re-hashing a key.
|
|
3981
|
+
pinTlsEnabled: resolvePinTls(DATA_DIR),
|
|
3982
|
+
tlsPinFingerprint: paired && pairing.tlsSpkiPinHex ? formatPinFingerprint(pairing.tlsSpkiPinHex) : null
|
|
3497
3983
|
});
|
|
3498
3984
|
});
|
|
3499
3985
|
app.get("/denials", authGuard, (req, res) => {
|
|
@@ -3562,11 +4048,19 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
|
|
|
3562
4048
|
}
|
|
3563
4049
|
});
|
|
3564
4050
|
app.use("/api", authGuard);
|
|
4051
|
+
var pinnedProxyAgent = SP_URL.startsWith("https:") ? buildPinnedHttpsAgent(() => {
|
|
4052
|
+
const pin = readPairing(DATA_DIR);
|
|
4053
|
+
return {
|
|
4054
|
+
enforce: resolvePinTls(DATA_DIR),
|
|
4055
|
+
pinnedSpkiHex: pin && pin.asUrl === SP_URL ? pin.tlsSpkiPinHex : void 0
|
|
4056
|
+
};
|
|
4057
|
+
}) : void 0;
|
|
3565
4058
|
app.use(
|
|
3566
4059
|
createProxyMiddleware({
|
|
3567
4060
|
target: SP_URL,
|
|
3568
4061
|
changeOrigin: true,
|
|
3569
4062
|
pathFilter: "/api",
|
|
4063
|
+
...pinnedProxyAgent ? { agent: pinnedProxyAgent } : {},
|
|
3570
4064
|
on: {
|
|
3571
4065
|
proxyReq: (proxyReq) => {
|
|
3572
4066
|
const cookie = vault.getSpCookie();
|
|
@@ -3574,6 +4068,21 @@ app.use(
|
|
|
3574
4068
|
proxyReq.setHeader("Cookie", cookie);
|
|
3575
4069
|
}
|
|
3576
4070
|
},
|
|
4071
|
+
// A pin check failure means the connection never completed its TLS
|
|
4072
|
+
// handshake — nothing (not even this call's own cookie) reached the
|
|
4073
|
+
// network. Lock the gateway with the real reason, loudly; an ordinary
|
|
4074
|
+
// network error (AS outage) is NOT a pin failure (see isPinCheckError
|
|
4075
|
+
// / as-tls-pin.ts's module doc comment on outage misclassification)
|
|
4076
|
+
// and falls through to http-proxy-middleware's default error handling.
|
|
4077
|
+
error: (err, _req, res) => {
|
|
4078
|
+
if (!isPinCheckError(err)) return;
|
|
4079
|
+
console.error("[Control Plane] /api proxy: TLS pin mismatch \u2014 gateway LOCKED:", err.message);
|
|
4080
|
+
lockAsTlsMismatch();
|
|
4081
|
+
if (res && "writeHead" in res && !res.headersSent) {
|
|
4082
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
4083
|
+
res.end(JSON.stringify({ error: "as_tls_mismatch", message: err.message }));
|
|
4084
|
+
}
|
|
4085
|
+
},
|
|
3577
4086
|
proxyRes: (proxyRes, req) => {
|
|
3578
4087
|
const status = proxyRes.statusCode ?? 0;
|
|
3579
4088
|
if (status === 401 && vault.isUnlocked()) {
|
|
@@ -3627,7 +4136,7 @@ function detectRunningVersion() {
|
|
|
3627
4136
|
const bundlePkg = join11(dir, "..", "..", "package.json");
|
|
3628
4137
|
if (existsSync9(bundlePkg)) {
|
|
3629
4138
|
try {
|
|
3630
|
-
const pkg = JSON.parse(
|
|
4139
|
+
const pkg = JSON.parse(readFileSync11(bundlePkg, "utf8"));
|
|
3631
4140
|
if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
|
|
3632
4141
|
return pkg.version;
|
|
3633
4142
|
}
|