drawio-mcp-server 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -3
- package/build/assets/downloader.js +16 -17
- package/build/config.js +253 -5
- package/build/config.test.js +449 -1
- package/build/emitter_bus.js +3 -0
- package/build/emitter_bus.test.js +7 -0
- package/build/index.capabilities.test.js +45 -0
- package/build/index.js +416 -82
- package/build/install-desktop-plugin.js +56 -0
- package/build/install-desktop-plugin.test.js +66 -0
- package/build/multi-transport.test.js +117 -0
- package/build/plugin/mcp-plugin.js +2180 -1105
- package/build/prefetch-assets.js +6 -5
- package/build/real-environment/document-targeting.test.js +124 -0
- package/build/real-environment/export-diagram.test.js +1 -1
- package/build/real-environment/harness.js +118 -31
- package/build/real-environment/import-mermaid.test.js +81 -0
- package/build/real-environment/pages-and-concurrency.test.js +448 -0
- package/build/real-environment/shapes.test.js +58 -0
- package/build/real-environment/tools.js +51 -6
- package/build/register-tool.js +31 -0
- package/build/request_queue.js +29 -0
- package/build/request_queue.test.js +98 -0
- package/build/stdio-transport-purity.test.js +88 -0
- package/build/strip-schema.js +61 -0
- package/build/tls/expiry.js +14 -0
- package/build/tls/expiry.test.js +54 -0
- package/build/tls/generate.js +123 -0
- package/build/tls/generate.test.js +115 -0
- package/build/tls/index.js +80 -0
- package/build/tls/index.test.js +141 -0
- package/build/tls/install-hint.js +45 -0
- package/build/tls/install-hint.test.js +32 -0
- package/build/tls/load.js +18 -0
- package/build/tls/load.test.js +40 -0
- package/build/tls/paths.js +30 -0
- package/build/tls/paths.test.js +53 -0
- package/build/tls/san.js +27 -0
- package/build/tls/san.test.js +72 -0
- package/build/tool-registry.test.js +823 -0
- package/build/tool.js +74 -15
- package/build/tool.test.js +250 -7
- package/build/tools/add-cell-of-shape.js +4 -2
- package/build/tools/add-edge.js +3 -1
- package/build/tools/add-rectangle.js +4 -2
- package/build/tools/copy-page.js +13 -0
- package/build/tools/create-layer.js +4 -2
- package/build/tools/create-page.js +8 -0
- package/build/tools/delete-cell-by-id.js +4 -2
- package/build/tools/edit-cell.js +3 -1
- package/build/tools/edit-edge.js +3 -1
- package/build/tools/export-diagram.js +7 -3
- package/build/tools/get-active-layer.js +4 -1
- package/build/tools/get-current-page.js +5 -0
- package/build/tools/get-selected-cell.js +4 -1
- package/build/tools/import-diagram.js +12 -2
- package/build/tools/import-mermaid.js +31 -0
- package/build/tools/index.js +14 -0
- package/build/tools/list-documents.js +17 -0
- package/build/tools/list-layers.js +4 -1
- package/build/tools/list-paged-model.js +4 -3
- package/build/tools/list-pages.js +5 -0
- package/build/tools/move-cell-to-layer.js +3 -1
- package/build/tools/rename-page.js +10 -0
- package/build/tools/set-active-layer.js +4 -2
- package/build/tools/set-cell-data.js +3 -1
- package/build/tools/set-cell-parent.js +3 -1
- package/build/tools/set-cell-shape.js +3 -1
- package/build/tools/shared.js +35 -0
- package/build/tools/shared.test.js +29 -0
- package/package.json +12 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { mkdtempSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveTlsMaterial } from "./index.js";
|
|
6
|
+
import { tlsFilePaths } from "./paths.js";
|
|
7
|
+
function tmpDir(prefix = "tls-resolve-") {
|
|
8
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
9
|
+
}
|
|
10
|
+
function writePem(dir, name, body) {
|
|
11
|
+
const p = join(dir, name);
|
|
12
|
+
writeFileSync(p, body);
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
const VALID_CERT = `-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n`;
|
|
16
|
+
const VALID_KEY = `-----BEGIN PRIVATE KEY-----\nMIIE\n-----END PRIVATE KEY-----\n`;
|
|
17
|
+
describe("resolveTlsMaterial — none", () => {
|
|
18
|
+
it("returns null when TLS disabled", () => {
|
|
19
|
+
expect(resolveTlsMaterial({
|
|
20
|
+
config: { tlsEnabled: false },
|
|
21
|
+
log: () => { },
|
|
22
|
+
})).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("resolveTlsMaterial — manual", () => {
|
|
26
|
+
it("loads cert + key from explicit paths", () => {
|
|
27
|
+
const dir = tmpDir();
|
|
28
|
+
const certPath = writePem(dir, "c.pem", VALID_CERT);
|
|
29
|
+
const keyPath = writePem(dir, "k.pem", VALID_KEY);
|
|
30
|
+
const m = resolveTlsMaterial({
|
|
31
|
+
config: { tlsEnabled: true, tlsCert: certPath, tlsKey: keyPath },
|
|
32
|
+
log: () => { },
|
|
33
|
+
});
|
|
34
|
+
expect(m).toEqual({ cert: VALID_CERT, key: VALID_KEY, caPath: undefined });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("resolveTlsMaterial — auto", () => {
|
|
38
|
+
it("first run generates CA + leaf and returns material with caPath", () => {
|
|
39
|
+
const dir = tmpDir();
|
|
40
|
+
const messages = [];
|
|
41
|
+
const m = resolveTlsMaterial({
|
|
42
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
43
|
+
log: (msg) => messages.push(msg),
|
|
44
|
+
});
|
|
45
|
+
expect(m?.cert).toContain("BEGIN CERTIFICATE");
|
|
46
|
+
expect(m?.key).toContain("PRIVATE KEY");
|
|
47
|
+
expect(m?.caPath).toBe(tlsFilePaths(dir).caCert);
|
|
48
|
+
expect(existsSync(tlsFilePaths(dir).caCert)).toBe(true);
|
|
49
|
+
expect(existsSync(tlsFilePaths(dir).caKey)).toBe(true);
|
|
50
|
+
expect(existsSync(tlsFilePaths(dir).serverCert)).toBe(true);
|
|
51
|
+
expect(existsSync(tlsFilePaths(dir).serverKey)).toBe(true);
|
|
52
|
+
expect(messages.some((m) => m.includes("Install the local CA"))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it("second run reuses material when SAN unchanged and material valid", () => {
|
|
55
|
+
const dir = tmpDir();
|
|
56
|
+
const m1 = resolveTlsMaterial({
|
|
57
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
58
|
+
log: () => { },
|
|
59
|
+
});
|
|
60
|
+
const caBefore = readFileSync(tlsFilePaths(dir).caCert, "utf8");
|
|
61
|
+
const leafBefore = readFileSync(tlsFilePaths(dir).serverCert, "utf8");
|
|
62
|
+
const m2 = resolveTlsMaterial({
|
|
63
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
64
|
+
log: () => { },
|
|
65
|
+
});
|
|
66
|
+
expect(m2?.cert).toBe(m1?.cert);
|
|
67
|
+
expect(readFileSync(tlsFilePaths(dir).caCert, "utf8")).toBe(caBefore);
|
|
68
|
+
expect(readFileSync(tlsFilePaths(dir).serverCert, "utf8")).toBe(leafBefore);
|
|
69
|
+
});
|
|
70
|
+
it("regenerates leaf only (CA preserved) when SAN changes", () => {
|
|
71
|
+
const dir = tmpDir();
|
|
72
|
+
resolveTlsMaterial({
|
|
73
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
74
|
+
log: () => { },
|
|
75
|
+
});
|
|
76
|
+
const caBefore = readFileSync(tlsFilePaths(dir).caCert, "utf8");
|
|
77
|
+
const leafBefore = readFileSync(tlsFilePaths(dir).serverCert, "utf8");
|
|
78
|
+
const m2 = resolveTlsMaterial({
|
|
79
|
+
config: {
|
|
80
|
+
tlsEnabled: true,
|
|
81
|
+
tlsAuto: true,
|
|
82
|
+
tlsDir: dir,
|
|
83
|
+
host: "192.168.1.10",
|
|
84
|
+
},
|
|
85
|
+
log: () => { },
|
|
86
|
+
});
|
|
87
|
+
expect(readFileSync(tlsFilePaths(dir).caCert, "utf8")).toBe(caBefore);
|
|
88
|
+
expect(readFileSync(tlsFilePaths(dir).serverCert, "utf8")).not.toBe(leafBefore);
|
|
89
|
+
expect(m2?.cert).toContain("BEGIN CERTIFICATE");
|
|
90
|
+
});
|
|
91
|
+
it("throws when both manual and auto specified", () => {
|
|
92
|
+
expect(() => resolveTlsMaterial({
|
|
93
|
+
config: {
|
|
94
|
+
tlsEnabled: true,
|
|
95
|
+
tlsAuto: true,
|
|
96
|
+
tlsCert: "/c",
|
|
97
|
+
tlsKey: "/k",
|
|
98
|
+
},
|
|
99
|
+
log: () => { },
|
|
100
|
+
})).toThrow(/cannot combine.*--tls-auto.*--tls-cert/i);
|
|
101
|
+
});
|
|
102
|
+
it("throws when tlsEnabled but no mode chosen", () => {
|
|
103
|
+
expect(() => resolveTlsMaterial({ config: { tlsEnabled: true }, log: () => { } })).toThrow(/--tls requires either --tls-auto or --tls-cert\/--tls-key/i);
|
|
104
|
+
});
|
|
105
|
+
it("renews leaf only (CA preserved) when leaf is within 30-day window", () => {
|
|
106
|
+
const dir = tmpDir();
|
|
107
|
+
resolveTlsMaterial({
|
|
108
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
109
|
+
log: () => { },
|
|
110
|
+
now: new Date("2024-01-01T00:00:00Z"),
|
|
111
|
+
});
|
|
112
|
+
const caBefore = readFileSync(tlsFilePaths(dir).caCert, "utf8");
|
|
113
|
+
const leafBefore = readFileSync(tlsFilePaths(dir).serverCert, "utf8");
|
|
114
|
+
const messages = [];
|
|
115
|
+
resolveTlsMaterial({
|
|
116
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
117
|
+
log: (msg) => messages.push(msg),
|
|
118
|
+
now: new Date("2024-12-10T00:00:00Z"),
|
|
119
|
+
});
|
|
120
|
+
expect(readFileSync(tlsFilePaths(dir).caCert, "utf8")).toBe(caBefore);
|
|
121
|
+
expect(readFileSync(tlsFilePaths(dir).serverCert, "utf8")).not.toBe(leafBefore);
|
|
122
|
+
expect(messages.some((m) => m.includes("Renewed TLS leaf"))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it("regenerates CA + leaf when CA is within 30-day window", () => {
|
|
125
|
+
const dir = tmpDir();
|
|
126
|
+
resolveTlsMaterial({
|
|
127
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
128
|
+
log: () => { },
|
|
129
|
+
now: new Date("2024-01-01T00:00:00Z"),
|
|
130
|
+
});
|
|
131
|
+
const caBefore = readFileSync(tlsFilePaths(dir).caCert, "utf8");
|
|
132
|
+
const messages = [];
|
|
133
|
+
resolveTlsMaterial({
|
|
134
|
+
config: { tlsEnabled: true, tlsAuto: true, tlsDir: dir, host: undefined },
|
|
135
|
+
log: (msg) => messages.push(msg),
|
|
136
|
+
now: new Date("2033-12-10T00:00:00Z"),
|
|
137
|
+
});
|
|
138
|
+
expect(readFileSync(tlsFilePaths(dir).caCert, "utf8")).not.toBe(caBefore);
|
|
139
|
+
expect(messages.some((m) => m.includes("Install the local CA"))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function caInstallHint(args) {
|
|
2
|
+
const { platform, caPath } = args;
|
|
3
|
+
if (platform === "darwin") {
|
|
4
|
+
return [
|
|
5
|
+
`Install the local CA into the macOS System keychain so browsers trust it:`,
|
|
6
|
+
``,
|
|
7
|
+
` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${caPath}`,
|
|
8
|
+
``,
|
|
9
|
+
`Then quit and restart the browser.`,
|
|
10
|
+
].join("\n");
|
|
11
|
+
}
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
return [
|
|
14
|
+
`Install the local CA into the Windows Trusted Root store (run as Administrator):`,
|
|
15
|
+
``,
|
|
16
|
+
` certutil -addstore -f ROOT "${caPath}"`,
|
|
17
|
+
``,
|
|
18
|
+
`Then quit and restart the browser.`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
if (platform === "linux") {
|
|
22
|
+
return [
|
|
23
|
+
`Install the local CA so the system and browsers trust it.`,
|
|
24
|
+
``,
|
|
25
|
+
`Debian/Ubuntu:`,
|
|
26
|
+
` sudo cp ${caPath} /usr/local/share/ca-certificates/drawio-mcp-ca.crt`,
|
|
27
|
+
` sudo update-ca-certificates`,
|
|
28
|
+
``,
|
|
29
|
+
`Fedora/RHEL:`,
|
|
30
|
+
` sudo cp ${caPath} /etc/pki/ca-trust/source/anchors/drawio-mcp-ca.crt`,
|
|
31
|
+
` sudo update-ca-trust extract`,
|
|
32
|
+
``,
|
|
33
|
+
`Arch/Manjaro:`,
|
|
34
|
+
` sudo trust anchor --store ${caPath}`,
|
|
35
|
+
``,
|
|
36
|
+
`Firefox uses its own NSS store; import via Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import.`,
|
|
37
|
+
``,
|
|
38
|
+
`Then quit and restart the browser.`,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
`Install the local CA at ${caPath} into your OS / browser trust store.`,
|
|
43
|
+
`Without this, browsers will reject the self-signed certificate.`,
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { caInstallHint } from "./install-hint.js";
|
|
3
|
+
describe("caInstallHint", () => {
|
|
4
|
+
it("Linux hint mentions update-ca-certificates and Firefox NSS", () => {
|
|
5
|
+
const hint = caInstallHint({ platform: "linux", caPath: "/x/ca.crt" });
|
|
6
|
+
expect(hint).toContain("/x/ca.crt");
|
|
7
|
+
expect(hint).toMatch(/update-ca-certificates|trust anchor/i);
|
|
8
|
+
expect(hint).toMatch(/firefox/i);
|
|
9
|
+
});
|
|
10
|
+
it("macOS hint mentions security add-trusted-cert", () => {
|
|
11
|
+
const hint = caInstallHint({ platform: "darwin", caPath: "/x/ca.crt" });
|
|
12
|
+
expect(hint).toContain("security add-trusted-cert");
|
|
13
|
+
expect(hint).toContain("/x/ca.crt");
|
|
14
|
+
});
|
|
15
|
+
it("Windows hint mentions certutil -addstore", () => {
|
|
16
|
+
const hint = caInstallHint({
|
|
17
|
+
platform: "win32",
|
|
18
|
+
caPath: "C:\\x\\ca.crt",
|
|
19
|
+
});
|
|
20
|
+
expect(hint).toContain("certutil");
|
|
21
|
+
expect(hint).toContain("ROOT");
|
|
22
|
+
expect(hint).toContain("C:\\x\\ca.crt");
|
|
23
|
+
});
|
|
24
|
+
it("falls back to generic hint on unknown platform", () => {
|
|
25
|
+
const hint = caInstallHint({
|
|
26
|
+
platform: "freebsd",
|
|
27
|
+
caPath: "/x/ca.crt",
|
|
28
|
+
});
|
|
29
|
+
expect(hint).toContain("/x/ca.crt");
|
|
30
|
+
expect(hint).toMatch(/trust store/i);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
export function loadManualMaterial(args) {
|
|
3
|
+
if (!existsSync(args.certPath)) {
|
|
4
|
+
throw new Error(`TLS cert file not found: ${args.certPath}`);
|
|
5
|
+
}
|
|
6
|
+
if (!existsSync(args.keyPath)) {
|
|
7
|
+
throw new Error(`TLS key file not found: ${args.keyPath}`);
|
|
8
|
+
}
|
|
9
|
+
const cert = readFileSync(args.certPath, "utf8");
|
|
10
|
+
const key = readFileSync(args.keyPath, "utf8");
|
|
11
|
+
if (!cert.includes("BEGIN CERTIFICATE")) {
|
|
12
|
+
throw new Error(`TLS cert at ${args.certPath} is not PEM-encoded (missing BEGIN CERTIFICATE)`);
|
|
13
|
+
}
|
|
14
|
+
if (!key.includes("PRIVATE KEY")) {
|
|
15
|
+
throw new Error(`TLS key at ${args.keyPath} is not PEM-encoded (missing PRIVATE KEY)`);
|
|
16
|
+
}
|
|
17
|
+
return { cert, key };
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { loadManualMaterial } from "./load.js";
|
|
6
|
+
function tmpFile(content) {
|
|
7
|
+
const dir = mkdtempSync(join(tmpdir(), "tls-load-"));
|
|
8
|
+
const path = join(dir, "file.pem");
|
|
9
|
+
writeFileSync(path, content);
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
const VALID_CERT = `-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n`;
|
|
13
|
+
const VALID_KEY = `-----BEGIN PRIVATE KEY-----\nMIIE\n-----END PRIVATE KEY-----\n`;
|
|
14
|
+
describe("loadManualMaterial", () => {
|
|
15
|
+
it("reads cert and key from disk", () => {
|
|
16
|
+
const certPath = tmpFile(VALID_CERT);
|
|
17
|
+
const keyPath = tmpFile(VALID_KEY);
|
|
18
|
+
const m = loadManualMaterial({ certPath, keyPath });
|
|
19
|
+
expect(m.cert).toBe(VALID_CERT);
|
|
20
|
+
expect(m.key).toBe(VALID_KEY);
|
|
21
|
+
});
|
|
22
|
+
it("throws when cert file missing", () => {
|
|
23
|
+
const keyPath = tmpFile(VALID_KEY);
|
|
24
|
+
expect(() => loadManualMaterial({ certPath: "/nope/cert.pem", keyPath })).toThrow(/cert.*not found/i);
|
|
25
|
+
});
|
|
26
|
+
it("throws when key file missing", () => {
|
|
27
|
+
const certPath = tmpFile(VALID_CERT);
|
|
28
|
+
expect(() => loadManualMaterial({ certPath, keyPath: "/nope/key.pem" })).toThrow(/key.*not found/i);
|
|
29
|
+
});
|
|
30
|
+
it("throws when cert lacks BEGIN CERTIFICATE marker", () => {
|
|
31
|
+
const certPath = tmpFile("not pem");
|
|
32
|
+
const keyPath = tmpFile(VALID_KEY);
|
|
33
|
+
expect(() => loadManualMaterial({ certPath, keyPath })).toThrow(/BEGIN CERTIFICATE/);
|
|
34
|
+
});
|
|
35
|
+
it("throws when key lacks PRIVATE KEY marker", () => {
|
|
36
|
+
const certPath = tmpFile(VALID_CERT);
|
|
37
|
+
const keyPath = tmpFile("not pem");
|
|
38
|
+
expect(() => loadManualMaterial({ certPath, keyPath })).toThrow(/PRIVATE KEY/);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join as joinPosix } from "node:path/posix";
|
|
2
|
+
import { join as joinWin32 } from "node:path/win32";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const APP_NAME = "drawio-mcp-server";
|
|
5
|
+
export function resolveTlsDir(args) {
|
|
6
|
+
if (args.override && args.override.length > 0)
|
|
7
|
+
return args.override;
|
|
8
|
+
if (args.platform === "darwin") {
|
|
9
|
+
return joinPosix(args.home, "Library", "Application Support", APP_NAME, "tls");
|
|
10
|
+
}
|
|
11
|
+
if (args.platform === "win32") {
|
|
12
|
+
const base = args.env.LOCALAPPDATA && args.env.LOCALAPPDATA.length > 0
|
|
13
|
+
? args.env.LOCALAPPDATA
|
|
14
|
+
: joinWin32(args.home, "AppData", "Local");
|
|
15
|
+
return joinWin32(base, APP_NAME, "tls");
|
|
16
|
+
}
|
|
17
|
+
// Linux + other unix
|
|
18
|
+
const xdg = args.env.XDG_DATA_HOME;
|
|
19
|
+
const base = xdg && xdg.length > 0 ? xdg : joinPosix(args.home, ".local", "share");
|
|
20
|
+
return joinPosix(base, APP_NAME, "tls");
|
|
21
|
+
}
|
|
22
|
+
export function tlsFilePaths(dir) {
|
|
23
|
+
return {
|
|
24
|
+
caCert: join(dir, "ca.crt"),
|
|
25
|
+
caKey: join(dir, "ca.key"),
|
|
26
|
+
serverCert: join(dir, "server.crt"),
|
|
27
|
+
serverKey: join(dir, "server.key"),
|
|
28
|
+
meta: join(dir, "meta.json"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveTlsDir, tlsFilePaths } from "./paths.js";
|
|
4
|
+
describe("resolveTlsDir", () => {
|
|
5
|
+
it("uses explicit override when provided", () => {
|
|
6
|
+
expect(resolveTlsDir({
|
|
7
|
+
override: "/tmp/x",
|
|
8
|
+
platform: "linux",
|
|
9
|
+
env: {},
|
|
10
|
+
home: "/h",
|
|
11
|
+
})).toBe("/tmp/x");
|
|
12
|
+
});
|
|
13
|
+
it("Linux: honours XDG_DATA_HOME", () => {
|
|
14
|
+
expect(resolveTlsDir({
|
|
15
|
+
platform: "linux",
|
|
16
|
+
env: { XDG_DATA_HOME: "/custom/data" },
|
|
17
|
+
home: "/home/u",
|
|
18
|
+
})).toBe("/custom/data/drawio-mcp-server/tls");
|
|
19
|
+
});
|
|
20
|
+
it("Linux: defaults to ~/.local/share when XDG_DATA_HOME is unset", () => {
|
|
21
|
+
expect(resolveTlsDir({ platform: "linux", env: {}, home: "/home/u" })).toBe("/home/u/.local/share/drawio-mcp-server/tls");
|
|
22
|
+
});
|
|
23
|
+
it("macOS: defaults to ~/Library/Application Support", () => {
|
|
24
|
+
expect(resolveTlsDir({ platform: "darwin", env: {}, home: "/Users/u" })).toBe("/Users/u/Library/Application Support/drawio-mcp-server/tls");
|
|
25
|
+
});
|
|
26
|
+
it("Windows: honours LOCALAPPDATA", () => {
|
|
27
|
+
expect(resolveTlsDir({
|
|
28
|
+
platform: "win32",
|
|
29
|
+
env: { LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local" },
|
|
30
|
+
home: "C:\\Users\\u",
|
|
31
|
+
})).toBe("C:\\Users\\u\\AppData\\Local\\drawio-mcp-server\\tls");
|
|
32
|
+
});
|
|
33
|
+
it("Windows: falls back to ~/AppData/Local when LOCALAPPDATA unset", () => {
|
|
34
|
+
expect(resolveTlsDir({ platform: "win32", env: {}, home: "C:\\Users\\u" })).toBe("C:\\Users\\u\\AppData\\Local\\drawio-mcp-server\\tls");
|
|
35
|
+
});
|
|
36
|
+
it("treats empty XDG_DATA_HOME as unset", () => {
|
|
37
|
+
expect(resolveTlsDir({
|
|
38
|
+
platform: "linux",
|
|
39
|
+
env: { XDG_DATA_HOME: "" },
|
|
40
|
+
home: "/h",
|
|
41
|
+
})).toBe("/h/.local/share/drawio-mcp-server/tls");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("tlsFilePaths", () => {
|
|
45
|
+
it("returns expected file names under given dir", () => {
|
|
46
|
+
const p = tlsFilePaths("/base");
|
|
47
|
+
expect(p.caCert).toBe(join("/base", "ca.crt"));
|
|
48
|
+
expect(p.caKey).toBe(join("/base", "ca.key"));
|
|
49
|
+
expect(p.serverCert).toBe(join("/base", "server.crt"));
|
|
50
|
+
expect(p.serverKey).toBe(join("/base", "server.key"));
|
|
51
|
+
expect(p.meta).toBe(join("/base", "meta.json"));
|
|
52
|
+
});
|
|
53
|
+
});
|
package/build/tls/san.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
const LOOPBACK_DEFAULTS = [
|
|
4
|
+
{ type: "dns", value: "localhost" },
|
|
5
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
6
|
+
{ type: "ip", value: "::1" },
|
|
7
|
+
];
|
|
8
|
+
export function buildSanList(host) {
|
|
9
|
+
const entries = [...LOOPBACK_DEFAULTS];
|
|
10
|
+
if (!host || host === "0.0.0.0" || host === "::")
|
|
11
|
+
return entries;
|
|
12
|
+
const family = isIP(host);
|
|
13
|
+
if (family === 0)
|
|
14
|
+
return entries;
|
|
15
|
+
const candidate = { type: "ip", value: host };
|
|
16
|
+
const exists = entries.some((e) => e.type === candidate.type && e.value === candidate.value);
|
|
17
|
+
if (!exists)
|
|
18
|
+
entries.push(candidate);
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
export function sanHash(entries) {
|
|
22
|
+
const canonical = [...entries]
|
|
23
|
+
.map((e) => `${e.type}:${e.value}`)
|
|
24
|
+
.sort()
|
|
25
|
+
.join("|");
|
|
26
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { buildSanList, sanHash } from "./san.js";
|
|
3
|
+
describe("buildSanList", () => {
|
|
4
|
+
it("returns loopback defaults when host is undefined", () => {
|
|
5
|
+
expect(buildSanList(undefined)).toEqual([
|
|
6
|
+
{ type: "dns", value: "localhost" },
|
|
7
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
8
|
+
{ type: "ip", value: "::1" },
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
it("appends explicit IPv4 host without duplicating loopback", () => {
|
|
12
|
+
expect(buildSanList("192.168.1.10")).toEqual([
|
|
13
|
+
{ type: "dns", value: "localhost" },
|
|
14
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
15
|
+
{ type: "ip", value: "::1" },
|
|
16
|
+
{ type: "ip", value: "192.168.1.10" },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
it("appends explicit IPv6 host without duplicating loopback", () => {
|
|
20
|
+
expect(buildSanList("fe80::1")).toEqual([
|
|
21
|
+
{ type: "dns", value: "localhost" },
|
|
22
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
23
|
+
{ type: "ip", value: "::1" },
|
|
24
|
+
{ type: "ip", value: "fe80::1" },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it("does not duplicate when host equals an existing loopback entry", () => {
|
|
28
|
+
expect(buildSanList("127.0.0.1")).toEqual([
|
|
29
|
+
{ type: "dns", value: "localhost" },
|
|
30
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
31
|
+
{ type: "ip", value: "::1" },
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
it("treats 0.0.0.0 as a wildcard binding, still using loopback SAN set", () => {
|
|
35
|
+
expect(buildSanList("0.0.0.0")).toEqual([
|
|
36
|
+
{ type: "dns", value: "localhost" },
|
|
37
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
38
|
+
{ type: "ip", value: "::1" },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
it("treats :: as an IPv6 wildcard binding, still using loopback SAN set", () => {
|
|
42
|
+
expect(buildSanList("::")).toEqual([
|
|
43
|
+
{ type: "dns", value: "localhost" },
|
|
44
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
45
|
+
{ type: "ip", value: "::1" },
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
it("silently ignores non-IP hosts (hostname strings are not added as SAN entries)", () => {
|
|
49
|
+
expect(buildSanList("example.com")).toEqual([
|
|
50
|
+
{ type: "dns", value: "localhost" },
|
|
51
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
52
|
+
{ type: "ip", value: "::1" },
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("sanHash", () => {
|
|
57
|
+
it("is stable for identical input", () => {
|
|
58
|
+
const a = sanHash(buildSanList("192.168.1.10"));
|
|
59
|
+
const b = sanHash(buildSanList("192.168.1.10"));
|
|
60
|
+
expect(a).toBe(b);
|
|
61
|
+
});
|
|
62
|
+
it("differs when host changes", () => {
|
|
63
|
+
const a = sanHash(buildSanList(undefined));
|
|
64
|
+
const b = sanHash(buildSanList("192.168.1.10"));
|
|
65
|
+
expect(a).not.toBe(b);
|
|
66
|
+
});
|
|
67
|
+
it("is order-insensitive (canonicalised)", () => {
|
|
68
|
+
const original = buildSanList("192.168.1.10");
|
|
69
|
+
const reversed = [...original].reverse();
|
|
70
|
+
expect(sanHash(reversed)).toBe(sanHash(original));
|
|
71
|
+
});
|
|
72
|
+
});
|