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,367 @@
1
+ import { spawn } from "child_process";
2
+ import { K8sConfig, resolvePaths } from "../config.js";
3
+ import {
4
+ ILlngTransport,
5
+ SessionFilter,
6
+ ConfigInfo,
7
+ SessionGetOptions,
8
+ SessionDeleteOptions,
9
+ } from "./interface.js";
10
+
11
+ export class K8sTransport implements ILlngTransport {
12
+ private paths: { cliPath: string; sessionsPath: string; configEditorPath: string };
13
+ private cachedPodName: string | null = null;
14
+
15
+ constructor(private config: K8sConfig) {
16
+ this.paths = resolvePaths(config.binPrefix);
17
+ }
18
+
19
+ private async resolvePod(): Promise<string> {
20
+ if (this.cachedPodName) {
21
+ return this.cachedPodName;
22
+ }
23
+
24
+ if (!this.config.namespace) {
25
+ throw new Error("K8s namespace is required but not configured");
26
+ }
27
+ if (!this.config.deployment && !this.config.podSelector) {
28
+ throw new Error("K8s deployment or podSelector is required but not configured");
29
+ }
30
+ const selector = this.config.podSelector || `app.kubernetes.io/name=${this.config.deployment}`;
31
+ const args = ["get", "pods", "-l", selector, "-o", "jsonpath={.items[0].metadata.name}"];
32
+
33
+ if (this.config.context) {
34
+ args.unshift("--context", this.config.context);
35
+ }
36
+ args.unshift("-n", this.config.namespace!);
37
+
38
+ const podName = await this.kubectl(args);
39
+ if (!podName || podName === "{}" || podName.trim() === "") {
40
+ throw new Error(
41
+ `No pod found for selector '${selector}' in namespace '${this.config.namespace}'`,
42
+ );
43
+ }
44
+
45
+ this.cachedPodName = podName.trim();
46
+ return this.cachedPodName;
47
+ }
48
+
49
+ private async kubectl(args: string[]): Promise<string> {
50
+ return new Promise((resolve, reject) => {
51
+ const proc = spawn("kubectl", args);
52
+ let stdout = "";
53
+
54
+ proc.stdout.on("data", (data) => {
55
+ stdout += data.toString();
56
+ });
57
+
58
+ proc.stderr.on("data", () => {
59
+ // stderr consumed for security
60
+ });
61
+
62
+ proc.on("close", (code) => {
63
+ if (code !== 0) {
64
+ reject(new Error(`kubectl command failed with exit code ${code}`));
65
+ } else {
66
+ resolve(stdout);
67
+ }
68
+ });
69
+
70
+ proc.on("error", () => {
71
+ reject(new Error("kubectl command execution failed"));
72
+ });
73
+ });
74
+ }
75
+
76
+ private async kubectlWithStdin(args: string[], input: string): Promise<string> {
77
+ return new Promise((resolve, reject) => {
78
+ const proc = spawn("kubectl", args);
79
+ let stdout = "";
80
+
81
+ proc.stdout.on("data", (data) => {
82
+ stdout += data.toString();
83
+ });
84
+
85
+ proc.stderr.on("data", () => {
86
+ // stderr consumed for security
87
+ });
88
+
89
+ proc.on("close", (code) => {
90
+ if (code !== 0) {
91
+ reject(new Error(`kubectl command failed with exit code ${code}`));
92
+ } else {
93
+ resolve(stdout);
94
+ }
95
+ });
96
+
97
+ proc.on("error", () => {
98
+ reject(new Error("kubectl command execution failed"));
99
+ });
100
+
101
+ proc.stdin.write(input);
102
+ proc.stdin.end();
103
+ });
104
+ }
105
+
106
+ private buildExecArgs(podName: string, command: string[]): string[] {
107
+ const args: string[] = [];
108
+
109
+ if (this.config.context) {
110
+ args.push("--context", this.config.context);
111
+ }
112
+ args.push("-n", this.config.namespace!);
113
+ args.push("exec", podName);
114
+
115
+ if (this.config.container) {
116
+ args.push("-c", this.config.container);
117
+ }
118
+
119
+ args.push("--", ...command);
120
+ return args;
121
+ }
122
+
123
+ private buildExecStdinArgs(podName: string, command: string[]): string[] {
124
+ const args: string[] = [];
125
+
126
+ if (this.config.context) {
127
+ args.push("--context", this.config.context);
128
+ }
129
+ args.push("-n", this.config.namespace!);
130
+ args.push("exec", "-i", podName);
131
+
132
+ if (this.config.container) {
133
+ args.push("-c", this.config.container);
134
+ }
135
+
136
+ args.push("--", ...command);
137
+ return args;
138
+ }
139
+
140
+ private async exec(command: string[]): Promise<string> {
141
+ const podName = await this.resolvePod();
142
+ const args = this.buildExecArgs(podName, command);
143
+ return this.kubectl(args);
144
+ }
145
+
146
+ private async execWithStdin(command: string[], input: string): Promise<string> {
147
+ const podName = await this.resolvePod();
148
+ const args = this.buildExecStdinArgs(podName, command);
149
+ return this.kubectlWithStdin(args, input);
150
+ }
151
+
152
+ private async execCli(subArgs: string[]): Promise<string> {
153
+ return this.exec([this.paths.cliPath, ...subArgs]);
154
+ }
155
+
156
+ private async execSessions(subArgs: string[]): Promise<string> {
157
+ return this.exec([this.paths.sessionsPath, ...subArgs]);
158
+ }
159
+
160
+ private pushSessionGetOptions(args: string[], options?: SessionGetOptions): void {
161
+ if (!options) return;
162
+ if (options.persistent) {
163
+ args.push("--persistent");
164
+ }
165
+ if (options.hash) {
166
+ args.push("--hash");
167
+ }
168
+ if (options.refreshTokens) {
169
+ args.push("--refresh-tokens");
170
+ }
171
+ if (options.backend) {
172
+ args.push("--backend", options.backend);
173
+ }
174
+ }
175
+
176
+ // Config methods
177
+ async configInfo(): Promise<ConfigInfo> {
178
+ const output = await this.execCli(["info"]);
179
+ const lines = output.trim().split("\n");
180
+ const data: Record<string, string> = {};
181
+ for (const line of lines) {
182
+ const match = line.match(/^(\S+)\s*:\s*(.*)$/);
183
+ if (match) {
184
+ data[match[1]] = match[2];
185
+ }
186
+ }
187
+ return {
188
+ cfgNum: parseInt(data["Num"] || "0", 10),
189
+ cfgAuthor: data["Author"] || "",
190
+ cfgDate: data["Date"] || "",
191
+ cfgLog: data["Log"],
192
+ };
193
+ }
194
+
195
+ async configGet(keys: string[]): Promise<Record<string, any>> {
196
+ const output = await this.execCli(["-json", "get", ...keys]);
197
+ return JSON.parse(output);
198
+ }
199
+
200
+ async configSet(pairs: Record<string, any>, log?: string): Promise<void> {
201
+ const args: string[] = ["set", "-yes", "1"];
202
+ for (const [key, value] of Object.entries(pairs)) {
203
+ args.push(key, String(value));
204
+ }
205
+ if (log) {
206
+ args.push("-log", log);
207
+ }
208
+ await this.execCli(args);
209
+ }
210
+
211
+ async configAddKey(key: string, subkey: string, value: string): Promise<void> {
212
+ await this.execCli(["addKey", key, subkey, value]);
213
+ }
214
+
215
+ async configDelKey(key: string, subkey: string): Promise<void> {
216
+ await this.execCli(["delKey", key, subkey]);
217
+ }
218
+
219
+ async configSave(): Promise<string> {
220
+ return await this.execCli(["save"]);
221
+ }
222
+
223
+ async configRestore(json: string): Promise<void> {
224
+ await this.execWithStdin([this.paths.cliPath, "restore", "-yes", "1", "-"], json);
225
+ }
226
+
227
+ async configMerge(json: string): Promise<void> {
228
+ await this.execWithStdin([this.paths.cliPath, "merge", "-yes", "1", "-"], json);
229
+ }
230
+
231
+ async configRollback(): Promise<void> {
232
+ await this.execCli(["rollback", "-yes", "1"]);
233
+ }
234
+
235
+ async configUpdateCache(): Promise<void> {
236
+ await this.execCli(["update-cache"]);
237
+ }
238
+
239
+ async configTestEmail(destination: string): Promise<void> {
240
+ await this.execCli(["test-email", destination]);
241
+ }
242
+
243
+ // Session methods
244
+ async sessionGet(id: string, options?: SessionGetOptions): Promise<Record<string, any>> {
245
+ const args = ["get", id];
246
+ this.pushSessionGetOptions(args, options);
247
+ const output = await this.execSessions(args);
248
+ return JSON.parse(output);
249
+ }
250
+
251
+ async sessionSearch(filters: SessionFilter): Promise<any[]> {
252
+ const args: string[] = ["search"];
253
+ if (filters.where) {
254
+ for (const [field, value] of Object.entries(filters.where)) {
255
+ args.push("--where", `${field}=${value}`);
256
+ }
257
+ }
258
+ if (filters.select && filters.select.length > 0) {
259
+ args.push("--select", filters.select.join(","));
260
+ }
261
+ if (filters.backend) {
262
+ args.push("--backend", filters.backend);
263
+ }
264
+ if (filters.count) {
265
+ args.push("--count");
266
+ }
267
+ if (filters.refreshTokens) {
268
+ args.push("--refresh-tokens");
269
+ }
270
+ if (filters.persistent) {
271
+ args.push("--persistent");
272
+ }
273
+ if (filters.hash) {
274
+ args.push("--hash");
275
+ }
276
+ if (filters.idOnly) {
277
+ args.push("--id-only");
278
+ }
279
+ const output = await this.execSessions(args);
280
+ return JSON.parse(output);
281
+ }
282
+
283
+ async sessionDelete(ids: string[], options?: SessionDeleteOptions): Promise<void> {
284
+ if (options?.where) {
285
+ // Where-based deletion uses lemonldap-ng-sessions delete
286
+ const args: string[] = ["delete"];
287
+ for (const [field, value] of Object.entries(options.where)) {
288
+ args.push("--where", `${field}=${value}`);
289
+ }
290
+ this.pushSessionGetOptions(args, options);
291
+ await this.execSessions(args);
292
+ } else {
293
+ // ID-based deletion uses llngDeleteSession
294
+ const deleteScriptPath = this.paths.cliPath.replace("lemonldap-ng-cli", "llngDeleteSession");
295
+ for (const id of ids) {
296
+ const args = [deleteScriptPath, id];
297
+ this.pushSessionGetOptions(args, options);
298
+ await this.exec(args);
299
+ }
300
+ }
301
+ }
302
+
303
+ async sessionSetKey(
304
+ id: string,
305
+ pairs: Record<string, any>,
306
+ options?: SessionGetOptions,
307
+ ): Promise<void> {
308
+ const args: string[] = ["setKey", id];
309
+ for (const [key, value] of Object.entries(pairs)) {
310
+ args.push(key, String(value));
311
+ }
312
+ this.pushSessionGetOptions(args, options);
313
+ await this.execSessions(args);
314
+ }
315
+
316
+ async sessionDelKey(id: string, keys: string[], options?: SessionGetOptions): Promise<void> {
317
+ const args: string[] = ["delKey", id, ...keys];
318
+ this.pushSessionGetOptions(args, options);
319
+ await this.execSessions(args);
320
+ }
321
+
322
+ async sessionBackup(
323
+ backend?: string,
324
+ refreshTokens?: boolean,
325
+ persistent?: boolean,
326
+ ): Promise<string> {
327
+ const args = ["search"];
328
+ if (backend) {
329
+ args.push("--backend", backend);
330
+ }
331
+ if (refreshTokens) {
332
+ args.push("--refresh-tokens");
333
+ }
334
+ if (persistent) {
335
+ args.push("--persistent");
336
+ }
337
+ const output = await this.execSessions(args);
338
+ return output;
339
+ }
340
+
341
+ // 2FA methods
342
+ async secondFactorsGet(_user: string): Promise<any[]> {
343
+ throw new Error("secondFactorsGet is not supported via CLI. Use API mode.");
344
+ }
345
+
346
+ async secondFactorsDelete(_user: string, _ids: string[]): Promise<void> {
347
+ throw new Error("secondFactorsDelete is not supported via CLI. Use API mode.");
348
+ }
349
+
350
+ async secondFactorsDelType(_user: string, _type: string): Promise<void> {
351
+ throw new Error("secondFactorsDelType is not supported via CLI. Use API mode.");
352
+ }
353
+
354
+ // Consents methods
355
+ async consentsGet(_user: string): Promise<any[]> {
356
+ throw new Error("consentsGet is not supported via CLI. Use API mode.");
357
+ }
358
+
359
+ async consentsDelete(_user: string, _ids: string[]): Promise<void> {
360
+ throw new Error("consentsDelete is not supported via CLI. Use API mode.");
361
+ }
362
+
363
+ async execScript(scriptName: string, args: string[]): Promise<string> {
364
+ const prefix = this.config.binPrefix || "/usr/share/lemonldap-ng/bin";
365
+ return this.exec([`${prefix}/${scriptName}`, ...args]);
366
+ }
367
+ }
@@ -0,0 +1,105 @@
1
+ import { LlngMultiConfig, LlngInstanceConfig, LlngConfig, OidcConfig } from "../config.js";
2
+ import { ILlngTransport } from "./interface.js";
3
+ import { ApiTransport } from "./api.js";
4
+ import { SshTransport } from "./ssh.js";
5
+ import { K8sTransport } from "./k8s.js";
6
+
7
+ export type TransportRole = "portal" | "manager";
8
+
9
+ interface TransportEntry {
10
+ portal: ILlngTransport;
11
+ manager?: ILlngTransport;
12
+ managerResolved?: boolean;
13
+ }
14
+
15
+ export class TransportRegistry {
16
+ private transports = new Map<string, TransportEntry>();
17
+ private configs: Record<string, LlngInstanceConfig>;
18
+ private defaultInstance: string;
19
+
20
+ constructor(multiConfig: LlngMultiConfig) {
21
+ this.configs = multiConfig.instances;
22
+ this.defaultInstance = multiConfig.default;
23
+ }
24
+
25
+ private resolveInstance(instance?: string): { name: string; config: LlngInstanceConfig } {
26
+ const name = instance || this.defaultInstance;
27
+ const config = this.configs[name];
28
+ if (!config) {
29
+ throw new Error(
30
+ `Unknown instance '${name}'. Available instances: ${Object.keys(this.configs).join(", ")}`,
31
+ );
32
+ }
33
+ return { name, config };
34
+ }
35
+
36
+ private buildTransport(config: LlngConfig): ILlngTransport {
37
+ if (config.mode === "api") {
38
+ if (!config.api) {
39
+ throw new Error(`API mode requires 'api' configuration`);
40
+ }
41
+ return new ApiTransport(config.api);
42
+ } else if (config.mode === "k8s") {
43
+ if (!config.k8s) {
44
+ throw new Error(`K8s mode requires 'k8s' configuration`);
45
+ }
46
+ return new K8sTransport(config.k8s);
47
+ } else {
48
+ return new SshTransport(config.ssh ?? {});
49
+ }
50
+ }
51
+
52
+ private buildManagerConfig(config: LlngInstanceConfig): LlngConfig | undefined {
53
+ if (!config.manager) return undefined;
54
+ const m = config.manager;
55
+ const merged: LlngConfig = {
56
+ mode: m.mode ?? config.mode,
57
+ };
58
+ // Deep merge ssh: parent ssh + manager ssh overrides
59
+ if (config.mode === "ssh" || merged.mode === "ssh") {
60
+ merged.ssh = { ...config.ssh, ...m.ssh };
61
+ }
62
+ if (m.api) merged.api = m.api;
63
+ else if (config.api && merged.mode === "api") merged.api = config.api;
64
+ if (m.k8s) merged.k8s = m.k8s;
65
+ else if (config.k8s && merged.mode === "k8s") merged.k8s = config.k8s;
66
+ return merged;
67
+ }
68
+
69
+ getTransport(instance?: string, role?: TransportRole): ILlngTransport {
70
+ const { name, config } = this.resolveInstance(instance);
71
+
72
+ let entry = this.transports.get(name);
73
+ if (!entry) {
74
+ const portal = this.buildTransport(config);
75
+ entry = { portal };
76
+ this.transports.set(name, entry);
77
+ }
78
+
79
+ if (role === "manager") {
80
+ if (!entry.managerResolved) {
81
+ const managerConfig = this.buildManagerConfig(config);
82
+ entry.manager = managerConfig ? this.buildTransport(managerConfig) : undefined;
83
+ entry.managerResolved = true;
84
+ }
85
+ if (entry.manager) {
86
+ return entry.manager;
87
+ }
88
+ }
89
+ return entry.portal;
90
+ }
91
+
92
+ getOidcConfig(instance?: string): OidcConfig | undefined {
93
+ const { config } = this.resolveInstance(instance);
94
+ return config.oidc;
95
+ }
96
+
97
+ listInstances(): { name: string; mode: string; isDefault: boolean; hasManager: boolean }[] {
98
+ return Object.entries(this.configs).map(([name, config]) => ({
99
+ name,
100
+ mode: config.mode,
101
+ isDefault: name === this.defaultInstance,
102
+ hasManager: !!config.manager,
103
+ }));
104
+ }
105
+ }