@neuralnomads/codenomad-dev 0.20.0-dev-20260908-e5115fe8 → 0.20.0-dev-20260910-abaf6eba
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/index.js +29 -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/server/__tests__/instance-proxy.test.js +14 -0
- 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 +7 -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/tls.js +52 -32
- package/dist/server/tls.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/App-0ECD8eRb.js +43 -0
- package/public/assets/App-0ECD8eRb.js.map +1 -0
- package/public/assets/FilesTab-BVJawmU7.js +3 -0
- package/public/assets/{FilesTab-C8fnWsM4.js.map → FilesTab-BVJawmU7.js.map} +1 -1
- package/public/assets/{GitChangesTab-C3rapWwl.js → GitChangesTab-BVdfbJ_l.js} +3 -3
- package/public/assets/{GitChangesTab-C3rapWwl.js.map → GitChangesTab-BVdfbJ_l.js.map} +1 -1
- package/public/assets/{SplitFilePanel-D3S7Zfcc.js → SplitFilePanel-CSR5-RHY.js} +2 -2
- package/public/assets/{SplitFilePanel-D3S7Zfcc.js.map → SplitFilePanel-CSR5-RHY.js.map} +1 -1
- package/public/assets/{StatusTab-DHvQ-0to.js → StatusTab-DIodfPQd.js} +2 -2
- package/public/assets/{StatusTab-DHvQ-0to.js.map → StatusTab-DIodfPQd.js.map} +1 -1
- package/public/assets/{align-justify-BIN0BnZw.js → align-justify-kDFhSvpm.js} +2 -2
- package/public/assets/{align-justify-BIN0BnZw.js.map → align-justify-kDFhSvpm.js.map} +1 -1
- package/public/assets/{bundle-full-BfEvraVO.js → bundle-full-B4eYITdG.js} +2 -2
- package/public/assets/{bundle-full-BfEvraVO.js.map → bundle-full-B4eYITdG.js.map} +1 -1
- package/public/assets/{client-state-CBKOWfhu.js → client-state-D3g-WH8W.js} +2 -2
- package/public/assets/{client-state-CBKOWfhu.js.map → client-state-D3g-WH8W.js.map} +1 -1
- package/public/assets/{diff-viewer-6oxyPi3-.js → diff-viewer-BLL0OcUK.js} +2 -2
- package/public/assets/{diff-viewer-6oxyPi3-.js.map → diff-viewer-BLL0OcUK.js.map} +1 -1
- package/public/assets/index-BIqxsbaj.js +2 -0
- package/public/assets/{index-C9znv1NB.js.map → index-BIqxsbaj.js.map} +1 -1
- package/public/assets/index-BtuH3zKP.js +2 -0
- package/public/assets/index-BtuH3zKP.js.map +1 -0
- package/public/assets/index-Bz_OQJYA.css +1 -0
- package/public/assets/{index-BhBg6Tnf.js → index-CeF-jpOV.js} +2 -2
- package/public/assets/{index-BhBg6Tnf.js.map → index-CeF-jpOV.js.map} +1 -1
- package/public/assets/index-CrOPzgHN.js +2 -0
- package/public/assets/index-CrOPzgHN.js.map +1 -0
- package/public/assets/index-D21P0GMO.js +2 -0
- package/public/assets/index-D21P0GMO.js.map +1 -0
- package/public/assets/index-DLRrUgI7.js +2 -0
- package/public/assets/index-DLRrUgI7.js.map +1 -0
- package/public/assets/index-DLtH2OSD.js +2 -0
- package/public/assets/index-DLtH2OSD.js.map +1 -0
- package/public/assets/index-DWnsOFes.js +2 -0
- package/public/assets/index-DWnsOFes.js.map +1 -0
- package/public/assets/index-DbfYo3i-.js +2 -0
- package/public/assets/index-DbfYo3i-.js.map +1 -0
- package/public/assets/index-DnqYBc2b.js +2 -0
- package/public/assets/index-DnqYBc2b.js.map +1 -0
- package/public/assets/index-ZnQhzm1X.js +2 -0
- package/public/assets/index-ZnQhzm1X.js.map +1 -0
- package/public/assets/index-e5yyN2yK.js +3 -0
- package/public/assets/index-e5yyN2yK.js.map +1 -0
- package/public/assets/{loading-CanSHf7z.js → loading-CaoeVfIV.js} +2 -2
- package/public/assets/{loading-CanSHf7z.js.map → loading-CaoeVfIV.js.map} +1 -1
- package/public/assets/main-CeMcnDRn.js +48 -0
- package/public/assets/main-CeMcnDRn.js.map +1 -0
- package/public/assets/{markdown-B04qkFuc.js → markdown-BeJ9YIFU.js} +4 -4
- package/public/assets/{markdown-B04qkFuc.js.map → markdown-BeJ9YIFU.js.map} +1 -1
- package/public/assets/{monaco-viewer-BB3_mBkA.js → monaco-viewer-Cc9bWg7I.js} +2 -2
- package/public/assets/{monaco-viewer-BB3_mBkA.js.map → monaco-viewer-Cc9bWg7I.js.map} +1 -1
- package/public/assets/{startup-state-settings-card-DE11-Je6.js → startup-state-settings-card-BvpJJ6ei.js} +2 -2
- package/public/assets/{startup-state-settings-card-DE11-Je6.js.map → startup-state-settings-card-BvpJJ6ei.js.map} +1 -1
- package/public/assets/{terminal-square-W8UL0Uzs.js → terminal-square-DBCSEAmh.js} +2 -2
- package/public/assets/{terminal-square-W8UL0Uzs.js.map → terminal-square-DBCSEAmh.js.map} +1 -1
- package/public/assets/tool-call-BJiSHSx7.js +60 -0
- package/public/assets/tool-call-BJiSHSx7.js.map +1 -0
- package/public/assets/{unified-picker-CkVc32ly.js → unified-picker-CAEyj3a0.js} +2 -2
- package/public/assets/{unified-picker-CkVc32ly.js.map → unified-picker-CAEyj3a0.js.map} +1 -1
- package/public/assets/{wrap-text-CR-cuiJP.js → wrap-text-DJNV0ypl.js} +2 -2
- package/public/assets/{wrap-text-CR-cuiJP.js.map → wrap-text-DJNV0ypl.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-DPj4p3An.js +0 -43
- package/public/assets/App-DPj4p3An.js.map +0 -1
- package/public/assets/FilesTab-C8fnWsM4.js +0 -3
- package/public/assets/index-BJrZpSe6.js +0 -2
- package/public/assets/index-BJrZpSe6.js.map +0 -1
- package/public/assets/index-B_9dhJTl.js +0 -2
- package/public/assets/index-B_9dhJTl.js.map +0 -1
- package/public/assets/index-BeWVYvfJ.js +0 -2
- package/public/assets/index-BeWVYvfJ.js.map +0 -1
- package/public/assets/index-C9znv1NB.js +0 -2
- package/public/assets/index-CD9b6FoA.js +0 -2
- package/public/assets/index-CD9b6FoA.js.map +0 -1
- package/public/assets/index-CT42quXi.css +0 -1
- package/public/assets/index-CmJg6xjr.js +0 -2
- package/public/assets/index-CmJg6xjr.js.map +0 -1
- package/public/assets/index-DHNlQHw2.js +0 -2
- package/public/assets/index-DHNlQHw2.js.map +0 -1
- package/public/assets/index-DdklToXY.js +0 -2
- package/public/assets/index-DdklToXY.js.map +0 -1
- package/public/assets/index-Dr5L4afe.js +0 -2
- package/public/assets/index-Dr5L4afe.js.map +0 -1
- package/public/assets/index-DsmOPul3.js +0 -2
- package/public/assets/index-DsmOPul3.js.map +0 -1
- package/public/assets/index-ef9_CbWU.js +0 -3
- package/public/assets/index-ef9_CbWU.js.map +0 -1
- package/public/assets/main-COtTRZe6.js +0 -48
- package/public/assets/main-COtTRZe6.js.map +0 -1
- package/public/assets/tool-call-irc_6gLg.js +0 -60
- package/public/assets/tool-call-irc_6gLg.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":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAQvG,MAAM,UAAU,uBAAuB,CAAC,IAIvC;IACC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;IAC/B,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAA;IACzC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,OAAO,GAAqB,EAAE,CAAA;IAEpC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,KAA8B,EAAE,EAAE;QAChE,MAAM,YAAY,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAA;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,CAAA;QACpC,IAAI,CAAC,SAAS,IAAI,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,IAAI,eAAe,CAAC,YAAY,CAAC,CAAC;YAAE,OAAM;QAC5G,MAAM,MAAM,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;QAChD,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,YAAY,EAAE,CAAA;QACvC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAM;QACzB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,QAAQ,MAAM,gBAAgB,CAAC,YAAY,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;IACzH,CAAC,CAAA;IAED,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,CAAA;QAClC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,OAAO;gBAAE,SAAQ;YACtB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,YAAY,GAAG,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;gBAC9D,IAAI,YAAY,KAAK,eAAe,IAAI,CAAC,CAAC,eAAe,KAAK,CAAC,IAAI,YAAY,KAAK,CAAC,CAAC;oBAAE,SAAQ;gBAChG,MAAM,KAAK,GAA4B,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAA;gBAC/E,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,UAAU,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IAClE,CAAC;IAED,MAAM,WAAW,GAA4C,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;IAEtG,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3B,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;QAC9D,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO,UAAU,CAAA;QAEvC,OAAO,CAAC,CAAA;IACV,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAItC;IACC,MAAM,GAAG,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAA;IACzC,MAAM,WAAW,GAAG,wBAAwB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAA;IACnG,OAAO;QACL,GAAG;QACH,WAAW;QACX,gBAAgB,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS;KAC5C,CAAA;AACH,CAAC;AAED,SAAS,wBAAwB,CAAC,SAA2B;IAC3D,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AACjH,CAAC;AAED,SAAS,sBAAsB,CAAC,EAAU;IACxC,IAAI,aAAa,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAA;IAC/B,IAAI,eAAe,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAA;IACjC,OAAO,CAAC,CAAA;AACV,CAAC;AAED,SAAS,eAAe,CAAC,EAAU;IACjC,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,CAAA;IAC5B,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC9B,OAAO,KAAK,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,CAAA;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,EAAU;IAC/B,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,CAAA;IAC5B,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAE9B,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IAC7B,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAChD,OAAO,KAAK,KAAK,GAAG,IAAI,MAAM,IAAI,EAAE,IAAI,MAAM,IAAI,EAAE,CAAA;AACtD,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,EAAU;IACjC,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7D,OAAO,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,MAAM,IAAI,YAAY,IAAI,MAAM,CAAA;AAC3F,CAAC","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"]}
|
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
|
package/dist/server/tls.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tls.js","sourceRoot":"","sources":["../../src/server/tls.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"crypto\"\nimport fs from \"fs\"\nimport path from \"path\"\nimport { createRequire } from \"module\"\nimport type { Logger } from \"../logger\"\n\nconst require = createRequire(import.meta.url)\n\ntype Forge = typeof import(\"node-forge\")\n\nfunction loadForge(): Forge {\n // node-forge is CJS in many installs; require keeps this compatible with our ESM output.\n return require(\"node-forge\") as Forge\n}\n\nexport interface ResolvedHttpsOptions {\n httpsOptions: { key: string | Buffer; cert: string | Buffer; ca?: string | Buffer }\n /** Path to CA certificate suitable for NODE_EXTRA_CA_CERTS. */\n caCertPath?: string\n mode: \"provided\" | \"generated\"\n}\n\nexport interface ResolveHttpsOptionsArgs {\n enabled: boolean\n configDir: string\n host: string\n tlsKeyPath?: string\n tlsCertPath?: string\n tlsCaPath?: string\n tlsSANs?: string\n logger: Logger\n}\n\nconst LEAF_VALIDITY_DAYS = 30\nconst ROTATE_IF_EXPIRES_WITHIN_DAYS = 3\n\nconst CA_VALIDITY_DAYS = 365\n\nexport function resolveHttpsOptions(args: ResolveHttpsOptionsArgs): ResolvedHttpsOptions | null {\n if (!args.enabled) {\n return null\n }\n\n const hasProvided = Boolean(args.tlsKeyPath && args.tlsCertPath)\n if (hasProvided) {\n const key = fs.readFileSync(args.tlsKeyPath!, \"utf-8\")\n const cert = fs.readFileSync(args.tlsCertPath!, \"utf-8\")\n const ca = args.tlsCaPath ? fs.readFileSync(args.tlsCaPath, \"utf-8\") : undefined\n return {\n httpsOptions: { key, cert, ca },\n caCertPath: args.tlsCaPath,\n mode: \"provided\",\n }\n }\n\n return ensureGeneratedTls(args)\n}\n\nfunction ensureGeneratedTls(args: ResolveHttpsOptionsArgs): ResolvedHttpsOptions {\n const tlsDir = path.join(args.configDir, \"tls\")\n const caKeyPath = path.join(tlsDir, \"ca-key.pem\")\n const caCertPath = path.join(tlsDir, \"ca-cert.pem\")\n const keyPath = path.join(tlsDir, \"server-key.pem\")\n const certPath = path.join(tlsDir, \"server-cert.pem\")\n\n fs.mkdirSync(tlsDir, { recursive: true })\n\n const shouldRotateLeaf = () => {\n try {\n if (!fs.existsSync(certPath)) return true\n const pem = fs.readFileSync(certPath, \"utf-8\")\n const x509 = new crypto.X509Certificate(pem)\n const validToMs = Date.parse(x509.validTo)\n if (!Number.isFinite(validToMs)) return true\n const rotateAt = validToMs - ROTATE_IF_EXPIRES_WITHIN_DAYS * 24 * 60 * 60 * 1000\n return Date.now() >= rotateAt\n } catch {\n return true\n }\n }\n\n const shouldRotateCa = () => {\n try {\n if (!fs.existsSync(caCertPath)) return true\n const pem = fs.readFileSync(caCertPath, \"utf-8\")\n const x509 = new crypto.X509Certificate(pem)\n const validToMs = Date.parse(x509.validTo)\n if (!Number.isFinite(validToMs)) return true\n // CA rotates only when expired.\n return Date.now() >= validToMs\n } catch {\n return true\n }\n }\n\n if (shouldRotateCa() || !fs.existsSync(caKeyPath)) {\n const { caKeyPem, caCertPem } = generateCaCertificate()\n writePemFile(caKeyPath, caKeyPem, 0o600)\n writePemFile(caCertPath, caCertPem, 0o644)\n args.logger.info({ caCertPath }, \"Generated self-signed CodeNomad CA certificate\")\n }\n\n if (shouldRotateLeaf() || !fs.existsSync(keyPath)) {\n const caKeyPem = fs.readFileSync(caKeyPath, \"utf-8\")\n const caCertPem = fs.readFileSync(caCertPath, \"utf-8\")\n\n const { keyPem, certPem } = generateServerCertificate({\n host: args.host,\n tlsSANs: args.tlsSANs,\n caKeyPem,\n caCertPem,\n })\n\n writePemFile(keyPath, keyPem, 0o600)\n writePemFile(certPath, certPem, 0o644)\n args.logger.info({ certPath }, \"Generated CodeNomad HTTPS certificate\")\n }\n\n const key = fs.readFileSync(keyPath, \"utf-8\")\n const cert = fs.readFileSync(certPath, \"utf-8\")\n const ca = fs.readFileSync(caCertPath, \"utf-8\")\n\n // Present the CA as part of the chain.\n const chainedCert = `${cert.trim()}\\n${ca.trim()}\\n`\n\n return {\n httpsOptions: {\n key,\n cert: chainedCert,\n },\n caCertPath,\n mode: \"generated\",\n }\n}\n\nfunction writePemFile(filePath: string, content: string, mode: number) {\n fs.writeFileSync(filePath, content, { encoding: \"utf-8\", mode })\n try {\n fs.chmodSync(filePath, mode)\n } catch {\n // best effort on platforms that ignore chmod\n }\n}\n\nfunction generateCaCertificate(): { caKeyPem: string; caCertPem: string } {\n const forge = loadForge()\n\n const keys = forge.pki.rsa.generateKeyPair(2048)\n const cert = forge.pki.createCertificate()\n cert.publicKey = keys.publicKey\n cert.serialNumber = crypto.randomBytes(16).toString(\"hex\")\n\n const now = new Date()\n const notBefore = new Date(now.getTime() - 60_000)\n const notAfter = new Date(now.getTime() + CA_VALIDITY_DAYS * 24 * 60 * 60 * 1000)\n cert.validity.notBefore = notBefore\n cert.validity.notAfter = notAfter\n\n const attrs = [{ name: \"commonName\", value: \"CodeNomad Local CA\" }]\n cert.setSubject(attrs)\n cert.setIssuer(attrs)\n\n cert.setExtensions([\n { name: \"basicConstraints\", cA: true },\n { name: \"keyUsage\", keyCertSign: true, cRLSign: true, digitalSignature: true },\n { name: \"subjectKeyIdentifier\" },\n ])\n\n cert.sign(keys.privateKey, forge.md.sha256.create())\n\n return {\n caKeyPem: forge.pki.privateKeyToPem(keys.privateKey),\n caCertPem: forge.pki.certificateToPem(cert),\n }\n}\n\nfunction generateServerCertificate(args: {\n host: string\n tlsSANs?: string\n caKeyPem: string\n caCertPem: string\n}): { keyPem: string; certPem: string } {\n const forge = loadForge()\n\n const caKey = forge.pki.privateKeyFromPem(args.caKeyPem)\n const caCert = forge.pki.certificateFromPem(args.caCertPem)\n\n const keys = forge.pki.rsa.generateKeyPair(2048)\n const cert = forge.pki.createCertificate()\n cert.publicKey = keys.publicKey\n cert.serialNumber = crypto.randomBytes(16).toString(\"hex\")\n\n const now = new Date()\n const notBefore = new Date(now.getTime() - 60_000)\n const notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * 24 * 60 * 60 * 1000)\n cert.validity.notBefore = notBefore\n cert.validity.notAfter = notAfter\n\n const commonName = pickCommonName(args.host)\n cert.setSubject([{ name: \"commonName\", value: commonName }])\n cert.setIssuer(caCert.subject.attributes)\n\n const san = buildSubjectAltNames(args.host, args.tlsSANs)\n\n cert.setExtensions([\n { name: \"basicConstraints\", cA: false },\n { name: \"keyUsage\", digitalSignature: true, keyEncipherment: true },\n { name: \"extKeyUsage\", serverAuth: true },\n { name: \"subjectAltName\", altNames: san },\n { name: \"subjectKeyIdentifier\" },\n ])\n\n cert.sign(caKey, forge.md.sha256.create())\n\n return {\n keyPem: forge.pki.privateKeyToPem(keys.privateKey),\n certPem: forge.pki.certificateToPem(cert),\n }\n}\n\nfunction pickCommonName(host: string): string {\n if (!host || host === \"0.0.0.0\") {\n return \"localhost\"\n }\n if (host === \"127.0.0.1\") {\n return \"localhost\"\n }\n return host\n}\n\nfunction buildSubjectAltNames(host: string, tlsSANs?: string): Array<{ type: number; value?: string; ip?: string }> {\n const dns = new Set<string>()\n const ips = new Set<string>()\n\n dns.add(\"localhost\")\n ips.add(\"127.0.0.1\")\n\n if (host && host !== \"0.0.0.0\") {\n if (isIPv4(host)) {\n ips.add(host)\n } else {\n dns.add(host)\n }\n }\n\n for (const token of splitList(tlsSANs)) {\n if (isIPv4(token)) {\n ips.add(token)\n } else if (token) {\n dns.add(token)\n }\n }\n\n const altNames: Array<{ type: number; value?: string; ip?: string }> = []\n\n // 2 = DNS, 7 = IP\n for (const name of Array.from(dns)) {\n altNames.push({ type: 2, value: name })\n }\n for (const ip of Array.from(ips)) {\n altNames.push({ type: 7, ip })\n }\n\n return altNames\n}\n\nfunction splitList(input: string | undefined): string[] {\n if (!input) return []\n return input\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean)\n}\n\nfunction isIPv4(value: string): boolean {\n const parts = value.split(\".\")\n if (parts.length !== 4) return false\n return parts.every((part) => {\n if (!/^[0-9]+$/.test(part)) return false\n const num = Number(part)\n return Number.isInteger(num) && num >= 0 && num <= 255\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tls.js","sourceRoot":"","sources":["../../src/server/tls.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"crypto\"\nimport fs from \"fs\"\nimport path from \"path\"\nimport { createRequire } from \"module\"\nimport { isIP } from \"node:net\"\nimport type { Logger } from \"../logger\"\nimport { isWildcardHost, normalizeNetworkHost, stripIPv6Zone } from \"./network-host\"\n\nconst require = createRequire(import.meta.url)\n\ntype Forge = typeof import(\"node-forge\")\n\nfunction loadForge(): Forge {\n // node-forge is CJS in many installs; require keeps this compatible with our ESM output.\n return require(\"node-forge\") as Forge\n}\n\nexport interface ResolvedHttpsOptions {\n httpsOptions: { key: string | Buffer; cert: string | Buffer; ca?: string | Buffer }\n /** Path to CA certificate suitable for NODE_EXTRA_CA_CERTS. */\n caCertPath?: string\n mode: \"provided\" | \"generated\"\n}\n\nexport interface ResolveHttpsOptionsArgs {\n enabled: boolean\n configDir: string\n host: string\n tlsKeyPath?: string\n tlsCertPath?: string\n tlsCaPath?: string\n tlsSANs?: string\n logger: Logger\n}\n\nconst LEAF_VALIDITY_DAYS = 30\nconst ROTATE_IF_EXPIRES_WITHIN_DAYS = 3\n\nconst CA_VALIDITY_DAYS = 365\n\nexport function resolveHttpsOptions(args: ResolveHttpsOptionsArgs): ResolvedHttpsOptions | null {\n if (!args.enabled) {\n return null\n }\n\n const hasProvided = Boolean(args.tlsKeyPath && args.tlsCertPath)\n if (hasProvided) {\n const key = fs.readFileSync(args.tlsKeyPath!, \"utf-8\")\n const cert = fs.readFileSync(args.tlsCertPath!, \"utf-8\")\n const ca = args.tlsCaPath ? fs.readFileSync(args.tlsCaPath, \"utf-8\") : undefined\n return {\n httpsOptions: { key, cert, ca },\n caCertPath: args.tlsCaPath,\n mode: \"provided\",\n }\n }\n\n return ensureGeneratedTls(args)\n}\n\nfunction ensureGeneratedTls(args: ResolveHttpsOptionsArgs): ResolvedHttpsOptions {\n const tlsDir = path.join(args.configDir, \"tls\")\n const caKeyPath = path.join(tlsDir, \"ca-key.pem\")\n const caCertPath = path.join(tlsDir, \"ca-cert.pem\")\n const keyPath = path.join(tlsDir, \"server-key.pem\")\n const certPath = path.join(tlsDir, \"server-cert.pem\")\n\n fs.mkdirSync(tlsDir, { recursive: true })\n\n const shouldRotateLeaf = () => {\n try {\n if (!fs.existsSync(certPath)) return true\n const pem = fs.readFileSync(certPath, \"utf-8\")\n const certificate = new crypto.X509Certificate(pem)\n const validToMs = Date.parse(certificate.validTo)\n if (!Number.isFinite(validToMs)) return true\n const rotateAt = validToMs - ROTATE_IF_EXPIRES_WITHIN_DAYS * 24 * 60 * 60 * 1000\n return Date.now() >= rotateAt || !certificateCoversRequiredSans(certificate, args.host, args.tlsSANs)\n } catch {\n return true\n }\n }\n\n const shouldRotateCa = () => {\n try {\n if (!fs.existsSync(caCertPath)) return true\n const pem = fs.readFileSync(caCertPath, \"utf-8\")\n const x509 = new crypto.X509Certificate(pem)\n const validToMs = Date.parse(x509.validTo)\n if (!Number.isFinite(validToMs)) return true\n // CA rotates only when expired.\n return Date.now() >= validToMs\n } catch {\n return true\n }\n }\n\n if (shouldRotateCa() || !fs.existsSync(caKeyPath)) {\n const { caKeyPem, caCertPem } = generateCaCertificate()\n writePemFile(caKeyPath, caKeyPem, 0o600)\n writePemFile(caCertPath, caCertPem, 0o644)\n args.logger.info({ caCertPath }, \"Generated self-signed CodeNomad CA certificate\")\n }\n\n if (shouldRotateLeaf() || !fs.existsSync(keyPath)) {\n const caKeyPem = fs.readFileSync(caKeyPath, \"utf-8\")\n const caCertPem = fs.readFileSync(caCertPath, \"utf-8\")\n\n const { keyPem, certPem } = generateServerCertificate({\n host: args.host,\n tlsSANs: args.tlsSANs,\n caKeyPem,\n caCertPem,\n })\n\n writePemFile(keyPath, keyPem, 0o600)\n writePemFile(certPath, certPem, 0o644)\n args.logger.info({ certPath }, \"Generated CodeNomad HTTPS certificate\")\n }\n\n const key = fs.readFileSync(keyPath, \"utf-8\")\n const cert = fs.readFileSync(certPath, \"utf-8\")\n const ca = fs.readFileSync(caCertPath, \"utf-8\")\n\n // Present the CA as part of the chain.\n const chainedCert = `${cert.trim()}\\n${ca.trim()}\\n`\n\n return {\n httpsOptions: {\n key,\n cert: chainedCert,\n },\n caCertPath,\n mode: \"generated\",\n }\n}\n\nfunction writePemFile(filePath: string, content: string, mode: number) {\n fs.writeFileSync(filePath, content, { encoding: \"utf-8\", mode })\n try {\n fs.chmodSync(filePath, mode)\n } catch {\n // best effort on platforms that ignore chmod\n }\n}\n\nfunction generateCaCertificate(): { caKeyPem: string; caCertPem: string } {\n const forge = loadForge()\n\n const keys = forge.pki.rsa.generateKeyPair(2048)\n const cert = forge.pki.createCertificate()\n cert.publicKey = keys.publicKey\n cert.serialNumber = crypto.randomBytes(16).toString(\"hex\")\n\n const now = new Date()\n const notBefore = new Date(now.getTime() - 60_000)\n const notAfter = new Date(now.getTime() + CA_VALIDITY_DAYS * 24 * 60 * 60 * 1000)\n cert.validity.notBefore = notBefore\n cert.validity.notAfter = notAfter\n\n const attrs = [{ name: \"commonName\", value: \"CodeNomad Local CA\" }]\n cert.setSubject(attrs)\n cert.setIssuer(attrs)\n\n cert.setExtensions([\n { name: \"basicConstraints\", cA: true },\n { name: \"keyUsage\", keyCertSign: true, cRLSign: true, digitalSignature: true },\n { name: \"subjectKeyIdentifier\" },\n ])\n\n cert.sign(keys.privateKey, forge.md.sha256.create())\n\n return {\n caKeyPem: forge.pki.privateKeyToPem(keys.privateKey),\n caCertPem: forge.pki.certificateToPem(cert),\n }\n}\n\nfunction generateServerCertificate(args: {\n host: string\n tlsSANs?: string\n caKeyPem: string\n caCertPem: string\n}): { keyPem: string; certPem: string } {\n const forge = loadForge()\n\n const caKey = forge.pki.privateKeyFromPem(args.caKeyPem)\n const caCert = forge.pki.certificateFromPem(args.caCertPem)\n\n const keys = forge.pki.rsa.generateKeyPair(2048)\n const cert = forge.pki.createCertificate()\n cert.publicKey = keys.publicKey\n cert.serialNumber = crypto.randomBytes(16).toString(\"hex\")\n\n const now = new Date()\n const notBefore = new Date(now.getTime() - 60_000)\n const notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * 24 * 60 * 60 * 1000)\n cert.validity.notBefore = notBefore\n cert.validity.notAfter = notAfter\n\n const commonName = pickCommonName(args.host)\n cert.setSubject([{ name: \"commonName\", value: commonName }])\n cert.setIssuer(caCert.subject.attributes)\n\n const san = buildSubjectAltNames(args.host, args.tlsSANs)\n\n cert.setExtensions([\n { name: \"basicConstraints\", cA: false },\n { name: \"keyUsage\", digitalSignature: true, keyEncipherment: true },\n { name: \"extKeyUsage\", serverAuth: true },\n { name: \"subjectAltName\", altNames: san },\n { name: \"subjectKeyIdentifier\" },\n ])\n\n cert.sign(caKey, forge.md.sha256.create())\n\n return {\n keyPem: forge.pki.privateKeyToPem(keys.privateKey),\n certPem: forge.pki.certificateToPem(cert),\n }\n}\n\nfunction pickCommonName(host: string): string {\n const normalizedHost = normalizeCertificateHost(host)\n if (!normalizedHost || isWildcardHost(normalizedHost)) {\n return \"localhost\"\n }\n if (normalizedHost === \"127.0.0.1\") {\n return \"localhost\"\n }\n return normalizedHost\n}\n\nfunction buildSubjectAltNames(host: string, tlsSANs?: string): Array<{ type: number; value?: string; ip?: string }> {\n const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs)\n const altNames: Array<{ type: number; value?: string; ip?: string }> = []\n\n // 2 = DNS, 7 = IP\n for (const name of dns) {\n altNames.push({ type: 2, value: name })\n }\n for (const ip of ips) {\n altNames.push({ type: 7, ip })\n }\n\n return altNames\n}\n\nfunction resolveRequiredSubjectAltNames(host: string, tlsSANs?: string): { dns: Set<string>; ips: Set<string> } {\n const dns = new Set<string>()\n const ips = new Set<string>()\n\n dns.add(\"localhost\")\n ips.add(\"127.0.0.1\")\n\n const normalizedHost = normalizeCertificateHost(host)\n if (isWildcardHost(normalizedHost) && isIP(normalizedHost) === 6) {\n ips.add(\"::1\")\n } else if (normalizedHost) {\n if (isIP(normalizedHost)) {\n ips.add(normalizedHost)\n } else {\n dns.add(normalizedHost)\n }\n }\n\n for (const token of splitList(tlsSANs)) {\n const normalizedToken = normalizeCertificateHost(token)\n if (isIP(normalizedToken)) {\n ips.add(normalizedToken)\n } else if (normalizedToken) {\n dns.add(normalizedToken)\n }\n }\n\n return { dns, ips }\n}\n\nfunction normalizeCertificateHost(host: string): string {\n return normalizeNetworkHost(stripIPv6Zone(host))\n}\n\nfunction certificateCoversRequiredSans(certificate: crypto.X509Certificate, host: string, tlsSANs?: string): boolean {\n const { dns, ips } = resolveRequiredSubjectAltNames(host, tlsSANs)\n const existingDns = new Set(\n (certificate.subjectAltName ?? \"\")\n .split(/,\\s*/)\n .filter((entry) => entry.startsWith(\"DNS:\"))\n .map((entry) => entry.slice(4).toLowerCase()),\n )\n\n for (const name of dns) {\n if (!existingDns.has(name.toLowerCase())) return false\n }\n for (const ip of ips) {\n if (!certificate.checkIP(ip)) return false\n }\n return true\n}\n\nfunction splitList(input: string | undefined): string[] {\n if (!input) return []\n return input\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean)\n}\n"]}
|