@mseep/mcp-swarmpit 0.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.
Files changed (98) hide show
  1. package/CLAUDE.md +128 -0
  2. package/README.md +416 -0
  3. package/dist/client.d.ts +107 -0
  4. package/dist/client.js +297 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/config.d.ts +8 -0
  7. package/dist/config.js +41 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +41 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/sanitize.d.ts +41 -0
  13. package/dist/sanitize.js +165 -0
  14. package/dist/sanitize.js.map +1 -0
  15. package/dist/test/config.test.d.ts +1 -0
  16. package/dist/test/config.test.js +103 -0
  17. package/dist/test/config.test.js.map +1 -0
  18. package/dist/test/file-ref.test.d.ts +1 -0
  19. package/dist/test/file-ref.test.js +163 -0
  20. package/dist/test/file-ref.test.js.map +1 -0
  21. package/dist/test/helpers.test.d.ts +1 -0
  22. package/dist/test/helpers.test.js +133 -0
  23. package/dist/test/helpers.test.js.map +1 -0
  24. package/dist/test/sanitize.test.d.ts +1 -0
  25. package/dist/test/sanitize.test.js +207 -0
  26. package/dist/test/sanitize.test.js.map +1 -0
  27. package/dist/tools/admin.d.ts +3 -0
  28. package/dist/tools/admin.js +64 -0
  29. package/dist/tools/admin.js.map +1 -0
  30. package/dist/tools/configs.d.ts +4 -0
  31. package/dist/tools/configs.js +70 -0
  32. package/dist/tools/configs.js.map +1 -0
  33. package/dist/tools/dashboard.d.ts +3 -0
  34. package/dist/tools/dashboard.js +41 -0
  35. package/dist/tools/dashboard.js.map +1 -0
  36. package/dist/tools/helpers.d.ts +16 -0
  37. package/dist/tools/helpers.js +74 -0
  38. package/dist/tools/helpers.js.map +1 -0
  39. package/dist/tools/networks.d.ts +3 -0
  40. package/dist/tools/networks.js +70 -0
  41. package/dist/tools/networks.js.map +1 -0
  42. package/dist/tools/nodes.d.ts +3 -0
  43. package/dist/tools/nodes.js +59 -0
  44. package/dist/tools/nodes.js.map +1 -0
  45. package/dist/tools/register.d.ts +3 -0
  46. package/dist/tools/register.js +30 -0
  47. package/dist/tools/register.js.map +1 -0
  48. package/dist/tools/secrets.d.ts +4 -0
  49. package/dist/tools/secrets.js +70 -0
  50. package/dist/tools/secrets.js.map +1 -0
  51. package/dist/tools/services.d.ts +4 -0
  52. package/dist/tools/services.js +198 -0
  53. package/dist/tools/services.js.map +1 -0
  54. package/dist/tools/stacks.d.ts +4 -0
  55. package/dist/tools/stacks.js +196 -0
  56. package/dist/tools/stacks.js.map +1 -0
  57. package/dist/tools/tasks.d.ts +3 -0
  58. package/dist/tools/tasks.js +23 -0
  59. package/dist/tools/tasks.js.map +1 -0
  60. package/dist/tools/timeseries.d.ts +3 -0
  61. package/dist/tools/timeseries.js +41 -0
  62. package/dist/tools/timeseries.js.map +1 -0
  63. package/dist/tools/util.d.ts +3 -0
  64. package/dist/tools/util.js +10 -0
  65. package/dist/tools/util.js.map +1 -0
  66. package/dist/tools/volumes.d.ts +3 -0
  67. package/dist/tools/volumes.js +59 -0
  68. package/dist/tools/volumes.js.map +1 -0
  69. package/dist/types.d.ts +119 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +43 -0
  73. package/src/client.ts +391 -0
  74. package/src/config.ts +57 -0
  75. package/src/index.ts +49 -0
  76. package/src/sanitize.ts +218 -0
  77. package/src/test/config.test.ts +118 -0
  78. package/src/test/file-ref.test.ts +191 -0
  79. package/src/test/helpers.test.ts +147 -0
  80. package/src/test/sanitize.test.ts +234 -0
  81. package/src/tools/admin.ts +93 -0
  82. package/src/tools/configs.ts +101 -0
  83. package/src/tools/dashboard.ts +65 -0
  84. package/src/tools/helpers.ts +91 -0
  85. package/src/tools/networks.ts +99 -0
  86. package/src/tools/nodes.ts +88 -0
  87. package/src/tools/register.ts +36 -0
  88. package/src/tools/secrets.ts +101 -0
  89. package/src/tools/services.ts +283 -0
  90. package/src/tools/stacks.ts +282 -0
  91. package/src/tools/tasks.ts +37 -0
  92. package/src/tools/timeseries.ts +65 -0
  93. package/src/tools/util.ts +20 -0
  94. package/src/tools/volumes.ts +88 -0
  95. package/src/types.ts +131 -0
  96. package/swagger.json +1 -0
  97. package/swarmpit-config.example.json +9 -0
  98. package/tsconfig.json +15 -0
