@suveren/gateway 0.25.1 → 0.27.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/bin/suveren-gateway.js +10 -1
- package/content/guides/3-mandates.md +1 -1
- package/dist/control-plane/index.mjs +845 -449
- package/dist/mcp-server/http.mjs +306 -50
- package/dist/ui/assets/index-CIR3mXNv.js +120 -0
- package/dist/ui/assets/index-DeEzJCqI.css +1 -0
- package/dist/ui/index.html +2 -2
- package/lib/policy.d.mts +3 -1
- package/lib/policy.mjs +28 -2
- package/node_modules/ip-address/README.md +70 -70
- package/node_modules/ip-address/dist/ipv6.js +7 -4
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/dist/v6/constants.d.ts +3 -0
- package/node_modules/ip-address/dist/v6/constants.js +3 -0
- package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/ip-address/package.json +2 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-Ccst5MQ7.css +0 -1
- package/dist/ui/assets/index-CxVyYf7C.js +0 -120
|
@@ -127,6 +127,13 @@ var Vault = class {
|
|
|
127
127
|
this.clearKey();
|
|
128
128
|
this.lockedReason = "as-tls-mismatch";
|
|
129
129
|
}
|
|
130
|
+
/** Lock because this is a managed gateway (IT policy `Team`) and the
|
|
131
|
+
* signed-in account's active workspace is no longer that team — see
|
|
132
|
+
* lib/managed-team.ts. Mirrors lockAsKeyMismatch() exactly. */
|
|
133
|
+
lockManagedWrongTeam() {
|
|
134
|
+
this.clearKey();
|
|
135
|
+
this.lockedReason = "managed-wrong-team";
|
|
136
|
+
}
|
|
130
137
|
/**
|
|
131
138
|
* Boot-time only: record that the gateway starts locked because the
|
|
132
139
|
* resolved Authority Server URL differs from the one it was last paired
|
|
@@ -1299,7 +1306,7 @@ import { dirname as dirname4, join as join6 } from "path";
|
|
|
1299
1306
|
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
1300
1307
|
import { join as join5 } from "path";
|
|
1301
1308
|
import { spawnSync } from "child_process";
|
|
1302
|
-
var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod"];
|
|
1309
|
+
var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod", "Team"];
|
|
1303
1310
|
var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
|
|
1304
1311
|
function registryKeyPath(hive) {
|
|
1305
1312
|
const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
|
|
@@ -1335,6 +1342,11 @@ function validateCaFile(candidate) {
|
|
|
1335
1342
|
if (!existsSync4(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
|
|
1336
1343
|
return { ok: true, path: trimmed };
|
|
1337
1344
|
}
|
|
1345
|
+
function validateTeamId(candidate) {
|
|
1346
|
+
const trimmed = candidate.trim();
|
|
1347
|
+
if (!trimmed) return { ok: false, error: "The team id is empty." };
|
|
1348
|
+
return { ok: true, id: trimmed };
|
|
1349
|
+
}
|
|
1338
1350
|
function validateProxyUrl(candidate) {
|
|
1339
1351
|
const trimmed = candidate.trim();
|
|
1340
1352
|
if (!trimmed) return { ok: false, error: "The proxy URL is empty." };
|
|
@@ -1508,6 +1520,14 @@ function readPolicy() {
|
|
|
1508
1520
|
policy.installMethod = "managed";
|
|
1509
1521
|
locked.add("installMethod");
|
|
1510
1522
|
}
|
|
1523
|
+
if (merged.Team !== void 0) {
|
|
1524
|
+
const v = validateTeamId(String(merged.Team));
|
|
1525
|
+
if (!v.ok) {
|
|
1526
|
+
throw new Error(`Invalid policy Team ("${String(merged.Team)}") from ${sourceOf.Team}: ${v.error}`);
|
|
1527
|
+
}
|
|
1528
|
+
policy.team = v.id;
|
|
1529
|
+
locked.add("team");
|
|
1530
|
+
}
|
|
1511
1531
|
cached = { policy, locked };
|
|
1512
1532
|
return cached;
|
|
1513
1533
|
}
|
|
@@ -1624,151 +1644,443 @@ function clearPinTlsExpectedFingerprint(dataDir) {
|
|
|
1624
1644
|
writeAsConfig(dataDir, { pinTlsExpectedFingerprint: void 0 });
|
|
1625
1645
|
}
|
|
1626
1646
|
|
|
1627
|
-
// src/
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1647
|
+
// src/lib/managed-team.ts
|
|
1648
|
+
async function activeTeamId(asUrl, cookie, pinning) {
|
|
1649
|
+
const { res } = await fetchAs(
|
|
1650
|
+
`${asUrl}/api/groups/me`,
|
|
1651
|
+
{ headers: { Cookie: cookie }, signal: AbortSignal.timeout(1e4) },
|
|
1652
|
+
pinning
|
|
1653
|
+
);
|
|
1654
|
+
if (!res.ok) throw new Error(`GET /api/groups/me answered ${res.status}`);
|
|
1655
|
+
const data = await res.json();
|
|
1656
|
+
return data.group?.id ?? null;
|
|
1657
|
+
}
|
|
1658
|
+
var ManagedTeamScheduler = class {
|
|
1659
|
+
constructor(check2, opts = {}) {
|
|
1660
|
+
this.check = check2;
|
|
1661
|
+
this.setIntervalFn = opts.setIntervalFn ?? setInterval;
|
|
1662
|
+
this.clearIntervalFn = opts.clearIntervalFn ?? clearInterval;
|
|
1663
|
+
this.intervalMs = opts.intervalMs ?? 5 * 6e4;
|
|
1664
|
+
}
|
|
1665
|
+
timer = null;
|
|
1666
|
+
setIntervalFn;
|
|
1667
|
+
clearIntervalFn;
|
|
1668
|
+
intervalMs;
|
|
1669
|
+
start() {
|
|
1670
|
+
if (this.timer) return;
|
|
1671
|
+
this.timer = this.setIntervalFn(() => {
|
|
1672
|
+
void this.check();
|
|
1673
|
+
}, this.intervalMs);
|
|
1674
|
+
}
|
|
1675
|
+
stop() {
|
|
1676
|
+
if (this.timer) this.clearIntervalFn(this.timer);
|
|
1677
|
+
this.timer = null;
|
|
1678
|
+
}
|
|
1679
|
+
/** Test-only: run the check immediately, bypassing the timer. */
|
|
1680
|
+
runNow() {
|
|
1681
|
+
return this.check();
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// src/lib/event-bus.ts
|
|
1686
|
+
var EventBus = class {
|
|
1687
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1688
|
+
/**
|
|
1689
|
+
* Subscribe to all bus events.
|
|
1690
|
+
* Returns an unsubscribe function — callers MUST call it on disconnect / unmount
|
|
1691
|
+
* to avoid listener leaks.
|
|
1692
|
+
*/
|
|
1693
|
+
subscribe(handler) {
|
|
1694
|
+
this.listeners.add(handler);
|
|
1695
|
+
return () => {
|
|
1696
|
+
this.listeners.delete(handler);
|
|
1650
1697
|
};
|
|
1651
1698
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
} else {
|
|
1661
|
-
const fetched = await fetchAsPublicKey(asUrl);
|
|
1662
|
-
if (!fetched.ok) {
|
|
1663
|
-
return { ok: false, error: "as_unreachable", message: fetched.message };
|
|
1699
|
+
emit(type, payload) {
|
|
1700
|
+
const event = { type, payload };
|
|
1701
|
+
for (const handler of this.listeners) {
|
|
1702
|
+
try {
|
|
1703
|
+
handler(event);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
console.error("[EventBus] subscriber threw:", err);
|
|
1706
|
+
}
|
|
1664
1707
|
}
|
|
1665
|
-
candidateKey = fetched.publicKeyHex;
|
|
1666
1708
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1709
|
+
/** How many listeners are currently subscribed. Useful for tests and health checks. */
|
|
1710
|
+
get listenerCount() {
|
|
1711
|
+
return this.listeners.size;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
var eventBus = new EventBus();
|
|
1715
|
+
|
|
1716
|
+
// src/lib/desktop-notify.ts
|
|
1717
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1718
|
+
|
|
1719
|
+
// src/lib/notifier-icon.ts
|
|
1720
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
1721
|
+
import { homedir as homedir2 } from "os";
|
|
1722
|
+
import { join as join7, dirname as dirname5 } from "path";
|
|
1723
|
+
var ICON_BASE64 = "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";
|
|
1724
|
+
function ensureNotifierIcon(dataDir) {
|
|
1673
1725
|
try {
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
}
|
|
1680
|
-
if (err instanceof AsChallengeUnreachableError) {
|
|
1681
|
-
return { ok: false, error: "as_unverified", message: `${err.message}. Refusing to sign in.` };
|
|
1726
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join7(homedir2(), ".suveren");
|
|
1727
|
+
const path = join7(dir, "notifier-icon.png");
|
|
1728
|
+
if (!existsSync6(path)) {
|
|
1729
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
1730
|
+
writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
|
|
1682
1731
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1732
|
+
return path;
|
|
1733
|
+
} catch {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/lib/desktop-notify.ts
|
|
1739
|
+
function osaQuote(value) {
|
|
1740
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1741
|
+
}
|
|
1742
|
+
function psQuote(value) {
|
|
1743
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1744
|
+
}
|
|
1745
|
+
function xmlEscape(value) {
|
|
1746
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1747
|
+
}
|
|
1748
|
+
var WINDOWS_AUMID = "Suveren.Gateway";
|
|
1749
|
+
function windowsToastScript(title, message, url, iconPath) {
|
|
1750
|
+
const toastXml = `<toast${url ? ` activationType="protocol" launch="${xmlEscape(url)}"` : ""}><visual><binding template="ToastGeneric"><text>${xmlEscape(title)}</text><text>${xmlEscape(message)}</text>` + (iconPath ? `<image placement="appLogoOverride" src="${xmlEscape(iconPath)}"/>` : "") + `</binding></visual><audio src="ms-winsoundevent:Notification.Default"/></toast>`;
|
|
1751
|
+
return `$ErrorActionPreference='SilentlyContinue'; $aumid=${psQuote(WINDOWS_AUMID)}; $reg="HKCU:\\Software\\Classes\\AppUserModelId\\$aumid"; if (-not (Test-Path $reg)) { New-Item -Path $reg -Force | Out-Null }; New-ItemProperty -Path $reg -Name DisplayName -Value 'Suveren' -PropertyType String -Force | Out-Null; ` + (iconPath ? `New-ItemProperty -Path $reg -Name IconUri -Value ${psQuote(iconPath)} -PropertyType String -Force | Out-Null; ` : "") + `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] | Out-Null; $doc = New-Object Windows.Data.Xml.Dom.XmlDocument; $doc.LoadXml(${psQuote(toastXml)}); $toast = New-Object Windows.UI.Notifications.ToastNotification $doc; [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($aumid).Show($toast)`;
|
|
1752
|
+
}
|
|
1753
|
+
var MAC_SOUND = "Glass";
|
|
1754
|
+
function buildNotifyCommand(platform, title, message, options = {}) {
|
|
1755
|
+
const { url, hasTerminalNotifier: hasTerminalNotifier2 = false, iconPath } = options;
|
|
1756
|
+
switch (platform) {
|
|
1757
|
+
case "darwin":
|
|
1758
|
+
if (url && hasTerminalNotifier2) {
|
|
1685
1759
|
return {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1760
|
+
cmd: "terminal-notifier",
|
|
1761
|
+
args: [
|
|
1762
|
+
"-title",
|
|
1763
|
+
title,
|
|
1764
|
+
"-message",
|
|
1765
|
+
message,
|
|
1766
|
+
"-open",
|
|
1767
|
+
url,
|
|
1768
|
+
"-sound",
|
|
1769
|
+
MAC_SOUND,
|
|
1770
|
+
...iconPath ? ["-appIcon", iconPath] : [],
|
|
1771
|
+
"-group",
|
|
1772
|
+
"ai.suveren.gateway"
|
|
1773
|
+
]
|
|
1689
1774
|
};
|
|
1690
1775
|
}
|
|
1691
1776
|
return {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1777
|
+
cmd: "osascript",
|
|
1778
|
+
args: [
|
|
1779
|
+
"-e",
|
|
1780
|
+
`display notification ${osaQuote(message)} with title ${osaQuote(title)} sound name ${osaQuote(MAC_SOUND)}`
|
|
1781
|
+
]
|
|
1695
1782
|
};
|
|
1696
|
-
|
|
1697
|
-
|
|
1783
|
+
case "win32":
|
|
1784
|
+
return {
|
|
1785
|
+
cmd: "powershell",
|
|
1786
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", windowsToastScript(title, message, url, iconPath)]
|
|
1787
|
+
};
|
|
1788
|
+
case "linux":
|
|
1789
|
+
return {
|
|
1790
|
+
cmd: "notify-send",
|
|
1791
|
+
args: [
|
|
1792
|
+
"--app-name=Suveren",
|
|
1793
|
+
...iconPath ? ["-i", iconPath] : [],
|
|
1794
|
+
title,
|
|
1795
|
+
message
|
|
1796
|
+
]
|
|
1797
|
+
};
|
|
1798
|
+
default:
|
|
1799
|
+
return null;
|
|
1698
1800
|
}
|
|
1801
|
+
}
|
|
1802
|
+
function lockedNotification(port2) {
|
|
1803
|
+
const url = `http://localhost:${port2}`;
|
|
1699
1804
|
return {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
// an existing pin was enforced instead — see fetchAs) wins; otherwise
|
|
1704
|
-
// whatever was already on file carries through unchanged.
|
|
1705
|
-
tlsSpkiPinHex: capturedTlsSpkiHex ?? existingTlsPin
|
|
1805
|
+
title: "Suveren is running but locked",
|
|
1806
|
+
message: `Your agent has no authority until you unlock it. Open ${url} and enter your API key.`,
|
|
1807
|
+
url
|
|
1706
1808
|
};
|
|
1707
1809
|
}
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1810
|
+
function sessionExpiredNotification(port2) {
|
|
1811
|
+
const url = `http://localhost:${port2}`;
|
|
1812
|
+
return {
|
|
1813
|
+
title: "Suveren Gateway locked",
|
|
1814
|
+
message: "Your sign-in has ended. Sign in again so your agents can act.",
|
|
1815
|
+
url
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
var terminalNotifierCache = null;
|
|
1819
|
+
function hasTerminalNotifier() {
|
|
1820
|
+
if (terminalNotifierCache !== null) return terminalNotifierCache;
|
|
1821
|
+
try {
|
|
1822
|
+
const probe = spawnSync2("terminal-notifier", ["-help"], { stdio: "ignore" });
|
|
1823
|
+
terminalNotifierCache = probe.error === void 0;
|
|
1824
|
+
} catch {
|
|
1825
|
+
terminalNotifierCache = false;
|
|
1826
|
+
}
|
|
1827
|
+
return terminalNotifierCache;
|
|
1828
|
+
}
|
|
1829
|
+
function notifySpawnOptions(platform) {
|
|
1830
|
+
return {
|
|
1831
|
+
detached: platform !== "win32",
|
|
1832
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function notify(title, message, platform = process.platform, url) {
|
|
1836
|
+
const command = buildNotifyCommand(platform, title, message, {
|
|
1837
|
+
url,
|
|
1838
|
+
hasTerminalNotifier: platform === "darwin" ? hasTerminalNotifier() : false,
|
|
1839
|
+
// Windows and Linux show it on the toast itself; macOS only via
|
|
1840
|
+
// terminal-notifier. Failure to write it must never block the sound.
|
|
1841
|
+
iconPath: ensureNotifierIcon()
|
|
1842
|
+
});
|
|
1843
|
+
if (!command) return;
|
|
1844
|
+
try {
|
|
1845
|
+
const child = spawn(command.cmd, command.args, notifySpawnOptions(platform));
|
|
1846
|
+
let stderr = "";
|
|
1847
|
+
child.stderr?.on("data", (d) => {
|
|
1848
|
+
stderr += d.toString().slice(0, 500);
|
|
1849
|
+
});
|
|
1850
|
+
child.on("close", (code) => {
|
|
1851
|
+
if (code !== 0) {
|
|
1852
|
+
console.error(`[notify] ${command.cmd} exited ${code}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
child.on("error", () => {
|
|
1856
|
+
});
|
|
1857
|
+
child.unref();
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/lib/session-lock.ts
|
|
1863
|
+
var SESSION_LOCKED_MESSAGE = "Your sign-in ended after 30 days or was revoked. Sign in again.";
|
|
1864
|
+
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.";
|
|
1865
|
+
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.";
|
|
1866
|
+
function managedWrongTeamMessage(team) {
|
|
1867
|
+
return `This gateway is set up by your IT for team ${team}. Your account is no longer in this team. Sign in with your team account, or ask your IT.`;
|
|
1868
|
+
}
|
|
1869
|
+
function createSessionLock(deps) {
|
|
1870
|
+
const { vault: vault2, port: port2 } = deps;
|
|
1871
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
1872
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
1873
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
1874
|
+
return function lockExpiredSession2() {
|
|
1875
|
+
if (!vault2.isUnlocked()) return;
|
|
1876
|
+
vault2.lockExpired();
|
|
1877
|
+
console.error("[Control Plane] Authority Server session ended \u2014 gateway LOCKED");
|
|
1878
|
+
void unconfigureSessionFn().catch((err) => {
|
|
1879
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
1880
|
+
});
|
|
1881
|
+
emit("session-locked", { reason: "expired", message: SESSION_LOCKED_MESSAGE });
|
|
1882
|
+
const { title, message, url } = sessionExpiredNotification(port2);
|
|
1883
|
+
notifyFn(title, message, process.platform, url);
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function createAsKeyMismatchLock(deps) {
|
|
1887
|
+
const { vault: vault2, port: port2 } = deps;
|
|
1888
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
1889
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
1890
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
1891
|
+
return function lockOnAsKeyMismatch() {
|
|
1892
|
+
if (!vault2.isUnlocked()) return;
|
|
1893
|
+
vault2.lockAsKeyMismatch();
|
|
1894
|
+
console.error("[Control Plane] Authority Server key mismatch \u2014 gateway LOCKED");
|
|
1895
|
+
void unconfigureSessionFn("as-key-mismatch").catch((err) => {
|
|
1896
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
1897
|
+
});
|
|
1898
|
+
emit("session-locked", { reason: "as-key-mismatch", message: AS_KEY_MISMATCH_MESSAGE });
|
|
1899
|
+
const { title, url } = sessionExpiredNotification(port2);
|
|
1900
|
+
notifyFn(title, AS_KEY_MISMATCH_MESSAGE, process.platform, url);
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function createAsTlsMismatchLock(deps) {
|
|
1904
|
+
const { vault: vault2, port: port2 } = deps;
|
|
1905
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
1906
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
1907
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
1908
|
+
return function lockOnAsTlsMismatch() {
|
|
1909
|
+
if (!vault2.isUnlocked()) return;
|
|
1910
|
+
vault2.lockAsTlsMismatch();
|
|
1911
|
+
console.error("[Control Plane] Authority Server TLS pin mismatch \u2014 gateway LOCKED");
|
|
1912
|
+
void unconfigureSessionFn("as-tls-mismatch").catch((err) => {
|
|
1913
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
1914
|
+
});
|
|
1915
|
+
emit("session-locked", { reason: "as-tls-mismatch", message: AS_TLS_MISMATCH_MESSAGE });
|
|
1916
|
+
const { title, url } = sessionExpiredNotification(port2);
|
|
1917
|
+
notifyFn(title, AS_TLS_MISMATCH_MESSAGE, process.platform, url);
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function createManagedWrongTeamLock(deps) {
|
|
1921
|
+
const { vault: vault2, port: port2 } = deps;
|
|
1922
|
+
const notifyFn = deps.notifyFn ?? notify;
|
|
1923
|
+
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
1924
|
+
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
1925
|
+
return function lockOnManagedWrongTeam(team) {
|
|
1926
|
+
if (!vault2.isUnlocked()) return;
|
|
1927
|
+
vault2.lockManagedWrongTeam();
|
|
1928
|
+
console.error("[Control Plane] Managed gateway: signed-in account is no longer in the locked team \u2014 gateway LOCKED");
|
|
1929
|
+
const message = managedWrongTeamMessage(team);
|
|
1930
|
+
void unconfigureSessionFn("managed-wrong-team").catch((err) => {
|
|
1931
|
+
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
1932
|
+
});
|
|
1933
|
+
emit("session-locked", { reason: "managed-wrong-team", message });
|
|
1934
|
+
const { title, url } = sessionExpiredNotification(port2);
|
|
1935
|
+
notifyFn(title, message, process.platform, url);
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/routes/auth.ts
|
|
1940
|
+
var DEFAULT_SP_URL = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
|
|
1941
|
+
async function fetchAsPublicKey(asUrl) {
|
|
1942
|
+
try {
|
|
1943
|
+
const { res } = await fetchAs(`${asUrl}/api/as/pubkey`, { signal: AbortSignal.timeout(5e3) }, { enforce: false });
|
|
1944
|
+
if (!res.ok) {
|
|
1945
|
+
return {
|
|
1946
|
+
ok: false,
|
|
1947
|
+
message: `Could not reach the Authority Server at ${asUrl} to read its signing key (HTTP ${res.status}). Refusing to sign in.`
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
const body = await res.json().catch(() => null);
|
|
1951
|
+
if (!body || typeof body.publicKey !== "string" || !body.publicKey) {
|
|
1952
|
+
return {
|
|
1953
|
+
ok: false,
|
|
1954
|
+
message: `The Authority Server at ${asUrl} returned a malformed signing key. Refusing to sign in.`
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
return { ok: true, publicKeyHex: body.publicKey };
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
return {
|
|
1960
|
+
ok: false,
|
|
1961
|
+
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.`
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async function checkAsKeyBeforeLogin(asUrl, dataDir) {
|
|
1966
|
+
const existing = readPairing(dataDir);
|
|
1967
|
+
const pinnedKey = existing && existing.asUrl === asUrl ? existing.publicKeyHex : void 0;
|
|
1968
|
+
const existingTlsPin = (existing && existing.asUrl === asUrl ? existing.tlsSpkiPinHex : void 0) ?? resolvePinTlsExpectedFingerprint(dataDir);
|
|
1969
|
+
let candidateKey;
|
|
1970
|
+
if (pinnedKey) {
|
|
1971
|
+
candidateKey = pinnedKey;
|
|
1972
|
+
} else {
|
|
1973
|
+
const fetched = await fetchAsPublicKey(asUrl);
|
|
1974
|
+
if (!fetched.ok) {
|
|
1975
|
+
return { ok: false, error: "as_unreachable", message: fetched.message };
|
|
1976
|
+
}
|
|
1977
|
+
candidateKey = fetched.publicKeyHex;
|
|
1978
|
+
}
|
|
1979
|
+
const pinning = {
|
|
1980
|
+
enforce: resolvePinTls(dataDir),
|
|
1981
|
+
pinnedSpkiHex: existingTlsPin,
|
|
1982
|
+
capture: true
|
|
1983
|
+
};
|
|
1984
|
+
let capturedTlsSpkiHex;
|
|
1985
|
+
try {
|
|
1986
|
+
const result = await verifyAsHoldsKey(asUrl, candidateKey, pinning);
|
|
1987
|
+
capturedTlsSpkiHex = result.capturedTlsSpkiHex;
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
if (err instanceof AsTlsMismatchError) {
|
|
1990
|
+
return { ok: false, error: "as_tls_mismatch", message: err.message };
|
|
1991
|
+
}
|
|
1992
|
+
if (err instanceof AsChallengeUnreachableError) {
|
|
1993
|
+
return { ok: false, error: "as_unverified", message: `${err.message}. Refusing to sign in.` };
|
|
1994
|
+
}
|
|
1995
|
+
if (err instanceof AsChallengeInvalidError) {
|
|
1996
|
+
if (pinnedKey) {
|
|
1997
|
+
return {
|
|
1998
|
+
ok: false,
|
|
1999
|
+
error: "as_key_mismatch",
|
|
2000
|
+
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.`
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
return {
|
|
2004
|
+
ok: false,
|
|
2005
|
+
error: "as_unverified",
|
|
2006
|
+
message: `The Authority Server at ${asUrl} could not prove it holds the signing key it presented \u2014 ${err.message}. Refusing to sign in.`
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
throw err;
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
ok: true,
|
|
2013
|
+
publicKeyHex: candidateKey,
|
|
2014
|
+
// A fresh capture (always present for https, unless pin-tls is on and
|
|
2015
|
+
// an existing pin was enforced instead — see fetchAs) wins; otherwise
|
|
2016
|
+
// whatever was already on file carries through unchanged.
|
|
2017
|
+
tlsSpkiPinHex: capturedTlsSpkiHex ?? existingTlsPin
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished, pairing, lockAsTlsMismatch2 = () => {
|
|
2021
|
+
}, managedTeamId) {
|
|
2022
|
+
const router = Router();
|
|
2023
|
+
const SP_URL2 = pairing?.asUrl ?? DEFAULT_SP_URL;
|
|
2024
|
+
router.post("/login", loginRateLimit2, async (req, res) => {
|
|
2025
|
+
const apiKey = req.headers["x-api-key"] || req.body?.apiKey;
|
|
2026
|
+
const confirmWipe = req.body?.confirmWipe === true;
|
|
2027
|
+
if (!apiKey) {
|
|
2028
|
+
res.status(400).json({ error: "Missing API key (X-API-Key header or body.apiKey)" });
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
try {
|
|
2032
|
+
let keyCheck = null;
|
|
2033
|
+
if (pairing) {
|
|
2034
|
+
keyCheck = await checkAsKeyBeforeLogin(pairing.asUrl, pairing.dataDir);
|
|
2035
|
+
if (!keyCheck.ok) {
|
|
2036
|
+
const status = keyCheck.error === "as_key_mismatch" || keyCheck.error === "as_tls_mismatch" ? 409 : 502;
|
|
2037
|
+
res.status(status).json({
|
|
2038
|
+
error: keyCheck.error,
|
|
2039
|
+
message: keyCheck.message
|
|
2040
|
+
});
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const sessionPinning = {
|
|
2045
|
+
enforce: Boolean(pairing && resolvePinTls(pairing.dataDir)),
|
|
2046
|
+
pinnedSpkiHex: keyCheck?.tlsSpkiPinHex
|
|
2047
|
+
};
|
|
2048
|
+
let spRes;
|
|
2049
|
+
try {
|
|
2050
|
+
const result = await fetchAs(
|
|
2051
|
+
`${SP_URL2}/api/auth/session`,
|
|
2052
|
+
{
|
|
2053
|
+
method: "POST",
|
|
2054
|
+
headers: {
|
|
2055
|
+
"Content-Type": "application/json",
|
|
2056
|
+
"X-API-Key": apiKey,
|
|
2057
|
+
// The AS grants a 30-day gateway session ONLY to a login that
|
|
2058
|
+
// identifies itself this way; without it, a 24-hour browser
|
|
2059
|
+
// session (see doc at the top of this file).
|
|
2060
|
+
...clientVersionHeaders()
|
|
2061
|
+
},
|
|
2062
|
+
// Belt-and-suspenders on top of fetchAs's own default bound
|
|
2063
|
+
// (as-tls-pin.ts): this is the exact call that hung in
|
|
2064
|
+
// production (2026-10-02) with NO timeout at all — a dead
|
|
2065
|
+
// keep-alive socket left the whole login request stuck forever.
|
|
2066
|
+
signal: AbortSignal.timeout(1e4)
|
|
2067
|
+
},
|
|
2068
|
+
sessionPinning
|
|
2069
|
+
);
|
|
2070
|
+
spRes = result.res;
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
if (err instanceof AsTlsMismatchError) {
|
|
2073
|
+
res.status(409).json({ error: "as_tls_mismatch", message: err.message });
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
res.status(502).json({
|
|
2077
|
+
error: "as_unreachable",
|
|
2078
|
+
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.`
|
|
2079
|
+
});
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (!spRes.ok) {
|
|
2083
|
+
const err = await spRes.json().catch(() => ({ error: "Invalid API key" }));
|
|
1772
2084
|
res.status(spRes.status).json(err);
|
|
1773
2085
|
return;
|
|
1774
2086
|
}
|
|
@@ -1788,6 +2100,25 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
1788
2100
|
const data = await spRes.json();
|
|
1789
2101
|
const setCookieHeaders = spRes.headers.getSetCookie?.() ?? [];
|
|
1790
2102
|
const sessionCookie = setCookieHeaders.join("; ");
|
|
2103
|
+
if (managedTeamId) {
|
|
2104
|
+
let activeTeam;
|
|
2105
|
+
try {
|
|
2106
|
+
activeTeam = await activeTeamId(SP_URL2, sessionCookie, sessionPinning);
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
res.status(502).json({
|
|
2109
|
+
error: "as_unreachable",
|
|
2110
|
+
message: `Could not confirm your team membership with the Authority Server \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to sign in.`
|
|
2111
|
+
});
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (activeTeam !== managedTeamId) {
|
|
2115
|
+
res.status(403).json({
|
|
2116
|
+
error: "managed_wrong_team",
|
|
2117
|
+
message: managedWrongTeamMessage(managedTeamId)
|
|
2118
|
+
});
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
1791
2122
|
vault2.setSpCookie(sessionCookie);
|
|
1792
2123
|
await vault2.deriveAndSetKey(apiKey);
|
|
1793
2124
|
if (vault2.isVaultFromDifferentKey()) {
|
|
@@ -2561,17 +2892,17 @@ async function testAIConnectivity(config) {
|
|
|
2561
2892
|
}
|
|
2562
2893
|
|
|
2563
2894
|
// src/lib/agent-brief-store.ts
|
|
2564
|
-
import { existsSync as
|
|
2565
|
-
import { homedir as
|
|
2566
|
-
import { join as
|
|
2895
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
2896
|
+
import { homedir as homedir3 } from "os";
|
|
2897
|
+
import { join as join8 } from "path";
|
|
2567
2898
|
var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
|
|
2568
2899
|
function agentBriefPath() {
|
|
2569
|
-
const dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
2570
|
-
return
|
|
2900
|
+
const dataDir = process.env.SUVEREN_DATA_DIR ?? join8(homedir3(), ".suveren");
|
|
2901
|
+
return join8(dataDir, "context.md");
|
|
2571
2902
|
}
|
|
2572
2903
|
function readAgentBrief() {
|
|
2573
2904
|
const filePath = agentBriefPath();
|
|
2574
|
-
if (!
|
|
2905
|
+
if (!existsSync7(filePath)) return "";
|
|
2575
2906
|
return readFileSync8(filePath, "utf-8");
|
|
2576
2907
|
}
|
|
2577
2908
|
|
|
@@ -2853,39 +3184,6 @@ async function backfillReadPolicyDefaults(data) {
|
|
|
2853
3184
|
|
|
2854
3185
|
// src/routes/mcp.ts
|
|
2855
3186
|
import { Router as Router5 } from "express";
|
|
2856
|
-
|
|
2857
|
-
// src/lib/event-bus.ts
|
|
2858
|
-
var EventBus = class {
|
|
2859
|
-
listeners = /* @__PURE__ */ new Set();
|
|
2860
|
-
/**
|
|
2861
|
-
* Subscribe to all bus events.
|
|
2862
|
-
* Returns an unsubscribe function — callers MUST call it on disconnect / unmount
|
|
2863
|
-
* to avoid listener leaks.
|
|
2864
|
-
*/
|
|
2865
|
-
subscribe(handler) {
|
|
2866
|
-
this.listeners.add(handler);
|
|
2867
|
-
return () => {
|
|
2868
|
-
this.listeners.delete(handler);
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
2871
|
-
emit(type, payload) {
|
|
2872
|
-
const event = { type, payload };
|
|
2873
|
-
for (const handler of this.listeners) {
|
|
2874
|
-
try {
|
|
2875
|
-
handler(event);
|
|
2876
|
-
} catch (err) {
|
|
2877
|
-
console.error("[EventBus] subscriber threw:", err);
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
/** How many listeners are currently subscribed. Useful for tests and health checks. */
|
|
2882
|
-
get listenerCount() {
|
|
2883
|
-
return this.listeners.size;
|
|
2884
|
-
}
|
|
2885
|
-
};
|
|
2886
|
-
var eventBus = new EventBus();
|
|
2887
|
-
|
|
2888
|
-
// src/routes/mcp.ts
|
|
2889
3187
|
function createMCPRouter() {
|
|
2890
3188
|
const router = Router5();
|
|
2891
3189
|
router.get("/health", async (_req, res) => {
|
|
@@ -2958,7 +3256,7 @@ function createMCPRouter() {
|
|
|
2958
3256
|
// src/routes/autostart.ts
|
|
2959
3257
|
import { Router as Router6 } from "express";
|
|
2960
3258
|
import { execFile } from "child_process";
|
|
2961
|
-
import { existsSync as
|
|
3259
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2962
3260
|
import { dirname as pathDirname, resolve } from "path";
|
|
2963
3261
|
import { fileURLToPath } from "url";
|
|
2964
3262
|
import { promisify } from "util";
|
|
@@ -2979,7 +3277,7 @@ function findCli(dirname10 = MODULE_DIR) {
|
|
|
2979
3277
|
// resolves differently between a shell and a login service — and it would
|
|
2980
3278
|
// mask a genuinely missing CLI whenever the cwd happened to be the repo.
|
|
2981
3279
|
];
|
|
2982
|
-
return candidates.find(
|
|
3280
|
+
return candidates.find(existsSync8) ?? null;
|
|
2983
3281
|
}
|
|
2984
3282
|
async function cli(args) {
|
|
2985
3283
|
const bin = findCli();
|
|
@@ -2998,208 +3296,62 @@ async function cli(args) {
|
|
|
2998
3296
|
function safe(handler) {
|
|
2999
3297
|
return (req, res) => {
|
|
3000
3298
|
handler(req, res).catch((err) => {
|
|
3001
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3002
|
-
console.error("[Autostart] handler failed:", message);
|
|
3003
|
-
if (!res.headersSent) res.status(500).json({ error: "autostart_failed", message });
|
|
3004
|
-
});
|
|
3005
|
-
};
|
|
3006
|
-
}
|
|
3007
|
-
function createAutostartRouter() {
|
|
3008
|
-
const router = Router6();
|
|
3009
|
-
router.get("/", safe(async (_req, res) => {
|
|
3010
|
-
const platform = process.platform;
|
|
3011
|
-
const supported = SUPPORTED.has(platform);
|
|
3012
|
-
const bin = findCli();
|
|
3013
|
-
if (!supported || !bin) {
|
|
3014
|
-
res.json({
|
|
3015
|
-
supported: false,
|
|
3016
|
-
installed: false,
|
|
3017
|
-
platform,
|
|
3018
|
-
// Be specific about WHICH of the two reasons applies — "unavailable"
|
|
3019
|
-
// with no cause is the kind of dead end that generates support mail.
|
|
3020
|
-
reason: !supported ? `Autostart is not available on ${platform}.` : "Autostart needs the packaged gateway; it is unavailable when running from source."
|
|
3021
|
-
});
|
|
3022
|
-
return;
|
|
3023
|
-
}
|
|
3024
|
-
const { output } = await cli(["service", "status"]);
|
|
3025
|
-
res.json({
|
|
3026
|
-
supported: true,
|
|
3027
|
-
installed: /Login service:\s*installed/i.test(output),
|
|
3028
|
-
platform,
|
|
3029
|
-
detail: output
|
|
3030
|
-
});
|
|
3031
|
-
}));
|
|
3032
|
-
router.post("/", safe(async (_req, res) => {
|
|
3033
|
-
if (!SUPPORTED.has(process.platform) || !findCli()) {
|
|
3034
|
-
res.status(400).json({ error: "unsupported", message: "Autostart is not available here." });
|
|
3035
|
-
return;
|
|
3036
|
-
}
|
|
3037
|
-
const { ok, output } = await cli(["service", "install"]);
|
|
3038
|
-
if (!ok) {
|
|
3039
|
-
res.status(500).json({ error: "install_failed", message: output });
|
|
3040
|
-
return;
|
|
3041
|
-
}
|
|
3042
|
-
res.json({ ok: true, installed: true, detail: output });
|
|
3043
|
-
}));
|
|
3044
|
-
router.delete("/", safe(async (_req, res) => {
|
|
3045
|
-
if (!SUPPORTED.has(process.platform) || !findCli()) {
|
|
3046
|
-
res.status(400).json({ error: "unsupported", message: "Autostart is not available here." });
|
|
3047
|
-
return;
|
|
3048
|
-
}
|
|
3049
|
-
const { ok, output } = await cli(["service", "uninstall"]);
|
|
3050
|
-
if (!ok) {
|
|
3051
|
-
res.status(500).json({ error: "uninstall_failed", message: output });
|
|
3052
|
-
return;
|
|
3053
|
-
}
|
|
3054
|
-
res.json({ ok: true, installed: false, detail: output });
|
|
3055
|
-
}));
|
|
3056
|
-
return router;
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
// src/lib/desktop-notify.ts
|
|
3060
|
-
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
3061
|
-
|
|
3062
|
-
// src/lib/notifier-icon.ts
|
|
3063
|
-
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync8 } from "fs";
|
|
3064
|
-
import { homedir as homedir3 } from "os";
|
|
3065
|
-
import { join as join8, dirname as dirname5 } from "path";
|
|
3066
|
-
var ICON_BASE64 = "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";
|
|
3067
|
-
function ensureNotifierIcon(dataDir) {
|
|
3068
|
-
try {
|
|
3069
|
-
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join8(homedir3(), ".suveren");
|
|
3070
|
-
const path = join8(dir, "notifier-icon.png");
|
|
3071
|
-
if (!existsSync8(path)) {
|
|
3072
|
-
mkdirSync4(dirname5(path), { recursive: true });
|
|
3073
|
-
writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
|
|
3074
|
-
}
|
|
3075
|
-
return path;
|
|
3076
|
-
} catch {
|
|
3077
|
-
return null;
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
// src/lib/desktop-notify.ts
|
|
3082
|
-
function osaQuote(value) {
|
|
3083
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3084
|
-
}
|
|
3085
|
-
function psQuote(value) {
|
|
3086
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
3087
|
-
}
|
|
3088
|
-
function xmlEscape(value) {
|
|
3089
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3090
|
-
}
|
|
3091
|
-
var WINDOWS_AUMID = "Suveren.Gateway";
|
|
3092
|
-
function windowsToastScript(title, message, url, iconPath) {
|
|
3093
|
-
const toastXml = `<toast${url ? ` activationType="protocol" launch="${xmlEscape(url)}"` : ""}><visual><binding template="ToastGeneric"><text>${xmlEscape(title)}</text><text>${xmlEscape(message)}</text>` + (iconPath ? `<image placement="appLogoOverride" src="${xmlEscape(iconPath)}"/>` : "") + `</binding></visual><audio src="ms-winsoundevent:Notification.Default"/></toast>`;
|
|
3094
|
-
return `$ErrorActionPreference='SilentlyContinue'; $aumid=${psQuote(WINDOWS_AUMID)}; $reg="HKCU:\\Software\\Classes\\AppUserModelId\\$aumid"; if (-not (Test-Path $reg)) { New-Item -Path $reg -Force | Out-Null }; New-ItemProperty -Path $reg -Name DisplayName -Value 'Suveren' -PropertyType String -Force | Out-Null; ` + (iconPath ? `New-ItemProperty -Path $reg -Name IconUri -Value ${psQuote(iconPath)} -PropertyType String -Force | Out-Null; ` : "") + `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] | Out-Null; $doc = New-Object Windows.Data.Xml.Dom.XmlDocument; $doc.LoadXml(${psQuote(toastXml)}); $toast = New-Object Windows.UI.Notifications.ToastNotification $doc; [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($aumid).Show($toast)`;
|
|
3095
|
-
}
|
|
3096
|
-
var MAC_SOUND = "Glass";
|
|
3097
|
-
function buildNotifyCommand(platform, title, message, options = {}) {
|
|
3098
|
-
const { url, hasTerminalNotifier: hasTerminalNotifier2 = false, iconPath } = options;
|
|
3099
|
-
switch (platform) {
|
|
3100
|
-
case "darwin":
|
|
3101
|
-
if (url && hasTerminalNotifier2) {
|
|
3102
|
-
return {
|
|
3103
|
-
cmd: "terminal-notifier",
|
|
3104
|
-
args: [
|
|
3105
|
-
"-title",
|
|
3106
|
-
title,
|
|
3107
|
-
"-message",
|
|
3108
|
-
message,
|
|
3109
|
-
"-open",
|
|
3110
|
-
url,
|
|
3111
|
-
"-sound",
|
|
3112
|
-
MAC_SOUND,
|
|
3113
|
-
...iconPath ? ["-appIcon", iconPath] : [],
|
|
3114
|
-
"-group",
|
|
3115
|
-
"ai.suveren.gateway"
|
|
3116
|
-
]
|
|
3117
|
-
};
|
|
3118
|
-
}
|
|
3119
|
-
return {
|
|
3120
|
-
cmd: "osascript",
|
|
3121
|
-
args: [
|
|
3122
|
-
"-e",
|
|
3123
|
-
`display notification ${osaQuote(message)} with title ${osaQuote(title)} sound name ${osaQuote(MAC_SOUND)}`
|
|
3124
|
-
]
|
|
3125
|
-
};
|
|
3126
|
-
case "win32":
|
|
3127
|
-
return {
|
|
3128
|
-
cmd: "powershell",
|
|
3129
|
-
args: ["-NoProfile", "-NonInteractive", "-Command", windowsToastScript(title, message, url, iconPath)]
|
|
3130
|
-
};
|
|
3131
|
-
case "linux":
|
|
3132
|
-
return {
|
|
3133
|
-
cmd: "notify-send",
|
|
3134
|
-
args: [
|
|
3135
|
-
"--app-name=Suveren",
|
|
3136
|
-
...iconPath ? ["-i", iconPath] : [],
|
|
3137
|
-
title,
|
|
3138
|
-
message
|
|
3139
|
-
]
|
|
3140
|
-
};
|
|
3141
|
-
default:
|
|
3142
|
-
return null;
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
function lockedNotification(port2) {
|
|
3146
|
-
const url = `http://localhost:${port2}`;
|
|
3147
|
-
return {
|
|
3148
|
-
title: "Suveren is running but locked",
|
|
3149
|
-
message: `Your agent has no authority until you unlock it. Open ${url} and enter your API key.`,
|
|
3150
|
-
url
|
|
3151
|
-
};
|
|
3152
|
-
}
|
|
3153
|
-
function sessionExpiredNotification(port2) {
|
|
3154
|
-
const url = `http://localhost:${port2}`;
|
|
3155
|
-
return {
|
|
3156
|
-
title: "Suveren Gateway locked",
|
|
3157
|
-
message: "Your sign-in has ended. Sign in again so your agents can act.",
|
|
3158
|
-
url
|
|
3159
|
-
};
|
|
3160
|
-
}
|
|
3161
|
-
var terminalNotifierCache = null;
|
|
3162
|
-
function hasTerminalNotifier() {
|
|
3163
|
-
if (terminalNotifierCache !== null) return terminalNotifierCache;
|
|
3164
|
-
try {
|
|
3165
|
-
const probe = spawnSync2("terminal-notifier", ["-help"], { stdio: "ignore" });
|
|
3166
|
-
terminalNotifierCache = probe.error === void 0;
|
|
3167
|
-
} catch {
|
|
3168
|
-
terminalNotifierCache = false;
|
|
3169
|
-
}
|
|
3170
|
-
return terminalNotifierCache;
|
|
3171
|
-
}
|
|
3172
|
-
function notifySpawnOptions(platform) {
|
|
3173
|
-
return {
|
|
3174
|
-
detached: platform !== "win32",
|
|
3175
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
function notify(title, message, platform = process.platform, url) {
|
|
3179
|
-
const command = buildNotifyCommand(platform, title, message, {
|
|
3180
|
-
url,
|
|
3181
|
-
hasTerminalNotifier: platform === "darwin" ? hasTerminalNotifier() : false,
|
|
3182
|
-
// Windows and Linux show it on the toast itself; macOS only via
|
|
3183
|
-
// terminal-notifier. Failure to write it must never block the sound.
|
|
3184
|
-
iconPath: ensureNotifierIcon()
|
|
3185
|
-
});
|
|
3186
|
-
if (!command) return;
|
|
3187
|
-
try {
|
|
3188
|
-
const child = spawn(command.cmd, command.args, notifySpawnOptions(platform));
|
|
3189
|
-
let stderr = "";
|
|
3190
|
-
child.stderr?.on("data", (d) => {
|
|
3191
|
-
stderr += d.toString().slice(0, 500);
|
|
3192
|
-
});
|
|
3193
|
-
child.on("close", (code) => {
|
|
3194
|
-
if (code !== 0) {
|
|
3195
|
-
console.error(`[notify] ${command.cmd} exited ${code}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
3196
|
-
}
|
|
3299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3300
|
+
console.error("[Autostart] handler failed:", message);
|
|
3301
|
+
if (!res.headersSent) res.status(500).json({ error: "autostart_failed", message });
|
|
3197
3302
|
});
|
|
3198
|
-
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
function createAutostartRouter() {
|
|
3306
|
+
const router = Router6();
|
|
3307
|
+
router.get("/", safe(async (_req, res) => {
|
|
3308
|
+
const platform = process.platform;
|
|
3309
|
+
const supported = SUPPORTED.has(platform);
|
|
3310
|
+
const bin = findCli();
|
|
3311
|
+
if (!supported || !bin) {
|
|
3312
|
+
res.json({
|
|
3313
|
+
supported: false,
|
|
3314
|
+
installed: false,
|
|
3315
|
+
platform,
|
|
3316
|
+
// Be specific about WHICH of the two reasons applies — "unavailable"
|
|
3317
|
+
// with no cause is the kind of dead end that generates support mail.
|
|
3318
|
+
reason: !supported ? `Autostart is not available on ${platform}.` : "Autostart needs the packaged gateway; it is unavailable when running from source."
|
|
3319
|
+
});
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
const { output } = await cli(["service", "status"]);
|
|
3323
|
+
res.json({
|
|
3324
|
+
supported: true,
|
|
3325
|
+
installed: /Login service:\s*installed/i.test(output),
|
|
3326
|
+
platform,
|
|
3327
|
+
detail: output
|
|
3199
3328
|
});
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3329
|
+
}));
|
|
3330
|
+
router.post("/", safe(async (_req, res) => {
|
|
3331
|
+
if (!SUPPORTED.has(process.platform) || !findCli()) {
|
|
3332
|
+
res.status(400).json({ error: "unsupported", message: "Autostart is not available here." });
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
const { ok, output } = await cli(["service", "install"]);
|
|
3336
|
+
if (!ok) {
|
|
3337
|
+
res.status(500).json({ error: "install_failed", message: output });
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
res.json({ ok: true, installed: true, detail: output });
|
|
3341
|
+
}));
|
|
3342
|
+
router.delete("/", safe(async (_req, res) => {
|
|
3343
|
+
if (!SUPPORTED.has(process.platform) || !findCli()) {
|
|
3344
|
+
res.status(400).json({ error: "unsupported", message: "Autostart is not available here." });
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const { ok, output } = await cli(["service", "uninstall"]);
|
|
3348
|
+
if (!ok) {
|
|
3349
|
+
res.status(500).json({ error: "uninstall_failed", message: output });
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
res.json({ ok: true, installed: false, detail: output });
|
|
3353
|
+
}));
|
|
3354
|
+
return router;
|
|
3203
3355
|
}
|
|
3204
3356
|
|
|
3205
3357
|
// src/routes/encrypt-intent.ts
|
|
@@ -3426,6 +3578,106 @@ function createEvidenceExportRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
|
|
|
3426
3578
|
// src/routes/proposal-preview.ts
|
|
3427
3579
|
import { Router as Router10 } from "express";
|
|
3428
3580
|
|
|
3581
|
+
// src/lib/content-crypto.ts
|
|
3582
|
+
import { createHash as createHash4 } from "crypto";
|
|
3583
|
+
import { canonicalize as canonicalize2 } from "@hap/core";
|
|
3584
|
+
var CONTENT_ALG = "HPKE-X25519-HKDF-SHA256-AES256GCM";
|
|
3585
|
+
function computeProposalContentHash(toolArgs) {
|
|
3586
|
+
return `sha256:${createHash4("sha256").update(canonicalize2(toolArgs)).digest("hex")}`;
|
|
3587
|
+
}
|
|
3588
|
+
function toB64(bytes) {
|
|
3589
|
+
return Buffer.from(bytes).toString("base64");
|
|
3590
|
+
}
|
|
3591
|
+
function keysToWire(encryptedKeys) {
|
|
3592
|
+
const keys = {};
|
|
3593
|
+
for (const [userId, wrap] of Object.entries(encryptedKeys)) {
|
|
3594
|
+
keys[userId] = { ct: toB64(wrap.ct), enc: toB64(wrap.enc) };
|
|
3595
|
+
}
|
|
3596
|
+
return keys;
|
|
3597
|
+
}
|
|
3598
|
+
async function fetchUserPubkey(userId, deps) {
|
|
3599
|
+
const pin = readPairing(deps.dataDir);
|
|
3600
|
+
const pinning = {
|
|
3601
|
+
enforce: resolvePinTls(deps.dataDir),
|
|
3602
|
+
pinnedSpkiHex: pin && pin.asUrl === deps.spUrl ? pin.tlsSpkiPinHex : void 0
|
|
3603
|
+
};
|
|
3604
|
+
try {
|
|
3605
|
+
const { res } = await fetchAs(
|
|
3606
|
+
`${deps.spUrl}/api/users/${encodeURIComponent(userId)}/pubkey`,
|
|
3607
|
+
{ headers: { Cookie: deps.cookie }, signal: AbortSignal.timeout(5e3) },
|
|
3608
|
+
pinning
|
|
3609
|
+
);
|
|
3610
|
+
if (!res.ok) return null;
|
|
3611
|
+
const data = await res.json().catch(() => ({}));
|
|
3612
|
+
return typeof data.pubkey === "string" ? data.pubkey : null;
|
|
3613
|
+
} catch {
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
async function resolveRecipientPubkeys(recipientUserIds, deps) {
|
|
3618
|
+
const ids = Array.from(/* @__PURE__ */ new Set([deps.selfUserId, ...recipientUserIds]));
|
|
3619
|
+
const resolved = [];
|
|
3620
|
+
const unreadableFor = [];
|
|
3621
|
+
for (const userId of ids) {
|
|
3622
|
+
const pubkeyB64 = await fetchUserPubkey(userId, deps);
|
|
3623
|
+
if (!pubkeyB64) {
|
|
3624
|
+
unreadableFor.push(userId);
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
try {
|
|
3628
|
+
const bytes = new Uint8Array(Buffer.from(pubkeyB64, "base64"));
|
|
3629
|
+
if (bytes.length !== 32) {
|
|
3630
|
+
unreadableFor.push(userId);
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
resolved.push({ userId, publicKey: bytes });
|
|
3634
|
+
} catch {
|
|
3635
|
+
unreadableFor.push(userId);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
return { resolved, unreadableFor };
|
|
3639
|
+
}
|
|
3640
|
+
async function encryptProposalContent(toolArgs, recipientUserIds, deps) {
|
|
3641
|
+
const canonical = canonicalize2(toolArgs);
|
|
3642
|
+
const hash = computeProposalContentHash(toolArgs);
|
|
3643
|
+
const { resolved, unreadableFor } = await resolveRecipientPubkeys(recipientUserIds, deps);
|
|
3644
|
+
if (resolved.length === 0) {
|
|
3645
|
+
throw new Error("No recipient (including the submitter) has a registered end-to-end key \u2014 cannot encrypt this proposal.");
|
|
3646
|
+
}
|
|
3647
|
+
const encrypted = await encryptForRecipients(canonical, resolved);
|
|
3648
|
+
return {
|
|
3649
|
+
content: { v: 1, alg: CONTENT_ALG, ciphertext: toB64(encrypted.intentCiphertext), keys: keysToWire(encrypted.encryptedKeys), hash },
|
|
3650
|
+
unreadableFor
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
async function encryptProposalNote(note, recipientUserIds, deps) {
|
|
3654
|
+
const { resolved, unreadableFor } = await resolveRecipientPubkeys(recipientUserIds, deps);
|
|
3655
|
+
if (resolved.length === 0) {
|
|
3656
|
+
throw new Error("No recipient (including the submitter) has a registered end-to-end key \u2014 cannot encrypt this note.");
|
|
3657
|
+
}
|
|
3658
|
+
const encrypted = await encryptForRecipients(note, resolved);
|
|
3659
|
+
return {
|
|
3660
|
+
noteEncrypted: { ciphertext: toB64(encrypted.intentCiphertext), keys: keysToWire(encrypted.encryptedKeys) },
|
|
3661
|
+
unreadableFor
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
async function decryptProposalContent(wire, myUserId, myPrivateKey) {
|
|
3665
|
+
const wrap = wire.keys[myUserId];
|
|
3666
|
+
if (!wrap) {
|
|
3667
|
+
throw new Error("No key wrap for this account in this content \u2014 this device cannot read it.");
|
|
3668
|
+
}
|
|
3669
|
+
const encrypted = {
|
|
3670
|
+
intentCiphertext: new Uint8Array(Buffer.from(wire.ciphertext, "base64")),
|
|
3671
|
+
encryptedKeys: {
|
|
3672
|
+
[myUserId]: {
|
|
3673
|
+
ct: new Uint8Array(Buffer.from(wrap.ct, "base64")),
|
|
3674
|
+
enc: new Uint8Array(Buffer.from(wrap.enc, "base64"))
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
return decryptIntent(encrypted, myUserId, myPrivateKey);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3429
3681
|
// src/lib/execution-journal-reader.ts
|
|
3430
3682
|
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3431
3683
|
import { join as join10 } from "path";
|
|
@@ -3492,8 +3744,25 @@ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
|
|
|
3492
3744
|
res.status(502).json({ error: `Authority Server unreachable: ${err instanceof Error ? err.message : String(err)}` });
|
|
3493
3745
|
return;
|
|
3494
3746
|
}
|
|
3747
|
+
let toolArgs;
|
|
3748
|
+
if (proposal.toolArgs) {
|
|
3749
|
+
toolArgs = proposal.toolArgs;
|
|
3750
|
+
} else if (proposal.content) {
|
|
3751
|
+
try {
|
|
3752
|
+
const kp = await loadOrGenerateKeyPair(vault2);
|
|
3753
|
+
const me = vault2.getSessionUser();
|
|
3754
|
+
const plaintext = await decryptProposalContent(proposal.content, me?.id ?? "", kp.privateKey);
|
|
3755
|
+
toolArgs = JSON.parse(plaintext);
|
|
3756
|
+
} catch (err) {
|
|
3757
|
+
res.json({ status: "unavailable", reason: "connector_error", message: `Could not decrypt this proposal's content on this device: ${err instanceof Error ? err.message : String(err)}` });
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
} else {
|
|
3761
|
+
res.status(502).json({ error: `Proposal ${proposalId} carries neither tool_args nor content.` });
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3495
3764
|
try {
|
|
3496
|
-
const result = await getToolPreview({ proposalId, tool: proposal.tool, toolArgs
|
|
3765
|
+
const result = await getToolPreview({ proposalId, tool: proposal.tool, toolArgs });
|
|
3497
3766
|
if (result.status !== "ok") {
|
|
3498
3767
|
res.json(result);
|
|
3499
3768
|
return;
|
|
@@ -3539,7 +3808,7 @@ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
|
|
|
3539
3808
|
// src/routes/evidence.ts
|
|
3540
3809
|
import { Router as Router11 } from "express";
|
|
3541
3810
|
import { createPublicKey as createPublicKey2, verify as verifyEd25519 } from "crypto";
|
|
3542
|
-
import { verifyTicketSignature, encodeDidKey, canonicalize as
|
|
3811
|
+
import { verifyTicketSignature, encodeDidKey, canonicalize as canonicalize3 } from "@hap/core";
|
|
3543
3812
|
function issuerFromPublicKeyHex(publicKeyHex) {
|
|
3544
3813
|
return encodeDidKey(Buffer.from(publicKeyHex, "hex"));
|
|
3545
3814
|
}
|
|
@@ -3558,7 +3827,7 @@ function verifyPreV07TicketSignature(receipt, asPublicKeyHex) {
|
|
|
3558
3827
|
});
|
|
3559
3828
|
const sig = Buffer.from(signature, "base64");
|
|
3560
3829
|
if (sig.length !== 64) return false;
|
|
3561
|
-
return verifyEd25519(null, Buffer.from(
|
|
3830
|
+
return verifyEd25519(null, Buffer.from(canonicalize3(signed), "utf8"), key, sig);
|
|
3562
3831
|
} catch {
|
|
3563
3832
|
return false;
|
|
3564
3833
|
}
|
|
@@ -3770,6 +4039,161 @@ function createDecryptIntentRouter(vault2) {
|
|
|
3770
4039
|
return router;
|
|
3771
4040
|
}
|
|
3772
4041
|
|
|
4042
|
+
// src/routes/decrypt-proposal-content.ts
|
|
4043
|
+
import { Router as Router14 } from "express";
|
|
4044
|
+
function createDecryptProposalContentRouter(vault2) {
|
|
4045
|
+
const router = Router14();
|
|
4046
|
+
router.post("/", async (req, res) => {
|
|
4047
|
+
const { ciphertext, keys } = req.body;
|
|
4048
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) {
|
|
4049
|
+
res.status(400).json({ error: "Missing or invalid field: ciphertext (base64 string)" });
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
if (typeof keys !== "object" || keys === null) {
|
|
4053
|
+
res.status(400).json({ error: "Missing or invalid field: keys (object)" });
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
const me = vault2.getSessionUser();
|
|
4057
|
+
if (!me) {
|
|
4058
|
+
res.status(401).json({ error: "No Authority Server session." });
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
try {
|
|
4062
|
+
const kp = await loadOrGenerateKeyPair(vault2);
|
|
4063
|
+
const plaintext = await decryptProposalContent(
|
|
4064
|
+
{ ciphertext, keys },
|
|
4065
|
+
me.id,
|
|
4066
|
+
kp.privateKey
|
|
4067
|
+
);
|
|
4068
|
+
res.json({ plaintext });
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
console.error("[Control Plane] decrypt-proposal-content error:", err);
|
|
4071
|
+
const msg = err instanceof Error ? err.message : "Decryption failed";
|
|
4072
|
+
res.status(msg.includes("No key wrap") ? 403 : 500).json({ error: msg });
|
|
4073
|
+
}
|
|
4074
|
+
});
|
|
4075
|
+
return router;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// src/routes/encrypt-proposal-note.ts
|
|
4079
|
+
import { Router as Router15 } from "express";
|
|
4080
|
+
var MAX_NOTE_LENGTH = 500;
|
|
4081
|
+
function createEncryptProposalNoteRouter(vault2, spUrl, dataDir) {
|
|
4082
|
+
const router = Router15();
|
|
4083
|
+
router.post("/", async (req, res) => {
|
|
4084
|
+
const { note, recipientUserIds } = req.body;
|
|
4085
|
+
if (typeof note !== "string" || note.length === 0 || note.length > MAX_NOTE_LENGTH) {
|
|
4086
|
+
res.status(400).json({ error: `note must be a non-empty string up to ${MAX_NOTE_LENGTH} characters` });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
if (!Array.isArray(recipientUserIds) || !recipientUserIds.every((id) => typeof id === "string")) {
|
|
4090
|
+
res.status(400).json({ error: "recipientUserIds must be a string array" });
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
const me = vault2.getSessionUser();
|
|
4094
|
+
const cookie = vault2.getSpCookie();
|
|
4095
|
+
if (!me || !cookie) {
|
|
4096
|
+
res.status(401).json({ error: "No Authority Server session." });
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
try {
|
|
4100
|
+
const { noteEncrypted, unreadableFor } = await encryptProposalNote(
|
|
4101
|
+
note,
|
|
4102
|
+
recipientUserIds,
|
|
4103
|
+
{ spUrl, cookie, dataDir, selfUserId: me.id }
|
|
4104
|
+
);
|
|
4105
|
+
res.json({ noteEncrypted, unreadableFor });
|
|
4106
|
+
} catch (err) {
|
|
4107
|
+
console.error("[Control Plane] encrypt-proposal-note error:", err);
|
|
4108
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Encryption failed" });
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
return router;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// src/routes/internal-content-crypto.ts
|
|
4115
|
+
import { Router as Router16 } from "express";
|
|
4116
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4117
|
+
var MAX_RECIPIENTS = 50;
|
|
4118
|
+
function secretMatches(provided, expected) {
|
|
4119
|
+
if (!expected || !provided) return false;
|
|
4120
|
+
const a = Buffer.from(provided);
|
|
4121
|
+
const b = Buffer.from(expected);
|
|
4122
|
+
return a.length === b.length && timingSafeEqual2(a, b);
|
|
4123
|
+
}
|
|
4124
|
+
function createInternalContentCryptoRouter(getSecret, vault2, spUrl, dataDir) {
|
|
4125
|
+
const router = Router16();
|
|
4126
|
+
router.post("/encrypt-content", async (req, res) => {
|
|
4127
|
+
if (!secretMatches(req.header("X-Internal-Secret") ?? void 0, getSecret())) {
|
|
4128
|
+
res.status(401).json({ error: "unauthorized" });
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
const { toolArgs, recipientUserIds } = req.body ?? {};
|
|
4132
|
+
if (typeof toolArgs !== "object" || toolArgs === null || Array.isArray(toolArgs)) {
|
|
4133
|
+
res.status(400).json({ error: "toolArgs must be an object" });
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
if (!Array.isArray(recipientUserIds) || !recipientUserIds.every((id) => typeof id === "string")) {
|
|
4137
|
+
res.status(400).json({ error: "recipientUserIds must be a string array" });
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
if (recipientUserIds.length > MAX_RECIPIENTS) {
|
|
4141
|
+
res.status(400).json({ error: `too many recipients (max ${MAX_RECIPIENTS})` });
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
const me = vault2.getSessionUser();
|
|
4145
|
+
const cookie = vault2.getSpCookie();
|
|
4146
|
+
if (!me || !cookie) {
|
|
4147
|
+
res.status(401).json({ error: "no Authority Server session" });
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
try {
|
|
4151
|
+
const { content, unreadableFor } = await encryptProposalContent(
|
|
4152
|
+
toolArgs,
|
|
4153
|
+
recipientUserIds,
|
|
4154
|
+
{ spUrl, cookie, dataDir, selfUserId: me.id }
|
|
4155
|
+
);
|
|
4156
|
+
res.json({ content, unreadableFor });
|
|
4157
|
+
} catch (err) {
|
|
4158
|
+
console.error("[Control Plane] /internal/encrypt-content failed:", err);
|
|
4159
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "encryption failed" });
|
|
4160
|
+
}
|
|
4161
|
+
});
|
|
4162
|
+
router.post("/decrypt-content", async (req, res) => {
|
|
4163
|
+
if (!secretMatches(req.header("X-Internal-Secret") ?? void 0, getSecret())) {
|
|
4164
|
+
res.status(401).json({ error: "unauthorized" });
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
const { ciphertext, keys } = req.body ?? {};
|
|
4168
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) {
|
|
4169
|
+
res.status(400).json({ error: "ciphertext (base64 string) is required" });
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
if (typeof keys !== "object" || keys === null) {
|
|
4173
|
+
res.status(400).json({ error: "keys (object) is required" });
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
const me = vault2.getSessionUser();
|
|
4177
|
+
if (!me) {
|
|
4178
|
+
res.status(401).json({ error: "no Authority Server session" });
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
try {
|
|
4182
|
+
const kp = await loadOrGenerateKeyPair(vault2);
|
|
4183
|
+
const plaintext = await decryptProposalContent(
|
|
4184
|
+
{ ciphertext, keys },
|
|
4185
|
+
me.id,
|
|
4186
|
+
kp.privateKey
|
|
4187
|
+
);
|
|
4188
|
+
res.json({ plaintext });
|
|
4189
|
+
} catch (err) {
|
|
4190
|
+
console.error("[Control Plane] /internal/decrypt-content failed:", err);
|
|
4191
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "decryption failed" });
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
return router;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
3773
4197
|
// src/lib/update-checker.ts
|
|
3774
4198
|
import { execSync } from "child_process";
|
|
3775
4199
|
import { dirname as dirname6 } from "path";
|
|
@@ -3908,7 +4332,7 @@ data: ${data}
|
|
|
3908
4332
|
}
|
|
3909
4333
|
|
|
3910
4334
|
// src/routes/gateway-settings.ts
|
|
3911
|
-
import { Router as
|
|
4335
|
+
import { Router as Router17 } from "express";
|
|
3912
4336
|
|
|
3913
4337
|
// src/lib/gateway-settings.ts
|
|
3914
4338
|
import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
@@ -3944,7 +4368,7 @@ function writeSettings(patch, dataDir) {
|
|
|
3944
4368
|
|
|
3945
4369
|
// src/routes/gateway-settings.ts
|
|
3946
4370
|
function createGatewaySettingsRouter() {
|
|
3947
|
-
const router =
|
|
4371
|
+
const router = Router17();
|
|
3948
4372
|
router.get("/", (_req, res) => {
|
|
3949
4373
|
res.json(readSettings());
|
|
3950
4374
|
});
|
|
@@ -3967,7 +4391,7 @@ function createGatewaySettingsRouter() {
|
|
|
3967
4391
|
}
|
|
3968
4392
|
|
|
3969
4393
|
// src/routes/archived-mandates.ts
|
|
3970
|
-
import { Router as
|
|
4394
|
+
import { Router as Router18 } from "express";
|
|
3971
4395
|
|
|
3972
4396
|
// src/lib/archived-mandates-store.ts
|
|
3973
4397
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
@@ -4010,7 +4434,7 @@ function unarchiveMandate(id, dataDir) {
|
|
|
4010
4434
|
|
|
4011
4435
|
// src/routes/archived-mandates.ts
|
|
4012
4436
|
function createArchivedMandatesRouter(dataDir) {
|
|
4013
|
-
const router =
|
|
4437
|
+
const router = Router18();
|
|
4014
4438
|
router.get("/", (_req, res) => {
|
|
4015
4439
|
res.json({ archived: readArchived(dataDir) });
|
|
4016
4440
|
});
|
|
@@ -4032,24 +4456,24 @@ function createArchivedMandatesRouter(dataDir) {
|
|
|
4032
4456
|
}
|
|
4033
4457
|
|
|
4034
4458
|
// src/routes/internal-events.ts
|
|
4035
|
-
import { Router as
|
|
4036
|
-
import { timingSafeEqual as
|
|
4459
|
+
import { Router as Router19 } from "express";
|
|
4460
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4037
4461
|
var ALLOWED = ["proposal-added", "action-approval-needed"];
|
|
4038
4462
|
var SESSION_EXPIRED = "session-expired";
|
|
4039
4463
|
var AS_KEY_MISMATCH = "as-key-mismatch";
|
|
4040
4464
|
var AS_TLS_MISMATCH = "as-tls-mismatch";
|
|
4041
|
-
function
|
|
4465
|
+
function secretMatches2(provided, expected) {
|
|
4042
4466
|
if (!expected) return false;
|
|
4043
4467
|
if (!provided) return false;
|
|
4044
4468
|
const a = Buffer.from(provided);
|
|
4045
4469
|
const b = Buffer.from(expected);
|
|
4046
4470
|
if (a.length !== b.length) return false;
|
|
4047
|
-
return
|
|
4471
|
+
return timingSafeEqual3(a, b);
|
|
4048
4472
|
}
|
|
4049
4473
|
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch, onAsTlsMismatch) {
|
|
4050
|
-
const router =
|
|
4474
|
+
const router = Router19();
|
|
4051
4475
|
router.post("/event", (req, res) => {
|
|
4052
|
-
if (!
|
|
4476
|
+
if (!secretMatches2(req.headers["x-internal-secret"], getSecret())) {
|
|
4053
4477
|
res.status(401).json({ error: "Unauthorized" });
|
|
4054
4478
|
return;
|
|
4055
4479
|
}
|
|
@@ -4081,8 +4505,8 @@ function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch
|
|
|
4081
4505
|
}
|
|
4082
4506
|
|
|
4083
4507
|
// src/routes/internal-mandate.ts
|
|
4084
|
-
import { Router as
|
|
4085
|
-
import { timingSafeEqual as
|
|
4508
|
+
import { Router as Router20 } from "express";
|
|
4509
|
+
import { timingSafeEqual as timingSafeEqual4, randomUUID } from "crypto";
|
|
4086
4510
|
|
|
4087
4511
|
// src/lib/mandate-ceremony.ts
|
|
4088
4512
|
import {
|
|
@@ -4256,16 +4680,16 @@ async function createMandate(req, deps) {
|
|
|
4256
4680
|
}
|
|
4257
4681
|
|
|
4258
4682
|
// src/routes/internal-mandate.ts
|
|
4259
|
-
function
|
|
4683
|
+
function secretMatches3(provided, expected) {
|
|
4260
4684
|
if (!expected || !provided) return false;
|
|
4261
4685
|
const a = Buffer.from(provided);
|
|
4262
4686
|
const b = Buffer.from(expected);
|
|
4263
|
-
return a.length === b.length &&
|
|
4687
|
+
return a.length === b.length && timingSafeEqual4(a, b);
|
|
4264
4688
|
}
|
|
4265
4689
|
function createInternalMandateRouter(getSecret, deps) {
|
|
4266
|
-
const router =
|
|
4690
|
+
const router = Router20();
|
|
4267
4691
|
router.post("/mandate", async (req, res) => {
|
|
4268
|
-
if (!
|
|
4692
|
+
if (!secretMatches3(req.header("X-Internal-Secret") ?? void 0, getSecret())) {
|
|
4269
4693
|
res.status(401).json({ error: "unauthorized" });
|
|
4270
4694
|
return;
|
|
4271
4695
|
}
|
|
@@ -4380,62 +4804,6 @@ function startNotificationDispatcher(opts = {}) {
|
|
|
4380
4804
|
return new NotificationDispatcher(opts).start();
|
|
4381
4805
|
}
|
|
4382
4806
|
|
|
4383
|
-
// src/lib/session-lock.ts
|
|
4384
|
-
var SESSION_LOCKED_MESSAGE = "Your sign-in ended after 30 days or was revoked. Sign in again.";
|
|
4385
|
-
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.";
|
|
4386
|
-
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.";
|
|
4387
|
-
function createSessionLock(deps) {
|
|
4388
|
-
const { vault: vault2, port: port2 } = deps;
|
|
4389
|
-
const notifyFn = deps.notifyFn ?? notify;
|
|
4390
|
-
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
4391
|
-
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
4392
|
-
return function lockExpiredSession2() {
|
|
4393
|
-
if (!vault2.isUnlocked()) return;
|
|
4394
|
-
vault2.lockExpired();
|
|
4395
|
-
console.error("[Control Plane] Authority Server session ended \u2014 gateway LOCKED");
|
|
4396
|
-
void unconfigureSessionFn().catch((err) => {
|
|
4397
|
-
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
4398
|
-
});
|
|
4399
|
-
emit("session-locked", { reason: "expired", message: SESSION_LOCKED_MESSAGE });
|
|
4400
|
-
const { title, message, url } = sessionExpiredNotification(port2);
|
|
4401
|
-
notifyFn(title, message, process.platform, url);
|
|
4402
|
-
};
|
|
4403
|
-
}
|
|
4404
|
-
function createAsKeyMismatchLock(deps) {
|
|
4405
|
-
const { vault: vault2, port: port2 } = deps;
|
|
4406
|
-
const notifyFn = deps.notifyFn ?? notify;
|
|
4407
|
-
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
4408
|
-
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
4409
|
-
return function lockOnAsKeyMismatch() {
|
|
4410
|
-
if (!vault2.isUnlocked()) return;
|
|
4411
|
-
vault2.lockAsKeyMismatch();
|
|
4412
|
-
console.error("[Control Plane] Authority Server key mismatch \u2014 gateway LOCKED");
|
|
4413
|
-
void unconfigureSessionFn("as-key-mismatch").catch((err) => {
|
|
4414
|
-
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
4415
|
-
});
|
|
4416
|
-
emit("session-locked", { reason: "as-key-mismatch", message: AS_KEY_MISMATCH_MESSAGE });
|
|
4417
|
-
const { title, url } = sessionExpiredNotification(port2);
|
|
4418
|
-
notifyFn(title, AS_KEY_MISMATCH_MESSAGE, process.platform, url);
|
|
4419
|
-
};
|
|
4420
|
-
}
|
|
4421
|
-
function createAsTlsMismatchLock(deps) {
|
|
4422
|
-
const { vault: vault2, port: port2 } = deps;
|
|
4423
|
-
const notifyFn = deps.notifyFn ?? notify;
|
|
4424
|
-
const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
|
|
4425
|
-
const emit = deps.emit ?? eventBus.emit.bind(eventBus);
|
|
4426
|
-
return function lockOnAsTlsMismatch() {
|
|
4427
|
-
if (!vault2.isUnlocked()) return;
|
|
4428
|
-
vault2.lockAsTlsMismatch();
|
|
4429
|
-
console.error("[Control Plane] Authority Server TLS pin mismatch \u2014 gateway LOCKED");
|
|
4430
|
-
void unconfigureSessionFn("as-tls-mismatch").catch((err) => {
|
|
4431
|
-
console.error("[Control Plane] Failed to push cleared session to MCP:", err);
|
|
4432
|
-
});
|
|
4433
|
-
emit("session-locked", { reason: "as-tls-mismatch", message: AS_TLS_MISMATCH_MESSAGE });
|
|
4434
|
-
const { title, url } = sessionExpiredNotification(port2);
|
|
4435
|
-
notifyFn(title, AS_TLS_MISMATCH_MESSAGE, process.platform, url);
|
|
4436
|
-
};
|
|
4437
|
-
}
|
|
4438
|
-
|
|
4439
4807
|
// src/lib/session-expiry-scheduler.ts
|
|
4440
4808
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
4441
4809
|
var SessionExpiryScheduler = class {
|
|
@@ -4555,6 +4923,7 @@ var SP_URL = resolveAsUrl(DATA_DIR);
|
|
|
4555
4923
|
validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
|
|
4556
4924
|
var POLICY_LOCKED = [...readPolicy().locked];
|
|
4557
4925
|
var POLICY_LOCKED_INSTALL_METHOD = readPolicy().policy.installMethod;
|
|
4926
|
+
var POLICY_LOCKED_TEAM = readPolicy().policy.team;
|
|
4558
4927
|
var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
|
|
4559
4928
|
var HAP_MODE = process.env.HAP_MODE ?? "personal";
|
|
4560
4929
|
var UI_DIST = process.env.HAP_UI_DIST ?? join14(import.meta.dirname ?? __dirname, "../../ui/dist");
|
|
@@ -4594,6 +4963,7 @@ function loginRateLimit(req, res, next) {
|
|
|
4594
4963
|
var lockExpiredSession = createSessionLock({ vault, port });
|
|
4595
4964
|
var lockAsKeyMismatch = createAsKeyMismatchLock({ vault, port });
|
|
4596
4965
|
var lockAsTlsMismatch = createAsTlsMismatchLock({ vault, port });
|
|
4966
|
+
var lockManagedWrongTeam = createManagedWrongTeamLock({ vault, port });
|
|
4597
4967
|
var sessionExpiryScheduler = new SessionExpiryScheduler(
|
|
4598
4968
|
() => vault.getSessionExpiresAt(),
|
|
4599
4969
|
lockExpiredSession
|
|
@@ -4603,8 +4973,26 @@ app.use("/auth", jsonParser, createAuthRouter(
|
|
|
4603
4973
|
requireAuth(vault),
|
|
4604
4974
|
loginRateLimit,
|
|
4605
4975
|
() => sessionExpiryScheduler.reschedule(),
|
|
4606
|
-
{ asUrl: SP_URL, dataDir: DATA_DIR }
|
|
4976
|
+
{ asUrl: SP_URL, dataDir: DATA_DIR },
|
|
4977
|
+
lockAsTlsMismatch,
|
|
4978
|
+
POLICY_LOCKED_TEAM
|
|
4607
4979
|
));
|
|
4980
|
+
var managedTeamScheduler = POLICY_LOCKED_TEAM ? new ManagedTeamScheduler(async () => {
|
|
4981
|
+
if (!vault.isUnlocked()) return;
|
|
4982
|
+
const cookie = vault.getSpCookie();
|
|
4983
|
+
if (!cookie) return;
|
|
4984
|
+
try {
|
|
4985
|
+
const pin = readPairing(DATA_DIR);
|
|
4986
|
+
const team = await activeTeamId(SP_URL, cookie, {
|
|
4987
|
+
enforce: resolvePinTls(DATA_DIR),
|
|
4988
|
+
pinnedSpkiHex: pin && pin.asUrl === SP_URL ? pin.tlsSpkiPinHex : void 0
|
|
4989
|
+
});
|
|
4990
|
+
if (team !== POLICY_LOCKED_TEAM) lockManagedWrongTeam(POLICY_LOCKED_TEAM);
|
|
4991
|
+
} catch (err) {
|
|
4992
|
+
console.error("[Control Plane] Managed-team check failed (will retry):", err);
|
|
4993
|
+
}
|
|
4994
|
+
}) : null;
|
|
4995
|
+
managedTeamScheduler?.start();
|
|
4608
4996
|
function resolveOrigin(req) {
|
|
4609
4997
|
const fwdHost = req.get("x-forwarded-host");
|
|
4610
4998
|
if (fwdHost) return `${req.protocol}://${fwdHost}`;
|
|
@@ -4838,6 +5226,7 @@ app.use("/internal", jsonParser, createInternalMandateRouter(() => internalSecre
|
|
|
4838
5226
|
encrypt: encryptIntentForRecipients,
|
|
4839
5227
|
deliverGateContent: (args) => pushGateContent(args)
|
|
4840
5228
|
})));
|
|
5229
|
+
app.use("/internal", jsonParser, createInternalContentCryptoRouter(() => internalSecret2, vault, SP_URL, DATA_DIR));
|
|
4841
5230
|
app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
|
|
4842
5231
|
app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
|
|
4843
5232
|
app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
|
|
@@ -4851,6 +5240,8 @@ app.use("/proposal-status", authGuard, createProposalStatusRouter(SP_URL, vault,
|
|
|
4851
5240
|
app.use("/api/evidence", authGuard, createEvidenceRouter());
|
|
4852
5241
|
app.use("/api/report", jsonParser, authGuard, createReportRouter(() => RUNNING_VERSION));
|
|
4853
5242
|
app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
|
|
5243
|
+
app.use("/api/decrypt-proposal-content", jsonParser, authGuard, createDecryptProposalContentRouter(vault));
|
|
5244
|
+
app.use("/api/encrypt-proposal-note", jsonParser, authGuard, createEncryptProposalNoteRouter(vault, SP_URL, DATA_DIR));
|
|
4854
5245
|
app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
|
|
4855
5246
|
app.put("/integrations/:id/read-policy", jsonParser, authGuard, async (req, res) => {
|
|
4856
5247
|
const id = req.params.id;
|
|
@@ -5141,8 +5532,13 @@ app.use(
|
|
|
5141
5532
|
eventBus.emit("proposal-approved");
|
|
5142
5533
|
} else if (method === "POST" && /^\/api\/proposals\/[^/]+\/reject$/.test(path)) {
|
|
5143
5534
|
eventBus.emit("proposal-rejected");
|
|
5144
|
-
} else if (method === "POST" && (path === "/api/groups" || path === "/api/groups/join" || /^\/api\/groups\/[^/]+\/(leave|disable|reactivate|admin-transfer)$/.test(path)))
|
|
5535
|
+
} else if (method === "POST" && (path === "/api/groups" || path === "/api/groups/join" || /^\/api\/groups\/[^/]+\/(leave|disable|reactivate|admin-transfer)$/.test(path)) || // WS0 (work-plan "Added 2026-10-11"): withdrawing a pending join
|
|
5536
|
+
// request — the account stays Personal, but the onboarding UI's
|
|
5537
|
+
// waiting state ends, same as every other membership-affecting
|
|
5538
|
+
// call here.
|
|
5539
|
+
method === "DELETE" && path === "/api/groups/join") {
|
|
5145
5540
|
eventBus.emit("team-membership-changed");
|
|
5541
|
+
if (managedTeamScheduler) void managedTeamScheduler.runNow();
|
|
5146
5542
|
}
|
|
5147
5543
|
}
|
|
5148
5544
|
}
|