@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.
- package/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- 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 };
|