@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.
@@ -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 = (0, import_cookie.parse)(cookieHeader);
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(50).regex(/^[a-z0-9_]+$/, "Only lowercase letters, digits and underscores"),
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(50).regex(/^[a-z0-9_]+$/, "Only lowercase letters, digits and underscores"),
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 underscores only">
7077
- <div class="hint">Lowercase letters, digits and underscores only</div>
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/daemon.ts
7151
- import rateLimit from "express-rate-limit";
7152
- import cors from "cors";
7153
- import { z as z4 } from "zod";
7154
- function validate3(schema) {
7155
- return (req, res, next) => {
7156
- const result = schema.safeParse(req.body);
7157
- if (!result.success) {
7158
- return res.status(400).json({
7159
- error: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
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
- req.body = result.data;
7163
- next();
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
- */