@suveren/gateway 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/bin/suveren-gateway.js +203 -5
- package/content/integrations/erp.json +184 -0
- package/content/integrations/index.json +2 -1
- package/dist/control-plane/index.mjs +653 -179
- package/dist/mcp-server/http.mjs +466 -259
- package/dist/ui/assets/{index-CIiOCgjJ.js → index-B1qNl7zo.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/lib/as-pairing.mjs +77 -0
- package/lib/config.mjs +48 -0
- 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
|
@@ -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,450 @@ 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
|
+
async function fetchAs(url, init, pinning) {
|
|
835
|
+
if (!pinning.enforce && !pinning.capture) {
|
|
836
|
+
const res2 = await fetch(url, init);
|
|
837
|
+
return { res: res2 };
|
|
838
|
+
}
|
|
839
|
+
if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
|
|
840
|
+
throw new AsTlsMismatchError(
|
|
841
|
+
`pin-tls is on but no certificate is pinned yet for ${url} \u2014 sign in again to establish it (re-pairing required).`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
|
|
845
|
+
const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
|
|
846
|
+
try {
|
|
847
|
+
const res2 = await undiciFetch(url, { ...init, dispatcher: dispatcher2 });
|
|
848
|
+
return { res: res2 };
|
|
849
|
+
} catch (err) {
|
|
850
|
+
if (!isPinCheckError(err)) throw err;
|
|
851
|
+
throw new AsTlsMismatchError(
|
|
852
|
+
`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.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
|
|
857
|
+
let res;
|
|
858
|
+
try {
|
|
859
|
+
res = await undiciFetch(url, { ...init, dispatcher });
|
|
860
|
+
} catch (err) {
|
|
861
|
+
if (!isPinCheckError(err)) throw err;
|
|
862
|
+
throw new AsTlsMismatchError(
|
|
863
|
+
`could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
const cert = getCapturedCert();
|
|
867
|
+
return { res, capturedSpkiHex: cert ? spkiSha256Hex(cert) : void 0 };
|
|
868
|
+
}
|
|
869
|
+
function describeFetchError(err) {
|
|
870
|
+
if (err instanceof Error) {
|
|
871
|
+
const cause = err.cause;
|
|
872
|
+
if (cause instanceof Error) return cause.message;
|
|
873
|
+
return err.message;
|
|
874
|
+
}
|
|
875
|
+
return String(err);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/lib/as-challenge.ts
|
|
879
|
+
var AsChallengeUnreachableError = class extends Error {
|
|
880
|
+
constructor(message) {
|
|
881
|
+
super(message);
|
|
882
|
+
this.name = "AsChallengeUnreachableError";
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var AsChallengeInvalidError = class extends Error {
|
|
886
|
+
constructor(message) {
|
|
887
|
+
super(message);
|
|
888
|
+
this.name = "AsChallengeInvalidError";
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
var CHALLENGE_MAX_SKEW_SECONDS = 300;
|
|
892
|
+
var CHALLENGE_TYPE = "hap-as-challenge";
|
|
893
|
+
var CHALLENGE_DOMAIN_PREFIX = "hap-as-challenge\0";
|
|
894
|
+
function challengeSigningBytes(unsigned) {
|
|
895
|
+
return Buffer.concat([
|
|
896
|
+
Buffer.from(CHALLENGE_DOMAIN_PREFIX, "utf-8"),
|
|
897
|
+
Buffer.from(canonicalize({ typ: unsigned.typ, nonce: unsigned.nonce, issuedAt: unsigned.issuedAt }), "utf-8")
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
function publicKeyFromHex(publicKeyHex) {
|
|
901
|
+
const raw = Buffer.from(publicKeyHex, "hex");
|
|
902
|
+
return createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") }, format: "jwk" });
|
|
903
|
+
}
|
|
904
|
+
function verifyChallengeSignature(publicKeyHex, unsigned, signature) {
|
|
905
|
+
try {
|
|
906
|
+
const base64 = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
907
|
+
const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
|
|
908
|
+
const sigBytes = Buffer.from(base64 + padding, "base64");
|
|
909
|
+
const key = publicKeyFromHex(publicKeyHex);
|
|
910
|
+
return cryptoVerify(null, challengeSigningBytes(unsigned), key, sigBytes);
|
|
911
|
+
} catch {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async function verifyAsHoldsKey(asUrl, publicKeyHex, pinning = { enforce: false, capture: true }) {
|
|
916
|
+
const nonce = randomBytes2(32).toString("base64url");
|
|
917
|
+
let res;
|
|
918
|
+
let capturedSpkiHex;
|
|
919
|
+
try {
|
|
920
|
+
const result = await fetchAs(
|
|
921
|
+
`${asUrl}/api/as/challenge`,
|
|
922
|
+
{
|
|
923
|
+
method: "POST",
|
|
924
|
+
headers: { "Content-Type": "application/json" },
|
|
925
|
+
body: JSON.stringify({ nonce }),
|
|
926
|
+
signal: AbortSignal.timeout(5e3)
|
|
927
|
+
},
|
|
928
|
+
pinning
|
|
929
|
+
);
|
|
930
|
+
res = result.res;
|
|
931
|
+
capturedSpkiHex = result.capturedSpkiHex;
|
|
932
|
+
} catch (err) {
|
|
933
|
+
if (err instanceof AsTlsMismatchError) throw err;
|
|
934
|
+
throw new AsChallengeUnreachableError(
|
|
935
|
+
`could not reach the Authority Server at ${asUrl} to verify it holds its signing key \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (!res.ok) {
|
|
939
|
+
throw new AsChallengeUnreachableError(
|
|
940
|
+
`the Authority Server at ${asUrl} answered its signing-key challenge with HTTP ${res.status}`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const body = await res.json().catch(() => null);
|
|
944
|
+
if (!body) {
|
|
945
|
+
throw new AsChallengeUnreachableError(
|
|
946
|
+
`the Authority Server at ${asUrl} returned a malformed response to its signing-key challenge`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (body.typ !== CHALLENGE_TYPE) {
|
|
950
|
+
throw new AsChallengeInvalidError(
|
|
951
|
+
`the challenge response carries the wrong type ("${String(body.typ)}", expected "${CHALLENGE_TYPE}")`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
if (body.nonce !== nonce) {
|
|
955
|
+
throw new AsChallengeInvalidError(
|
|
956
|
+
"the challenge response echoes a different nonce than the one sent"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
if (typeof body.issuedAt !== "number" || !Number.isFinite(body.issuedAt)) {
|
|
960
|
+
throw new AsChallengeInvalidError("the challenge response carries no valid issuedAt");
|
|
961
|
+
}
|
|
962
|
+
const skew = Math.abs(Math.floor(Date.now() / 1e3) - body.issuedAt);
|
|
963
|
+
if (skew > CHALLENGE_MAX_SKEW_SECONDS) {
|
|
964
|
+
throw new AsChallengeInvalidError(
|
|
965
|
+
`the challenge response is timestamped ${skew}s away from now, beyond the ${CHALLENGE_MAX_SKEW_SECONDS}s tolerance`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (typeof body.signature !== "string" || !body.signature) {
|
|
969
|
+
throw new AsChallengeInvalidError("the challenge response carries no signature");
|
|
970
|
+
}
|
|
971
|
+
if (!verifyChallengeSignature(publicKeyHex, { typ: body.typ, nonce: body.nonce, issuedAt: body.issuedAt }, body.signature)) {
|
|
972
|
+
throw new AsChallengeInvalidError(
|
|
973
|
+
"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."
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
return { capturedTlsSpkiHex: capturedSpkiHex };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/lib/as-config.ts
|
|
980
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
981
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
982
|
+
var DEFAULT_AS_URL = "https://www.suveren.ai";
|
|
983
|
+
function configPath(dataDir) {
|
|
984
|
+
return join4(dataDir, "config.json");
|
|
985
|
+
}
|
|
986
|
+
function readAsConfigStrict(dataDir) {
|
|
987
|
+
const path = configPath(dataDir);
|
|
988
|
+
if (!existsSync3(path)) return {};
|
|
989
|
+
let data;
|
|
990
|
+
try {
|
|
991
|
+
data = JSON.parse(readFileSync5(path, "utf-8"));
|
|
992
|
+
} catch (err) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const out = {};
|
|
998
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
999
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
1000
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
1001
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
1004
|
+
function readAsConfig(dataDir) {
|
|
1005
|
+
const path = configPath(dataDir);
|
|
1006
|
+
if (!existsSync3(path)) return {};
|
|
1007
|
+
try {
|
|
1008
|
+
const data = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1009
|
+
const out = {};
|
|
1010
|
+
if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
|
|
1011
|
+
if (typeof data.caFile === "string") out.caFile = data.caFile;
|
|
1012
|
+
if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
|
|
1013
|
+
if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
|
|
1014
|
+
return out;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function writeAsConfig(dataDir, patch) {
|
|
1020
|
+
const merged = { ...readAsConfig(dataDir), ...patch };
|
|
1021
|
+
const path = configPath(dataDir);
|
|
1022
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
|
|
1023
|
+
writeFileSync3(path, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1024
|
+
return merged;
|
|
1025
|
+
}
|
|
1026
|
+
function validateAsUrl(candidate) {
|
|
1027
|
+
const trimmed = candidate.trim();
|
|
1028
|
+
if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
|
|
1029
|
+
let parsed;
|
|
1030
|
+
try {
|
|
1031
|
+
parsed = new URL(trimmed);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return { ok: false, error: `"${trimmed}" is not a valid URL.` };
|
|
1034
|
+
}
|
|
1035
|
+
const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
1036
|
+
if (parsed.protocol !== "https:" && !isLocalHttp) {
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
1043
|
+
if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
|
|
1044
|
+
normalized = normalized.slice(0, -1);
|
|
1045
|
+
} else if (normalized === `${parsed.origin}/`) {
|
|
1046
|
+
normalized = parsed.origin;
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true, url: normalized };
|
|
1049
|
+
}
|
|
1050
|
+
function resolveAsUrl(dataDir, flag) {
|
|
1051
|
+
if (flag) {
|
|
1052
|
+
const v = validateAsUrl(flag);
|
|
1053
|
+
if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
|
|
1054
|
+
return v.url;
|
|
1055
|
+
}
|
|
1056
|
+
const envUrl = process.env.SUVEREN_AS_URL;
|
|
1057
|
+
if (envUrl) {
|
|
1058
|
+
const v = validateAsUrl(envUrl);
|
|
1059
|
+
if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
|
|
1060
|
+
return v.url;
|
|
1061
|
+
}
|
|
1062
|
+
const saved = readAsConfigStrict(dataDir).asUrl;
|
|
1063
|
+
if (saved) {
|
|
1064
|
+
const v = validateAsUrl(saved);
|
|
1065
|
+
if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir}/config.json: ${v.error}`);
|
|
1066
|
+
return v.url;
|
|
1067
|
+
}
|
|
1068
|
+
return DEFAULT_AS_URL;
|
|
1069
|
+
}
|
|
1070
|
+
function resolvePinTls(dataDir) {
|
|
1071
|
+
return readAsConfig(dataDir).pinTls === true;
|
|
1072
|
+
}
|
|
1073
|
+
function validatePinTlsForUrl(pinTls, asUrl) {
|
|
1074
|
+
if (!pinTls) return;
|
|
1075
|
+
if (!asUrl.startsWith("https:")) {
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`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\`.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function resolvePinTlsExpectedFingerprint(dataDir) {
|
|
1082
|
+
return readAsConfig(dataDir).pinTlsExpectedFingerprint;
|
|
1083
|
+
}
|
|
1084
|
+
function clearPinTlsExpectedFingerprint(dataDir) {
|
|
1085
|
+
if (readAsConfig(dataDir).pinTlsExpectedFingerprint === void 0) return;
|
|
1086
|
+
writeAsConfig(dataDir, { pinTlsExpectedFingerprint: void 0 });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
701
1089
|
// src/routes/auth.ts
|
|
702
1090
|
var DEFAULT_SP_URL = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
|
|
703
|
-
async function
|
|
704
|
-
let publicKeyHex;
|
|
1091
|
+
async function fetchAsPublicKey(asUrl) {
|
|
705
1092
|
try {
|
|
706
1093
|
const res = await fetch(`${asUrl}/api/as/pubkey`, { signal: AbortSignal.timeout(5e3) });
|
|
707
1094
|
if (!res.ok) {
|
|
708
1095
|
return {
|
|
709
1096
|
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.`
|
|
1097
|
+
message: `Could not reach the Authority Server at ${asUrl} to read its signing key (HTTP ${res.status}). Refusing to sign in.`
|
|
712
1098
|
};
|
|
713
1099
|
}
|
|
714
1100
|
const body = await res.json().catch(() => null);
|
|
715
1101
|
if (!body || typeof body.publicKey !== "string" || !body.publicKey) {
|
|
716
1102
|
return {
|
|
717
1103
|
ok: false,
|
|
718
|
-
error: "as_unreachable",
|
|
719
1104
|
message: `The Authority Server at ${asUrl} returned a malformed signing key. Refusing to sign in.`
|
|
720
1105
|
};
|
|
721
1106
|
}
|
|
722
|
-
publicKeyHex
|
|
1107
|
+
return { ok: true, publicKeyHex: body.publicKey };
|
|
723
1108
|
} catch (err) {
|
|
724
1109
|
return {
|
|
725
1110
|
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.`
|
|
1111
|
+
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
1112
|
};
|
|
729
1113
|
}
|
|
1114
|
+
}
|
|
1115
|
+
async function checkAsKeyBeforeLogin(asUrl, dataDir) {
|
|
730
1116
|
const existing = readPairing(dataDir);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1117
|
+
const pinnedKey = existing && existing.asUrl === asUrl ? existing.publicKeyHex : void 0;
|
|
1118
|
+
const existingTlsPin = (existing && existing.asUrl === asUrl ? existing.tlsSpkiPinHex : void 0) ?? resolvePinTlsExpectedFingerprint(dataDir);
|
|
1119
|
+
let candidateKey;
|
|
1120
|
+
if (pinnedKey) {
|
|
1121
|
+
candidateKey = pinnedKey;
|
|
1122
|
+
} else {
|
|
1123
|
+
const fetched = await fetchAsPublicKey(asUrl);
|
|
1124
|
+
if (!fetched.ok) {
|
|
1125
|
+
return { ok: false, error: "as_unreachable", message: fetched.message };
|
|
1126
|
+
}
|
|
1127
|
+
candidateKey = fetched.publicKeyHex;
|
|
737
1128
|
}
|
|
738
|
-
|
|
1129
|
+
const pinning = {
|
|
1130
|
+
enforce: resolvePinTls(dataDir),
|
|
1131
|
+
pinnedSpkiHex: existingTlsPin,
|
|
1132
|
+
capture: true
|
|
1133
|
+
};
|
|
1134
|
+
let capturedTlsSpkiHex;
|
|
1135
|
+
try {
|
|
1136
|
+
const result = await verifyAsHoldsKey(asUrl, candidateKey, pinning);
|
|
1137
|
+
capturedTlsSpkiHex = result.capturedTlsSpkiHex;
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1140
|
+
return { ok: false, error: "as_tls_mismatch", message: err.message };
|
|
1141
|
+
}
|
|
1142
|
+
if (err instanceof AsChallengeUnreachableError) {
|
|
1143
|
+
return { ok: false, error: "as_unverified", message: `${err.message}. Refusing to sign in.` };
|
|
1144
|
+
}
|
|
1145
|
+
if (err instanceof AsChallengeInvalidError) {
|
|
1146
|
+
if (pinnedKey) {
|
|
1147
|
+
return {
|
|
1148
|
+
ok: false,
|
|
1149
|
+
error: "as_key_mismatch",
|
|
1150
|
+
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.`
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
ok: false,
|
|
1155
|
+
error: "as_unverified",
|
|
1156
|
+
message: `The Authority Server at ${asUrl} could not prove it holds the signing key it presented \u2014 ${err.message}. Refusing to sign in.`
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
throw err;
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
ok: true,
|
|
1163
|
+
publicKeyHex: candidateKey,
|
|
1164
|
+
// A fresh capture (always present for https, unless pin-tls is on and
|
|
1165
|
+
// an existing pin was enforced instead — see fetchAs) wins; otherwise
|
|
1166
|
+
// whatever was already on file carries through unchanged.
|
|
1167
|
+
tlsSpkiPinHex: capturedTlsSpkiHex ?? existingTlsPin
|
|
1168
|
+
};
|
|
739
1169
|
}
|
|
740
|
-
function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished, pairing) {
|
|
1170
|
+
function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished, pairing, lockAsTlsMismatch2 = () => {
|
|
1171
|
+
}) {
|
|
741
1172
|
const router = Router();
|
|
742
1173
|
const SP_URL2 = pairing?.asUrl ?? DEFAULT_SP_URL;
|
|
743
1174
|
router.post("/login", loginRateLimit2, async (req, res) => {
|
|
@@ -752,24 +1183,43 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
752
1183
|
if (pairing) {
|
|
753
1184
|
keyCheck = await checkAsKeyBeforeLogin(pairing.asUrl, pairing.dataDir);
|
|
754
1185
|
if (!keyCheck.ok) {
|
|
755
|
-
|
|
1186
|
+
const status = keyCheck.error === "as_key_mismatch" || keyCheck.error === "as_tls_mismatch" ? 409 : 502;
|
|
1187
|
+
res.status(status).json({
|
|
756
1188
|
error: keyCheck.error,
|
|
757
1189
|
message: keyCheck.message
|
|
758
1190
|
});
|
|
759
1191
|
return;
|
|
760
1192
|
}
|
|
761
1193
|
}
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1194
|
+
const sessionPinning = {
|
|
1195
|
+
enforce: Boolean(pairing && resolvePinTls(pairing.dataDir)),
|
|
1196
|
+
pinnedSpkiHex: keyCheck?.tlsSpkiPinHex
|
|
1197
|
+
};
|
|
1198
|
+
let spRes;
|
|
1199
|
+
try {
|
|
1200
|
+
const result = await fetchAs(
|
|
1201
|
+
`${SP_URL2}/api/auth/session`,
|
|
1202
|
+
{
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
headers: {
|
|
1205
|
+
"Content-Type": "application/json",
|
|
1206
|
+
"X-API-Key": apiKey,
|
|
1207
|
+
// The AS grants a 30-day gateway session ONLY to a login that
|
|
1208
|
+
// identifies itself this way; without it, a 24-hour browser
|
|
1209
|
+
// session (see doc at the top of this file).
|
|
1210
|
+
...clientVersionHeaders()
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
sessionPinning
|
|
1214
|
+
);
|
|
1215
|
+
spRes = result.res;
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1218
|
+
res.status(409).json({ error: "as_tls_mismatch", message: err.message });
|
|
1219
|
+
return;
|
|
771
1220
|
}
|
|
772
|
-
|
|
1221
|
+
throw err;
|
|
1222
|
+
}
|
|
773
1223
|
if (!spRes.ok) {
|
|
774
1224
|
const err = await spRes.json().catch(() => ({ error: "Invalid API key" }));
|
|
775
1225
|
res.status(spRes.status).json(err);
|
|
@@ -778,9 +1228,15 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
778
1228
|
if (pairing && keyCheck?.publicKeyHex) {
|
|
779
1229
|
const existing = readPairing(pairing.dataDir);
|
|
780
1230
|
if (!existing || existing.asUrl !== pairing.asUrl) {
|
|
781
|
-
writePairing(pairing.dataDir, pairing.asUrl, keyCheck.publicKeyHex
|
|
1231
|
+
writePairing(pairing.dataDir, pairing.asUrl, keyCheck.publicKeyHex, {
|
|
1232
|
+
tlsSpkiPinHex: keyCheck.tlsSpkiPinHex
|
|
1233
|
+
});
|
|
782
1234
|
console.error(`[Control Plane] Paired with Authority Server ${pairing.asUrl} (fingerprint ${fingerprintOf(keyCheck.publicKeyHex)})`);
|
|
1235
|
+
} else if (keyCheck.tlsSpkiPinHex && keyCheck.tlsSpkiPinHex !== existing.tlsSpkiPinHex) {
|
|
1236
|
+
recordTlsPin(pairing.dataDir, pairing.asUrl, keyCheck.tlsSpkiPinHex);
|
|
1237
|
+
console.error(`[Control Plane] TLS pin for Authority Server ${pairing.asUrl} captured/updated.`);
|
|
783
1238
|
}
|
|
1239
|
+
clearPinTlsExpectedFingerprint(pairing.dataDir);
|
|
784
1240
|
}
|
|
785
1241
|
const data = await spRes.json();
|
|
786
1242
|
const setCookieHeaders = spRes.headers.getSetCookie?.() ?? [];
|
|
@@ -828,10 +1284,11 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
828
1284
|
const kp = await loadOrGenerateKeyPair(vault2);
|
|
829
1285
|
const localPubkeyB64 = Buffer.from(kp.publicKey).toString("base64");
|
|
830
1286
|
const spCookie = vault2.getSpCookie();
|
|
831
|
-
const meKeyRes = await
|
|
832
|
-
|
|
833
|
-
signal: AbortSignal.timeout(5e3)
|
|
834
|
-
|
|
1287
|
+
const { res: meKeyRes } = await fetchAs(
|
|
1288
|
+
`${SP_URL2}/api/users/me/pubkey`,
|
|
1289
|
+
{ headers: spCookie ? { Cookie: spCookie } : {}, signal: AbortSignal.timeout(5e3) },
|
|
1290
|
+
sessionPinning
|
|
1291
|
+
);
|
|
835
1292
|
let needsUpdate = false;
|
|
836
1293
|
if (meKeyRes.status === 404) {
|
|
837
1294
|
needsUpdate = true;
|
|
@@ -840,15 +1297,19 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
840
1297
|
needsUpdate = meKeyData.pubkey !== localPubkeyB64;
|
|
841
1298
|
}
|
|
842
1299
|
if (needsUpdate) {
|
|
843
|
-
const putRes = await
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1300
|
+
const { res: putRes } = await fetchAs(
|
|
1301
|
+
`${SP_URL2}/api/users/me/pubkey`,
|
|
1302
|
+
{
|
|
1303
|
+
method: "PUT",
|
|
1304
|
+
headers: {
|
|
1305
|
+
"Content-Type": "application/json",
|
|
1306
|
+
...spCookie ? { Cookie: spCookie } : {}
|
|
1307
|
+
},
|
|
1308
|
+
body: JSON.stringify({ pubkey: localPubkeyB64 }),
|
|
1309
|
+
signal: AbortSignal.timeout(5e3)
|
|
848
1310
|
},
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
});
|
|
1311
|
+
sessionPinning
|
|
1312
|
+
);
|
|
852
1313
|
if (!putRes.ok) {
|
|
853
1314
|
console.error(`[Control Plane] E2EE pubkey registration failed: ${putRes.status}`);
|
|
854
1315
|
} else {
|
|
@@ -858,7 +1319,12 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
858
1319
|
console.error("[Control Plane] E2EE pubkey already up to date");
|
|
859
1320
|
}
|
|
860
1321
|
} catch (err) {
|
|
861
|
-
|
|
1322
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1323
|
+
console.error("[Control Plane] E2EE pubkey auto-register aborted \u2014 TLS pin mismatch:", err.message);
|
|
1324
|
+
lockAsTlsMismatch2();
|
|
1325
|
+
} else {
|
|
1326
|
+
console.error("[Control Plane] E2EE pubkey auto-register failed:", err);
|
|
1327
|
+
}
|
|
862
1328
|
}
|
|
863
1329
|
for (const credId of vault2.listCredentials()) {
|
|
864
1330
|
try {
|
|
@@ -895,12 +1361,20 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
895
1361
|
router.post("/logout", logoutAuth, async (_req, res) => {
|
|
896
1362
|
const cookie = vault2.getSpCookie();
|
|
897
1363
|
vault2.clearKey();
|
|
1364
|
+
const logoutPinning = pairing ? { enforce: resolvePinTls(pairing.dataDir), pinnedSpkiHex: readPairing(pairing.dataDir)?.tlsSpkiPinHex } : { enforce: false };
|
|
898
1365
|
await Promise.allSettled([
|
|
899
1366
|
unconfigureSession(),
|
|
900
|
-
cookie ?
|
|
901
|
-
|
|
902
|
-
headers: { cookie, ...clientVersionHeaders() },
|
|
903
|
-
|
|
1367
|
+
cookie ? fetchAs(
|
|
1368
|
+
`${SP_URL2}/api/auth/logout`,
|
|
1369
|
+
{ method: "POST", headers: { cookie, ...clientVersionHeaders() }, redirect: "manual" },
|
|
1370
|
+
logoutPinning
|
|
1371
|
+
).catch((err) => {
|
|
1372
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1373
|
+
console.error("[Control Plane] Logout: TLS pin mismatch talking to the Authority Server \u2014 local sign-out still applied:", err.message);
|
|
1374
|
+
lockAsTlsMismatch2();
|
|
1375
|
+
} else {
|
|
1376
|
+
throw err;
|
|
1377
|
+
}
|
|
904
1378
|
}) : Promise.resolve()
|
|
905
1379
|
]);
|
|
906
1380
|
res.json({ ok: true });
|
|
@@ -1512,18 +1986,18 @@ async function testAIConnectivity(config) {
|
|
|
1512
1986
|
}
|
|
1513
1987
|
|
|
1514
1988
|
// src/lib/agent-brief-store.ts
|
|
1515
|
-
import { existsSync as
|
|
1989
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
1516
1990
|
import { homedir as homedir2 } from "os";
|
|
1517
|
-
import { join as
|
|
1991
|
+
import { join as join5 } from "path";
|
|
1518
1992
|
var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
|
|
1519
1993
|
function agentBriefPath() {
|
|
1520
|
-
const dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
1521
|
-
return
|
|
1994
|
+
const dataDir = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
|
|
1995
|
+
return join5(dataDir, "context.md");
|
|
1522
1996
|
}
|
|
1523
1997
|
function readAgentBrief() {
|
|
1524
1998
|
const filePath = agentBriefPath();
|
|
1525
|
-
if (!
|
|
1526
|
-
return
|
|
1999
|
+
if (!existsSync4(filePath)) return "";
|
|
2000
|
+
return readFileSync6(filePath, "utf-8");
|
|
1527
2001
|
}
|
|
1528
2002
|
|
|
1529
2003
|
// src/routes/ai.ts
|
|
@@ -1887,7 +2361,7 @@ function createMCPRouter() {
|
|
|
1887
2361
|
// src/routes/autostart.ts
|
|
1888
2362
|
import { Router as Router6 } from "express";
|
|
1889
2363
|
import { execFile } from "child_process";
|
|
1890
|
-
import { existsSync as
|
|
2364
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1891
2365
|
import { dirname as pathDirname, resolve } from "path";
|
|
1892
2366
|
import { fileURLToPath } from "url";
|
|
1893
2367
|
import { promisify } from "util";
|
|
@@ -1908,7 +2382,7 @@ function findCli(dirname9 = MODULE_DIR) {
|
|
|
1908
2382
|
// resolves differently between a shell and a login service — and it would
|
|
1909
2383
|
// mask a genuinely missing CLI whenever the cwd happened to be the repo.
|
|
1910
2384
|
];
|
|
1911
|
-
return candidates.find(
|
|
2385
|
+
return candidates.find(existsSync5) ?? null;
|
|
1912
2386
|
}
|
|
1913
2387
|
async function cli(args) {
|
|
1914
2388
|
const bin = findCli();
|
|
@@ -1989,17 +2463,17 @@ function createAutostartRouter() {
|
|
|
1989
2463
|
import { spawn, spawnSync } from "child_process";
|
|
1990
2464
|
|
|
1991
2465
|
// src/lib/notifier-icon.ts
|
|
1992
|
-
import { writeFileSync as
|
|
2466
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
1993
2467
|
import { homedir as homedir3 } from "os";
|
|
1994
|
-
import { join as
|
|
2468
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
1995
2469
|
var ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAACxLAAAsSwGlPZapAAAaEElEQVR42u2deXxNV9fH7/O2z/vHk+HmJoIoQQxpBtVPi+LTIWZqeDQ6V0tRFUWIeagSRAheyasxVNUsWtRQSquoMTRi9mgJooZWhBqaSIX97nWa9CVN4ubes8/Z55zf+ny+H1rknrv3Wr+zh7XXttmMaY94e3vXdDgcL3JifHx8ZnE2cvbw/z7MyeBc5eRxGABukFfgS+RTh8nHCnxtFvke+SD3xRrkkzaYGPP19fXmDd6eM403eDoCG0gqFPu5j07ltCOfReS6YbwRn+ANGsd/TeW/5sPBgMG4U+C7Ezh1ENFOGB9K+fLG6skbbiccCJgJ7tPH+K9D/f39KyLS/z7Eb84baD2pJpwFWGBksN5utzezetz/o2BevwdOASxKOn/5vUKxYKnA51/8tYLVejgBAA7HQUsIgZ+fXzB/43+LDgegWLZzwk0X+AEBAf/iX2wMtu8AePgaASeRvyy9zLKd147zMzoWgDLtGpzjv7Y1cuw/WvDWv4sOBcAl7tFogMfSfxsq8vlDB3IF240OBEAVfuDbhkGGCH7+oB35A19DpwGgKlf5S7WD7PP9DzDkB0DolCBG1mH/UHQQAJosEMZLdTS34GgkOgcA7fiMFtplCP7P0RkA6DISWK5nHQLK5Z+DjgBAVxGYrdecfwI6AAApGKv1an9vNDoAUo0EorXc58dWHwBycZeO1wvP8ONko7EBkDNZiL+gq4mK/38ivRcA6acCe4WcHeA//H/QwMAM+Po6WKNQH9arlZ0ldPZmKwZ4sfQ4T/afKZ7s4gwP9tsnf0K/p/+3f4Kn8ncm879L/4b+Lf0MiUVgstqLfu0L0hDhQMCQVK3kYL1be7Pl0V7sXJIH+32ee9DPSOE/K6qVt/KzJUwZflHNYh5n4UTAaPjxt/TLje3s8/5e7OocT7eDviToZ5MYdOKfJcvIgOoJ8ArEnmq8/afAmYCRKOfnYN2a2dmBOHFBXxI0lXi3qV15BgnaYqK78/4wzh9wKmAU6C18ZJKH5oFflCOTPNlLjex6t0ceLzYa4k713q1wKmAEQqr5sAW9vXQP/KKsHujF6gT56Nk237tUbdjxZ+luOBeQnjeft7NLH3tIF/yF/Jrswbo21W80wHMDIl15+6NuP5Caiv4+bGYPL2kDvygfd/fiz6xLWx0o0yiAL/z9Gw4GZN/W2/qhp2GCv5AdH3mxoMq6tNmLZRGAXXAyICu1A33YD+ONF/yF0O5EaDUfrbcF9zh9USecDMjK41V92PEE4wZ/IfQd6LtovBbQxJm3/3o4GpB12E9puUYP/kLS+ChG4yzCdaUGf/ny5Ss4cEU3kHTBz4hz/oexZZSnlguDdyjGS9v6GwhnAzIy6z1v0wV/ITO6eclROIT/hYNwNiAbb0fYTRv8hbyrXZ5AWknBHw5nA7IRzrPoKJHG7AJAiUwa7gzUQZFPYAjoHL7Zg78QOqqsUbvGFrf6vxcOB2SiY0O7ZYK/EDrMpME6wK6ib387Jx9OB2Q60ivDqT6tORzvqcVR4js838e7aMUfOB6QBjrPb7XgL6RLE7u2qcFcAKbB6YBMNfv0KOYhU4KQ6MpCPOYT7p8CpMPxgCxQGS+rBn8hkeKLify1HfgI/4/bcDwgC1TDz+oCsLSv8B2B28rFot7e3jXhdEAWAgMcLHu2p+UFgAqNij4nwA8HVafhf1s4HpAFKt1t9eAv5P2WwqcBrUkAYuB4QBYoGQbBr800QDkXQPeLw/GALKv/alzaYRYyEz1E7wYkkwBshPMBGWjMr9xC4D9IwxAfkSOADSQAe+B8QAbo3j0E/YO818IuUgB2kgAcgfMBGaCLOhH0DxL/prfINj9kw71/QBZWDsACYFG+GCB0IfA0CUA2nA84Q+XKldnzzz/POnbsyFq2bMlCQ0NV/flp47H/X5R94zxF9mkWCUAenBuUFvQjRoxg6enpLD8/nxW1ixcvstmzZ7MGDRq4/Vk/ToUAFOU/UzzFZgPCyUFJREVFsezsbOaM3b17ly1dupSFhIS4/HkXZiDgi/Lz/3oI7WMIAPj7Wfxy5di8efOYK5aTk6P8e1c+99ocBPzfU4IhAEDTZBxftmTJEuaOVahQAQIAAQBGZPDgwcxdCwoKwhQAUwDgDvwCBxYREcHeeOMN1rVrV2Xl3Z35tTPUrl2b3bp1y20BeOKJJ7AIaIxFQAiAdIUweaCvWbOG5ebmFhtcp06dYhMnTmQ1atRQ/bOnTZvG1LDGjRtjG9AY24AQABnw9/dnPXr0YIcOHXI6yG7cuMFiYmKUObtac//z58+rIgAtWrRAIpAxEoEgAHry2GOPsWHDhrHMzEyXgy0lJcXlRbf7oX18teydd95BKrAxUoEhAHpQsWJFNnbsWHb16lVVAo5EwN2RAAWtWjZu3DgcBjLAYSAIgA7Qot7p06eZ2jZo0CC3nmvAgAGqPcuyZctceoZGOA78N54J8YEAmIGGDRuyrVu3MlF28+ZNtxYG+/Tpo9qzpKWluVwQhIpgIPALCoIkiS8PDgEQXeQyMJDNnDmT/fHHH0y0xcfHu/ycnTp1Uu05fvvtN5QEU4El4isDQwBEQifmzp49y7SyjIwMl5+1Vq1a7N69e6o9i6uHg6JaYSFQw6KgEABRufRjxozR5K1f1MLCwlx+7r1796r2HAMHDkRZcDfLglNbQAAMBmXB7dmzh+llNJR35/SfWrZ69WpMA+S+GAQCoDZdunRR5r96Wvfu3d1KSDp+/Lgqz3HlyhXm5+fn2noErgZjLzWyQwCMxPDhw5Uz8Xpb586d3foedP7g9u3bqjwL/SxXdwPSLXw56A8aXA4KAVAxjXfhwoVMFmvatKnb34mSgtRYv0hKSnL5Gd618PXg70TYNfNfCIAbVKlShW3evFma4Kc3N2UZqvHdaC3B2WpAJdmlS5dcLg5Szs/Bjkyy3ijgULwn8/N1QABk5/HHH2cHDx5kMtm6detU/Y7BwcFs5cqVbm0PRkZGuvz5/37GjmvBIQDyER4erun+vrP20ksvCfm+bdq0YQcOHHDpmZYvX+7WZ3/R3zqjgJRoL819GQLgQtEMOpMvm23btk3o96YV/b59+7LLly+X6bmorgG1maufG1bdh/2SbP7gv/SxBwup5gMBkBnKtT927Jh0wU9zdVer8LiS2kyLe3l5eU4/X2Jioluf2fkF808F3nrBRxefhgA4SdWqVZXa+LIZFQZp1aqV5u1Rr149tmHDBqee8ffff2c1a9Z06/M+7m7e5KDErl66+TUEwAkqVarEUlNTpQt+yv1/7rnndG0bWnc4ceKE0INKSg0Ffwf7bpT51gM2j/BkFfwdEACZWbVqlVSBf/36dTZ58mRFmGTJhejfvz/Lysoq8ZmvXbvmdh3DqpXMVTeQEn60yPeHALjBqFGjpAl82lf/6KOPlOmIrGskc+fOZXfu3Cn2+Slhyu3t16o+7HiC8UXg2GQPFhzoo3ufQQBKoUOHDiU6s5ZGuw6UaqxWko9oqCow7UoUd30YHZF2eyeGBw5VyzVq8FOasx4r/hCAMu71l3XLS8Qb//3331et8q8M5c9oF4WmDG4vylYy5prA5pH6D/shAE4U7dRzxZ/y8GfNmqVsuZmlACqVLCu00aNHq/Oz+eLZjG7G2B249akHS3rXS9cFPwiAk8yYMUO34N+xYwd76qmnTJk6TXcO0jSABE7Nrcs3nrezixJfK/ZrsrYHfCAAbtCsWTOWn5+veeDTWgNtlbl6eMYo0GnFffv2KZeQqHm7ES0OLugt32hg9UAvFh7kI21/QADugy7YUKsgRlmMLgZp3bq15cqjL1iwQPX1jY4N7exwvKcUp/raN7BL3w8QgPugvXWtjY4T07Fiq96M1L59e/XPLfDjtF2b2nXJGaC9fRrua3mkFwKgAs8++6zmRTwXL16s3AKMugpiBJCq6tDx2mX9vIQWGqWfTTX8qIyXr6+x2h4CUHDSTctVfzpfHxsbC+HV8n6GgD+vHqNApQs33A36s4meSt3+nrx0d5UA47YLBIBDN/NqGfyUNot21w96S9OVWxS8k97yVm7gpcQiyjC8wHcTrs35E/o9/b+9/M/o79BFnXRXn+jruiAAGkJDcDpUo5WptQcOAARABWJiYjQL/gkTJsDpAARAFgICApR0Wy0sOTkZDgcgADJB13dpYd9//70q+e8AQABULPLhbtlrZ+zcuXNuV8MBAAKgMv369dOkTr8aF3UAAAFQGVfLXJfFRo4cCScDEADZoLx70ZaWlmb6gz0AAmBIVqxYIXzo36hRIzgYgAA4JLzYoyw17V2xuLg4OBeAAMgIFdUUaRcuXDBM7T4ALCcA+/fvFyoAtLsAxwIQAAkJCQlx66ZbZ6r3IuEHQAAkZdCgQULf/t26dYNTAQiArBRXq14t+/nnn7HtByAAslK9enWhFX/oXAEcSj5IlKkCMY3+qOTb1KlT2YgRI1jHjh2Vw2AQAIt80ffee0/ovj/y/eWiVq1abPr06aVe7nLr1i2lLNvTTz8NATA7dGedKFu6dCmCTjKxp8tInTXKCxk/frxSGg4CYFIOHjwoTABoOInAk4OEhASX+3Ht2rVKaXgIgAmP/oq65PPq1auo7Oswz03Oy5YtgwCYjXbt2mH4b3JeeOEF1RZ5o6KiIACo/OOcde7cGQEoAbt371atT2nhkEaNEACTQHM7Uff50e02CEB9admypep9O2DAAAiAWTh58qQQAaCFRQSg/sycOVP1vt2+fTsEwGGKSyB8WW5urhAB+OSTTxCAJq3ulJOTY4lzHaYXgODgYGHz/169eiEAJeD69etC+rdu3boQAKPTvHlzYQJQr149BKAEI7y7d+8K6d8WLVpAAIwOndATYfn5+Tj6Kwk3b94U0sdvvfUWBAAVgEo+/Yfgk4MzZ84I6WMrXOJqegGYM2eOEOegfWcEnxzs27dPSB9PnDgRAmB0lixZIsQ5UlJSEHyS8NVXXwnpYzpABgFwoAR4cZaUlITgk4R58+YJ6WNKIIMAGJx169YJcQ4qLoHgk4P4+HghffzNN99AAIwOdaIIGzduHIJPEmJjY4X08aZNmyAARoeu5sa9fxAAV2zjxo0QAKOTmpoqxDkGDx6M4JMEGo2JsK+//hoCAAHACEB2qJyXCNuwYQMEwOjQPE6EUekpBJ8c0H69CFu/fj0EwOgsX74cJwGR7OWS0Q4SBADOUax98cUXCD6TF3xZuHAhBMDo0H69CNuyZQuCTxL27t2LXA8IQPHQYp0Iy8zMRPBJwrlz54T0cUxMDATA6PTp00eIc9AZdNQD1B+q408Xe6DgKwSgWOjSDlFGpagRhPoSEREhrH+bNWsGATA6oaGhwhyErqBCEOpLv379hPVveHg4BAA140q25ORkBKHO0HasCKNLRqxwTZglBCAtLU2Ikxw5cgRBaNJiIEePHmUoC46iIKXavXv3lGuoEYj6UL16dWF3PtK14RAAB+oCPsy6du2KYNSJHj16COvXoUOHQgDMwmuvvSbMUebPn49gNFmaN1mbNm0gAGahWrVqShlvEZaVlYXrwXWgXLly7MqVK8JyPCpXrgwBMBPp6enC3havv/46gtJE+R0//fQTw/XgJiMxMVGYw3z55ZcISo2hNhdln332GQTAbERGRgpzmNu3byvTDASmNtDOi6j0X7I333wTAmA2KlWqpASqKBsyZAiCUyNGjRolVMytdMbDZiXH2bFjhzDHoRNpWAwUD93HmJGRIawfN2/ebKn2tJQAjB07lom0Dz74AEEqmN69ewvtQ6uN5CwlAHXq1BF2lTTZyZMnmZ+fHwJV4NbfqVOnhArAk08+CQHANMB16969O4JVEH379hXad4cPH7Zcm1pOAEQVCGH3XRuOQiFiFnFFVf5hFr7rwXICEBgYyHJycoQ60tSpUxG0BsrjIMvNzVUOF0EALMCqVauEOhPtUdevXx+BqxKNGzdWzueLtKVLl1qybS0pAK+++ioTbdu2bWO+vr4IYDehRVVRZ/7vt9atW0MArLSaTCv2om306NEIYjf58MMPhffTiRMnLNu+Nqt+cdGLgYVlpZo3b45AdhF6K4se+ls9i9OyAkBZe6JXlQt3BYKCghDQLuT7X7x4UXj//PLLLywgIAACYEVI+bUwumeeUlgR2M5P0bZu3apJ31j9DIelBaBixYrKG0ALs8I9c2pAC6cLFizQpE8uXbpk6be/5QVAi/MB9xtdY40gL51p06Zp1h9WuPoLAvAQqPTThQsXNHE4qiIcHR2NQC+B4cOHaxb8tP5jhbr/EAAnoMq+WhmJADk62v1BqAqvyINaRa1Xr15odwjA/0PnwLW06dOno90L5vzx8fGatv3OnTuRpAUBeJAGDRoILTNVnM2ePVtZ8bZycY9FixZp2uZU8adevXrweQjA36G3stZGKcNWvF2oZs2amo+6yOLi4uDrEIDioWO858+f19wp6TNbtGhhmXZu1aqVLu38448/YuEPAlA67du3F3aJyMNOEI4YMcLUFYVo3k25/Vqk97JiLvto27YtfBwC8HAmTZrE9LJDhw6xiIgI07UpzbtpuqOXTZkyBb4NAXA+FZVWivUyGg1MnjxZyVQ0QyUfWlvR461faFQGzsqLrRAAFwgLCxN291xZklWoDp4RzxFQwNFee2Zmpq5t+Ouvv7Lg4GD4NASg7LzyyitK4o7eRkHUv39/Q7zFaA2DEqu0qLfwMKO1HLpDEL4MAXAZGorLYqdPn1aKjNAWmmztREeeKZuPnlEWi42NhQ9DANxfuV68eDGTySiZ5fPPP1d2LPQcFdBnv/zyy8pFnSKvXXPFUlJSkO0HAVCveMi3337LZLTs7GzF2bt06cKqVKmiSQIP3X2wZMkSzY5Sl9U2bdqEa9ogAOqfGkxPT2cyG+0e7N+/n82dO1e5poyq6bozQqDEqKZNmyrl05KTk5Xvr+WBHVcsNTVV2XmAz0IAVKd27dpSzXGZk3UJqSwZBcbKlSuVQKZ1jTFjxjxAQkIC+/TTT9natWuVLVD6nrIHe1E7fvy4JWv7QwA0hO6OO3PmDIPJZWfPnmUhISHwUQiAeGhf+ejRo4g6SYxy/MPDw+GbEADtoKGmFhdWwEo3WvOQcVsUAmCR04NbtmxBFOpkdLaAFmfhixAAXSsL08IZTFtbtmwZSq1DAORJFqKVdD2OEVvNqI2pujKSfCAA0kHnzenwCUyMZWVlsU6dOsHXIADyEhoaquy5w9S1Xbt2YZsPAmCc1GFKuDFaIo2MRm1IxTxwnh8CYMjad3T1NMw1y8jIwHFeCIDxRwO0QCjbaTnZ05epipAZKiJBAMBfNfG2b9+O6GYPv7TjmWeegc9AAMwHVcuJiorCWQJW/E29PXv2hJ9AAKwxLSAhoAMsVrfLly8rUySrX9MNAbAgdEkF1fqTtbAGE3wZCl2SisCHAOBMAT9TMHDgQEucMKQ6A/RdscAHAQDF0KRJEzZ//nyWk5NjmqCnHZDVq1crW3pI4YUAACdr79E1WnRTkFGNjktHR0ezwMBA9CkEALhKnTp12ODBg5Wjx1pfYV4Wy83NZd999x0bOXIkq1+/PvoOAgDUht6mdPHGokWL2LFjx3Q/gUh1+GbMmMEiIyOxoAcBAHosILZp00a5WXjFihXs1KlTQu7hu3HjhjKkp/WJIUOGsA4dOrAaNWqgDyAAQMaEIzo517x5c/b222+zYcOGsaSkJOUyEVqMI9asWaNU1CmEhu7057NmzWJxcXFs0KBBykiDLh+pW7cu2tXEApCHhgDAktwmAchGQwBgSbJIAM6iIQCwJKdtPj4+R9AQAFiSQyQAu9EQAFgPHvs7SQA2ojEAsKQAbCABmI3GAMCSJNMiYAwaAgBLjgCiSQDaojEAsCStbd7e3jXQEABYD7vdXs3G7RHKCEKDAGCp4X8uxT4JAE0D0tEoAFiKNFuhcTWYigYBwFIjgMn3C0A7NAoAlqLNXwLAj4968f9xB40CgCW4QzFvu9/4KCAVDQOARVKAixr/g/FoHAAsQWxxAhCGhgHAEoTbijP+BwfQOACYmh9sJRnOBQBg+vl/vxIFwMPDozx2AwAw7+o/v5i2gq004wqxHg0FgClZa3uY8QMCzdBQAJjy8E+EzRnjo4BdaDAATDX332Nz1vhfbo9GA8Ckqb9O2D/4P9iPRgPAFBygmC6LANj4Pe6voOEAMMXcv6PNBaNRwBY0IACGZluZ3/73jQJC+Q/4A40IgCHJ4zEcYnPHqHAAGhIAQzLB5q4FBAT8i/+gM2hMAAxFZoUKFTxsapjjz9Lh99CoABiCe0rJbzWNTwWmoGEBMETST7xNgD2KDEEApA/+VB6r/xQhADQVqMK5goYGQEqucgGoahNp/AM68A+6i8YGQCryOS/atDD+Qb3Q4ABINfTva9PS+IeORcMDIEXwj7bpYfzDk9ABAOga/LNsOtoj/AGWoyMA0CX4U/664FNnEZiJDgFAU+bR1rxNFuMPNBSdAoBxE33U2CLsjS1CAIRu9fWyyWwF5cSy0VkAqMoVzfb51cgYpAsI0WkAqDLk38er+lS3Gcwe5Q8/BlMCANw61ZcoLLdfo9HAi3Q2GZ0JQJk4q/qRXr2soKgIjQZuo2MBKBUqwZfo7+/vaTObeXl51eLzmU3oZACKZRsnzGZ24wsakfyLpqPDAVDY72rpbkMb/+LPcr6DAwCLru7vom1zl8t2m2hE0IQ3yDoHriYH5od8fB35vA32oHl7e/vyxumJHAJgwrf9MUqXL1++fAVEunPTgzBOLG+43RgZACO+6QtqaI6lC3YQ0W6Yn5+fF+UTFFQmTsN2IpAQ8skfuI8mkK+achtPInuEz6GCKFGC/9qfjiNzvi5Q3IOcjILzCHlwTOAmeQW+RD51kKan5Gv898n81+gCH6Q03f8yYiD9H4Lpi7OZQSaFAAAAAElFTkSuQmCC";
|
|
1996
2470
|
function ensureNotifierIcon(dataDir) {
|
|
1997
2471
|
try {
|
|
1998
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
1999
|
-
const path =
|
|
2000
|
-
if (!
|
|
2001
|
-
|
|
2002
|
-
|
|
2472
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
|
|
2473
|
+
const path = join6(dir, "notifier-icon.png");
|
|
2474
|
+
if (!existsSync6(path)) {
|
|
2475
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
2476
|
+
writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
|
|
2003
2477
|
}
|
|
2004
2478
|
return path;
|
|
2005
2479
|
} catch {
|
|
@@ -2201,24 +2675,24 @@ import { Router as Router9 } from "express";
|
|
|
2201
2675
|
|
|
2202
2676
|
// src/routes/approved-intents.ts
|
|
2203
2677
|
import { Router as Router8 } from "express";
|
|
2204
|
-
import { readFileSync as
|
|
2678
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
2205
2679
|
import { homedir as homedir4 } from "os";
|
|
2206
|
-
import { join as
|
|
2207
|
-
var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
2208
|
-
var FILE_PATH =
|
|
2680
|
+
import { join as join7 } from "path";
|
|
2681
|
+
var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
|
|
2682
|
+
var FILE_PATH = join7(SUVEREN_DATA_DIR, "approved-intents.enc.json");
|
|
2209
2683
|
function readFile() {
|
|
2210
|
-
if (!
|
|
2684
|
+
if (!existsSync7(FILE_PATH)) {
|
|
2211
2685
|
return { version: 1, entries: {} };
|
|
2212
2686
|
}
|
|
2213
2687
|
try {
|
|
2214
|
-
return JSON.parse(
|
|
2688
|
+
return JSON.parse(readFileSync7(FILE_PATH, "utf-8"));
|
|
2215
2689
|
} catch {
|
|
2216
2690
|
return { version: 1, entries: {} };
|
|
2217
2691
|
}
|
|
2218
2692
|
}
|
|
2219
2693
|
function writeFile(data) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2694
|
+
mkdirSync5(SUVEREN_DATA_DIR, { recursive: true });
|
|
2695
|
+
writeFileSync5(FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
2222
2696
|
}
|
|
2223
2697
|
function readApprovedIntents(vault2) {
|
|
2224
2698
|
const file = readFile();
|
|
@@ -2267,7 +2741,7 @@ function createApprovedIntentsRouter(vault2) {
|
|
|
2267
2741
|
|
|
2268
2742
|
// src/routes/evidence-export.ts
|
|
2269
2743
|
var AS_FETCH_TIMEOUT_MS = 2e4;
|
|
2270
|
-
function createEvidenceExportRouter(spUrl, vault2) {
|
|
2744
|
+
function createEvidenceExportRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2) {
|
|
2271
2745
|
const router = Router9();
|
|
2272
2746
|
const getSpCookie = () => vault2.getSpCookie();
|
|
2273
2747
|
router.get("/", async (_req, res) => {
|
|
@@ -2287,17 +2761,29 @@ function createEvidenceExportRouter(spUrl, vault2) {
|
|
|
2287
2761
|
const controller = new AbortController();
|
|
2288
2762
|
const timeout = setTimeout(() => controller.abort(), AS_FETCH_TIMEOUT_MS);
|
|
2289
2763
|
try {
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2764
|
+
const pin = readPairing(dataDir);
|
|
2765
|
+
const { res: asRes } = await fetchAs(
|
|
2766
|
+
`${spUrl}/api/receipts/export`,
|
|
2767
|
+
{ headers: { Cookie: cookie }, signal: controller.signal },
|
|
2768
|
+
{
|
|
2769
|
+
enforce: resolvePinTls(dataDir),
|
|
2770
|
+
pinnedSpkiHex: pin && pin.asUrl === spUrl ? pin.tlsSpkiPinHex : void 0
|
|
2771
|
+
// Never capture here — only the sign-in challenge establishes or
|
|
2772
|
+
// refreshes a pin (as-challenge.ts).
|
|
2773
|
+
}
|
|
2774
|
+
);
|
|
2294
2775
|
if (asRes.ok) {
|
|
2295
2776
|
authorityServer = await asRes.json();
|
|
2296
2777
|
} else {
|
|
2297
2778
|
asError = `Authority Server export failed: ${asRes.status}`;
|
|
2298
2779
|
}
|
|
2299
2780
|
} catch (err) {
|
|
2300
|
-
|
|
2781
|
+
if (err instanceof AsTlsMismatchError) {
|
|
2782
|
+
lockAsTlsMismatch2();
|
|
2783
|
+
asError = err.message;
|
|
2784
|
+
} else {
|
|
2785
|
+
asError = `Authority Server unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
2786
|
+
}
|
|
2301
2787
|
} finally {
|
|
2302
2788
|
clearTimeout(timeout);
|
|
2303
2789
|
}
|
|
@@ -2483,7 +2969,7 @@ function createDecryptIntentRouter(vault2) {
|
|
|
2483
2969
|
|
|
2484
2970
|
// src/lib/update-checker.ts
|
|
2485
2971
|
import { execSync } from "child_process";
|
|
2486
|
-
import { dirname as
|
|
2972
|
+
import { dirname as dirname5 } from "path";
|
|
2487
2973
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2488
2974
|
var GHCR_IMAGE = "suverenai/suveren-gateway";
|
|
2489
2975
|
var NPM_PACKAGE = "@suveren/gateway";
|
|
@@ -2549,7 +3035,7 @@ function compareSemver(a, b) {
|
|
|
2549
3035
|
return 0;
|
|
2550
3036
|
}
|
|
2551
3037
|
function checkDev() {
|
|
2552
|
-
const cwd =
|
|
3038
|
+
const cwd = dirname5(fileURLToPath2(import.meta.url));
|
|
2553
3039
|
try {
|
|
2554
3040
|
execSync("git fetch origin --quiet", { cwd, stdio: "ignore", timeout: 15e3 });
|
|
2555
3041
|
const out = execSync("git rev-list HEAD..origin/main --count", {
|
|
@@ -2622,19 +3108,19 @@ data: ${data}
|
|
|
2622
3108
|
import { Router as Router12 } from "express";
|
|
2623
3109
|
|
|
2624
3110
|
// src/lib/gateway-settings.ts
|
|
2625
|
-
import { readFileSync as
|
|
3111
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
2626
3112
|
import { homedir as homedir5 } from "os";
|
|
2627
|
-
import { join as
|
|
3113
|
+
import { join as join8, dirname as dirname6 } from "path";
|
|
2628
3114
|
var DEFAULT_SETTINGS = {
|
|
2629
3115
|
desktopNotifications: true
|
|
2630
3116
|
};
|
|
2631
3117
|
function settingsPath(dataDir) {
|
|
2632
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
2633
|
-
return
|
|
3118
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
|
|
3119
|
+
return join8(dir, "settings.json");
|
|
2634
3120
|
}
|
|
2635
3121
|
function readSettings(dataDir) {
|
|
2636
3122
|
try {
|
|
2637
|
-
const raw = JSON.parse(
|
|
3123
|
+
const raw = JSON.parse(readFileSync8(settingsPath(dataDir), "utf8"));
|
|
2638
3124
|
return {
|
|
2639
3125
|
desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
|
|
2640
3126
|
};
|
|
@@ -2648,8 +3134,8 @@ function writeSettings(patch, dataDir) {
|
|
|
2648
3134
|
...typeof patch.desktopNotifications === "boolean" ? { desktopNotifications: patch.desktopNotifications } : {}
|
|
2649
3135
|
};
|
|
2650
3136
|
const path = settingsPath(dataDir);
|
|
2651
|
-
|
|
2652
|
-
|
|
3137
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
3138
|
+
writeFileSync6(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2653
3139
|
return next;
|
|
2654
3140
|
}
|
|
2655
3141
|
|
|
@@ -2681,20 +3167,20 @@ function createGatewaySettingsRouter() {
|
|
|
2681
3167
|
import { Router as Router13 } from "express";
|
|
2682
3168
|
|
|
2683
3169
|
// src/lib/archived-mandates-store.ts
|
|
2684
|
-
import { readFileSync as
|
|
3170
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
2685
3171
|
import { homedir as homedir6 } from "os";
|
|
2686
|
-
import { join as
|
|
3172
|
+
import { join as join9, dirname as dirname7 } from "path";
|
|
2687
3173
|
var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
2688
3174
|
function isValidMandateId(id) {
|
|
2689
3175
|
return typeof id === "string" && ID_PATTERN.test(id);
|
|
2690
3176
|
}
|
|
2691
3177
|
function storePath(dataDir) {
|
|
2692
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ??
|
|
2693
|
-
return
|
|
3178
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
|
|
3179
|
+
return join9(dir, "archived-mandates.json");
|
|
2694
3180
|
}
|
|
2695
3181
|
function readArchived(dataDir) {
|
|
2696
3182
|
try {
|
|
2697
|
-
const raw = JSON.parse(
|
|
3183
|
+
const raw = JSON.parse(readFileSync9(storePath(dataDir), "utf8"));
|
|
2698
3184
|
if (!Array.isArray(raw.archived)) return [];
|
|
2699
3185
|
return [...new Set(raw.archived.filter(isValidMandateId))];
|
|
2700
3186
|
} catch {
|
|
@@ -2704,9 +3190,9 @@ function readArchived(dataDir) {
|
|
|
2704
3190
|
function writeArchived(ids, dataDir) {
|
|
2705
3191
|
const next = [...new Set(ids.filter(isValidMandateId))].sort();
|
|
2706
3192
|
const path = storePath(dataDir);
|
|
2707
|
-
|
|
3193
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
2708
3194
|
const tmp = `${path}.tmp`;
|
|
2709
|
-
|
|
3195
|
+
writeFileSync7(tmp, JSON.stringify({ archived: next }, null, 2) + "\n", "utf8");
|
|
2710
3196
|
renameSync(tmp, path);
|
|
2711
3197
|
return next;
|
|
2712
3198
|
}
|
|
@@ -2748,6 +3234,7 @@ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
|
2748
3234
|
var ALLOWED = ["proposal-added", "action-approval-needed"];
|
|
2749
3235
|
var SESSION_EXPIRED = "session-expired";
|
|
2750
3236
|
var AS_KEY_MISMATCH = "as-key-mismatch";
|
|
3237
|
+
var AS_TLS_MISMATCH = "as-tls-mismatch";
|
|
2751
3238
|
function secretMatches(provided, expected) {
|
|
2752
3239
|
if (!expected) return false;
|
|
2753
3240
|
if (!provided) return false;
|
|
@@ -2756,7 +3243,7 @@ function secretMatches(provided, expected) {
|
|
|
2756
3243
|
if (a.length !== b.length) return false;
|
|
2757
3244
|
return timingSafeEqual2(a, b);
|
|
2758
3245
|
}
|
|
2759
|
-
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch) {
|
|
3246
|
+
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch, onAsTlsMismatch) {
|
|
2760
3247
|
const router = Router14();
|
|
2761
3248
|
router.post("/event", (req, res) => {
|
|
2762
3249
|
if (!secretMatches(req.headers["x-internal-secret"], getSecret())) {
|
|
@@ -2774,6 +3261,11 @@ function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch
|
|
|
2774
3261
|
res.json({ ok: true });
|
|
2775
3262
|
return;
|
|
2776
3263
|
}
|
|
3264
|
+
if (type === AS_TLS_MISMATCH) {
|
|
3265
|
+
onAsTlsMismatch?.();
|
|
3266
|
+
res.json({ ok: true });
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
2777
3269
|
if (typeof type !== "string" || !ALLOWED.includes(type)) {
|
|
2778
3270
|
res.status(400).json({ error: "Unsupported event type" });
|
|
2779
3271
|
return;
|
|
@@ -2873,6 +3365,7 @@ function startNotificationDispatcher(opts = {}) {
|
|
|
2873
3365
|
// src/lib/session-lock.ts
|
|
2874
3366
|
var SESSION_LOCKED_MESSAGE = "Your sign-in ended after 30 days or was revoked. Sign in again.";
|
|
2875
3367
|
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.";
|
|
3368
|
+
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
3369
|
function createSessionLock(deps) {
|
|
2877
3370
|
const { vault: vault2, port: port2 } = deps;
|
|
2878
3371
|
const notifyFn = deps.notifyFn ?? notify;
|
|
@@ -2907,6 +3400,23 @@ function createAsKeyMismatchLock(deps) {
|
|
|
2907
3400
|
notifyFn(title, AS_KEY_MISMATCH_MESSAGE, process.platform, url);
|
|
2908
3401
|
};
|
|
2909
3402
|
}
|
|
3403
|
+
function createAsTlsMismatchLock(deps) {
|
|
3404
|
+
const { vault: vault2, port: port2 } = deps;
|
|
3405
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
3406
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
3407
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
3408
|
+
return function lockOnAsTlsMismatch() {
|
|
3409
|
+
if (!vault2.isUnlocked()) return;
|
|
3410
|
+
vault2.lockAsTlsMismatch();
|
|
3411
|
+
console.error("[Control Plane] Authority Server TLS pin mismatch \u2014 gateway LOCKED");
|
|
3412
|
+
void unconfigureSessionFn("as-tls-mismatch").catch((err) => {
|
|
3413
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
3414
|
+
});
|
|
3415
|
+
emit("session-locked", { reason: "as-tls-mismatch", message: AS_TLS_MISMATCH_MESSAGE });
|
|
3416
|
+
const { title, url } = sessionExpiredNotification(port2);
|
|
3417
|
+
notifyFn(title, AS_TLS_MISMATCH_MESSAGE, process.platform, url);
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
2910
3420
|
|
|
2911
3421
|
// src/lib/session-expiry-scheduler.ts
|
|
2912
3422
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
@@ -2976,17 +3486,17 @@ function buildSessionHealth(vault2) {
|
|
|
2976
3486
|
}
|
|
2977
3487
|
|
|
2978
3488
|
// src/lib/denials-reader.ts
|
|
2979
|
-
import { existsSync as
|
|
2980
|
-
import { join as
|
|
3489
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
|
|
3490
|
+
import { join as join10 } from "path";
|
|
2981
3491
|
function loadDenials(dataDir, decrypt) {
|
|
2982
|
-
const encPath =
|
|
2983
|
-
const plainPath =
|
|
2984
|
-
if (
|
|
2985
|
-
const { blob } = JSON.parse(
|
|
3492
|
+
const encPath = join10(dataDir, "denials.enc.json");
|
|
3493
|
+
const plainPath = join10(dataDir, "denials.json");
|
|
3494
|
+
if (existsSync8(encPath)) {
|
|
3495
|
+
const { blob } = JSON.parse(readFileSync10(encPath, "utf-8"));
|
|
2986
3496
|
return JSON.parse(decrypt(blob)).records ?? [];
|
|
2987
3497
|
}
|
|
2988
|
-
if (
|
|
2989
|
-
return JSON.parse(
|
|
3498
|
+
if (existsSync8(plainPath)) {
|
|
3499
|
+
return JSON.parse(readFileSync10(plainPath, "utf-8")).records ?? [];
|
|
2990
3500
|
}
|
|
2991
3501
|
return [];
|
|
2992
3502
|
}
|
|
@@ -3000,74 +3510,6 @@ function selectDenials(records, opts = {}) {
|
|
|
3000
3510
|
return { count, records: out };
|
|
3001
3511
|
}
|
|
3002
3512
|
|
|
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
3513
|
// src/index.ts
|
|
3072
3514
|
(function migrateDataDir() {
|
|
3073
3515
|
if (process.env.SUVEREN_DATA_DIR) return;
|
|
@@ -3085,6 +3527,7 @@ function resolveAsUrl(dataDir, flag) {
|
|
|
3085
3527
|
})();
|
|
3086
3528
|
var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
|
|
3087
3529
|
var SP_URL = resolveAsUrl(DATA_DIR);
|
|
3530
|
+
validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
|
|
3088
3531
|
var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
|
|
3089
3532
|
var HAP_MODE = process.env.HAP_MODE ?? "personal";
|
|
3090
3533
|
var UI_DIST = process.env.HAP_UI_DIST ?? join11(import.meta.dirname ?? __dirname, "../../ui/dist");
|
|
@@ -3099,7 +3542,7 @@ var vault = new Vault(DATA_DIR);
|
|
|
3099
3542
|
vault.markBootLockedAsUrlChanged();
|
|
3100
3543
|
}
|
|
3101
3544
|
})();
|
|
3102
|
-
var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ??
|
|
3545
|
+
var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ?? randomBytes3(32).toString("hex");
|
|
3103
3546
|
setInternalSecret(internalSecret2);
|
|
3104
3547
|
var app = express();
|
|
3105
3548
|
var jsonParser = express.json();
|
|
@@ -3123,6 +3566,7 @@ function loginRateLimit(req, res, next) {
|
|
|
3123
3566
|
}
|
|
3124
3567
|
var lockExpiredSession = createSessionLock({ vault, port });
|
|
3125
3568
|
var lockAsKeyMismatch = createAsKeyMismatchLock({ vault, port });
|
|
3569
|
+
var lockAsTlsMismatch = createAsTlsMismatchLock({ vault, port });
|
|
3126
3570
|
var sessionExpiryScheduler = new SessionExpiryScheduler(
|
|
3127
3571
|
() => vault.getSessionExpiresAt(),
|
|
3128
3572
|
lockExpiredSession
|
|
@@ -3339,7 +3783,7 @@ app.get("/auth/oauth/:integrationId/health", authGuard, async (req, res) => {
|
|
|
3339
3783
|
}
|
|
3340
3784
|
});
|
|
3341
3785
|
app.get("/events", requireAllowedHost, requireAuthQueryOrHeader(vault), createEventsHandler());
|
|
3342
|
-
app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession, lockAsKeyMismatch));
|
|
3786
|
+
app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession, lockAsKeyMismatch, lockAsTlsMismatch));
|
|
3343
3787
|
app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
|
|
3344
3788
|
app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
|
|
3345
3789
|
app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
|
|
@@ -3348,7 +3792,7 @@ app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
|
|
|
3348
3792
|
app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRouter());
|
|
3349
3793
|
app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
|
|
3350
3794
|
app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
|
|
3351
|
-
app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault));
|
|
3795
|
+
app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
|
|
3352
3796
|
app.use("/api/evidence", authGuard, createEvidenceRouter());
|
|
3353
3797
|
app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
|
|
3354
3798
|
app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
|
|
@@ -3489,11 +3933,18 @@ app.get("/skipped-commitments", authGuard, async (_req, res) => {
|
|
|
3489
3933
|
});
|
|
3490
3934
|
app.get("/as-pairing", authGuard, (_req, res) => {
|
|
3491
3935
|
const pairing = readPairing(DATA_DIR);
|
|
3936
|
+
const paired = pairing !== null && pairing.asUrl === SP_URL;
|
|
3492
3937
|
res.json({
|
|
3493
3938
|
asUrl: SP_URL,
|
|
3494
|
-
paired
|
|
3495
|
-
fingerprint:
|
|
3496
|
-
pairedAt:
|
|
3939
|
+
paired,
|
|
3940
|
+
fingerprint: paired ? fingerprintOf(pairing.publicKeyHex) : null,
|
|
3941
|
+
pairedAt: paired ? pairing.pairedAt : null,
|
|
3942
|
+
// Opt-in TLS pinning (`config set pin-tls on`) — see as-tls-pin.ts.
|
|
3943
|
+
// `tlsPinFingerprint` uses the SAME display convention as the signing
|
|
3944
|
+
// key (formatPinFingerprint mirrors fingerprintOf's grouping), applied
|
|
3945
|
+
// to the already-computed SPKI digest rather than re-hashing a key.
|
|
3946
|
+
pinTlsEnabled: resolvePinTls(DATA_DIR),
|
|
3947
|
+
tlsPinFingerprint: paired && pairing.tlsSpkiPinHex ? formatPinFingerprint(pairing.tlsSpkiPinHex) : null
|
|
3497
3948
|
});
|
|
3498
3949
|
});
|
|
3499
3950
|
app.get("/denials", authGuard, (req, res) => {
|
|
@@ -3562,11 +4013,19 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
|
|
|
3562
4013
|
}
|
|
3563
4014
|
});
|
|
3564
4015
|
app.use("/api", authGuard);
|
|
4016
|
+
var pinnedProxyAgent = SP_URL.startsWith("https:") ? buildPinnedHttpsAgent(() => {
|
|
4017
|
+
const pin = readPairing(DATA_DIR);
|
|
4018
|
+
return {
|
|
4019
|
+
enforce: resolvePinTls(DATA_DIR),
|
|
4020
|
+
pinnedSpkiHex: pin && pin.asUrl === SP_URL ? pin.tlsSpkiPinHex : void 0
|
|
4021
|
+
};
|
|
4022
|
+
}) : void 0;
|
|
3565
4023
|
app.use(
|
|
3566
4024
|
createProxyMiddleware({
|
|
3567
4025
|
target: SP_URL,
|
|
3568
4026
|
changeOrigin: true,
|
|
3569
4027
|
pathFilter: "/api",
|
|
4028
|
+
...pinnedProxyAgent ? { agent: pinnedProxyAgent } : {},
|
|
3570
4029
|
on: {
|
|
3571
4030
|
proxyReq: (proxyReq) => {
|
|
3572
4031
|
const cookie = vault.getSpCookie();
|
|
@@ -3574,6 +4033,21 @@ app.use(
|
|
|
3574
4033
|
proxyReq.setHeader("Cookie", cookie);
|
|
3575
4034
|
}
|
|
3576
4035
|
},
|
|
4036
|
+
// A pin check failure means the connection never completed its TLS
|
|
4037
|
+
// handshake — nothing (not even this call's own cookie) reached the
|
|
4038
|
+
// network. Lock the gateway with the real reason, loudly; an ordinary
|
|
4039
|
+
// network error (AS outage) is NOT a pin failure (see isPinCheckError
|
|
4040
|
+
// / as-tls-pin.ts's module doc comment on outage misclassification)
|
|
4041
|
+
// and falls through to http-proxy-middleware's default error handling.
|
|
4042
|
+
error: (err, _req, res) => {
|
|
4043
|
+
if (!isPinCheckError(err)) return;
|
|
4044
|
+
console.error("[Control Plane] /api proxy: TLS pin mismatch \u2014 gateway LOCKED:", err.message);
|
|
4045
|
+
lockAsTlsMismatch();
|
|
4046
|
+
if (res && "writeHead" in res && !res.headersSent) {
|
|
4047
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
4048
|
+
res.end(JSON.stringify({ error: "as_tls_mismatch", message: err.message }));
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
3577
4051
|
proxyRes: (proxyRes, req) => {
|
|
3578
4052
|
const status = proxyRes.statusCode ?? 0;
|
|
3579
4053
|
if (status === 401 && vault.isUnlocked()) {
|
|
@@ -3627,7 +4101,7 @@ function detectRunningVersion() {
|
|
|
3627
4101
|
const bundlePkg = join11(dir, "..", "..", "package.json");
|
|
3628
4102
|
if (existsSync9(bundlePkg)) {
|
|
3629
4103
|
try {
|
|
3630
|
-
const pkg = JSON.parse(
|
|
4104
|
+
const pkg = JSON.parse(readFileSync11(bundlePkg, "utf8"));
|
|
3631
4105
|
if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
|
|
3632
4106
|
return pkg.version;
|
|
3633
4107
|
}
|