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,229 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+
3
+ const BASE_URL = process.env.LLNG_TEST_URL || "http://localhost:19876";
4
+
5
+ describe("API Transport Integration", () => {
6
+ let available = false;
7
+ let sessionCookie = "";
8
+
9
+ async function authenticate(): Promise<string | null> {
10
+ try {
11
+ // First, get the login form to extract CSRF token
12
+ const formResp = await fetch(`${BASE_URL}/`, {
13
+ headers: {
14
+ Host: "auth.example.com",
15
+ },
16
+ });
17
+
18
+ if (!formResp.ok) {
19
+ console.log("Failed to fetch login form:", formResp.status);
20
+ return null;
21
+ }
22
+
23
+ const html = await formResp.text();
24
+
25
+ // Extract CSRF token from the form
26
+ const tokenMatch = html.match(/name="token"\s+value="([^"]+)"/);
27
+ if (!tokenMatch) {
28
+ console.log("No CSRF token found in login form");
29
+ return null;
30
+ }
31
+
32
+ const token = tokenMatch[1];
33
+ if (!token) {
34
+ console.log("CSRF token is empty");
35
+ return null;
36
+ }
37
+
38
+ // Authenticate to portal as dwho/dwho (default demo user)
39
+ const resp = await fetch(`${BASE_URL}/`, {
40
+ method: "POST",
41
+ headers: {
42
+ Host: "auth.example.com",
43
+ "Content-Type": "application/x-www-form-urlencoded",
44
+ },
45
+ body: `user=dwho&password=dwho&token=${token}`,
46
+ redirect: "manual", // Don't follow redirects
47
+ });
48
+
49
+ // Should get 302 redirect with session cookie
50
+ if (resp.status !== 302 && resp.status !== 200) {
51
+ console.log("Auth failed with status:", resp.status);
52
+ return null;
53
+ }
54
+
55
+ // Extract lemonldap cookie
56
+ const setCookie = resp.headers.get("set-cookie");
57
+ if (!setCookie) {
58
+ console.log("No set-cookie header received");
59
+ return null;
60
+ }
61
+
62
+ const match = setCookie.match(/lemonldap=([^;]+)/);
63
+ if (!match) {
64
+ console.log("No lemonldap cookie found in:", setCookie);
65
+ return null;
66
+ }
67
+
68
+ return match[1];
69
+ } catch (e: unknown) {
70
+ console.log("Authentication error:", e instanceof Error ? e.message : String(e));
71
+ return null;
72
+ }
73
+ }
74
+
75
+ beforeAll(async () => {
76
+ // Check if LLNG portal is available and authenticate
77
+ try {
78
+ const resp = await fetch(`${BASE_URL}/`, {
79
+ headers: {
80
+ Host: "auth.example.com",
81
+ },
82
+ });
83
+
84
+ if (!resp.ok && resp.status !== 302) {
85
+ available = false;
86
+ return;
87
+ }
88
+
89
+ // Try to authenticate
90
+ const cookie = await authenticate();
91
+ if (cookie) {
92
+ sessionCookie = cookie;
93
+ available = true;
94
+ } else {
95
+ available = false;
96
+ }
97
+ } catch {
98
+ available = false;
99
+ }
100
+ });
101
+
102
+ it("should detect LLNG portal availability", () => {
103
+ if (!available) {
104
+ console.log("LLNG portal not available or auth failed - skipping API tests");
105
+ }
106
+ // Don't fail - just report availability
107
+ });
108
+
109
+ it("should access portal login page", async () => {
110
+ if (!available) return;
111
+
112
+ try {
113
+ const resp = await fetch(`${BASE_URL}/`, {
114
+ headers: {
115
+ Host: "auth.example.com",
116
+ },
117
+ });
118
+
119
+ expect(resp.ok).toBe(true);
120
+ const html = await resp.text();
121
+
122
+ // Should contain login form
123
+ expect(html).toContain("lemonldap-ng");
124
+ } catch (e: unknown) {
125
+ console.log("Portal access failed:", e instanceof Error ? e.message : String(e));
126
+ throw e;
127
+ }
128
+ });
129
+
130
+ it("should authenticate as demo user", async () => {
131
+ if (!available) return;
132
+
133
+ expect(sessionCookie).toBeTruthy();
134
+ expect(sessionCookie.length).toBeGreaterThan(0);
135
+ });
136
+
137
+ it("should access manager API with session cookie", async () => {
138
+ if (!available) return;
139
+
140
+ try {
141
+ const resp = await fetch(`${BASE_URL}/confs/latest`, {
142
+ headers: {
143
+ Host: "manager.example.com",
144
+ Cookie: `lemonldap=${sessionCookie}`,
145
+ },
146
+ });
147
+
148
+ // Manager API requires specific group membership (e.g., "timelords" group)
149
+ // The demo user dwho might not have manager permissions by default
150
+ if (resp.status === 403) {
151
+ console.log(
152
+ "Manager API returned 403 - user lacks required permissions (expected for demo user)",
153
+ );
154
+ return; // Skip test gracefully
155
+ }
156
+
157
+ if (!resp.ok) {
158
+ const text = await resp.text();
159
+ console.log("Manager API response:", resp.status, text);
160
+ throw new Error(`Manager API returned ${resp.status}`);
161
+ }
162
+
163
+ const config = await resp.json();
164
+
165
+ // Should have cfgNum
166
+ expect(config.cfgNum).toBeDefined();
167
+ const cfgNum = typeof config.cfgNum === "string" ? parseInt(config.cfgNum) : config.cfgNum;
168
+ expect(cfgNum).toBeGreaterThan(0);
169
+
170
+ // Should have basic config keys
171
+ expect(config.portal).toBeDefined();
172
+ expect(config.domain).toBeDefined();
173
+ } catch (e: unknown) {
174
+ console.log("Manager API access failed:", e instanceof Error ? e.message : String(e));
175
+ throw e;
176
+ }
177
+ });
178
+
179
+ it("should access manager REST API endpoints", async () => {
180
+ if (!available) return;
181
+
182
+ try {
183
+ // Test accessing configuration list
184
+ const resp = await fetch(`${BASE_URL}/confs`, {
185
+ headers: {
186
+ Host: "manager.example.com",
187
+ Cookie: `lemonldap=${sessionCookie}`,
188
+ },
189
+ });
190
+
191
+ // Manager REST API also requires permissions
192
+ if (resp.status === 403) {
193
+ console.log(
194
+ "REST API returned 403 - user lacks required permissions (expected for demo user)",
195
+ );
196
+ return;
197
+ }
198
+
199
+ if (!resp.ok) {
200
+ const text = await resp.text();
201
+ console.log("REST API response:", resp.status, text);
202
+ // Don't fail - might not be available in all deployments
203
+ return;
204
+ }
205
+
206
+ const data = await resp.json();
207
+ expect(Array.isArray(data) || typeof data === "object").toBe(true);
208
+ } catch (e: unknown) {
209
+ console.log("REST API access failed:", e instanceof Error ? e.message : String(e));
210
+ // Don't fail - REST API might not be enabled
211
+ }
212
+ });
213
+
214
+ it("should demonstrate that manager API requires authorization", async () => {
215
+ if (!available) return;
216
+
217
+ // Document the authorization requirements for the manager API
218
+ // In a real deployment, you would need:
219
+ // 1. A user account with manager permissions (e.g., member of "timelords" group)
220
+ // 2. Proper session cookie after authentication
221
+ // 3. Correct Host header for the manager vhost
222
+ //
223
+ // For testing purposes, SSH transport via docker exec is more reliable
224
+ // since CLI commands bypass web-based authorization checks
225
+ console.log(
226
+ 'Manager API requires group membership (e.g., inGroup("timelords")) or specific user access',
227
+ );
228
+ });
229
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { execSync } from "child_process";
3
+
4
+ describe("OIDC RP Integration (via docker exec)", () => {
5
+ let available = false;
6
+ let containerName = "";
7
+
8
+ function dockerExec(cmd: string): string {
9
+ return execSync(`docker exec ${containerName} ${cmd}`, {
10
+ encoding: "utf-8",
11
+ stdio: ["pipe", "pipe", "ignore"], // capture stdout, ignore stderr
12
+ }).trim();
13
+ }
14
+
15
+ function dockerBash(script: string): string {
16
+ return dockerExec(`bash -c '${script.replace(/'/g, "'\\''")}'`);
17
+ }
18
+
19
+ beforeAll(async () => {
20
+ // Detect container name dynamically
21
+ try {
22
+ const output = execSync(
23
+ `docker ps --filter ancestor=yadd/lemonldap-ng-full --format '{{.Names}}'`,
24
+ { encoding: "utf-8" },
25
+ );
26
+ const name = output.trim().split("\n")[0];
27
+ if (name) {
28
+ containerName = name;
29
+ available = true;
30
+ }
31
+ } catch {
32
+ available = false;
33
+ }
34
+
35
+ if (!available) return;
36
+
37
+ // Set up OIDC RP configuration
38
+ try {
39
+ // Add hosts entry for auth.example.com
40
+ dockerBash("echo '127.0.0.1 auth.example.com' >> /etc/hosts");
41
+
42
+ // Enable OIDC issuer
43
+ dockerBash("lemonldap-ng-cli -yes 1 set issuerDBOpenIDConnectActivation 1 2>/dev/null");
44
+
45
+ // Generate signing keys
46
+ dockerBash("/usr/share/lemonldap-ng/bin/rotateOidcKeys 2>/dev/null");
47
+
48
+ // Create RP via CLI merge
49
+ const rpConfig = JSON.stringify({
50
+ oidcRPMetaDataOptions: {
51
+ "integ-test-rp": {
52
+ oidcRPMetaDataOptionsClientID: "integ-test-client",
53
+ oidcRPMetaDataOptionsRedirectUris: "http://localhost/callback",
54
+ oidcRPMetaDataOptionsPublic: 1,
55
+ oidcRPMetaDataOptionsBypassConsent: 1,
56
+ oidcRPMetaDataOptionsIDTokenSignAlg: "RS256",
57
+ },
58
+ },
59
+ oidcRPMetaDataExportedVars: {
60
+ "integ-test-rp": {
61
+ name: "cn",
62
+ preferred_username: "uid",
63
+ email: "mail",
64
+ },
65
+ },
66
+ });
67
+ dockerBash(
68
+ `echo '${rpConfig.replace(/'/g, "'\\''")}' | lemonldap-ng-cli -yes 1 merge - 2>/dev/null`,
69
+ );
70
+ } catch (e: unknown) {
71
+ console.log("OIDC setup failed:", e instanceof Error ? e.message : String(e));
72
+ available = false;
73
+ }
74
+ });
75
+
76
+ afterAll(async () => {
77
+ if (!available) return;
78
+
79
+ // Cleanup: remove RP configuration
80
+ try {
81
+ dockerBash("lemonldap-ng-cli -yes 1 delKey oidcRPMetaDataOptions integ-test-rp 2>/dev/null");
82
+ dockerBash(
83
+ "lemonldap-ng-cli -yes 1 delKey oidcRPMetaDataExportedVars integ-test-rp 2>/dev/null",
84
+ );
85
+ } catch (e: unknown) {
86
+ console.log("OIDC cleanup failed:", e instanceof Error ? e.message : String(e));
87
+ }
88
+ });
89
+
90
+ it("should detect Docker container availability", () => {
91
+ if (!available) {
92
+ console.log("LLNG container not available - skipping integration tests");
93
+ }
94
+ // Don't fail - just report availability
95
+ });
96
+
97
+ it("should add OIDC RP and verify config", async () => {
98
+ if (!available) return;
99
+
100
+ try {
101
+ const output = dockerBash(
102
+ "lemonldap-ng-cli -yes 1 get oidcRPMetaDataOptions/integ-test-rp 2>/dev/null",
103
+ );
104
+
105
+ // Verify the RP contains required fields
106
+ expect(output).toContain("oidcRPMetaDataOptionsClientID");
107
+ expect(output).toContain("oidcRPMetaDataOptionsIDTokenSignAlg");
108
+ } catch (e: unknown) {
109
+ console.log("OIDC RP verification failed:", e instanceof Error ? e.message : String(e));
110
+ throw e;
111
+ }
112
+ });
113
+
114
+ it("should complete full OIDC flow via llng CLI", async () => {
115
+ if (!available) return;
116
+
117
+ try {
118
+ const flowScript = `
119
+ rm -f /root/.cache/llng-cookies && mkdir -p /root/.cache && \
120
+ llng --llng-url http://auth.example.com --login dwho --password dwho \
121
+ --client-id integ-test-client --pkce --redirect-uri "http://localhost/callback" \
122
+ user_info
123
+ `;
124
+
125
+ const output = dockerBash(flowScript);
126
+
127
+ // Parse JSON output and verify user info
128
+ const userInfo = JSON.parse(output);
129
+ expect(userInfo.sub).toBe("dwho");
130
+ expect(userInfo.email).toBe("dwho@badwolf.org");
131
+ expect(userInfo.name).toBe("Doctor Who");
132
+ expect(userInfo.preferred_username).toBe("dwho");
133
+ } catch (e: unknown) {
134
+ console.log("OIDC flow failed:", e instanceof Error ? e.message : String(e));
135
+ throw e;
136
+ }
137
+ });
138
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { execSync } from "child_process";
3
+
4
+ describe("SSH Transport Integration (via docker exec)", () => {
5
+ let available = false;
6
+ let containerName = "";
7
+
8
+ function dockerExec(cmd: string): string {
9
+ return execSync(`docker exec ${containerName} ${cmd}`, {
10
+ encoding: "utf-8",
11
+ stdio: ["pipe", "pipe", "ignore"], // capture stdout, ignore stderr
12
+ }).trim();
13
+ }
14
+
15
+ beforeAll(async () => {
16
+ // Detect container name dynamically
17
+ try {
18
+ const output = execSync(
19
+ `docker ps --filter ancestor=yadd/lemonldap-ng-full --format '{{.Names}}'`,
20
+ { encoding: "utf-8" },
21
+ );
22
+ const name = output.trim().split("\n")[0];
23
+ if (name) {
24
+ containerName = name;
25
+ available = true;
26
+ }
27
+ } catch {
28
+ available = false;
29
+ }
30
+ });
31
+
32
+ it("should detect Docker container availability", () => {
33
+ if (!available) {
34
+ console.log("LLNG container not available - skipping integration tests");
35
+ }
36
+ // Don't fail - just report availability
37
+ });
38
+
39
+ it("should get config info via info command", async () => {
40
+ if (!available) return;
41
+
42
+ try {
43
+ const output = dockerExec("lemonldap-ng-cli info");
44
+
45
+ // Parse text output - expect lines like "Num : 1"
46
+ expect(output).toContain("Num");
47
+ expect(output).toContain("Author");
48
+ expect(output).toMatch(/Num\s+:\s+\d+/);
49
+ } catch (e: unknown) {
50
+ console.log("CLI info failed:", e instanceof Error ? e.message : String(e));
51
+ throw e;
52
+ }
53
+ });
54
+
55
+ it("should get config values via get command", async () => {
56
+ if (!available) return;
57
+
58
+ try {
59
+ const output = dockerExec("lemonldap-ng-cli get portal domain");
60
+
61
+ // Parse "key = value" lines
62
+ const lines = output.split("\n");
63
+ expect(lines.length).toBeGreaterThan(0);
64
+
65
+ // Should contain portal and domain values
66
+ expect(output).toMatch(/portal\s+=\s+\S+/);
67
+ expect(output).toMatch(/domain\s+=\s+\S+/);
68
+ } catch (e: unknown) {
69
+ console.log("CLI get failed:", e instanceof Error ? e.message : String(e));
70
+ throw e;
71
+ }
72
+ });
73
+
74
+ it("should export config via save command", async () => {
75
+ if (!available) return;
76
+
77
+ try {
78
+ const output = dockerExec("lemonldap-ng-cli save");
79
+
80
+ // save outputs JSON to stdout
81
+ const config = JSON.parse(output);
82
+ expect(config.cfgNum).toBeDefined();
83
+
84
+ // cfgNum might be a string or number
85
+ const cfgNum = typeof config.cfgNum === "string" ? parseInt(config.cfgNum) : config.cfgNum;
86
+ expect(cfgNum).toBeGreaterThan(0);
87
+ } catch (e: unknown) {
88
+ console.log("CLI save failed:", e instanceof Error ? e.message : String(e));
89
+ throw e;
90
+ }
91
+ });
92
+
93
+ it.skip("should set config value via set command (requires interactive confirmation)", () => {
94
+ // The CLI 'set' command requires interactive confirmation
95
+ // and does not work non-interactively.
96
+ });
97
+
98
+ it("should search sessions via lemonldap-ng-sessions", async () => {
99
+ if (!available) return;
100
+
101
+ try {
102
+ // lemonldap-ng-sessions search returns JSON array (might be empty)
103
+ const output = dockerExec("lemonldap-ng-sessions search");
104
+
105
+ // Parse JSON - should be an array
106
+ const sessions = JSON.parse(output);
107
+ expect(Array.isArray(sessions)).toBe(true);
108
+ } catch (e: unknown) {
109
+ console.log("Sessions CLI failed:", e instanceof Error ? e.message : String(e));
110
+ throw e;
111
+ }
112
+ });
113
+ });