velocious 1.0.570 → 1.0.571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +44 -0
  2. package/build/configuration-types.js +17 -0
  3. package/build/configuration.js +70 -0
  4. package/build/http-server/client/index.js +77 -4
  5. package/build/http-server/client/response-compression.js +224 -0
  6. package/build/http-server/client/response.js +57 -0
  7. package/build/http-server/websocket-events-host.js +91 -48
  8. package/build/src/configuration-types.d.ts +55 -0
  9. package/build/src/configuration-types.d.ts.map +1 -1
  10. package/build/src/configuration-types.js +16 -1
  11. package/build/src/configuration.d.ts +6 -0
  12. package/build/src/configuration.d.ts.map +1 -1
  13. package/build/src/configuration.js +61 -1
  14. package/build/src/http-server/client/index.d.ts +17 -1
  15. package/build/src/http-server/client/index.d.ts.map +1 -1
  16. package/build/src/http-server/client/index.js +74 -5
  17. package/build/src/http-server/client/response-compression.d.ts +71 -0
  18. package/build/src/http-server/client/response-compression.d.ts.map +1 -0
  19. package/build/src/http-server/client/response-compression.js +197 -0
  20. package/build/src/http-server/client/response.d.ts +27 -0
  21. package/build/src/http-server/client/response.d.ts.map +1 -1
  22. package/build/src/http-server/client/response.js +49 -1
  23. package/build/src/http-server/websocket-events-host.d.ts +28 -11
  24. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  25. package/build/src/http-server/websocket-events-host.js +84 -46
  26. package/build/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +4 -3
  28. package/scripts/docker-run.sh +20 -0
  29. package/scripts/verify-docker-dev-environment.js +579 -0
  30. package/src/configuration-types.js +17 -0
  31. package/src/configuration.js +70 -0
  32. package/src/http-server/client/index.js +77 -4
  33. package/src/http-server/client/response-compression.js +224 -0
  34. package/src/http-server/client/response.js +57 -0
  35. package/src/http-server/websocket-events-host.js +91 -48
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Static contract verifier for the canonical Docker development environment.
4
+ *
5
+ * Dependency-free: checks the tracked Dockerfile, compose.yml, .env.example,
6
+ * .dockerignore, .gitignore, and scripts/docker-run.sh against the canonical
7
+ * development-environment contract, then runs negative probes derived from
8
+ * the real files to prove the checks actually catch regressions (for example
9
+ * a standalone project `npm install` or a `COPY` of project source).
10
+ */
11
+
12
+ import fs from "node:fs"
13
+ import path from "node:path"
14
+ import {fileURLToPath} from "node:url"
15
+
16
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
17
+
18
+ const APPROVED_BASE_DIGEST = "sha256:3131b4cc82a783df6c9df078f86e01819a13594b865c2cad47bd1bca2b7063bb"
19
+ const PROVIDER_PACKAGES = ["@moonshot-ai/kimi-code", "@openai/codex", "@anthropic-ai/claude-code", "opencode-ai"]
20
+ const COMPOSE_FORBIDDEN_PATTERN = /hermes|kimi|threadwire|codex|claude|opencode/iu
21
+
22
+ // The complete universal apt coding/debugging baseline shared with the
23
+ // current awesome_tasks canonical image.
24
+ const BASELINE_APT_PACKAGES = [
25
+ "bash",
26
+ "ca-certificates",
27
+ "curl",
28
+ "wget",
29
+ "git",
30
+ "git-lfs",
31
+ "gh",
32
+ "openssh-client",
33
+ "gnupg",
34
+ "jq",
35
+ "ripgrep",
36
+ "fd-find",
37
+ "fzf",
38
+ "less",
39
+ "file",
40
+ "tree",
41
+ "bat",
42
+ "nano",
43
+ "vim-tiny",
44
+ "unzip",
45
+ "zip",
46
+ "xz-utils",
47
+ "bzip2",
48
+ "tar",
49
+ "gzip",
50
+ "rsync",
51
+ "patch",
52
+ "diffutils",
53
+ "gawk",
54
+ "findutils",
55
+ "coreutils",
56
+ "procps",
57
+ "psmisc",
58
+ "lsof",
59
+ "iproute2",
60
+ "iputils-ping",
61
+ "dnsutils",
62
+ "netcat-openbsd",
63
+ "socat",
64
+ "util-linux",
65
+ "tini",
66
+ "python3",
67
+ "python3-venv",
68
+ "python3-pip",
69
+ "sqlite3",
70
+ "shellcheck",
71
+ "tmux",
72
+ "zsh",
73
+ "man-db",
74
+ "build-essential",
75
+ "pkg-config",
76
+ "libssl-dev"
77
+ ]
78
+
79
+ /**
80
+ * Escapes a string for use inside a regular expression.
81
+ *
82
+ * @param {string} value - The literal to escape.
83
+ * @returns {string} - The escaped literal.
84
+ */
85
+ function escapeRegExp(value) {
86
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
87
+ }
88
+
89
+ /**
90
+ * Checks whether an apt package name appears as a whole token.
91
+ *
92
+ * @param {string} content - The Dockerfile content.
93
+ * @param {string} packageName - The apt package name.
94
+ * @returns {boolean} - True when the package name is present.
95
+ */
96
+ function containsAptPackage(content, packageName) {
97
+ const pattern = new RegExp(`(?<![\\w+.-])${escapeRegExp(packageName)}(?![\\w+.-])`, "u")
98
+
99
+ return pattern.test(content)
100
+ }
101
+
102
+ /**
103
+ * Joins backslash line continuations so per-instruction checks see each
104
+ * logical Dockerfile instruction as a single line.
105
+ *
106
+ * @param {string} content - The Dockerfile content.
107
+ * @returns {string[]} - The logical lines.
108
+ */
109
+ function dockerfileLogicalLines(content) {
110
+ return content.replace(/\\\r?\n[ \t]*/g, " ").split("\n")
111
+ }
112
+
113
+ /**
114
+ * Verifies canonical artifact names at the repository root.
115
+ *
116
+ * @param {string[]} entries - Root directory entries.
117
+ * @returns {string[]} - The problems found.
118
+ */
119
+ function verifyRootEntries(entries) {
120
+ const problems = []
121
+
122
+ for (const required of ["Dockerfile", "compose.yml", ".env.example", ".dockerignore"]) {
123
+ if (!entries.includes(required)) {
124
+ problems.push(`Missing required canonical artifact: ${required}`)
125
+ }
126
+ }
127
+
128
+ const allowedDockerNames = new Set(["Dockerfile", "compose.yml", ".dockerignore"])
129
+
130
+ for (const entry of entries) {
131
+ if (/dockerfile|compose/iu.test(entry) && !allowedDockerNames.has(entry)) {
132
+ problems.push(`Forbidden Dockerfile/Compose variant basename: ${entry}`)
133
+ }
134
+ }
135
+
136
+ return problems
137
+ }
138
+
139
+ /**
140
+ * Verifies the source-independent canonical Dockerfile.
141
+ *
142
+ * @param {string} content - The Dockerfile content.
143
+ * @returns {string[]} - The problems found.
144
+ */
145
+ function verifyDockerfile(content) {
146
+ const problems = []
147
+
148
+ if (!content.includes(`FROM ubuntu:26.04@${APPROVED_BASE_DIGEST}`)) {
149
+ problems.push(`Dockerfile must be based on ubuntu:26.04 pinned by the approved digest ${APPROVED_BASE_DIGEST}`)
150
+ }
151
+
152
+ if (/^COPY\s/imu.test(content)) {
153
+ problems.push("Dockerfile must be source-independent: COPY instructions are forbidden")
154
+ }
155
+
156
+ for (const match of content.matchAll(/^ADD\s+(\S+)/gmu)) {
157
+ if (!match[1].startsWith("https://registry.npmjs.org/")) {
158
+ problems.push(`Dockerfile must be source-independent: ADD source is not remote registry metadata: ${match[1]}`)
159
+ }
160
+ }
161
+
162
+ for (const providerPackage of PROVIDER_PACKAGES) {
163
+ if (!content.includes(`https://registry.npmjs.org/${providerPackage}/latest`)) {
164
+ problems.push(`Missing remote latest-metadata cache key for provider package: ${providerPackage}`)
165
+ }
166
+ }
167
+
168
+ if (/npm\s+ci/iu.test(content)) {
169
+ problems.push("Dockerfile must not install project dependencies: npm ci is forbidden")
170
+ }
171
+
172
+ for (const line of dockerfileLogicalLines(content)) {
173
+ if (/npm\s+install/iu.test(line) && !line.includes("--global")) {
174
+ problems.push(`Dockerfile must not install project dependencies: npm install without --global: ${line.trim()}`)
175
+ }
176
+ }
177
+
178
+ for (const providerPackage of PROVIDER_PACKAGES) {
179
+ if (!content.includes(`"${providerPackage}"`)) {
180
+ problems.push(`Missing bare unversioned provider CLI install spec: ${providerPackage}`)
181
+ }
182
+
183
+ if (content.includes(`${providerPackage}@`)) {
184
+ problems.push(`Provider CLI install spec must stay bare and unversioned: ${providerPackage}`)
185
+ }
186
+ }
187
+
188
+ if (/^ARG\s+\w*(KIMI|CODEX|CLAUDE|OPENCODE|PROVIDER)\w*/imu.test(content)) {
189
+ problems.push("Provider version ARGs/pins are forbidden in the Dockerfile")
190
+ }
191
+
192
+ if (!content.includes("--strict-allow-scripts")) {
193
+ problems.push("Missing --strict-allow-scripts lifecycle-script allowlisting")
194
+ }
195
+
196
+ if (!content.includes("--allow-scripts")) {
197
+ problems.push("Missing --allow-scripts lifecycle-script allowlisting")
198
+ }
199
+
200
+ for (const probe of ["kimi --version", "codex --version", "claude --version", "opencode --version"]) {
201
+ if (!content.includes(probe)) {
202
+ problems.push(`Missing provider CLI command probe: ${probe}`)
203
+ }
204
+ }
205
+
206
+ if (!content.includes("deb.nodesource.com/gpgkey/nodesource-repo.gpg.key")) {
207
+ problems.push("Missing signed NodeSource gpg key download")
208
+ }
209
+
210
+ if (!/NODESOURCE_KEY_SHA256=[0-9a-f]{64}/u.test(content)) {
211
+ problems.push("Missing pinned NodeSource key checksum (NODESOURCE_KEY_SHA256)")
212
+ }
213
+
214
+ if (!/sha256sum\s+--check/u.test(content)) {
215
+ problems.push("Missing sha256sum verification of the NodeSource key")
216
+ }
217
+
218
+ if (!content.includes("node_24.x")) {
219
+ problems.push("Missing NodeSource Node.js 24.x repository")
220
+ }
221
+
222
+ for (const aptPackage of BASELINE_APT_PACKAGES) {
223
+ if (!containsAptPackage(content, aptPackage)) {
224
+ problems.push(`Missing baseline apt package: ${aptPackage}`)
225
+ }
226
+ }
227
+
228
+ if (/threadwire/iu.test(content)) {
229
+ problems.push("Threadwire must not be installed in the image: it stays parent orchestration")
230
+ }
231
+
232
+ if (!/usermod\s+--login\s+dev/u.test(content)) {
233
+ problems.push("Dockerfile must rename Ubuntu's existing UID/GID 1000 identity to dev")
234
+ }
235
+
236
+ if (!/^USER\s+dev\s*$/mu.test(content)) {
237
+ problems.push("Dockerfile must run as USER dev (UID/GID 1000)")
238
+ }
239
+
240
+ if (!/^ENV\s+HOME=\/home\/dev\s*$/mu.test(content)) {
241
+ problems.push("Dockerfile must set ENV HOME=/home/dev")
242
+ }
243
+
244
+ if (!/^WORKDIR\s+\/home\/dev\/velocious\s*$/mu.test(content)) {
245
+ problems.push("Dockerfile must set WORKDIR /home/dev/velocious")
246
+ }
247
+
248
+ if (/chown/iu.test(content)) {
249
+ problems.push("Dockerfile must not contain source ownership fixups (chown)")
250
+ }
251
+
252
+ return problems
253
+ }
254
+
255
+ /**
256
+ * Extracts the service names declared under the top-level services key.
257
+ *
258
+ * @param {string} content - The compose.yml content.
259
+ * @returns {string[] | null} - The service names, or null when no services key exists.
260
+ */
261
+ function composeServiceNames(content) {
262
+ const lines = content.split("\n")
263
+ const servicesIndex = lines.findIndex((line) => /^services:\s*$/u.test(line))
264
+
265
+ if (servicesIndex == -1) return null
266
+
267
+ const names = []
268
+
269
+ for (let i = servicesIndex + 1; i < lines.length; i++) {
270
+ const line = lines[i]
271
+
272
+ if (/^\S/u.test(line)) break
273
+
274
+ const match = line.match(/^ {2}([^\s:]+):\s*$/u)
275
+
276
+ if (match) names.push(match[1])
277
+ }
278
+
279
+ return names
280
+ }
281
+
282
+ /**
283
+ * Extracts the volume entries of the first service-level volumes block.
284
+ *
285
+ * @param {string} content - The compose.yml content.
286
+ * @returns {string[]} - The volume entries.
287
+ */
288
+ function composeServiceVolumes(content) {
289
+ const lines = content.split("\n")
290
+ const volumes = []
291
+ let volumesIndent = null
292
+
293
+ for (const line of lines) {
294
+ if (volumesIndent === null) {
295
+ const volumesMatch = line.match(/^(\s+)volumes:\s*$/u)
296
+
297
+ if (volumesMatch) volumesIndent = volumesMatch[1].length
298
+
299
+ continue
300
+ }
301
+
302
+ const entryMatch = line.match(/^(\s+)-\s+(.+?)\s*$/u)
303
+
304
+ if (entryMatch && entryMatch[1].length > volumesIndent) {
305
+ volumes.push(entryMatch[2])
306
+ continue
307
+ }
308
+
309
+ if (line.trim() == "" || line.trimStart().startsWith("#")) continue
310
+
311
+ break
312
+ }
313
+
314
+ return volumes
315
+ }
316
+
317
+ /**
318
+ * Verifies the canonical compose.yml.
319
+ *
320
+ * @param {string} content - The compose.yml content.
321
+ * @returns {string[]} - The problems found.
322
+ */
323
+ function verifyCompose(content) {
324
+ const problems = []
325
+
326
+ if (!/^name:\s*velocious\s*$/mu.test(content)) {
327
+ problems.push("compose.yml must declare the standard Compose project name: velocious")
328
+ }
329
+
330
+ if (COMPOSE_FORBIDDEN_PATTERN.test(content)) {
331
+ problems.push(`compose.yml must not contain provider/orchestrator coupling (matched: ${content.match(COMPOSE_FORBIDDEN_PATTERN)?.[0]})`)
332
+ }
333
+
334
+ const serviceNames = composeServiceNames(content)
335
+
336
+ if (!serviceNames || serviceNames.length != 1 || serviceNames[0] != "dev") {
337
+ problems.push(`compose.yml must declare exactly one dev service (found: ${serviceNames ? serviceNames.join(", ") : "none"})`)
338
+ }
339
+
340
+ if (!/^\s+init:\s*true\s*$/mu.test(content)) {
341
+ problems.push("The dev service must set init: true")
342
+ }
343
+
344
+ if (!/^\s+user:\s*"?1000:1000"?\s*$/mu.test(content)) {
345
+ problems.push("The dev service must run as user 1000:1000")
346
+ }
347
+
348
+ if (!/^\s+working_dir:\s*\/home\/dev\/velocious\s*$/mu.test(content)) {
349
+ problems.push("The dev service must set working_dir /home/dev/velocious")
350
+ }
351
+
352
+ for (const envLine of ["GH_CONFIG_DIR: /home/dev/.config/gh", "HOME: /home/dev", "NODE_ENV: development"]) {
353
+ if (!content.includes(envLine)) {
354
+ problems.push(`The dev service is missing environment entry: ${envLine}`)
355
+ }
356
+ }
357
+
358
+ const expectedVolumes = [
359
+ "${DEV_HOME_PATH:-/home/dev}:/home/dev",
360
+ "${GH_CONFIG_SOURCE_PATH:?Set GH_CONFIG_SOURCE_PATH in .env}:/home/dev/.config/gh:ro"
361
+ ]
362
+ const actualVolumes = composeServiceVolumes(content)
363
+
364
+ if (actualVolumes.length != expectedVolumes.length || !expectedVolumes.every((volume) => actualVolumes.includes(volume))) {
365
+ problems.push(`The dev service must mount exactly the development-home and read-only GH config binds (found: ${actualVolumes.join(", ") || "none"})`)
366
+ }
367
+
368
+ if (!/^\s+command:\s*(\[\s*"sleep",\s*"infinity"\s*\]|sleep\s+infinity)\s*$/mu.test(content)) {
369
+ problems.push("The dev service must run sleep infinity")
370
+ }
371
+
372
+ if (/container_name:/u.test(content)) {
373
+ problems.push("The dev service must not declare a fixed container_name")
374
+ }
375
+
376
+ if (/^\s+ports:\s*$/mu.test(content)) {
377
+ problems.push("The dev service must not publish fixed host ports")
378
+ }
379
+
380
+ if (/^volumes:\s*$/mu.test(content)) {
381
+ problems.push("compose.yml must not declare top-level named volumes")
382
+ }
383
+
384
+ return problems
385
+ }
386
+
387
+ /**
388
+ * Verifies the portable .env.example.
389
+ *
390
+ * @param {string} content - The .env.example content.
391
+ * @returns {string[]} - The problems found.
392
+ */
393
+ function verifyEnvExample(content) {
394
+ const problems = []
395
+
396
+ if (!content.includes("GH_CONFIG_SOURCE_PATH")) {
397
+ problems.push(".env.example must document GH_CONFIG_SOURCE_PATH")
398
+ }
399
+
400
+ if (!content.includes("DEV_HOME_PATH")) {
401
+ problems.push(".env.example must document DEV_HOME_PATH")
402
+ }
403
+
404
+ return problems
405
+ }
406
+
407
+ /**
408
+ * Verifies the .dockerignore exclusions.
409
+ *
410
+ * @param {string} content - The .dockerignore content.
411
+ * @returns {string[]} - The problems found.
412
+ */
413
+ function verifyDockerignore(content) {
414
+ const problems = []
415
+
416
+ for (const required of [".git", ".env", "!.env.example", "node_modules", "build", "coverage", "tmp", "log"]) {
417
+ if (!content.split("\n").some((line) => line.trim() == required || line.trim() == `**/${required}`)) {
418
+ problems.push(`.dockerignore must exclude ${required}`)
419
+ }
420
+ }
421
+
422
+ return problems
423
+ }
424
+
425
+ /**
426
+ * Verifies that the root .env is gitignored.
427
+ *
428
+ * @param {string} content - The .gitignore content.
429
+ * @returns {string[]} - The problems found.
430
+ */
431
+ function verifyGitignore(content) {
432
+ if (!/^\/?\.env\s*$/mu.test(content)) {
433
+ return [".gitignore must ignore the root .env file"]
434
+ }
435
+
436
+ return []
437
+ }
438
+
439
+ /**
440
+ * Verifies the canonical scripts/docker-run.sh helper.
441
+ *
442
+ * @param {string} content - The docker-run.sh content.
443
+ * @returns {string[]} - The problems found.
444
+ */
445
+ function verifyDockerRunSh(content) {
446
+ const problems = []
447
+
448
+ for (const required of ["DEV_HOME_PATH", "velocious", "compose.yml", "--project-directory", "--rm"]) {
449
+ if (!content.includes(required)) {
450
+ problems.push(`scripts/docker-run.sh must reference ${required}`)
451
+ }
452
+ }
453
+
454
+ if (!/compose[^\n]*\brun\b/iu.test(content)) {
455
+ problems.push("scripts/docker-run.sh must execute commands through docker compose run")
456
+ }
457
+
458
+ if (/threadwire/iu.test(content)) {
459
+ problems.push("scripts/docker-run.sh must not reference Threadwire: it stays parent orchestration")
460
+ }
461
+
462
+ return problems
463
+ }
464
+
465
+ /**
466
+ * Builds negative probes derived from the real files. Each probe mutates the
467
+ * real content with a regression and must be caught by the matching verifier.
468
+ *
469
+ * @param {{dockerfile: string, compose: string}} contents - The real file contents.
470
+ * @returns {{name: string, problems: string[], expected: string}[]} - The probes.
471
+ */
472
+ function negativeProbes(contents) {
473
+ return [
474
+ {
475
+ name: "Dockerfile COPY project source regression",
476
+ problems: verifyDockerfile(`${contents.dockerfile}\nCOPY package.json ./\n`),
477
+ expected: "COPY"
478
+ },
479
+ {
480
+ name: "Dockerfile standalone project npm ci regression",
481
+ problems: verifyDockerfile(`${contents.dockerfile}\nRUN npm ci\n`),
482
+ expected: "npm ci"
483
+ },
484
+ {
485
+ name: "Dockerfile multiline project npm install regression",
486
+ problems: verifyDockerfile(`${contents.dockerfile}\nRUN npm \\\n install left-pad\n`),
487
+ expected: "npm install"
488
+ },
489
+ {
490
+ name: "Dockerfile unapproved base digest regression",
491
+ problems: verifyDockerfile(contents.dockerfile.replaceAll(APPROVED_BASE_DIGEST, `sha256:${"0".repeat(64)}`)),
492
+ expected: "digest"
493
+ },
494
+ {
495
+ name: "compose fixed container_name regression",
496
+ problems: verifyCompose(contents.compose.replace(" init: true", " container_name: velocious-dev\n init: true")),
497
+ expected: "container_name"
498
+ },
499
+ {
500
+ name: "compose provider credential mount regression",
501
+ problems: verifyCompose(contents.compose.replace(" command:", " - ${HOME}/.kimi:/home/dev/.kimi\n command:")),
502
+ expected: "kimi"
503
+ },
504
+ {
505
+ name: "compose second service regression",
506
+ problems: verifyCompose(`${contents.compose}\n worker:\n image: ubuntu\n`),
507
+ expected: "dev"
508
+ }
509
+ ]
510
+ }
511
+
512
+ /**
513
+ * Runs the full repository verification.
514
+ *
515
+ * @returns {number} - The process exit code.
516
+ */
517
+ function main() {
518
+ const problems = []
519
+
520
+ problems.push(...verifyRootEntries(fs.readdirSync(repoRoot)))
521
+
522
+ const dockerfilePath = path.join(repoRoot, "Dockerfile")
523
+ const composePath = path.join(repoRoot, "compose.yml")
524
+ const dockerfile = fs.existsSync(dockerfilePath) ? fs.readFileSync(dockerfilePath, "utf8") : null
525
+ const compose = fs.existsSync(composePath) ? fs.readFileSync(composePath, "utf8") : null
526
+
527
+ if (dockerfile !== null) problems.push(...verifyDockerfile(dockerfile))
528
+ if (compose !== null) problems.push(...verifyCompose(compose))
529
+
530
+ for (const [relativePath, verify] of [
531
+ [".env.example", verifyEnvExample],
532
+ [".dockerignore", verifyDockerignore],
533
+ [".gitignore", verifyGitignore],
534
+ [path.join("scripts", "docker-run.sh"), verifyDockerRunSh]
535
+ ]) {
536
+ const absolutePath = path.join(repoRoot, relativePath)
537
+
538
+ if (!fs.existsSync(absolutePath)) {
539
+ problems.push(`Missing required file: ${relativePath}`)
540
+ continue
541
+ }
542
+
543
+ problems.push(...verify(fs.readFileSync(absolutePath, "utf8")))
544
+ }
545
+
546
+ const dockerRunPath = path.join(repoRoot, "scripts", "docker-run.sh")
547
+
548
+ if (fs.existsSync(dockerRunPath) && process.platform != "win32") {
549
+ const mode = fs.statSync(dockerRunPath).mode
550
+
551
+ if ((mode & 0o111) == 0) {
552
+ problems.push("scripts/docker-run.sh must be executable")
553
+ }
554
+ }
555
+
556
+ if (dockerfile !== null && compose !== null) {
557
+ for (const probe of negativeProbes({dockerfile, compose})) {
558
+ if (!probe.problems.some((problem) => problem.toLowerCase().includes(probe.expected.toLowerCase()))) {
559
+ problems.push(`Negative probe was not caught by the verifier: ${probe.name}`)
560
+ }
561
+ }
562
+ }
563
+
564
+ if (problems.length > 0) {
565
+ console.error("Docker development environment verification failed:")
566
+
567
+ for (const problem of problems) {
568
+ console.error(`- ${problem}`)
569
+ }
570
+
571
+ return 1
572
+ }
573
+
574
+ console.log("Docker development environment verification passed (contract checks and negative probes).")
575
+
576
+ return 0
577
+ }
578
+
579
+ process.exitCode = main()
@@ -249,8 +249,25 @@
249
249
  * @property {number} [unreachableReportMs] - Grace window (ms) a beacon connect/disconnect blip must persist before it is reported as a framework-error. Transient outages that recover within this window (e.g. a deploy restarting the broker) are not reported. Defaults to 30000.
