@blamejs/core 0.8.42 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/daemon.js ADDED
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.daemon
4
+ * @nav Production
5
+ * @title Daemon
6
+ *
7
+ * @intro
8
+ * Long-running process orchestration — supervisor wiring around
9
+ * `b.appShutdown`, foreground signal handling, detached-fork spawn
10
+ * via `b.processSpawn`, PID-file health probes, and a
11
+ * SIGTERM-then-SIGKILL restart policy on stop.
12
+ *
13
+ * Two operator paths share one entry point:
14
+ *
15
+ * 1. Foreground service mode (no `command`): the current process
16
+ * acquires `pidFile`, redirects stdout/stderr to `logFile`, and
17
+ * installs signal handlers (defaults: SIGTERM, SIGINT, SIGHUP)
18
+ * that route through a `b.appShutdown` orchestrator the operator
19
+ * can extend with `addPhase`.
20
+ *
21
+ * 2. Detached fork mode (`command` + `args`): the parent spawns the
22
+ * child via `b.processSpawn` (filtered env), writes the child PID
23
+ * to `pidFile`, hands the log fd to the child's stdout/stderr,
24
+ * and returns immediately so the parent can exit.
25
+ *
26
+ * Stale-PID handling — when `pidFile` exists but the recorded PID is
27
+ * no longer alive, `start` and `stop` clean up the sidecar and emit
28
+ * `daemon.stale_pid_cleaned`. Cross-process linkage uses
29
+ * `b.appShutdown.pidLock`, which layers O_EXCL atomic-create +
30
+ * signal-0 liveness probe + reap-on-stale.
31
+ *
32
+ * Audit events: `daemon.started` (pidFile + logFile + commandKind +
33
+ * pid), `daemon.stopped` (pidFile + signal + waitMs + escalated),
34
+ * `daemon.stale_pid_cleaned` (pidFile + stalePid).
35
+ *
36
+ * @card
37
+ * Long-running process orchestration — supervisor wiring around `b.appShutdown`, foreground signal handling, detached-fork spawn via `b.processSpawn`, PID-file health probes, and a SIGTERM-then-SIGKILL restart policy on stop.
38
+ */
39
+
40
+ var fs = require("fs");
41
+ var path = require("path");
42
+ var nb = require("./numeric-bounds");
43
+ var appShutdown = require("./app-shutdown");
44
+ var processSpawn = require("./process-spawn");
45
+ var lazyRequire = require("./lazy-require");
46
+ var safeAsync = require("./safe-async");
47
+ var atomicFile = require("./atomic-file");
48
+ var validateOpts = require("./validate-opts");
49
+ var C = require("./constants");
50
+ var { boot } = require("./log");
51
+ var { defineClass } = require("./framework-error");
52
+
53
+ var audit = lazyRequire(function () { return require("./audit"); });
54
+
55
+ var DaemonError = defineClass("DaemonError", { alwaysPermanent: true });
56
+ var log = boot("daemon");
57
+
58
+ // Tunables. Operator overrides via opts on stop(); for start() the
59
+ // defaults are baked in so the operator surface stays minimal.
60
+ var DEFAULT_STOP_TIMEOUT_MS = C.TIME.seconds(30);
61
+ var DEFAULT_STOP_SIGNAL = "SIGTERM";
62
+ var DEFAULT_POLL_MS = 100;
63
+ var DEFAULT_LOG_FILE_MODE = 0o600;
64
+
65
+ function _safeAuditEmit(action, outcome, metadata) {
66
+ try {
67
+ audit().safeEmit({
68
+ action: action,
69
+ outcome: outcome || "success",
70
+ metadata: metadata || {},
71
+ });
72
+ } catch (_e) { /* drop-silent — by design */ }
73
+ }
74
+
75
+ function _isLivePid(pid) {
76
+ if (typeof pid !== "number" || !isFinite(pid) || pid <= 0) return false;
77
+ try { process.kill(pid, 0); return true; }
78
+ catch (e) { return e && e.code === "EPERM"; }
79
+ }
80
+
81
+ function _readPidFile(pidFile) {
82
+ try {
83
+ var raw = fs.readFileSync(pidFile, "utf8");
84
+ var pid = parseInt(String(raw).trim(), 10);
85
+ return isFinite(pid) && pid > 0 ? pid : null;
86
+ } catch (_e) { return null; }
87
+ }
88
+
89
+ function _validateStartOpts(opts) {
90
+ validateOpts.requireObject(opts, "daemon.start", DaemonError, "daemon/bad-opts");
91
+ validateOpts.requireNonEmptyString(opts.pidFile,
92
+ "daemon.start: opts.pidFile (absolute path recommended)",
93
+ DaemonError, "daemon/bad-pid-file");
94
+ validateOpts.optionalNonEmptyString(opts.logFile,
95
+ "daemon.start: opts.logFile", DaemonError, "daemon/bad-log-file");
96
+ validateOpts.optionalNonEmptyStringArray(opts.signals,
97
+ "daemon.start: opts.signals", DaemonError, "daemon/bad-signals");
98
+ if (Array.isArray(opts.signals) && opts.signals.length === 0) {
99
+ throw new DaemonError("daemon/bad-signals",
100
+ "daemon.start: opts.signals must be a non-empty array of POSIX signal names");
101
+ }
102
+ validateOpts.optionalNonEmptyString(opts.command,
103
+ "daemon.start: opts.command (path to executable)",
104
+ DaemonError, "daemon/bad-command");
105
+ if (opts.args !== undefined && !Array.isArray(opts.args)) {
106
+ throw new DaemonError("daemon/bad-args",
107
+ "daemon.start: opts.args must be an array of strings when present");
108
+ }
109
+ if (opts.command === undefined && opts.args !== undefined) {
110
+ throw new DaemonError("daemon/bad-args",
111
+ "daemon.start: opts.args requires opts.command");
112
+ }
113
+ }
114
+
115
+ function _validateStopOpts(opts) {
116
+ validateOpts.requireObject(opts, "daemon.stop", DaemonError, "daemon/bad-opts");
117
+ validateOpts.requireNonEmptyString(opts.pidFile,
118
+ "daemon.stop: opts.pidFile", DaemonError, "daemon/bad-pid-file");
119
+ validateOpts.optionalNonEmptyString(opts.signal,
120
+ "daemon.stop: opts.signal", DaemonError, "daemon/bad-signal");
121
+ nb.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
122
+ "daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
123
+ nb.requirePositiveFiniteIntIfPresent(opts.pollMs,
124
+ "daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
125
+ }
126
+
127
+ function _maybeReapStale(pidFile) {
128
+ var existing = _readPidFile(pidFile);
129
+ if (existing === null) return false;
130
+ if (_isLivePid(existing) && existing !== process.pid) {
131
+ // Live owner — caller will receive a daemon/already-running below.
132
+ return false;
133
+ }
134
+ if (existing === process.pid) return false;
135
+ // Stale: PID is gone (or signal-0 returned ESRCH). Reap + audit.
136
+ try { fs.unlinkSync(pidFile); } catch (_e) { /* race: another reaper */ }
137
+ _safeAuditEmit("daemon.stale_pid_cleaned", "success", {
138
+ pidFile: pidFile,
139
+ stalePid: existing,
140
+ });
141
+ return true;
142
+ }
143
+
144
+ // Open the log file (append mode, 0o600) and return the fd.
145
+ // Used both by detached-spawn (passed via stdio) and by foreground
146
+ // redirect of the current process' stdout/stderr.
147
+ function _openLogFd(logFile) {
148
+ if (typeof logFile !== "string" || logFile.length === 0) return null;
149
+ atomicFile.ensureDir(path.dirname(logFile));
150
+ var fd = fs.openSync(logFile, "a", DEFAULT_LOG_FILE_MODE);
151
+ return fd;
152
+ }
153
+
154
+ // Redirect the current process's stdout/stderr file descriptors at the
155
+ // given fd. Implemented via fs.writeSync streams: Node doesn't expose a
156
+ // portable dup2, so we replace process.stdout.write / process.stderr.write
157
+ // with a writer that pushes to the log fd. This is the standard
158
+ // pattern for foreground daemons that don't want to lose output when
159
+ // detached from a terminal.
160
+ function _redirectStdio(fd) {
161
+ if (typeof fd !== "number") return;
162
+ function _writer(chunk, encOrCb, maybeCb) {
163
+ var enc = typeof encOrCb === "string" ? encOrCb : "utf8";
164
+ var cb = typeof encOrCb === "function" ? encOrCb : maybeCb;
165
+ var buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), enc);
166
+ try { fs.writeSync(fd, buf); }
167
+ catch (_e) { /* log fd closed underneath us — drop */ }
168
+ if (typeof cb === "function") cb();
169
+ return true;
170
+ }
171
+ process.stdout.write = _writer;
172
+ process.stderr.write = _writer;
173
+ }
174
+
175
+ // Track foreground orchestrators per pidFile so stop() / repeat
176
+ // start() in the same process don't double-install signals.
177
+ var _foregroundOrchestrators = Object.create(null);
178
+
179
+ /**
180
+ * @primitive b.daemon.start
181
+ * @signature b.daemon.start(opts)
182
+ * @since 0.6.0
183
+ * @status stable
184
+ * @related b.daemon.stop, b.appShutdown.create, b.processSpawn.spawn
185
+ *
186
+ * Acquire `pidFile`, optionally redirect stdout/stderr to `logFile`,
187
+ * and either install signal handlers in the current process
188
+ * (foreground mode) or spawn a detached child (when `command` is
189
+ * supplied). Reaps a stale pidfile before acquire and emits
190
+ * `daemon.stale_pid_cleaned` when one is found.
191
+ *
192
+ * Returns `{ pid, pidFile, logFile, mode }`. In foreground mode the
193
+ * return value also exposes `orchestrator` (the underlying
194
+ * `b.appShutdown` handle), `addPhase` (operator-supplied shutdown
195
+ * phases), and `shutdown` (manual trigger). In detached mode `mode`
196
+ * is `"detached"`; in foreground mode it is `"foreground"`.
197
+ *
198
+ * Throws `DaemonError("daemon/already-running")` when the pidfile is
199
+ * held by a live PID, `DaemonError("daemon/spawn-failed")` when the
200
+ * detached spawn errors, and `DaemonError("daemon/log-open-failed")`
201
+ * when the log file cannot be opened in foreground mode.
202
+ *
203
+ * @opts
204
+ * pidFile: string, // absolute path of the PID sidecar (required)
205
+ * logFile: string, // append-mode log; redirects stdout+stderr
206
+ * signals: string[], // foreground signals; default: SIGTERM/SIGINT/SIGHUP
207
+ * command: string, // executable for detached-fork mode
208
+ * args: string[], // argv for the detached child
209
+ * cwd: string, // cwd for the detached child
210
+ *
211
+ * @example
212
+ * var handle = b.daemon.start({
213
+ * pidFile: "/tmp/blamejs-daemon-demo.pid",
214
+ * signals: ["SIGTERM", "SIGINT"],
215
+ * });
216
+ * handle.mode; // → "foreground"
217
+ * handle.pidFile; // → "/tmp/blamejs-daemon-demo.pid"
218
+ * typeof handle.shutdown; // → "function"
219
+ * await handle.shutdown();
220
+ */
221
+ function start(opts) {
222
+ _validateStartOpts(opts);
223
+ var pidFile = opts.pidFile;
224
+ var logFile = opts.logFile || null;
225
+ var signals = Array.isArray(opts.signals) && opts.signals.length > 0
226
+ ? opts.signals.slice()
227
+ : ["SIGTERM", "SIGINT", "SIGHUP"];
228
+
229
+ // Reap a stale pidfile if present, then attempt acquire.
230
+ _maybeReapStale(pidFile);
231
+
232
+ // Detached-fork mode — caller wants us to spawn the child, write its
233
+ // PID into pidFile, and return without taking the lock ourselves.
234
+ if (typeof opts.command === "string" && opts.command.length > 0) {
235
+ var existingLive = _readPidFile(pidFile);
236
+ if (existingLive !== null && _isLivePid(existingLive)) {
237
+ throw new DaemonError("daemon/already-running",
238
+ "daemon.start: pidFile '" + pidFile + "' held by live PID " + existingLive);
239
+ }
240
+ var logFd = logFile ? _openLogFd(logFile) : "ignore";
241
+ var child;
242
+ try {
243
+ child = processSpawn.spawn(opts.command, opts.args || [], {
244
+ detached: true,
245
+ stdio: ["ignore", logFd, logFd],
246
+ cwd: typeof opts.cwd === "string" ? opts.cwd : undefined,
247
+ });
248
+ } catch (e) {
249
+ try { if (typeof logFd === "number") fs.closeSync(logFd); }
250
+ catch (_c) { /* best-effort */ }
251
+ throw new DaemonError("daemon/spawn-failed",
252
+ "daemon.start: spawn failed: " + ((e && e.message) || String(e)));
253
+ }
254
+ // Write the child's PID via atomic temp+rename so a concurrent
255
+ // observer never sees a half-written pidFile.
256
+ atomicFile.ensureDir(path.dirname(pidFile));
257
+ var pidStr = String(child.pid) + "\n";
258
+ atomicFile.writeSync(pidFile, pidStr, { fileMode: 0o600 });
259
+ // Detach so the child survives parent exit.
260
+ try { child.unref(); } catch (_u) { /* best-effort */ }
261
+ if (typeof logFd === "number") {
262
+ // Parent doesn't need its handle to the log; child inherited it.
263
+ try { fs.closeSync(logFd); } catch (_c) { /* best-effort */ }
264
+ }
265
+ _safeAuditEmit("daemon.started", "success", {
266
+ pidFile: pidFile,
267
+ logFile: logFile,
268
+ commandKind: "detached-fork",
269
+ pid: child.pid,
270
+ });
271
+ log("daemon started (detached) pid=" + child.pid + " pidFile=" + pidFile);
272
+ return { pid: child.pid, pidFile: pidFile, logFile: logFile, mode: "detached" };
273
+ }
274
+
275
+ // Foreground mode — current process owns pidFile + signals.
276
+ var lock = appShutdown.pidLock(pidFile);
277
+ try { lock.acquire(); }
278
+ catch (e) {
279
+ if (e && /pidlock-held/.test(e.code || "")) {
280
+ throw new DaemonError("daemon/already-running",
281
+ "daemon.start: pidFile '" + pidFile + "' already held: " + e.message);
282
+ }
283
+ throw new DaemonError("daemon/pid-acquire-failed",
284
+ "daemon.start: failed to acquire pidFile '" + pidFile + "': " +
285
+ ((e && e.message) || String(e)));
286
+ }
287
+
288
+ var logFdForeground = null;
289
+ if (logFile) {
290
+ try {
291
+ logFdForeground = _openLogFd(logFile);
292
+ _redirectStdio(logFdForeground);
293
+ } catch (e) {
294
+ try { lock.release(); } catch (_r) { /* best-effort */ }
295
+ throw new DaemonError("daemon/log-open-failed",
296
+ "daemon.start: failed to open logFile '" + logFile + "': " +
297
+ ((e && e.message) || String(e)));
298
+ }
299
+ }
300
+
301
+ var orchestrator = appShutdown.create({
302
+ signals: signals,
303
+ installSignalHandlers: true,
304
+ phases: [
305
+ {
306
+ name: "pidLock-release",
307
+ run: function () {
308
+ try { lock.release(); } catch (_e) { /* best-effort */ }
309
+ if (logFdForeground !== null) {
310
+ try { fs.closeSync(logFdForeground); } catch (_c) { /* best-effort */ }
311
+ }
312
+ },
313
+ timeoutMs: C.TIME.seconds(2),
314
+ },
315
+ ],
316
+ });
317
+ _foregroundOrchestrators[pidFile] = orchestrator;
318
+
319
+ _safeAuditEmit("daemon.started", "success", {
320
+ pidFile: pidFile,
321
+ logFile: logFile,
322
+ commandKind: "foreground",
323
+ pid: process.pid,
324
+ signals: signals,
325
+ });
326
+ log("daemon started (foreground) pid=" + process.pid + " pidFile=" + pidFile);
327
+
328
+ return {
329
+ pid: process.pid,
330
+ pidFile: pidFile,
331
+ logFile: logFile,
332
+ mode: "foreground",
333
+ orchestrator: orchestrator,
334
+ addPhase: orchestrator.addPhase,
335
+ shutdown: orchestrator.shutdown,
336
+ };
337
+ }
338
+
339
+ /**
340
+ * @primitive b.daemon.stop
341
+ * @signature b.daemon.stop(opts)
342
+ * @since 0.6.0
343
+ * @status stable
344
+ * @related b.daemon.start, b.appShutdown.create
345
+ *
346
+ * Read `pidFile`, send `signal` (default `SIGTERM`), poll for exit up
347
+ * to `timeoutMs` (default 30 s), then escalate to `SIGKILL`. Cleans
348
+ * up the pidfile on successful exit and emits `daemon.stopped` with
349
+ * `escalated: true|false` recording whether SIGKILL was needed.
350
+ *
351
+ * Returns `{ stopped, pid, signal, escalated?, reason? }`. `reason`
352
+ * is `"no-pidfile"` when nothing was running and `"stale"` when the
353
+ * pidfile pointed at a dead PID (the file is removed and a
354
+ * `daemon.stale_pid_cleaned` audit row lands).
355
+ *
356
+ * @opts
357
+ * pidFile: string, // absolute path of the PID sidecar (required)
358
+ * signal: string, // initial signal; default "SIGTERM"
359
+ * timeoutMs: number, // wait before SIGKILL escalation; default 30 s
360
+ * pollMs: number, // liveness-probe interval; default 100 ms
361
+ * abortSignal: AbortSignal, // forwarded to b.safeAsync.sleep
362
+ *
363
+ * @example
364
+ * var report = await b.daemon.stop({
365
+ * pidFile: "/tmp/blamejs-daemon-demo.pid",
366
+ * timeoutMs: b.constants.TIME.seconds(5),
367
+ * });
368
+ * report.stopped; // → false
369
+ * report.reason; // → "no-pidfile"
370
+ */
371
+ async function stop(opts) {
372
+ _validateStopOpts(opts);
373
+ var pidFile = opts.pidFile;
374
+ var signal = opts.signal || DEFAULT_STOP_SIGNAL;
375
+ var timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : DEFAULT_STOP_TIMEOUT_MS;
376
+ var pollMs = typeof opts.pollMs === "number" ? opts.pollMs : DEFAULT_POLL_MS;
377
+
378
+ var pid = _readPidFile(pidFile);
379
+ if (pid === null) {
380
+ return { stopped: false, pid: null, reason: "no-pidfile" };
381
+ }
382
+ if (!_isLivePid(pid)) {
383
+ // Stale — clean up and report.
384
+ try { fs.unlinkSync(pidFile); } catch (_e) { /* best-effort */ }
385
+ _safeAuditEmit("daemon.stale_pid_cleaned", "success", { pidFile: pidFile, stalePid: pid });
386
+ return { stopped: false, pid: pid, reason: "stale" };
387
+ }
388
+
389
+ var t0 = Date.now();
390
+ // First signal — typically SIGTERM. Wait up to timeoutMs for exit.
391
+ try { process.kill(pid, signal); }
392
+ catch (e) {
393
+ if (e && e.code === "ESRCH") {
394
+ // Died between read and kill — cleanup + report.
395
+ try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
396
+ _safeAuditEmit("daemon.stopped", "success", {
397
+ pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
398
+ });
399
+ return { stopped: true, pid: pid, signal: signal };
400
+ }
401
+ throw new DaemonError("daemon/kill-failed",
402
+ "daemon.stop: kill(" + pid + ", " + signal + ") failed: " + e.message);
403
+ }
404
+
405
+ var deadline = t0 + timeoutMs;
406
+ while (Date.now() < deadline) {
407
+ if (!_isLivePid(pid)) {
408
+ try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
409
+ _safeAuditEmit("daemon.stopped", "success", {
410
+ pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
411
+ });
412
+ return { stopped: true, pid: pid, signal: signal };
413
+ }
414
+ await safeAsync.sleep(pollMs, { signal: opts.abortSignal });
415
+ }
416
+
417
+ // Timed out — escalate to SIGKILL.
418
+ try { process.kill(pid, "SIGKILL"); }
419
+ catch (e) {
420
+ if (!(e && e.code === "ESRCH")) {
421
+ throw new DaemonError("daemon/kill-failed",
422
+ "daemon.stop: SIGKILL escalation failed for pid " + pid + ": " + e.message);
423
+ }
424
+ }
425
+ // Wait briefly for the kernel to reap.
426
+ var killDeadline = Date.now() + C.TIME.seconds(2);
427
+ while (Date.now() < killDeadline) {
428
+ if (!_isLivePid(pid)) break;
429
+ await safeAsync.sleep(pollMs, { signal: opts.abortSignal });
430
+ }
431
+ try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
432
+ _safeAuditEmit("daemon.stopped", "success", {
433
+ pidFile: pidFile, signal: "SIGKILL", waitMs: Date.now() - t0, escalated: true,
434
+ });
435
+ return { stopped: true, pid: pid, signal: "SIGKILL", escalated: true };
436
+ }
437
+
438
+ // Test-only — drop process-wide foreground orchestrator state so smoke
439
+ // tests can re-run start() in the same process without leaking signal
440
+ // handlers across cases.
441
+ function _resetForTest() {
442
+ var keys = Object.keys(_foregroundOrchestrators);
443
+ for (var i = 0; i < keys.length; i++) {
444
+ try { _foregroundOrchestrators[keys[i]]._resetForTest(); } catch (_e) { /* best-effort */ }
445
+ }
446
+ _foregroundOrchestrators = Object.create(null);
447
+ }
448
+
449
+ module.exports = {
450
+ start: start,
451
+ stop: stop,
452
+ DaemonError: DaemonError,
453
+ DEFAULT_STOP_SIGNAL: DEFAULT_STOP_SIGNAL,
454
+ DEFAULT_STOP_TIMEOUT_MS: DEFAULT_STOP_TIMEOUT_MS,
455
+ _resetForTest: _resetForTest,
456
+ };