@blamejs/core 0.6.36 → 0.6.58
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 +34 -0
- package/README.md +2 -2
- package/lib/audit.js +1 -0
- package/lib/bundler.js +166 -31
- package/lib/i18n-messageformat.js +398 -0
- package/lib/i18n.js +17 -0
- package/lib/log-stream-otlp-grpc.js +413 -0
- package/lib/log-stream.js +8 -0
- package/lib/mail.js +18 -3
- package/lib/mtls-ca.js +155 -0
- package/lib/mtls-engine-default.js +40 -0
- package/lib/object-store/azure-blob-bucket-ops.js +291 -0
- package/lib/object-store/gcs-bucket-ops.js +327 -0
- package/lib/object-store/index.js +65 -14
- package/lib/object-store/sigv4-bucket-ops.js +639 -39
- package/lib/object-store/sigv4.js +10 -3
- package/lib/protobuf-encoder.js +184 -0
- package/lib/queue-sqs.js +314 -0
- package/lib/queue.js +2 -2
- package/lib/safe-buffer.js +15 -2
- package/lib/totp.js +11 -3
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OTLP gRPC log sink — OpenTelemetry Protocol logs over gRPC.
|
|
4
|
+
*
|
|
5
|
+
* Companion to lib/log-stream-otlp.js (HTTP/JSON). The OTel collector
|
|
6
|
+
* accepts both transports; gRPC is the higher-throughput path with
|
|
7
|
+
* less per-batch overhead and is preferred for production deployments
|
|
8
|
+
* pushing >100K logs/s.
|
|
9
|
+
*
|
|
10
|
+
* Wire format:
|
|
11
|
+
* - HTTP/2 (node:http2) POST to
|
|
12
|
+
* /opentelemetry.proto.collector.logs.v1.LogsService/Export
|
|
13
|
+
* - content-type: application/grpc+proto
|
|
14
|
+
* - te: trailers
|
|
15
|
+
* - Body: 1-byte compression flag (0 = none) + 4-byte big-endian
|
|
16
|
+
* length + protobuf-encoded ExportLogsServiceRequest
|
|
17
|
+
* - Response status: HTTP/2 trailer `grpc-status` (0 = OK)
|
|
18
|
+
*
|
|
19
|
+
* The protobuf body is encoded by hand via lib/protobuf-encoder.js —
|
|
20
|
+
* no schema parser ships in the bundle, consistent with the framework's
|
|
21
|
+
* vendoring stance. The OTel logs schema is small and stable
|
|
22
|
+
* (https://github.com/open-telemetry/opentelemetry-proto/blob/main/
|
|
23
|
+
* opentelemetry/proto/logs/v1/logs.proto).
|
|
24
|
+
*
|
|
25
|
+
* Severity mapping follows the OTel spec: debug=5 / info=9 / warn=13 /
|
|
26
|
+
* error=17.
|
|
27
|
+
*/
|
|
28
|
+
var http2 = require("node:http2");
|
|
29
|
+
var C = require("./constants");
|
|
30
|
+
var pb = require("./protobuf-encoder");
|
|
31
|
+
var safeUrl = require("./safe-url");
|
|
32
|
+
var { LogStreamError } = require("./framework-error");
|
|
33
|
+
|
|
34
|
+
var _err = LogStreamError.factory;
|
|
35
|
+
|
|
36
|
+
var DEFAULTS = {
|
|
37
|
+
scopeName: "blamejs",
|
|
38
|
+
batchSize: 100,
|
|
39
|
+
maxBatchAgeMs: C.TIME.seconds(5),
|
|
40
|
+
timeoutMs: C.TIME.seconds(30),
|
|
41
|
+
bufferLimit: 10000,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
var SEVERITY = {
|
|
45
|
+
debug: { number: 5, text: "DEBUG" },
|
|
46
|
+
info: { number: 9, text: "INFO" },
|
|
47
|
+
warn: { number: 13, text: "WARN" },
|
|
48
|
+
error: { number: 17, text: "ERROR" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ---- Protobuf message shapes (OTel logs.proto) ----
|
|
52
|
+
//
|
|
53
|
+
// Field numbers match
|
|
54
|
+
// https://github.com/open-telemetry/opentelemetry-proto/blob/main/
|
|
55
|
+
// opentelemetry/proto/{common,resource,logs}/v1/*.proto
|
|
56
|
+
//
|
|
57
|
+
// AnyValue (common.proto) — proto3 oneof; we emit one of:
|
|
58
|
+
// string_value = 1, bool_value = 2, int_value = 3 (int64 varint),
|
|
59
|
+
// double_value = 4, bytes_value = 7
|
|
60
|
+
function _encodeAnyValue(v) {
|
|
61
|
+
if (v == null) return pb.string(1, ""); // string_value=""
|
|
62
|
+
if (typeof v === "string") return pb.string(1, v);
|
|
63
|
+
if (typeof v === "boolean")return pb.bool(2, v);
|
|
64
|
+
if (typeof v === "number") {
|
|
65
|
+
if (Number.isInteger(v) && v >= 0) return pb.uint64(3, v); // int_value (proto3 int64 varint)
|
|
66
|
+
return pb.double(4, v); // double_value
|
|
67
|
+
}
|
|
68
|
+
if (Buffer.isBuffer(v)) return pb.bytes(7, v);
|
|
69
|
+
// Fallback — JSON-stringify objects/arrays into string_value rather
|
|
70
|
+
// than implementing the full ArrayValue / KeyValueList branches.
|
|
71
|
+
// Operators wanting structured nested attributes shape their record
|
|
72
|
+
// up-front into top-level scalars or stringified JSON.
|
|
73
|
+
try { return pb.string(1, JSON.stringify(v)); }
|
|
74
|
+
catch (_e) { return pb.string(1, String(v)); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// KeyValue (common.proto): key=1 (string), value=2 (AnyValue)
|
|
78
|
+
function _encodeKeyValue(key, value) {
|
|
79
|
+
return Buffer.concat([
|
|
80
|
+
pb.string(1, key),
|
|
81
|
+
pb.embeddedMessage(2, _encodeAnyValue(value)),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _encodeAttributes(obj) {
|
|
86
|
+
if (!obj) return [];
|
|
87
|
+
var out = [];
|
|
88
|
+
var keys = Object.keys(obj);
|
|
89
|
+
for (var i = 0; i < keys.length; i++) {
|
|
90
|
+
var k = keys[i];
|
|
91
|
+
var v = obj[k];
|
|
92
|
+
if (v === undefined) continue;
|
|
93
|
+
out.push(_encodeKeyValue(k, v));
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Resource (resource.proto): attributes=1 (repeated KeyValue),
|
|
99
|
+
// dropped_attributes_count=2
|
|
100
|
+
function _encodeResource(attrs) {
|
|
101
|
+
var kvs = _encodeAttributes(attrs);
|
|
102
|
+
if (kvs.length === 0) return Buffer.alloc(0);
|
|
103
|
+
var pieces = kvs.map(function (kvBody) { return pb.embeddedMessage(1, kvBody); });
|
|
104
|
+
return Buffer.concat(pieces);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// InstrumentationScope (common.proto): name=1, version=2
|
|
108
|
+
function _encodeScope(name, version) {
|
|
109
|
+
return Buffer.concat([
|
|
110
|
+
pb.string(1, name || DEFAULTS.scopeName),
|
|
111
|
+
pb.string(2, version || ""),
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// LogRecord (logs.proto):
|
|
116
|
+
// time_unix_nano = 1 (fixed64)
|
|
117
|
+
// severity_number = 2 (enum / varint)
|
|
118
|
+
// severity_text = 3 (string)
|
|
119
|
+
// body = 5 (AnyValue)
|
|
120
|
+
// attributes = 6 (repeated KeyValue)
|
|
121
|
+
// observed_time_unix_nano = 11 (fixed64)
|
|
122
|
+
function _encodeLogRecord(record) {
|
|
123
|
+
var sev = SEVERITY[record.level] || SEVERITY.info;
|
|
124
|
+
var tsMs = record.ts || Date.now();
|
|
125
|
+
// Convert ms to ns. Use BigInt to avoid 53-bit precision loss on
|
|
126
|
+
// operators emitting > year-2255 timestamps. For ms-resolution
|
|
127
|
+
// records the LSB nanos are 0; we still send fixed64.
|
|
128
|
+
var tsNs = BigInt(tsMs) * 1000000n;
|
|
129
|
+
var attrPieces = _encodeAttributes(record.meta).map(function (kvBody) {
|
|
130
|
+
return pb.embeddedMessage(6, kvBody);
|
|
131
|
+
});
|
|
132
|
+
var msg = (record.message != null ? String(record.message) : "");
|
|
133
|
+
return Buffer.concat([
|
|
134
|
+
pb.fixed64(1, tsNs),
|
|
135
|
+
pb.uint32(2, sev.number),
|
|
136
|
+
pb.string(3, sev.text),
|
|
137
|
+
pb.embeddedMessage(5, pb.string(1, msg)), // body.string_value
|
|
138
|
+
Buffer.concat(attrPieces),
|
|
139
|
+
pb.fixed64(11, tsNs),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ScopeLogs (logs.proto): scope=1 (InstrumentationScope),
|
|
144
|
+
// log_records=2 (repeated LogRecord), schema_url=3
|
|
145
|
+
function _encodeScopeLogs(records, scopeName, scopeVersion) {
|
|
146
|
+
var recordPieces = records.map(function (rec) {
|
|
147
|
+
return pb.embeddedMessage(2, _encodeLogRecord(rec));
|
|
148
|
+
});
|
|
149
|
+
return Buffer.concat([
|
|
150
|
+
pb.embeddedMessage(1, _encodeScope(scopeName, scopeVersion)),
|
|
151
|
+
Buffer.concat(recordPieces),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ResourceLogs (logs.proto): resource=1 (Resource),
|
|
156
|
+
// scope_logs=2 (repeated ScopeLogs), schema_url=3
|
|
157
|
+
function _encodeResourceLogs(records, cfg) {
|
|
158
|
+
var resourceBody = _encodeResource(_resourceAttrs(cfg));
|
|
159
|
+
var scopeLogsBody = _encodeScopeLogs(records, cfg.scopeName, cfg.scopeVersion);
|
|
160
|
+
return Buffer.concat([
|
|
161
|
+
pb.embeddedMessage(1, resourceBody),
|
|
162
|
+
pb.embeddedMessage(2, scopeLogsBody),
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ExportLogsServiceRequest (collector logs.proto):
|
|
167
|
+
// resource_logs = 1 (repeated ResourceLogs)
|
|
168
|
+
function _encodeExportRequest(records, cfg) {
|
|
169
|
+
return pb.embeddedMessage(1, _encodeResourceLogs(records, cfg));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _resourceAttrs(cfg) {
|
|
173
|
+
var attrs = Object.assign({}, cfg.resourceAttributes || {});
|
|
174
|
+
if (cfg.serviceName) attrs["service.name"] = cfg.serviceName;
|
|
175
|
+
if (cfg.serviceVersion) attrs["service.version"] = cfg.serviceVersion;
|
|
176
|
+
return attrs;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---- gRPC framing ----
|
|
180
|
+
|
|
181
|
+
function _frame(messageBuf) {
|
|
182
|
+
// 1 byte (compression flag, 0 = uncompressed) + 4 bytes (length, BE)
|
|
183
|
+
// + message bytes.
|
|
184
|
+
var hdr = Buffer.alloc(5);
|
|
185
|
+
hdr[0] = 0;
|
|
186
|
+
hdr.writeUInt32BE(messageBuf.length, 1);
|
|
187
|
+
return Buffer.concat([hdr, messageBuf]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- HTTP/2 client ----
|
|
191
|
+
//
|
|
192
|
+
// One client per sink instance. The OTLP gRPC server keeps the
|
|
193
|
+
// connection alive across many Export calls; we re-create on
|
|
194
|
+
// disconnect.
|
|
195
|
+
function _makeClient(cfg) {
|
|
196
|
+
var url = new URL(cfg.url);
|
|
197
|
+
var authority = url.protocol + "//" + url.host;
|
|
198
|
+
var sessionOpts = {};
|
|
199
|
+
if (cfg.ca) sessionOpts.ca = cfg.ca;
|
|
200
|
+
if (cfg.servername) sessionOpts.servername = cfg.servername;
|
|
201
|
+
if (cfg.allowInsecure) sessionOpts.rejectUnauthorized = false;
|
|
202
|
+
var session = http2.connect(authority, sessionOpts);
|
|
203
|
+
session.on("error", function () { /* surfaced through request err */ });
|
|
204
|
+
if (typeof session.unref === "function") session.unref();
|
|
205
|
+
return session;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _doExport(session, cfg, records) {
|
|
209
|
+
return new Promise(function (resolve, reject) {
|
|
210
|
+
var body = _encodeExportRequest(records, cfg);
|
|
211
|
+
var framed = _frame(body);
|
|
212
|
+
|
|
213
|
+
var headers = Object.assign({
|
|
214
|
+
":method": "POST",
|
|
215
|
+
":path": "/opentelemetry.proto.collector.logs.v1.LogsService/Export",
|
|
216
|
+
"content-type": "application/grpc+proto",
|
|
217
|
+
"te": "trailers",
|
|
218
|
+
"grpc-encoding": "identity",
|
|
219
|
+
"grpc-accept-encoding": "identity",
|
|
220
|
+
}, cfg.headers || {});
|
|
221
|
+
|
|
222
|
+
var req = session.request(headers);
|
|
223
|
+
var resStatus = null;
|
|
224
|
+
var trailers = null;
|
|
225
|
+
var errored = false;
|
|
226
|
+
|
|
227
|
+
var timer = setTimeout(function () {
|
|
228
|
+
errored = true;
|
|
229
|
+
try { req.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
|
|
230
|
+
reject(_err("ETIMEDOUT",
|
|
231
|
+
"otlp-grpc: request timed out after " + cfg.timeoutMs + "ms"));
|
|
232
|
+
}, cfg.timeoutMs);
|
|
233
|
+
|
|
234
|
+
req.on("response", function (h) {
|
|
235
|
+
resStatus = h[":status"];
|
|
236
|
+
});
|
|
237
|
+
req.on("trailers", function (t) { trailers = t; });
|
|
238
|
+
req.on("error", function (e) {
|
|
239
|
+
if (errored) return;
|
|
240
|
+
errored = true;
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
reject(_err("HTTP2_ERROR", "otlp-grpc: " + (e && e.message || String(e))));
|
|
243
|
+
});
|
|
244
|
+
req.on("close", function () {
|
|
245
|
+
if (errored) return;
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
if (resStatus !== 200) {
|
|
248
|
+
return reject(_err("HTTP_ERROR",
|
|
249
|
+
"otlp-grpc: HTTP/2 status " + resStatus));
|
|
250
|
+
}
|
|
251
|
+
// Trailers MUST carry grpc-status. The OTel collector returns
|
|
252
|
+
// grpc-status: 0 on success.
|
|
253
|
+
var grpcStatus = trailers && trailers["grpc-status"];
|
|
254
|
+
var grpcMessage = trailers && trailers["grpc-message"];
|
|
255
|
+
if (grpcStatus === undefined) {
|
|
256
|
+
return reject(_err("HTTP_ERROR",
|
|
257
|
+
"otlp-grpc: response missing grpc-status trailer"));
|
|
258
|
+
}
|
|
259
|
+
if (String(grpcStatus) !== "0") {
|
|
260
|
+
return reject(_err("HTTP_ERROR",
|
|
261
|
+
"otlp-grpc: grpc-status " + grpcStatus +
|
|
262
|
+
(grpcMessage ? " — " + grpcMessage : "")));
|
|
263
|
+
}
|
|
264
|
+
resolve();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
req.end(framed);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---- Public sink factory ----
|
|
272
|
+
|
|
273
|
+
function create(config) {
|
|
274
|
+
if (!config || !config.url) {
|
|
275
|
+
throw _err("BAD_OPT", "log-stream otlp-grpc: { url } is required");
|
|
276
|
+
}
|
|
277
|
+
// Reject http:// without explicit allowInsecure — gRPC endpoints
|
|
278
|
+
// are virtually always TLS in real deployments.
|
|
279
|
+
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
280
|
+
safeUrl.parse(config.url, {
|
|
281
|
+
allowedProtocols: allowedProtocols,
|
|
282
|
+
errorClass: LogStreamError,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
var cfg = Object.assign({}, DEFAULTS, config);
|
|
286
|
+
cfg.batchSize = Number(cfg.batchSize) || DEFAULTS.batchSize;
|
|
287
|
+
cfg.maxBatchAgeMs = Number(cfg.maxBatchAgeMs) || DEFAULTS.maxBatchAgeMs;
|
|
288
|
+
cfg.timeoutMs = Number(cfg.timeoutMs) || DEFAULTS.timeoutMs;
|
|
289
|
+
cfg.bufferLimit = Number(cfg.bufferLimit) || DEFAULTS.bufferLimit;
|
|
290
|
+
|
|
291
|
+
var onDrop = typeof config.onDrop === "function" ? config.onDrop : null;
|
|
292
|
+
function _emitDrop(reason, batch, err) {
|
|
293
|
+
if (!onDrop) return;
|
|
294
|
+
try { onDrop({ reason: reason, batch: batch, error: err || null }); }
|
|
295
|
+
catch (_e) {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var buffer = [];
|
|
299
|
+
var inFlight = false;
|
|
300
|
+
var closed = false;
|
|
301
|
+
var flushTimer = null;
|
|
302
|
+
var session = null;
|
|
303
|
+
var inflightPromise = null;
|
|
304
|
+
|
|
305
|
+
function _ensureSession() {
|
|
306
|
+
if (session && !session.destroyed) return session;
|
|
307
|
+
session = _makeClient(cfg);
|
|
308
|
+
return session;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _scheduleFlush() {
|
|
312
|
+
if (flushTimer || closed) return;
|
|
313
|
+
flushTimer = setTimeout(function () {
|
|
314
|
+
flushTimer = null;
|
|
315
|
+
_flush().catch(function () { /* drained internally */ });
|
|
316
|
+
}, cfg.maxBatchAgeMs);
|
|
317
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function _flush() {
|
|
321
|
+
if (inFlight || buffer.length === 0) return;
|
|
322
|
+
inFlight = true;
|
|
323
|
+
try {
|
|
324
|
+
while (buffer.length > 0 && !closed) {
|
|
325
|
+
var batch = buffer.splice(0, cfg.batchSize);
|
|
326
|
+
try {
|
|
327
|
+
var s = _ensureSession();
|
|
328
|
+
inflightPromise = _doExport(s, cfg, batch);
|
|
329
|
+
await inflightPromise;
|
|
330
|
+
} catch (e) {
|
|
331
|
+
_emitDrop("send-failed", batch, e);
|
|
332
|
+
} finally {
|
|
333
|
+
inflightPromise = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
inFlight = false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function emit(record) {
|
|
342
|
+
if (closed) {
|
|
343
|
+
_emitDrop("sink-closed", [record], null);
|
|
344
|
+
return Promise.resolve({ accepted: false, reason: "closed" });
|
|
345
|
+
}
|
|
346
|
+
if (buffer.length >= cfg.bufferLimit) {
|
|
347
|
+
var dropped = buffer.shift();
|
|
348
|
+
_emitDrop("overflow", [dropped], null);
|
|
349
|
+
}
|
|
350
|
+
buffer.push(record);
|
|
351
|
+
if (buffer.length >= cfg.batchSize) {
|
|
352
|
+
// Drain immediately; don't await — emit is fire-and-forget on the
|
|
353
|
+
// hot path. Errors surface via onDrop.
|
|
354
|
+
_flush().catch(function () {});
|
|
355
|
+
} else {
|
|
356
|
+
_scheduleFlush();
|
|
357
|
+
}
|
|
358
|
+
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function close() {
|
|
362
|
+
closed = true;
|
|
363
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
364
|
+
// Drain buffered records before tearing down the HTTP/2 session.
|
|
365
|
+
if (inflightPromise) { try { await inflightPromise; } catch (_e) {} }
|
|
366
|
+
var pending = buffer.splice(0, buffer.length);
|
|
367
|
+
if (pending.length > 0) {
|
|
368
|
+
try {
|
|
369
|
+
var s = _ensureSession();
|
|
370
|
+
await _doExport(s, cfg, pending);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
_emitDrop("send-failed", pending, e);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Tear down the HTTP/2 session. `session.close()` is the *graceful*
|
|
376
|
+
// close — it waits for in-flight streams to complete on their own
|
|
377
|
+
// and won't free the underlying socket while any stream is still
|
|
378
|
+
// open. By this point we've already awaited `inflightPromise` and
|
|
379
|
+
// run a final `_doExport` for any buffered records, so there's
|
|
380
|
+
// nothing left to drain; `session.destroy()` is the structurally
|
|
381
|
+
// correct call. (Calling only close() left the test fixture's
|
|
382
|
+
// server.close() hanging indefinitely on Linux CI runners — the
|
|
383
|
+
// close() was returning while the underlying TCP socket stayed
|
|
384
|
+
// connected, blocking the server-side close from completing.)
|
|
385
|
+
try {
|
|
386
|
+
if (session) {
|
|
387
|
+
try { session.close(); } catch (_e1) { /* best-effort graceful */ }
|
|
388
|
+
try { session.destroy(); } catch (_e2) { /* socket teardown */ }
|
|
389
|
+
}
|
|
390
|
+
} catch (_e) {}
|
|
391
|
+
session = null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
protocol: "otlp-grpc",
|
|
396
|
+
emit: emit,
|
|
397
|
+
close: close,
|
|
398
|
+
// Test hooks — encode without sending.
|
|
399
|
+
_encodeForTest: function (records) { return _encodeExportRequest(records, cfg); },
|
|
400
|
+
_frameForTest: _frame,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
create: create,
|
|
406
|
+
// Exposed for layer-0 tests that verify the wire encoding without
|
|
407
|
+
// standing up an HTTP/2 server.
|
|
408
|
+
_encodeAnyValue: _encodeAnyValue,
|
|
409
|
+
_encodeKeyValue: _encodeKeyValue,
|
|
410
|
+
_encodeLogRecord: _encodeLogRecord,
|
|
411
|
+
_encodeExportRequest: _encodeExportRequest,
|
|
412
|
+
_frame: _frame,
|
|
413
|
+
};
|
package/lib/log-stream.js
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* Model. Operators with an OTel collector running
|
|
15
15
|
* (k8s, cloud) get standard log forwarding without a
|
|
16
16
|
* vendor-specific adapter.
|
|
17
|
+
* otlp-grpc — same OTel Logs Data Model but over gRPC (HTTP/2 +
|
|
18
|
+
* hand-encoded protobuf). Higher-throughput than the
|
|
19
|
+
* JSON variant; preferred for production deployments
|
|
20
|
+
* pushing >100K logs/s straight to a remote OTel
|
|
21
|
+
* collector. No protobuf parser ships in the bundle —
|
|
22
|
+
* the encoder is the framework's own (lib/protobuf-encoder.js).
|
|
17
23
|
* cloudwatch — AWS CloudWatch Logs (PutLogEvents) over HTTPS with
|
|
18
24
|
* SigV4. Pass { autoCreate: true } to have the framework
|
|
19
25
|
* issue CreateLogGroup + CreateLogStream on first emit
|
|
@@ -49,6 +55,7 @@
|
|
|
49
55
|
var localProto = require("./log-stream-local");
|
|
50
56
|
var webhookProto = require("./log-stream-webhook");
|
|
51
57
|
var otlpProto = require("./log-stream-otlp");
|
|
58
|
+
var otlpGrpcProto = require("./log-stream-otlp-grpc");
|
|
52
59
|
var cloudwatchProto = require("./log-stream-cloudwatch");
|
|
53
60
|
var syslogProto = require("./log-stream-syslog");
|
|
54
61
|
var redactor = require("./redact");
|
|
@@ -63,6 +70,7 @@ var dispatcher = protocolDispatcher.create({
|
|
|
63
70
|
"local": localProto,
|
|
64
71
|
"webhook": webhookProto,
|
|
65
72
|
"otlp": otlpProto,
|
|
73
|
+
"otlp-grpc": otlpGrpcProto,
|
|
66
74
|
"cloudwatch": cloudwatchProto,
|
|
67
75
|
"syslog": syslogProto,
|
|
68
76
|
},
|
package/lib/mail.js
CHANGED
|
@@ -454,8 +454,20 @@ function smtpTransport(opts) {
|
|
|
454
454
|
if (opts.ecdhCurve) tlsOpts.ecdhCurve = opts.ecdhCurve;
|
|
455
455
|
if (opts.ca) tlsOpts.ca = opts.ca;
|
|
456
456
|
|
|
457
|
+
// SNI is only legal for hostnames; IP literals must omit servername
|
|
458
|
+
// (Node's tls.connect throws "Setting the TLS ServerName to an IP
|
|
459
|
+
// address is not permitted" otherwise). Operators with private CAs
|
|
460
|
+
// and an IP-only target pass `opts.servername: "expected-cn.example"`
|
|
461
|
+
// explicitly. Same convention as lib/redis-client.js.
|
|
462
|
+
var host = opts.host;
|
|
463
|
+
var servername = opts.servername;
|
|
464
|
+
if (servername === undefined) {
|
|
465
|
+
servername = (/^\d+\.\d+\.\d+\.\d+$/.test(host) || (host && host.indexOf(":") !== -1))
|
|
466
|
+
? undefined : host;
|
|
467
|
+
}
|
|
468
|
+
|
|
457
469
|
var cfg = {
|
|
458
|
-
host:
|
|
470
|
+
host: host,
|
|
459
471
|
port: port,
|
|
460
472
|
user: opts.user,
|
|
461
473
|
pass: opts.pass,
|
|
@@ -463,6 +475,7 @@ function smtpTransport(opts) {
|
|
|
463
475
|
ehloName: ehloName,
|
|
464
476
|
timeoutMs: timeoutMs,
|
|
465
477
|
tlsOpts: tlsOpts,
|
|
478
|
+
servername: servername,
|
|
466
479
|
dkimSigner: opts.dkimSigner || null,
|
|
467
480
|
};
|
|
468
481
|
|
|
@@ -542,7 +555,8 @@ function _smtpSend(message, cfg) {
|
|
|
542
555
|
|
|
543
556
|
function connect() {
|
|
544
557
|
if (cfg.useImplicitTLS) {
|
|
545
|
-
var tlsConnectOpts = Object.assign({
|
|
558
|
+
var tlsConnectOpts = Object.assign({}, cfg.tlsOpts);
|
|
559
|
+
if (cfg.servername) tlsConnectOpts.servername = cfg.servername;
|
|
546
560
|
attachSocket(tls().connect(cfg.port, cfg.host, tlsConnectOpts));
|
|
547
561
|
} else {
|
|
548
562
|
attachSocket(net().createConnection(cfg.port, cfg.host));
|
|
@@ -562,7 +576,8 @@ function _smtpSend(message, cfg) {
|
|
|
562
576
|
}
|
|
563
577
|
else if (step === 10) {
|
|
564
578
|
if (code !== 220) { fail("starttls-rejected (code " + code + ")"); return; }
|
|
565
|
-
var tlsConnectOpts = Object.assign({ socket: socket
|
|
579
|
+
var tlsConnectOpts = Object.assign({ socket: socket }, cfg.tlsOpts);
|
|
580
|
+
if (cfg.servername) tlsConnectOpts.servername = cfg.servername;
|
|
566
581
|
var tlsSocket = tls().connect(tlsConnectOpts, function () {
|
|
567
582
|
upgradedToTLS = true;
|
|
568
583
|
try { socket.removeAllListeners("data"); } catch (_e) { /* listeners migrate to upgraded socket */ }
|
package/lib/mtls-ca.js
CHANGED
|
@@ -90,6 +90,14 @@ var DEFAULT_PATHS = {
|
|
|
90
90
|
caKey: "ca.key",
|
|
91
91
|
caKeySealed: "ca.key.sealed",
|
|
92
92
|
caCert: "ca.crt",
|
|
93
|
+
// Revocation registry — JSON file under dataDir tracking revoked
|
|
94
|
+
// serial numbers. Operators export this as a CRL via
|
|
95
|
+
// ca.generateCrl() (engine.generateCrl signs the list with the CA
|
|
96
|
+
// key). Persisted as JSON rather than a stored CRL because the
|
|
97
|
+
// signed CRL is a derivative artifact — the registry survives CA
|
|
98
|
+
// rotation, the CRL doesn't.
|
|
99
|
+
revocations: "revocations.json",
|
|
100
|
+
crl: "ca.crl",
|
|
93
101
|
};
|
|
94
102
|
|
|
95
103
|
var VALID_SEAL_MODES = { auto: 1, required: 1, disabled: 1 };
|
|
@@ -100,6 +108,8 @@ function _resolvePaths(dataDir, paths) {
|
|
|
100
108
|
caKey: path.join(dataDir, p.caKey),
|
|
101
109
|
caKeySealed: path.join(dataDir, p.caKeySealed),
|
|
102
110
|
caCert: path.join(dataDir, p.caCert),
|
|
111
|
+
revocations: path.join(dataDir, p.revocations),
|
|
112
|
+
crl: path.join(dataDir, p.crl),
|
|
103
113
|
};
|
|
104
114
|
}
|
|
105
115
|
|
|
@@ -318,6 +328,147 @@ function create(opts) {
|
|
|
318
328
|
return result;
|
|
319
329
|
}
|
|
320
330
|
|
|
331
|
+
// ---- Revocation registry + CRL ----
|
|
332
|
+
|
|
333
|
+
function _loadRevocations() {
|
|
334
|
+
if (!fs.existsSync(paths.revocations)) return { revocations: [] };
|
|
335
|
+
try {
|
|
336
|
+
var json = JSON.parse(fs.readFileSync(paths.revocations, "utf8"));
|
|
337
|
+
if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
|
|
338
|
+
return json;
|
|
339
|
+
} catch (e) {
|
|
340
|
+
throw new MtlsCaError("mtls-ca/revocation-corrupt",
|
|
341
|
+
"could not parse " + paths.revocations + ": " +
|
|
342
|
+
((e && e.message) || String(e)));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _saveRevocations(state) {
|
|
347
|
+
var atomicFile = require("./atomic-file");
|
|
348
|
+
atomicFile.writeSync(paths.revocations,
|
|
349
|
+
JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function _normalizeSerial(s) {
|
|
353
|
+
if (!s || typeof s !== "string") {
|
|
354
|
+
throw new MtlsCaError("mtls-ca/bad-serial",
|
|
355
|
+
"serial number must be a non-empty string");
|
|
356
|
+
}
|
|
357
|
+
// Strip the optional leading `0x` and any common separators
|
|
358
|
+
// (`:` or `-` or whitespace). What remains MUST be hex — otherwise
|
|
359
|
+
// we silently accept gibberish like "xyz-not-hex" (which previously
|
|
360
|
+
// normalised to a single "e" because the strip-non-hex regex left
|
|
361
|
+
// exactly one valid char). Operators pasting an openssl-printed
|
|
362
|
+
// serial use any of: "0xABC123", "AB:C1:23", "AB-C1-23", "abc 123";
|
|
363
|
+
// a typo or non-serial string fails fast instead of registering a
|
|
364
|
+
// phantom revocation row.
|
|
365
|
+
var stripped = s.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
|
|
366
|
+
if (!/^[0-9a-fA-F]+$/.test(stripped)) {
|
|
367
|
+
throw new MtlsCaError("mtls-ca/bad-serial",
|
|
368
|
+
"serial number contains non-hex characters " +
|
|
369
|
+
"(allowed shapes: hex with optional 0x prefix, ':', '-', or whitespace " +
|
|
370
|
+
"as separators): " + JSON.stringify(s));
|
|
371
|
+
}
|
|
372
|
+
return stripped.toLowerCase();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Map operator-friendly reason codes to RFC 5280 numeric codes used
|
|
376
|
+
// by X.509 CRLs. Default "unspecified" (0) when omitted.
|
|
377
|
+
var CRL_REASON_BY_NAME = {
|
|
378
|
+
"unspecified": 0,
|
|
379
|
+
"keyCompromise": 1,
|
|
380
|
+
"key-compromise": 1,
|
|
381
|
+
"caCompromise": 2,
|
|
382
|
+
"ca-compromise": 2,
|
|
383
|
+
"affiliationChanged": 3,
|
|
384
|
+
"superseded": 4,
|
|
385
|
+
"cessationOfOperation": 5,
|
|
386
|
+
"cessation-of-operation": 5,
|
|
387
|
+
"certificateHold": 6,
|
|
388
|
+
"removeFromCRL": 8,
|
|
389
|
+
"privilegeWithdrawn": 9,
|
|
390
|
+
"aACompromise": 10,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
function revoke(serialNumber, opts3) {
|
|
394
|
+
var serial = _normalizeSerial(serialNumber);
|
|
395
|
+
opts3 = opts3 || {};
|
|
396
|
+
var reasonName = opts3.reason || "unspecified";
|
|
397
|
+
var reasonCode = CRL_REASON_BY_NAME[reasonName];
|
|
398
|
+
if (reasonCode === undefined) {
|
|
399
|
+
throw new MtlsCaError("mtls-ca/bad-reason",
|
|
400
|
+
"revoke: unknown reason '" + reasonName + "' (valid: " +
|
|
401
|
+
Object.keys(CRL_REASON_BY_NAME).join(", ") + ")");
|
|
402
|
+
}
|
|
403
|
+
var state = _loadRevocations();
|
|
404
|
+
var existing = state.revocations.find(function (r) {
|
|
405
|
+
return r.serialNumber === serial;
|
|
406
|
+
});
|
|
407
|
+
if (existing) {
|
|
408
|
+
// Idempotent — repeated revoke() of the same serial doesn't
|
|
409
|
+
// shift the revokedAt timestamp.
|
|
410
|
+
return existing;
|
|
411
|
+
}
|
|
412
|
+
var entry = {
|
|
413
|
+
serialNumber: serial,
|
|
414
|
+
reason: reasonName,
|
|
415
|
+
reasonCode: reasonCode,
|
|
416
|
+
revokedAt: Date.now(),
|
|
417
|
+
};
|
|
418
|
+
state.revocations.push(entry);
|
|
419
|
+
_saveRevocations(state);
|
|
420
|
+
return entry;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isRevoked(serialNumber) {
|
|
424
|
+
var serial = _normalizeSerial(serialNumber);
|
|
425
|
+
var state = _loadRevocations();
|
|
426
|
+
return state.revocations.some(function (r) {
|
|
427
|
+
return r.serialNumber === serial;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getRevocations() {
|
|
432
|
+
return _loadRevocations().revocations.slice();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Generate a signed X.509 CRL covering every entry in the registry.
|
|
436
|
+
// RFC 5280 — issuer = CA subject, signed by the CA private key.
|
|
437
|
+
// Operators publish the resulting PEM at a CRL distribution point
|
|
438
|
+
// referenced from issued certs (cert extension support is on the
|
|
439
|
+
// engine roadmap; for now operators set up the URL externally).
|
|
440
|
+
async function generateCrl(opts3) {
|
|
441
|
+
opts3 = opts3 || {};
|
|
442
|
+
if (typeof engine.generateCrl !== "function") {
|
|
443
|
+
throw new MtlsCaError("mtls-ca/engine-no-crl",
|
|
444
|
+
"configured engine does not implement generateCrl(); the bundled " +
|
|
445
|
+
"engine ships in v0.6.45+");
|
|
446
|
+
}
|
|
447
|
+
var ca = await initCA();
|
|
448
|
+
var revocations = _loadRevocations().revocations;
|
|
449
|
+
var nowMs = Date.now();
|
|
450
|
+
var thisUpdate = opts3.thisUpdate || new Date(nowMs);
|
|
451
|
+
var nextUpdate = opts3.nextUpdate ||
|
|
452
|
+
new Date(nowMs + 7 * 24 * 60 * 60 * 1000); // 7d default
|
|
453
|
+
var crlPem = await engine.generateCrl({
|
|
454
|
+
caCertPem: ca.caCertPem,
|
|
455
|
+
caKeyPem: ca.caKeyPem,
|
|
456
|
+
revocations: revocations,
|
|
457
|
+
thisUpdate: thisUpdate,
|
|
458
|
+
nextUpdate: nextUpdate,
|
|
459
|
+
});
|
|
460
|
+
if (typeof crlPem !== "string" || crlPem.length === 0) {
|
|
461
|
+
throw new MtlsCaError("mtls-ca/bad-engine-output",
|
|
462
|
+
"engine.generateCrl must return a non-empty PEM string");
|
|
463
|
+
}
|
|
464
|
+
if (opts3.persist !== false) {
|
|
465
|
+
var atomicFile = require("./atomic-file");
|
|
466
|
+
atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
|
|
467
|
+
}
|
|
468
|
+
return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,
|
|
469
|
+
entryCount: revocations.length, path: paths.crl };
|
|
470
|
+
}
|
|
471
|
+
|
|
321
472
|
return {
|
|
322
473
|
exists: exists,
|
|
323
474
|
keyExists: keyExists,
|
|
@@ -328,6 +479,10 @@ function create(opts) {
|
|
|
328
479
|
initCA: initCA,
|
|
329
480
|
generateClientCert: generateClientCert,
|
|
330
481
|
generateClientP12: generateClientP12,
|
|
482
|
+
revoke: revoke,
|
|
483
|
+
isRevoked: isRevoked,
|
|
484
|
+
getRevocations: getRevocations,
|
|
485
|
+
generateCrl: generateCrl,
|
|
331
486
|
paths: paths,
|
|
332
487
|
generation: generation,
|
|
333
488
|
caKeySealedMode: caKeySealedMode,
|