@blamejs/core 0.4.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 (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,520 @@
1
+ "use strict";
2
+ /**
3
+ * Async resilience + safety primitives.
4
+ *
5
+ * The framework's async surfaces (external-db queries, cluster
6
+ * coordination, queue operations, audit chain writes) all share the
7
+ * same hazards: races between interleaved awaits, unbounded retries
8
+ * masking real failures, hangs from unresponsive backends, and partial
9
+ * results from operator-supplied drivers. This module collects the
10
+ * primitives the framework uses to handle those hazards consistently.
11
+ *
12
+ * Design posture:
13
+ *
14
+ * - **AbortSignal everywhere.** Every primitive that takes time
15
+ * accepts an `AbortSignal` and aborts cleanly when the signal
16
+ * fires. This is the modern Node.js convention (Node 18+) and
17
+ * replaces the older "cancellation token" pattern. Operators who
18
+ * don't pass a signal get the legacy non-cancellable behaviour.
19
+ *
20
+ * - **Error.cause preserved.** Wrapper errors set `.cause` to the
21
+ * original failure so debugging traces back to the root. Callers
22
+ * who walk `.cause` chains see the full picture.
23
+ *
24
+ * - **No leaked Promises.** Mutex / Semaphore release on path-out
25
+ * in finally blocks — even cancellation. No pending acquirer
26
+ * stays referenced after its abort.
27
+ *
28
+ * - **Bounded by default.** Semaphore / Queue have explicit limits
29
+ * and reject acquisitions over the limit rather than growing
30
+ * unboundedly. Operators size limits explicitly for their workload.
31
+ *
32
+ * - **Fail loud.** Errors propagate; primitives never silently
33
+ * swallow. safeAwait() opt-in for callers who need {error, value}
34
+ * tuples; everything else throws / rejects.
35
+ *
36
+ * Public API:
37
+ *
38
+ * withTimeout(promise, ms, opts?) promise; rejects on timeout
39
+ * withSignal(promise, signal) promise; rejects on abort
40
+ * withTimeoutSignal(signal, ms) AbortSignal composing user
41
+ * signal + a fresh timeout. Used
42
+ * by I/O primitives that already
43
+ * accept a signal and want to
44
+ * add a wall-clock deadline.
45
+ * sleep(ms, opts?) promise that resolves after ms;
46
+ * opts.signal aborts mid-sleep,
47
+ * timer is unref'd so a pending
48
+ * sleep doesn't keep the process
49
+ * alive
50
+ * safeAwait(promise) [error, value] never throws
51
+ *
52
+ * Mutex class; .runExclusive(fn)
53
+ * Semaphore(limit) class; .runWith(fn)
54
+ * Once(fn) class; .invoke()
55
+ *
56
+ * asyncRetry(fn, opts?) re-export from object-store-retry
57
+ * CircuitBreaker(name, opts?) re-export from object-store-retry
58
+ *
59
+ * SafeAsyncError error class
60
+ *
61
+ * Best-practice notes for callers:
62
+ *
63
+ * - Always pair `withTimeout` with the external-db / network calls
64
+ * where operator-supplied drivers might hang. The framework's
65
+ * external-db wrapper already retries; timeout puts a ceiling on
66
+ * each individual attempt.
67
+ *
68
+ * - Wrap chain-writes with Mutex.runExclusive. Audit chain hashing
69
+ * reads the previous tip and writes a successor; without
70
+ * serialization, concurrent awaiting record() calls can hash
71
+ * against the same prev-tip and produce a forked chain. Mutex
72
+ * prevents this in single-process; for cross-process coordination
73
+ * the cluster module's leader election is the correct primitive.
74
+ *
75
+ * - Use Once for boot-time lazy init (counter primer, schema
76
+ * check). Multiple concurrent first-callers correctly all wait
77
+ * on the same in-flight init Promise rather than each starting
78
+ * their own.
79
+ *
80
+ * - Use safeAwait for fire-and-forget paths (audit hooks in
81
+ * middleware) that previously used try/catch — preserves the
82
+ * "log + continue" pattern without unhandled-rejection warnings.
83
+ *
84
+ * - Prefer Promise.allSettled over Promise.all when partial failure
85
+ * is acceptable (e.g. emitting to multiple log sinks; one sink
86
+ * down shouldn't block the others). The framework's log-stream
87
+ * dispatcher already does this.
88
+ */
89
+
90
+ var { FrameworkError } = require("./framework-error");
91
+
92
+ class SafeAsyncError extends FrameworkError {
93
+ constructor(message, code, cause) {
94
+ super(message);
95
+ this.name = "SafeAsyncError";
96
+ this.code = code || "async/invalid";
97
+ if (cause !== undefined) this.cause = cause;
98
+ this.isSafeAsyncError = true;
99
+ }
100
+ }
101
+
102
+ // ---- withTimeout ----
103
+ //
104
+ // Race the promise against a timer. On timeout, the wrapper rejects with
105
+ // SafeAsyncError(code=async/timeout). The original promise continues
106
+ // running in the background — the framework cannot cancel an arbitrary
107
+ // async operation; only signal-aware ones can be aborted (see withSignal).
108
+ //
109
+ // opts.signal: AbortSignal — aborting the signal also rejects the wrapper
110
+ // with code=async/aborted.
111
+ // opts.name: diagnostic label included in the timeout message.
112
+
113
+ function withTimeout(promise, ms, opts) {
114
+ opts = opts || {};
115
+ if (typeof ms !== "number" || ms <= 0 || !Number.isFinite(ms)) {
116
+ throw new SafeAsyncError("withTimeout: ms must be a positive finite number", "async/bad-arg");
117
+ }
118
+ return new Promise(function (resolve, reject) {
119
+ var settled = false;
120
+ // The timer is ref'd intentionally: while a withTimeout is pending,
121
+ // the process should stay alive until either the underlying promise
122
+ // settles or the timeout fires. unref'ing here would let Node exit
123
+ // mid-await and the awaited Promise would never resolve.
124
+ var timer = setTimeout(function () {
125
+ if (settled) return;
126
+ settled = true;
127
+ reject(new SafeAsyncError(
128
+ "operation timed out after " + ms + "ms" + (opts.name ? " (" + opts.name + ")" : ""),
129
+ "async/timeout"
130
+ ));
131
+ }, ms);
132
+
133
+ function _onAbort() {
134
+ if (settled) return;
135
+ settled = true;
136
+ clearTimeout(timer);
137
+ reject(new SafeAsyncError(
138
+ "operation aborted" + (opts.name ? " (" + opts.name + ")" : ""),
139
+ "async/aborted",
140
+ opts.signal && opts.signal.reason
141
+ ));
142
+ }
143
+ if (opts.signal) {
144
+ if (opts.signal.aborted) { _onAbort(); return; }
145
+ opts.signal.addEventListener("abort", _onAbort, { once: true });
146
+ }
147
+
148
+ Promise.resolve(promise).then(function (v) {
149
+ if (settled) return;
150
+ settled = true;
151
+ clearTimeout(timer);
152
+ if (opts.signal) opts.signal.removeEventListener("abort", _onAbort);
153
+ resolve(v);
154
+ }, function (e) {
155
+ if (settled) return;
156
+ settled = true;
157
+ clearTimeout(timer);
158
+ if (opts.signal) opts.signal.removeEventListener("abort", _onAbort);
159
+ reject(e);
160
+ });
161
+ });
162
+ }
163
+
164
+ // ---- withSignal ----
165
+ //
166
+ // Race the promise against an AbortSignal. The original promise continues
167
+ // running in the background; only the wrapper's resolution is short-
168
+ // circuited. Useful for plumbing a single signal through a chain of awaits.
169
+
170
+ function withSignal(promise, signal) {
171
+ if (!signal) return Promise.resolve(promise);
172
+ return new Promise(function (resolve, reject) {
173
+ var settled = false;
174
+ function _onAbort() {
175
+ if (settled) return;
176
+ settled = true;
177
+ reject(new SafeAsyncError(
178
+ "operation aborted",
179
+ "async/aborted",
180
+ signal.reason
181
+ ));
182
+ }
183
+ if (signal.aborted) { _onAbort(); return; }
184
+ signal.addEventListener("abort", _onAbort, { once: true });
185
+ Promise.resolve(promise).then(function (v) {
186
+ if (settled) return;
187
+ settled = true;
188
+ signal.removeEventListener("abort", _onAbort);
189
+ resolve(v);
190
+ }, function (e) {
191
+ if (settled) return;
192
+ settled = true;
193
+ signal.removeEventListener("abort", _onAbort);
194
+ reject(e);
195
+ });
196
+ });
197
+ }
198
+
199
+ // ---- sleep ----
200
+ //
201
+ // Promise that resolves after `ms` milliseconds. opts.signal aborts the
202
+ // sleep cleanly — the wrapper rejects with SafeAsyncError(async/aborted).
203
+ //
204
+ // opts.unref (default false): if true, the timer is unref'd so a pending
205
+ // sleep does NOT keep the process alive. Use for fire-and-forget /
206
+ // heartbeat-style patterns where the program should be free to exit if
207
+ // nothing else is keeping it busy. The default is ref'd to match the
208
+ // natural meaning of `await sleep(ms)` — "I'm waiting, this IS my work,
209
+ // don't exit out from under me." (An unref'd-only event loop in Node
210
+ // exits even with pending awaits — the unref'd timer is not enough to
211
+ // hold the loop alive, so the awaiting promise never resolves.)
212
+ //
213
+ // ms <= 0 resolves immediately (matches setTimeout's clamp-to-1ms but
214
+ // without the wasted tick). Non-finite ms rejects.
215
+
216
+ function sleep(ms, opts) {
217
+ if (typeof ms !== "number" || !Number.isFinite(ms)) {
218
+ return Promise.reject(new SafeAsyncError(
219
+ "sleep: ms must be a finite number", "async/bad-arg"
220
+ ));
221
+ }
222
+ if (ms <= 0) return Promise.resolve();
223
+
224
+ var signal = opts && opts.signal;
225
+ if (signal && signal.aborted) {
226
+ return Promise.reject(new SafeAsyncError(
227
+ "sleep aborted before start", "async/aborted", signal.reason
228
+ ));
229
+ }
230
+
231
+ return new Promise(function (resolve, reject) {
232
+ var settled = false;
233
+ var timer = setTimeout(function () {
234
+ if (settled) return;
235
+ settled = true;
236
+ if (signal) signal.removeEventListener("abort", _onAbort);
237
+ resolve();
238
+ }, ms);
239
+ if (opts && opts.unref) timer.unref();
240
+
241
+ function _onAbort() {
242
+ if (settled) return;
243
+ settled = true;
244
+ clearTimeout(timer);
245
+ reject(new SafeAsyncError(
246
+ "sleep aborted", "async/aborted", signal.reason
247
+ ));
248
+ }
249
+ if (signal) signal.addEventListener("abort", _onAbort, { once: true });
250
+ });
251
+ }
252
+
253
+ // ---- withTimeoutSignal ----
254
+ //
255
+ // Returns an AbortSignal that fires when EITHER the input signal aborts
256
+ // OR `ms` milliseconds elapse. Wraps the I/O-primitive composition that
257
+ // http-client was doing inline twice (h1 + h2 paths).
258
+ //
259
+ // var sig = withTimeoutSignal(userSignal, 5000);
260
+ // await fetch(url, { signal: sig });
261
+ //
262
+ // Edge cases:
263
+ // - userSignal == null and ms not positive → returns null
264
+ // (caller's "no signal needed" path)
265
+ // - userSignal == null → returns AbortSignal.timeout(ms)
266
+ // - ms not positive (0/undefined) → returns userSignal unchanged
267
+ // - both → AbortSignal.any([user, timeout])
268
+ //
269
+ // Returning null when neither is requested means the caller can pass
270
+ // the result straight to APIs that treat null as "no abort" (the http
271
+ // `signal` option does), with no special-case branching needed.
272
+
273
+ function withTimeoutSignal(signal, ms) {
274
+ var hasTimeout = typeof ms === "number" && ms > 0 && Number.isFinite(ms);
275
+ if (!signal && !hasTimeout) return null;
276
+ if (!signal) return AbortSignal.timeout(ms);
277
+ if (!hasTimeout) return signal;
278
+ return AbortSignal.any([signal, AbortSignal.timeout(ms)]);
279
+ }
280
+
281
+ // ---- safeAwait ----
282
+ //
283
+ // Go-style [error, value] tuple. Never throws. Lets callers handle the
284
+ // "expected may fail; log and continue" pattern without try/catch
285
+ // scaffolding.
286
+ //
287
+ // var [err, value] = await safeAwait(somePromise);
288
+ // if (err) { /* log + continue */ }
289
+
290
+ async function safeAwait(promise) {
291
+ try {
292
+ var v = await promise;
293
+ return [null, v];
294
+ } catch (e) {
295
+ return [e, null];
296
+ }
297
+ }
298
+
299
+ // ---- Mutex ----
300
+ //
301
+ // Async mutex — only one async region holds the lock at a time. Acquirers
302
+ // queue in arrival order. .runExclusive(fn) is the recommended call form
303
+ // (lock release is automatic via finally even if fn throws); .acquire()
304
+ // + .release() are exposed for callers needing finer control.
305
+ //
306
+ // Implementation note: a queued acquirer that's never released (operator
307
+ // bug) blocks the entire mutex. We don't add a hard-coded timeout because
308
+ // timeouts mask bugs and the real fix is releasing properly. If a caller
309
+ // wants a deadline, wrap the runExclusive call with withTimeout.
310
+
311
+ class Mutex {
312
+ constructor() {
313
+ this._waiters = []; // [{ resolve, reject, signal, onAbort }]
314
+ this._held = false;
315
+ }
316
+
317
+ // acquire(opts?) — opts.signal aborts a waiting acquirer cleanly.
318
+ // When the signal fires, the waiter is removed from the queue and
319
+ // its Promise rejects with async/aborted. The slot it WOULD have
320
+ // taken stays free for the next waiter. If the mutex isn't held,
321
+ // acquire returns immediately regardless of signal state.
322
+ acquire(opts) {
323
+ var self = this;
324
+ var signal = opts && opts.signal;
325
+ if (!self._held) {
326
+ self._held = true;
327
+ return Promise.resolve();
328
+ }
329
+ if (signal && signal.aborted) {
330
+ return Promise.reject(new SafeAsyncError(
331
+ "Mutex.acquire aborted", "async/aborted", signal.reason
332
+ ));
333
+ }
334
+ return new Promise(function (resolve, reject) {
335
+ var entry = { resolve: resolve, reject: reject, signal: signal, onAbort: null };
336
+ if (signal) {
337
+ entry.onAbort = function () {
338
+ var idx = self._waiters.indexOf(entry);
339
+ if (idx === -1) return; // already taken or already aborted
340
+ self._waiters.splice(idx, 1);
341
+ reject(new SafeAsyncError(
342
+ "Mutex.acquire aborted while waiting", "async/aborted", signal.reason
343
+ ));
344
+ };
345
+ signal.addEventListener("abort", entry.onAbort, { once: true });
346
+ }
347
+ self._waiters.push(entry);
348
+ });
349
+ }
350
+
351
+ release() {
352
+ if (!this._held) {
353
+ throw new SafeAsyncError("release on unheld Mutex", "async/bad-release");
354
+ }
355
+ if (this._waiters.length > 0) {
356
+ var next = this._waiters.shift();
357
+ if (next.signal && next.onAbort) {
358
+ next.signal.removeEventListener("abort", next.onAbort);
359
+ }
360
+ next.resolve();
361
+ } else {
362
+ this._held = false;
363
+ }
364
+ }
365
+
366
+ async runExclusive(fn, opts) {
367
+ await this.acquire(opts);
368
+ try {
369
+ return await fn();
370
+ } finally {
371
+ this.release();
372
+ }
373
+ }
374
+
375
+ isHeld() { return this._held; }
376
+ pendingCount() { return this._waiters.length; }
377
+ }
378
+
379
+ // ---- Semaphore ----
380
+ //
381
+ // Bounded concurrency: at most `limit` simultaneous holders. Acquirers
382
+ // over the limit wait their turn. Use cases: limit external-db query
383
+ // concurrency, throttle outbound webhook fan-out, cap parallel file I/O.
384
+ //
385
+ // .runWith(fn) is the recommended form (release on finally); .acquire/
386
+ // .release are exposed for finer control.
387
+
388
+ class Semaphore {
389
+ constructor(limit) {
390
+ if (typeof limit !== "number" || limit < 1 || !Number.isInteger(limit)) {
391
+ throw new SafeAsyncError("Semaphore limit must be a positive integer", "async/bad-arg");
392
+ }
393
+ this._limit = limit;
394
+ this._inFlight = 0;
395
+ this._waiters = []; // [{ resolve, reject, signal, onAbort }]
396
+ }
397
+
398
+ // acquire(opts?) — opts.signal aborts a waiting acquirer cleanly.
399
+ acquire(opts) {
400
+ var self = this;
401
+ var signal = opts && opts.signal;
402
+ if (self._inFlight < self._limit) {
403
+ self._inFlight += 1;
404
+ return Promise.resolve();
405
+ }
406
+ if (signal && signal.aborted) {
407
+ return Promise.reject(new SafeAsyncError(
408
+ "Semaphore.acquire aborted", "async/aborted", signal.reason
409
+ ));
410
+ }
411
+ return new Promise(function (resolve, reject) {
412
+ var entry = { resolve: resolve, reject: reject, signal: signal, onAbort: null };
413
+ if (signal) {
414
+ entry.onAbort = function () {
415
+ var idx = self._waiters.indexOf(entry);
416
+ if (idx === -1) return;
417
+ self._waiters.splice(idx, 1);
418
+ reject(new SafeAsyncError(
419
+ "Semaphore.acquire aborted while waiting", "async/aborted", signal.reason
420
+ ));
421
+ };
422
+ signal.addEventListener("abort", entry.onAbort, { once: true });
423
+ }
424
+ self._waiters.push(entry);
425
+ });
426
+ }
427
+
428
+ release() {
429
+ if (this._inFlight === 0) {
430
+ throw new SafeAsyncError("release on idle Semaphore", "async/bad-release");
431
+ }
432
+ if (this._waiters.length > 0) {
433
+ var next = this._waiters.shift();
434
+ if (next.signal && next.onAbort) {
435
+ next.signal.removeEventListener("abort", next.onAbort);
436
+ }
437
+ next.resolve();
438
+ } else {
439
+ this._inFlight -= 1;
440
+ }
441
+ }
442
+
443
+ async runWith(fn, opts) {
444
+ await this.acquire(opts);
445
+ try {
446
+ return await fn();
447
+ } finally {
448
+ this.release();
449
+ }
450
+ }
451
+
452
+ inFlight() { return this._inFlight; }
453
+ pendingCount() { return this._waiters.length; }
454
+ }
455
+
456
+ // ---- Once ----
457
+ //
458
+ // Single-flight memoization. The first .invoke() call runs the function;
459
+ // subsequent calls (concurrent or later) await the same in-flight Promise
460
+ // and receive the same result. If the first invocation throws, the cached
461
+ // result is the rejected Promise — re-invocation will NOT retry.
462
+ //
463
+ // Use case: lazy boot-time init where multiple call sites might first-
464
+ // touch concurrently (counter primer, schema-check, key load). Without
465
+ // Once, the second concurrent caller would start its own init and produce
466
+ // double-initialization.
467
+ //
468
+ // .reset() clears the cached Promise so the next .invoke() runs fresh.
469
+ // Useful for tests, hot-reload, and operator-driven re-init after a
470
+ // transient init failure. To make resets explicit (avoid silent stale
471
+ // state), reset() does NOT cancel an in-flight first invocation —
472
+ // callers awaiting the prior invoke continue to see its result.
473
+
474
+ class Once {
475
+ constructor(fn) {
476
+ if (typeof fn !== "function") {
477
+ throw new SafeAsyncError("Once: argument must be a function", "async/bad-arg");
478
+ }
479
+ this._fn = fn;
480
+ this._promise = null;
481
+ }
482
+
483
+ invoke() {
484
+ if (this._promise === null) {
485
+ this._promise = Promise.resolve().then(this._fn);
486
+ }
487
+ return this._promise;
488
+ }
489
+
490
+ reset() {
491
+ this._promise = null;
492
+ }
493
+
494
+ hasInvoked() { return this._promise !== null; }
495
+ }
496
+
497
+ // ---- Re-exports of resilience primitives from lib/retry.js ----
498
+ //
499
+ // withRetry + CircuitBreaker live in lib/retry.js (the canonical home).
500
+ // We re-export them here under safe-async-shaped names so call sites
501
+ // reaching for async safety primitives find them in one place.
502
+
503
+ var retryHelper = require("./retry");
504
+
505
+ var asyncRetry = retryHelper.withRetry;
506
+ var CircuitBreaker = retryHelper.CircuitBreaker;
507
+
508
+ module.exports = {
509
+ withTimeout: withTimeout,
510
+ withSignal: withSignal,
511
+ withTimeoutSignal: withTimeoutSignal,
512
+ sleep: sleep,
513
+ safeAwait: safeAwait,
514
+ Mutex: Mutex,
515
+ Semaphore: Semaphore,
516
+ Once: Once,
517
+ asyncRetry: asyncRetry,
518
+ CircuitBreaker: CircuitBreaker,
519
+ SafeAsyncError: SafeAsyncError,
520
+ };
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Buffer-safety primitives — centralizes the input-normalize, capped
4
+ * chunk collection, and secure-zero patterns that were scattered across
5
+ * lib/parsers/*, lib/atomic-file.js, lib/object-store-*.js, and
6
+ * lib/log-stream-*.js.
7
+ *
8
+ * Public API:
9
+ * safeBuffer.normalizeText(input, { maxBytes, stripBom, errorClass })
10
+ * Accept string | Buffer | Uint8Array → returns string. Strips a
11
+ * leading UTF-8 BOM (U+FEFF) by default. Throws errorClass(message,
12
+ * code) if input is the wrong type or exceeds maxBytes.
13
+ *
14
+ * safeBuffer.toBuffer(data, { maxBytes, errorClass })
15
+ * Accept Buffer | Uint8Array | string → returns Buffer. Throws
16
+ * errorClass on type mismatch or oversize.
17
+ *
18
+ * safeBuffer.boundedChunkCollector({ maxBytes, errorClass })
19
+ * Returns { push(chunk), result(), bytesCollected() }. Each push()
20
+ * enforces the cap on every chunk — the OOM defense for unbounded
21
+ * HTTP response bodies replacing the previous `chunks.push(c)` +
22
+ * `Buffer.concat(chunks)` pattern that accumulated arbitrary bytes
23
+ * before checking size.
24
+ *
25
+ * safeBuffer.secureZero(buf)
26
+ * Best-effort zero of buf contents (`buf.fill(0)`). JavaScript can't
27
+ * truly zero memory — V8 may have copies — but `fill(0)` removes the
28
+ * in-Buffer reference so a heap-dump won't show the secret in this
29
+ * particular Buffer. No-op on non-Buffers.
30
+ *
31
+ * Why a default error class:
32
+ * Each caller (xml-safe, json-safe, atomic-file, ...) wants to throw
33
+ * its own format-specific error class with a particular `code`. The
34
+ * helpers accept `{ errorClass }` so the byte-handling lives here but
35
+ * the error type stays format-aware (existing tests check
36
+ * e.code === "xml/too-large" etc.). A default SafeBufferError is used
37
+ * if the caller doesn't pass one.
38
+ */
39
+
40
+ var { FrameworkError } = require("./framework-error");
41
+
42
+ class SafeBufferError extends FrameworkError {
43
+ constructor(message, code) {
44
+ super(message);
45
+ this.name = "SafeBufferError";
46
+ this.code = code || "buffer/invalid";
47
+ this.isSafeBufferError = true;
48
+ }
49
+ }
50
+
51
+ function _throw(errorClass, message, code) {
52
+ var Cls = errorClass || SafeBufferError;
53
+ throw new Cls(message, code);
54
+ }
55
+
56
+ // ---- normalizeText ----
57
+
58
+ function normalizeText(input, opts) {
59
+ opts = opts || {};
60
+ var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0) ? opts.maxBytes : null;
61
+ var stripBom = opts.stripBom !== false; // default true
62
+ var errClass = opts.errorClass;
63
+ var typeCode = opts.typeCode || "buffer/wrong-input-type";
64
+ var sizeCode = opts.sizeCode || "buffer/too-large";
65
+ var typeMsg = opts.typeMessage || "input must be string, Buffer, or Uint8Array";
66
+ var sizeMsg = opts.sizeMessage || "input exceeds maxBytes";
67
+
68
+ var text;
69
+ if (typeof input === "string") text = input;
70
+ else if (Buffer.isBuffer(input)) text = input.toString("utf8");
71
+ else if (input instanceof Uint8Array) text = Buffer.from(input).toString("utf8");
72
+ else _throw(errClass, typeMsg, typeCode);
73
+
74
+ if (stripBom && text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
75
+
76
+ if (maxBytes !== null && Buffer.byteLength(text, "utf8") > maxBytes) {
77
+ _throw(errClass, sizeMsg, sizeCode);
78
+ }
79
+ return text;
80
+ }
81
+
82
+ // ---- toBuffer ----
83
+
84
+ function toBuffer(data, opts) {
85
+ opts = opts || {};
86
+ var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0) ? opts.maxBytes : null;
87
+ var errClass = opts.errorClass;
88
+ var typeCode = opts.typeCode || "buffer/wrong-input-type";
89
+ var sizeCode = opts.sizeCode || "buffer/too-large";
90
+ var typeMsg = opts.typeMessage || "data must be Buffer, Uint8Array, or string";
91
+ var sizeMsg = opts.sizeMessage || "data exceeds maxBytes";
92
+
93
+ var buf;
94
+ if (Buffer.isBuffer(data)) buf = data;
95
+ else if (typeof data === "string") buf = Buffer.from(data, "utf8");
96
+ else if (data instanceof Uint8Array) buf = Buffer.from(data);
97
+ else _throw(errClass, typeMsg, typeCode);
98
+
99
+ if (maxBytes !== null && buf.length > maxBytes) {
100
+ _throw(errClass, sizeMsg, sizeCode);
101
+ }
102
+ return buf;
103
+ }
104
+
105
+ // ---- boundedChunkCollector ----
106
+ //
107
+ // Replaces the unbounded `chunks.push(c); ... Buffer.concat(chunks)`
108
+ // pattern in HTTP response handlers. The cap is enforced at push() time
109
+ // so a 10-GB response from a hostile/misbehaving upstream rejects on the
110
+ // chunk that overflows — without first accumulating the whole 10 GB in
111
+ // the chunks array.
112
+
113
+ function boundedChunkCollector(opts) {
114
+ opts = opts || {};
115
+ var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0) ? opts.maxBytes : null;
116
+ if (maxBytes === null) {
117
+ throw new SafeBufferError("boundedChunkCollector requires maxBytes", "buffer/bad-arg");
118
+ }
119
+ var errClass = opts.errorClass;
120
+ var sizeCode = opts.sizeCode || "buffer/too-large";
121
+ var sizeMsg = opts.sizeMessage || "stream body exceeds maxBytes";
122
+
123
+ var chunks = [];
124
+ var total = 0;
125
+
126
+ return {
127
+ push: function (chunk) {
128
+ // Accept Buffer or Uint8Array (Node's res.on('data') yields Buffer
129
+ // by default but consumers may have set encoding to get strings).
130
+ if (typeof chunk === "string") chunk = Buffer.from(chunk, "utf8");
131
+ else if (!Buffer.isBuffer(chunk) && chunk instanceof Uint8Array) chunk = Buffer.from(chunk);
132
+ if (!Buffer.isBuffer(chunk)) {
133
+ _throw(errClass, "chunk must be Buffer, Uint8Array, or string", "buffer/wrong-input-type");
134
+ }
135
+ if (total + chunk.length > maxBytes) {
136
+ _throw(errClass, sizeMsg, sizeCode);
137
+ }
138
+ chunks.push(chunk);
139
+ total += chunk.length;
140
+ },
141
+ result: function () {
142
+ return Buffer.concat(chunks, total);
143
+ },
144
+ bytesCollected: function () { return total; },
145
+ };
146
+ }
147
+
148
+ // ---- secureZero ----
149
+
150
+ function secureZero(buf) {
151
+ if (Buffer.isBuffer(buf) || buf instanceof Uint8Array) {
152
+ try { buf.fill(0); } catch (_e) { /* best effort — locked memory etc. */ }
153
+ }
154
+ }
155
+
156
+ module.exports = {
157
+ normalizeText: normalizeText,
158
+ toBuffer: toBuffer,
159
+ boundedChunkCollector: boundedChunkCollector,
160
+ secureZero: secureZero,
161
+ SafeBufferError: SafeBufferError,
162
+ };