@openqa/cli 2.0.0 → 2.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.
- package/README.md +202 -5
- package/dist/cli/daemon.js +1459 -221
- package/dist/cli/dashboard.html.js +3 -0
- package/dist/cli/env-config.js +391 -0
- package/dist/cli/env-routes.js +820 -0
- package/dist/cli/env.html.js +679 -0
- package/dist/cli/index.js +4537 -2323
- package/dist/cli/server.js +2168 -12
- package/install.sh +19 -10
- package/package.json +2 -1
package/dist/cli/daemon.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
1
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
3
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
4
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
5
|
}) : x)(function(x) {
|
|
@@ -13,29 +9,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
13
9
|
var __esm = (fn, res) => function __init() {
|
|
14
10
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
11
|
};
|
|
16
|
-
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
-
};
|
|
19
12
|
var __export = (target, all) => {
|
|
20
13
|
for (var name in all)
|
|
21
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
15
|
};
|
|
23
|
-
var __copyProps = (to, from, except, desc) => {
|
|
24
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
-
for (let key of __getOwnPropNames(from))
|
|
26
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
-
}
|
|
29
|
-
return to;
|
|
30
|
-
};
|
|
31
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
-
mod
|
|
38
|
-
));
|
|
39
16
|
|
|
40
17
|
// node_modules/tsup/assets/esm_shims.js
|
|
41
18
|
import path from "path";
|
|
@@ -137,173 +114,6 @@ var init_coverage = __esm({
|
|
|
137
114
|
}
|
|
138
115
|
});
|
|
139
116
|
|
|
140
|
-
// node_modules/cookie/index.js
|
|
141
|
-
var require_cookie = __commonJS({
|
|
142
|
-
"node_modules/cookie/index.js"(exports) {
|
|
143
|
-
"use strict";
|
|
144
|
-
init_esm_shims();
|
|
145
|
-
exports.parse = parse;
|
|
146
|
-
exports.serialize = serialize;
|
|
147
|
-
var __toString = Object.prototype.toString;
|
|
148
|
-
var __hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
149
|
-
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
150
|
-
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
|
151
|
-
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
152
|
-
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
|
153
|
-
function parse(str, opt) {
|
|
154
|
-
if (typeof str !== "string") {
|
|
155
|
-
throw new TypeError("argument str must be a string");
|
|
156
|
-
}
|
|
157
|
-
var obj = {};
|
|
158
|
-
var len = str.length;
|
|
159
|
-
if (len < 2) return obj;
|
|
160
|
-
var dec = opt && opt.decode || decode;
|
|
161
|
-
var index = 0;
|
|
162
|
-
var eqIdx = 0;
|
|
163
|
-
var endIdx = 0;
|
|
164
|
-
do {
|
|
165
|
-
eqIdx = str.indexOf("=", index);
|
|
166
|
-
if (eqIdx === -1) break;
|
|
167
|
-
endIdx = str.indexOf(";", index);
|
|
168
|
-
if (endIdx === -1) {
|
|
169
|
-
endIdx = len;
|
|
170
|
-
} else if (eqIdx > endIdx) {
|
|
171
|
-
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
var keyStartIdx = startIndex(str, index, eqIdx);
|
|
175
|
-
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
|
176
|
-
var key = str.slice(keyStartIdx, keyEndIdx);
|
|
177
|
-
if (!__hasOwnProperty.call(obj, key)) {
|
|
178
|
-
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
|
179
|
-
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
|
180
|
-
if (str.charCodeAt(valStartIdx) === 34 && str.charCodeAt(valEndIdx - 1) === 34) {
|
|
181
|
-
valStartIdx++;
|
|
182
|
-
valEndIdx--;
|
|
183
|
-
}
|
|
184
|
-
var val = str.slice(valStartIdx, valEndIdx);
|
|
185
|
-
obj[key] = tryDecode(val, dec);
|
|
186
|
-
}
|
|
187
|
-
index = endIdx + 1;
|
|
188
|
-
} while (index < len);
|
|
189
|
-
return obj;
|
|
190
|
-
}
|
|
191
|
-
function startIndex(str, index, max) {
|
|
192
|
-
do {
|
|
193
|
-
var code = str.charCodeAt(index);
|
|
194
|
-
if (code !== 32 && code !== 9) return index;
|
|
195
|
-
} while (++index < max);
|
|
196
|
-
return max;
|
|
197
|
-
}
|
|
198
|
-
function endIndex(str, index, min) {
|
|
199
|
-
while (index > min) {
|
|
200
|
-
var code = str.charCodeAt(--index);
|
|
201
|
-
if (code !== 32 && code !== 9) return index + 1;
|
|
202
|
-
}
|
|
203
|
-
return min;
|
|
204
|
-
}
|
|
205
|
-
function serialize(name, val, opt) {
|
|
206
|
-
var enc = opt && opt.encode || encodeURIComponent;
|
|
207
|
-
if (typeof enc !== "function") {
|
|
208
|
-
throw new TypeError("option encode is invalid");
|
|
209
|
-
}
|
|
210
|
-
if (!cookieNameRegExp.test(name)) {
|
|
211
|
-
throw new TypeError("argument name is invalid");
|
|
212
|
-
}
|
|
213
|
-
var value = enc(val);
|
|
214
|
-
if (!cookieValueRegExp.test(value)) {
|
|
215
|
-
throw new TypeError("argument val is invalid");
|
|
216
|
-
}
|
|
217
|
-
var str = name + "=" + value;
|
|
218
|
-
if (!opt) return str;
|
|
219
|
-
if (null != opt.maxAge) {
|
|
220
|
-
var maxAge = Math.floor(opt.maxAge);
|
|
221
|
-
if (!isFinite(maxAge)) {
|
|
222
|
-
throw new TypeError("option maxAge is invalid");
|
|
223
|
-
}
|
|
224
|
-
str += "; Max-Age=" + maxAge;
|
|
225
|
-
}
|
|
226
|
-
if (opt.domain) {
|
|
227
|
-
if (!domainValueRegExp.test(opt.domain)) {
|
|
228
|
-
throw new TypeError("option domain is invalid");
|
|
229
|
-
}
|
|
230
|
-
str += "; Domain=" + opt.domain;
|
|
231
|
-
}
|
|
232
|
-
if (opt.path) {
|
|
233
|
-
if (!pathValueRegExp.test(opt.path)) {
|
|
234
|
-
throw new TypeError("option path is invalid");
|
|
235
|
-
}
|
|
236
|
-
str += "; Path=" + opt.path;
|
|
237
|
-
}
|
|
238
|
-
if (opt.expires) {
|
|
239
|
-
var expires = opt.expires;
|
|
240
|
-
if (!isDate(expires) || isNaN(expires.valueOf())) {
|
|
241
|
-
throw new TypeError("option expires is invalid");
|
|
242
|
-
}
|
|
243
|
-
str += "; Expires=" + expires.toUTCString();
|
|
244
|
-
}
|
|
245
|
-
if (opt.httpOnly) {
|
|
246
|
-
str += "; HttpOnly";
|
|
247
|
-
}
|
|
248
|
-
if (opt.secure) {
|
|
249
|
-
str += "; Secure";
|
|
250
|
-
}
|
|
251
|
-
if (opt.partitioned) {
|
|
252
|
-
str += "; Partitioned";
|
|
253
|
-
}
|
|
254
|
-
if (opt.priority) {
|
|
255
|
-
var priority = typeof opt.priority === "string" ? opt.priority.toLowerCase() : opt.priority;
|
|
256
|
-
switch (priority) {
|
|
257
|
-
case "low":
|
|
258
|
-
str += "; Priority=Low";
|
|
259
|
-
break;
|
|
260
|
-
case "medium":
|
|
261
|
-
str += "; Priority=Medium";
|
|
262
|
-
break;
|
|
263
|
-
case "high":
|
|
264
|
-
str += "; Priority=High";
|
|
265
|
-
break;
|
|
266
|
-
default:
|
|
267
|
-
throw new TypeError("option priority is invalid");
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (opt.sameSite) {
|
|
271
|
-
var sameSite = typeof opt.sameSite === "string" ? opt.sameSite.toLowerCase() : opt.sameSite;
|
|
272
|
-
switch (sameSite) {
|
|
273
|
-
case true:
|
|
274
|
-
str += "; SameSite=Strict";
|
|
275
|
-
break;
|
|
276
|
-
case "lax":
|
|
277
|
-
str += "; SameSite=Lax";
|
|
278
|
-
break;
|
|
279
|
-
case "strict":
|
|
280
|
-
str += "; SameSite=Strict";
|
|
281
|
-
break;
|
|
282
|
-
case "none":
|
|
283
|
-
str += "; SameSite=None";
|
|
284
|
-
break;
|
|
285
|
-
default:
|
|
286
|
-
throw new TypeError("option sameSite is invalid");
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return str;
|
|
290
|
-
}
|
|
291
|
-
function decode(str) {
|
|
292
|
-
return str.indexOf("%") !== -1 ? decodeURIComponent(str) : str;
|
|
293
|
-
}
|
|
294
|
-
function isDate(val) {
|
|
295
|
-
return __toString.call(val) === "[object Date]";
|
|
296
|
-
}
|
|
297
|
-
function tryDecode(str, decode2) {
|
|
298
|
-
try {
|
|
299
|
-
return decode2(str);
|
|
300
|
-
} catch (e) {
|
|
301
|
-
return str;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
117
|
// cli/daemon.ts
|
|
308
118
|
init_esm_shims();
|
|
309
119
|
|
|
@@ -3856,8 +3666,8 @@ async function verifyPassword(plain, stored) {
|
|
|
3856
3666
|
|
|
3857
3667
|
// cli/auth/jwt.ts
|
|
3858
3668
|
init_esm_shims();
|
|
3859
|
-
var import_cookie = __toESM(require_cookie(), 1);
|
|
3860
3669
|
import { createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
3670
|
+
import { parse as parseCookies } from "cookie";
|
|
3861
3671
|
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3862
3672
|
var COOKIE_NAME = "openqa_token";
|
|
3863
3673
|
var _secret = null;
|
|
@@ -3912,7 +3722,7 @@ function clearAuthCookie(res) {
|
|
|
3912
3722
|
function extractToken(req) {
|
|
3913
3723
|
const cookieHeader = req.headers.cookie ?? "";
|
|
3914
3724
|
if (cookieHeader) {
|
|
3915
|
-
const cookies = (
|
|
3725
|
+
const cookies = parseCookies(cookieHeader);
|
|
3916
3726
|
if (cookies[COOKIE_NAME]) return cookies[COOKIE_NAME];
|
|
3917
3727
|
}
|
|
3918
3728
|
const auth = req.headers.authorization ?? "";
|
|
@@ -3980,7 +3790,7 @@ var loginSchema = z3.object({
|
|
|
3980
3790
|
password: z3.string().min(1).max(200)
|
|
3981
3791
|
});
|
|
3982
3792
|
var setupSchema = z3.object({
|
|
3983
|
-
username: z3.string().min(3).max(
|
|
3793
|
+
username: z3.string().min(3).max(100).regex(/^[a-z0-9_.@-]+$/, "Only lowercase letters, digits, and ._@- characters"),
|
|
3984
3794
|
password: z3.string().min(8).max(200)
|
|
3985
3795
|
});
|
|
3986
3796
|
var changePasswordSchema = z3.object({
|
|
@@ -3988,7 +3798,7 @@ var changePasswordSchema = z3.object({
|
|
|
3988
3798
|
newPassword: z3.string().min(8).max(200)
|
|
3989
3799
|
});
|
|
3990
3800
|
var createUserSchema = z3.object({
|
|
3991
|
-
username: z3.string().min(3).max(
|
|
3801
|
+
username: z3.string().min(3).max(100).regex(/^[a-z0-9_.@-]+$/, "Only lowercase letters, digits, and ._@- characters"),
|
|
3992
3802
|
password: z3.string().min(8).max(200),
|
|
3993
3803
|
role: z3.enum(["admin", "viewer"])
|
|
3994
3804
|
});
|
|
@@ -4118,6 +3928,759 @@ function createAuthRouter(db2) {
|
|
|
4118
3928
|
return router;
|
|
4119
3929
|
}
|
|
4120
3930
|
|
|
3931
|
+
// cli/env-routes.ts
|
|
3932
|
+
init_esm_shims();
|
|
3933
|
+
import { Router as Router3 } from "express";
|
|
3934
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
3935
|
+
import { join as join6 } from "path";
|
|
3936
|
+
|
|
3937
|
+
// cli/env-config.ts
|
|
3938
|
+
init_esm_shims();
|
|
3939
|
+
var ENV_VARIABLES = [
|
|
3940
|
+
// ============================================================================
|
|
3941
|
+
// LLM CONFIGURATION
|
|
3942
|
+
// ============================================================================
|
|
3943
|
+
{
|
|
3944
|
+
key: "LLM_PROVIDER",
|
|
3945
|
+
type: "select",
|
|
3946
|
+
category: "llm",
|
|
3947
|
+
required: true,
|
|
3948
|
+
description: "LLM provider to use for AI operations",
|
|
3949
|
+
options: ["openai", "anthropic", "ollama"],
|
|
3950
|
+
placeholder: "openai",
|
|
3951
|
+
restartRequired: true
|
|
3952
|
+
},
|
|
3953
|
+
{
|
|
3954
|
+
key: "OPENAI_API_KEY",
|
|
3955
|
+
type: "password",
|
|
3956
|
+
category: "llm",
|
|
3957
|
+
required: false,
|
|
3958
|
+
description: "OpenAI API key (required if LLM_PROVIDER=openai)",
|
|
3959
|
+
placeholder: "sk-...",
|
|
3960
|
+
sensitive: true,
|
|
3961
|
+
testable: true,
|
|
3962
|
+
validation: (value) => {
|
|
3963
|
+
if (!value) return { valid: true };
|
|
3964
|
+
if (!value.startsWith("sk-")) {
|
|
3965
|
+
return { valid: false, error: 'OpenAI API key must start with "sk-"' };
|
|
3966
|
+
}
|
|
3967
|
+
if (value.length < 20) {
|
|
3968
|
+
return { valid: false, error: "API key seems too short" };
|
|
3969
|
+
}
|
|
3970
|
+
return { valid: true };
|
|
3971
|
+
},
|
|
3972
|
+
restartRequired: true
|
|
3973
|
+
},
|
|
3974
|
+
{
|
|
3975
|
+
key: "ANTHROPIC_API_KEY",
|
|
3976
|
+
type: "password",
|
|
3977
|
+
category: "llm",
|
|
3978
|
+
required: false,
|
|
3979
|
+
description: "Anthropic API key (required if LLM_PROVIDER=anthropic)",
|
|
3980
|
+
placeholder: "sk-ant-...",
|
|
3981
|
+
sensitive: true,
|
|
3982
|
+
testable: true,
|
|
3983
|
+
validation: (value) => {
|
|
3984
|
+
if (!value) return { valid: true };
|
|
3985
|
+
if (!value.startsWith("sk-ant-")) {
|
|
3986
|
+
return { valid: false, error: 'Anthropic API key must start with "sk-ant-"' };
|
|
3987
|
+
}
|
|
3988
|
+
return { valid: true };
|
|
3989
|
+
},
|
|
3990
|
+
restartRequired: true
|
|
3991
|
+
},
|
|
3992
|
+
{
|
|
3993
|
+
key: "OLLAMA_BASE_URL",
|
|
3994
|
+
type: "url",
|
|
3995
|
+
category: "llm",
|
|
3996
|
+
required: false,
|
|
3997
|
+
description: "Ollama server URL (required if LLM_PROVIDER=ollama)",
|
|
3998
|
+
placeholder: "http://localhost:11434",
|
|
3999
|
+
testable: true,
|
|
4000
|
+
validation: (value) => {
|
|
4001
|
+
if (!value) return { valid: true };
|
|
4002
|
+
try {
|
|
4003
|
+
new URL(value);
|
|
4004
|
+
return { valid: true };
|
|
4005
|
+
} catch {
|
|
4006
|
+
return { valid: false, error: "Invalid URL format" };
|
|
4007
|
+
}
|
|
4008
|
+
},
|
|
4009
|
+
restartRequired: true
|
|
4010
|
+
},
|
|
4011
|
+
{
|
|
4012
|
+
key: "LLM_MODEL",
|
|
4013
|
+
type: "text",
|
|
4014
|
+
category: "llm",
|
|
4015
|
+
required: false,
|
|
4016
|
+
description: "LLM model to use (e.g., gpt-4, claude-3-opus, llama2)",
|
|
4017
|
+
placeholder: "gpt-4",
|
|
4018
|
+
restartRequired: true
|
|
4019
|
+
},
|
|
4020
|
+
// ============================================================================
|
|
4021
|
+
// SECURITY
|
|
4022
|
+
// ============================================================================
|
|
4023
|
+
{
|
|
4024
|
+
key: "OPENQA_JWT_SECRET",
|
|
4025
|
+
type: "password",
|
|
4026
|
+
category: "security",
|
|
4027
|
+
required: true,
|
|
4028
|
+
description: "Secret key for JWT token signing (min 32 characters)",
|
|
4029
|
+
placeholder: "Generate with: openssl rand -hex 32",
|
|
4030
|
+
sensitive: true,
|
|
4031
|
+
validation: (value) => {
|
|
4032
|
+
if (!value) return { valid: false, error: "JWT secret is required" };
|
|
4033
|
+
if (value.length < 32) {
|
|
4034
|
+
return { valid: false, error: "JWT secret must be at least 32 characters" };
|
|
4035
|
+
}
|
|
4036
|
+
return { valid: true };
|
|
4037
|
+
},
|
|
4038
|
+
restartRequired: true
|
|
4039
|
+
},
|
|
4040
|
+
{
|
|
4041
|
+
key: "OPENQA_AUTH_DISABLED",
|
|
4042
|
+
type: "boolean",
|
|
4043
|
+
category: "security",
|
|
4044
|
+
required: false,
|
|
4045
|
+
description: "\u26A0\uFE0F DANGER: Disable authentication (NEVER use in production!)",
|
|
4046
|
+
placeholder: "false",
|
|
4047
|
+
validation: (value) => {
|
|
4048
|
+
if (value === "true" && process.env.NODE_ENV === "production") {
|
|
4049
|
+
return { valid: false, error: "Cannot disable auth in production!" };
|
|
4050
|
+
}
|
|
4051
|
+
return { valid: true };
|
|
4052
|
+
},
|
|
4053
|
+
restartRequired: true
|
|
4054
|
+
},
|
|
4055
|
+
{
|
|
4056
|
+
key: "NODE_ENV",
|
|
4057
|
+
type: "select",
|
|
4058
|
+
category: "security",
|
|
4059
|
+
required: false,
|
|
4060
|
+
description: "Node environment (production enables security features)",
|
|
4061
|
+
options: ["development", "production", "test"],
|
|
4062
|
+
placeholder: "production",
|
|
4063
|
+
restartRequired: true
|
|
4064
|
+
},
|
|
4065
|
+
// ============================================================================
|
|
4066
|
+
// TARGET APPLICATION
|
|
4067
|
+
// ============================================================================
|
|
4068
|
+
{
|
|
4069
|
+
key: "SAAS_URL",
|
|
4070
|
+
type: "url",
|
|
4071
|
+
category: "target",
|
|
4072
|
+
required: true,
|
|
4073
|
+
description: "URL of the application to test",
|
|
4074
|
+
placeholder: "https://your-app.com",
|
|
4075
|
+
testable: true,
|
|
4076
|
+
validation: (value) => {
|
|
4077
|
+
if (!value) return { valid: false, error: "Target URL is required" };
|
|
4078
|
+
try {
|
|
4079
|
+
const url = new URL(value);
|
|
4080
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
4081
|
+
return { valid: false, error: "URL must use http or https protocol" };
|
|
4082
|
+
}
|
|
4083
|
+
return { valid: true };
|
|
4084
|
+
} catch {
|
|
4085
|
+
return { valid: false, error: "Invalid URL format" };
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
},
|
|
4089
|
+
{
|
|
4090
|
+
key: "SAAS_AUTH_TYPE",
|
|
4091
|
+
type: "select",
|
|
4092
|
+
category: "target",
|
|
4093
|
+
required: false,
|
|
4094
|
+
description: "Authentication type for target application",
|
|
4095
|
+
options: ["none", "basic", "session"],
|
|
4096
|
+
placeholder: "none"
|
|
4097
|
+
},
|
|
4098
|
+
{
|
|
4099
|
+
key: "SAAS_USERNAME",
|
|
4100
|
+
type: "text",
|
|
4101
|
+
category: "target",
|
|
4102
|
+
required: false,
|
|
4103
|
+
description: "Username for target application authentication",
|
|
4104
|
+
placeholder: "test@example.com"
|
|
4105
|
+
},
|
|
4106
|
+
{
|
|
4107
|
+
key: "SAAS_PASSWORD",
|
|
4108
|
+
type: "password",
|
|
4109
|
+
category: "target",
|
|
4110
|
+
required: false,
|
|
4111
|
+
description: "Password for target application authentication",
|
|
4112
|
+
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
|
4113
|
+
sensitive: true
|
|
4114
|
+
},
|
|
4115
|
+
// ============================================================================
|
|
4116
|
+
// GITHUB INTEGRATION
|
|
4117
|
+
// ============================================================================
|
|
4118
|
+
{
|
|
4119
|
+
key: "GITHUB_TOKEN",
|
|
4120
|
+
type: "password",
|
|
4121
|
+
category: "github",
|
|
4122
|
+
required: false,
|
|
4123
|
+
description: "GitHub personal access token for issue creation",
|
|
4124
|
+
placeholder: "ghp_...",
|
|
4125
|
+
sensitive: true,
|
|
4126
|
+
testable: true,
|
|
4127
|
+
validation: (value) => {
|
|
4128
|
+
if (!value) return { valid: true };
|
|
4129
|
+
if (!value.startsWith("ghp_") && !value.startsWith("github_pat_")) {
|
|
4130
|
+
return { valid: false, error: 'GitHub token must start with "ghp_" or "github_pat_"' };
|
|
4131
|
+
}
|
|
4132
|
+
return { valid: true };
|
|
4133
|
+
}
|
|
4134
|
+
},
|
|
4135
|
+
{
|
|
4136
|
+
key: "GITHUB_OWNER",
|
|
4137
|
+
type: "text",
|
|
4138
|
+
category: "github",
|
|
4139
|
+
required: false,
|
|
4140
|
+
description: "GitHub repository owner/organization",
|
|
4141
|
+
placeholder: "your-username"
|
|
4142
|
+
},
|
|
4143
|
+
{
|
|
4144
|
+
key: "GITHUB_REPO",
|
|
4145
|
+
type: "text",
|
|
4146
|
+
category: "github",
|
|
4147
|
+
required: false,
|
|
4148
|
+
description: "GitHub repository name",
|
|
4149
|
+
placeholder: "your-repo"
|
|
4150
|
+
},
|
|
4151
|
+
{
|
|
4152
|
+
key: "GITHUB_BRANCH",
|
|
4153
|
+
type: "text",
|
|
4154
|
+
category: "github",
|
|
4155
|
+
required: false,
|
|
4156
|
+
description: "GitHub branch to monitor",
|
|
4157
|
+
placeholder: "main"
|
|
4158
|
+
},
|
|
4159
|
+
// ============================================================================
|
|
4160
|
+
// WEB SERVER
|
|
4161
|
+
// ============================================================================
|
|
4162
|
+
{
|
|
4163
|
+
key: "WEB_PORT",
|
|
4164
|
+
type: "number",
|
|
4165
|
+
category: "web",
|
|
4166
|
+
required: false,
|
|
4167
|
+
description: "Port for web server",
|
|
4168
|
+
placeholder: "4242",
|
|
4169
|
+
validation: (value) => {
|
|
4170
|
+
if (!value) return { valid: true };
|
|
4171
|
+
const port = parseInt(value, 10);
|
|
4172
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
4173
|
+
return { valid: false, error: "Port must be between 1 and 65535" };
|
|
4174
|
+
}
|
|
4175
|
+
return { valid: true };
|
|
4176
|
+
},
|
|
4177
|
+
restartRequired: true
|
|
4178
|
+
},
|
|
4179
|
+
{
|
|
4180
|
+
key: "WEB_HOST",
|
|
4181
|
+
type: "text",
|
|
4182
|
+
category: "web",
|
|
4183
|
+
required: false,
|
|
4184
|
+
description: "Host to bind web server (0.0.0.0 for all interfaces)",
|
|
4185
|
+
placeholder: "0.0.0.0",
|
|
4186
|
+
restartRequired: true
|
|
4187
|
+
},
|
|
4188
|
+
{
|
|
4189
|
+
key: "CORS_ORIGINS",
|
|
4190
|
+
type: "text",
|
|
4191
|
+
category: "web",
|
|
4192
|
+
required: false,
|
|
4193
|
+
description: "Allowed CORS origins (comma-separated)",
|
|
4194
|
+
placeholder: "https://your-domain.com,https://app.example.com",
|
|
4195
|
+
restartRequired: true
|
|
4196
|
+
},
|
|
4197
|
+
// ============================================================================
|
|
4198
|
+
// AGENT CONFIGURATION
|
|
4199
|
+
// ============================================================================
|
|
4200
|
+
{
|
|
4201
|
+
key: "AGENT_AUTO_START",
|
|
4202
|
+
type: "boolean",
|
|
4203
|
+
category: "agent",
|
|
4204
|
+
required: false,
|
|
4205
|
+
description: "Auto-start agent on server launch",
|
|
4206
|
+
placeholder: "false"
|
|
4207
|
+
},
|
|
4208
|
+
{
|
|
4209
|
+
key: "AGENT_INTERVAL_MS",
|
|
4210
|
+
type: "number",
|
|
4211
|
+
category: "agent",
|
|
4212
|
+
required: false,
|
|
4213
|
+
description: "Agent run interval in milliseconds (1 hour = 3600000)",
|
|
4214
|
+
placeholder: "3600000",
|
|
4215
|
+
validation: (value) => {
|
|
4216
|
+
if (!value) return { valid: true };
|
|
4217
|
+
const interval = parseInt(value, 10);
|
|
4218
|
+
if (isNaN(interval) || interval < 6e4) {
|
|
4219
|
+
return { valid: false, error: "Interval must be at least 60000ms (1 minute)" };
|
|
4220
|
+
}
|
|
4221
|
+
return { valid: true };
|
|
4222
|
+
}
|
|
4223
|
+
},
|
|
4224
|
+
{
|
|
4225
|
+
key: "AGENT_MAX_ITERATIONS",
|
|
4226
|
+
type: "number",
|
|
4227
|
+
category: "agent",
|
|
4228
|
+
required: false,
|
|
4229
|
+
description: "Maximum iterations per agent session",
|
|
4230
|
+
placeholder: "20",
|
|
4231
|
+
validation: (value) => {
|
|
4232
|
+
if (!value) return { valid: true };
|
|
4233
|
+
const max = parseInt(value, 10);
|
|
4234
|
+
if (isNaN(max) || max < 1 || max > 1e3) {
|
|
4235
|
+
return { valid: false, error: "Max iterations must be between 1 and 1000" };
|
|
4236
|
+
}
|
|
4237
|
+
return { valid: true };
|
|
4238
|
+
}
|
|
4239
|
+
},
|
|
4240
|
+
{
|
|
4241
|
+
key: "GIT_LISTENER_ENABLED",
|
|
4242
|
+
type: "boolean",
|
|
4243
|
+
category: "agent",
|
|
4244
|
+
required: false,
|
|
4245
|
+
description: "Enable git merge/pipeline detection",
|
|
4246
|
+
placeholder: "true"
|
|
4247
|
+
},
|
|
4248
|
+
{
|
|
4249
|
+
key: "GIT_POLL_INTERVAL_MS",
|
|
4250
|
+
type: "number",
|
|
4251
|
+
category: "agent",
|
|
4252
|
+
required: false,
|
|
4253
|
+
description: "Git polling interval in milliseconds",
|
|
4254
|
+
placeholder: "60000"
|
|
4255
|
+
},
|
|
4256
|
+
// ============================================================================
|
|
4257
|
+
// DATABASE
|
|
4258
|
+
// ============================================================================
|
|
4259
|
+
{
|
|
4260
|
+
key: "DB_PATH",
|
|
4261
|
+
type: "text",
|
|
4262
|
+
category: "database",
|
|
4263
|
+
required: false,
|
|
4264
|
+
description: "Path to SQLite database file",
|
|
4265
|
+
placeholder: "./data/openqa.db",
|
|
4266
|
+
restartRequired: true
|
|
4267
|
+
},
|
|
4268
|
+
// ============================================================================
|
|
4269
|
+
// NOTIFICATIONS
|
|
4270
|
+
// ============================================================================
|
|
4271
|
+
{
|
|
4272
|
+
key: "SLACK_WEBHOOK_URL",
|
|
4273
|
+
type: "url",
|
|
4274
|
+
category: "notifications",
|
|
4275
|
+
required: false,
|
|
4276
|
+
description: "Slack webhook URL for notifications",
|
|
4277
|
+
placeholder: "https://hooks.slack.com/services/...",
|
|
4278
|
+
sensitive: true,
|
|
4279
|
+
testable: true,
|
|
4280
|
+
validation: (value) => {
|
|
4281
|
+
if (!value) return { valid: true };
|
|
4282
|
+
if (!value.startsWith("https://hooks.slack.com/")) {
|
|
4283
|
+
return { valid: false, error: "Invalid Slack webhook URL" };
|
|
4284
|
+
}
|
|
4285
|
+
return { valid: true };
|
|
4286
|
+
}
|
|
4287
|
+
},
|
|
4288
|
+
{
|
|
4289
|
+
key: "DISCORD_WEBHOOK_URL",
|
|
4290
|
+
type: "url",
|
|
4291
|
+
category: "notifications",
|
|
4292
|
+
required: false,
|
|
4293
|
+
description: "Discord webhook URL for notifications",
|
|
4294
|
+
placeholder: "https://discord.com/api/webhooks/...",
|
|
4295
|
+
sensitive: true,
|
|
4296
|
+
testable: true,
|
|
4297
|
+
validation: (value) => {
|
|
4298
|
+
if (!value) return { valid: true };
|
|
4299
|
+
if (!value.startsWith("https://discord.com/api/webhooks/")) {
|
|
4300
|
+
return { valid: false, error: "Invalid Discord webhook URL" };
|
|
4301
|
+
}
|
|
4302
|
+
return { valid: true };
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
];
|
|
4306
|
+
function getEnvVariable(key) {
|
|
4307
|
+
return ENV_VARIABLES.find((v) => v.key === key);
|
|
4308
|
+
}
|
|
4309
|
+
function validateEnvValue(key, value) {
|
|
4310
|
+
const envVar = getEnvVariable(key);
|
|
4311
|
+
if (!envVar) return { valid: false, error: "Unknown environment variable" };
|
|
4312
|
+
if (envVar.required && !value) {
|
|
4313
|
+
return { valid: false, error: "This field is required" };
|
|
4314
|
+
}
|
|
4315
|
+
if (envVar.validation) {
|
|
4316
|
+
return envVar.validation(value);
|
|
4317
|
+
}
|
|
4318
|
+
return { valid: true };
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
// cli/env-routes.ts
|
|
4322
|
+
function createEnvRouter() {
|
|
4323
|
+
const router = Router3();
|
|
4324
|
+
const ENV_FILE_PATH = join6(process.cwd(), ".env");
|
|
4325
|
+
function readEnvFile() {
|
|
4326
|
+
if (!existsSync4(ENV_FILE_PATH)) {
|
|
4327
|
+
return {};
|
|
4328
|
+
}
|
|
4329
|
+
const content = readFileSync5(ENV_FILE_PATH, "utf-8");
|
|
4330
|
+
const env = {};
|
|
4331
|
+
content.split("\n").forEach((line) => {
|
|
4332
|
+
line = line.trim();
|
|
4333
|
+
if (!line || line.startsWith("#")) return;
|
|
4334
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
4335
|
+
if (match) {
|
|
4336
|
+
const [, key, value] = match;
|
|
4337
|
+
env[key.trim()] = value.trim().replace(/^["']|["']$/g, "");
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
return env;
|
|
4341
|
+
}
|
|
4342
|
+
function writeEnvFile(env) {
|
|
4343
|
+
const lines = [
|
|
4344
|
+
"# OpenQA Environment Configuration",
|
|
4345
|
+
"# Auto-generated by OpenQA Dashboard",
|
|
4346
|
+
"# Last updated: " + (/* @__PURE__ */ new Date()).toISOString(),
|
|
4347
|
+
""
|
|
4348
|
+
];
|
|
4349
|
+
const categories = {};
|
|
4350
|
+
ENV_VARIABLES.forEach((v) => {
|
|
4351
|
+
if (!categories[v.category]) {
|
|
4352
|
+
categories[v.category] = [];
|
|
4353
|
+
}
|
|
4354
|
+
const value = env[v.key] || "";
|
|
4355
|
+
if (value || v.required) {
|
|
4356
|
+
categories[v.category].push(`${v.key}=${value}`);
|
|
4357
|
+
}
|
|
4358
|
+
});
|
|
4359
|
+
const categoryNames = {
|
|
4360
|
+
llm: "LLM CONFIGURATION",
|
|
4361
|
+
security: "SECURITY",
|
|
4362
|
+
target: "TARGET APPLICATION",
|
|
4363
|
+
github: "GITHUB INTEGRATION",
|
|
4364
|
+
web: "WEB SERVER",
|
|
4365
|
+
agent: "AGENT CONFIGURATION",
|
|
4366
|
+
database: "DATABASE",
|
|
4367
|
+
notifications: "NOTIFICATIONS"
|
|
4368
|
+
};
|
|
4369
|
+
Object.entries(categories).forEach(([category, vars]) => {
|
|
4370
|
+
if (vars.length > 0) {
|
|
4371
|
+
lines.push("# " + "=".repeat(76));
|
|
4372
|
+
lines.push(`# ${categoryNames[category] || category.toUpperCase()}`);
|
|
4373
|
+
lines.push("# " + "=".repeat(76));
|
|
4374
|
+
lines.push(...vars);
|
|
4375
|
+
lines.push("");
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4378
|
+
writeFileSync3(ENV_FILE_PATH, lines.join("\n"));
|
|
4379
|
+
}
|
|
4380
|
+
router.get("/api/env", requireAuth, requireAdmin, (_req, res) => {
|
|
4381
|
+
try {
|
|
4382
|
+
const envFile = readEnvFile();
|
|
4383
|
+
const processEnv = process.env;
|
|
4384
|
+
const variables = ENV_VARIABLES.map((v) => ({
|
|
4385
|
+
key: v.key,
|
|
4386
|
+
value: envFile[v.key] || processEnv[v.key] || "",
|
|
4387
|
+
type: v.type,
|
|
4388
|
+
category: v.category,
|
|
4389
|
+
required: v.required,
|
|
4390
|
+
description: v.description,
|
|
4391
|
+
placeholder: v.placeholder,
|
|
4392
|
+
options: v.options,
|
|
4393
|
+
sensitive: v.sensitive,
|
|
4394
|
+
testable: v.testable,
|
|
4395
|
+
restartRequired: v.restartRequired,
|
|
4396
|
+
// Mask sensitive values
|
|
4397
|
+
displayValue: v.sensitive && (envFile[v.key] || processEnv[v.key]) ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : envFile[v.key] || processEnv[v.key] || ""
|
|
4398
|
+
}));
|
|
4399
|
+
res.json({
|
|
4400
|
+
variables,
|
|
4401
|
+
envFileExists: existsSync4(ENV_FILE_PATH),
|
|
4402
|
+
lastModified: existsSync4(ENV_FILE_PATH) ? new Date(readFileSync5(ENV_FILE_PATH, "utf-8").match(/Last updated: (.+)/)?.[1] || 0).toISOString() : null
|
|
4403
|
+
});
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
res.status(500).json({
|
|
4406
|
+
error: "Failed to read environment variables",
|
|
4407
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4408
|
+
});
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4411
|
+
router.get("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4412
|
+
try {
|
|
4413
|
+
const { key } = req.params;
|
|
4414
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4415
|
+
if (!envVar) {
|
|
4416
|
+
res.status(404).json({ error: "Environment variable not found" });
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
const envFile = readEnvFile();
|
|
4420
|
+
const value = envFile[key] || process.env[key] || "";
|
|
4421
|
+
res.json({
|
|
4422
|
+
...envVar,
|
|
4423
|
+
value,
|
|
4424
|
+
displayValue: envVar.sensitive && value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value
|
|
4425
|
+
});
|
|
4426
|
+
} catch (error) {
|
|
4427
|
+
res.status(500).json({
|
|
4428
|
+
error: "Failed to read environment variable",
|
|
4429
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
});
|
|
4433
|
+
router.put("/api/env/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4434
|
+
try {
|
|
4435
|
+
const { key } = req.params;
|
|
4436
|
+
const { value } = req.body;
|
|
4437
|
+
const validation = validateEnvValue(key, value);
|
|
4438
|
+
if (!validation.valid) {
|
|
4439
|
+
res.status(400).json({ error: validation.error });
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
const env = readEnvFile();
|
|
4443
|
+
if (value === "" || value === null || value === void 0) {
|
|
4444
|
+
delete env[key];
|
|
4445
|
+
} else {
|
|
4446
|
+
env[key] = value;
|
|
4447
|
+
}
|
|
4448
|
+
writeEnvFile(env);
|
|
4449
|
+
if (value) {
|
|
4450
|
+
process.env[key] = value;
|
|
4451
|
+
} else {
|
|
4452
|
+
delete process.env[key];
|
|
4453
|
+
}
|
|
4454
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4455
|
+
res.json({
|
|
4456
|
+
success: true,
|
|
4457
|
+
key,
|
|
4458
|
+
value: envVar?.sensitive ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : value,
|
|
4459
|
+
restartRequired: envVar?.restartRequired || false
|
|
4460
|
+
});
|
|
4461
|
+
} catch (error) {
|
|
4462
|
+
res.status(500).json({
|
|
4463
|
+
error: "Failed to update environment variable",
|
|
4464
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
});
|
|
4468
|
+
router.post("/api/env/bulk", requireAuth, requireAdmin, (req, res) => {
|
|
4469
|
+
try {
|
|
4470
|
+
const { variables } = req.body;
|
|
4471
|
+
if (!variables || typeof variables !== "object") {
|
|
4472
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
const errors = {};
|
|
4476
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
4477
|
+
const validation = validateEnvValue(key, value);
|
|
4478
|
+
if (!validation.valid) {
|
|
4479
|
+
errors[key] = validation.error || "Invalid value";
|
|
4480
|
+
}
|
|
4481
|
+
});
|
|
4482
|
+
if (Object.keys(errors).length > 0) {
|
|
4483
|
+
res.status(400).json({ error: "Validation failed", errors });
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
const env = readEnvFile();
|
|
4487
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
4488
|
+
if (value === "" || value === null || value === void 0) {
|
|
4489
|
+
delete env[key];
|
|
4490
|
+
delete process.env[key];
|
|
4491
|
+
} else {
|
|
4492
|
+
env[key] = value;
|
|
4493
|
+
process.env[key] = value;
|
|
4494
|
+
}
|
|
4495
|
+
});
|
|
4496
|
+
writeEnvFile(env);
|
|
4497
|
+
const restartRequired = Object.keys(variables).some((key) => {
|
|
4498
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4499
|
+
return envVar?.restartRequired;
|
|
4500
|
+
});
|
|
4501
|
+
res.json({
|
|
4502
|
+
success: true,
|
|
4503
|
+
updated: Object.keys(variables).length,
|
|
4504
|
+
restartRequired
|
|
4505
|
+
});
|
|
4506
|
+
} catch (error) {
|
|
4507
|
+
res.status(500).json({
|
|
4508
|
+
error: "Failed to update environment variables",
|
|
4509
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
router.post("/api/env/test/:key", requireAuth, requireAdmin, async (req, res) => {
|
|
4514
|
+
try {
|
|
4515
|
+
const { key } = req.params;
|
|
4516
|
+
const { value } = req.body;
|
|
4517
|
+
const envVar = ENV_VARIABLES.find((v) => v.key === key);
|
|
4518
|
+
if (!envVar || !envVar.testable) {
|
|
4519
|
+
res.status(400).json({ error: "This variable cannot be tested" });
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
let testResult;
|
|
4523
|
+
switch (key) {
|
|
4524
|
+
case "OPENAI_API_KEY":
|
|
4525
|
+
testResult = await testOpenAIKey(value);
|
|
4526
|
+
break;
|
|
4527
|
+
case "ANTHROPIC_API_KEY":
|
|
4528
|
+
testResult = await testAnthropicKey(value);
|
|
4529
|
+
break;
|
|
4530
|
+
case "OLLAMA_BASE_URL":
|
|
4531
|
+
testResult = await testOllamaURL(value);
|
|
4532
|
+
break;
|
|
4533
|
+
case "GITHUB_TOKEN":
|
|
4534
|
+
testResult = await testGitHubToken(value);
|
|
4535
|
+
break;
|
|
4536
|
+
case "SAAS_URL":
|
|
4537
|
+
testResult = await testURL(value);
|
|
4538
|
+
break;
|
|
4539
|
+
case "SLACK_WEBHOOK_URL":
|
|
4540
|
+
testResult = await testSlackWebhook(value);
|
|
4541
|
+
break;
|
|
4542
|
+
case "DISCORD_WEBHOOK_URL":
|
|
4543
|
+
testResult = await testDiscordWebhook(value);
|
|
4544
|
+
break;
|
|
4545
|
+
default:
|
|
4546
|
+
testResult = { success: false, message: "Test not implemented for this variable" };
|
|
4547
|
+
}
|
|
4548
|
+
res.json(testResult);
|
|
4549
|
+
} catch (error) {
|
|
4550
|
+
res.status(500).json({
|
|
4551
|
+
success: false,
|
|
4552
|
+
message: error instanceof Error ? error.message : "Test failed"
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
});
|
|
4556
|
+
router.post("/api/env/generate/:key", requireAuth, requireAdmin, (req, res) => {
|
|
4557
|
+
try {
|
|
4558
|
+
const { key } = req.params;
|
|
4559
|
+
let generated;
|
|
4560
|
+
switch (key) {
|
|
4561
|
+
case "OPENQA_JWT_SECRET":
|
|
4562
|
+
generated = Array.from(
|
|
4563
|
+
{ length: 64 },
|
|
4564
|
+
() => Math.floor(Math.random() * 16).toString(16)
|
|
4565
|
+
).join("");
|
|
4566
|
+
break;
|
|
4567
|
+
default:
|
|
4568
|
+
res.status(400).json({ error: "Generation not supported for this variable" });
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
res.json({ success: true, value: generated });
|
|
4572
|
+
} catch (error) {
|
|
4573
|
+
res.status(500).json({
|
|
4574
|
+
error: "Failed to generate value",
|
|
4575
|
+
details: error instanceof Error ? error.message : String(error)
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
});
|
|
4579
|
+
return router;
|
|
4580
|
+
}
|
|
4581
|
+
async function testOpenAIKey(apiKey) {
|
|
4582
|
+
try {
|
|
4583
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
4584
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
4585
|
+
});
|
|
4586
|
+
if (response.ok) {
|
|
4587
|
+
return { success: true, message: "OpenAI API key is valid" };
|
|
4588
|
+
}
|
|
4589
|
+
return { success: false, message: "Invalid OpenAI API key" };
|
|
4590
|
+
} catch {
|
|
4591
|
+
return { success: false, message: "Failed to connect to OpenAI API" };
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
async function testAnthropicKey(apiKey) {
|
|
4595
|
+
try {
|
|
4596
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
4597
|
+
method: "POST",
|
|
4598
|
+
headers: {
|
|
4599
|
+
"x-api-key": apiKey,
|
|
4600
|
+
"anthropic-version": "2023-06-01",
|
|
4601
|
+
"content-type": "application/json"
|
|
4602
|
+
},
|
|
4603
|
+
body: JSON.stringify({
|
|
4604
|
+
model: "claude-3-haiku-20240307",
|
|
4605
|
+
max_tokens: 1,
|
|
4606
|
+
messages: [{ role: "user", content: "test" }]
|
|
4607
|
+
})
|
|
4608
|
+
});
|
|
4609
|
+
if (response.status === 200 || response.status === 400) {
|
|
4610
|
+
return { success: true, message: "Anthropic API key is valid" };
|
|
4611
|
+
}
|
|
4612
|
+
return { success: false, message: "Invalid Anthropic API key" };
|
|
4613
|
+
} catch {
|
|
4614
|
+
return { success: false, message: "Failed to connect to Anthropic API" };
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
async function testOllamaURL(url) {
|
|
4618
|
+
try {
|
|
4619
|
+
const response = await fetch(`${url}/api/tags`);
|
|
4620
|
+
if (response.ok) {
|
|
4621
|
+
return { success: true, message: "Ollama server is accessible" };
|
|
4622
|
+
}
|
|
4623
|
+
return { success: false, message: "Ollama server returned an error" };
|
|
4624
|
+
} catch {
|
|
4625
|
+
return { success: false, message: "Cannot connect to Ollama server" };
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
async function testGitHubToken(token) {
|
|
4629
|
+
try {
|
|
4630
|
+
const response = await fetch("https://api.github.com/user", {
|
|
4631
|
+
headers: { "Authorization": `token ${token}` }
|
|
4632
|
+
});
|
|
4633
|
+
if (response.ok) {
|
|
4634
|
+
const data = await response.json();
|
|
4635
|
+
return { success: true, message: `GitHub token is valid (user: ${data.login})` };
|
|
4636
|
+
}
|
|
4637
|
+
return { success: false, message: "Invalid GitHub token" };
|
|
4638
|
+
} catch {
|
|
4639
|
+
return { success: false, message: "Failed to connect to GitHub API" };
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
async function testURL(url) {
|
|
4643
|
+
try {
|
|
4644
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
4645
|
+
if (response.ok) {
|
|
4646
|
+
return { success: true, message: "URL is accessible" };
|
|
4647
|
+
}
|
|
4648
|
+
return { success: false, message: `URL returned status ${response.status}` };
|
|
4649
|
+
} catch {
|
|
4650
|
+
return { success: false, message: "Cannot connect to URL" };
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
async function testSlackWebhook(url) {
|
|
4654
|
+
try {
|
|
4655
|
+
const response = await fetch(url, {
|
|
4656
|
+
method: "POST",
|
|
4657
|
+
headers: { "Content-Type": "application/json" },
|
|
4658
|
+
body: JSON.stringify({ text: "OpenQA webhook test" })
|
|
4659
|
+
});
|
|
4660
|
+
if (response.ok) {
|
|
4661
|
+
return { success: true, message: "Slack webhook is valid" };
|
|
4662
|
+
}
|
|
4663
|
+
return { success: false, message: "Invalid Slack webhook" };
|
|
4664
|
+
} catch {
|
|
4665
|
+
return { success: false, message: "Failed to connect to Slack webhook" };
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
async function testDiscordWebhook(url) {
|
|
4669
|
+
try {
|
|
4670
|
+
const response = await fetch(url, {
|
|
4671
|
+
method: "POST",
|
|
4672
|
+
headers: { "Content-Type": "application/json" },
|
|
4673
|
+
body: JSON.stringify({ content: "OpenQA webhook test" })
|
|
4674
|
+
});
|
|
4675
|
+
if (response.ok || response.status === 204) {
|
|
4676
|
+
return { success: true, message: "Discord webhook is valid" };
|
|
4677
|
+
}
|
|
4678
|
+
return { success: false, message: "Invalid Discord webhook" };
|
|
4679
|
+
} catch {
|
|
4680
|
+
return { success: false, message: "Failed to connect to Discord webhook" };
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4121
4684
|
// cli/dashboard.html.ts
|
|
4122
4685
|
init_esm_shims();
|
|
4123
4686
|
function getDashboardHTML() {
|
|
@@ -4862,6 +5425,9 @@ function getDashboardHTML() {
|
|
|
4862
5425
|
<a class="nav-item" href="/config">
|
|
4863
5426
|
<span class="icon">\u2699</span> Config
|
|
4864
5427
|
</a>
|
|
5428
|
+
<a class="nav-item" href="/config/env">
|
|
5429
|
+
<span class="icon">\u{1F527}</span> Environment
|
|
5430
|
+
</a>
|
|
4865
5431
|
</div>
|
|
4866
5432
|
|
|
4867
5433
|
<div class="sidebar-footer">
|
|
@@ -7072,9 +7638,9 @@ function getSetupHTML() {
|
|
|
7072
7638
|
|
|
7073
7639
|
<form id="setupForm">
|
|
7074
7640
|
<div class="field">
|
|
7075
|
-
<label for="username">Username</label>
|
|
7076
|
-
<input type="text" id="username" name="username" autocomplete="username" autofocus required pattern="[a-z0-9_]+" title="Lowercase letters, digits and
|
|
7077
|
-
<div class="hint">
|
|
7641
|
+
<label for="username">Username or Email</label>
|
|
7642
|
+
<input type="text" id="username" name="username" autocomplete="username" autofocus required pattern="[a-z0-9_.@-]+" title="Lowercase letters, digits, and ._@- characters">
|
|
7643
|
+
<div class="hint">Use your email or a username (lowercase, digits, ._@- allowed)</div>
|
|
7078
7644
|
</div>
|
|
7079
7645
|
<div class="field">
|
|
7080
7646
|
<label for="password">Password</label>
|
|
@@ -7147,21 +7713,699 @@ function getSetupHTML() {
|
|
|
7147
7713
|
</html>`;
|
|
7148
7714
|
}
|
|
7149
7715
|
|
|
7150
|
-
// cli/
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7716
|
+
// cli/env.html.ts
|
|
7717
|
+
init_esm_shims();
|
|
7718
|
+
function getEnvHTML() {
|
|
7719
|
+
return `<!DOCTYPE html>
|
|
7720
|
+
<html lang="en">
|
|
7721
|
+
<head>
|
|
7722
|
+
<meta charset="UTF-8">
|
|
7723
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7724
|
+
<title>Environment Variables - OpenQA</title>
|
|
7725
|
+
<style>
|
|
7726
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7727
|
+
|
|
7728
|
+
body {
|
|
7729
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
7730
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
7731
|
+
min-height: 100vh;
|
|
7732
|
+
padding: 20px;
|
|
7161
7733
|
}
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7734
|
+
|
|
7735
|
+
.container {
|
|
7736
|
+
max-width: 1200px;
|
|
7737
|
+
margin: 0 auto;
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
.header {
|
|
7741
|
+
background: rgba(255, 255, 255, 0.95);
|
|
7742
|
+
backdrop-filter: blur(10px);
|
|
7743
|
+
padding: 20px 30px;
|
|
7744
|
+
border-radius: 12px;
|
|
7745
|
+
margin-bottom: 20px;
|
|
7746
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
7747
|
+
display: flex;
|
|
7748
|
+
justify-content: space-between;
|
|
7749
|
+
align-items: center;
|
|
7750
|
+
}
|
|
7751
|
+
|
|
7752
|
+
.header h1 {
|
|
7753
|
+
font-size: 24px;
|
|
7754
|
+
color: #1a202c;
|
|
7755
|
+
display: flex;
|
|
7756
|
+
align-items: center;
|
|
7757
|
+
gap: 10px;
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
.header-actions {
|
|
7761
|
+
display: flex;
|
|
7762
|
+
gap: 10px;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
.btn {
|
|
7766
|
+
padding: 10px 20px;
|
|
7767
|
+
border: none;
|
|
7768
|
+
border-radius: 8px;
|
|
7769
|
+
font-size: 14px;
|
|
7770
|
+
font-weight: 600;
|
|
7771
|
+
cursor: pointer;
|
|
7772
|
+
transition: all 0.2s;
|
|
7773
|
+
text-decoration: none;
|
|
7774
|
+
display: inline-flex;
|
|
7775
|
+
align-items: center;
|
|
7776
|
+
gap: 8px;
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
.btn-primary {
|
|
7780
|
+
background: #667eea;
|
|
7781
|
+
color: white;
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
.btn-primary:hover {
|
|
7785
|
+
background: #5568d3;
|
|
7786
|
+
transform: translateY(-1px);
|
|
7787
|
+
}
|
|
7788
|
+
|
|
7789
|
+
.btn-secondary {
|
|
7790
|
+
background: #e2e8f0;
|
|
7791
|
+
color: #4a5568;
|
|
7792
|
+
}
|
|
7793
|
+
|
|
7794
|
+
.btn-secondary:hover {
|
|
7795
|
+
background: #cbd5e0;
|
|
7796
|
+
}
|
|
7797
|
+
|
|
7798
|
+
.btn-success {
|
|
7799
|
+
background: #48bb78;
|
|
7800
|
+
color: white;
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7803
|
+
.btn-success:hover {
|
|
7804
|
+
background: #38a169;
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
.btn:disabled {
|
|
7808
|
+
opacity: 0.5;
|
|
7809
|
+
cursor: not-allowed;
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
.content {
|
|
7813
|
+
background: rgba(255, 255, 255, 0.95);
|
|
7814
|
+
backdrop-filter: blur(10px);
|
|
7815
|
+
padding: 30px;
|
|
7816
|
+
border-radius: 12px;
|
|
7817
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7820
|
+
.tabs {
|
|
7821
|
+
display: flex;
|
|
7822
|
+
gap: 10px;
|
|
7823
|
+
margin-bottom: 30px;
|
|
7824
|
+
border-bottom: 2px solid #e2e8f0;
|
|
7825
|
+
padding-bottom: 10px;
|
|
7826
|
+
}
|
|
7827
|
+
|
|
7828
|
+
.tab {
|
|
7829
|
+
padding: 10px 20px;
|
|
7830
|
+
border: none;
|
|
7831
|
+
background: none;
|
|
7832
|
+
font-size: 14px;
|
|
7833
|
+
font-weight: 600;
|
|
7834
|
+
color: #718096;
|
|
7835
|
+
cursor: pointer;
|
|
7836
|
+
border-bottom: 3px solid transparent;
|
|
7837
|
+
transition: all 0.2s;
|
|
7838
|
+
}
|
|
7839
|
+
|
|
7840
|
+
.tab.active {
|
|
7841
|
+
color: #667eea;
|
|
7842
|
+
border-bottom-color: #667eea;
|
|
7843
|
+
}
|
|
7844
|
+
|
|
7845
|
+
.tab:hover {
|
|
7846
|
+
color: #667eea;
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
.category-section {
|
|
7850
|
+
display: none;
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
.category-section.active {
|
|
7854
|
+
display: block;
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7857
|
+
.category-header {
|
|
7858
|
+
display: flex;
|
|
7859
|
+
justify-content: space-between;
|
|
7860
|
+
align-items: center;
|
|
7861
|
+
margin-bottom: 20px;
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7864
|
+
.category-title {
|
|
7865
|
+
font-size: 18px;
|
|
7866
|
+
font-weight: 600;
|
|
7867
|
+
color: #2d3748;
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
.env-grid {
|
|
7871
|
+
display: grid;
|
|
7872
|
+
gap: 20px;
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7875
|
+
.env-item {
|
|
7876
|
+
border: 1px solid #e2e8f0;
|
|
7877
|
+
border-radius: 8px;
|
|
7878
|
+
padding: 20px;
|
|
7879
|
+
transition: all 0.2s;
|
|
7880
|
+
}
|
|
7881
|
+
|
|
7882
|
+
.env-item:hover {
|
|
7883
|
+
border-color: #cbd5e0;
|
|
7884
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
7885
|
+
}
|
|
7886
|
+
|
|
7887
|
+
.env-item-header {
|
|
7888
|
+
display: flex;
|
|
7889
|
+
justify-content: space-between;
|
|
7890
|
+
align-items: flex-start;
|
|
7891
|
+
margin-bottom: 10px;
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7894
|
+
.env-label {
|
|
7895
|
+
font-weight: 600;
|
|
7896
|
+
color: #2d3748;
|
|
7897
|
+
font-size: 14px;
|
|
7898
|
+
display: flex;
|
|
7899
|
+
align-items: center;
|
|
7900
|
+
gap: 8px;
|
|
7901
|
+
}
|
|
7902
|
+
|
|
7903
|
+
.required-badge {
|
|
7904
|
+
background: #fc8181;
|
|
7905
|
+
color: white;
|
|
7906
|
+
font-size: 10px;
|
|
7907
|
+
padding: 2px 6px;
|
|
7908
|
+
border-radius: 4px;
|
|
7909
|
+
font-weight: 700;
|
|
7910
|
+
}
|
|
7911
|
+
|
|
7912
|
+
.env-description {
|
|
7913
|
+
font-size: 13px;
|
|
7914
|
+
color: #718096;
|
|
7915
|
+
margin-bottom: 10px;
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7918
|
+
.env-input-group {
|
|
7919
|
+
display: flex;
|
|
7920
|
+
gap: 10px;
|
|
7921
|
+
align-items: center;
|
|
7922
|
+
}
|
|
7923
|
+
|
|
7924
|
+
.env-input {
|
|
7925
|
+
flex: 1;
|
|
7926
|
+
padding: 10px 12px;
|
|
7927
|
+
border: 1px solid #e2e8f0;
|
|
7928
|
+
border-radius: 6px;
|
|
7929
|
+
font-size: 14px;
|
|
7930
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
7931
|
+
transition: all 0.2s;
|
|
7932
|
+
}
|
|
7933
|
+
|
|
7934
|
+
.env-input:focus {
|
|
7935
|
+
outline: none;
|
|
7936
|
+
border-color: #667eea;
|
|
7937
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
7938
|
+
}
|
|
7939
|
+
|
|
7940
|
+
.env-input.error {
|
|
7941
|
+
border-color: #fc8181;
|
|
7942
|
+
}
|
|
7943
|
+
|
|
7944
|
+
.env-actions {
|
|
7945
|
+
display: flex;
|
|
7946
|
+
gap: 5px;
|
|
7947
|
+
}
|
|
7948
|
+
|
|
7949
|
+
.icon-btn {
|
|
7950
|
+
padding: 8px;
|
|
7951
|
+
border: none;
|
|
7952
|
+
background: #e2e8f0;
|
|
7953
|
+
border-radius: 6px;
|
|
7954
|
+
cursor: pointer;
|
|
7955
|
+
transition: all 0.2s;
|
|
7956
|
+
font-size: 16px;
|
|
7957
|
+
}
|
|
7958
|
+
|
|
7959
|
+
.icon-btn:hover {
|
|
7960
|
+
background: #cbd5e0;
|
|
7961
|
+
}
|
|
7962
|
+
|
|
7963
|
+
.icon-btn.test {
|
|
7964
|
+
background: #bee3f8;
|
|
7965
|
+
color: #2c5282;
|
|
7966
|
+
}
|
|
7967
|
+
|
|
7968
|
+
.icon-btn.test:hover {
|
|
7969
|
+
background: #90cdf4;
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
.icon-btn.generate {
|
|
7973
|
+
background: #c6f6d5;
|
|
7974
|
+
color: #22543d;
|
|
7975
|
+
}
|
|
7976
|
+
|
|
7977
|
+
.icon-btn.generate:hover {
|
|
7978
|
+
background: #9ae6b4;
|
|
7979
|
+
}
|
|
7980
|
+
|
|
7981
|
+
.error-message {
|
|
7982
|
+
color: #e53e3e;
|
|
7983
|
+
font-size: 12px;
|
|
7984
|
+
margin-top: 5px;
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
.success-message {
|
|
7988
|
+
color: #38a169;
|
|
7989
|
+
font-size: 12px;
|
|
7990
|
+
margin-top: 5px;
|
|
7991
|
+
}
|
|
7992
|
+
|
|
7993
|
+
.alert {
|
|
7994
|
+
padding: 15px 20px;
|
|
7995
|
+
border-radius: 8px;
|
|
7996
|
+
margin-bottom: 20px;
|
|
7997
|
+
display: flex;
|
|
7998
|
+
align-items: center;
|
|
7999
|
+
gap: 10px;
|
|
8000
|
+
}
|
|
8001
|
+
|
|
8002
|
+
.alert-warning {
|
|
8003
|
+
background: #fef5e7;
|
|
8004
|
+
border-left: 4px solid #f59e0b;
|
|
8005
|
+
color: #92400e;
|
|
8006
|
+
}
|
|
8007
|
+
|
|
8008
|
+
.alert-info {
|
|
8009
|
+
background: #eff6ff;
|
|
8010
|
+
border-left: 4px solid #3b82f6;
|
|
8011
|
+
color: #1e40af;
|
|
8012
|
+
}
|
|
8013
|
+
|
|
8014
|
+
.alert-success {
|
|
8015
|
+
background: #f0fdf4;
|
|
8016
|
+
border-left: 4px solid #10b981;
|
|
8017
|
+
color: #065f46;
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
.loading {
|
|
8021
|
+
text-align: center;
|
|
8022
|
+
padding: 40px;
|
|
8023
|
+
color: #718096;
|
|
8024
|
+
}
|
|
8025
|
+
|
|
8026
|
+
.spinner {
|
|
8027
|
+
border: 3px solid #e2e8f0;
|
|
8028
|
+
border-top: 3px solid #667eea;
|
|
8029
|
+
border-radius: 50%;
|
|
8030
|
+
width: 40px;
|
|
8031
|
+
height: 40px;
|
|
8032
|
+
animation: spin 1s linear infinite;
|
|
8033
|
+
margin: 0 auto 20px;
|
|
8034
|
+
}
|
|
8035
|
+
|
|
8036
|
+
@keyframes spin {
|
|
8037
|
+
0% { transform: rotate(0deg); }
|
|
8038
|
+
100% { transform: rotate(360deg); }
|
|
8039
|
+
}
|
|
8040
|
+
|
|
8041
|
+
.modal {
|
|
8042
|
+
display: none;
|
|
8043
|
+
position: fixed;
|
|
8044
|
+
top: 0;
|
|
8045
|
+
left: 0;
|
|
8046
|
+
right: 0;
|
|
8047
|
+
bottom: 0;
|
|
8048
|
+
background: rgba(0, 0, 0, 0.5);
|
|
8049
|
+
z-index: 1000;
|
|
8050
|
+
align-items: center;
|
|
8051
|
+
justify-content: center;
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
.modal.show {
|
|
8055
|
+
display: flex;
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
.modal-content {
|
|
8059
|
+
background: white;
|
|
8060
|
+
padding: 30px;
|
|
8061
|
+
border-radius: 12px;
|
|
8062
|
+
max-width: 500px;
|
|
8063
|
+
width: 90%;
|
|
8064
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
.modal-header {
|
|
8068
|
+
font-size: 20px;
|
|
8069
|
+
font-weight: 600;
|
|
8070
|
+
margin-bottom: 15px;
|
|
8071
|
+
color: #2d3748;
|
|
8072
|
+
}
|
|
8073
|
+
|
|
8074
|
+
.modal-body {
|
|
8075
|
+
margin-bottom: 20px;
|
|
8076
|
+
color: #4a5568;
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
.modal-footer {
|
|
8080
|
+
display: flex;
|
|
8081
|
+
gap: 10px;
|
|
8082
|
+
justify-content: flex-end;
|
|
8083
|
+
}
|
|
8084
|
+
</style>
|
|
8085
|
+
</head>
|
|
8086
|
+
<body>
|
|
8087
|
+
<div class="container">
|
|
8088
|
+
<div class="header">
|
|
8089
|
+
<h1>
|
|
8090
|
+
<span>\u2699\uFE0F</span>
|
|
8091
|
+
Environment Variables
|
|
8092
|
+
</h1>
|
|
8093
|
+
<div class="header-actions">
|
|
8094
|
+
<a href="/config" class="btn btn-secondary">\u2190 Back to Config</a>
|
|
8095
|
+
<button id="saveBtn" class="btn btn-success" disabled>\u{1F4BE} Save Changes</button>
|
|
8096
|
+
</div>
|
|
8097
|
+
</div>
|
|
8098
|
+
|
|
8099
|
+
<div class="content">
|
|
8100
|
+
<div id="loading" class="loading">
|
|
8101
|
+
<div class="spinner"></div>
|
|
8102
|
+
<div>Loading environment variables...</div>
|
|
8103
|
+
</div>
|
|
8104
|
+
|
|
8105
|
+
<div id="main" style="display: none;">
|
|
8106
|
+
<div id="alerts"></div>
|
|
8107
|
+
|
|
8108
|
+
<div class="tabs">
|
|
8109
|
+
<button class="tab active" data-category="llm">\u{1F916} LLM</button>
|
|
8110
|
+
<button class="tab" data-category="security">\u{1F512} Security</button>
|
|
8111
|
+
<button class="tab" data-category="target">\u{1F3AF} Target App</button>
|
|
8112
|
+
<button class="tab" data-category="github">\u{1F419} GitHub</button>
|
|
8113
|
+
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
8114
|
+
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
8115
|
+
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
8116
|
+
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
8117
|
+
</div>
|
|
8118
|
+
|
|
8119
|
+
<div id="categories"></div>
|
|
8120
|
+
</div>
|
|
8121
|
+
</div>
|
|
8122
|
+
</div>
|
|
8123
|
+
|
|
8124
|
+
<!-- Test Result Modal -->
|
|
8125
|
+
<div id="testModal" class="modal">
|
|
8126
|
+
<div class="modal-content">
|
|
8127
|
+
<div class="modal-header">Test Result</div>
|
|
8128
|
+
<div class="modal-body" id="testResult"></div>
|
|
8129
|
+
<div class="modal-footer">
|
|
8130
|
+
<button class="btn btn-secondary" onclick="closeTestModal()">Close</button>
|
|
8131
|
+
</div>
|
|
8132
|
+
</div>
|
|
8133
|
+
</div>
|
|
8134
|
+
|
|
8135
|
+
<script>
|
|
8136
|
+
let envVariables = [];
|
|
8137
|
+
let changedVariables = {};
|
|
8138
|
+
let restartRequired = false;
|
|
8139
|
+
|
|
8140
|
+
// Load environment variables
|
|
8141
|
+
async function loadEnvVariables() {
|
|
8142
|
+
try {
|
|
8143
|
+
const response = await fetch('/api/env');
|
|
8144
|
+
if (!response.ok) throw new Error('Failed to load variables');
|
|
8145
|
+
|
|
8146
|
+
const data = await response.json();
|
|
8147
|
+
envVariables = data.variables;
|
|
8148
|
+
|
|
8149
|
+
renderCategories();
|
|
8150
|
+
document.getElementById('loading').style.display = 'none';
|
|
8151
|
+
document.getElementById('main').style.display = 'block';
|
|
8152
|
+
} catch (error) {
|
|
8153
|
+
showAlert('error', 'Failed to load environment variables: ' + error.message);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
|
|
8157
|
+
// Render categories
|
|
8158
|
+
function renderCategories() {
|
|
8159
|
+
const container = document.getElementById('categories');
|
|
8160
|
+
const categories = [...new Set(envVariables.map(v => v.category))];
|
|
8161
|
+
|
|
8162
|
+
categories.forEach((category, index) => {
|
|
8163
|
+
const section = document.createElement('div');
|
|
8164
|
+
section.className = 'category-section' + (index === 0 ? ' active' : '');
|
|
8165
|
+
section.dataset.category = category;
|
|
8166
|
+
|
|
8167
|
+
const vars = envVariables.filter(v => v.category === category);
|
|
8168
|
+
|
|
8169
|
+
section.innerHTML = \`
|
|
8170
|
+
<div class="category-header">
|
|
8171
|
+
<div class="category-title">\${getCategoryTitle(category)}</div>
|
|
8172
|
+
</div>
|
|
8173
|
+
<div class="env-grid">
|
|
8174
|
+
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
8175
|
+
</div>
|
|
8176
|
+
\`;
|
|
8177
|
+
|
|
8178
|
+
container.appendChild(section);
|
|
8179
|
+
});
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8182
|
+
// Render single env item
|
|
8183
|
+
function renderEnvItem(envVar) {
|
|
8184
|
+
const inputType = envVar.type === 'password' ? 'password' : 'text';
|
|
8185
|
+
const value = envVar.displayValue || '';
|
|
8186
|
+
|
|
8187
|
+
return \`
|
|
8188
|
+
<div class="env-item" data-key="\${envVar.key}">
|
|
8189
|
+
<div class="env-item-header">
|
|
8190
|
+
<div class="env-label">
|
|
8191
|
+
\${envVar.key}
|
|
8192
|
+
\${envVar.required ? '<span class="required-badge">REQUIRED</span>' : ''}
|
|
8193
|
+
</div>
|
|
8194
|
+
</div>
|
|
8195
|
+
<div class="env-description">\${envVar.description}</div>
|
|
8196
|
+
<div class="env-input-group">
|
|
8197
|
+
\${envVar.type === 'select' ?
|
|
8198
|
+
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
8199
|
+
<option value="">-- Select --</option>
|
|
8200
|
+
\${envVar.options.map(opt =>
|
|
8201
|
+
\`<option value="\${opt}" \${value === opt ? 'selected' : ''}>\${opt}</option>\`
|
|
8202
|
+
).join('')}
|
|
8203
|
+
</select>\` :
|
|
8204
|
+
envVar.type === 'boolean' ?
|
|
8205
|
+
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
8206
|
+
<option value="">-- Select --</option>
|
|
8207
|
+
<option value="true" \${value === 'true' ? 'selected' : ''}>true</option>
|
|
8208
|
+
<option value="false" \${value === 'false' ? 'selected' : ''}>false</option>
|
|
8209
|
+
</select>\` :
|
|
8210
|
+
\`<input
|
|
8211
|
+
type="\${inputType}"
|
|
8212
|
+
class="env-input"
|
|
8213
|
+
data-key="\${envVar.key}"
|
|
8214
|
+
value="\${value}"
|
|
8215
|
+
placeholder="\${envVar.placeholder || ''}"
|
|
8216
|
+
onchange="handleChange(this)"
|
|
8217
|
+
/>\`
|
|
8218
|
+
}
|
|
8219
|
+
<div class="env-actions">
|
|
8220
|
+
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
8221
|
+
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
8222
|
+
</div>
|
|
8223
|
+
</div>
|
|
8224
|
+
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
8225
|
+
<div class="success-message" id="success-\${envVar.key}"></div>
|
|
8226
|
+
</div>
|
|
8227
|
+
\`;
|
|
8228
|
+
}
|
|
8229
|
+
|
|
8230
|
+
// Handle input change
|
|
8231
|
+
function handleChange(input) {
|
|
8232
|
+
const key = input.dataset.key;
|
|
8233
|
+
const value = input.value;
|
|
8234
|
+
|
|
8235
|
+
changedVariables[key] = value;
|
|
8236
|
+
document.getElementById('saveBtn').disabled = false;
|
|
8237
|
+
|
|
8238
|
+
// Clear messages
|
|
8239
|
+
document.getElementById(\`error-\${key}\`).textContent = '';
|
|
8240
|
+
document.getElementById(\`success-\${key}\`).textContent = '';
|
|
8241
|
+
}
|
|
8242
|
+
|
|
8243
|
+
// Save changes
|
|
8244
|
+
async function saveChanges() {
|
|
8245
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
8246
|
+
saveBtn.disabled = true;
|
|
8247
|
+
saveBtn.textContent = '\u{1F4BE} Saving...';
|
|
8248
|
+
|
|
8249
|
+
try {
|
|
8250
|
+
const response = await fetch('/api/env/bulk', {
|
|
8251
|
+
method: 'POST',
|
|
8252
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8253
|
+
body: JSON.stringify({ variables: changedVariables }),
|
|
8254
|
+
});
|
|
8255
|
+
|
|
8256
|
+
if (!response.ok) {
|
|
8257
|
+
const error = await response.json();
|
|
8258
|
+
throw new Error(error.error || 'Failed to save');
|
|
8259
|
+
}
|
|
8260
|
+
|
|
8261
|
+
const result = await response.json();
|
|
8262
|
+
restartRequired = result.restartRequired;
|
|
8263
|
+
|
|
8264
|
+
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
8265
|
+
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
8266
|
+
|
|
8267
|
+
changedVariables = {};
|
|
8268
|
+
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8269
|
+
|
|
8270
|
+
// Reload to show updated values
|
|
8271
|
+
setTimeout(() => location.reload(), 2000);
|
|
8272
|
+
} catch (error) {
|
|
8273
|
+
showAlert('error', 'Failed to save: ' + error.message);
|
|
8274
|
+
saveBtn.disabled = false;
|
|
8275
|
+
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
|
|
8279
|
+
// Test variable
|
|
8280
|
+
async function testVariable(key) {
|
|
8281
|
+
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
8282
|
+
const value = input.value;
|
|
8283
|
+
|
|
8284
|
+
if (!value) {
|
|
8285
|
+
showAlert('warning', 'Please enter a value first');
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
8288
|
+
|
|
8289
|
+
try {
|
|
8290
|
+
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
8291
|
+
method: 'POST',
|
|
8292
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8293
|
+
body: JSON.stringify({ value }),
|
|
8294
|
+
});
|
|
8295
|
+
|
|
8296
|
+
const result = await response.json();
|
|
8297
|
+
showTestResult(result);
|
|
8298
|
+
} catch (error) {
|
|
8299
|
+
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
|
|
8303
|
+
// Generate secret
|
|
8304
|
+
async function generateSecret(key) {
|
|
8305
|
+
try {
|
|
8306
|
+
const response = await fetch(\`/api/env/generate/\${key}\`, {
|
|
8307
|
+
method: 'POST',
|
|
8308
|
+
});
|
|
8309
|
+
|
|
8310
|
+
if (!response.ok) throw new Error('Failed to generate');
|
|
8311
|
+
|
|
8312
|
+
const result = await response.json();
|
|
8313
|
+
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
8314
|
+
input.value = result.value;
|
|
8315
|
+
handleChange(input);
|
|
8316
|
+
|
|
8317
|
+
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
8318
|
+
} catch (error) {
|
|
8319
|
+
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
|
|
8323
|
+
// Show test result
|
|
8324
|
+
function showTestResult(result) {
|
|
8325
|
+
const modal = document.getElementById('testModal');
|
|
8326
|
+
const resultDiv = document.getElementById('testResult');
|
|
8327
|
+
|
|
8328
|
+
resultDiv.innerHTML = \`
|
|
8329
|
+
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
8330
|
+
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
8331
|
+
</div>
|
|
8332
|
+
\`;
|
|
8333
|
+
|
|
8334
|
+
modal.classList.add('show');
|
|
8335
|
+
}
|
|
8336
|
+
|
|
8337
|
+
function closeTestModal() {
|
|
8338
|
+
document.getElementById('testModal').classList.remove('show');
|
|
8339
|
+
}
|
|
8340
|
+
|
|
8341
|
+
// Show alert
|
|
8342
|
+
function showAlert(type, message) {
|
|
8343
|
+
const alerts = document.getElementById('alerts');
|
|
8344
|
+
const alertClass = type === 'error' ? 'alert-warning' :
|
|
8345
|
+
type === 'success' ? 'alert-success' : 'alert-info';
|
|
8346
|
+
|
|
8347
|
+
alerts.innerHTML = \`
|
|
8348
|
+
<div class="alert \${alertClass}">
|
|
8349
|
+
\${message}
|
|
8350
|
+
</div>
|
|
8351
|
+
\`;
|
|
8352
|
+
|
|
8353
|
+
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8356
|
+
// Get category title
|
|
8357
|
+
function getCategoryTitle(category) {
|
|
8358
|
+
const titles = {
|
|
8359
|
+
llm: '\u{1F916} LLM Configuration',
|
|
8360
|
+
security: '\u{1F512} Security Settings',
|
|
8361
|
+
target: '\u{1F3AF} Target Application',
|
|
8362
|
+
github: '\u{1F419} GitHub Integration',
|
|
8363
|
+
web: '\u{1F310} Web Server',
|
|
8364
|
+
agent: '\u{1F916} Agent Configuration',
|
|
8365
|
+
database: '\u{1F4BE} Database',
|
|
8366
|
+
notifications: '\u{1F514} Notifications',
|
|
8367
|
+
};
|
|
8368
|
+
return titles[category] || category;
|
|
8369
|
+
}
|
|
8370
|
+
|
|
8371
|
+
// Tab switching
|
|
8372
|
+
document.addEventListener('click', (e) => {
|
|
8373
|
+
if (e.target.classList.contains('tab')) {
|
|
8374
|
+
const category = e.target.dataset.category;
|
|
8375
|
+
|
|
8376
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
8377
|
+
e.target.classList.add('active');
|
|
8378
|
+
|
|
8379
|
+
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
8380
|
+
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
8381
|
+
}
|
|
8382
|
+
});
|
|
8383
|
+
|
|
8384
|
+
// Save button
|
|
8385
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
8386
|
+
|
|
8387
|
+
// Load on page load
|
|
8388
|
+
loadEnvVariables();
|
|
8389
|
+
</script>
|
|
8390
|
+
</body>
|
|
8391
|
+
</html>`;
|
|
8392
|
+
}
|
|
8393
|
+
|
|
8394
|
+
// cli/daemon.ts
|
|
8395
|
+
import rateLimit from "express-rate-limit";
|
|
8396
|
+
import cors from "cors";
|
|
8397
|
+
import { z as z4 } from "zod";
|
|
8398
|
+
function validate3(schema) {
|
|
8399
|
+
return (req, res, next) => {
|
|
8400
|
+
const result = schema.safeParse(req.body);
|
|
8401
|
+
if (!result.success) {
|
|
8402
|
+
return res.status(400).json({
|
|
8403
|
+
error: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
|
|
8404
|
+
});
|
|
8405
|
+
}
|
|
8406
|
+
req.body = result.data;
|
|
8407
|
+
next();
|
|
8408
|
+
};
|
|
7165
8409
|
}
|
|
7166
8410
|
var saasConfigSchema2 = z4.object({
|
|
7167
8411
|
name: z4.string().min(1).max(200).optional(),
|
|
@@ -7238,6 +8482,7 @@ app.use(["/api/agent/start", "/api/project/setup", "/api/project/test", "/api/br
|
|
|
7238
8482
|
var wss = new WebSocketServer({ noServer: true });
|
|
7239
8483
|
var agent = null;
|
|
7240
8484
|
app.use(createAuthRouter(db));
|
|
8485
|
+
app.use(createEnvRouter());
|
|
7241
8486
|
app.use("/api", (req, res, next) => {
|
|
7242
8487
|
const PUBLIC_PATHS = ["/auth/login", "/auth/logout", "/setup"];
|
|
7243
8488
|
if (PUBLIC_PATHS.some((p) => req.path === p || req.path.startsWith(p + "/"))) return next();
|
|
@@ -7261,6 +8506,9 @@ app.get("/", authOrRedirect(db), (_req, res) => {
|
|
|
7261
8506
|
app.get("/config", authOrRedirect(db), (_req, res) => {
|
|
7262
8507
|
res.send(getConfigHTML(cfg));
|
|
7263
8508
|
});
|
|
8509
|
+
app.get("/config/env", authOrRedirect(db), (_req, res) => {
|
|
8510
|
+
res.send(getEnvHTML());
|
|
8511
|
+
});
|
|
7264
8512
|
app.get("/kanban", authOrRedirect(db), (_req, res) => {
|
|
7265
8513
|
res.send(getKanbanHTML());
|
|
7266
8514
|
});
|
|
@@ -7560,13 +8808,3 @@ process.on("SIGINT", () => {
|
|
|
7560
8808
|
process.exit(0);
|
|
7561
8809
|
});
|
|
7562
8810
|
});
|
|
7563
|
-
/*! Bundled license information:
|
|
7564
|
-
|
|
7565
|
-
cookie/index.js:
|
|
7566
|
-
(*!
|
|
7567
|
-
* cookie
|
|
7568
|
-
* Copyright(c) 2012-2014 Roman Shtylman
|
|
7569
|
-
* Copyright(c) 2015 Douglas Christopher Wilson
|
|
7570
|
-
* MIT Licensed
|
|
7571
|
-
*)
|
|
7572
|
-
*/
|