llng-mcp 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 (114) hide show
  1. package/.github/workflows/ci.yml +77 -0
  2. package/.prettierrc +7 -0
  3. package/LICENSE +661 -0
  4. package/README.md +502 -0
  5. package/dist/__tests__/api-transport.test.d.ts +1 -0
  6. package/dist/__tests__/api-transport.test.js +577 -0
  7. package/dist/__tests__/api-transport.test.js.map +1 -0
  8. package/dist/__tests__/config.test.d.ts +1 -0
  9. package/dist/__tests__/config.test.js +472 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/integration/api-mode.test.d.ts +1 -0
  12. package/dist/__tests__/integration/api-mode.test.js +199 -0
  13. package/dist/__tests__/integration/api-mode.test.js.map +1 -0
  14. package/dist/__tests__/integration/oidc-rp.test.d.ts +1 -0
  15. package/dist/__tests__/integration/oidc-rp.test.js +120 -0
  16. package/dist/__tests__/integration/oidc-rp.test.js.map +1 -0
  17. package/dist/__tests__/integration/ssh-mode.test.d.ts +1 -0
  18. package/dist/__tests__/integration/ssh-mode.test.js +101 -0
  19. package/dist/__tests__/integration/ssh-mode.test.js.map +1 -0
  20. package/dist/__tests__/k8s-transport.test.d.ts +1 -0
  21. package/dist/__tests__/k8s-transport.test.js +254 -0
  22. package/dist/__tests__/k8s-transport.test.js.map +1 -0
  23. package/dist/__tests__/oidc-tools.test.d.ts +1 -0
  24. package/dist/__tests__/oidc-tools.test.js +457 -0
  25. package/dist/__tests__/oidc-tools.test.js.map +1 -0
  26. package/dist/__tests__/registry.test.d.ts +1 -0
  27. package/dist/__tests__/registry.test.js +96 -0
  28. package/dist/__tests__/registry.test.js.map +1 -0
  29. package/dist/__tests__/ssh-transport.test.d.ts +1 -0
  30. package/dist/__tests__/ssh-transport.test.js +618 -0
  31. package/dist/__tests__/ssh-transport.test.js.map +1 -0
  32. package/dist/__tests__/tools.test.d.ts +1 -0
  33. package/dist/__tests__/tools.test.js +525 -0
  34. package/dist/__tests__/tools.test.js.map +1 -0
  35. package/dist/config.d.ts +65 -0
  36. package/dist/config.js +506 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +42 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/resources/documentation.d.ts +5 -0
  42. package/dist/resources/documentation.js +56 -0
  43. package/dist/resources/documentation.js.map +1 -0
  44. package/dist/tools/cli-utilities.d.ts +3 -0
  45. package/dist/tools/cli-utilities.js +187 -0
  46. package/dist/tools/cli-utilities.js.map +1 -0
  47. package/dist/tools/config.d.ts +6 -0
  48. package/dist/tools/config.js +326 -0
  49. package/dist/tools/config.js.map +1 -0
  50. package/dist/tools/consents.d.ts +3 -0
  51. package/dist/tools/consents.js +39 -0
  52. package/dist/tools/consents.js.map +1 -0
  53. package/dist/tools/instances.d.ts +3 -0
  54. package/dist/tools/instances.js +14 -0
  55. package/dist/tools/instances.js.map +1 -0
  56. package/dist/tools/oidc-rp.d.ts +6 -0
  57. package/dist/tools/oidc-rp.js +246 -0
  58. package/dist/tools/oidc-rp.js.map +1 -0
  59. package/dist/tools/oidc.d.ts +3 -0
  60. package/dist/tools/oidc.js +343 -0
  61. package/dist/tools/oidc.js.map +1 -0
  62. package/dist/tools/secondfactors.d.ts +3 -0
  63. package/dist/tools/secondfactors.js +62 -0
  64. package/dist/tools/secondfactors.js.map +1 -0
  65. package/dist/tools/sessions.d.ts +6 -0
  66. package/dist/tools/sessions.js +300 -0
  67. package/dist/tools/sessions.js.map +1 -0
  68. package/dist/transport/api.d.ts +35 -0
  69. package/dist/transport/api.js +327 -0
  70. package/dist/transport/api.js.map +1 -0
  71. package/dist/transport/interface.d.ts +50 -0
  72. package/dist/transport/interface.js +2 -0
  73. package/dist/transport/interface.js.map +1 -0
  74. package/dist/transport/k8s.d.ts +41 -0
  75. package/dist/transport/k8s.js +303 -0
  76. package/dist/transport/k8s.js.map +1 -0
  77. package/dist/transport/registry.d.ts +20 -0
  78. package/dist/transport/registry.js +91 -0
  79. package/dist/transport/registry.js.map +1 -0
  80. package/dist/transport/ssh.d.ts +37 -0
  81. package/dist/transport/ssh.js +353 -0
  82. package/dist/transport/ssh.js.map +1 -0
  83. package/docker-compose.test.yml +16 -0
  84. package/eslint.config.js +21 -0
  85. package/package.json +38 -0
  86. package/src/__tests__/api-transport.test.ts +746 -0
  87. package/src/__tests__/config.test.ts +587 -0
  88. package/src/__tests__/integration/api-mode.test.ts +229 -0
  89. package/src/__tests__/integration/oidc-rp.test.ts +138 -0
  90. package/src/__tests__/integration/ssh-mode.test.ts +113 -0
  91. package/src/__tests__/k8s-transport.test.ts +342 -0
  92. package/src/__tests__/oidc-tools.test.ts +554 -0
  93. package/src/__tests__/registry.test.ts +110 -0
  94. package/src/__tests__/ssh-transport.test.ts +805 -0
  95. package/src/__tests__/tools.test.ts +735 -0
  96. package/src/config.ts +605 -0
  97. package/src/index.ts +48 -0
  98. package/src/resources/documentation.ts +65 -0
  99. package/src/tools/cli-utilities.ts +207 -0
  100. package/src/tools/config.ts +382 -0
  101. package/src/tools/consents.ts +50 -0
  102. package/src/tools/instances.ts +21 -0
  103. package/src/tools/oidc-rp.ts +299 -0
  104. package/src/tools/oidc.ts +434 -0
  105. package/src/tools/secondfactors.ts +78 -0
  106. package/src/tools/sessions.ts +342 -0
  107. package/src/transport/api.ts +429 -0
  108. package/src/transport/interface.ts +58 -0
  109. package/src/transport/k8s.ts +367 -0
  110. package/src/transport/registry.ts +105 -0
  111. package/src/transport/ssh.ts +430 -0
  112. package/tsconfig.json +16 -0
  113. package/vitest.config.ts +8 -0
  114. package/vitest.integration.config.ts +9 -0
