@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
package/lib/jobs.js ADDED
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ /**
3
+ * jobs — name → handler registry on top of lib/queue.
4
+ *
5
+ * lib/queue handles dispatch (queue backend + lease + retry + breaker
6
+ * + audit emit). lib/jobs is the application-level pattern most apps
7
+ * actually use: register a handler ahead of time, enqueue by name,
8
+ * the framework runs the right code without per-call wiring.
9
+ *
10
+ * var jobs = b.jobs.create();
11
+ * jobs.define("send-welcome", async function ({ userId }) { … });
12
+ * jobs.define("rebuild-index", async function () { … });
13
+ *
14
+ * await jobs.enqueue("send-welcome", { userId: "u-1" });
15
+ *
16
+ * await jobs.start(); // consumers running
17
+ * …
18
+ * await jobs.shutdown(); // drain in-flight + stop consuming
19
+ *
20
+ * Each defined name maps to one queue (queue named after the job
21
+ * name) with one consumer per jobs instance. Operators wanting
22
+ * higher concurrency for a specific job pass concurrency in the
23
+ * define options.
24
+ *
25
+ * Built on b.queue, which must be initialized first:
26
+ *
27
+ * b.queue.init({ backends: { primary: { protocol: "local" } } });
28
+ * var jobs = b.jobs.create();
29
+ * jobs.define(...);
30
+ * await jobs.start();
31
+ *
32
+ * createApp wires this for you when opts.jobs is a function — the
33
+ * factory boots queue with the default 'local' backend, instantiates
34
+ * a jobs registry, calls opts.jobs(jobs) so the operator defines
35
+ * handlers, and starts consumption before listen().
36
+ *
37
+ * Public API:
38
+ *
39
+ * jobs.create(opts?) → instance
40
+ * opts.queueBackend — backend name to dispatch through
41
+ * (default 'primary' — the convention
42
+ * queue.init uses when given a
43
+ * single-backend config)
44
+ * opts.consumerDefaults — defaults forwarded to queue.consume
45
+ * ({ concurrency, leaseDurationMs,
46
+ * pollIntervalMs, fastPollMs })
47
+ * opts.allowUnregisteredEnqueue default false. When true, jobs.enqueue
48
+ * accepts names not yet defined
49
+ * (useful in test fixtures or for
50
+ * apps that expect lazy handler
51
+ * registration).
52
+ *
53
+ * jobs.define(name, handler, defineOpts?)
54
+ * handler signature: async function (job) { … } where job =
55
+ * { jobId, queueName, payload, attempts, maxAttempts, traceId,
56
+ * classification, enqueuedAt, leaseExpiresAt }.
57
+ * defineOpts is forwarded to queue.consume — concurrency etc.
58
+ * Throws if name is already defined.
59
+ *
60
+ * await jobs.enqueue(name, payload, enqueueOpts?)
61
+ * enqueueOpts is forwarded to queue.enqueue — delaySeconds,
62
+ * maxAttempts, traceId, classification.
63
+ * Returns { jobId, queueName, enqueuedAt, ... } from queue.enqueue.
64
+ *
65
+ * await jobs.start()
66
+ * Begins consuming each defined queue. Idempotent — a second
67
+ * start() is a no-op once consumers are already running.
68
+ *
69
+ * await jobs.shutdown(opts?)
70
+ * Stops consumers, drains in-flight via queue.shutdown.
71
+ * opts.timeoutMs forwards to queue.shutdown.
72
+ *
73
+ * jobs.stats()
74
+ * → { defined: [string], started: boolean }
75
+ */
76
+ var queue = require("./queue");
77
+ var validateOpts = require("./validate-opts");
78
+ var { JobsError } = require("./framework-error");
79
+
80
+ function create(opts) {
81
+ opts = opts || {};
82
+ validateOpts(opts, [
83
+ "queueBackend", "consumerDefaults", "allowUnregisteredEnqueue",
84
+ ], "b.jobs");
85
+ var queueBackend = opts.queueBackend || "primary";
86
+ var consumerDefaults = opts.consumerDefaults || {};
87
+ var allowUnregistered = !!opts.allowUnregisteredEnqueue;
88
+
89
+ // name → { handler, defineOpts, consumerHandle (after start) }
90
+ var registry = new Map();
91
+ var started = false;
92
+
93
+ var _err = JobsError.factory;
94
+
95
+ function define(name, handler, defineOpts) {
96
+ if (typeof name !== "string" || name.length === 0) {
97
+ throw _err("INVALID_NAME", "jobs.define: name must be a non-empty string", true);
98
+ }
99
+ if (typeof handler !== "function") {
100
+ throw _err("INVALID_HANDLER", "jobs.define: handler must be a function", true);
101
+ }
102
+ if (registry.has(name)) {
103
+ throw _err("DUPLICATE_NAME",
104
+ "jobs.define: '" + name + "' is already defined", true);
105
+ }
106
+ if (started) {
107
+ // Defining after start would mean the new handler doesn't run
108
+ // until the next start cycle. Reject loudly so operators don't
109
+ // wonder why their newly-defined handler is silent.
110
+ throw _err("ALREADY_STARTED",
111
+ "jobs.define: cannot register '" + name + "' after start() — " +
112
+ "define all handlers before calling start()", true);
113
+ }
114
+ registry.set(name, {
115
+ handler: handler,
116
+ defineOpts: defineOpts || {},
117
+ });
118
+ }
119
+
120
+ async function enqueue(name, payload, enqueueOpts) {
121
+ if (typeof name !== "string" || name.length === 0) {
122
+ throw _err("INVALID_NAME", "jobs.enqueue: name must be a non-empty string", true);
123
+ }
124
+ if (!allowUnregistered && !registry.has(name)) {
125
+ throw _err("UNDEFINED_NAME",
126
+ "jobs.enqueue: '" + name + "' has no registered handler. " +
127
+ "Either define(name, handler) first, or pass " +
128
+ "{ allowUnregisteredEnqueue: true } to jobs.create.", true);
129
+ }
130
+ return await queue.enqueue(name, payload, Object.assign(
131
+ { backend: queueBackend },
132
+ enqueueOpts || {}
133
+ ));
134
+ }
135
+
136
+ async function start() {
137
+ if (started) return;
138
+ var consumerOpts = Object.assign({ backend: queueBackend }, consumerDefaults);
139
+ registry.forEach(function (entry, name) {
140
+ var perJobOpts = Object.assign({}, consumerOpts, entry.defineOpts);
141
+ entry.consumerHandle = queue.consume(name, entry.handler, perJobOpts);
142
+ });
143
+ started = true;
144
+ }
145
+
146
+ async function shutdown(shutdownOpts) {
147
+ if (!started) {
148
+ // Even when not started, queue.shutdown handles its own state.
149
+ try { await queue.shutdown(shutdownOpts); } catch (_e) {}
150
+ return;
151
+ }
152
+ started = false;
153
+ await queue.shutdown(shutdownOpts);
154
+ // Don't clear the registry — operators inspecting stats() after
155
+ // shutdown should still see what was defined; only the running
156
+ // state changes.
157
+ }
158
+
159
+ function stats() {
160
+ return {
161
+ defined: Array.from(registry.keys()),
162
+ started: started,
163
+ };
164
+ }
165
+
166
+ function _resetForTest() {
167
+ registry.clear();
168
+ started = false;
169
+ }
170
+
171
+ return {
172
+ define: define,
173
+ enqueue: enqueue,
174
+ start: start,
175
+ shutdown: shutdown,
176
+ stats: stats,
177
+ _resetForTest: _resetForTest,
178
+ };
179
+ }
180
+
181
+ module.exports = { create: create };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Lazy-require — cached deferred-load helper.
4
+ *
5
+ * Centralizes the pattern used to break circular-load chains between
6
+ * modules that depend on each other through different code paths
7
+ * (audit ↔ db, vault ↔ db, middleware ↔ audit). Every dependent module
8
+ * was carrying its own copy of:
9
+ *
10
+ * var _db = null;
11
+ * function db() { if (!_db) _db = require("./db"); return _db; }
12
+ * // and `_db = null;` in _resetForTest
13
+ *
14
+ * `lazyRequire(loader)` returns a callable getter `db()` that does the
15
+ * cache-on-first-call dance once, plus a `db.reset()` for test
16
+ * teardown. The `loader` is a function (NOT a path string) so the
17
+ * inner `require()` resolves relative to the CALLER's __filename, not
18
+ * lib/lazy-require.js — passing a string here would break relative
19
+ * paths from any module not co-located with lazy-require.js.
20
+ *
21
+ * Usage:
22
+ *
23
+ * var lazyRequire = require("./lazy-require");
24
+ * var db = lazyRequire(function () { return require("./db"); });
25
+ * // ... later ...
26
+ * db().findOne(...); // first call resolves + caches
27
+ * // in _resetForTest:
28
+ * db.reset();
29
+ */
30
+
31
+ function lazyRequire(loader) {
32
+ if (typeof loader !== "function") {
33
+ throw new Error("lazyRequire(loader): loader must be a function returning the require() result");
34
+ }
35
+ // Separate `loaded` flag from `cached` so a loader that legitimately
36
+ // returns null/undefined/0/false caches that value instead of re-running
37
+ // on every subsequent call.
38
+ var loaded = false;
39
+ var cached;
40
+ function get() {
41
+ if (!loaded) { cached = loader(); loaded = true; }
42
+ return cached;
43
+ }
44
+ get.reset = function () { loaded = false; cached = undefined; };
45
+ return get;
46
+ }
47
+
48
+ module.exports = lazyRequire;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Local file-based log sink with append-only journaling + rotation.
4
+ *
5
+ * Each event is written as one JSON line (jsonl format — one row per
6
+ * event). Files rotate by size (default 100 MiB) and/or age (default
7
+ * 7 days). Old rotations are gzip-compressed automatically and capped at
8
+ * a configured count (default 30) — older rotations are deleted.
9
+ *
10
+ * The active log file is opened in append mode and never updated in place.
11
+ * Operators can apply OS-level immutability (Linux chattr +a) to the
12
+ * directory if they want stronger tamper-resistance — the framework
13
+ * doesn't fight that; appends still work.
14
+ *
15
+ * Config:
16
+ * {
17
+ * dir: './logs/operational'
18
+ * maxFileBytes: C.BYTES.mib(100)
19
+ * maxFileAgeMs: C.TIME.days(7)
20
+ * keepRotations: 30
21
+ * compressRotations: true
22
+ * fileMode: 0o600
23
+ * fileNamePrefix: 'blamejs'
24
+ * }
25
+ */
26
+ var fs = require("fs");
27
+ var path = require("path");
28
+ var zlib = require("zlib");
29
+ var atomicFile = require("./atomic-file");
30
+ var C = require("./constants");
31
+ var { LogStreamError } = require("./framework-error");
32
+
33
+ var DEFAULTS = {
34
+ maxFileBytes: C.BYTES.mib(100),
35
+ maxFileAgeMs: C.TIME.days(7),
36
+ keepRotations: 30,
37
+ compressRotations: true,
38
+ fileMode: 0o600,
39
+ fileNamePrefix: "blamejs",
40
+ };
41
+
42
+ var _err = LogStreamError.factory;
43
+
44
+ function create(config) {
45
+ if (!config || !config.dir) throw new Error("log-stream local requires { dir }");
46
+ var cfg = Object.assign({}, DEFAULTS, config);
47
+ var dir = path.resolve(cfg.dir);
48
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
49
+
50
+ var activePath = path.join(dir, cfg.fileNamePrefix + ".log");
51
+ var fd = null;
52
+ var openedAt = 0;
53
+ var bytesWritten = 0;
54
+
55
+ function _open() {
56
+ fd = fs.openSync(activePath, "a", cfg.fileMode);
57
+ openedAt = Date.now();
58
+ try {
59
+ var stat = fs.fstatSync(fd);
60
+ bytesWritten = stat.size;
61
+ } catch (_e) {
62
+ bytesWritten = 0;
63
+ }
64
+ }
65
+ _open();
66
+
67
+ function _shouldRotate() {
68
+ if (cfg.maxFileBytes && bytesWritten >= cfg.maxFileBytes) return true;
69
+ if (cfg.maxFileAgeMs && (Date.now() - openedAt) >= cfg.maxFileAgeMs) return true;
70
+ return false;
71
+ }
72
+
73
+ function _rotate() {
74
+ try {
75
+ if (fd != null) { try { fs.closeSync(fd); } catch (_e) {} fd = null; }
76
+ // Build rotated filename: blamejs-YYYYMMDDTHHMMSSZ.log
77
+ var stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
78
+ var rotated = path.join(dir, cfg.fileNamePrefix + "-" + stamp + ".log");
79
+ if (fs.existsSync(activePath)) {
80
+ fs.renameSync(activePath, rotated);
81
+ if (cfg.compressRotations) {
82
+ var data = fs.readFileSync(rotated);
83
+ var gz = zlib.gzipSync(data);
84
+ atomicFile.writeSync(rotated + ".gz", gz, { fileMode: cfg.fileMode });
85
+ fs.unlinkSync(rotated);
86
+ }
87
+ }
88
+ _pruneOld();
89
+ } finally {
90
+ _open();
91
+ }
92
+ }
93
+
94
+ function _pruneOld() {
95
+ if (!cfg.keepRotations || cfg.keepRotations <= 0) return;
96
+ var entries = atomicFile.listDir(dir, {
97
+ filter: function (f) {
98
+ return f.startsWith(cfg.fileNamePrefix + "-") &&
99
+ (f.endsWith(".log") || f.endsWith(".log.gz"));
100
+ },
101
+ includeStat: true,
102
+ }).sort(function (a, b) { return b.mtimeMs - a.mtimeMs; }); // newest first
103
+ for (var i = cfg.keepRotations; i < entries.length; i++) {
104
+ try { fs.unlinkSync(entries[i].fullPath); } catch (_e) { /* best effort */ }
105
+ }
106
+ }
107
+
108
+ function emit(record) {
109
+ if (_shouldRotate()) _rotate();
110
+ var line = JSON.stringify(record) + "\n";
111
+ var buf = Buffer.from(line, "utf8");
112
+ fs.writeSync(fd, buf, 0, buf.length, null);
113
+ bytesWritten += buf.length;
114
+ return Promise.resolve({ bytes: buf.length });
115
+ }
116
+
117
+ function close() {
118
+ if (fd != null) {
119
+ try { fs.fsyncSync(fd); } catch (_e) { /* best effort */ }
120
+ try { fs.closeSync(fd); } catch (_e) {}
121
+ fd = null;
122
+ }
123
+ return Promise.resolve();
124
+ }
125
+
126
+ function getActivePath() { return activePath; }
127
+
128
+ return {
129
+ protocol: "local",
130
+ emit: emit,
131
+ close: close,
132
+ rotate: function () { _rotate(); return Promise.resolve(); },
133
+ getActivePath: getActivePath,
134
+ };
135
+ }
136
+
137
+ module.exports = { create: create };
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /**
3
+ * Generic webhook log sink — HTTP POST one event (or a batch) at a time.
4
+ *
5
+ * Covers most SIEM ingestion endpoints with simple HTTP POST + JSON body:
6
+ * Splunk HEC — { auth: 'header', headers: { Authorization: 'Splunk <token>' } }
7
+ * Datadog Logs — { auth: 'header', headers: { 'DD-API-KEY': '<key>' } }
8
+ * Sumo Logic HTTP source — no auth (URL is the secret)
9
+ * Grafana Loki push API — { auth: 'basic' }
10
+ * Generic OpenTelemetry HTTP — { headers: { 'Content-Type': 'application/x-protobuf' } } — caller controls body shape
11
+ *
12
+ * Streaming model: events accumulate in a per-sink queue; a worker drains
13
+ * it in batches (default size 100, max age 5s) to balance throughput
14
+ * against latency. On webhook 5xx / network errors the batch retries with
15
+ * exponential backoff (via the framework's retry module). On permanent 4xx
16
+ * the batch is dropped and an audit event is recorded.
17
+ *
18
+ * Config:
19
+ * {
20
+ * url: 'https://siem.example.com/ingest'
21
+ * auth: 'none'|'bearer'|'basic'|'header'
22
+ * token / username+password / headers
23
+ * batchSize: 100
24
+ * maxBatchAgeMs: C.TIME.seconds(5)
25
+ * contentType: 'application/json'
26
+ * bodyShape: 'array' | 'ndjson' | 'singleEnvelope'
27
+ * timeoutMs: C.TIME.seconds(30)
28
+ * retry: { maxAttempts, baseDelayMs, ... }
29
+ * bufferLimit: 10000 // ring-buffer cap; drops oldest on overflow
30
+ * }
31
+ */
32
+ var C = require("./constants");
33
+ var retryHelper = require("./retry");
34
+ var { LogStreamError } = require("./framework-error");
35
+ var httpClient = require("./http-client");
36
+ var safeUrl = require("./safe-url");
37
+ var authHeader = require("./auth-header");
38
+
39
+ // Webhook responses are ack-only (status + small body). 1 MiB cap is
40
+ // generous; misbehaving log-aggregator endpoints don't get to OOM us.
41
+ var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
42
+
43
+ var DEFAULTS = {
44
+ batchSize: 100,
45
+ maxBatchAgeMs: C.TIME.seconds(5),
46
+ contentType: "application/json",
47
+ bodyShape: "array",
48
+ timeoutMs: C.TIME.seconds(30),
49
+ bufferLimit: 10000,
50
+ };
51
+
52
+ var _err = LogStreamError.factory;
53
+
54
+ // Auth-header construction is delegated to lib/auth-header for the
55
+ // none/bearer/basic triple. The "header" mode (pass-through arbitrary
56
+ // headers) is handled here — it's not an auth scheme, just header
57
+ // merging that's traditionally been bundled in the same config knob.
58
+ function _authHeaders(config) {
59
+ if (config.auth === "header") return Object.assign({}, config.headers || {});
60
+ return authHeader.fromConfig(config);
61
+ }
62
+
63
+ function _post(url, body, headers, timeoutMs, allowedProtocols) {
64
+ return httpClient.request({
65
+ method: "POST",
66
+ url: url,
67
+ headers: headers,
68
+ body: body,
69
+ idleTimeoutMs: timeoutMs,
70
+ maxResponseBytes: MAX_RESPONSE_BYTES,
71
+ errorClass: LogStreamError,
72
+ allowedProtocols: allowedProtocols,
73
+ });
74
+ }
75
+
76
+ function _serializeBatch(records, shape) {
77
+ if (shape === "ndjson") {
78
+ return Buffer.from(records.map(function (r) { return JSON.stringify(r); }).join("\n") + "\n", "utf8");
79
+ }
80
+ if (shape === "singleEnvelope") {
81
+ return Buffer.from(JSON.stringify({ events: records }), "utf8");
82
+ }
83
+ // default: array
84
+ return Buffer.from(JSON.stringify(records), "utf8");
85
+ }
86
+
87
+ function create(config) {
88
+ if (!config || !config.url) throw new Error("log-stream webhook requires { url }");
89
+ var cfg = Object.assign({}, DEFAULTS, config);
90
+ // Fail fast on misconfig — validate URL shape + scheme at create time
91
+ // rather than at first emit. Default is HTTPS-only; operators with an
92
+ // internal cleartext aggregator pass cfg.allowedProtocols
93
+ // (safeUrl.ALLOW_HTTP_ALL).
94
+ safeUrl.parse(cfg.url, {
95
+ allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
96
+ errorClass: LogStreamError,
97
+ });
98
+ var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
99
+ var buffer = [];
100
+ var dropCount = 0;
101
+ var flushTimer = null;
102
+ var inFlight = false;
103
+ var closed = false;
104
+
105
+ function _scheduleFlush() {
106
+ if (flushTimer) return;
107
+ flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, cfg.maxBatchAgeMs);
108
+ flushTimer.unref();
109
+ }
110
+
111
+ async function _flush() {
112
+ if (inFlight) return;
113
+ if (buffer.length === 0) return;
114
+ inFlight = true;
115
+ try {
116
+ while (buffer.length > 0 && !closed) {
117
+ var batch = buffer.splice(0, cfg.batchSize);
118
+ var body = _serializeBatch(batch, cfg.bodyShape);
119
+ try {
120
+ await retryHelper.withRetry(function () {
121
+ return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols);
122
+ }, cfg.retry);
123
+ } catch {
124
+ // Batch permanently rejected — surface via the dropped counter.
125
+ // Caller's audit hook recorded the drop already at the dispatcher.
126
+ break;
127
+ }
128
+ }
129
+ } finally {
130
+ inFlight = false;
131
+ if (buffer.length > 0) _scheduleFlush();
132
+ }
133
+ }
134
+
135
+ function emit(record) {
136
+ if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
137
+ if (buffer.length >= cfg.bufferLimit) {
138
+ buffer.shift(); // drop oldest
139
+ dropCount += 1;
140
+ }
141
+ buffer.push(record);
142
+ if (buffer.length >= cfg.batchSize) {
143
+ // Don't await — non-blocking flush. Caller's emit returns immediately.
144
+ _flush().catch(function () {});
145
+ } else {
146
+ _scheduleFlush();
147
+ }
148
+ return Promise.resolve({ accepted: true, queued: buffer.length });
149
+ }
150
+
151
+ async function close() {
152
+ closed = true;
153
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
154
+ await _flush();
155
+ }
156
+
157
+ function stats() {
158
+ return { queued: buffer.length, dropped: dropCount, inFlight: inFlight };
159
+ }
160
+
161
+ return {
162
+ protocol: "webhook",
163
+ emit: emit,
164
+ close: close,
165
+ stats: stats,
166
+ flush: _flush,
167
+ };
168
+ }
169
+
170
+ module.exports = { create: create };