quadwork 1.11.0 → 1.12.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 +7 -0
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +2 -2
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +2 -2
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/0nk3kw~5j75~v.css +2 -0
- package/out/_next/static/chunks/{0gaekhrfy94vz.js → 11h7y0f5o9.hx.js} +1 -1
- package/out/_next/static/chunks/{0hirada7763yr.js → 13w.n.3zipzvz.js} +9 -9
- package/out/_not-found/__next._full.txt +2 -2
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +2 -2
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +2 -2
- package/out/app-shell/__next._full.txt +2 -2
- package/out/app-shell/__next._head.txt +1 -1
- package/out/app-shell/__next._index.txt +2 -2
- package/out/app-shell/__next._tree.txt +2 -2
- package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
- package/out/app-shell/__next.app-shell.txt +1 -1
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/project/_/__next._full.txt +3 -3
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +2 -2
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/queue/__next._full.txt +2 -2
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +2 -2
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +2 -2
- package/out/project/_.html +1 -1
- package/out/project/_.txt +3 -3
- package/out/settings/__next._full.txt +2 -2
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +2 -2
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +1 -1
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +2 -2
- package/out/setup/__next._full.txt +2 -2
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +2 -2
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +1 -1
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +2 -2
- package/package.json +7 -7
- package/server/__tests__/bridge-auto-stop-guard.test.js +134 -0
- package/server/__tests__/rate-limit-handling.test.js +168 -0
- package/server/__tests__/scrub-secrets.test.js +234 -0
- package/server/__tests__/v1110-security-qa.test.js +312 -0
- package/server/index.js +10 -51
- package/server/routes.js +168 -73
- package/server/scrub-secrets.js +51 -0
- package/out/_next/static/chunks/0a4.d381szseh.css +0 -2
- /package/out/_next/static/{a1_CwwdhUp5-lHCPnFaTw → nkNB54Q5aOvoEsUmAlro2}/_buildManifest.js +0 -0
- /package/out/_next/static/{a1_CwwdhUp5-lHCPnFaTw → nkNB54Q5aOvoEsUmAlro2}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{a1_CwwdhUp5-lHCPnFaTw → nkNB54Q5aOvoEsUmAlro2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #549: QA verification for v1.11.0 security hardening.
|
|
3
|
+
*
|
|
4
|
+
* Covers PRs #543, #546, #547, #548:
|
|
5
|
+
* - #543: execFileSync refactor (no shell interpolation)
|
|
6
|
+
* - #546: Next.js upgrade to 16.2.4
|
|
7
|
+
* - #547: File/directory permission hardening (0o700/0o600)
|
|
8
|
+
* - #548: WebSocket resize input validation
|
|
9
|
+
*
|
|
10
|
+
* Integration-level items (wizard flow, browser open, AC start,
|
|
11
|
+
* multi-tab WS) require a live server and are verified by code-path
|
|
12
|
+
* analysis in the PR description.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const assert = require("assert");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
|
|
22
|
+
function test(name, fn) {
|
|
23
|
+
try {
|
|
24
|
+
fn();
|
|
25
|
+
passed++;
|
|
26
|
+
console.log(` PASS ${name}`);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
failed++;
|
|
29
|
+
console.error(` FAIL ${name}`);
|
|
30
|
+
console.error(` ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ROOT = path.resolve(__dirname, "../..");
|
|
35
|
+
const SERVER_DIR = path.resolve(__dirname, "..");
|
|
36
|
+
|
|
37
|
+
console.log("\n#549 QA — v1.11.0 security hardening\n");
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// PR #543 — execFileSync refactor
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
console.log("--- PR #543: execFileSync refactor ---\n");
|
|
44
|
+
|
|
45
|
+
test("bin/quadwork.js has no execSync import", () => {
|
|
46
|
+
const src = fs.readFileSync(path.join(ROOT, "bin/quadwork.js"), "utf-8");
|
|
47
|
+
// execFileSync is OK, execSync is not
|
|
48
|
+
const lines = src.split("\n");
|
|
49
|
+
const badLines = lines.filter(
|
|
50
|
+
(l) => /\bexecSync\b/.test(l) && !/execFileSync/.test(l)
|
|
51
|
+
);
|
|
52
|
+
assert.strictEqual(badLines.length, 0, `Found execSync: ${badLines[0]}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("server/index.js has no execSync import", () => {
|
|
56
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
57
|
+
const lines = src.split("\n");
|
|
58
|
+
const badLines = lines.filter(
|
|
59
|
+
(l) => /\bexecSync\b/.test(l) && !/execFileSync/.test(l)
|
|
60
|
+
);
|
|
61
|
+
assert.strictEqual(badLines.length, 0, `Found execSync: ${badLines[0]}`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("server/install-agentchattr.js has no execSync import", () => {
|
|
65
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "install-agentchattr.js"), "utf-8");
|
|
66
|
+
const lines = src.split("\n");
|
|
67
|
+
const badLines = lines.filter(
|
|
68
|
+
(l) => /\bexecSync\b/.test(l) && !/execFileSync/.test(l)
|
|
69
|
+
);
|
|
70
|
+
assert.strictEqual(badLines.length, 0, `Found execSync: ${badLines[0]}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("scripts/e2e-per-project.js has no execSync import", () => {
|
|
74
|
+
const src = fs.readFileSync(path.join(ROOT, "scripts/e2e-per-project.js"), "utf-8");
|
|
75
|
+
const lines = src.split("\n");
|
|
76
|
+
const badLines = lines.filter(
|
|
77
|
+
(l) => /\bexecSync\b/.test(l) && !/execFileSync/.test(l)
|
|
78
|
+
);
|
|
79
|
+
assert.strictEqual(badLines.length, 0, `Found execSync: ${badLines[0]}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("bin/quadwork.js run() uses execFileSync with array args", () => {
|
|
83
|
+
const src = fs.readFileSync(path.join(ROOT, "bin/quadwork.js"), "utf-8");
|
|
84
|
+
assert(src.includes("function run(cmd, args = [], opts = {})"), "run() signature should accept array args");
|
|
85
|
+
assert(src.includes("execFileSync(cmd, args,"), "run() should call execFileSync with cmd, args");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("bin/quadwork.js which() uses run() with array args", () => {
|
|
89
|
+
const src = fs.readFileSync(path.join(ROOT, "bin/quadwork.js"), "utf-8");
|
|
90
|
+
assert(src.includes('run("which", [cmd])'), "which() should use array args");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("bin/quadwork.js browser-open uses execFileSync (no shell)", () => {
|
|
94
|
+
const src = fs.readFileSync(path.join(ROOT, "bin/quadwork.js"), "utf-8");
|
|
95
|
+
// Windows path: cmd /c start
|
|
96
|
+
assert(src.includes('execFileSync("cmd", ["/c", "start"'), "Windows browser open should use execFileSync");
|
|
97
|
+
// macOS/Linux: open or xdg-open
|
|
98
|
+
assert(src.includes('execFileSync(process.platform === "darwin" ? "open" : "xdg-open"'), "macOS/Linux browser open should use execFileSync");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("server/index.js isCliInstalled uses execFileSync", () => {
|
|
102
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
103
|
+
assert(src.includes('execFileSync("which", [cmd]'), "isCliInstalled should use execFileSync");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("server/index.js killProcessOnPort uses execFileSync", () => {
|
|
107
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
108
|
+
assert(src.includes('execFileSync("lsof", ["-ti"'), "killProcessOnPort should use execFileSync");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// PR #546 — Next.js upgrade
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
console.log("\n--- PR #546: Next.js upgrade ---\n");
|
|
116
|
+
|
|
117
|
+
test("package.json specifies Next.js >= 16.2.4 (not vulnerable 16.2.1-16.2.3)", () => {
|
|
118
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8"));
|
|
119
|
+
const nextVersion = pkg.dependencies?.next || "";
|
|
120
|
+
// Strip leading ^ or ~ for comparison
|
|
121
|
+
const bare = nextVersion.replace(/^[~^]+/, "");
|
|
122
|
+
const [major, minor, patch] = bare.split(".").map(Number);
|
|
123
|
+
assert(
|
|
124
|
+
major > 16 || (major === 16 && minor > 2) || (major === 16 && minor === 2 && patch >= 4),
|
|
125
|
+
`Expected next >= 16.2.4, got ${nextVersion}`
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("package-lock.json has Next.js 16.2.4+ (resolved version)", () => {
|
|
130
|
+
const lock = JSON.parse(fs.readFileSync(path.join(ROOT, "package-lock.json"), "utf-8"));
|
|
131
|
+
const nextPkg = lock.packages?.["node_modules/next"];
|
|
132
|
+
assert(nextPkg, "next should be in package-lock.json");
|
|
133
|
+
const version = nextPkg.version;
|
|
134
|
+
const [major, minor, patch] = version.split(".").map(Number);
|
|
135
|
+
assert(
|
|
136
|
+
major > 16 || (major === 16 && minor > 2) || (major === 16 && minor === 2 && patch >= 4),
|
|
137
|
+
`Expected >= 16.2.4, got ${version}`
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("next build succeeds (runs build from scratch)", () => {
|
|
142
|
+
// Actually run the build to verify Next.js 16.2.4 works.
|
|
143
|
+
const { execFileSync } = require("child_process");
|
|
144
|
+
try {
|
|
145
|
+
execFileSync("npx", ["next", "build"], {
|
|
146
|
+
cwd: ROOT,
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
timeout: 120000,
|
|
149
|
+
});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
assert.fail(`next build failed: ${e.stderr?.toString().slice(-500) || e.message}`);
|
|
152
|
+
}
|
|
153
|
+
const outIndex = path.join(ROOT, "out", "index.html");
|
|
154
|
+
assert(fs.existsSync(outIndex), "out/index.html should exist after build");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// PR #547 — File/directory permission hardening
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
console.log("\n--- PR #547: File/directory permission hardening ---\n");
|
|
162
|
+
|
|
163
|
+
test("server/config.js exports ensureSecureDir", () => {
|
|
164
|
+
const config = require(path.join(SERVER_DIR, "config.js"));
|
|
165
|
+
assert(typeof config.ensureSecureDir === "function", "ensureSecureDir should be exported");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("server/config.js exports writeSecureFile", () => {
|
|
169
|
+
const config = require(path.join(SERVER_DIR, "config.js"));
|
|
170
|
+
assert(typeof config.writeSecureFile === "function", "writeSecureFile should be exported");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("server/config.js exports writeConfig", () => {
|
|
174
|
+
const config = require(path.join(SERVER_DIR, "config.js"));
|
|
175
|
+
assert(typeof config.writeConfig === "function", "writeConfig should be exported");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("ensureSecureDir creates dir with 0o700", () => {
|
|
179
|
+
const { ensureSecureDir } = require(path.join(SERVER_DIR, "config.js"));
|
|
180
|
+
const tmpDir = path.join(require("os").tmpdir(), `qw-qa-test-${Date.now()}`);
|
|
181
|
+
try {
|
|
182
|
+
ensureSecureDir(tmpDir);
|
|
183
|
+
const stat = fs.statSync(tmpDir);
|
|
184
|
+
assert(stat.isDirectory(), "should be a directory");
|
|
185
|
+
assert.strictEqual(stat.mode & 0o777, 0o700, `expected 0o700, got ${(stat.mode & 0o777).toString(8)}`);
|
|
186
|
+
} finally {
|
|
187
|
+
try { fs.rmdirSync(tmpDir); } catch {}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("writeSecureFile creates file with 0o600", () => {
|
|
192
|
+
const { writeSecureFile } = require(path.join(SERVER_DIR, "config.js"));
|
|
193
|
+
const tmpFile = path.join(require("os").tmpdir(), `qw-qa-test-${Date.now()}.txt`);
|
|
194
|
+
try {
|
|
195
|
+
writeSecureFile(tmpFile, "test content");
|
|
196
|
+
const stat = fs.statSync(tmpFile);
|
|
197
|
+
assert(stat.isFile(), "should be a file");
|
|
198
|
+
assert.strictEqual(stat.mode & 0o777, 0o600, `expected 0o600, got ${(stat.mode & 0o777).toString(8)}`);
|
|
199
|
+
assert.strictEqual(fs.readFileSync(tmpFile, "utf-8"), "test content");
|
|
200
|
+
} finally {
|
|
201
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("server/index.js imports ensureSecureDir and writeSecureFile", () => {
|
|
206
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
207
|
+
assert(src.includes("ensureSecureDir"), "should import ensureSecureDir");
|
|
208
|
+
assert(src.includes("writeSecureFile"), "should import writeSecureFile");
|
|
209
|
+
assert(src.includes("writeConfig"), "should import writeConfig");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("server/routes.js imports ensureSecureDir and writeSecureFile", () => {
|
|
213
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "routes.js"), "utf-8");
|
|
214
|
+
assert(src.includes("ensureSecureDir"), "should import ensureSecureDir");
|
|
215
|
+
assert(src.includes("writeSecureFile"), "should import writeSecureFile");
|
|
216
|
+
assert(src.includes("writeConfig"), "should import writeConfig");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("no raw mkdirSync without mode in server files", () => {
|
|
220
|
+
const files = ["index.js", "routes.js", "config.js"];
|
|
221
|
+
for (const file of files) {
|
|
222
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, file), "utf-8");
|
|
223
|
+
const lines = src.split("\n");
|
|
224
|
+
const badLines = lines.filter(
|
|
225
|
+
(l) =>
|
|
226
|
+
/fs\.mkdirSync\(/.test(l) &&
|
|
227
|
+
!l.includes("mode:") &&
|
|
228
|
+
!l.includes("ensureSecureDir") &&
|
|
229
|
+
!l.trim().startsWith("//")
|
|
230
|
+
);
|
|
231
|
+
assert.strictEqual(
|
|
232
|
+
badLines.length,
|
|
233
|
+
0,
|
|
234
|
+
`${file} has raw mkdirSync without mode: ${badLines[0]}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("bin/quadwork.js uses ensureSecureDir for directory creation", () => {
|
|
240
|
+
const src = fs.readFileSync(path.join(ROOT, "bin/quadwork.js"), "utf-8");
|
|
241
|
+
assert(src.includes("ensureSecureDir"), "bin/quadwork.js should use ensureSecureDir");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("install-agentchattr.js uses mode: 0o700 for mkdirSync", () => {
|
|
245
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "install-agentchattr.js"), "utf-8");
|
|
246
|
+
const lines = src.split("\n");
|
|
247
|
+
const mkdirLines = lines.filter((l) => /fs\.mkdirSync\(/.test(l) && !l.trim().startsWith("//"));
|
|
248
|
+
for (const line of mkdirLines) {
|
|
249
|
+
assert(line.includes("mode: 0o700"), `Missing mode: 0o700 in: ${line.trim()}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("install-agentchattr.js uses mode: 0o600 for writeFileSync (security-sensitive files)", () => {
|
|
254
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "install-agentchattr.js"), "utf-8");
|
|
255
|
+
const lines = src.split("\n");
|
|
256
|
+
const writeLines = lines.filter(
|
|
257
|
+
(l) =>
|
|
258
|
+
/fs\.writeFileSync\(/.test(l) &&
|
|
259
|
+
!l.trim().startsWith("//") &&
|
|
260
|
+
// CSS/JS patches are non-sensitive (cosmetic overrides to AC UI)
|
|
261
|
+
!l.includes("cssPath") &&
|
|
262
|
+
!l.includes("jsPath")
|
|
263
|
+
);
|
|
264
|
+
for (const line of writeLines) {
|
|
265
|
+
assert(line.includes("mode: 0o600"), `Missing mode: 0o600 in: ${line.trim()}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// PR #548 — WebSocket resize validation
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
console.log("\n--- PR #548: WebSocket resize validation ---\n");
|
|
274
|
+
|
|
275
|
+
test("server/index.js has typeof number check for resize cols", () => {
|
|
276
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
277
|
+
assert(src.includes('typeof parsed.cols === "number"'), "should check typeof cols");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("server/index.js has typeof number check for resize rows", () => {
|
|
281
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
282
|
+
assert(src.includes('typeof parsed.rows === "number"'), "should check typeof rows");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("server/index.js validates Number.isFinite for resize", () => {
|
|
286
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
287
|
+
assert(src.includes("Number.isFinite(parsed.cols)"), "should validate isFinite for cols");
|
|
288
|
+
assert(src.includes("Number.isFinite(parsed.rows)"), "should validate isFinite for rows");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("server/index.js bounds-checks resize to 1-500", () => {
|
|
292
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
293
|
+
assert(src.includes("parsed.cols >= 1"), "should lower-bound cols");
|
|
294
|
+
assert(src.includes("parsed.cols <= 500"), "should upper-bound cols");
|
|
295
|
+
assert(src.includes("parsed.rows >= 1"), "should lower-bound rows");
|
|
296
|
+
assert(src.includes("parsed.rows <= 500"), "should upper-bound rows");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("resize validation is inside the resize handler block", () => {
|
|
300
|
+
const src = fs.readFileSync(path.join(SERVER_DIR, "index.js"), "utf-8");
|
|
301
|
+
// Verify the validation is between 'parsed.type === "resize"' and 'session.term.resize'
|
|
302
|
+
const resizeIdx = src.indexOf('parsed.type === "resize"');
|
|
303
|
+
const termResizeIdx = src.indexOf("session.term.resize(parsed.cols, parsed.rows)");
|
|
304
|
+
const typeofIdx = src.indexOf('typeof parsed.cols === "number"');
|
|
305
|
+
assert(resizeIdx > 0 && termResizeIdx > resizeIdx && typeofIdx > resizeIdx && typeofIdx < termResizeIdx,
|
|
306
|
+
"validation should be between resize type check and term.resize call");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed, ${passed + failed} total\n`);
|
|
312
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/server/index.js
CHANGED
|
@@ -1402,7 +1402,12 @@ async function sendTriggerMessage(projectId) {
|
|
|
1402
1402
|
console.log(`[auto-trigger] ${projectId}: caffeinate auto-stopped (no active triggers remain)`);
|
|
1403
1403
|
}
|
|
1404
1404
|
// #518: also stop bridges when batch completes
|
|
1405
|
-
|
|
1405
|
+
// #542: transition guard — only stop if not already stopped for this completion
|
|
1406
|
+
const prev = _bridgeBatchPrev.get(projectId);
|
|
1407
|
+
_bridgeBatchPrev.set(projectId, { complete: true, hasItems: !!(bp.items && bp.items.length) });
|
|
1408
|
+
if (!prev || !prev.complete) {
|
|
1409
|
+
await autoStopBridges(projectId, project, qwPort);
|
|
1410
|
+
}
|
|
1406
1411
|
return;
|
|
1407
1412
|
}
|
|
1408
1413
|
}
|
|
@@ -1718,55 +1723,8 @@ app.use((req, res, next) => {
|
|
|
1718
1723
|
}
|
|
1719
1724
|
});
|
|
1720
1725
|
|
|
1721
|
-
// --- #538: PTY output secret scrubbing ---
|
|
1722
|
-
|
|
1723
|
-
// replay so echoed credentials are not exposed to dashboard clients.
|
|
1724
|
-
//
|
|
1725
|
-
// Threat model: QuadWork binds to 127.0.0.1 only. The scrub is
|
|
1726
|
-
// defense-in-depth — it reduces exposure if a secret is accidentally
|
|
1727
|
-
// echoed, but cannot catch every possible format. Operators who handle
|
|
1728
|
-
// highly sensitive credentials should avoid echoing them in agent
|
|
1729
|
-
// terminals.
|
|
1730
|
-
//
|
|
1731
|
-
// Live chunks from term.onData() are typically line-aligned (shell
|
|
1732
|
-
// flushes on newline), so per-chunk scrubbing catches the vast majority
|
|
1733
|
-
// of secrets. A secret split across two chunks is a theoretical edge
|
|
1734
|
-
// case that the scrollback scrub (which sees the full buffer) covers
|
|
1735
|
-
// on reconnect.
|
|
1736
|
-
|
|
1737
|
-
// Patterns that indicate a line contains a secret value.
|
|
1738
|
-
const _SECRET_NAME_RE = /\b\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|PASSPHRASE|AUTH)\w*\s*[=:]/i;
|
|
1739
|
-
// Known API key prefixes (Anthropic, GitHub, OpenAI, etc.).
|
|
1740
|
-
const _API_KEY_PREFIX_RE = /\b(sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36,}|ghu_[A-Za-z0-9]{36,}|ghs_[A-Za-z0-9]{36,}|sk-[A-Za-z0-9]{20,}|xoxb-[A-Za-z0-9-]{20,}|xoxp-[A-Za-z0-9-]{20,})\b/;
|
|
1741
|
-
// Bearer authorization headers.
|
|
1742
|
-
const _BEARER_RE = /\bBearer\s+[A-Za-z0-9_.+/=-]{20,}/i;
|
|
1743
|
-
const _REDACTED = "[REDACTED]";
|
|
1744
|
-
|
|
1745
|
-
function scrubSecrets(text) {
|
|
1746
|
-
if (!text) return text;
|
|
1747
|
-
return text.split("\n").map((line) => {
|
|
1748
|
-
// Strip ANSI escape codes for pattern matching, but redact the
|
|
1749
|
-
// original line (preserves terminal formatting around non-secret
|
|
1750
|
-
// lines while ensuring secrets inside styled output are caught).
|
|
1751
|
-
const plain = line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
1752
|
-
if (_SECRET_NAME_RE.test(plain)) {
|
|
1753
|
-
// Redact the value portion after the = or : delimiter.
|
|
1754
|
-
return line.replace(/([=:])\s*\S.*/, `$1 ${_REDACTED}`);
|
|
1755
|
-
}
|
|
1756
|
-
if (_API_KEY_PREFIX_RE.test(plain)) {
|
|
1757
|
-
return line.replace(_API_KEY_PREFIX_RE, _REDACTED);
|
|
1758
|
-
}
|
|
1759
|
-
if (_BEARER_RE.test(plain)) {
|
|
1760
|
-
return line.replace(/\bBearer\s+[A-Za-z0-9_.+/=-]{20,}/gi, `Bearer ${_REDACTED}`);
|
|
1761
|
-
}
|
|
1762
|
-
return line;
|
|
1763
|
-
}).join("\n");
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
function scrubScrollback(buf) {
|
|
1767
|
-
if (!buf || buf.length === 0) return buf;
|
|
1768
|
-
return Buffer.from(scrubSecrets(buf.toString("utf-8")), "utf-8");
|
|
1769
|
-
}
|
|
1726
|
+
// --- #538: PTY output secret scrubbing (extracted to scrub-secrets.js) ---
|
|
1727
|
+
const { scrubSecrets, scrubScrollback } = require("./scrub-secrets");
|
|
1770
1728
|
|
|
1771
1729
|
// --- WebSocket + PTY ---
|
|
1772
1730
|
// WS connects to an existing PTY session (started via lifecycle API)
|
|
@@ -1933,7 +1891,8 @@ async function autoStopPollingTick() {
|
|
|
1933
1891
|
}
|
|
1934
1892
|
}
|
|
1935
1893
|
// #518: also stop bridges when batch completes
|
|
1936
|
-
|
|
1894
|
+
// #542: only fire on the transition (incomplete→complete), not every tick
|
|
1895
|
+
if (hasBridgeAuto && (!prev || !prev.complete)) {
|
|
1937
1896
|
await autoStopBridges(project.id, project, qwPort);
|
|
1938
1897
|
}
|
|
1939
1898
|
}
|