@@ -0,0 +1,103 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { loadConfig } from "../config.js";
4
+ describe("loadConfig", () => {
5
+ const saved = { ...process.env };
6
+ beforeEach(() => {
7
+ // Clear all SWARMPIT_ vars
8
+ for (const key of Object.keys(process.env)) {
9
+ if (key.startsWith("SWARMPIT_"))
10
+ delete process.env[key];
11
+ }
12
+ });
13
+ afterEach(() => {
14
+ // Restore original env
15
+ for (const key of Object.keys(process.env)) {
16
+ if (key.startsWith("SWARMPIT_"))
17
+ delete process.env[key];
18
+ }
19
+ Object.assign(process.env, saved);
20
+ });
21
+ it("loads config from env vars", () => {
22
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
23
+ process.env.SWARMPIT_TOKEN = "test-token";
24
+ const config = loadConfig();
25
+ assert.equal(config.url, "https://swarmpit.example.com");
26
+ assert.equal(config.token, "test-token");
27
+ assert.equal(config.redact, "all");
28
+ });
29
+ it("strips trailing slashes from URL", () => {
30
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com///";
31
+ process.env.SWARMPIT_TOKEN = "test-token";
32
+ const config = loadConfig();
33
+ assert.equal(config.url, "https://swarmpit.example.com");
34
+ });
35
+ it("reads SWARMPIT_REDACT mode", () => {
36
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
37
+ process.env.SWARMPIT_TOKEN = "test-token";
38
+ process.env.SWARMPIT_REDACT = "sensitive";
39
+ const config = loadConfig();
40
+ assert.equal(config.redact, "sensitive");
41
+ });
42
+ it("accepts none redact mode", () => {
43
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
44
+ process.env.SWARMPIT_TOKEN = "test-token";
45
+ process.env.SWARMPIT_REDACT = "none";
46
+ const config = loadConfig();
47
+ assert.equal(config.redact, "none");
48
+ });
49
+ it("throws on missing SWARMPIT_URL", () => {
50
+ process.env.SWARMPIT_TOKEN = "test-token";
51
+ assert.throws(() => loadConfig(), /SWARMPIT_URL is not set/);
52
+ });
53
+ it("throws on missing SWARMPIT_TOKEN", () => {
54
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
55
+ assert.throws(() => loadConfig(), /Neither SWARMPIT_TOKEN nor SWARMPIT_TOKEN_FILE is set/);
56
+ });
57
+ it("loads token from SWARMPIT_TOKEN_FILE", async () => {
58
+ const { writeFileSync, unlinkSync, mkdtempSync, rmSync } = await import("node:fs");
59
+ const { tmpdir } = await import("node:os");
60
+ const { join } = await import("node:path");
61
+ const dir = mkdtempSync(join(tmpdir(), "mcp-token-"));
62
+ const path = join(dir, "token");
63
+ writeFileSync(path, "file-loaded-token\n"); // trailing newline should be stripped
64
+ try {
65
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
66
+ process.env.SWARMPIT_TOKEN_FILE = path;
67
+ const config = loadConfig();
68
+ assert.equal(config.token, "file-loaded-token");
69
+ }
70
+ finally {
71
+ rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ });
74
+ it("SWARMPIT_TOKEN_FILE takes precedence over SWARMPIT_TOKEN", async () => {
75
+ const { writeFileSync, mkdtempSync, rmSync } = await import("node:fs");
76
+ const { tmpdir } = await import("node:os");
77
+ const { join } = await import("node:path");
78
+ const dir = mkdtempSync(join(tmpdir(), "mcp-token-"));
79
+ const path = join(dir, "token");
80
+ writeFileSync(path, "from-file");
81
+ try {
82
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
83
+ process.env.SWARMPIT_TOKEN = "from-env";
84
+ process.env.SWARMPIT_TOKEN_FILE = path;
85
+ assert.equal(loadConfig().token, "from-file");
86
+ }
87
+ finally {
88
+ rmSync(dir, { recursive: true, force: true });
89
+ }
90
+ });
91
+ it("throws when SWARMPIT_TOKEN_FILE points to missing file", () => {
92
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
93
+ process.env.SWARMPIT_TOKEN_FILE = "/nonexistent/token-file";
94
+ assert.throws(() => loadConfig(), /could not be read/);
95
+ });
96
+ it("throws on invalid SWARMPIT_REDACT", () => {
97
+ process.env.SWARMPIT_URL = "https://swarmpit.example.com";
98
+ process.env.SWARMPIT_TOKEN = "test-token";
99
+ process.env.SWARMPIT_REDACT = "invalid";
100
+ assert.throws(() => loadConfig(), /SWARMPIT_REDACT/);
101
+ });
102
+ });
103
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/test/config.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,163 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { registerConfigTools } from "../tools/configs.js";
7
+ import { registerSecretTools } from "../tools/secrets.js";
8
+ import { registerStackTools } from "../tools/stacks.js";
9
+ /**
10
+ * Minimal mock that captures tool handlers registered on the MCP server,
11
+ * so we can invoke them directly in tests without the transport layer.
12
+ */
13
+ function mockServer() {
14
+ const handlers = {};
15
+ return {
16
+ handlers,
17
+ tool(name, _desc, _schema, handler) {
18
+ handlers[name] = handler;
19
+ },
20
+ };
21
+ }
22
+ function mockClient() {
23
+ const calls = {};
24
+ const record = (method) => (...args) => {
25
+ (calls[method] ||= []).push(args);
26
+ return Promise.resolve(undefined);
27
+ };
28
+ return {
29
+ calls,
30
+ listSecrets: record("listSecrets"),
31
+ listConfigs: record("listConfigs"),
32
+ createSecret: record("createSecret"),
33
+ createConfig: record("createConfig"),
34
+ createStack: record("createStack"),
35
+ updateStack: record("updateStack"),
36
+ getStackFile: () => Promise.resolve({ compose: "" }),
37
+ createStackFile: record("createStackFile"),
38
+ };
39
+ }
40
+ describe("$file integration", () => {
41
+ let tmpDir;
42
+ let filePath;
43
+ beforeEach(() => {
44
+ tmpDir = mkdtempSync(join(tmpdir(), "mcp-swarmpit-test-"));
45
+ filePath = join(tmpDir, "data.txt");
46
+ });
47
+ afterEach(() => {
48
+ rmSync(tmpDir, { recursive: true, force: true });
49
+ });
50
+ it("create_config reads data from $file", async () => {
51
+ writeFileSync(filePath, "<html>hello from file</html>");
52
+ const server = mockServer();
53
+ const client = mockClient();
54
+ registerConfigTools(server, client, "sensitive");
55
+ const result = await server.handlers.create_config({
56
+ configName: "my_config",
57
+ data: { $file: filePath },
58
+ });
59
+ assert.equal(result.isError, undefined);
60
+ const [arg] = client.calls.createConfig[0];
61
+ assert.equal(arg.configName, "my_config");
62
+ assert.equal(arg.data, "<html>hello from file</html>");
63
+ });
64
+ it("create_secret reads data from $file", async () => {
65
+ writeFileSync(filePath, "super-secret-token");
66
+ const server = mockServer();
67
+ const client = mockClient();
68
+ registerSecretTools(server, client, "sensitive");
69
+ const result = await server.handlers.create_secret({
70
+ secretName: "api_token",
71
+ data: { $file: filePath },
72
+ });
73
+ assert.equal(result.isError, undefined);
74
+ const [arg] = client.calls.createSecret[0];
75
+ assert.equal(arg.secretName, "api_token");
76
+ assert.equal(arg.data, "super-secret-token");
77
+ });
78
+ it("create_stack reads compose from $file", async () => {
79
+ const compose = "version: '3.3'\nservices:\n web:\n image: nginx\n";
80
+ writeFileSync(filePath, compose);
81
+ const server = mockServer();
82
+ const client = mockClient();
83
+ registerStackTools(server, client, "sensitive");
84
+ const result = await server.handlers.create_stack({
85
+ name: "myapp",
86
+ compose: { $file: filePath },
87
+ });
88
+ assert.equal(result.isError, undefined);
89
+ const [arg] = client.calls.createStack[0];
90
+ assert.equal(arg.name, "myapp");
91
+ assert.equal(arg.spec.compose, compose);
92
+ });
93
+ it("update_stack reads compose from $file", async () => {
94
+ const compose = "version: '3.3'\nservices:\n web:\n image: nginx:updated\n";
95
+ writeFileSync(filePath, compose);
96
+ const server = mockServer();
97
+ const client = mockClient();
98
+ registerStackTools(server, client, "sensitive");
99
+ const result = await server.handlers.update_stack({
100
+ name: "myapp",
101
+ compose: { $file: filePath },
102
+ });
103
+ assert.equal(result.isError, undefined);
104
+ const [name, yaml] = client.calls.updateStack[0];
105
+ assert.equal(name, "myapp");
106
+ assert.equal(yaml, compose);
107
+ });
108
+ it("create_stack_file reads compose from $file", async () => {
109
+ const compose = "version: '3.3'\nservices: {}\n";
110
+ writeFileSync(filePath, compose);
111
+ const server = mockServer();
112
+ const client = mockClient();
113
+ registerStackTools(server, client, "sensitive");
114
+ const result = await server.handlers.create_stack_file({
115
+ name: "myapp",
116
+ compose: { $file: filePath },
117
+ });
118
+ assert.equal(result.isError, undefined);
119
+ const [name, yaml] = client.calls.createStackFile[0];
120
+ assert.equal(name, "myapp");
121
+ assert.equal(yaml, compose);
122
+ });
123
+ it("create_config with $file does not resolve $env: inside raw config data", async () => {
124
+ writeFileSync(filePath, "$env:SHOULD_NOT_RESOLVE");
125
+ const server = mockServer();
126
+ const client = mockClient();
127
+ registerConfigTools(server, client, "sensitive");
128
+ await server.handlers.create_config({
129
+ configName: "x",
130
+ data: { $file: filePath },
131
+ });
132
+ const [arg] = client.calls.createConfig[0];
133
+ assert.equal(arg.data, "$env:SHOULD_NOT_RESOLVE");
134
+ });
135
+ it("create_stack with $file resolves $env: refs inside compose", async () => {
136
+ process.env.TEST_STACK_FILE_VAR = "resolved-value";
137
+ const compose = "services:\n app:\n environment:\n SECRET: $env:TEST_STACK_FILE_VAR\n";
138
+ writeFileSync(filePath, compose);
139
+ const server = mockServer();
140
+ const client = mockClient();
141
+ registerStackTools(server, client, "sensitive");
142
+ const result = await server.handlers.create_stack({
143
+ name: "myapp",
144
+ compose: { $file: filePath },
145
+ });
146
+ assert.equal(result.isError, undefined);
147
+ const [arg] = client.calls.createStack[0];
148
+ assert.ok(arg.spec.compose.includes("SECRET: resolved-value"));
149
+ delete process.env.TEST_STACK_FILE_VAR;
150
+ });
151
+ it("returns isError for missing file", async () => {
152
+ const server = mockServer();
153
+ const client = mockClient();
154
+ registerConfigTools(server, client, "sensitive");
155
+ const result = await server.handlers.create_config({
156
+ configName: "x",
157
+ data: { $file: "/nonexistent/path/xyz" },
158
+ });
159
+ assert.equal(result.isError, true);
160
+ assert.equal(client.calls.createConfig, undefined);
161
+ });
162
+ });
163
+ //# sourceMappingURL=file-ref.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-ref.test.js","sourceRoot":"","sources":["../../src/test/file-ref.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,aAAa,EAAc,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;GAGG;AACH,SAAS,UAAU;IACjB,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAC9C,OAAO;QACL,QAAQ;QACR,IAAI,CAAC,IAAY,EAAE,KAAa,EAAE,OAAgB,EAAE,OAAiB;YACnE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;QAC3B,CAAC;KACmE,CAAC;AACzE,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,KAAK,GAAgC,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE;QACxD,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,OAAO,CAAC,OAAO,CAAC,SAAoB,CAAC,CAAC;IAC/C,CAAC,CAAC;IACF,OAAO;QACL,KAAK;QACL,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC;QAClC,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC;QAClC,YAAY,EAAE,MAAM,CAAC,cAAc,CAAC;QACpC,YAAY,EAAE,MAAM,CAAC,cAAc,CAAC;QACpC,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC;QAClC,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC;QAClC,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACpD,eAAe,EAAE,MAAM,CAAC,iBAAiB,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,IAAI,MAAc,CAAC;IACnB,IAAI,QAAgB,CAAC;IAErB,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC3D,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,aAAa,CAAC,QAAQ,EAAE,8BAA8B,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,mBAAmB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAEnE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YACjD,UAAU,EAAE,WAAW;YACvB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC1B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAA2C,CAAC;QACrF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,8BAA8B,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,aAAa,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,mBAAmB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAEnE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YACjD,UAAU,EAAE,WAAW;YACvB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC1B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAA2C,CAAC;QACrF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,OAAO,GAAG,uDAAuD,CAAC;QACxE,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,kBAAkB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YAChD,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC7B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAkD,CAAC;QAC3F,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,OAAO,GAAG,+DAA+D,CAAC;QAChF,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,kBAAkB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YAChD,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC7B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAqB,CAAC;QACrE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5B,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,OAAO,GAAG,gCAAgC,CAAC;QACjD,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,kBAAkB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC;YACrD,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC7B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAqB,CAAC;QACzE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5B,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,aAAa,CAAC,QAAQ,EAAE,yBAAyB,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,mBAAmB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAEnE,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YAClC,UAAU,EAAE,GAAG;YACf,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC1B,CAAC,CAAC;QAEH,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAuB,CAAC;QACjE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,yBAAyB,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC1E,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;QACnD,MAAM,OAAO,GAAG,+EAA+E,CAAC;QAChG,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,kBAAkB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YAChD,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE;SAC7B,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAoC,CAAC;QAC7E,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC,CAAC;QAC/D,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,mBAAmB,CAAC,MAAe,EAAE,MAAe,EAAE,WAAW,CAAC,CAAC;QAEnE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YACjD,UAAU,EAAE,GAAG;YACf,IAAI,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE;SACzC,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { toolResult, toolError, resolveEnvRef, prepareServiceForUpdate, resolveData } from "../tools/helpers.js";
4
+ import { writeFileSync, unlinkSync } from "node:fs";
5
+ describe("toolResult", () => {
6
+ it("wraps data as JSON text content", () => {
7
+ const result = toolResult({ foo: "bar" });
8
+ assert.equal(result.content.length, 1);
9
+ assert.equal(result.content[0].type, "text");
10
+ assert.deepEqual(JSON.parse(result.content[0].text), { foo: "bar" });
11
+ });
12
+ });
13
+ describe("toolError", () => {
14
+ it("wraps Error as text with isError flag", () => {
15
+ const result = toolError(new Error("something broke"));
16
+ assert.equal(result.isError, true);
17
+ assert.equal(result.content[0].text, "something broke");
18
+ });
19
+ it("wraps string as text with isError flag", () => {
20
+ const result = toolError("plain string error");
21
+ assert.equal(result.isError, true);
22
+ assert.equal(result.content[0].text, "plain string error");
23
+ });
24
+ });
25
+ describe("resolveEnvRef", () => {
26
+ it("returns plain strings as-is", () => {
27
+ assert.equal(resolveEnvRef("hello"), "hello");
28
+ });
29
+ it("resolves { $env: VAR } from process.env", () => {
30
+ process.env.TEST_RESOLVE_VAR = "resolved-value";
31
+ assert.equal(resolveEnvRef({ $env: "TEST_RESOLVE_VAR" }), "resolved-value");
32
+ delete process.env.TEST_RESOLVE_VAR;
33
+ });
34
+ it("throws on missing env var ref", () => {
35
+ delete process.env.NONEXISTENT_REF;
36
+ assert.throws(() => resolveEnvRef({ $env: "NONEXISTENT_REF" }), /NONEXISTENT_REF/);
37
+ });
38
+ it("throws on invalid value type", () => {
39
+ assert.throws(() => resolveEnvRef(42), /Invalid env var value/);
40
+ });
41
+ });
42
+ describe("prepareServiceForUpdate", () => {
43
+ it("strips null values", () => {
44
+ const result = prepareServiceForUpdate({
45
+ serviceName: "test",
46
+ mode: "replicated",
47
+ tty: null,
48
+ dir: null,
49
+ command: null,
50
+ });
51
+ assert.equal(result.serviceName, "test");
52
+ assert.equal("tty" in result, false);
53
+ assert.equal("dir" in result, false);
54
+ assert.equal("command" in result, false);
55
+ });
56
+ it("strips read-only fields", () => {
57
+ const result = prepareServiceForUpdate({
58
+ id: "abc123",
59
+ createdAt: "2024-01-01",
60
+ updatedAt: "2024-01-02",
61
+ state: "running",
62
+ status: { tasks: { running: 1, total: 1 } },
63
+ serviceName: "test",
64
+ mode: "replicated",
65
+ });
66
+ assert.equal("id" in result, false);
67
+ assert.equal("createdAt" in result, false);
68
+ assert.equal("updatedAt" in result, false);
69
+ assert.equal("state" in result, false);
70
+ assert.equal("status" in result, false);
71
+ assert.equal(result.serviceName, "test");
72
+ });
73
+ it("trims repository to name and tag only", () => {
74
+ const result = prepareServiceForUpdate({
75
+ serviceName: "test",
76
+ mode: "replicated",
77
+ repository: {
78
+ name: "nginx",
79
+ tag: "alpine",
80
+ image: "nginx:alpine",
81
+ imageDigest: "sha256:abc123",
82
+ },
83
+ });
84
+ const repo = result.repository;
85
+ assert.equal(repo.name, "nginx");
86
+ assert.equal(repo.tag, "alpine");
87
+ assert.equal("image" in repo, false);
88
+ assert.equal("imageDigest" in repo, false);
89
+ });
90
+ it("handles repository with empty imageDigest", () => {
91
+ const result = prepareServiceForUpdate({
92
+ serviceName: "test",
93
+ mode: "replicated",
94
+ repository: {
95
+ name: "nginx",
96
+ tag: "alpine",
97
+ image: "nginx:alpine",
98
+ imageDigest: "",
99
+ },
100
+ });
101
+ const repo = result.repository;
102
+ assert.equal(repo.name, "nginx");
103
+ assert.equal(repo.tag, "alpine");
104
+ assert.equal("imageDigest" in repo, false);
105
+ });
106
+ });
107
+ describe("resolveData", () => {
108
+ it("returns plain strings as-is", () => {
109
+ assert.equal(resolveData("hello"), "hello");
110
+ });
111
+ it("reads from file path via $file", () => {
112
+ const path = "/tmp/mcp-swarmpit-test-data.txt";
113
+ writeFileSync(path, "contents from file");
114
+ try {
115
+ assert.equal(resolveData({ $file: path }), "contents from file");
116
+ }
117
+ finally {
118
+ unlinkSync(path);
119
+ }
120
+ });
121
+ it("resolves $env references", () => {
122
+ process.env.TEST_DATA_VAR = "env-value";
123
+ assert.equal(resolveData({ $env: "TEST_DATA_VAR" }), "env-value");
124
+ delete process.env.TEST_DATA_VAR;
125
+ });
126
+ it("throws on missing file", () => {
127
+ assert.throws(() => resolveData({ $file: "/nonexistent/path/xyz" }), /could not be read/);
128
+ });
129
+ it("throws on invalid value type", () => {
130
+ assert.throws(() => resolveData(42), /Invalid data/);
131
+ });
132
+ });
133
+ //# sourceMappingURL=helpers.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.test.js","sourceRoot":"","sources":["../../src/test/helpers.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACjH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpD,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAsB,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAsB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,SAAS,CAAC,oBAAoB,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAsB,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAC5E,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,uBAAuB,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,MAAM,GAAG,uBAAuB,CAAC;YACrC,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,YAAY;YAClB,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,IAAI;YACT,OAAO,EAAE,IAAI;SACd,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,MAAM,GAAG,uBAAuB,CAAC;YACrC,EAAE,EAAE,QAAQ;YACZ,SAAS,EAAE,YAAY;YACvB,SAAS,EAAE,YAAY;YACvB,KAAK,EAAE,SAAS;YAChB,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YAC3C,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,YAAY;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,MAAM,GAAG,uBAAuB,CAAC;YACrC,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE;gBACV,IAAI,EAAE,OAAO;gBACb,GAAG,EAAE,QAAQ;gBACb,KAAK,EAAE,cAAc;gBACrB,WAAW,EAAE,eAAe;aAC7B;SACF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,UAAqC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,aAAa,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,MAAM,GAAG,uBAAuB,CAAC;YACrC,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE;gBACV,IAAI,EAAE,OAAO;gBACb,GAAG,EAAE,QAAQ;gBACb,KAAK,EAAE,cAAc;gBACrB,WAAW,EAAE,EAAE;aAChB;SACF,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,UAAqC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,CAAC,aAAa,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,IAAI,GAAG,iCAAiC,CAAC;QAC/C,aAAa,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,oBAAoB,CAAC,CAAC;QACnE,CAAC;gBAAS,CAAC;YACT,UAAU,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,WAAW,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,EAAE,mBAAmB,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,207 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { sanitizeService, sanitizeServices, sanitizeComposeYaml, resolveComposeEnvRefs, restoreRedactedValues, setExtraRedactPatterns, } from "../sanitize.js";
4
+ function makeService(overrides = {}) {
5
+ return {
6
+ id: "test-id",
7
+ version: 1,
8
+ createdAt: "2024-01-01T00:00:00Z",
9
+ updatedAt: "2024-01-01T00:00:00Z",
10
+ repository: { name: "test", tag: "latest", image: "test:latest", imageDigest: "sha256:abc" },
11
+ serviceName: "test_service",
12
+ mode: "replicated",
13
+ replicas: 1,
14
+ state: "running",
15
+ status: { tasks: { running: 1, total: 1 }, update: "", message: "" },
16
+ ports: [],
17
+ mounts: [],
18
+ networks: [],
19
+ secrets: [],
20
+ configs: [],
21
+ variables: [],
22
+ labels: [],
23
+ command: null,
24
+ stack: "test",
25
+ resources: { reservation: { cpu: 0, memory: 0 }, limit: { cpu: 0, memory: 0 } },
26
+ ...overrides,
27
+ };
28
+ }
29
+ describe("sanitizeService", () => {
30
+ it("redacts all env vars when redactAll=true", () => {
31
+ const service = makeService({
32
+ variables: [
33
+ { name: "NODE_ENV", value: "production" },
34
+ { name: "DB_PASS", value: "secret123" },
35
+ ],
36
+ });
37
+ const result = sanitizeService(service, true);
38
+ assert.equal(result.variables[0].value, "[REDACTED]");
39
+ assert.equal(result.variables[1].value, "[REDACTED]");
40
+ });
41
+ it("only redacts sensitive vars when redactAll=false", () => {
42
+ const service = makeService({
43
+ variables: [
44
+ { name: "NODE_ENV", value: "production" },
45
+ { name: "DB_PASS", value: "secret123" },
46
+ { name: "API_KEY", value: "key123" },
47
+ { name: "LOG_LEVEL", value: "debug" },
48
+ { name: "SECRET_VALUE", value: "hidden" },
49
+ { name: "AUTH_HEADER", value: "bearer xyz" },
50
+ ],
51
+ });
52
+ const result = sanitizeService(service, false);
53
+ assert.equal(result.variables[0].value, "production"); // NODE_ENV — not sensitive
54
+ assert.equal(result.variables[1].value, "[REDACTED]"); // DB_PASS — matches "pass"
55
+ assert.equal(result.variables[2].value, "[REDACTED]"); // API_KEY — matches "key"
56
+ assert.equal(result.variables[3].value, "debug"); // LOG_LEVEL — not sensitive
57
+ assert.equal(result.variables[4].value, "[REDACTED]"); // SECRET_VALUE — matches "secret"
58
+ assert.equal(result.variables[5].value, "[REDACTED]"); // AUTH_HEADER — matches "auth"
59
+ });
60
+ it("redacts extra custom patterns", () => {
61
+ setExtraRedactPatterns(["GRAFANA", "RPC"]);
62
+ const service = makeService({
63
+ variables: [
64
+ { name: "GRAFANA", value: "https://grafana.example.com" },
65
+ { name: "RPC_URL", value: "ws://node:9999" },
66
+ { name: "LOG_LEVEL", value: "debug" },
67
+ ],
68
+ });
69
+ const result = sanitizeService(service, false);
70
+ assert.equal(result.variables[0].value, "[REDACTED]"); // GRAFANA — custom pattern
71
+ assert.equal(result.variables[1].value, "[REDACTED]"); // RPC_URL — custom pattern
72
+ assert.equal(result.variables[2].value, "debug"); // LOG_LEVEL — no match
73
+ setExtraRedactPatterns([]); // clean up
74
+ });
75
+ it("preserves env var names", () => {
76
+ const service = makeService({
77
+ variables: [{ name: "PASSWORD", value: "secret" }],
78
+ });
79
+ const result = sanitizeService(service, true);
80
+ assert.equal(result.variables[0].name, "PASSWORD");
81
+ });
82
+ it("does not modify command args", () => {
83
+ const service = makeService({
84
+ command: ["--db", "postgres://user:secret@host:5432/db"],
85
+ });
86
+ const result = sanitizeService(service, true);
87
+ assert.deepEqual(result.command, ["--db", "postgres://user:secret@host:5432/db"]);
88
+ });
89
+ it("strips secret data fields but keeps references", () => {
90
+ const service = makeService({
91
+ secrets: [{ id: "s1", secretName: "db_pass", secretTarget: "/run/secrets/db_pass" }],
92
+ configs: [{ id: "c1", configName: "app_config", configTarget: "/etc/app.conf" }],
93
+ });
94
+ const result = sanitizeService(service);
95
+ assert.deepEqual(result.secrets, [{ id: "s1", secretName: "db_pass", secretTarget: "/run/secrets/db_pass" }]);
96
+ assert.deepEqual(result.configs, [{ id: "c1", configName: "app_config", configTarget: "/etc/app.conf" }]);
97
+ });
98
+ });
99
+ describe("sanitizeServices", () => {
100
+ it("sanitizes an array of services", () => {
101
+ const services = [
102
+ makeService({ variables: [{ name: "PASSWORD", value: "s1" }] }),
103
+ makeService({ variables: [{ name: "TOKEN", value: "s2" }] }),
104
+ ];
105
+ const result = sanitizeServices(services, false);
106
+ assert.equal(result[0].variables[0].value, "[REDACTED]");
107
+ assert.equal(result[1].variables[0].value, "[REDACTED]");
108
+ });
109
+ });
110
+ describe("sanitizeComposeYaml", () => {
111
+ it("redacts all env vars in key-value form when redactAll=true", () => {
112
+ const yaml = `services:
113
+ app:
114
+ environment:
115
+ NODE_ENV: production
116
+ DB_HOST: localhost`;
117
+ const result = sanitizeComposeYaml(yaml, true);
118
+ assert.ok(result.includes("NODE_ENV: [REDACTED]"));
119
+ assert.ok(result.includes("DB_HOST: [REDACTED]"));
120
+ });
121
+ it("only redacts sensitive vars when redactAll=false", () => {
122
+ const yaml = `services:
123
+ app:
124
+ environment:
125
+ NODE_ENV: production
126
+ DB_PASSWORD: secret123`;
127
+ const result = sanitizeComposeYaml(yaml, false);
128
+ assert.ok(result.includes("NODE_ENV: production"));
129
+ assert.ok(result.includes("DB_PASSWORD: [REDACTED]"));
130
+ });
131
+ it("redacts list form env vars", () => {
132
+ const yaml = `services:
133
+ app:
134
+ environment:
135
+ - NODE_ENV=production
136
+ - DB_PASSWORD=secret123`;
137
+ const result = sanitizeComposeYaml(yaml, false);
138
+ assert.ok(result.includes("- NODE_ENV=production"));
139
+ assert.ok(result.includes("- DB_PASSWORD=[REDACTED]"));
140
+ });
141
+ it("does not redact YAML references or objects", () => {
142
+ const yaml = `services:
143
+ app:
144
+ environment:
145
+ CONFIG: &default_config
146
+ REF: *default_config`;
147
+ const result = sanitizeComposeYaml(yaml, true);
148
+ assert.ok(result.includes("CONFIG: &default_config"));
149
+ assert.ok(result.includes("REF: *default_config"));
150
+ });
151
+ it("does not modify commands in compose", () => {
152
+ const yaml = `services:
153
+ app:
154
+ command: ["--db", "postgres://user:secret@host/db"]`;
155
+ const result = sanitizeComposeYaml(yaml, true);
156
+ assert.ok(result.includes("postgres://user:secret@host/db"));
157
+ });
158
+ });
159
+ describe("resolveComposeEnvRefs", () => {
160
+ it("resolves $env:VAR_NAME in key-value form", () => {
161
+ process.env.TEST_SECRET = "resolved-value";
162
+ const yaml = ` DB_PASSWORD: $env:TEST_SECRET`;
163
+ const result = resolveComposeEnvRefs(yaml);
164
+ assert.ok(result.includes("DB_PASSWORD: resolved-value"));
165
+ delete process.env.TEST_SECRET;
166
+ });
167
+ it("resolves $env:VAR_NAME in list form", () => {
168
+ process.env.TEST_SECRET = "resolved-value";
169
+ const yaml = ` - DB_PASSWORD=$env:TEST_SECRET`;
170
+ const result = resolveComposeEnvRefs(yaml);
171
+ assert.ok(result.includes("- DB_PASSWORD=resolved-value"));
172
+ delete process.env.TEST_SECRET;
173
+ });
174
+ it("passes through plain values unchanged", () => {
175
+ const yaml = ` NODE_ENV: production`;
176
+ const result = resolveComposeEnvRefs(yaml);
177
+ assert.equal(result, yaml);
178
+ });
179
+ it("throws on missing env var", () => {
180
+ delete process.env.NONEXISTENT_VAR;
181
+ const yaml = ` DB_PASSWORD: $env:NONEXISTENT_VAR`;
182
+ assert.throws(() => resolveComposeEnvRefs(yaml), /NONEXISTENT_VAR/);
183
+ });
184
+ });
185
+ describe("restoreRedactedValues", () => {
186
+ it("restores [REDACTED] from original in key-value form", () => {
187
+ const edited = ` DB_PASSWORD: [REDACTED]\n NODE_ENV: staging`;
188
+ const original = ` DB_PASSWORD: secret123\n NODE_ENV: production`;
189
+ const result = restoreRedactedValues(edited, original);
190
+ assert.ok(result.includes("DB_PASSWORD: secret123"));
191
+ assert.ok(result.includes("NODE_ENV: staging"));
192
+ });
193
+ it("restores [REDACTED] from original in list form", () => {
194
+ const edited = ` - DB_PASSWORD=[REDACTED]\n - NODE_ENV=staging`;
195
+ const original = ` - DB_PASSWORD=secret123\n - NODE_ENV=production`;
196
+ const result = restoreRedactedValues(edited, original);
197
+ assert.ok(result.includes("- DB_PASSWORD=secret123"));
198
+ assert.ok(result.includes("- NODE_ENV=staging"));
199
+ });
200
+ it("leaves non-redacted values as-is", () => {
201
+ const edited = ` NODE_ENV: staging`;
202
+ const original = ` NODE_ENV: production`;
203
+ const result = restoreRedactedValues(edited, original);
204
+ assert.ok(result.includes("NODE_ENV: staging"));
205
+ });
206
+ });
207
+ //# sourceMappingURL=sanitize.test.js.map