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,430 @@
1
+ import { spawn } from "child_process";
2
+ import { SshConfig, resolvePaths } from "../config.js";
3
+ import {
4
+ ILlngTransport,
5
+ SessionFilter,
6
+ ConfigInfo,
7
+ SessionGetOptions,
8
+ SessionDeleteOptions,
9
+ } from "./interface.js";
10
+
11
+ export class SshTransport implements ILlngTransport {
12
+ private paths: { cliPath: string; sessionsPath: string; configEditorPath: string };
13
+
14
+ constructor(private config: SshConfig) {
15
+ this.paths = resolvePaths(
16
+ config.binPrefix,
17
+ config.cliPath,
18
+ config.sessionsPath,
19
+ config.configEditorPath,
20
+ );
21
+ }
22
+
23
+ private async exec(args: string[], env?: Record<string, string>): Promise<string> {
24
+ return new Promise((resolve, reject) => {
25
+ let cmd: string;
26
+ let cmdArgs: string[];
27
+
28
+ if (this.config.host) {
29
+ // SSH mode
30
+ cmd = "ssh";
31
+ cmdArgs = [];
32
+
33
+ if (this.config.port) {
34
+ cmdArgs.push("-p", this.config.port.toString());
35
+ }
36
+
37
+ const hostSpec = this.config.user
38
+ ? `${this.config.user}@${this.config.host}`
39
+ : this.config.host;
40
+ cmdArgs.push(hostSpec);
41
+
42
+ // Build the remote command
43
+ let remoteCmd = args.map((arg) => this.shellQuote(arg)).join(" ");
44
+
45
+ // If env vars are provided, prefix with env command
46
+ if (env) {
47
+ const envPrefix = Object.entries(env)
48
+ .map(([k, v]) => `${k}=${this.shellQuote(v)}`)
49
+ .join(" ");
50
+ remoteCmd = `env ${envPrefix} ${remoteCmd}`;
51
+ }
52
+
53
+ // Insert remoteCommand between sudo and the LLNG command
54
+ if (this.config.remoteCommand) {
55
+ remoteCmd = `${this.config.remoteCommand} ${remoteCmd}`;
56
+ }
57
+
58
+ if (this.config.sudo) {
59
+ remoteCmd = `sudo -u ${this.shellQuote(this.config.sudo)} ${remoteCmd}`;
60
+ }
61
+
62
+ cmdArgs.push(remoteCmd);
63
+ } else {
64
+ // Local mode
65
+ if (this.config.sudo) {
66
+ cmd = "sudo";
67
+ cmdArgs = ["-u", this.config.sudo];
68
+ if (this.config.remoteCommand) {
69
+ cmdArgs.push(...this.config.remoteCommand.split(" "), ...args);
70
+ } else {
71
+ cmdArgs.push(...args);
72
+ }
73
+ } else if (this.config.remoteCommand) {
74
+ const parts = this.config.remoteCommand.split(" ");
75
+ cmd = parts[0];
76
+ cmdArgs = [...parts.slice(1), ...args];
77
+ } else {
78
+ cmd = args[0];
79
+ cmdArgs = args.slice(1);
80
+ }
81
+ }
82
+
83
+ const spawnOpts = env ? { env: { ...process.env, ...env } } : undefined;
84
+ const proc = spawn(cmd, cmdArgs, spawnOpts);
85
+ let stdout = "";
86
+
87
+ proc.stdout.on("data", (data) => {
88
+ stdout += data.toString();
89
+ });
90
+
91
+ proc.stderr.on("data", () => {
92
+ // stderr is consumed but not exposed to clients for security
93
+ });
94
+
95
+ proc.on("close", (code) => {
96
+ if (code !== 0) {
97
+ reject(new Error(`Command failed with exit code ${code}`));
98
+ } else {
99
+ resolve(stdout);
100
+ }
101
+ });
102
+
103
+ proc.on("error", () => {
104
+ reject(new Error("Command execution failed"));
105
+ });
106
+ });
107
+ }
108
+
109
+ private async execWithStdin(args: string[], input: string): Promise<string> {
110
+ return new Promise((resolve, reject) => {
111
+ let cmd: string;
112
+ let cmdArgs: string[];
113
+
114
+ if (this.config.host) {
115
+ // SSH mode
116
+ cmd = "ssh";
117
+ cmdArgs = [];
118
+
119
+ if (this.config.port) {
120
+ cmdArgs.push("-p", this.config.port.toString());
121
+ }
122
+
123
+ const hostSpec = this.config.user
124
+ ? `${this.config.user}@${this.config.host}`
125
+ : this.config.host;
126
+ cmdArgs.push(hostSpec);
127
+
128
+ // Build the remote command
129
+ let remoteCmd = args.map((arg) => this.shellQuote(arg)).join(" ");
130
+
131
+ if (this.config.remoteCommand) {
132
+ remoteCmd = `${this.config.remoteCommand} ${remoteCmd}`;
133
+ }
134
+
135
+ if (this.config.sudo) {
136
+ remoteCmd = `sudo -u ${this.shellQuote(this.config.sudo)} ${remoteCmd}`;
137
+ }
138
+
139
+ cmdArgs.push(remoteCmd);
140
+ } else {
141
+ // Local mode
142
+ if (this.config.sudo) {
143
+ cmd = "sudo";
144
+ cmdArgs = ["-u", this.config.sudo];
145
+ if (this.config.remoteCommand) {
146
+ cmdArgs.push(...this.config.remoteCommand.split(" "), ...args);
147
+ } else {
148
+ cmdArgs.push(...args);
149
+ }
150
+ } else if (this.config.remoteCommand) {
151
+ const parts = this.config.remoteCommand.split(" ");
152
+ cmd = parts[0];
153
+ cmdArgs = [...parts.slice(1), ...args];
154
+ } else {
155
+ cmd = args[0];
156
+ cmdArgs = args.slice(1);
157
+ }
158
+ }
159
+
160
+ const proc = spawn(cmd, cmdArgs);
161
+ let stdout = "";
162
+
163
+ proc.stdout.on("data", (data) => {
164
+ stdout += data.toString();
165
+ });
166
+
167
+ proc.stderr.on("data", () => {
168
+ // stderr is consumed but not exposed to clients for security
169
+ });
170
+
171
+ proc.on("close", (code) => {
172
+ if (code !== 0) {
173
+ reject(new Error(`Command failed with exit code ${code}`));
174
+ } else {
175
+ resolve(stdout);
176
+ }
177
+ });
178
+
179
+ proc.on("error", () => {
180
+ reject(new Error("Command execution failed"));
181
+ });
182
+
183
+ // Write input to stdin
184
+ proc.stdin.write(input);
185
+ proc.stdin.end();
186
+ });
187
+ }
188
+
189
+ private shellQuote(arg: string): string {
190
+ // Simple shell quoting - escape single quotes and wrap in single quotes
191
+ return `'${arg.replace(/'/g, "'\\''")}'`;
192
+ }
193
+
194
+ private async execCli(subArgs: string[]): Promise<string> {
195
+ return this.exec([this.paths.cliPath, ...subArgs]);
196
+ }
197
+
198
+ private async execSessions(subArgs: string[]): Promise<string> {
199
+ return this.exec([this.paths.sessionsPath, ...subArgs]);
200
+ }
201
+
202
+ private async execConfigEditor(subArgs: string[]): Promise<string> {
203
+ return this.exec([this.paths.configEditorPath, ...subArgs], { EDITOR: "cat" });
204
+ }
205
+
206
+ private pushSessionGetOptions(args: string[], options?: SessionGetOptions): void {
207
+ if (!options) return;
208
+ if (options.persistent) {
209
+ args.push("--persistent");
210
+ }
211
+ if (options.hash) {
212
+ args.push("--hash");
213
+ }
214
+ if (options.refreshTokens) {
215
+ args.push("--refresh-tokens");
216
+ }
217
+ if (options.backend) {
218
+ args.push("--backend", options.backend);
219
+ }
220
+ }
221
+
222
+ // Config methods
223
+ async configInfo(): Promise<ConfigInfo> {
224
+ const output = await this.execCli(["info"]);
225
+ // Parse text output like "Num : 1\nAuthor : The LemonLDAP::NG team\n..."
226
+ const lines = output.trim().split("\n");
227
+ const data: Record<string, string> = {};
228
+ for (const line of lines) {
229
+ const match = line.match(/^(\S+)\s*:\s*(.*)$/);
230
+ if (match) {
231
+ data[match[1]] = match[2];
232
+ }
233
+ }
234
+ return {
235
+ cfgNum: parseInt(data["Num"] || "0", 10),
236
+ cfgAuthor: data["Author"] || "",
237
+ cfgDate: data["Date"] || "",
238
+ cfgLog: data["Log"],
239
+ };
240
+ }
241
+
242
+ async configGet(keys: string[]): Promise<Record<string, any>> {
243
+ const output = await this.execCli(["-json", "get", ...keys]);
244
+ return JSON.parse(output);
245
+ }
246
+
247
+ async configSet(pairs: Record<string, any>, log?: string): Promise<void> {
248
+ const args: string[] = ["set", "-yes", "1"];
249
+
250
+ for (const [key, value] of Object.entries(pairs)) {
251
+ args.push(key, String(value));
252
+ }
253
+
254
+ if (log) {
255
+ args.push("-log", log);
256
+ }
257
+
258
+ await this.execCli(args);
259
+ }
260
+
261
+ async configAddKey(key: string, subkey: string, value: string): Promise<void> {
262
+ await this.execCli(["addKey", key, subkey, value]);
263
+ }
264
+
265
+ async configDelKey(key: string, subkey: string): Promise<void> {
266
+ await this.execCli(["delKey", key, subkey]);
267
+ }
268
+
269
+ async configSave(): Promise<string> {
270
+ return await this.execCli(["save"]);
271
+ }
272
+
273
+ async configRestore(json: string): Promise<void> {
274
+ await this.execWithStdin([this.paths.cliPath, "restore", "-yes", "1", "-"], json);
275
+ }
276
+
277
+ async configMerge(json: string): Promise<void> {
278
+ await this.execWithStdin([this.paths.cliPath, "merge", "-yes", "1", "-"], json);
279
+ }
280
+
281
+ async configRollback(): Promise<void> {
282
+ await this.execCli(["rollback", "-yes", "1"]);
283
+ }
284
+
285
+ async configUpdateCache(): Promise<void> {
286
+ await this.execCli(["update-cache"]);
287
+ }
288
+
289
+ async configTestEmail(destination: string): Promise<void> {
290
+ await this.execCli(["test-email", destination]);
291
+ }
292
+
293
+ // Session methods
294
+ async sessionGet(id: string, options?: SessionGetOptions): Promise<Record<string, any>> {
295
+ const args = ["get", id];
296
+ this.pushSessionGetOptions(args, options);
297
+
298
+ const output = await this.execSessions(args);
299
+ return JSON.parse(output);
300
+ }
301
+
302
+ async sessionSearch(filters: SessionFilter): Promise<any[]> {
303
+ const args: string[] = ["search"];
304
+
305
+ if (filters.where) {
306
+ for (const [field, value] of Object.entries(filters.where)) {
307
+ args.push("--where", `${field}=${value}`);
308
+ }
309
+ }
310
+
311
+ if (filters.select && filters.select.length > 0) {
312
+ args.push("--select", filters.select.join(","));
313
+ }
314
+
315
+ if (filters.backend) {
316
+ args.push("--backend", filters.backend);
317
+ }
318
+
319
+ if (filters.count) {
320
+ args.push("--count");
321
+ }
322
+
323
+ if (filters.refreshTokens) {
324
+ args.push("--refresh-tokens");
325
+ }
326
+
327
+ if (filters.persistent) {
328
+ args.push("--persistent");
329
+ }
330
+
331
+ if (filters.hash) {
332
+ args.push("--hash");
333
+ }
334
+
335
+ if (filters.idOnly) {
336
+ args.push("--id-only");
337
+ }
338
+
339
+ const output = await this.execSessions(args);
340
+ return JSON.parse(output);
341
+ }
342
+
343
+ async sessionDelete(ids: string[], options?: SessionDeleteOptions): Promise<void> {
344
+ if (options?.where) {
345
+ // Where-based deletion uses lemonldap-ng-sessions delete
346
+ const args: string[] = ["delete"];
347
+ for (const [field, value] of Object.entries(options.where)) {
348
+ args.push("--where", `${field}=${value}`);
349
+ }
350
+ this.pushSessionGetOptions(args, options);
351
+ await this.execSessions(args);
352
+ } else {
353
+ // ID-based deletion uses llngDeleteSession
354
+ const deleteScriptPath =
355
+ this.config.deleteSessionPath ||
356
+ this.paths.cliPath.replace("lemonldap-ng-cli", "llngDeleteSession");
357
+ for (const id of ids) {
358
+ const args = [deleteScriptPath, id];
359
+ this.pushSessionGetOptions(args, options);
360
+ await this.exec(args);
361
+ }
362
+ }
363
+ }
364
+
365
+ async sessionSetKey(
366
+ id: string,
367
+ pairs: Record<string, any>,
368
+ options?: SessionGetOptions,
369
+ ): Promise<void> {
370
+ const args: string[] = ["setKey", id];
371
+ for (const [key, value] of Object.entries(pairs)) {
372
+ args.push(key, String(value));
373
+ }
374
+ this.pushSessionGetOptions(args, options);
375
+ await this.execSessions(args);
376
+ }
377
+
378
+ async sessionDelKey(id: string, keys: string[], options?: SessionGetOptions): Promise<void> {
379
+ const args: string[] = ["delKey", id, ...keys];
380
+ this.pushSessionGetOptions(args, options);
381
+ await this.execSessions(args);
382
+ }
383
+
384
+ async sessionBackup(
385
+ backend?: string,
386
+ refreshTokens?: boolean,
387
+ persistent?: boolean,
388
+ ): Promise<string> {
389
+ // Return all sessions as JSON via search with no filters
390
+ const args = ["search"];
391
+ if (backend) {
392
+ args.push("--backend", backend);
393
+ }
394
+ if (refreshTokens) {
395
+ args.push("--refresh-tokens");
396
+ }
397
+ if (persistent) {
398
+ args.push("--persistent");
399
+ }
400
+ const output = await this.execSessions(args);
401
+ return output;
402
+ }
403
+
404
+ // 2FA methods
405
+ async secondFactorsGet(_user: string): Promise<any[]> {
406
+ throw new Error("secondFactorsGet is not supported via CLI. Use API mode.");
407
+ }
408
+
409
+ async secondFactorsDelete(_user: string, _ids: string[]): Promise<void> {
410
+ throw new Error("secondFactorsDelete is not supported via CLI. Use API mode.");
411
+ }
412
+
413
+ async secondFactorsDelType(_user: string, _type: string): Promise<void> {
414
+ throw new Error("secondFactorsDelType is not supported via CLI. Use API mode.");
415
+ }
416
+
417
+ // Consents methods
418
+ async consentsGet(_user: string): Promise<any[]> {
419
+ throw new Error("consentsGet is not supported via CLI. Use API mode.");
420
+ }
421
+
422
+ async consentsDelete(_user: string, _ids: string[]): Promise<void> {
423
+ throw new Error("consentsDelete is not supported via CLI. Use API mode.");
424
+ }
425
+
426
+ async execScript(scriptName: string, args: string[]): Promise<string> {
427
+ const prefix = this.config.binPrefix || "/usr/share/lemonldap-ng/bin";
428
+ return this.exec([`${prefix}/${scriptName}`, ...args]);
429
+ }
430
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/__tests__/*.test.ts'],
6
+ exclude: ['src/__tests__/integration/**'],
7
+ },
8
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/__tests__/integration/**/*.test.ts'],
6
+ testTimeout: 30000,
7
+ hookTimeout: 60000,
8
+ },
9
+ });