@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/tracing.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* tracing — OpenTelemetry seam without an OTel runtime dependency.
|
|
4
|
+
*
|
|
5
|
+
* The framework doesn't bundle the OTel SDK — operators install
|
|
6
|
+
* `@opentelemetry/api` (and an exporter) themselves when they want
|
|
7
|
+
* tracing. This module:
|
|
8
|
+
*
|
|
9
|
+
* - Detects if @opentelemetry/api is installed (try/catch require,
|
|
10
|
+
* cached). When it's there, every framework span call routes into
|
|
11
|
+
* OTel's real tracer and shows up in the operator's exporter
|
|
12
|
+
* (Jaeger, Zipkin, OTLP, console, whatever they wired).
|
|
13
|
+
* - When OTel ISN'T installed, every call is a pass-through. The
|
|
14
|
+
* wrapped function still executes, return values still propagate,
|
|
15
|
+
* thrown errors still escape — but no span is created and no
|
|
16
|
+
* overhead is paid beyond one cached lookup.
|
|
17
|
+
*
|
|
18
|
+
* Public API:
|
|
19
|
+
*
|
|
20
|
+
* var t = b.tracing.create({
|
|
21
|
+
* instrumentationName: "blamejs",
|
|
22
|
+
* instrumentationVersion: "1.0.0",
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Wrap async work in a span. Returns whatever fn returns.
|
|
26
|
+
* var result = await t.span("my-op", async function (span) {
|
|
27
|
+
* span.setAttribute("user_id", "abc");
|
|
28
|
+
* span.addEvent("cache-miss");
|
|
29
|
+
* return await doWork();
|
|
30
|
+
* }, { kind: "internal", attributes: { route: "/users" } });
|
|
31
|
+
*
|
|
32
|
+
* // Sync variant.
|
|
33
|
+
* var x = t.spanSync("compute", function (span) { ... return v; });
|
|
34
|
+
*
|
|
35
|
+
* // Read / write the current active span.
|
|
36
|
+
* t.currentSpan(); // null when no active span
|
|
37
|
+
* t.setAttributes({ user_id: "abc" }); // sets on current
|
|
38
|
+
* t.recordException(err); // records on current
|
|
39
|
+
*
|
|
40
|
+
* // HTTP propagation. contextHeaders() returns headers to add to
|
|
41
|
+
* // outbound requests (W3C `traceparent`); extractContext(headers)
|
|
42
|
+
* // pulls a parent context from inbound headers.
|
|
43
|
+
* var headers = t.contextHeaders();
|
|
44
|
+
* var parentCtx = t.extractContext(req.headers);
|
|
45
|
+
*
|
|
46
|
+
* // Auto-span request middleware — wraps each handler in a span
|
|
47
|
+
* // named after method + route pattern.
|
|
48
|
+
* router.use(t.requestMiddleware());
|
|
49
|
+
*
|
|
50
|
+
* // Framework-internal hot-path tap — wraps fn in a span named
|
|
51
|
+
* // `name` if a registry is active; pass-through otherwise. Like
|
|
52
|
+
* // metrics.tap() but for tracing instead of counting.
|
|
53
|
+
* b.tracing.tap("audit.record", attributes, fn);
|
|
54
|
+
*
|
|
55
|
+
* Even WITHOUT @opentelemetry/api installed:
|
|
56
|
+
* - contextHeaders() / extractContext() still parse and emit the
|
|
57
|
+
* W3C traceparent format. So a framework process without OTel
|
|
58
|
+
* can still propagate trace IDs through logs and HTTP for
|
|
59
|
+
* correlation, even without span telemetry. Operators get the
|
|
60
|
+
* "trace ID per request" plumbing as a free baseline.
|
|
61
|
+
* - currentSpan() returns a minimal pass-through "span" object so
|
|
62
|
+
* operator code can call setAttribute / addEvent / recordException
|
|
63
|
+
* unconditionally — they're no-ops without OTel.
|
|
64
|
+
*
|
|
65
|
+
* Why no @otel runtime dep:
|
|
66
|
+
* - The framework keeps zero npm runtime deps. Apps that want tracing
|
|
67
|
+
* install OTel themselves; apps that don't pay nothing.
|
|
68
|
+
* - The OTel API is unstable enough that pinning a vendored version
|
|
69
|
+
* would create more churn than it saves.
|
|
70
|
+
*
|
|
71
|
+
* Out of scope (with structural reasons):
|
|
72
|
+
* - Vendoring the SDK: see above.
|
|
73
|
+
* - Sampling decisions: OTel handles this when wired; without OTel
|
|
74
|
+
* there's nothing to sample.
|
|
75
|
+
* - Exporter integration: belongs to the OTel SDK, not the framework.
|
|
76
|
+
* - Custom propagators: framework ships W3C traceparent only. OTel
|
|
77
|
+
* adds others (b3, jaeger) when wired by the operator.
|
|
78
|
+
* - Async-context propagation across setTimeout / setImmediate:
|
|
79
|
+
* OTel's NodeSDK auto-instrumentation handles this when installed;
|
|
80
|
+
* without OTel, framework code uses fn-passing rather than
|
|
81
|
+
* async-context, which is fine for the surfaces we instrument.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
var nodeCrypto = require("node:crypto");
|
|
85
|
+
var validateOpts = require("./validate-opts");
|
|
86
|
+
var { defineClass } = require("./framework-error");
|
|
87
|
+
var { resolveRoute, captureResponseStatus } = require("./request-helpers");
|
|
88
|
+
|
|
89
|
+
var TracingError = defineClass("TracingError", { alwaysPermanent: true });
|
|
90
|
+
|
|
91
|
+
// ---- OTel API detection (cached) ----
|
|
92
|
+
//
|
|
93
|
+
// Tri-state: undefined=not yet checked, null=not available, object=available.
|
|
94
|
+
// We cache the lookup so the per-call cost is one === comparison after
|
|
95
|
+
// the first call.
|
|
96
|
+
|
|
97
|
+
var _otel = undefined;
|
|
98
|
+
|
|
99
|
+
function _getOtel() {
|
|
100
|
+
if (_otel !== undefined) return _otel;
|
|
101
|
+
try { _otel = require("@opentelemetry/api"); }
|
|
102
|
+
catch (_e) { _otel = null; }
|
|
103
|
+
return _otel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Test seam — letting tests force the OTel resolution. Pass `null` to
|
|
107
|
+
// simulate "not installed" or an object to simulate a vendored OTel API.
|
|
108
|
+
function _setOtelForTest(value) { _otel = value === undefined ? undefined : value; }
|
|
109
|
+
|
|
110
|
+
// ---- W3C traceparent parsing ----
|
|
111
|
+
//
|
|
112
|
+
// Format: 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>
|
|
113
|
+
// Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
|
114
|
+
//
|
|
115
|
+
// We accept only version 00 (the only released W3C version). Future
|
|
116
|
+
// versions are technically forward-compatible but would extend the
|
|
117
|
+
// suffix; we ignore unrecognized versions to be safe.
|
|
118
|
+
|
|
119
|
+
var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
120
|
+
// Special-case all-zeros: invalid per spec but malicious clients send them.
|
|
121
|
+
var ZERO_TRACE_ID = "0".repeat(32);
|
|
122
|
+
var ZERO_SPAN_ID = "0".repeat(16);
|
|
123
|
+
|
|
124
|
+
function _parseTraceparent(value) {
|
|
125
|
+
if (typeof value !== "string") return null;
|
|
126
|
+
var m = value.match(TRACEPARENT_RE);
|
|
127
|
+
if (!m) return null;
|
|
128
|
+
if (m[1] === ZERO_TRACE_ID || m[2] === ZERO_SPAN_ID) return null;
|
|
129
|
+
return { traceId: m[1], spanId: m[2], flags: m[3] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _formatTraceparent(traceId, spanId, flags) {
|
|
133
|
+
return "00-" + traceId + "-" + spanId + "-" + (flags || "01");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _newTraceId() {
|
|
137
|
+
return nodeCrypto.randomBytes(16).toString("hex");
|
|
138
|
+
}
|
|
139
|
+
function _newSpanId() {
|
|
140
|
+
return nodeCrypto.randomBytes(8).toString("hex");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- Pass-through span (used when OTel isn't installed) ----
|
|
144
|
+
//
|
|
145
|
+
// Operator code does span.setAttribute() / span.addEvent() /
|
|
146
|
+
// span.recordException() unconditionally. When OTel is absent these
|
|
147
|
+
// calls go to a minimal stub that no-ops but keeps the API shape
|
|
148
|
+
// compatible. Trace ID is still tracked so log correlation works.
|
|
149
|
+
|
|
150
|
+
function _passthroughSpan(traceId, spanId, parentSpanId) {
|
|
151
|
+
return {
|
|
152
|
+
spanContext: function () {
|
|
153
|
+
return { traceId: traceId, spanId: spanId, traceFlags: 1, isRemote: false };
|
|
154
|
+
},
|
|
155
|
+
setAttribute: function () { return this; },
|
|
156
|
+
setAttributes: function () { return this; },
|
|
157
|
+
addEvent: function () { return this; },
|
|
158
|
+
recordException: function () { return this; },
|
|
159
|
+
setStatus: function () { return this; },
|
|
160
|
+
updateName: function () { return this; },
|
|
161
|
+
end: function () { },
|
|
162
|
+
// Internal — used by the registry to thread parent context.
|
|
163
|
+
_isPassthrough: true,
|
|
164
|
+
_parentSpanId: parentSpanId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- Pass-through tracer (used when OTel isn't installed) ----
|
|
169
|
+
//
|
|
170
|
+
// Implements just enough of the OTel Tracer interface to drive the
|
|
171
|
+
// framework's span() / spanSync() helpers. Real OTel takes over when
|
|
172
|
+
// installed; the wrapper code below doesn't change.
|
|
173
|
+
|
|
174
|
+
function _passthroughTracer() {
|
|
175
|
+
// Active-span stack. Single-threaded JS lets us use a process-level
|
|
176
|
+
// stack rather than async-context. Pure synchronous span lifetimes
|
|
177
|
+
// are correct; async work that spans suspension points won't have
|
|
178
|
+
// currentSpan() right inside continuations — but the operator's fn
|
|
179
|
+
// still receives the span as its first arg, so attribute/event
|
|
180
|
+
// setting via that reference always works.
|
|
181
|
+
var stack = [];
|
|
182
|
+
return {
|
|
183
|
+
startSpan: function (_name, _opts) {
|
|
184
|
+
var traceId = stack.length > 0 ? stack[stack.length - 1].spanContext().traceId : _newTraceId();
|
|
185
|
+
var span = _passthroughSpan(traceId, _newSpanId(),
|
|
186
|
+
stack.length > 0 ? stack[stack.length - 1].spanContext().spanId : null);
|
|
187
|
+
return span;
|
|
188
|
+
},
|
|
189
|
+
_push: function (span) { stack.push(span); },
|
|
190
|
+
_pop: function () { return stack.pop(); },
|
|
191
|
+
_peek: function () { return stack.length > 0 ? stack[stack.length - 1] : null; },
|
|
192
|
+
_isPassthrough: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- Registry factory ----
|
|
197
|
+
|
|
198
|
+
function create(opts) {
|
|
199
|
+
opts = opts || {};
|
|
200
|
+
validateOpts(opts, [
|
|
201
|
+
"instrumentationName", "instrumentationVersion",
|
|
202
|
+
], "b.tracing");
|
|
203
|
+
var instrumentationName = opts.instrumentationName || "blamejs";
|
|
204
|
+
var instrumentationVersion = opts.instrumentationVersion || "0.0.0";
|
|
205
|
+
|
|
206
|
+
// Resolve the tracer once. If @opentelemetry/api is installed, this
|
|
207
|
+
// is a real Tracer; otherwise a passthrough. Cached per-registry so
|
|
208
|
+
// the passthrough stack persists across span() and currentSpan() calls
|
|
209
|
+
// (otherwise each call would see a fresh empty stack).
|
|
210
|
+
var _cachedPassthrough = null;
|
|
211
|
+
function _tracer() {
|
|
212
|
+
var otel = _getOtel();
|
|
213
|
+
if (otel) return otel.trace.getTracer(instrumentationName, instrumentationVersion);
|
|
214
|
+
if (!_cachedPassthrough) _cachedPassthrough = _passthroughTracer();
|
|
215
|
+
return _cachedPassthrough;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _isReal() { return _getOtel() !== null; }
|
|
219
|
+
|
|
220
|
+
// Wrap fn in a span. The span is automatically end()'d on return /
|
|
221
|
+
// throw / promise rejection. exception is recorded on throw and
|
|
222
|
+
// span status set to ERROR.
|
|
223
|
+
function span(name, fn, sopts) {
|
|
224
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
225
|
+
throw new TracingError("tracing/bad-name",
|
|
226
|
+
"span name must be a non-empty string");
|
|
227
|
+
}
|
|
228
|
+
if (typeof fn !== "function") {
|
|
229
|
+
throw new TracingError("tracing/bad-fn",
|
|
230
|
+
"span body must be a function");
|
|
231
|
+
}
|
|
232
|
+
sopts = sopts || {};
|
|
233
|
+
var tracer = _tracer();
|
|
234
|
+
var otel = _getOtel();
|
|
235
|
+
|
|
236
|
+
// Real OTel path — use context.with so async-context propagation
|
|
237
|
+
// works automatically once the SDK is wired.
|
|
238
|
+
if (otel) {
|
|
239
|
+
var spanInst = tracer.startSpan(name, {
|
|
240
|
+
kind: _kindFromString(otel, sopts.kind),
|
|
241
|
+
attributes: sopts.attributes,
|
|
242
|
+
});
|
|
243
|
+
// Run fn inside the span's context so child spans nest.
|
|
244
|
+
var ctx = otel.trace.setSpan(otel.context.active(), spanInst);
|
|
245
|
+
return otel.context.with(ctx, function () {
|
|
246
|
+
try {
|
|
247
|
+
var ret = fn(spanInst);
|
|
248
|
+
if (ret && typeof ret.then === "function") {
|
|
249
|
+
return ret.then(
|
|
250
|
+
function (v) { spanInst.end(); return v; },
|
|
251
|
+
function (e) {
|
|
252
|
+
spanInst.recordException(e);
|
|
253
|
+
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
254
|
+
spanInst.end();
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
spanInst.end();
|
|
260
|
+
return ret;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
spanInst.recordException(e);
|
|
263
|
+
spanInst.setStatus({ code: 2, message: (e && e.message) || String(e) });
|
|
264
|
+
spanInst.end();
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Pass-through path — still wrap so operator code that calls
|
|
271
|
+
// span.setAttribute / addEvent / recordException works, just
|
|
272
|
+
// without telemetry output.
|
|
273
|
+
var pSpan = tracer.startSpan(name, sopts);
|
|
274
|
+
if (sopts.attributes) pSpan.setAttributes(sopts.attributes);
|
|
275
|
+
tracer._push(pSpan);
|
|
276
|
+
var done = false;
|
|
277
|
+
function _finish() { if (!done) { done = true; tracer._pop(); pSpan.end(); } }
|
|
278
|
+
try {
|
|
279
|
+
var res = fn(pSpan);
|
|
280
|
+
if (res && typeof res.then === "function") {
|
|
281
|
+
return res.then(
|
|
282
|
+
function (v) { _finish(); return v; },
|
|
283
|
+
function (e) { _finish(); throw e; }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
_finish();
|
|
287
|
+
return res;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
_finish();
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function spanSync(name, fn, sopts) {
|
|
295
|
+
if (typeof fn !== "function") {
|
|
296
|
+
throw new TracingError("tracing/bad-fn", "spanSync body must be a function");
|
|
297
|
+
}
|
|
298
|
+
// Same shape as span(); fn must return a non-Promise. We don't
|
|
299
|
+
// enforce that, but we don't await either — async fn used here
|
|
300
|
+
// ends the span before resolution.
|
|
301
|
+
return span(name, fn, sopts);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function currentSpan() {
|
|
305
|
+
var otel = _getOtel();
|
|
306
|
+
if (otel) {
|
|
307
|
+
var s = otel.trace.getActiveSpan();
|
|
308
|
+
return s || null;
|
|
309
|
+
}
|
|
310
|
+
var t = _tracer();
|
|
311
|
+
return t._peek();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setAttributes(attrs) {
|
|
315
|
+
var s = currentSpan();
|
|
316
|
+
if (s) s.setAttributes(attrs);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function recordException(err) {
|
|
320
|
+
var s = currentSpan();
|
|
321
|
+
if (s) {
|
|
322
|
+
s.recordException(err);
|
|
323
|
+
s.setStatus({ code: 2, message: (err && err.message) || String(err) });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- W3C propagation (works with or without OTel) ----
|
|
328
|
+
|
|
329
|
+
function contextHeaders() {
|
|
330
|
+
var s = currentSpan();
|
|
331
|
+
if (!s) return {};
|
|
332
|
+
var sc = s.spanContext ? s.spanContext() : null;
|
|
333
|
+
if (!sc || !sc.traceId || sc.traceId === ZERO_TRACE_ID) return {};
|
|
334
|
+
return {
|
|
335
|
+
traceparent: _formatTraceparent(sc.traceId, sc.spanId,
|
|
336
|
+
(sc.traceFlags === undefined ? 1 : sc.traceFlags).toString(16).padStart(2, "0")),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Parse traceparent from incoming headers. With OTel installed we
|
|
341
|
+
// use its propagation API for correctness across propagator types
|
|
342
|
+
// an operator might have configured; without OTel we fall back to
|
|
343
|
+
// our own parser which always understands W3C.
|
|
344
|
+
function extractContext(headers) {
|
|
345
|
+
if (!headers || typeof headers !== "object") return null;
|
|
346
|
+
var raw = headers.traceparent || headers["Traceparent"] || headers["TRACEPARENT"];
|
|
347
|
+
var parsed = _parseTraceparent(raw);
|
|
348
|
+
if (!parsed) return null;
|
|
349
|
+
return parsed;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- request middleware ----
|
|
353
|
+
|
|
354
|
+
function requestMiddleware() {
|
|
355
|
+
return function tracingMiddleware(req, res, next) {
|
|
356
|
+
// Span starts BEFORE the router populates req.routePattern, so
|
|
357
|
+
// initial name+http.route come from the URL fallback. We promote
|
|
358
|
+
// both to the template form at res.end if the matcher set one.
|
|
359
|
+
var initialRoute = resolveRoute(req);
|
|
360
|
+
var spanName = "HTTP " + (req.method || "GET") + " " + initialRoute;
|
|
361
|
+
var attrs = {
|
|
362
|
+
"http.method": req.method || "GET",
|
|
363
|
+
"http.route": initialRoute,
|
|
364
|
+
"http.url": req.url || "",
|
|
365
|
+
};
|
|
366
|
+
var parent = extractContext(req.headers);
|
|
367
|
+
if (parent) {
|
|
368
|
+
attrs["traceparent.parent"] = parent.traceId + "-" + parent.spanId;
|
|
369
|
+
}
|
|
370
|
+
span(spanName, function (s) {
|
|
371
|
+
req.span = s;
|
|
372
|
+
captureResponseStatus(res, function (status) {
|
|
373
|
+
try {
|
|
374
|
+
s.setAttribute("http.status_code", status);
|
|
375
|
+
// Promote to route template if the router resolved one.
|
|
376
|
+
var finalRoute = resolveRoute(req);
|
|
377
|
+
if (finalRoute !== initialRoute) {
|
|
378
|
+
s.setAttribute("http.route", finalRoute);
|
|
379
|
+
if (typeof s.updateName === "function") {
|
|
380
|
+
s.updateName("HTTP " + (req.method || "GET") + " " + finalRoute);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (_e) { /* span attr write must not break the response */ }
|
|
384
|
+
});
|
|
385
|
+
return next();
|
|
386
|
+
}, { attributes: attrs }).catch(function () {
|
|
387
|
+
// Span error already recorded by span() wrapper; the next()
|
|
388
|
+
// chain handles request-level error handling separately.
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---- framework auto-tap ----
|
|
394
|
+
//
|
|
395
|
+
// Like metrics.tap, the tracing tap routes framework hot-path calls
|
|
396
|
+
// into spans when this registry is the active one.
|
|
397
|
+
|
|
398
|
+
var _activeOnTap = null;
|
|
399
|
+
|
|
400
|
+
function tap(name, attributes, fn) {
|
|
401
|
+
if (typeof attributes === "function") {
|
|
402
|
+
fn = attributes; attributes = null;
|
|
403
|
+
}
|
|
404
|
+
if (typeof fn !== "function") {
|
|
405
|
+
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
406
|
+
}
|
|
407
|
+
if (_activeOnTap !== this && registry._isActive !== true) {
|
|
408
|
+
// No active registry — execute fn directly. Caller never knows.
|
|
409
|
+
return fn(currentSpan());
|
|
410
|
+
}
|
|
411
|
+
return span(name, fn, { attributes: attributes });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// OTel SpanKind enum values (from @opentelemetry/api). When OTel
|
|
415
|
+
// isn't installed, kind is just an attribute string.
|
|
416
|
+
function _kindFromString(otel, kindStr) {
|
|
417
|
+
if (!otel || !otel.SpanKind) return undefined;
|
|
418
|
+
if (kindStr === "server") return otel.SpanKind.SERVER;
|
|
419
|
+
if (kindStr === "client") return otel.SpanKind.CLIENT;
|
|
420
|
+
if (kindStr === "producer") return otel.SpanKind.PRODUCER;
|
|
421
|
+
if (kindStr === "consumer") return otel.SpanKind.CONSUMER;
|
|
422
|
+
return otel.SpanKind.INTERNAL;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
var registry = {
|
|
426
|
+
span: span,
|
|
427
|
+
spanSync: spanSync,
|
|
428
|
+
currentSpan: currentSpan,
|
|
429
|
+
setAttributes: setAttributes,
|
|
430
|
+
recordException: recordException,
|
|
431
|
+
contextHeaders: contextHeaders,
|
|
432
|
+
extractContext: extractContext,
|
|
433
|
+
requestMiddleware: requestMiddleware,
|
|
434
|
+
tap: tap,
|
|
435
|
+
isReal: _isReal,
|
|
436
|
+
_isActive: true,
|
|
437
|
+
deactivate: function () {
|
|
438
|
+
registry._isActive = false;
|
|
439
|
+
if (_globalRegistry === registry) _globalRegistry = null;
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
_globalRegistry = registry;
|
|
443
|
+
return registry;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- Global tap stub for framework modules ----
|
|
447
|
+
//
|
|
448
|
+
// Same pattern as metrics.tap: framework modules call b.tracing.tap()
|
|
449
|
+
// at hot paths. Without an active registry the call is a pass-through
|
|
450
|
+
// that just executes fn. With one, it routes into span().
|
|
451
|
+
|
|
452
|
+
var _globalRegistry = null;
|
|
453
|
+
|
|
454
|
+
function tap(name, attributes, fn) {
|
|
455
|
+
if (typeof attributes === "function") {
|
|
456
|
+
fn = attributes; attributes = null;
|
|
457
|
+
}
|
|
458
|
+
if (typeof fn !== "function") {
|
|
459
|
+
throw new TracingError("tracing/bad-fn", "tap fn must be a function");
|
|
460
|
+
}
|
|
461
|
+
if (_globalRegistry === null || _globalRegistry._isActive !== true) {
|
|
462
|
+
return fn(null);
|
|
463
|
+
}
|
|
464
|
+
return _globalRegistry.span(name, fn, { attributes: attributes });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function _resetForTest() {
|
|
468
|
+
_globalRegistry = null;
|
|
469
|
+
_setOtelForTest(undefined);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = {
|
|
473
|
+
create: create,
|
|
474
|
+
tap: tap,
|
|
475
|
+
TracingError: TracingError,
|
|
476
|
+
_setOtelForTest: _setOtelForTest,
|
|
477
|
+
_resetForTest: _resetForTest,
|
|
478
|
+
// Internal helpers exposed for tests
|
|
479
|
+
_parseTraceparent: _parseTraceparent,
|
|
480
|
+
_formatTraceparent: _formatTraceparent,
|
|
481
|
+
_newTraceId: _newTraceId,
|
|
482
|
+
_newSpanId: _newSpanId,
|
|
483
|
+
TRACEPARENT_RE: TRACEPARENT_RE,
|
|
484
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* validate-opts — shared Tier-A helper for primitive create() functions.
|
|
4
|
+
*
|
|
5
|
+
* Catches silent operator typos: `cors({ allowedOrigins: [] })` (wrong
|
|
6
|
+
* key name) currently looks like config but does nothing — the primitive
|
|
7
|
+
* sees opts.origins as undefined and falls back to defaults. With this
|
|
8
|
+
* helper the create() throws at boot with a clear message instead of
|
|
9
|
+
* the typo silently weakening the operator's setup.
|
|
10
|
+
*
|
|
11
|
+
* var validateOpts = require("./validate-opts");
|
|
12
|
+
*
|
|
13
|
+
* function create(opts) {
|
|
14
|
+
* opts = opts || {};
|
|
15
|
+
* validateOpts(opts, [
|
|
16
|
+
* "origins", "siteOrigin", "methods", "headers",
|
|
17
|
+
* "exposeHeaders", "credentials", "maxAgeSeconds", "refuseUnknown",
|
|
18
|
+
* ], "middleware.cors");
|
|
19
|
+
* ...
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* The exported `optional(...)` form lets a primitive accept a
|
|
23
|
+
* sparsely-populated opts object and only validate keys that ARE present.
|
|
24
|
+
*
|
|
25
|
+
* Throws a plain Error with a code-shaped message — primitives that want
|
|
26
|
+
* a typed error wrap the call.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function _format(primitive, unknownKey, allowedKeys) {
|
|
30
|
+
return primitive + ": unknown option '" + unknownKey + "'. " +
|
|
31
|
+
"Allowed keys: " + allowedKeys.slice().sort().join(", ") + ".";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function check(opts, allowedKeys, primitive) {
|
|
35
|
+
if (opts == null) return;
|
|
36
|
+
if (typeof opts !== "object") {
|
|
37
|
+
throw new Error(primitive + ": opts must be an object (got " + typeof opts + ")");
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(allowedKeys) || allowedKeys.length === 0) {
|
|
40
|
+
throw new Error("validate-opts: allowedKeys must be a non-empty array");
|
|
41
|
+
}
|
|
42
|
+
if (typeof primitive !== "string" || primitive.length === 0) {
|
|
43
|
+
throw new Error("validate-opts: primitive name must be a non-empty string");
|
|
44
|
+
}
|
|
45
|
+
var allowSet = Object.create(null);
|
|
46
|
+
for (var i = 0; i < allowedKeys.length; i++) allowSet[allowedKeys[i]] = true;
|
|
47
|
+
var keys = Object.keys(opts);
|
|
48
|
+
for (var j = 0; j < keys.length; j++) {
|
|
49
|
+
if (!allowSet[keys[j]]) {
|
|
50
|
+
throw new Error(_format(primitive, keys[j], allowedKeys));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = check;
|
|
56
|
+
module.exports.check = check;
|