@@ -0,0 +1,342 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { K8sTransport } from "../transport/k8s.js";
3
+ import { K8sConfig } from "../config.js";
4
+ import * as child_process from "child_process";
5
+ import { EventEmitter } from "events";
6
+
7
+ vi.mock("child_process");
8
+
9
+ class MockChildProcess extends EventEmitter {
10
+ stdin = {
11
+ write: vi.fn(),
12
+ end: vi.fn(),
13
+ };
14
+ stdout = new EventEmitter();
15
+ stderr = new EventEmitter();
16
+
17
+ constructor() {
18
+ super();
19
+ }
20
+ }
21
+
22
+ function mockSpawn(stdout: string, stderr = "", exitCode = 0): MockChildProcess {
23
+ const proc = new MockChildProcess();
24
+ process.nextTick(() => {
25
+ if (stdout) {
26
+ proc.stdout.emit("data", Buffer.from(stdout));
27
+ }
28
+ if (stderr) {
29
+ proc.stderr.emit("data", Buffer.from(stderr));
30
+ }
31
+ proc.emit("close", exitCode);
32
+ });
33
+ return proc;
34
+ }
35
+
36
+ describe("K8sTransport", () => {
37
+ const defaultConfig: K8sConfig = {
38
+ context: "my-cluster",
39
+ namespace: "auth",
40
+ deployment: "lemonldap-ng",
41
+ };
42
+
43
+ let spawnCalls: Array<{ cmd: string; args: string[] }> = [];
44
+ let spawnCallIndex = 0;
45
+
46
+ const setupSpawnMock = (...responses: Array<{ stdout: string; exitCode?: number }>) => {
47
+ spawnCallIndex = 0;
48
+ vi.mocked(child_process.spawn).mockImplementation((cmd: string, args?: readonly string[]) => {
49
+ spawnCalls.push({ cmd, args: args ? [...args] : [] });
50
+ const resp = responses[spawnCallIndex] || responses[responses.length - 1];
51
+ spawnCallIndex++;
52
+ return mockSpawn(resp.stdout, "", resp.exitCode ?? 0) as any;
53
+ });
54
+ };
55
+
56
+ beforeEach(() => {
57
+ vi.resetAllMocks();
58
+ spawnCalls = [];
59
+ spawnCallIndex = 0;
60
+ });
61
+
62
+ afterEach(() => {
63
+ spawnCalls = [];
64
+ });
65
+
66
+ describe("Pod resolution", () => {
67
+ it("resolves pod name using default label selector", async () => {
68
+ setupSpawnMock(
69
+ { stdout: "lemonldap-ng-abc123" },
70
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
71
+ );
72
+
73
+ const transport = new K8sTransport(defaultConfig);
74
+ await transport.configInfo();
75
+
76
+ // First call: get pods
77
+ expect(spawnCalls[0].cmd).toBe("kubectl");
78
+ expect(spawnCalls[0].args).toEqual([
79
+ "-n",
80
+ "auth",
81
+ "--context",
82
+ "my-cluster",
83
+ "get",
84
+ "pods",
85
+ "-l",
86
+ "app.kubernetes.io/name=lemonldap-ng",
87
+ "-o",
88
+ "jsonpath={.items[0].metadata.name}",
89
+ ]);
90
+
91
+ // Second call: exec
92
+ expect(spawnCalls[1].cmd).toBe("kubectl");
93
+ expect(spawnCalls[1].args).toContain("exec");
94
+ expect(spawnCalls[1].args).toContain("lemonldap-ng-abc123");
95
+ });
96
+
97
+ it("caches pod name across calls", async () => {
98
+ setupSpawnMock(
99
+ { stdout: "lemonldap-ng-abc123" },
100
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
101
+ { stdout: JSON.stringify({ domain: "example.com" }) },
102
+ );
103
+
104
+ const transport = new K8sTransport(defaultConfig);
105
+ await transport.configInfo();
106
+ await transport.configGet(["domain"]);
107
+
108
+ // Pod resolution should only happen once (first call)
109
+ const getPodsCalls = spawnCalls.filter(
110
+ (c) => c.args.includes("get") && c.args.includes("pods"),
111
+ );
112
+ expect(getPodsCalls).toHaveLength(1);
113
+ });
114
+
115
+ it("uses custom podSelector", async () => {
116
+ const config: K8sConfig = {
117
+ ...defaultConfig,
118
+ podSelector: "app=llng,tier=auth",
119
+ };
120
+
121
+ setupSpawnMock(
122
+ { stdout: "llng-pod-xyz" },
123
+ { stdout: "Num : 1\nAuthor : admin\nDate : 2025-01-30" },
124
+ );
125
+
126
+ const transport = new K8sTransport(config);
127
+ await transport.configInfo();
128
+
129
+ expect(spawnCalls[0].args).toContain("app=llng,tier=auth");
130
+ });
131
+
132
+ it("throws when no pod found", async () => {
133
+ setupSpawnMock({ stdout: "" });
134
+
135
+ const transport = new K8sTransport(defaultConfig);
136
+ await expect(transport.configInfo()).rejects.toThrow("No pod found for selector");
137
+ });
138
+ });
139
+
140
+ describe("Command execution", () => {
141
+ it("builds correct exec command with context, namespace, and container", async () => {
142
+ const config: K8sConfig = {
143
+ ...defaultConfig,
144
+ container: "sso",
145
+ };
146
+
147
+ setupSpawnMock(
148
+ { stdout: "llng-pod-123" },
149
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
150
+ );
151
+
152
+ const transport = new K8sTransport(config);
153
+ await transport.configInfo();
154
+
155
+ const execCall = spawnCalls[1];
156
+ expect(execCall.args).toEqual([
157
+ "--context",
158
+ "my-cluster",
159
+ "-n",
160
+ "auth",
161
+ "exec",
162
+ "llng-pod-123",
163
+ "-c",
164
+ "sso",
165
+ "--",
166
+ "/usr/share/lemonldap-ng/bin/lemonldap-ng-cli",
167
+ "info",
168
+ ]);
169
+ });
170
+
171
+ it("exec without container omits -c flag", async () => {
172
+ setupSpawnMock(
173
+ { stdout: "llng-pod-123" },
174
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
175
+ );
176
+
177
+ const transport = new K8sTransport(defaultConfig);
178
+ await transport.configInfo();
179
+
180
+ const execCall = spawnCalls[1];
181
+ expect(execCall.args).not.toContain("-c");
182
+ });
183
+
184
+ it("works without context", async () => {
185
+ const config: K8sConfig = {
186
+ namespace: "auth",
187
+ deployment: "lemonldap-ng",
188
+ };
189
+
190
+ setupSpawnMock(
191
+ { stdout: "llng-pod-123" },
192
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
193
+ );
194
+
195
+ const transport = new K8sTransport(config);
196
+ await transport.configInfo();
197
+
198
+ expect(spawnCalls[0].args).not.toContain("--context");
199
+ expect(spawnCalls[1].args).not.toContain("--context");
200
+ });
201
+ });
202
+
203
+ describe("binPrefix", () => {
204
+ it("custom binPrefix resolves paths", async () => {
205
+ const config: K8sConfig = {
206
+ ...defaultConfig,
207
+ binPrefix: "/opt/llng/bin",
208
+ };
209
+
210
+ setupSpawnMock(
211
+ { stdout: "llng-pod-123" },
212
+ { stdout: "Num : 42\nAuthor : admin\nDate : 2025-01-30" },
213
+ );
214
+
215
+ const transport = new K8sTransport(config);
216
+ await transport.configInfo();
217
+
218
+ const execCall = spawnCalls[1];
219
+ expect(execCall.args).toContain("/opt/llng/bin/lemonldap-ng-cli");
220
+ });
221
+ });
222
+
223
+ describe("CLI methods", () => {
224
+ it("configGet passes keys", async () => {
225
+ setupSpawnMock(
226
+ { stdout: "llng-pod-123" },
227
+ { stdout: JSON.stringify({ domain: "example.com", portal: "https://portal.example.com" }) },
228
+ );
229
+
230
+ const transport = new K8sTransport(defaultConfig);
231
+ const result = await transport.configGet(["domain", "portal"]);
232
+
233
+ expect(result).toEqual({ domain: "example.com", portal: "https://portal.example.com" });
234
+ });
235
+
236
+ it("configSet passes key-value pairs", async () => {
237
+ setupSpawnMock({ stdout: "llng-pod-123" }, { stdout: "" });
238
+
239
+ const transport = new K8sTransport(defaultConfig);
240
+ await transport.configSet({ domain: "example.com" }, "Update");
241
+
242
+ const execCall = spawnCalls[1];
243
+ expect(execCall.args).toContain("set");
244
+ expect(execCall.args).toContain("-yes");
245
+ expect(execCall.args).toContain("domain");
246
+ expect(execCall.args).toContain("example.com");
247
+ expect(execCall.args).toContain("-log");
248
+ expect(execCall.args).toContain("Update");
249
+ });
250
+
251
+ it("sessionSearch builds correct args", async () => {
252
+ setupSpawnMock({ stdout: "llng-pod-123" }, { stdout: '[{"id": "s1"}]' });
253
+
254
+ const transport = new K8sTransport(defaultConfig);
255
+ const result = await transport.sessionSearch({ where: { uid: "john" } });
256
+
257
+ expect(result).toEqual([{ id: "s1" }]);
258
+ });
259
+
260
+ it("unsupported methods throw", async () => {
261
+ const transport = new K8sTransport(defaultConfig);
262
+ await expect(transport.secondFactorsGet("user")).rejects.toThrow("Use API mode");
263
+ await expect(transport.consentsGet("user")).rejects.toThrow("Use API mode");
264
+ });
265
+
266
+ it("sessionSetKey uses execSessions with setKey command", async () => {
267
+ setupSpawnMock({ stdout: "llng-pod-123" }, { stdout: "" });
268
+
269
+ const transport = new K8sTransport(defaultConfig);
270
+ await transport.sessionSetKey("sid1", { uid: "jane" });
271
+
272
+ const execCall = spawnCalls[1];
273
+ expect(execCall.args).toContain("setKey");
274
+ });
275
+
276
+ it("sessionDelKey uses execSessions with delKey command", async () => {
277
+ setupSpawnMock({ stdout: "llng-pod-123" }, { stdout: "" });
278
+
279
+ const transport = new K8sTransport(defaultConfig);
280
+ await transport.sessionDelKey("sid1", ["key1"]);
281
+
282
+ const execCall = spawnCalls[1];
283
+ expect(execCall.args).toContain("delKey");
284
+ });
285
+ });
286
+
287
+ describe("stdin methods", () => {
288
+ it("configRestore passes JSON via stdin", async () => {
289
+ let stdinContent = "";
290
+ let callIdx = 0;
291
+ vi.mocked(child_process.spawn).mockImplementation((cmd: string, args?: readonly string[]) => {
292
+ spawnCalls.push({ cmd, args: args ? [...args] : [] });
293
+ const proc = new MockChildProcess();
294
+ if (callIdx === 1) {
295
+ proc.stdin.write = vi.fn((data: any) => {
296
+ stdinContent += data;
297
+ return true;
298
+ });
299
+ }
300
+ callIdx++;
301
+ process.nextTick(() => {
302
+ if (callIdx === 1) {
303
+ proc.stdout.emit("data", Buffer.from("llng-pod-123"));
304
+ }
305
+ proc.emit("close", 0);
306
+ });
307
+ return proc as any;
308
+ });
309
+
310
+ const transport = new K8sTransport(defaultConfig);
311
+ const json = '{"domain": "example.com"}';
312
+ await transport.configRestore(json);
313
+
314
+ expect(stdinContent).toBe(json);
315
+ // The exec call should include -i flag for stdin
316
+ const execCall = spawnCalls[1];
317
+ expect(execCall.args).toContain("-i");
318
+ expect(execCall.args).toContain("restore");
319
+ });
320
+ });
321
+
322
+ describe("execScript", () => {
323
+ it("executes script with default binPrefix", async () => {
324
+ setupSpawnMock({ stdout: "lemonldap-ng-abc123" }, { stdout: "Keys rotated successfully" });
325
+
326
+ const transport = new K8sTransport(defaultConfig);
327
+ const result = await transport.execScript("rotateOidcKeys", []);
328
+
329
+ // First call is pod resolution
330
+ expect(spawnCalls[0].cmd).toBe("kubectl");
331
+ expect(spawnCalls[0].args).toContain("get");
332
+ expect(spawnCalls[0].args).toContain("pods");
333
+
334
+ // Second call is the exec with the script
335
+ expect(spawnCalls[1].cmd).toBe("kubectl");
336
+ expect(spawnCalls[1].args).toContain("exec");
337
+ expect(spawnCalls[1].args).toContain("/usr/share/lemonldap-ng/bin/rotateOidcKeys");
338
+
339
+ expect(result).toBe("Keys rotated successfully");
340
+ });
341
+ });
342
+ });