@neuralnomads/codenomad-dev 0.20.0-dev-20260909-855d2ee7 → 0.20.0-dev-20260911-ea37f97b
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/dist/api-types.js.map +1 -1
- package/dist/index.js +42 -31
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +9 -1
- package/dist/index.test.js.map +1 -1
- package/dist/opencode/pruning-installation.js +65 -0
- package/dist/opencode/pruning-installation.js.map +1 -0
- package/dist/opencode/pruning-installation.test.js +101 -0
- package/dist/opencode/pruning-installation.test.js.map +1 -0
- package/dist/opencode/pruning-lifecycle.js +66 -0
- package/dist/opencode/pruning-lifecycle.js.map +1 -0
- package/dist/opencode/session-pruning/claim-fence.js +33 -0
- package/dist/opencode/session-pruning/claim-fence.js.map +1 -0
- package/dist/opencode/session-pruning/claim-fence.test.js +104 -0
- package/dist/opencode/session-pruning/claim-fence.test.js.map +1 -0
- package/dist/opencode/session-pruning/contract.js +40 -0
- package/dist/opencode/session-pruning/contract.js.map +1 -0
- package/dist/opencode/session-pruning/database-path.js +17 -0
- package/dist/opencode/session-pruning/database-path.js.map +1 -0
- package/dist/opencode/session-pruning/database-path.test.js +22 -0
- package/dist/opencode/session-pruning/database-path.test.js.map +1 -0
- package/dist/opencode/session-pruning/desktop-plugin.js +14 -0
- package/dist/opencode/session-pruning/desktop-plugin.js.map +1 -0
- package/dist/opencode/session-pruning/index.js +2 -0
- package/dist/opencode/session-pruning/index.js.map +1 -0
- package/dist/opencode/session-pruning/isolated-store.js +18 -0
- package/dist/opencode/session-pruning/isolated-store.js.map +1 -0
- package/dist/opencode/session-pruning/planner.js +36 -0
- package/dist/opencode/session-pruning/planner.js.map +1 -0
- package/dist/opencode/session-pruning/plugin.js +45 -0
- package/dist/opencode/session-pruning/plugin.js.map +1 -0
- package/dist/opencode/session-pruning/presence.js +42 -0
- package/dist/opencode/session-pruning/presence.js.map +1 -0
- package/dist/opencode/session-pruning/preview-store.js +21 -0
- package/dist/opencode/session-pruning/preview-store.js.map +1 -0
- package/dist/opencode/session-pruning/pruning.test.js +172 -0
- package/dist/opencode/session-pruning/pruning.test.js.map +1 -0
- package/dist/opencode/session-pruning/revision.js +18 -0
- package/dist/opencode/session-pruning/revision.js.map +1 -0
- package/dist/opencode/session-pruning/service.js +56 -0
- package/dist/opencode/session-pruning/service.js.map +1 -0
- package/dist/opencode/session-pruning/service.test.js +74 -0
- package/dist/opencode/session-pruning/service.test.js.map +1 -0
- package/dist/opencode/session-pruning/storage-path.js +6 -0
- package/dist/opencode/session-pruning/storage-path.js.map +1 -0
- package/dist/opencode/session-pruning/transaction.js +53 -0
- package/dist/opencode/session-pruning/transaction.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.js +84 -0
- package/dist/opencode/session-pruning/tui-reload.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.native.js +269 -0
- package/dist/opencode/session-pruning/tui-reload.native.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.test.js +71 -0
- package/dist/opencode/session-pruning/tui-reload.test.js.map +1 -0
- package/dist/opencode/session-pruning/tui.js +12 -0
- package/dist/opencode/session-pruning/tui.js.map +1 -0
- package/dist/plugins/session-pruning/plugin.mjs +26559 -0
- package/dist/server/__tests__/instance-proxy.test.js +10 -5
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/__tests__/listener-base-url.test.js +26 -1
- package/dist/server/__tests__/listener-base-url.test.js.map +1 -1
- package/dist/server/__tests__/network-addresses.test.js +40 -0
- package/dist/server/__tests__/network-addresses.test.js.map +1 -1
- package/dist/server/__tests__/network-host.test.js +34 -0
- package/dist/server/__tests__/network-host.test.js.map +1 -0
- package/dist/server/__tests__/tls-ipv6.test.js +54 -0
- package/dist/server/__tests__/tls-ipv6.test.js.map +1 -0
- package/dist/server/http-server.js +8 -6
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/listener-base-url.js +12 -3
- package/dist/server/listener-base-url.js.map +1 -1
- package/dist/server/network-addresses.js +20 -42
- package/dist/server/network-addresses.js.map +1 -1
- package/dist/server/network-host.js +62 -0
- package/dist/server/network-host.js.map +1 -0
- package/dist/server/routes/meta.js +2 -4
- package/dist/server/routes/meta.js.map +1 -1
- package/dist/server/routes/session-pruning.js +45 -0
- package/dist/server/routes/session-pruning.js.map +1 -0
- package/dist/server/routes/session-pruning.test.js +98 -0
- package/dist/server/routes/session-pruning.test.js.map +1 -0
- package/dist/server/tls.js +52 -32
- package/dist/server/tls.js.map +1 -1
- package/dist/workspaces/manager.js +1 -0
- package/dist/workspaces/manager.js.map +1 -1
- package/package.json +6 -3
- package/public/assets/App-0-FcHzPS.js +44 -0
- package/public/assets/App-0-FcHzPS.js.map +1 -0
- package/public/assets/FilesTab-CRpGDs1B.js +3 -0
- package/public/assets/{FilesTab-z7UrjcAQ.js.map → FilesTab-CRpGDs1B.js.map} +1 -1
- package/public/assets/{GitChangesTab-C04bYKhs.js → GitChangesTab-DxFebTiq.js} +3 -3
- package/public/assets/{GitChangesTab-C04bYKhs.js.map → GitChangesTab-DxFebTiq.js.map} +1 -1
- package/public/assets/{SplitFilePanel-B29MqCuU.js → SplitFilePanel-DgTMrrO-.js} +2 -2
- package/public/assets/{SplitFilePanel-B29MqCuU.js.map → SplitFilePanel-DgTMrrO-.js.map} +1 -1
- package/public/assets/{StatusTab-wUKfwnFf.js → StatusTab-LKUA9cbH.js} +2 -2
- package/public/assets/{StatusTab-wUKfwnFf.js.map → StatusTab-LKUA9cbH.js.map} +1 -1
- package/public/assets/{align-justify-glU678D9.js → align-justify-v_yPXSys.js} +2 -2
- package/public/assets/{align-justify-glU678D9.js.map → align-justify-v_yPXSys.js.map} +1 -1
- package/public/assets/{bundle-full-BrsCElYv.js → bundle-full-HzL_Jtqk.js} +2 -2
- package/public/assets/{bundle-full-BrsCElYv.js.map → bundle-full-HzL_Jtqk.js.map} +1 -1
- package/public/assets/{client-state-j9Iu7F-E.js → client-state-CF-PJ0GG.js} +2 -2
- package/public/assets/{client-state-j9Iu7F-E.js.map → client-state-CF-PJ0GG.js.map} +1 -1
- package/public/assets/{diff-viewer-B0VS1voD.js → diff-viewer-BFDQBDVy.js} +2 -2
- package/public/assets/{diff-viewer-B0VS1voD.js.map → diff-viewer-BFDQBDVy.js.map} +1 -1
- package/public/assets/index-8iPlm2r0.js +2 -0
- package/public/assets/index-8iPlm2r0.js.map +1 -0
- package/public/assets/index-B185mC7m.js +2 -0
- package/public/assets/index-B185mC7m.js.map +1 -0
- package/public/assets/index-Bbj1xF8i.js +2 -0
- package/public/assets/index-Bbj1xF8i.js.map +1 -0
- package/public/assets/index-BsmFN3k1.js +2 -0
- package/public/assets/index-BsmFN3k1.js.map +1 -0
- package/public/assets/index-Bz_OQJYA.css +1 -0
- package/public/assets/index-C52CfqK3.js +2 -0
- package/public/assets/index-C52CfqK3.js.map +1 -0
- package/public/assets/index-CHYmKl10.js +2 -0
- package/public/assets/index-CHYmKl10.js.map +1 -0
- package/public/assets/index-CTKYD_VJ.js +2 -0
- package/public/assets/index-CTKYD_VJ.js.map +1 -0
- package/public/assets/index-D2UHSOG5.js +2 -0
- package/public/assets/{index-ClaHfu-1.js.map → index-D2UHSOG5.js.map} +1 -1
- package/public/assets/index-DDoB_Z36.js +2 -0
- package/public/assets/index-DDoB_Z36.js.map +1 -0
- package/public/assets/{index-BzAEHHap.js → index-DcwAtZw-.js} +3 -3
- package/public/assets/index-DcwAtZw-.js.map +1 -0
- package/public/assets/index-DxUD3RYr.js +2 -0
- package/public/assets/index-DxUD3RYr.js.map +1 -0
- package/public/assets/{index-B6bX3AMT.js → index-RK9gf0uk.js} +2 -2
- package/public/assets/{index-B6bX3AMT.js.map → index-RK9gf0uk.js.map} +1 -1
- package/public/assets/{loading-CYiiwb08.js → loading-BSB82snY.js} +2 -2
- package/public/assets/{loading-CYiiwb08.js.map → loading-BSB82snY.js.map} +1 -1
- package/public/assets/main-CZztwrfv.js +48 -0
- package/public/assets/main-CZztwrfv.js.map +1 -0
- package/public/assets/{markdown-DWvSdCZz.js → markdown-DtidoL2Z.js} +4 -4
- package/public/assets/{markdown-DWvSdCZz.js.map → markdown-DtidoL2Z.js.map} +1 -1
- package/public/assets/{monaco-viewer-Cc9bWg7I.js → monaco-viewer-CpxDfB-6.js} +2 -2
- package/public/assets/monaco-viewer-CpxDfB-6.js.map +1 -0
- package/public/assets/{startup-state-settings-card-2KHpBL8U.js → startup-state-settings-card-BY741Rt0.js} +2 -2
- package/public/assets/{startup-state-settings-card-2KHpBL8U.js.map → startup-state-settings-card-BY741Rt0.js.map} +1 -1
- package/public/assets/{terminal-square-lzHuj_of.js → terminal-square-BdC_jwm3.js} +2 -2
- package/public/assets/{terminal-square-lzHuj_of.js.map → terminal-square-BdC_jwm3.js.map} +1 -1
- package/public/assets/{tool-call-DjkszeT4.js → tool-call-DbtCsJ4x.js} +10 -10
- package/public/assets/{tool-call-DjkszeT4.js.map → tool-call-DbtCsJ4x.js.map} +1 -1
- package/public/assets/{unified-picker-5JmIM6a0.js → unified-picker-C0eo1chJ.js} +2 -2
- package/public/assets/{unified-picker-5JmIM6a0.js.map → unified-picker-C0eo1chJ.js.map} +1 -1
- package/public/assets/{wrap-text-N0Cav2Vp.js → wrap-text-BuMtdvMx.js} +2 -2
- package/public/assets/{wrap-text-N0Cav2Vp.js.map → wrap-text-BuMtdvMx.js.map} +1 -1
- package/public/index.html +4 -4
- package/public/loading.html +4 -4
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-Nlt9zV-v.js +0 -43
- package/public/assets/App-Nlt9zV-v.js.map +0 -1
- package/public/assets/FilesTab-z7UrjcAQ.js +0 -3
- package/public/assets/index-BIRLnDWY.js +0 -2
- package/public/assets/index-BIRLnDWY.js.map +0 -1
- package/public/assets/index-BuScpzpP.js +0 -2
- package/public/assets/index-BuScpzpP.js.map +0 -1
- package/public/assets/index-BzAEHHap.js.map +0 -1
- package/public/assets/index-C-DZt-oz.css +0 -1
- package/public/assets/index-C9RXVQl9.js +0 -2
- package/public/assets/index-C9RXVQl9.js.map +0 -1
- package/public/assets/index-ClaHfu-1.js +0 -2
- package/public/assets/index-DBx5htn3.js +0 -2
- package/public/assets/index-DBx5htn3.js.map +0 -1
- package/public/assets/index-DHHMf3u1.js +0 -2
- package/public/assets/index-DHHMf3u1.js.map +0 -1
- package/public/assets/index-DN4Kstsd.js +0 -2
- package/public/assets/index-DN4Kstsd.js.map +0 -1
- package/public/assets/index-DbMyNDyo.js +0 -2
- package/public/assets/index-DbMyNDyo.js.map +0 -1
- package/public/assets/index-rcsmuqww.js +0 -2
- package/public/assets/index-rcsmuqww.js.map +0 -1
- package/public/assets/index-uz2QFRlk.js +0 -2
- package/public/assets/index-uz2QFRlk.js.map +0 -1
- package/public/assets/main-OmXs7qtG.js +0 -48
- package/public/assets/main-OmXs7qtG.js.map +0 -1
- package/public/assets/monaco-viewer-Cc9bWg7I.js.map +0 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from "./network-host";
|
|
3
|
+
export function resolvePreferredRemoteListener(input) {
|
|
4
|
+
return input.httpsStart ?? input.httpStart ?? null;
|
|
5
|
+
}
|
|
1
6
|
export function resolvePluginBaseUrl(input) {
|
|
2
7
|
const loopbackListener = [input.httpStart, input.httpsStart].find((listener) => listener && acceptsLoopback(listener.bindHost));
|
|
3
8
|
if (loopbackListener) {
|
|
4
|
-
|
|
9
|
+
const bindHost = normalizeNetworkHost(loopbackListener.bindHost);
|
|
10
|
+
const loopbackHost = isWildcardHost(bindHost)
|
|
11
|
+
? isIP(bindHost) === 6 ? "::1" : "127.0.0.1"
|
|
12
|
+
: bindHost;
|
|
13
|
+
return `${loopbackListener.protocol}://${formatHostForUrl(loopbackHost)}:${loopbackListener.port}`;
|
|
5
14
|
}
|
|
6
15
|
if (input.remoteUrl) {
|
|
7
16
|
return input.remoteUrl;
|
|
@@ -10,7 +19,7 @@ export function resolvePluginBaseUrl(input) {
|
|
|
10
19
|
if (!fallbackListener) {
|
|
11
20
|
throw new Error("No listeners started");
|
|
12
21
|
}
|
|
13
|
-
return `${fallbackListener.protocol}://${fallbackListener.bindHost}:${fallbackListener.port}`;
|
|
22
|
+
return `${fallbackListener.protocol}://${formatHostForUrl(fallbackListener.bindHost)}:${fallbackListener.port}`;
|
|
14
23
|
}
|
|
15
24
|
export function resolveAutomationBridgeUrl(listener) {
|
|
16
25
|
if (listener.protocol !== "http" || !acceptsLoopback(listener.bindHost)) {
|
|
@@ -19,6 +28,6 @@ export function resolveAutomationBridgeUrl(listener) {
|
|
|
19
28
|
return `http://127.0.0.1:${listener.port}`;
|
|
20
29
|
}
|
|
21
30
|
function acceptsLoopback(bindHost) {
|
|
22
|
-
return bindHost === "
|
|
31
|
+
return bindHost === "localhost" || isWildcardHost(bindHost) || isLoopbackHost(bindHost);
|
|
23
32
|
}
|
|
24
33
|
//# sourceMappingURL=listener-base-url.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener-base-url.js","sourceRoot":"","sources":["../../src/server/listener-base-url.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"listener-base-url.js","sourceRoot":"","sources":["../../src/server/listener-base-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAcvG,MAAM,UAAU,8BAA8B,CAC5C,KAAkE;IAElE,OAAO,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,gBAAgB,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC/H,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,oBAAoB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAA;QAChE,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC;YAC3C,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW;YAC5C,CAAC,CAAC,QAAQ,CAAA;QACZ,OAAO,GAAG,gBAAgB,CAAC,QAAQ,MAAM,gBAAgB,CAAC,YAAY,CAAC,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAA;IACpG,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,SAAS,CAAA;IACxB,CAAC;IAED,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5D,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;IACzC,CAAC;IAED,OAAO,GAAG,gBAAgB,CAAC,QAAQ,MAAM,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAA;AACjH,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,QAAqC;IAC9E,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,oBAAoB,QAAQ,CAAC,IAAI,EAAE,CAAA;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,OAAO,QAAQ,KAAK,WAAW,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,CAAA;AACzF,CAAC","sourcesContent":["import { isIP } from \"node:net\"\nimport { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from \"./network-host\"\n\nexport interface StartedListenerBaseUrlInput {\n protocol: \"http\" | \"https\"\n bindHost: string\n port: number\n}\n\nexport interface ResolvePluginBaseUrlInput {\n httpStart?: StartedListenerBaseUrlInput | null\n httpsStart?: StartedListenerBaseUrlInput | null\n remoteUrl?: string\n}\n\nexport function resolvePreferredRemoteListener(\n input: Pick<ResolvePluginBaseUrlInput, \"httpStart\" | \"httpsStart\">,\n): StartedListenerBaseUrlInput | null {\n return input.httpsStart ?? input.httpStart ?? null\n}\n\nexport function resolvePluginBaseUrl(input: ResolvePluginBaseUrlInput): string {\n const loopbackListener = [input.httpStart, input.httpsStart].find((listener) => listener && acceptsLoopback(listener.bindHost))\n if (loopbackListener) {\n const bindHost = normalizeNetworkHost(loopbackListener.bindHost)\n const loopbackHost = isWildcardHost(bindHost)\n ? isIP(bindHost) === 6 ? \"::1\" : \"127.0.0.1\"\n : bindHost\n return `${loopbackListener.protocol}://${formatHostForUrl(loopbackHost)}:${loopbackListener.port}`\n }\n\n if (input.remoteUrl) {\n return input.remoteUrl\n }\n\n const fallbackListener = input.httpStart ?? input.httpsStart\n if (!fallbackListener) {\n throw new Error(\"No listeners started\")\n }\n\n return `${fallbackListener.protocol}://${formatHostForUrl(fallbackListener.bindHost)}:${fallbackListener.port}`\n}\n\nexport function resolveAutomationBridgeUrl(listener: StartedListenerBaseUrlInput): string {\n if (listener.protocol !== \"http\" || !acceptsLoopback(listener.bindHost)) {\n throw new Error(\"Developer Mode requires a loopback HTTP listener\")\n }\n return `http://127.0.0.1:${listener.port}`\n}\n\nfunction acceptsLoopback(bindHost: string): boolean {\n return bindHost === \"localhost\" || isWildcardHost(bindHost) || isLoopbackHost(bindHost)\n}\n"]}
|
|
@@ -1,51 +1,40 @@
|
|
|
1
1
|
import os from "os";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from "./network-host";
|
|
2
4
|
export function resolveNetworkAddresses(args) {
|
|
3
|
-
const {
|
|
5
|
+
const { protocol, port } = args;
|
|
6
|
+
const host = normalizeNetworkHost(args.host);
|
|
4
7
|
const interfaces = os.networkInterfaces();
|
|
5
8
|
const seen = new Set();
|
|
6
9
|
const results = [];
|
|
7
10
|
const addAddress = (ip, scope) => {
|
|
8
|
-
|
|
11
|
+
const normalizedIp = normalizeNetworkHost(ip);
|
|
12
|
+
const ipVersion = isIP(normalizedIp);
|
|
13
|
+
if (!ipVersion || isWildcardHost(normalizedIp) || (ipVersion === 6 && isLinkLocalIPv6(normalizedIp)))
|
|
9
14
|
return;
|
|
10
|
-
const
|
|
15
|
+
const family = ipVersion === 6 ? "ipv6" : "ipv4";
|
|
16
|
+
const key = `${family}-${normalizedIp}`;
|
|
11
17
|
if (seen.has(key))
|
|
12
18
|
return;
|
|
13
19
|
seen.add(key);
|
|
14
|
-
results.push({ ip, family
|
|
20
|
+
results.push({ ip: normalizedIp, family, scope, remoteUrl: `${protocol}://${formatHostForUrl(normalizedIp)}:${port}` });
|
|
15
21
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const lowered = value.toLowerCase();
|
|
19
|
-
if (lowered === "ipv4") {
|
|
20
|
-
return "ipv4";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (value === 4)
|
|
24
|
-
return "ipv4";
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
|
-
if (host === "0.0.0.0") {
|
|
28
|
-
// Enumerate system interfaces (IPv4 only)
|
|
22
|
+
if (isWildcardHost(host)) {
|
|
23
|
+
const wildcardVersion = isIP(host);
|
|
29
24
|
for (const entries of Object.values(interfaces)) {
|
|
30
25
|
if (!entries)
|
|
31
26
|
continue;
|
|
32
27
|
for (const entry of entries) {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
-
continue;
|
|
36
|
-
if (!entry.address || entry.address === "0.0.0.0")
|
|
28
|
+
const entryVersion = isIP(normalizeNetworkHost(entry.address));
|
|
29
|
+
if (entryVersion !== wildcardVersion && !(wildcardVersion === 6 && entryVersion === 4))
|
|
37
30
|
continue;
|
|
38
31
|
const scope = entry.internal ? "loopback" : "external";
|
|
39
32
|
addAddress(entry.address, scope);
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Include explicitly configured host if it was IPv4
|
|
46
|
-
if (isIPv4Address(host) && host !== "0.0.0.0") {
|
|
47
|
-
const isLoopback = host.startsWith("127.");
|
|
48
|
-
addAddress(host, isLoopback ? "loopback" : "external");
|
|
36
|
+
else if (isIP(host)) {
|
|
37
|
+
addAddress(host, isLoopbackHost(host) ? "loopback" : "external");
|
|
49
38
|
}
|
|
50
39
|
const scopeWeight = { external: 0, internal: 1, loopback: 2 };
|
|
51
40
|
return results.sort((a, b) => {
|
|
@@ -93,23 +82,12 @@ function isPrivateIPv4(ip) {
|
|
|
93
82
|
return first === 172 && second >= 16 && second <= 31;
|
|
94
83
|
}
|
|
95
84
|
function parseIPv4(value) {
|
|
96
|
-
if (
|
|
85
|
+
if (isIP(value) !== 4)
|
|
97
86
|
return null;
|
|
98
87
|
return value.split(".").map((part) => Number(part));
|
|
99
88
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const parts = value.split(".");
|
|
104
|
-
if (parts.length !== 4)
|
|
105
|
-
return false;
|
|
106
|
-
return parts.every((part) => {
|
|
107
|
-
if (part.length === 0 || part.length > 3)
|
|
108
|
-
return false;
|
|
109
|
-
if (!/^[0-9]+$/.test(part))
|
|
110
|
-
return false;
|
|
111
|
-
const num = Number(part);
|
|
112
|
-
return Number.isInteger(num) && num >= 0 && num <= 255;
|
|
113
|
-
});
|
|
89
|
+
function isLinkLocalIPv6(ip) {
|
|
90
|
+
const firstSegment = Number.parseInt(ip.split(":", 1)[0], 16);
|
|
91
|
+
return Number.isInteger(firstSegment) && firstSegment >= 0xfe80 && firstSegment <= 0xfebf;
|
|
114
92
|
}
|
|
115
93
|
//# sourceMappingURL=network-addresses.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network-addresses.js","sourceRoot":"","sources":["../../src/server/network-addresses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"network-addresses.js","sourceRoot":"","sources":["../../src/server/network-addresses.ts"],"names":[],"mappings":"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","sourcesContent":["import os from \"os\"\nimport { isIP } from \"node:net\"\nimport type { NetworkAddress } from \"../api-types\"\nimport { formatHostForUrl, isLoopbackHost, isWildcardHost, normalizeNetworkHost } from \"./network-host\"\n\nexport interface ResolvedRemoteAddresses {\n all: NetworkAddress[]\n userVisible: NetworkAddress[]\n primaryRemoteUrl?: string\n}\n\nexport function resolveNetworkAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): NetworkAddress[] {\n const { protocol, port } = args\n const host = normalizeNetworkHost(args.host)\n const interfaces = os.networkInterfaces()\n const seen = new Set<string>()\n const results: NetworkAddress[] = []\n\n const addAddress = (ip: string, scope: NetworkAddress[\"scope\"]) => {\n const normalizedIp = normalizeNetworkHost(ip)\n const ipVersion = isIP(normalizedIp)\n if (!ipVersion || isWildcardHost(normalizedIp) || (ipVersion === 6 && isLinkLocalIPv6(normalizedIp))) return\n const family = ipVersion === 6 ? \"ipv6\" : \"ipv4\"\n const key = `${family}-${normalizedIp}`\n if (seen.has(key)) return\n seen.add(key)\n results.push({ ip: normalizedIp, family, scope, remoteUrl: `${protocol}://${formatHostForUrl(normalizedIp)}:${port}` })\n }\n\n if (isWildcardHost(host)) {\n const wildcardVersion = isIP(host)\n for (const entries of Object.values(interfaces)) {\n if (!entries) continue\n for (const entry of entries) {\n const entryVersion = isIP(normalizeNetworkHost(entry.address))\n if (entryVersion !== wildcardVersion && !(wildcardVersion === 6 && entryVersion === 4)) continue\n const scope: NetworkAddress[\"scope\"] = entry.internal ? \"loopback\" : \"external\"\n addAddress(entry.address, scope)\n }\n }\n } else if (isIP(host)) {\n addAddress(host, isLoopbackHost(host) ? \"loopback\" : \"external\")\n }\n\n const scopeWeight: Record<NetworkAddress[\"scope\"], number> = { external: 0, internal: 1, loopback: 2 }\n\n return results.sort((a, b) => {\n const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]\n if (scopeDelta !== 0) return scopeDelta\n\n return 0\n })\n}\n\nexport function resolveRemoteAddresses(args: {\n host: string\n protocol: \"http\" | \"https\"\n port: number\n}): ResolvedRemoteAddresses {\n const all = resolveNetworkAddresses(args)\n const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === \"external\"))\n return {\n all,\n userVisible,\n primaryRemoteUrl: userVisible[0]?.remoteUrl,\n }\n}\n\nfunction sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {\n return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))\n}\n\nfunction getUserVisiblePriority(ip: string): number {\n if (isPrivateIPv4(ip)) return 0\n if (isLinkLocalIPv4(ip)) return 2\n return 1\n}\n\nfunction isLinkLocalIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n return first === 169 && second === 254\n}\n\nfunction isPrivateIPv4(ip: string): boolean {\n const octets = parseIPv4(ip)\n if (!octets) return false\n const [first, second] = octets\n\n if (first === 10) return true\n if (first === 192 && second === 168) return true\n return first === 172 && second >= 16 && second <= 31\n}\n\nfunction parseIPv4(value: string): number[] | null {\n if (isIP(value) !== 4) return null\n return value.split(\".\").map((part) => Number(part))\n}\n\nfunction isLinkLocalIPv6(ip: string): boolean {\n const firstSegment = Number.parseInt(ip.split(\":\", 1)[0], 16)\n return Number.isInteger(firstSegment) && firstSegment >= 0xfe80 && firstSegment <= 0xfebf\n}\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { domainToASCII } from "node:url";
|
|
3
|
+
export function stripHostBrackets(host) {
|
|
4
|
+
const trimmed = host.trim();
|
|
5
|
+
return trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
6
|
+
}
|
|
7
|
+
export function normalizeNetworkHost(host) {
|
|
8
|
+
const value = stripHostBrackets(host).toLowerCase();
|
|
9
|
+
if (isIP(value) === 6) {
|
|
10
|
+
const canonical = canonicalIPv6(value);
|
|
11
|
+
const mapped = canonical?.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
12
|
+
if (mapped) {
|
|
13
|
+
const high = Number.parseInt(mapped[1], 16);
|
|
14
|
+
const low = Number.parseInt(mapped[2], 16);
|
|
15
|
+
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (isIP(value))
|
|
19
|
+
return value;
|
|
20
|
+
return domainToASCII(value) || value;
|
|
21
|
+
}
|
|
22
|
+
export function hasIPv6Zone(host) {
|
|
23
|
+
const value = stripHostBrackets(host);
|
|
24
|
+
return value.includes("%") && isIP(value) === 6;
|
|
25
|
+
}
|
|
26
|
+
export function stripIPv6Zone(host) {
|
|
27
|
+
const value = stripHostBrackets(host);
|
|
28
|
+
return hasIPv6Zone(value) ? value.slice(0, value.lastIndexOf("%")) : value;
|
|
29
|
+
}
|
|
30
|
+
export function isWildcardHost(host) {
|
|
31
|
+
const value = normalizeNetworkHost(host);
|
|
32
|
+
if (value === "0.0.0.0")
|
|
33
|
+
return true;
|
|
34
|
+
return isIP(value) === 6 && value.split(":").every((segment) => segment === "" || /^0+$/.test(segment));
|
|
35
|
+
}
|
|
36
|
+
export function isLoopbackHost(host) {
|
|
37
|
+
const value = normalizeNetworkHost(host);
|
|
38
|
+
if (value === "localhost")
|
|
39
|
+
return true;
|
|
40
|
+
if (isIP(value) === 4)
|
|
41
|
+
return value.startsWith("127.");
|
|
42
|
+
if (isIP(value) !== 6)
|
|
43
|
+
return false;
|
|
44
|
+
const segments = value.split(":");
|
|
45
|
+
const last = segments.pop();
|
|
46
|
+
return Boolean(last && /^0*1$/.test(last)) && segments.every((segment) => segment === "" || /^0+$/.test(segment));
|
|
47
|
+
}
|
|
48
|
+
export function formatHostForUrl(host) {
|
|
49
|
+
const value = normalizeNetworkHost(host);
|
|
50
|
+
return isIP(value) === 6 ? `[${value}]` : value;
|
|
51
|
+
}
|
|
52
|
+
function canonicalIPv6(value) {
|
|
53
|
+
if (hasIPv6Zone(value))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
return new URL(`http://[${value}]`).hostname.slice(1, -1);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=network-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-host.js","sourceRoot":"","sources":["../../src/server/network-host.ts"],"names":[],"mappings":"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","sourcesContent":["import { isIP } from \"node:net\"\nimport { domainToASCII } from \"node:url\"\n\nexport function stripHostBrackets(host: string): string {\n const trimmed = host.trim()\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\") ? trimmed.slice(1, -1) : trimmed\n}\n\nexport function normalizeNetworkHost(host: string): string {\n const value = stripHostBrackets(host).toLowerCase()\n if (isIP(value) === 6) {\n const canonical = canonicalIPv6(value)\n const mapped = canonical?.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n if (mapped) {\n const high = Number.parseInt(mapped[1], 16)\n const low = Number.parseInt(mapped[2], 16)\n return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`\n }\n }\n if (isIP(value)) return value\n return domainToASCII(value) || value\n}\n\nexport function hasIPv6Zone(host: string): boolean {\n const value = stripHostBrackets(host)\n return value.includes(\"%\") && isIP(value) === 6\n}\n\nexport function stripIPv6Zone(host: string): string {\n const value = stripHostBrackets(host)\n return hasIPv6Zone(value) ? value.slice(0, value.lastIndexOf(\"%\")) : value\n}\n\nexport function isWildcardHost(host: string): boolean {\n const value = normalizeNetworkHost(host)\n if (value === \"0.0.0.0\") return true\n return isIP(value) === 6 && value.split(\":\").every((segment) => segment === \"\" || /^0+$/.test(segment))\n}\n\nexport function isLoopbackHost(host: string): boolean {\n const value = normalizeNetworkHost(host)\n if (value === \"localhost\") return true\n if (isIP(value) === 4) return value.startsWith(\"127.\")\n if (isIP(value) !== 6) return false\n\n const segments = value.split(\":\")\n const last = segments.pop()\n return Boolean(last && /^0*1$/.test(last)) && segments.every((segment) => segment === \"\" || /^0+$/.test(segment))\n}\n\nexport function formatHostForUrl(host: string): string {\n const value = normalizeNetworkHost(host)\n return isIP(value) === 6 ? `[${value}]` : value\n}\n\nfunction canonicalIPv6(value: string): string | null {\n if (hasIPv6Zone(value)) return null\n try {\n return new URL(`http://[${value}]`).hostname.slice(1, -1)\n } catch {\n return null\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLoopbackHost, isWildcardHost } from "../network-host";
|
|
1
2
|
export function registerMetaRoutes(app, deps) {
|
|
2
3
|
app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta));
|
|
3
4
|
}
|
|
@@ -8,7 +9,7 @@ function buildMetaResponse(meta) {
|
|
|
8
9
|
...meta,
|
|
9
10
|
localPort,
|
|
10
11
|
remotePort: remote?.port,
|
|
11
|
-
listeningMode: meta.host
|
|
12
|
+
listeningMode: isWildcardHost(meta.host) || !isLoopbackHost(meta.host) ? "all" : "local",
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
function resolveLocalPort(meta) {
|
|
@@ -38,8 +39,5 @@ function resolveRemote(meta) {
|
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
function isLoopbackHost(host) {
|
|
42
|
-
return host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
43
|
-
}
|
|
44
42
|
// NetworkAddress shape is resolved in ../network-addresses
|
|
45
43
|
//# sourceMappingURL=meta.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/server/routes/meta.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/server/routes/meta.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAOhE,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,IAAe;IACtE,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAA;AACtE,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAgB;IACzC,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;IAElC,OAAO;QACL,GAAG,IAAI;QACP,SAAS;QACT,UAAU,EAAE,MAAM,EAAE,IAAI;QACxB,aAAa,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;KACzF,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAgB;IACxC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAA;IACV,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,IAAgB;IACrC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAA;QAChE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,2DAA2D","sourcesContent":["import { FastifyInstance } from \"fastify\"\nimport { ServerMeta } from \"../../api-types\"\nimport { isLoopbackHost, isWildcardHost } from \"../network-host\"\n \n\ninterface RouteDeps {\n serverMeta: ServerMeta\n}\n\nexport function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {\n app.get(\"/api/meta\", async () => buildMetaResponse(deps.serverMeta))\n}\n\nfunction buildMetaResponse(meta: ServerMeta): ServerMeta {\n const localPort = resolveLocalPort(meta)\n const remote = resolveRemote(meta)\n\n return {\n ...meta,\n localPort,\n remotePort: remote?.port,\n listeningMode: isWildcardHost(meta.host) || !isLoopbackHost(meta.host) ? \"all\" : \"local\",\n }\n}\n\nfunction resolveLocalPort(meta: ServerMeta): number {\n if (Number.isInteger(meta.localPort) && meta.localPort > 0) {\n return meta.localPort\n }\n try {\n const parsed = new URL(meta.localUrl)\n const port = Number(parsed.port)\n return Number.isInteger(port) && port > 0 ? port : 0\n } catch {\n return 0\n }\n}\n\nfunction resolveRemote(meta: ServerMeta): { protocol: \"http\" | \"https\"; port: number } | null {\n if (!meta.remoteUrl) {\n return null\n }\n try {\n const parsed = new URL(meta.remoteUrl)\n const protocol = parsed.protocol === \"https:\" ? \"https\" : \"http\"\n const port = Number(parsed.port)\n return { protocol, port: Number.isInteger(port) && port > 0 ? port : 0 }\n } catch {\n return null\n }\n}\n\n// NetworkAddress shape is resolved in ../network-addresses\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from "../../opencode/session-pruning/contract";
|
|
2
|
+
export function registerSessionPruningRoutes(app, deps) {
|
|
3
|
+
for (const method of ["preview", "prune"]) {
|
|
4
|
+
app.post(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {
|
|
5
|
+
const input = (method === "prune" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body);
|
|
6
|
+
if (!input.success)
|
|
7
|
+
return reply.code(400).send({ error: "Invalid pruning request" });
|
|
8
|
+
const client = await deps.workspaceManager.getSharedServiceClient();
|
|
9
|
+
const session = await client.session.get({ sessionID: input.data.sessionID });
|
|
10
|
+
if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {
|
|
11
|
+
return reply.code(403).send({ error: "Session does not belong to workspace" });
|
|
12
|
+
}
|
|
13
|
+
const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory);
|
|
14
|
+
if (!identity)
|
|
15
|
+
return reply.code(403).send({ error: "Session does not belong to workspace" });
|
|
16
|
+
const release = deps.worktreeDeletionFence.enter([identity]);
|
|
17
|
+
if (!release)
|
|
18
|
+
return reply.code(409).send({ error: "Worktree deletion is in progress" });
|
|
19
|
+
try {
|
|
20
|
+
// Narrow broker only: callers cannot choose RPC ID, method, location,
|
|
21
|
+
// database filename, SQL, or replacement content. Generic RPC stays blocked.
|
|
22
|
+
const result = await client.rpc.call({
|
|
23
|
+
rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },
|
|
24
|
+
}, { signal: AbortSignal.timeout(15_000) });
|
|
25
|
+
const output = (method === "prune" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output);
|
|
26
|
+
if (!output.success)
|
|
27
|
+
return { status: "blocked", reason: "unavailable" };
|
|
28
|
+
if (output.data.status === "pruned" && (output.data.messageID !== input.data.messageID
|
|
29
|
+
|| !("indexes" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {
|
|
30
|
+
return { status: "blocked", reason: "unavailable" };
|
|
31
|
+
}
|
|
32
|
+
return output.data;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Missing plugin, incompatible runtime, timeout: no legacy API fallback
|
|
36
|
+
// and no local-only deletion presented as a successful database write.
|
|
37
|
+
return { status: "blocked", reason: "unavailable" };
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
release();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=session-pruning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-pruning.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FastifyInstance } from \"fastify\"\nimport { messageTargetSchema, PRUNING_RPC_ID, prunePreviewSchema, pruneRequestSchema, pruneResultSchema } from \"../../opencode/session-pruning/contract\"\nimport type { WorkspaceManager } from \"../../workspaces/manager\"\nimport type { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\ninterface RouteDeps {\n workspaceManager: Pick<WorkspaceManager, \"getSharedServiceClient\" | \"ownsLocation\" | \"getWorktreeIdentityForPath\">\n worktreeDeletionFence: WorktreeDeletionFence\n}\n\nexport function registerSessionPruningRoutes(app: FastifyInstance, deps: RouteDeps): void {\n for (const method of [\"preview\", \"prune\"] as const) {\n app.post<{ Params: { id: string } }>(`/api/workspaces/:id/session-pruning/${method}`, { bodyLimit: 1024 * 1024 }, async (request, reply) => {\n const input = (method === \"prune\" ? pruneRequestSchema : messageTargetSchema).safeParse(request.body)\n if (!input.success) return reply.code(400).send({ error: \"Invalid pruning request\" })\n const client = await deps.workspaceManager.getSharedServiceClient()\n const session = await client.session.get({ sessionID: input.data.sessionID })\n if (!await deps.workspaceManager.ownsLocation(request.params.id, session.location)) {\n return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n }\n const identity = await deps.workspaceManager.getWorktreeIdentityForPath(request.params.id, session.location.directory)\n if (!identity) return reply.code(403).send({ error: \"Session does not belong to workspace\" })\n const release = deps.worktreeDeletionFence.enter([identity])\n if (!release) return reply.code(409).send({ error: \"Worktree deletion is in progress\" })\n try {\n // Narrow broker only: callers cannot choose RPC ID, method, location,\n // database filename, SQL, or replacement content. Generic RPC stays blocked.\n const result = await client.rpc.call({\n rpcID: PRUNING_RPC_ID, method, input: input.data, location: { directory: session.location.directory },\n }, { signal: AbortSignal.timeout(15_000) })\n const output = (method === \"prune\" ? pruneResultSchema : prunePreviewSchema).safeParse(result.output)\n if (!output.success) return { status: \"blocked\", reason: \"unavailable\" }\n if (output.data.status === \"pruned\" && (output.data.messageID !== input.data.messageID\n || !(\"indexes\" in input.data) || !Array.isArray(input.data.indexes) || output.data.removedCount !== input.data.indexes.length)) {\n return { status: \"blocked\", reason: \"unavailable\" }\n }\n return output.data\n } catch {\n // Missing plugin, incompatible runtime, timeout: no legacy API fallback\n // and no local-only deletion presented as a successful database write.\n return { status: \"blocked\", reason: \"unavailable\" }\n } finally { release() }\n })\n }\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import Fastify from "fastify";
|
|
4
|
+
import { registerSessionPruningRoutes } from "./session-pruning";
|
|
5
|
+
import { WorktreeDeletionFence } from "../../workspaces/worktree-session-evacuation";
|
|
6
|
+
const payload = { sessionID: "s", messageID: "m", revision: "a".repeat(64), indexes: [0] };
|
|
7
|
+
const url = "/api/workspaces/w/session-pruning/prune";
|
|
8
|
+
function fixture(options = {}) {
|
|
9
|
+
const calls = [];
|
|
10
|
+
const app = Fastify();
|
|
11
|
+
const fence = new WorktreeDeletionFence();
|
|
12
|
+
registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {
|
|
13
|
+
getSharedServiceClient: async () => ({
|
|
14
|
+
session: { get: async () => ({ location: { directory: "/owned/worktree" } }) },
|
|
15
|
+
rpc: { call: async (input, opts) => {
|
|
16
|
+
assert(opts.signal instanceof AbortSignal);
|
|
17
|
+
calls.push(input);
|
|
18
|
+
if (options.fail)
|
|
19
|
+
throw new Error("plugin unavailable");
|
|
20
|
+
return { output: options.output ?? { status: "blocked", reason: "maintenance_required" } };
|
|
21
|
+
} },
|
|
22
|
+
}),
|
|
23
|
+
ownsLocation: async () => options.owned !== false,
|
|
24
|
+
getWorktreeIdentityForPath: async () => "/owned/worktree",
|
|
25
|
+
} });
|
|
26
|
+
return { app, calls, fence };
|
|
27
|
+
}
|
|
28
|
+
test("broker pins RPC, method and native session location", async () => {
|
|
29
|
+
const { app, calls } = fixture();
|
|
30
|
+
try {
|
|
31
|
+
const response = await app.inject({ method: "POST", url, payload });
|
|
32
|
+
assert.equal(response.statusCode, 200);
|
|
33
|
+
assert.deepEqual(response.json(), { status: "blocked", reason: "maintenance_required" });
|
|
34
|
+
assert.deepEqual(calls, [{ rpcID: "codenomad.session-pruning", method: "prune", input: payload, location: { directory: "/owned/worktree" } }]);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await app.close();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
test("rejects unowned sessions before any RPC", async () => {
|
|
41
|
+
const { app, calls } = fixture({ owned: false });
|
|
42
|
+
try {
|
|
43
|
+
assert.equal((await app.inject({ method: "POST", url, payload })).statusCode, 403);
|
|
44
|
+
assert.equal(calls.length, 0);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await app.close();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
test("rejects caller-selected location, RPC, database and replacement content", async () => {
|
|
51
|
+
const { app, calls } = fixture();
|
|
52
|
+
try {
|
|
53
|
+
for (const extra of [{ location: { directory: "/other" } }, { rpcID: "other" }, { databasePath: "live.db" }, { content: [] }]) {
|
|
54
|
+
assert.equal((await app.inject({ method: "POST", url, payload: { ...payload, ...extra } })).statusCode, 400);
|
|
55
|
+
}
|
|
56
|
+
assert.equal(calls.length, 0);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await app.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
test("does not bypass worktree deletion fence", async () => {
|
|
63
|
+
const { app, calls, fence } = fixture();
|
|
64
|
+
try {
|
|
65
|
+
await fence.run("/owned/worktree", ["/owned/worktree"], async () => {
|
|
66
|
+
assert.equal((await app.inject({ method: "POST", url, payload })).statusCode, 409);
|
|
67
|
+
});
|
|
68
|
+
assert.equal(calls.length, 0);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await app.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
test("missing plugin, malformed response, wrong message or count never report success", async () => {
|
|
75
|
+
for (const options of [{ fail: true }, { output: {} }, { output: { status: "pruned", messageID: "other", revision: "a".repeat(64), removedCount: 1 } }, { output: { status: "pruned", messageID: "m", revision: "a".repeat(64), removedCount: 2 } }]) {
|
|
76
|
+
const { app, fence } = fixture(options);
|
|
77
|
+
try {
|
|
78
|
+
const response = await app.inject({ method: "POST", url, payload });
|
|
79
|
+
assert.deepEqual(response.json(), { status: "blocked", reason: "unavailable" });
|
|
80
|
+
await fence.run("/owned/worktree", ["/owned/worktree"], async () => { });
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await app.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test("preview accepts IDs only and uses a distinct fixed RPC method", async () => {
|
|
88
|
+
const { app, calls } = fixture({ output: { status: "preview", revision: "a".repeat(64), liveMutation: false, parts: [] } });
|
|
89
|
+
try {
|
|
90
|
+
const response = await app.inject({ method: "POST", url: url.replace(/prune$/, "preview"), payload: { sessionID: "s", messageID: "m" } });
|
|
91
|
+
assert.equal(response.json().status, "preview");
|
|
92
|
+
assert.equal(calls[0].method, "preview");
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await app.close();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=session-pruning.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-pruning.test.js","sourceRoot":"","sources":["../../../src/server/routes/session-pruning.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport { registerSessionPruningRoutes } from \"./session-pruning\"\nimport { WorktreeDeletionFence } from \"../../workspaces/worktree-session-evacuation\"\n\nconst payload = { sessionID: \"s\", messageID: \"m\", revision: \"a\".repeat(64), indexes: [0] }\nconst url = \"/api/workspaces/w/session-pruning/prune\"\nfunction fixture(options: { owned?: boolean; output?: unknown; fail?: boolean } = {}) {\n const calls: any[] = []\n const app = Fastify()\n const fence = new WorktreeDeletionFence()\n registerSessionPruningRoutes(app, { worktreeDeletionFence: fence, workspaceManager: {\n getSharedServiceClient: async () => ({\n session: { get: async () => ({ location: { directory: \"/owned/worktree\" } }) },\n rpc: { call: async (input: unknown, opts: any) => {\n assert(opts.signal instanceof AbortSignal)\n calls.push(input)\n if (options.fail) throw new Error(\"plugin unavailable\")\n return { output: options.output ?? { status: \"blocked\", reason: \"maintenance_required\" } }\n } },\n }) as any,\n ownsLocation: async () => options.owned !== false,\n getWorktreeIdentityForPath: async () => \"/owned/worktree\",\n } })\n return { app, calls, fence }\n}\n\ntest(\"broker pins RPC, method and native session location\", async () => {\n const { app, calls } = fixture()\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.equal(response.statusCode, 200)\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"maintenance_required\" })\n assert.deepEqual(calls, [{ rpcID: \"codenomad.session-pruning\", method: \"prune\", input: payload, location: { directory: \"/owned/worktree\" } }])\n } finally { await app.close() }\n})\n\ntest(\"rejects unowned sessions before any RPC\", async () => {\n const { app, calls } = fixture({ owned: false })\n try {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 403)\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"rejects caller-selected location, RPC, database and replacement content\", async () => {\n const { app, calls } = fixture()\n try {\n for (const extra of [{ location: { directory: \"/other\" } }, { rpcID: \"other\" }, { databasePath: \"live.db\" }, { content: [] }]) {\n assert.equal((await app.inject({ method: \"POST\", url, payload: { ...payload, ...extra } })).statusCode, 400)\n }\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"does not bypass worktree deletion fence\", async () => {\n const { app, calls, fence } = fixture()\n try {\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {\n assert.equal((await app.inject({ method: \"POST\", url, payload })).statusCode, 409)\n })\n assert.equal(calls.length, 0)\n } finally { await app.close() }\n})\n\ntest(\"missing plugin, malformed response, wrong message or count never report success\", async () => {\n for (const options of [{ fail: true }, { output: {} }, { output: { status: \"pruned\", messageID: \"other\", revision: \"a\".repeat(64), removedCount: 1 } }, { output: { status: \"pruned\", messageID: \"m\", revision: \"a\".repeat(64), removedCount: 2 } }]) {\n const { app, fence } = fixture(options)\n try {\n const response = await app.inject({ method: \"POST\", url, payload })\n assert.deepEqual(response.json(), { status: \"blocked\", reason: \"unavailable\" })\n await fence.run(\"/owned/worktree\", [\"/owned/worktree\"], async () => {})\n } finally { await app.close() }\n }\n})\n\ntest(\"preview accepts IDs only and uses a distinct fixed RPC method\", async () => {\n const { app, calls } = fixture({ output: { status: \"preview\", revision: \"a\".repeat(64), liveMutation: false, parts: [] } })\n try {\n const response = await app.inject({ method: \"POST\", url: url.replace(/prune$/, \"preview\"), payload: { sessionID: \"s\", messageID: \"m\" } })\n assert.equal(response.json().status, \"preview\")\n assert.equal(calls[0].method, \"preview\")\n } finally { await app.close() }\n})\n"]}
|
package/dist/server/tls.js
CHANGED
|
@@ -2,6 +2,8 @@ import crypto from "crypto";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
+
import { isIP } from "node:net";
|
|
6
|
+
import { isWildcardHost, normalizeNetworkHost, stripIPv6Zone } from "./network-host";
|
|
5
7
|
const require = createRequire(import.meta.url);
|
|
6
8
|
function loadForge() {
|
|
7
9
|
// node-forge is CJS in many installs; require keeps this compatible with our ESM output.
|
|
@@ -39,12 +41,12 @@ function ensureGeneratedTls(args) {
|
|
|
39
41
|
if (!fs.existsSync(certPath))
|
|
40
42
|
return true;
|
|
41
43
|
const pem = fs.readFileSync(certPath, "utf-8");
|
|
42
|
-
const
|
|
43
|
-
const validToMs = Date.parse(
|
|
44
|
+
const certificate = new crypto.X509Certificate(pem);
|
|
45
|
+
const validToMs = Date.parse(certificate.validTo);
|
|
44
46
|
if (!Number.isFinite(validToMs))
|
|
45
47
|
return true;
|
|
46
48
|
const rotateAt = validToMs - ROTATE_IF_EXPIRES_WITHIN_DAYS * 24 * 60 * 60 * 1000;
|
|
47
|
-
return Date.now() >= rotateAt;
|
|
49
|
+
return Date.now() >= rotateAt || !certificateCoversRequiredSans(certificate, args.host, args.tlsSANs);
|
|
48
50
|
}
|
|
49
51
|
catch {
|
|
50
52
|
return true;
|
|
@@ -164,44 +166,73 @@ function generateServerCertificate(args) {
|
|
|
164
166
|
};
|
|
165
167
|
}
|
|
166
168
|
function pickCommonName(host) {
|
|
167
|
-
|
|
169
|
+
const normalizedHost = normalizeCertificateHost(host);
|
|
170
|
+
if (!normalizedHost || isWildcardHost(normalizedHost)) {
|
|
168
171
|
return "localhost";
|
|
169
172
|
}
|
|
170
|
-
if (
|
|
173
|
+
if (normalizedHost === "127.0.0.1") {
|
|
171
174
|
return "localhost";
|
|
172
175
|
}
|
|
173
|
-
return
|
|
176
|
+
return normalizedHost;
|
|
174
177
|
}
|
|
175
178
|
function buildSubjectAltNames(host, tlsSANs) {
|
|
179
|
+
const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs);
|
|
180
|
+
const altNames = [];
|
|
181
|
+
// 2 = DNS, 7 = IP
|
|
182
|
+
for (const name of dns) {
|
|
183
|
+
altNames.push({ type: 2, value: name });
|
|
184
|
+
}
|
|
185
|
+
for (const ip of ips) {
|
|
186
|
+
altNames.push({ type: 7, ip });
|
|
187
|
+
}
|
|
188
|
+
return altNames;
|
|
189
|
+
}
|
|
190
|
+
function resolveRequiredSubjectAltNames(host, tlsSANs) {
|
|
176
191
|
const dns = new Set();
|
|
177
192
|
const ips = new Set();
|
|
178
193
|
dns.add("localhost");
|
|
179
194
|
ips.add("127.0.0.1");
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
const normalizedHost = normalizeCertificateHost(host);
|
|
196
|
+
if (isWildcardHost(normalizedHost) && isIP(normalizedHost) === 6) {
|
|
197
|
+
ips.add("::1");
|
|
198
|
+
}
|
|
199
|
+
else if (normalizedHost) {
|
|
200
|
+
if (isIP(normalizedHost)) {
|
|
201
|
+
ips.add(normalizedHost);
|
|
183
202
|
}
|
|
184
203
|
else {
|
|
185
|
-
dns.add(
|
|
204
|
+
dns.add(normalizedHost);
|
|
186
205
|
}
|
|
187
206
|
}
|
|
188
207
|
for (const token of splitList(tlsSANs)) {
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
const normalizedToken = normalizeCertificateHost(token);
|
|
209
|
+
if (isIP(normalizedToken)) {
|
|
210
|
+
ips.add(normalizedToken);
|
|
191
211
|
}
|
|
192
|
-
else if (
|
|
193
|
-
dns.add(
|
|
212
|
+
else if (normalizedToken) {
|
|
213
|
+
dns.add(normalizedToken);
|
|
194
214
|
}
|
|
195
215
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
return { dns, ips };
|
|
217
|
+
}
|
|
218
|
+
function normalizeCertificateHost(host) {
|
|
219
|
+
return normalizeNetworkHost(stripIPv6Zone(host));
|
|
220
|
+
}
|
|
221
|
+
function certificateCoversRequiredSans(certificate, host, tlsSANs) {
|
|
222
|
+
const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs);
|
|
223
|
+
const existingDns = new Set((certificate.subjectAltName ?? "")
|
|
224
|
+
.split(/,\s*/)
|
|
225
|
+
.filter((entry) => entry.startsWith("DNS:"))
|
|
226
|
+
.map((entry) => entry.slice(4).toLowerCase()));
|
|
227
|
+
for (const name of dns) {
|
|
228
|
+
if (!existingDns.has(name.toLowerCase()))
|
|
229
|
+
return false;
|
|
200
230
|
}
|
|
201
|
-
for (const ip of
|
|
202
|
-
|
|
231
|
+
for (const ip of ips) {
|
|
232
|
+
if (!certificate.checkIP(ip))
|
|
233
|
+
return false;
|
|
203
234
|
}
|
|
204
|
-
return
|
|
235
|
+
return true;
|
|
205
236
|
}
|
|
206
237
|
function splitList(input) {
|
|
207
238
|
if (!input)
|
|
@@ -211,15 +242,4 @@ function splitList(input) {
|
|
|
211
242
|
.map((part) => part.trim())
|
|
212
243
|
.filter(Boolean);
|
|
213
244
|
}
|
|
214
|
-
function isIPv4(value) {
|
|
215
|
-
const parts = value.split(".");
|
|
216
|
-
if (parts.length !== 4)
|
|
217
|
-
return false;
|
|
218
|
-
return parts.every((part) => {
|
|
219
|
-
if (!/^[0-9]+$/.test(part))
|
|
220
|
-
return false;
|
|
221
|
-
const num = Number(part);
|
|
222
|
-
return Number.isInteger(num) && num >= 0 && num <= 255;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
245
|
//# sourceMappingURL=tls.js.map
|