@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,199 +1,199 @@
|
|
|
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 safeAsync = require("./safe-async");
|
|
37
|
-
var safeUrl = require("./safe-url");
|
|
38
|
-
var authHeader = require("./auth-header");
|
|
39
|
-
|
|
40
|
-
// Webhook responses are ack-only (status + small body). 1 MiB cap is
|
|
41
|
-
// generous; misbehaving log-aggregator endpoints don't get to OOM us.
|
|
42
|
-
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
43
|
-
|
|
44
|
-
var DEFAULTS = {
|
|
45
|
-
batchSize: 100,
|
|
46
|
-
maxBatchAgeMs: C.TIME.seconds(5),
|
|
47
|
-
contentType: "application/json",
|
|
48
|
-
bodyShape: "array",
|
|
49
|
-
timeoutMs: C.TIME.seconds(30),
|
|
50
|
-
bufferLimit: C.BYTES.bytes(10000),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
var _err = LogStreamError.factory;
|
|
54
|
-
|
|
55
|
-
// Auth-header construction is delegated to lib/auth-header for the
|
|
56
|
-
// none/bearer/basic triple. The "header" mode (pass-through arbitrary
|
|
57
|
-
// headers) is handled here — it's not an auth scheme, just header
|
|
58
|
-
// merging that's traditionally been bundled in the same config knob.
|
|
59
|
-
function _authHeaders(config) {
|
|
60
|
-
if (config.auth === "header") return Object.assign({}, config.headers || {});
|
|
61
|
-
return authHeader.fromConfig(config);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
|
|
65
|
-
return httpClient.request({
|
|
66
|
-
method: "POST",
|
|
67
|
-
url: url,
|
|
68
|
-
headers: headers,
|
|
69
|
-
body: body,
|
|
70
|
-
idleTimeoutMs: timeoutMs,
|
|
71
|
-
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
72
|
-
errorClass: LogStreamError,
|
|
73
|
-
allowedProtocols: allowedProtocols,
|
|
74
|
-
allowInternal: allowInternal,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function _serializeBatch(records, shape) {
|
|
79
|
-
if (shape === "ndjson") {
|
|
80
|
-
return Buffer.from(records.map(function (r) { return JSON.stringify(r); }).join("\n") + "\n", "utf8");
|
|
81
|
-
}
|
|
82
|
-
if (shape === "singleEnvelope") {
|
|
83
|
-
return Buffer.from(JSON.stringify({ events: records }), "utf8");
|
|
84
|
-
}
|
|
85
|
-
// default: array
|
|
86
|
-
return Buffer.from(JSON.stringify(records), "utf8");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function create(config) {
|
|
90
|
-
if (!config || !config.url) throw new Error("log-stream webhook requires { url }");
|
|
91
|
-
var cfg = Object.assign({}, DEFAULTS, config);
|
|
92
|
-
// Fail fast on misconfig — validate URL shape + scheme at create time
|
|
93
|
-
// rather than at first emit. Default is HTTPS-only; operators with an
|
|
94
|
-
// internal cleartext aggregator pass cfg.allowedProtocols
|
|
95
|
-
// (safeUrl.ALLOW_HTTP_ALL).
|
|
96
|
-
safeUrl.parse(cfg.url, {
|
|
97
|
-
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
98
|
-
errorClass: LogStreamError,
|
|
99
|
-
});
|
|
100
|
-
var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
|
|
101
|
-
// onDrop callback: invoked when a batch is dropped, either by buffer
|
|
102
|
-
// overflow ("overflow") or by retry exhaustion ("retry-exhausted").
|
|
103
|
-
// Operator wiring this directly (without the framework's dispatcher
|
|
104
|
-
// wrapping) needs visibility into permanent-drop events; the
|
|
105
|
-
// dispatcher path emits its own audit, but a sink used in isolation
|
|
106
|
-
// would otherwise lose drops silently. The callback is invoked
|
|
107
|
-
// best-effort — a throw inside it is swallowed.
|
|
108
|
-
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
109
|
-
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
110
|
-
var buffer = [];
|
|
111
|
-
var dropCount = 0;
|
|
112
|
-
var inFlight = false;
|
|
113
|
-
var closed = false;
|
|
114
|
-
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
115
|
-
|
|
116
|
-
// Track the in-flight flush as a promise so close() can await it.
|
|
117
|
-
// Without this, a record arriving mid-flush gets buffered, the
|
|
118
|
-
// emit-time _flush() early-returns on `if (inFlight) return`, and
|
|
119
|
-
// the buffered record is stranded if shutdown fires before
|
|
120
|
-
// flushScheduler.schedule() drains it.
|
|
121
|
-
var inFlightPromise = null;
|
|
122
|
-
async function _flush() {
|
|
123
|
-
if (inFlight) return inFlightPromise;
|
|
124
|
-
if (buffer.length === 0) return;
|
|
125
|
-
inFlight = true;
|
|
126
|
-
inFlightPromise = (async function () {
|
|
127
|
-
try {
|
|
128
|
-
while (buffer.length > 0 && !closed) {
|
|
129
|
-
var batch = buffer.splice(0, cfg.batchSize);
|
|
130
|
-
var body = _serializeBatch(batch, cfg.bodyShape);
|
|
131
|
-
try {
|
|
132
|
-
await retryHelper.withRetry(function () {
|
|
133
|
-
return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
|
|
134
|
-
}, cfg.retry);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
// Batch permanently rejected — surface via dropCount AND the
|
|
137
|
-
// operator-supplied onDrop callback. The dispatcher path
|
|
138
|
-
// wraps its own audit hook around emit(); operators using
|
|
139
|
-
// this sink directly rely on dropCount + onDrop.
|
|
140
|
-
dropCount += batch.length;
|
|
141
|
-
_emitDrop("retry-exhausted", batch, e);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} finally {
|
|
146
|
-
inFlight = false;
|
|
147
|
-
inFlightPromise = null;
|
|
148
|
-
if (buffer.length > 0) flushScheduler.schedule();
|
|
149
|
-
}
|
|
150
|
-
})();
|
|
151
|
-
return inFlightPromise;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function emit(record) {
|
|
155
|
-
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
156
|
-
if (buffer.length >= cfg.bufferLimit) {
|
|
157
|
-
var dropped = buffer.shift(); // drop oldest
|
|
158
|
-
dropCount += 1;
|
|
159
|
-
_emitDrop("overflow", [dropped], null);
|
|
160
|
-
}
|
|
161
|
-
buffer.push(record);
|
|
162
|
-
if (buffer.length >= cfg.batchSize) {
|
|
163
|
-
// Don't await — non-blocking flush. Caller's emit returns immediately.
|
|
164
|
-
_flush().catch(function () {});
|
|
165
|
-
} else {
|
|
166
|
-
flushScheduler.schedule();
|
|
167
|
-
}
|
|
168
|
-
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function close() {
|
|
172
|
-
// Drain BEFORE flipping closed=true. _flush()'s while loop bails on
|
|
173
|
-
// !closed, so flipping the flag first leaves any buffered records
|
|
174
|
-
// stranded — the very records the operator queued just before
|
|
175
|
-
// calling shutdown(). Order: stop the timer, await any in-flight
|
|
176
|
-
// flush so its records POST before we touch the buffer, drain
|
|
177
|
-
// anything still queued, THEN refuse new enqueues.
|
|
178
|
-
flushScheduler.cancel();
|
|
179
|
-
if (inFlightPromise) {
|
|
180
|
-
try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
|
|
181
|
-
}
|
|
182
|
-
await _flush();
|
|
183
|
-
closed = true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function stats() {
|
|
187
|
-
return { queued: buffer.length, dropped: dropCount, inFlight: inFlight };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
protocol: "webhook",
|
|
192
|
-
emit: emit,
|
|
193
|
-
close: close,
|
|
194
|
-
stats: stats,
|
|
195
|
-
flush: _flush,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
module.exports = { create: create };
|
|
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 safeAsync = require("./safe-async");
|
|
37
|
+
var safeUrl = require("./safe-url");
|
|
38
|
+
var authHeader = require("./auth-header");
|
|
39
|
+
|
|
40
|
+
// Webhook responses are ack-only (status + small body). 1 MiB cap is
|
|
41
|
+
// generous; misbehaving log-aggregator endpoints don't get to OOM us.
|
|
42
|
+
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
43
|
+
|
|
44
|
+
var DEFAULTS = {
|
|
45
|
+
batchSize: 100,
|
|
46
|
+
maxBatchAgeMs: C.TIME.seconds(5),
|
|
47
|
+
contentType: "application/json",
|
|
48
|
+
bodyShape: "array",
|
|
49
|
+
timeoutMs: C.TIME.seconds(30),
|
|
50
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
var _err = LogStreamError.factory;
|
|
54
|
+
|
|
55
|
+
// Auth-header construction is delegated to lib/auth-header for the
|
|
56
|
+
// none/bearer/basic triple. The "header" mode (pass-through arbitrary
|
|
57
|
+
// headers) is handled here — it's not an auth scheme, just header
|
|
58
|
+
// merging that's traditionally been bundled in the same config knob.
|
|
59
|
+
function _authHeaders(config) {
|
|
60
|
+
if (config.auth === "header") return Object.assign({}, config.headers || {});
|
|
61
|
+
return authHeader.fromConfig(config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
|
|
65
|
+
return httpClient.request({
|
|
66
|
+
method: "POST",
|
|
67
|
+
url: url,
|
|
68
|
+
headers: headers,
|
|
69
|
+
body: body,
|
|
70
|
+
idleTimeoutMs: timeoutMs,
|
|
71
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
72
|
+
errorClass: LogStreamError,
|
|
73
|
+
allowedProtocols: allowedProtocols,
|
|
74
|
+
allowInternal: allowInternal,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _serializeBatch(records, shape) {
|
|
79
|
+
if (shape === "ndjson") {
|
|
80
|
+
return Buffer.from(records.map(function (r) { return JSON.stringify(r); }).join("\n") + "\n", "utf8");
|
|
81
|
+
}
|
|
82
|
+
if (shape === "singleEnvelope") {
|
|
83
|
+
return Buffer.from(JSON.stringify({ events: records }), "utf8");
|
|
84
|
+
}
|
|
85
|
+
// default: array
|
|
86
|
+
return Buffer.from(JSON.stringify(records), "utf8");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function create(config) {
|
|
90
|
+
if (!config || !config.url) throw new Error("log-stream webhook requires { url }");
|
|
91
|
+
var cfg = Object.assign({}, DEFAULTS, config);
|
|
92
|
+
// Fail fast on misconfig — validate URL shape + scheme at create time
|
|
93
|
+
// rather than at first emit. Default is HTTPS-only; operators with an
|
|
94
|
+
// internal cleartext aggregator pass cfg.allowedProtocols
|
|
95
|
+
// (safeUrl.ALLOW_HTTP_ALL).
|
|
96
|
+
safeUrl.parse(cfg.url, {
|
|
97
|
+
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
98
|
+
errorClass: LogStreamError,
|
|
99
|
+
});
|
|
100
|
+
var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
|
|
101
|
+
// onDrop callback: invoked when a batch is dropped, either by buffer
|
|
102
|
+
// overflow ("overflow") or by retry exhaustion ("retry-exhausted").
|
|
103
|
+
// Operator wiring this directly (without the framework's dispatcher
|
|
104
|
+
// wrapping) needs visibility into permanent-drop events; the
|
|
105
|
+
// dispatcher path emits its own audit, but a sink used in isolation
|
|
106
|
+
// would otherwise lose drops silently. The callback is invoked
|
|
107
|
+
// best-effort — a throw inside it is swallowed.
|
|
108
|
+
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
109
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
110
|
+
var buffer = [];
|
|
111
|
+
var dropCount = 0;
|
|
112
|
+
var inFlight = false;
|
|
113
|
+
var closed = false;
|
|
114
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
115
|
+
|
|
116
|
+
// Track the in-flight flush as a promise so close() can await it.
|
|
117
|
+
// Without this, a record arriving mid-flush gets buffered, the
|
|
118
|
+
// emit-time _flush() early-returns on `if (inFlight) return`, and
|
|
119
|
+
// the buffered record is stranded if shutdown fires before
|
|
120
|
+
// flushScheduler.schedule() drains it.
|
|
121
|
+
var inFlightPromise = null;
|
|
122
|
+
async function _flush() {
|
|
123
|
+
if (inFlight) return inFlightPromise;
|
|
124
|
+
if (buffer.length === 0) return;
|
|
125
|
+
inFlight = true;
|
|
126
|
+
inFlightPromise = (async function () {
|
|
127
|
+
try {
|
|
128
|
+
while (buffer.length > 0 && !closed) {
|
|
129
|
+
var batch = buffer.splice(0, cfg.batchSize);
|
|
130
|
+
var body = _serializeBatch(batch, cfg.bodyShape);
|
|
131
|
+
try {
|
|
132
|
+
await retryHelper.withRetry(function () {
|
|
133
|
+
return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
|
|
134
|
+
}, cfg.retry);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Batch permanently rejected — surface via dropCount AND the
|
|
137
|
+
// operator-supplied onDrop callback. The dispatcher path
|
|
138
|
+
// wraps its own audit hook around emit(); operators using
|
|
139
|
+
// this sink directly rely on dropCount + onDrop.
|
|
140
|
+
dropCount += batch.length;
|
|
141
|
+
_emitDrop("retry-exhausted", batch, e);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
inFlight = false;
|
|
147
|
+
inFlightPromise = null;
|
|
148
|
+
if (buffer.length > 0) flushScheduler.schedule();
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
return inFlightPromise;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function emit(record) {
|
|
155
|
+
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
156
|
+
if (buffer.length >= cfg.bufferLimit) {
|
|
157
|
+
var dropped = buffer.shift(); // drop oldest
|
|
158
|
+
dropCount += 1;
|
|
159
|
+
_emitDrop("overflow", [dropped], null);
|
|
160
|
+
}
|
|
161
|
+
buffer.push(record);
|
|
162
|
+
if (buffer.length >= cfg.batchSize) {
|
|
163
|
+
// Don't await — non-blocking flush. Caller's emit returns immediately.
|
|
164
|
+
_flush().catch(function () {});
|
|
165
|
+
} else {
|
|
166
|
+
flushScheduler.schedule();
|
|
167
|
+
}
|
|
168
|
+
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function close() {
|
|
172
|
+
// Drain BEFORE flipping closed=true. _flush()'s while loop bails on
|
|
173
|
+
// !closed, so flipping the flag first leaves any buffered records
|
|
174
|
+
// stranded — the very records the operator queued just before
|
|
175
|
+
// calling shutdown(). Order: stop the timer, await any in-flight
|
|
176
|
+
// flush so its records POST before we touch the buffer, drain
|
|
177
|
+
// anything still queued, THEN refuse new enqueues.
|
|
178
|
+
flushScheduler.cancel();
|
|
179
|
+
if (inFlightPromise) {
|
|
180
|
+
try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
|
|
181
|
+
}
|
|
182
|
+
await _flush();
|
|
183
|
+
closed = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stats() {
|
|
187
|
+
return { queued: buffer.length, dropped: dropCount, inFlight: inFlight };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
protocol: "webhook",
|
|
192
|
+
emit: emit,
|
|
193
|
+
close: close,
|
|
194
|
+
stats: stats,
|
|
195
|
+
flush: _flush,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { create: create };
|