250
250
  */
251
251
 
252
+ /**
253
+ * @typedef {object} HttpCompressionConfiguration
254
+ * @property {boolean} [enabled] - Whether buffered response compression is enabled. Defaults to true; set false to disable globally.
255
+ * @property {number} [threshold] - Minimum buffered body size in bytes before compression is applied. Defaults to 1024.
256
+ * @property {number} [brotliQuality] - Brotli encoder quality (0-11). Defaults to 4.
257
+ * @property {number} [gzipLevel] - Gzip compression level (0-9). Defaults to 6.
258
+ */
259
+
260
+ /**
261
+ * @typedef {object} NormalizedHttpCompressionConfiguration
262
+ * @property {boolean} enabled - Whether buffered HTTP response compression is enabled.
263
+ * @property {number} threshold - Minimum buffered body size in bytes before compression is applied.
264
+ * @property {number} brotliQuality - Brotli encoder quality (0-11).
265
+ * @property {number} gzipLevel - Gzip compression level (0-9).
266
+ */
267
+
252
268
  /**
253
269
  * @typedef {object} HttpServerConfiguration
270
+ * @property {boolean | HttpCompressionConfiguration} [compression] - Buffered response compression. Enabled with documented defaults when absent; false or {enabled: false} disables it globally.
254
271
  * @property {string} [host] - Hostname to bind the HTTP server to.
255
272
  * @property {boolean} [inProcess] - Run HTTP handlers in the main thread instead of worker threads.
256
273
  * @property {number} [maxWorkers] - Backward-compatible alias for workers.
@@ -120,6 +120,10 @@ const DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES = 256
120
120
  const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES = 16 * 1024 * 1024
121
121
  const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES = 256
122
122
 
123
+ const DEFAULT_COMPRESSION_THRESHOLD = 1024
124
+ const DEFAULT_COMPRESSION_BROTLI_QUALITY = 4
125
+ const DEFAULT_COMPRESSION_GZIP_LEVEL = 6
126
+
123
127
  /**
124
128
  * Validates a positive safe integer configuration value.
125
129
  * @param {?} value - Configured positive safe integer.
@@ -136,6 +140,63 @@ function positiveSafeInteger(value, name, defaultValue) {
136
140
  return value
137
141
  }
138
142
 
143
+ /**
144
+ * Validates an integer configuration value inside an inclusive range.
145
+ * @param {?} value - Configured integer.
146
+ * @param {string} name - Configuration key.
147
+ * @param {number} min - Minimum accepted value (inclusive).
148
+ * @param {number} max - Maximum accepted value (inclusive).
149
+ * @param {number} defaultValue - Default value.
150
+ * @returns {number} - Validated configured or default value.
151
+ */
152
+ function integerInRange(value, name, min, max, defaultValue) {
153
+ if (value === undefined) return defaultValue
154
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
155
+ throw new TypeError(`${name} must be an integer between ${min} and ${max}`)
156
+ }
157
+
158
+ return value
159
+ }
160
+
161
+ /**
162
+ * Normalizes the buffered HTTP response compression configuration. Compression is
163
+ * enabled by default when the setting is absent; `false` or `{enabled: false}`
164
+ * disables it globally.
165
+ * @param {boolean | import("./configuration-types.js").HttpCompressionConfiguration | undefined} value - Configured compression value.
166
+ * @returns {import("./configuration-types.js").NormalizedHttpCompressionConfiguration} - Normalized compression configuration.
167
+ */
168
+ function normalizeHttpCompression(value) {
169
+ if (value === undefined || value === true) {
170
+ return {enabled: true, threshold: DEFAULT_COMPRESSION_THRESHOLD, brotliQuality: DEFAULT_COMPRESSION_BROTLI_QUALITY, gzipLevel: DEFAULT_COMPRESSION_GZIP_LEVEL}
171
+ }
172
+
173
+ if (value === false) {
174
+ return {enabled: false, threshold: DEFAULT_COMPRESSION_THRESHOLD, brotliQuality: DEFAULT_COMPRESSION_BROTLI_QUALITY, gzipLevel: DEFAULT_COMPRESSION_GZIP_LEVEL}
175
+ }
176
+
177
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
178
+ throw new TypeError(`httpServer.compression must be a boolean or an object, got: ${String(value)}`)
179
+ }
180
+
181
+ const {brotliQuality, enabled, gzipLevel, threshold, ...restCompression} = value
182
+ const restCompressionKeys = Object.keys(restCompression)
183
+
184
+ if (restCompressionKeys.length > 0) {
185
+ throw new TypeError(`httpServer.compression received unknown keys: ${restCompressionKeys.join(", ")} (supported: brotliQuality, enabled, gzipLevel, threshold)`)
186
+ }
187
+
188
+ if (enabled !== undefined && typeof enabled !== "boolean") {
189
+ throw new TypeError(`httpServer.compression.enabled must be a boolean, got: ${String(enabled)}`)
190
+ }
191
+
192
+ return {
193
+ enabled: enabled ?? true,
194
+ threshold: positiveSafeInteger(threshold, "httpServer.compression.threshold", DEFAULT_COMPRESSION_THRESHOLD),
195
+ brotliQuality: integerInRange(brotliQuality, "httpServer.compression.brotliQuality", 0, 11, DEFAULT_COMPRESSION_BROTLI_QUALITY),
196
+ gzipLevel: integerInRange(gzipLevel, "httpServer.compression.gzipLevel", 0, 9, DEFAULT_COMPRESSION_GZIP_LEVEL)
197
+ }
198
+ }
199
+
139
200
  export default class VelociousConfiguration {
140
201
  /**
141
202
  * Close database connections promise.
@@ -236,6 +297,7 @@ export default class VelociousConfiguration {
236
297
 
237
298
  this.httpServer = {
238
299
  ...(httpServer || {}),
300
+ compression: normalizeHttpCompression(httpServer?.compression),
239
301
  websocketInboundQueue: {
240
302
  maxPendingBytes: positiveSafeInteger(websocketInboundQueue?.maxPendingBytes, "httpServer.websocketInboundQueue.maxPendingBytes", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_BYTES),
241
303
  maxPendingMessages: positiveSafeInteger(websocketInboundQueue?.maxPendingMessages, "httpServer.websocketInboundQueue.maxPendingMessages", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES)
@@ -500,6 +562,14 @@ export default class VelociousConfiguration {
500
562
  return this.cors
501
563
  }
502
564
 
565
+ /**
566
+ * Runs get http server compression.
567
+ * @returns {import("./configuration-types.js").NormalizedHttpCompressionConfiguration} - Normalized buffered response compression configuration.
568
+ */
569
+ getHttpServerCompression() {
570
+ return this.httpServer.compression
571
+ }
572
+
503
573
  /**
504
574
  * Runs get cookie secret.
505
575
  * @returns {string | undefined} - Cookie secret.