patchwarden 1.1.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -0,0 +1,294 @@
1
+ /**
2
+ * PatchWarden Control Center — shared infrastructure.
3
+ *
4
+ * Holds the fault-tolerant config bootstrap, path constants, the in-memory
5
+ * control token, port resolution, and the generic HTTP/file helpers reused by
6
+ * every route module. Importing this module performs the same bootstrap side
7
+ * effects the original monolithic controlCenter.ts did (config load, token
8
+ * generation, port resolution) so that a single import wires up the runtime.
9
+ */
10
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
11
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { randomUUID } from "node:crypto";
14
+ import { homedir } from "node:os";
15
+ import { type IncomingMessage, type ServerResponse } from "node:http";
16
+ import { loadConfig, type PatchWardenConfig } from "../config.js";
17
+ import { logger } from "../logging.js";
18
+
19
+ // ── Paths ─────────────────────────────────────────────────────────
20
+
21
+ export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
22
+ export const uiRoot = join(projectRoot, "ui");
23
+ export const manageScriptPath = join(projectRoot, "scripts", "control", "manage-patchwarden.ps1");
24
+
25
+ export const PAGE_ALIASES: Record<string, string> = {
26
+ "/dashboard.html": "pages/dashboard.html",
27
+ "/tasks.html": "pages/tasks.html",
28
+ "/workspace.html": "pages/workspace.html",
29
+ "/audit.html": "pages/audit.html",
30
+ "/task-detail.html": "pages/task-detail.html",
31
+ "/direct-sessions.html": "pages/direct-sessions.html",
32
+ "/logs.html": "pages/logs.html",
33
+ };
34
+
35
+ // ── Config (fault-tolerant bootstrap) ─────────────────────────────
36
+
37
+ export function createFallbackConfig(): PatchWardenConfig {
38
+ return {
39
+ workspaceRoot: process.cwd(),
40
+ plansDir: ".patchwarden/plans",
41
+ tasksDir: ".patchwarden/tasks",
42
+ assessmentsDir: ".patchwarden/assessments",
43
+ assessmentTtlSeconds: 3600,
44
+ agents: {},
45
+ allowedTestCommands: [],
46
+ repoAllowedTestCommands: {},
47
+ maxReadFileBytes: 200_000,
48
+ defaultTaskTimeoutSeconds: 900,
49
+ maxTaskTimeoutSeconds: 3600,
50
+ watcherStaleSeconds: 30,
51
+ toolProfile: "full",
52
+ enableDirectProfile: false,
53
+ directSessionsDir: ".patchwarden/direct-sessions",
54
+ directSessionTtlSeconds: 3600,
55
+ directMaxPatchBytes: 200_000,
56
+ directMaxFileBytes: 500_000,
57
+ };
58
+ }
59
+
60
+ export let config: PatchWardenConfig;
61
+ try {
62
+ config = loadConfig();
63
+ } catch (err) {
64
+ logger.warn(`[control-center] WARNING: Failed to load config. Using fallback defaults.`, {
65
+ error: errorMessage(err),
66
+ });
67
+ config = createFallbackConfig();
68
+ }
69
+
70
+ // ── Control token (in-memory only) ────────────────────────────────
71
+
72
+ export const controlToken = randomUUID();
73
+
74
+ // ── Port resolution ───────────────────────────────────────────────
75
+
76
+ export function resolvePort(): number {
77
+ const argv = process.argv.slice(2);
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const arg = argv[i];
80
+ if (arg === "--port" && i + 1 < argv.length) {
81
+ const n = parseInt(argv[i + 1], 10);
82
+ if (Number.isFinite(n) && n >= 0) return n;
83
+ }
84
+ const m = arg.match(/^--port=(\d+)$/);
85
+ if (m) {
86
+ const n = parseInt(m[1], 10);
87
+ if (Number.isFinite(n) && n >= 0) return n;
88
+ }
89
+ }
90
+ const envPort = parseInt(process.env.PATCHWARDEN_CONTROL_PORT || "", 10);
91
+ if (Number.isFinite(envPort) && envPort >= 0) return envPort;
92
+ return 8090;
93
+ }
94
+
95
+ export const port = resolvePort();
96
+ export const host = "127.0.0.1";
97
+
98
+ // Core/Direct probe base URLs — overridable for tests so the smoke test does
99
+ // not depend on the real 8080/8081 ports being free on the host.
100
+ export const CORE_BASE_URL = process.env.PATCHWARDEN_CORE_URL || "http://127.0.0.1:8080";
101
+ export const DIRECT_BASE_URL = process.env.PATCHWARDEN_DIRECT_URL || "http://127.0.0.1:8081";
102
+ export const DEFAULT_TUNNEL_CLIENT_EXE = "D:\\ai_agent\\tunnel-client-v0.0.9--context-conduit-topaz-windows-amd64\\tunnel-client.exe";
103
+
104
+ export const CONTROL_CENTER_FAVICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
105
+ <rect width="64" height="64" rx="14" fill="#0a0e14"/>
106
+ <path d="M32 52s17-8 17-27V14L32 8 15 14v11c0 19 17 27 17 27z" fill="#111820" stroke="#2dd4a8" stroke-width="4" stroke-linejoin="round"/>
107
+ <path d="M24 32l6 6 12-14" fill="none" stroke="#2dd4a8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
108
+ </svg>`;
109
+
110
+ // ── Helpers ───────────────────────────────────────────────────────
111
+
112
+ export function errorMessage(err: unknown): string {
113
+ return err instanceof Error ? err.message : String(err);
114
+ }
115
+
116
+ export function getRuntimeRoot(direct: boolean): string {
117
+ const base =
118
+ process.platform === "win32" && process.env.LOCALAPPDATA
119
+ ? join(process.env.LOCALAPPDATA, "patchwarden")
120
+ : join(homedir(), ".patchwarden");
121
+ return join(base, direct ? "runtime-direct" : "runtime");
122
+ }
123
+
124
+ export function getControlCenterLogDir(): string {
125
+ // Test/local override: when set to an absolute path, use it directly so the
126
+ // smoke test can redirect status/events/log files into a sandbox-writable
127
+ // directory under the project root instead of LOCALAPPDATA.
128
+ const override = process.env.PATCHWARDEN_CONTROL_LOG_DIR;
129
+ if (override && isAbsolute(override)) return override;
130
+ const base =
131
+ process.platform === "win32" && process.env.LOCALAPPDATA
132
+ ? join(process.env.LOCALAPPDATA, "patchwarden")
133
+ : join(homedir(), ".patchwarden");
134
+ return join(base, "control-center");
135
+ }
136
+
137
+ // Status + events files live alongside the control-center logs so the launcher
138
+ // can discover a running instance without probing the port blindly.
139
+ export const controlCenterStatusPath = join(getControlCenterLogDir(), "control-center-status.json");
140
+ export const controlCenterEventsPath = join(getControlCenterLogDir(), "control-center-events.jsonl");
141
+ export const MAX_EVENT_LINES = 2000;
142
+
143
+ export const ALLOWED_LOG_TAILS = new Set([100, 300, 1000]);
144
+
145
+ export function resolveTailParam(value: string | null): number {
146
+ if (value === null) return 100;
147
+ const n = parseInt(value, 10);
148
+ if (Number.isFinite(n) && ALLOWED_LOG_TAILS.has(n)) return n;
149
+ return 100;
150
+ }
151
+
152
+ export const CONTENT_TYPES: Record<string, string> = {
153
+ ".html": "text/html; charset=utf-8",
154
+ ".css": "text/css; charset=utf-8",
155
+ ".js": "application/javascript; charset=utf-8",
156
+ ".mjs": "application/javascript; charset=utf-8",
157
+ ".json": "application/json; charset=utf-8",
158
+ ".svg": "image/svg+xml",
159
+ ".png": "image/png",
160
+ ".jpg": "image/jpeg",
161
+ ".jpeg": "image/jpeg",
162
+ ".gif": "image/gif",
163
+ ".ico": "image/x-icon",
164
+ ".woff": "font/woff",
165
+ ".woff2": "font/woff2",
166
+ ".map": "application/json; charset=utf-8",
167
+ ".txt": "text/plain; charset=utf-8",
168
+ };
169
+
170
+ export function sendJson(res: ServerResponse, status: number, body: unknown): void {
171
+ let payload: string;
172
+ try {
173
+ payload = JSON.stringify(body);
174
+ } catch (err) {
175
+ payload = JSON.stringify({ error: `serialization failed: ${errorMessage(err)}` });
176
+ status = 500;
177
+ }
178
+ res.writeHead(status, {
179
+ "Content-Type": "application/json; charset=utf-8",
180
+ "Cache-Control": "no-store",
181
+ "X-Content-Type-Options": "nosniff",
182
+ });
183
+ res.end(payload);
184
+ }
185
+
186
+ export function readBody(req: IncomingMessage): Promise<unknown | null> {
187
+ return new Promise((resolve) => {
188
+ let total = 0;
189
+ const chunks: Buffer[] = [];
190
+ let aborted = false;
191
+ req.on("data", (chunk: Buffer) => {
192
+ if (aborted) return;
193
+ total += chunk.length;
194
+ if (total > 1024 * 1024) {
195
+ aborted = true;
196
+ try { req.destroy(); } catch { /* ignore */ }
197
+ resolve(null);
198
+ return;
199
+ }
200
+ chunks.push(chunk);
201
+ });
202
+ req.on("end", () => {
203
+ if (aborted) return;
204
+ if (chunks.length === 0) {
205
+ resolve(null);
206
+ return;
207
+ }
208
+ const text = Buffer.concat(chunks).toString("utf-8");
209
+ try {
210
+ resolve(JSON.parse(text));
211
+ } catch {
212
+ resolve(null);
213
+ }
214
+ });
215
+ req.on("error", () => resolve(null));
216
+ });
217
+ }
218
+
219
+ export function readJsonFileSafe<T = unknown>(filePath: string): T | null {
220
+ if (!existsSync(filePath)) return null;
221
+ try {
222
+ const raw = readFileSync(filePath, "utf-8");
223
+ return JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw) as T;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ export function isPathInside(root: string, target: string): boolean {
230
+ const rel = relative(root, target);
231
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
232
+ }
233
+
234
+ export function readJsonFileSafeUnder<T = unknown>(root: string, relPath: string): T | null {
235
+ const base = resolve(root);
236
+ const target = resolve(base, relPath);
237
+ if (!isPathInside(base, target) || !existsSync(target)) return null;
238
+ try {
239
+ const realBase = realpathSync(base);
240
+ const realTarget = realpathSync(target);
241
+ if (!isPathInside(realBase, realTarget)) return null;
242
+ return readJsonFileSafe<T>(realTarget);
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ export function readTextFileSafe(filePath: string): string | null {
249
+ if (!existsSync(filePath)) return null;
250
+ try {
251
+ return readFileSync(filePath, "utf-8");
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+
257
+ export function readFileTail(filePath: string, lines = 100): string {
258
+ if (!existsSync(filePath)) return "";
259
+ const content = readFileSync(filePath, "utf-8");
260
+ const allLines = content.split(/\r?\n/);
261
+ if (allLines.length > 0 && allLines[allLines.length - 1] === "") allLines.pop();
262
+ return allLines.slice(-lines).join("\n");
263
+ }
264
+
265
+ export function findLatestLog(dir: string, pattern: RegExp): string | null {
266
+ if (!existsSync(dir)) return null;
267
+ let entries;
268
+ try {
269
+ entries = readdirSync(dir, { withFileTypes: true });
270
+ } catch {
271
+ return null;
272
+ }
273
+ const files = entries.filter((e) => e.isFile() && pattern.test(e.name));
274
+ if (files.length === 0) return null;
275
+ let latestName = files[0].name;
276
+ let latestMtime = -1;
277
+ for (const f of files) {
278
+ try {
279
+ const m = statSync(join(dir, f.name)).mtimeMs;
280
+ if (m > latestMtime) {
281
+ latestMtime = m;
282
+ latestName = f.name;
283
+ }
284
+ } catch {
285
+ /* keep current */
286
+ }
287
+ }
288
+ return join(dir, latestName);
289
+ }
290
+
291
+ // Re-export widely-used node primitives so route modules have a single import
292
+ // surface for path/delimiter helpers that are also needed alongside the shared
293
+ // helpers above.
294
+ export { delimiter, extname, join, resolve, isAbsolute, relative };