@openqa/cli 2.0.0 → 2.1.1

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.
@@ -0,0 +1,820 @@
1
+ // cli/env-routes.ts
2
+ import { Router } from "express";
3
+ import { readFileSync, writeFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ // cli/env-config.ts
7
+ var ENV_VARIABLES = [
8
+ // ============================================================================
9
+ // LLM CONFIGURATION
10
+ // ============================================================================
11
+ {
12
+ key: "LLM_PROVIDER",
13
+ type: "select",
14
+ category: "llm",
15
+ required: true,
16
+ description: "LLM provider to use for AI operations",
17
+ options: ["openai", "anthropic", "ollama"],
18
+ placeholder: "openai",
19
+ restartRequired: true
20
+ },
21
+ {
22
+ key: "OPENAI_API_KEY",
23
+ type: "password",
24
+ category: "llm",
25
+ required: false,
26
+ description: "OpenAI API key (required if LLM_PROVIDER=openai)",
27
+ placeholder: "sk-...",
28
+ sensitive: true,
29
+ testable: true,
30
+ validation: (value) => {
31
+ if (!value) return { valid: true };
32
+ if (!value.startsWith("sk-")) {
33
+ return { valid: false, error: 'OpenAI API key must start with "sk-"' };
34
+ }
35
+ if (value.length < 20) {
36
+ return { valid: false, error: "API key seems too short" };
37
+ }
38
+ return { valid: true };
39
+ },
40
+ restartRequired: true
41
+ },
42
+ {
43
+ key: "ANTHROPIC_API_KEY",
44
+ type: "password",
45
+ category: "llm",
46
+ required: false,
47
+ description: "Anthropic API key (required if LLM_PROVIDER=anthropic)",
48
+ placeholder: "sk-ant-...",
49
+ sensitive: true,
50
+ testable: true,
51
+ validation: (value) => {
52
+ if (!value) return { valid: true };
53
+ if (!value.startsWith("sk-ant-")) {
54
+ return { valid: false, error: 'Anthropic API key must start with "sk-ant-"' };
55
+ }
56
+ return { valid: true };
57
+ },
58
+ restartRequired: true
59
+ },
60
+ {
61
+ key: "OLLAMA_BASE_URL",
62
+ type: "url",
63
+ category: "llm",
64
+ required: false,
65
+ description: "Ollama server URL (required if LLM_PROVIDER=ollama)",
66
+ placeholder: "http://localhost:11434",
67
+ testable: true,
68
+ validation: (value) => {
69
+ if (!value) return { valid: true };
70
+ try {
71
+ new URL(value);
72
+ return { valid: true };
73
+ } catch {
74
+ return { valid: false, error: "Invalid URL format" };
75
+ }
76
+ },
77
+ restartRequired: true
78
+ },
79
+ {
80
+ key: "LLM_MODEL",
81
+ type: "text",
82
+ category: "llm",
83
+ required: false,
84
+ description: "LLM model to use (e.g., gpt-4, claude-3-opus, llama2)",
85
+ placeholder: "gpt-4",
86
+ restartRequired: true
87
+ },
88
+ // ============================================================================
89
+ // SECURITY
90
+ // ============================================================================
91
+ {
92
+ key: "OPENQA_JWT_SECRET",
93
+ type: "password",
94
+ category: "security",
95
+ required: true,
96
+ description: "Secret key for JWT token signing (min 32 characters)",
97
+ placeholder: "Generate with: openssl rand -hex 32",
98
+ sensitive: true,
99
+ validation: (value) => {
100
+ if (!value) return { valid: false, error: "JWT secret is required" };
101
+ if (value.length < 32) {
102
+ return { valid: false, error: "JWT secret must be at least 32 characters" };
103
+ }
104
+ return { valid: true };
105
+ },
106
+ restartRequired: true
107
+ },
108
+ {
109
+ key: "OPENQA_AUTH_DISABLED",
110
+ type: "boolean",
111
+ category: "security",
112
+ required: false,
113
+ description: "\u26A0\uFE0F DANGER: Disable authentication (NEVER use in production!)",
114
+ placeholder: "false",
115
+ validation: (value) => {
116
+ if (value === "true" && process.env.NODE_ENV === "production") {
117
+ return { valid: false, error: "Cannot disable auth in production!" };
118
+ }
119
+ return { valid: true };
120
+ },
121
+ restartRequired: true
122
+ },
123
+ {
124
+ key: "NODE_ENV",
125
+ type: "select",
126
+ category: "security",
127
+ required: false,
128
+ description: "Node environment (production enables security features)",
129
+ options: ["development", "production", "test"],
130
+ placeholder: "production",
131
+ restartRequired: true
132
+ },
133
+ // ============================================================================
134
+ // TARGET APPLICATION
135
+ // ============================================================================
136
+ {
137
+ key: "SAAS_URL",
138
+ type: "url",
139
+ category: "target",
140
+ required: true,
141
+ description: "URL of the application to test",
142
+ placeholder: "https://your-app.com",
143
+ testable: true,
144
+ validation: (value) => {
145
+ if (!value) return { valid: false, error: "Target URL is required" };
146
+ try {
147
+ const url = new URL(value);
148
+ if (!["http:", "https:"].includes(url.protocol)) {
149
+ return { valid: false, error: "URL must use http or https protocol" };
150
+ }
151
+ return { valid: true };
152
+ } catch {
153
+ return { valid: false, error: "Invalid URL format" };
154
+ }
155
+ }
156
+ },
157
+ {
158
+ key: "SAAS_AUTH_TYPE",
159
+ type: "select",
160
+ category: "target",
161
+ required: false,
162
+ description: "Authentication type for target application",
163
+ options: ["none", "basic", "session"],
164
+ placeholder: "none"
165
+ },
166
+ {
167
+ key: "SAAS_USERNAME",
168
+ type: "text",
169
+ category: "target",
170
+ required: false,
171
+ description: "Username for target application authentication",
172
+ placeholder: "test@example.com"
173
+ },
174
+ {
175
+ key: "SAAS_PASSWORD",
176
+ type: "password",
177
+ category: "target",
178
+ required: false,
179
+ description: "Password for target application authentication",
180
+ placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
181
+ sensitive: true
182
+ },
183
+ // ============================================================================
184
+ // GITHUB INTEGRATION
185
+ // ============================================================================
186
+ {
187
+ key: "GITHUB_TOKEN",
188
+ type: "password",
189
+ category: "github",
190
+ required: false,
191
+ description: "GitHub personal access token for issue creation",
192
+ placeholder: "ghp_...",
193
+ sensitive: true,
194
+ testable: true,
195
+ validation: (value) => {
196
+ if (!value) return { valid: true };
197
+ if (!value.startsWith("ghp_") && !value.startsWith("github_pat_")) {
198
+ return { valid: false, error: 'GitHub token must start with "ghp_" or "github_pat_"' };
199
+ }
200
+ return { valid: true };
201
+ }
202
+ },
203
+ {
204
+ key: "GITHUB_OWNER",
205
+ type: "text",
206
+ category: "github",
207
+ required: false,
208
+ description: "GitHub repository owner/organization",
209
+ placeholder: "your-username"
210
+ },
211
+ {
212
+ key: "GITHUB_REPO",
213
+ type: "text",
214
+ category: "github",
215
+ required: false,
216
+ description: "GitHub repository name",
217
+ placeholder: "your-repo"
218
+ },
219
+ {
220
+ key: "GITHUB_BRANCH",
221
+ type: "text",
222
+ category: "github",
223
+ required: false,
224
+ description: "GitHub branch to monitor",
225
+ placeholder: "main"
226
+ },
227
+ // ============================================================================
228
+ // WEB SERVER
229
+ // ============================================================================
230
+ {
231
+ key: "WEB_PORT",
232
+ type: "number",
233
+ category: "web",
234
+ required: false,
235
+ description: "Port for web server",
236
+ placeholder: "4242",
237
+ validation: (value) => {
238
+ if (!value) return { valid: true };
239
+ const port = parseInt(value, 10);
240
+ if (isNaN(port) || port < 1 || port > 65535) {
241
+ return { valid: false, error: "Port must be between 1 and 65535" };
242
+ }
243
+ return { valid: true };
244
+ },
245
+ restartRequired: true
246
+ },
247
+ {
248
+ key: "WEB_HOST",
249
+ type: "text",
250
+ category: "web",
251
+ required: false,
252
+ description: "Host to bind web server (0.0.0.0 for all interfaces)",
253
+ placeholder: "0.0.0.0",
254
+ restartRequired: true
255
+ },
256
+ {
257
+ key: "CORS_ORIGINS",
258
+ type: "text",
259
+ category: "web",
260
+ required: false,
261
+ description: "Allowed CORS origins (comma-separated)",
262
+ placeholder: "https://your-domain.com,https://app.example.com",
263
+ restartRequired: true
264
+ },
265
+ // ============================================================================
266
+ // AGENT CONFIGURATION
267
+ // ============================================================================
268
+ {
269
+ key: "AGENT_AUTO_START",
270
+ type: "boolean",
271
+ category: "agent",
272
+ required: false,
273
+ description: "Auto-start agent on server launch",
274
+ placeholder: "false"
275
+ },
276
+ {
277
+ key: "AGENT_INTERVAL_MS",
278
+ type: "number",
279
+ category: "agent",
280
+ required: false,
281
+ description: "Agent run interval in milliseconds (1 hour = 3600000)",
282
+ placeholder: "3600000",
283
+ validation: (value) => {
284
+ if (!value) return { valid: true };
285
+ const interval = parseInt(value, 10);
286
+ if (isNaN(interval) || interval < 6e4) {
287
+ return { valid: false, error: "Interval must be at least 60000ms (1 minute)" };
288
+ }
289
+ return { valid: true };
290
+ }
291
+ },
292
+ {
293
+ key: "AGENT_MAX_ITERATIONS",
294
+ type: "number",
295
+ category: "agent",
296
+ required: false,
297
+ description: "Maximum iterations per agent session",
298
+ placeholder: "20",
299
+ validation: (value) => {
300
+ if (!value) return { valid: true };
301
+ const max = parseInt(value, 10);
302
+ if (isNaN(max) || max < 1 || max > 1e3) {
303
+ return { valid: false, error: "Max iterations must be between 1 and 1000" };
304
+ }
305
+ return { valid: true };
306
+ }
307
+ },
308
+ {
309
+ key: "GIT_LISTENER_ENABLED",
310
+ type: "boolean",
311
+ category: "agent",
312
+ required: false,
313
+ description: "Enable git merge/pipeline detection",
314
+ placeholder: "true"
315
+ },
316
+ {
317
+ key: "GIT_POLL_INTERVAL_MS",
318
+ type: "number",
319
+ category: "agent",
320
+ required: false,
321
+ description: "Git polling interval in milliseconds",
322
+ placeholder: "60000"
323
+ },
324
+ // ============================================================================
325
+ // DATABASE
326
+ // ============================================================================
327
+ {
328
+ key: "DB_PATH",
329
+ type: "text",
330
+ category: "database",
331
+ required: false,
332
+ description: "Path to SQLite database file",
333
+ placeholder: "./data/openqa.db",
334
+ restartRequired: true
335
+ },
336
+ // ============================================================================
337
+ // NOTIFICATIONS
338
+ // ============================================================================
339
+ {
340
+ key: "SLACK_WEBHOOK_URL",
341
+ type: "url",
342
+ category: "notifications",
343
+ required: false,
344
+ description: "Slack webhook URL for notifications",
345
+ placeholder: "https://hooks.slack.com/services/...",
346
+ sensitive: true,
347
+ testable: true,
348
+ validation: (value) => {
349
+ if (!value) return { valid: true };
350
+ if (!value.startsWith("https://hooks.slack.com/")) {
351
+ return { valid: false, error: "Invalid Slack webhook URL" };
352
+ }
353
+ return { valid: true };
354
+ }
355
+ },
356
+ {
357
+ key: "DISCORD_WEBHOOK_URL",
358
+ type: "url",
359
+ category: "notifications",
360
+ required: false,
361
+ description: "Discord webhook URL for notifications",
362
+ placeholder: "https://discord.com/api/webhooks/...",
363
+ sensitive: true,
364
+ testable: true,
365
+ validation: (value) => {
366
+ if (!value) return { valid: true };
367
+ if (!value.startsWith("https://discord.com/api/webhooks/")) {
368
+ return { valid: false, error: "Invalid Discord webhook URL" };
369
+ }
370
+ return { valid: true };
371
+ }
372
+ }
373
+ ];
374
+ function getEnvVariable(key) {
375
+ return ENV_VARIABLES.find((v) => v.key === key);
376
+ }
377
+ function validateEnvValue(key, value) {
378
+ const envVar = getEnvVariable(key);
379
+ if (!envVar) return { valid: false, error: "Unknown environment variable" };
380
+ if (envVar.required && !value) {
381
+ return { valid: false, error: "This field is required" };
382
+ }
383
+ if (envVar.validation) {
384
+ return envVar.validation(value);
385
+ }
386
+ return { valid: true };
387
+ }
388
+
389
+ // cli/auth/jwt.ts
390
+ import { createHmac, randomBytes } from "crypto";
391
+ import { parse as parseCookies } from "cookie";
392
+ var TTL_MS = 24 * 60 * 60 * 1e3;
393
+ var COOKIE_NAME = "openqa_token";
394
+ var _secret = null;
395
+ function getSecret() {
396
+ if (_secret) return _secret;
397
+ _secret = process.env.OPENQA_JWT_SECRET ?? null;
398
+ if (!_secret) {
399
+ _secret = randomBytes(32).toString("hex");
400
+ console.warn("[OpenQA] OPENQA_JWT_SECRET not set \u2014 using a volatile secret. All sessions will be invalidated on restart.");
401
+ }
402
+ return _secret;
403
+ }
404
+ function fromB64url(input) {
405
+ return Buffer.from(input, "base64url").toString("utf8");
406
+ }
407
+ function verifyToken(token) {
408
+ try {
409
+ const [header, body, sig] = token.split(".");
410
+ if (!header || !body || !sig) return null;
411
+ const expected = createHmac("sha256", getSecret()).update(`${header}.${body}`).digest("base64url");
412
+ if (expected !== sig) return null;
413
+ const payload = JSON.parse(fromB64url(body));
414
+ if (payload.exp < Math.floor(Date.now() / 1e3)) return null;
415
+ return payload;
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+ function extractToken(req) {
421
+ const cookieHeader = req.headers.cookie ?? "";
422
+ if (cookieHeader) {
423
+ const cookies = parseCookies(cookieHeader);
424
+ if (cookies[COOKIE_NAME]) return cookies[COOKIE_NAME];
425
+ }
426
+ const auth = req.headers.authorization ?? "";
427
+ if (auth.startsWith("Bearer ")) return auth.slice(7);
428
+ return null;
429
+ }
430
+
431
+ // cli/auth/middleware.ts
432
+ var AUTH_DISABLED = process.env.OPENQA_AUTH_DISABLED === "true";
433
+ function requireAuth(req, res, next) {
434
+ if (AUTH_DISABLED) {
435
+ req.user = { sub: "dev", username: "dev", role: "admin", iat: 0, exp: 0 };
436
+ return next();
437
+ }
438
+ const token = extractToken(req);
439
+ const payload = token ? verifyToken(token) : null;
440
+ if (!payload) {
441
+ res.status(401).json({ error: "Unauthorized" });
442
+ return;
443
+ }
444
+ req.user = payload;
445
+ next();
446
+ }
447
+ function requireAdmin(req, res, next) {
448
+ const user = req.user;
449
+ if (!user || user.role !== "admin") {
450
+ res.status(403).json({ error: "Forbidden \u2014 admin only" });
451
+ return;
452
+ }
453
+ next();
454
+ }
455
+
456
+ // cli/env-routes.ts
457
+ function createEnvRouter() {
458
+ const router = Router();
459
+ const ENV_FILE_PATH = join(process.cwd(), ".env");
460
+ function readEnvFile() {
461
+ if (!existsSync(ENV_FILE_PATH)) {
462
+ return {};
463
+ }
464
+ const content = readFileSync(ENV_FILE_PATH, "utf-8");
465
+ const env = {};
466
+ content.split("\n").forEach((line) => {
467
+ line = line.trim();
468
+ if (!line || line.startsWith("#")) return;
469
+ const match = line.match(/^([^=]+)=(.*)$/);
470
+ if (match) {
471
+ const [, key, value] = match;
472
+ env[key.trim()] = value.trim().replace(/^["']|["']$/g, "");
473
+ }
474
+ });
475
+ return env;
476
+ }
477
+ function writeEnvFile(env) {
478
+ const lines = [
479
+ "# OpenQA Environment Configuration",
480
+ "# Auto-generated by OpenQA Dashboard",
481
+ "# Last updated: " + (/* @__PURE__ */ new Date()).toISOString(),
482
+ ""
483
+ ];
484
+ const categories = {};
485
+ ENV_VARIABLES.forEach((v) => {
486
+ if (!categories[v.category]) {
487
+ categories[v.category] = [];
488
+ }
489
+ const value = env[v.key] || "";
490
+ if (value || v.required) {
491
+ categories[v.category].push(`${v.key}=${value}`);
492
+ }
493
+ });
494
+ const categoryNames = {
495
+ llm: "LLM CONFIGURATION",
496
+ security: "SECURITY",
497
+ target: "TARGET APPLICATION",
498
+ github: "GITHUB INTEGRATION",
499
+ web: "WEB SERVER",
500
+ agent: "AGENT CONFIGURATION",
501
+ database: "DATABASE",
502
+ notifications: "NOTIFICATIONS"
503
+ };
504
+ Object.entries(categories).forEach(([category, vars]) => {
505
+ if (vars.length > 0) {
506
+ lines.push("# " + "=".repeat(76));
507
+ lines.push(`# ${categoryNames[category] || category.toUpperCase()}`);
508
+ lines.push("# " + "=".repeat(76));
509
+ lines.push(...vars);
510
+ lines.push("");
511
+ }
512
+ });
513
+ writeFileSync(ENV_FILE_PATH, lines.join("\n"));
514
+ }
515
+ router.get("/api/env", requireAuth, requireAdmin, (_req, res) => {
516
+ try {
517
+ const envFile = readEnvFile();
518
+ const processEnv = process.env;
519
+ const variables = ENV_VARIABLES.map((v) => ({
520
+ key: v.key,
521
+ value: envFile[v.key] || processEnv[v.key] || "",
522
+ type: v.type,
523
+ category: v.category,
524
+ required: v.required,
525
+ description: v.description,
526
+ placeholder: v.placeholder,
527
+ options: v.options,
528
+ sensitive: v.sensitive,
529
+ testable: v.testable,
530
+ restartRequired: v.restartRequired,
531
+ // Mask sensitive values
532
+ displayValue: v.sensitive && (envFile[v.key] || processEnv[v.key]) ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : envFile[v.key] || processEnv[v.key] || ""
533
+ }));
534
+ res.json({
535
+ variables,
536
+ envFileExists: existsSync(ENV_FILE_PATH),
537
+ lastModified: existsSync(ENV_FILE_PATH) ? new Date(readFileSync(ENV_FILE_PATH, "utf-8").match(/Last updated: (.+)/)?.[1] || 0).toISOString() : null
538
+ });
539
+ } catch (error) {
540
+ res.status(500).json({
541
+ error: "Failed to read environment variables",
542
+ details: error instanceof Error ? error.message : String(error)
543
+ });
544
+ }
545
+ });
546
+ router.get("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
547
+ try {
548
+ const { key } = req.params;
549
+ const envVar = ENV_VARIABLES.find((v) => v.key === key);
550
+ if (!envVar) {
551
+ res.status(404).json({ error: "Environment variable not found" });
552
+ return;
553
+ }
554
+ const envFile = readEnvFile();
555
+ const value = envFile[key] || process.env[key] || "";
556
+ res.json({
557
+ ...envVar,
558
+ value,
559
+ displayValue: envVar.sensitive && value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value
560
+ });
561
+ } catch (error) {
562
+ res.status(500).json({
563
+ error: "Failed to read environment variable",
564
+ details: error instanceof Error ? error.message : String(error)
565
+ });
566
+ }
567
+ });
568
+ router.put("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
569
+ try {
570
+ const { key } = req.params;
571
+ const { value } = req.body;
572
+ const validation = validateEnvValue(key, value);
573
+ if (!validation.valid) {
574
+ res.status(400).json({ error: validation.error });
575
+ return;
576
+ }
577
+ const env = readEnvFile();
578
+ if (value === "" || value === null || value === void 0) {
579
+ delete env[key];
580
+ } else {
581
+ env[key] = value;
582
+ }
583
+ writeEnvFile(env);
584
+ if (value) {
585
+ process.env[key] = value;
586
+ } else {
587
+ delete process.env[key];
588
+ }
589
+ const envVar = ENV_VARIABLES.find((v) => v.key === key);
590
+ res.json({
591
+ success: true,
592
+ key,
593
+ value: envVar?.sensitive ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value,
594
+ restartRequired: envVar?.restartRequired || false
595
+ });
596
+ } catch (error) {
597
+ res.status(500).json({
598
+ error: "Failed to update environment variable",
599
+ details: error instanceof Error ? error.message : String(error)
600
+ });
601
+ }
602
+ });
603
+ router.post("/api/env/bulk", requireAuth, requireAdmin, (req, res) => {
604
+ try {
605
+ const { variables } = req.body;
606
+ if (!variables || typeof variables !== "object") {
607
+ res.status(400).json({ error: "Invalid request body" });
608
+ return;
609
+ }
610
+ const errors = {};
611
+ Object.entries(variables).forEach(([key, value]) => {
612
+ const validation = validateEnvValue(key, value);
613
+ if (!validation.valid) {
614
+ errors[key] = validation.error || "Invalid value";
615
+ }
616
+ });
617
+ if (Object.keys(errors).length > 0) {
618
+ res.status(400).json({ error: "Validation failed", errors });
619
+ return;
620
+ }
621
+ const env = readEnvFile();
622
+ Object.entries(variables).forEach(([key, value]) => {
623
+ if (value === "" || value === null || value === void 0) {
624
+ delete env[key];
625
+ delete process.env[key];
626
+ } else {
627
+ env[key] = value;
628
+ process.env[key] = value;
629
+ }
630
+ });
631
+ writeEnvFile(env);
632
+ const restartRequired = Object.keys(variables).some((key) => {
633
+ const envVar = ENV_VARIABLES.find((v) => v.key === key);
634
+ return envVar?.restartRequired;
635
+ });
636
+ res.json({
637
+ success: true,
638
+ updated: Object.keys(variables).length,
639
+ restartRequired
640
+ });
641
+ } catch (error) {
642
+ res.status(500).json({
643
+ error: "Failed to update environment variables",
644
+ details: error instanceof Error ? error.message : String(error)
645
+ });
646
+ }
647
+ });
648
+ router.post("/api/env/test/:key", requireAuth, requireAdmin, async (req, res) => {
649
+ try {
650
+ const { key } = req.params;
651
+ const { value } = req.body;
652
+ const envVar = ENV_VARIABLES.find((v) => v.key === key);
653
+ if (!envVar || !envVar.testable) {
654
+ res.status(400).json({ error: "This variable cannot be tested" });
655
+ return;
656
+ }
657
+ let testResult;
658
+ switch (key) {
659
+ case "OPENAI_API_KEY":
660
+ testResult = await testOpenAIKey(value);
661
+ break;
662
+ case "ANTHROPIC_API_KEY":
663
+ testResult = await testAnthropicKey(value);
664
+ break;
665
+ case "OLLAMA_BASE_URL":
666
+ testResult = await testOllamaURL(value);
667
+ break;
668
+ case "GITHUB_TOKEN":
669
+ testResult = await testGitHubToken(value);
670
+ break;
671
+ case "SAAS_URL":
672
+ testResult = await testURL(value);
673
+ break;
674
+ case "SLACK_WEBHOOK_URL":
675
+ testResult = await testSlackWebhook(value);
676
+ break;
677
+ case "DISCORD_WEBHOOK_URL":
678
+ testResult = await testDiscordWebhook(value);
679
+ break;
680
+ default:
681
+ testResult = { success: false, message: "Test not implemented for this variable" };
682
+ }
683
+ res.json(testResult);
684
+ } catch (error) {
685
+ res.status(500).json({
686
+ success: false,
687
+ message: error instanceof Error ? error.message : "Test failed"
688
+ });
689
+ }
690
+ });
691
+ router.post("/api/env/generate/:key", requireAuth, requireAdmin, (req, res) => {
692
+ try {
693
+ const { key } = req.params;
694
+ let generated;
695
+ switch (key) {
696
+ case "OPENQA_JWT_SECRET":
697
+ generated = Array.from(
698
+ { length: 64 },
699
+ () => Math.floor(Math.random() * 16).toString(16)
700
+ ).join("");
701
+ break;
702
+ default:
703
+ res.status(400).json({ error: "Generation not supported for this variable" });
704
+ return;
705
+ }
706
+ res.json({ success: true, value: generated });
707
+ } catch (error) {
708
+ res.status(500).json({
709
+ error: "Failed to generate value",
710
+ details: error instanceof Error ? error.message : String(error)
711
+ });
712
+ }
713
+ });
714
+ return router;
715
+ }
716
+ async function testOpenAIKey(apiKey) {
717
+ try {
718
+ const response = await fetch("https://api.openai.com/v1/models", {
719
+ headers: { "Authorization": `Bearer ${apiKey}` }
720
+ });
721
+ if (response.ok) {
722
+ return { success: true, message: "OpenAI API key is valid" };
723
+ }
724
+ return { success: false, message: "Invalid OpenAI API key" };
725
+ } catch {
726
+ return { success: false, message: "Failed to connect to OpenAI API" };
727
+ }
728
+ }
729
+ async function testAnthropicKey(apiKey) {
730
+ try {
731
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
732
+ method: "POST",
733
+ headers: {
734
+ "x-api-key": apiKey,
735
+ "anthropic-version": "2023-06-01",
736
+ "content-type": "application/json"
737
+ },
738
+ body: JSON.stringify({
739
+ model: "claude-3-haiku-20240307",
740
+ max_tokens: 1,
741
+ messages: [{ role: "user", content: "test" }]
742
+ })
743
+ });
744
+ if (response.status === 200 || response.status === 400) {
745
+ return { success: true, message: "Anthropic API key is valid" };
746
+ }
747
+ return { success: false, message: "Invalid Anthropic API key" };
748
+ } catch {
749
+ return { success: false, message: "Failed to connect to Anthropic API" };
750
+ }
751
+ }
752
+ async function testOllamaURL(url) {
753
+ try {
754
+ const response = await fetch(`${url}/api/tags`);
755
+ if (response.ok) {
756
+ return { success: true, message: "Ollama server is accessible" };
757
+ }
758
+ return { success: false, message: "Ollama server returned an error" };
759
+ } catch {
760
+ return { success: false, message: "Cannot connect to Ollama server" };
761
+ }
762
+ }
763
+ async function testGitHubToken(token) {
764
+ try {
765
+ const response = await fetch("https://api.github.com/user", {
766
+ headers: { "Authorization": `token ${token}` }
767
+ });
768
+ if (response.ok) {
769
+ const data = await response.json();
770
+ return { success: true, message: `GitHub token is valid (user: ${data.login})` };
771
+ }
772
+ return { success: false, message: "Invalid GitHub token" };
773
+ } catch {
774
+ return { success: false, message: "Failed to connect to GitHub API" };
775
+ }
776
+ }
777
+ async function testURL(url) {
778
+ try {
779
+ const response = await fetch(url, { method: "HEAD" });
780
+ if (response.ok) {
781
+ return { success: true, message: "URL is accessible" };
782
+ }
783
+ return { success: false, message: `URL returned status ${response.status}` };
784
+ } catch {
785
+ return { success: false, message: "Cannot connect to URL" };
786
+ }
787
+ }
788
+ async function testSlackWebhook(url) {
789
+ try {
790
+ const response = await fetch(url, {
791
+ method: "POST",
792
+ headers: { "Content-Type": "application/json" },
793
+ body: JSON.stringify({ text: "OpenQA webhook test" })
794
+ });
795
+ if (response.ok) {
796
+ return { success: true, message: "Slack webhook is valid" };
797
+ }
798
+ return { success: false, message: "Invalid Slack webhook" };
799
+ } catch {
800
+ return { success: false, message: "Failed to connect to Slack webhook" };
801
+ }
802
+ }
803
+ async function testDiscordWebhook(url) {
804
+ try {
805
+ const response = await fetch(url, {
806
+ method: "POST",
807
+ headers: { "Content-Type": "application/json" },
808
+ body: JSON.stringify({ content: "OpenQA webhook test" })
809
+ });
810
+ if (response.ok || response.status === 204) {
811
+ return { success: true, message: "Discord webhook is valid" };
812
+ }
813
+ return { success: false, message: "Invalid Discord webhook" };
814
+ } catch {
815
+ return { success: false, message: "Failed to connect to Discord webhook" };
816
+ }
817
+ }
818
+ export {
819
+ createEnvRouter
820
+ };