@blamejs/core 0.8.43 → 0.8.49
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 +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/observability.js
CHANGED
|
@@ -1,58 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.observability
|
|
4
|
+
* @nav Observability
|
|
5
|
+
* @title Observability
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Combined metrics + tracing tap surface — every framework hot
|
|
9
|
+
* path uses this one primitive to emit both a span and a counter
|
|
10
|
+
* bump in one call, with redact-aware metadata and breadcrumb
|
|
11
|
+
* integration into the audit chain.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Behavior:
|
|
18
|
-
* - tracing.tap wraps fn in a span (spec 9.8). Pass-through is
|
|
19
|
-
* fn(null) when no tracing registry is active — zero overhead.
|
|
20
|
-
* - After fn settles (either branch), metrics.tap fires once with
|
|
21
|
-
* the same name + the same attrs reused as labels. Existing
|
|
22
|
-
* metrics _tapHandler dispatches still work unchanged because
|
|
23
|
-
* the labels are the same shape modules previously passed.
|
|
24
|
-
* - If fn throws (sync) or rejects (async), metrics still fire
|
|
25
|
-
* before the throw propagates. Operators get the counter bump
|
|
26
|
-
* even on the failure path — the existing pattern across audit /
|
|
27
|
-
* vault / queue did the same.
|
|
28
|
-
*
|
|
29
|
-
* Why combine: every framework module that wanted both a span AND a
|
|
30
|
-
* counter previously wrote nested tap wrappers + try/catch. Centralizing
|
|
31
|
-
* keeps the call sites readable, eliminates boot-order drift each
|
|
32
|
-
* module had to reason about, and lets us change tap semantics
|
|
33
|
-
* (e.g. add a third sink) in one place.
|
|
13
|
+
* `tap(name, attrs, fn)` wraps `fn` in a tracing span (via
|
|
14
|
+
* `b.tracing.tap`) and bumps a metrics counter named `name` (via
|
|
15
|
+
* `b.metrics.tap`) when the function settles, on both the success
|
|
16
|
+
* and failure branches. `event(name, value, labels)` is the
|
|
17
|
+
* fire-and-forget shape — fires the counter only, no span — and
|
|
18
|
+
* `safeEvent` wraps it in a try/catch so per-request hot paths
|
|
19
|
+
* can't crash the request that triggered them when the metrics
|
|
20
|
+
* registry has a misconfigured counter or label name.
|
|
34
21
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
22
|
+
* `timed(name, fn, labels)` measures wall-clock duration of an
|
|
23
|
+
* operation and emits a counter event with `outcome: "ok"` /
|
|
24
|
+
* `"fail"` plus `duration_ms` in the labels — the standard pattern
|
|
25
|
+
* for per-call SLO tracking. `SEMCONV` carries the OTel
|
|
26
|
+
* semantic-convention attribute names (1.27+ stable namespace) so
|
|
27
|
+
* operators wiring the framework's tap into an OTel SDK don't
|
|
28
|
+
* maintain an aliasing table.
|
|
39
29
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
30
|
+
* `traceContext.parse` / `traceContext.build` parse and emit the
|
|
31
|
+
* W3C `traceparent` header per RFC; `traceContext.parseTracestate`
|
|
32
|
+
* / `traceContext.buildTracestate` cover the `tracestate` companion
|
|
33
|
+
* header (32-entry / 512-char W3C cap). `baggage.parse` /
|
|
34
|
+
* `baggage.build` cover the W3C Baggage header for cross-service
|
|
35
|
+
* user context (tenant / region / experiment).
|
|
44
36
|
*
|
|
45
|
-
*
|
|
37
|
+
* The drop-silent contract is intentional — observability runs in
|
|
38
|
+
* request hot paths where throwing on a misnamed metric would
|
|
39
|
+
* crash the request that triggered the emit. Bad input on
|
|
40
|
+
* `event` / `safeEvent` is dropped silently; bad input on `tap`
|
|
41
|
+
* throws at boot-time call sites where operators can fix typos
|
|
42
|
+
* before they corrupt the span tree AND the metrics route at the
|
|
43
|
+
* same time.
|
|
46
44
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* name. Convention: dotted lowercase ("audit.record", "queue.enqueue").
|
|
50
|
-
* attrs: object | null — passed verbatim to tracing.tap as span
|
|
51
|
-
* attributes AND to metrics.tap as labels. Modules previously
|
|
52
|
-
* passing two slightly-different objects to the two sinks should
|
|
53
|
-
* pass one unified shape.
|
|
54
|
-
* fn: function — sync or async. Return propagates; throws propagate
|
|
55
|
-
* after metrics fire.
|
|
45
|
+
* @card
|
|
46
|
+
* Combined metrics + tracing tap surface — every framework hot path uses this one primitive to emit both a span and a counter bump in one call, with redact-aware metadata and breadcrumb integration into the audit chain.
|
|
56
47
|
*/
|
|
57
48
|
var C = require("./constants");
|
|
58
49
|
var lazyRequire = require("./lazy-require");
|
|
@@ -86,6 +77,27 @@ function _safeMetricsTap(name, value, labels) {
|
|
|
86
77
|
//
|
|
87
78
|
// The handler signature mirrors metrics.tap: (name, value, labels).
|
|
88
79
|
// Pass null to remove the previously-installed handler.
|
|
80
|
+
/**
|
|
81
|
+
* @primitive b.observability.setTap
|
|
82
|
+
* @signature b.observability.setTap(handler)
|
|
83
|
+
* @since 0.7.40
|
|
84
|
+
* @related b.observability.tap, b.observability.event
|
|
85
|
+
*
|
|
86
|
+
* Install an external tap handler that receives every
|
|
87
|
+
* `(name, value, labels)` triple in addition to the framework's
|
|
88
|
+
* metrics module. Wired by `b.otelExport.create()` so an OTLP/HTTP
|
|
89
|
+
* exporter sees the same hot-path counters the framework emits
|
|
90
|
+
* internally. Pass `null` to remove the previously-installed handler.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* b.observability.setTap(function (name, value, labels) {
|
|
94
|
+
* console.log("[obs]", name, value, labels);
|
|
95
|
+
* });
|
|
96
|
+
* b.observability.event("audit.record", 1,
|
|
97
|
+
* { action: "auth.login", outcome: "success" });
|
|
98
|
+
* // → "[obs] audit.record 1 { action: 'auth.login', outcome: 'success' }"
|
|
99
|
+
* b.observability.setTap(null); // remove
|
|
100
|
+
*/
|
|
89
101
|
function setTap(handler) {
|
|
90
102
|
if (handler !== null && typeof handler !== "function") {
|
|
91
103
|
throw new TypeError("observability.setTap: handler must be a function or null, got " +
|
|
@@ -94,6 +106,32 @@ function setTap(handler) {
|
|
|
94
106
|
_externalTap = handler;
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
/**
|
|
110
|
+
* @primitive b.observability.tap
|
|
111
|
+
* @signature b.observability.tap(name, attrs, fn)
|
|
112
|
+
* @since 0.7.0
|
|
113
|
+
* @status stable
|
|
114
|
+
* @related b.observability.event, b.tracing.tap, b.metrics.tap
|
|
115
|
+
*
|
|
116
|
+
* Wrap `fn` in a tracing span (via `b.tracing.tap`) and bump a
|
|
117
|
+
* metrics counter named `name` (via `b.metrics.tap`) when the
|
|
118
|
+
* function settles. The same `attrs` object becomes both span
|
|
119
|
+
* attributes and metric labels. Counter fires on both the success
|
|
120
|
+
* and failure paths so dashboards never miss a failure-rate
|
|
121
|
+
* increment. The two-arg form `tap(name, fn)` skips attributes.
|
|
122
|
+
* Throws on bad input — typos in `name` would silently corrupt both
|
|
123
|
+
* the span tree and the metrics route, so this is a config-time
|
|
124
|
+
* boundary.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* var rows = await b.observability.tap("db.query",
|
|
128
|
+
* { table: "users" },
|
|
129
|
+
* async function (span) {
|
|
130
|
+
* span.setAttribute("db.statement", "SELECT id FROM users");
|
|
131
|
+
* return await db.queryAll("SELECT id FROM users");
|
|
132
|
+
* });
|
|
133
|
+
* // span ended, framework_db_query_total bumped by 1
|
|
134
|
+
*/
|
|
97
135
|
function tap(name, attrs, fn) {
|
|
98
136
|
if (typeof attrs === "function") { fn = attrs; attrs = null; }
|
|
99
137
|
// Throw on bad input: tap is called from many call sites and a typo
|
|
@@ -133,6 +171,24 @@ function tap(name, attrs, fn) {
|
|
|
133
171
|
// counter, not a 500. metrics.tap performs its own label-name regex
|
|
134
172
|
// validation; an invalid call surfaces in the metrics module log, not
|
|
135
173
|
// via a thrown exception.
|
|
174
|
+
/**
|
|
175
|
+
* @primitive b.observability.event
|
|
176
|
+
* @signature b.observability.event(name, value, labels)
|
|
177
|
+
* @since 0.7.0
|
|
178
|
+
* @status stable
|
|
179
|
+
* @related b.observability.tap, b.observability.safeEvent
|
|
180
|
+
*
|
|
181
|
+
* Fire-and-forget counter emit — same shape as `b.metrics.tap` but
|
|
182
|
+
* routed through observability so the operator's external tap
|
|
183
|
+
* (`setTap`) sees it too. Drop-silent on bad `name` by design: this
|
|
184
|
+
* runs in hot paths where throwing on a typo would crash the request
|
|
185
|
+
* that triggered the emit. Use `tap` instead when you also want a
|
|
186
|
+
* span around the emitting code.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* b.observability.event("queue.enqueue", 1, { queueName: "email" });
|
|
190
|
+
* b.observability.event("error.construct", 1, { class: "DatabaseError" });
|
|
191
|
+
*/
|
|
136
192
|
function event(name, value, labels) {
|
|
137
193
|
if (typeof name !== "string" || name.length === 0) return;
|
|
138
194
|
_safeMetricsTap(name, value, labels);
|
|
@@ -143,6 +199,23 @@ function event(name, value, labels) {
|
|
|
143
199
|
// triggered them when the metrics registry has a misconfigured
|
|
144
200
|
// counter or label name. Replaces the per-file `_emitEvent` helper
|
|
145
201
|
// that 7+ modules previously duplicated.
|
|
202
|
+
/**
|
|
203
|
+
* @primitive b.observability.safeEvent
|
|
204
|
+
* @signature b.observability.safeEvent(name, value, labels)
|
|
205
|
+
* @since 0.7.40
|
|
206
|
+
* @related b.observability.event, b.observability.tap
|
|
207
|
+
*
|
|
208
|
+
* Wraps `event` in a try/catch so per-request observability emits
|
|
209
|
+
* cannot crash the request that triggered them when the metrics
|
|
210
|
+
* registry has a misconfigured counter or label name. Replaces the
|
|
211
|
+
* per-file `_emitEvent` helper that several modules previously
|
|
212
|
+
* duplicated.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* // Inside a request handler — even with a typo in label name,
|
|
216
|
+
* // the request still completes.
|
|
217
|
+
* b.observability.safeEvent("auth.attempt", 1, { outcome: "success" });
|
|
218
|
+
*/
|
|
146
219
|
function safeEvent(name, value, labels) {
|
|
147
220
|
try { event(name, value, labels); }
|
|
148
221
|
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
@@ -164,6 +237,31 @@ function safeEvent(name, value, labels) {
|
|
|
164
237
|
// The operation name MUST be a stable string (not derived from input)
|
|
165
238
|
// to keep the metric cardinality bounded; operators dynamically
|
|
166
239
|
// scope-naming via prefix should use the labels parameter instead.
|
|
240
|
+
/**
|
|
241
|
+
* @primitive b.observability.timed
|
|
242
|
+
* @signature b.observability.timed(name, fn, labels)
|
|
243
|
+
* @since 0.7.40
|
|
244
|
+
* @status stable
|
|
245
|
+
* @related b.observability.event, b.observability.tap
|
|
246
|
+
*
|
|
247
|
+
* Measure wall-clock duration of a sync or async operation and emit
|
|
248
|
+
* a counter event with `outcome: "ok"` / `"fail"` plus `duration_ms`
|
|
249
|
+
* in the labels. Returns the wrapped function's return value
|
|
250
|
+
* verbatim; on throw, emits the failure event with `error_type` set
|
|
251
|
+
* to the error's `name` and re-throws. The `name` argument MUST be a
|
|
252
|
+
* stable string (not derived from input) to keep the metric
|
|
253
|
+
* cardinality bounded — operators dynamically scoping should put
|
|
254
|
+
* variable parts into `labels`.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* var rows = await b.observability.timed("db.query",
|
|
258
|
+
* async function () {
|
|
259
|
+
* return await db.queryAll("SELECT id FROM users");
|
|
260
|
+
* },
|
|
261
|
+
* { [b.observability.SEMCONV.DB_OPERATION_NAME]: "select" });
|
|
262
|
+
* // → emits db.query with { outcome: "ok", duration_ms: 12,
|
|
263
|
+
* // "db.operation.name": "select" }
|
|
264
|
+
*/
|
|
167
265
|
function timed(name, fn, labels) {
|
|
168
266
|
if (typeof name !== "string" || name.length === 0) {
|
|
169
267
|
throw new TypeError("observability.timed: name must be a non-empty string");
|
package/lib/openapi.js
CHANGED
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.openapi
|
|
3
|
+
* @module b.openapi
|
|
4
|
+
* @nav Other
|
|
5
|
+
* @title Openapi
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* OpenAPI 3.1 emitter from declarative route declarations + schemas
|
|
9
|
+
* (composable with `b.safeSchema`); JSON / YAML output. Operators
|
|
10
|
+
* describe their public HTTP surface as an OpenAPI 3.1 document the
|
|
11
|
+
* framework serves at `/openapi.json` (or any path) for downstream
|
|
12
|
+
* tooling: API consumers, Postman, code-generators, contract-test
|
|
13
|
+
* rigs.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* truth — it does NOT auto-walk b.router routes (operators
|
|
13
|
-
* want a smaller / different surface published than what
|
|
14
|
-
* exposes internally).
|
|
15
|
-
* route-shape is stable.
|
|
15
|
+
* The builder is FRAMEWORK-FACING: it produces a valid OpenAPI 3.1
|
|
16
|
+
* document, but the operator's hand-written contract is the source
|
|
17
|
+
* of truth — it does NOT auto-walk `b.router` routes (operators
|
|
18
|
+
* frequently want a smaller / different surface published than what
|
|
19
|
+
* the router exposes internally).
|
|
16
20
|
*
|
|
17
|
-
*
|
|
21
|
+
* The builder fluent surface is `path()` / `schema()` / `response()`
|
|
22
|
+
* / `parameter()` / `requestBody()` / `header()` / `example()` /
|
|
23
|
+
* `security.add()` / `security.require()` / `tag()` / `server()`,
|
|
24
|
+
* each returning the builder for chaining. Terminal calls are
|
|
25
|
+
* `toJson()` (3.1 JSON document with referential integrity checked
|
|
26
|
+
* — every security-scheme reference must resolve), `toJsonString()`,
|
|
27
|
+
* `toYaml()`, and `middleware(opts)` which mounts the cached
|
|
28
|
+
* document at request-time. Security-scheme builders for bearer /
|
|
29
|
+
* basic / apiKey / oauth2 / openIdConnect / mtls / dpop live on
|
|
30
|
+
* `b.openapi.security`.
|
|
18
31
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* builder.path(method, urlPattern, opts) // add operation
|
|
23
|
-
* builder.schema(name, schema) // reusable component schema
|
|
24
|
-
* builder.response(name, response) // reusable response
|
|
25
|
-
* builder.parameter(name, parameter) // reusable parameter
|
|
26
|
-
* builder.security.add(name, scheme) // add security scheme
|
|
27
|
-
* builder.security.require(requirement) // doc-level security
|
|
28
|
-
* builder.tag({ name, description }) // tag group
|
|
29
|
-
* builder.server({ url, description, variables }) // server URL
|
|
30
|
-
*
|
|
31
|
-
* builder.toJson() -> OpenAPI 3.1 JSON document
|
|
32
|
-
* builder.toYaml() -> YAML serialisation (if vendored YAML present)
|
|
33
|
-
* builder.middleware(opts) -> request-time middleware that serves the doc
|
|
34
|
-
*
|
|
35
|
-
* b.openapi.security.{bearer,basic,apiKey,oauth2,openIdConnect,mtls,dpop}
|
|
36
|
-
* -> security-scheme builders (delegated from openapi-security.js)
|
|
37
|
-
*
|
|
38
|
-
* b.openapi.schemaWalk(input)
|
|
39
|
-
* -> safeSchema -> JSON Schema utility (delegated)
|
|
32
|
+
* @card
|
|
33
|
+
* OpenAPI 3.1 emitter from declarative route declarations + schemas (composable with `b.safeSchema`); JSON / YAML output.
|
|
40
34
|
*/
|
|
41
35
|
|
|
42
36
|
var validateOpts = require("./validate-opts");
|
|
@@ -52,6 +46,43 @@ var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
|
|
|
52
46
|
|
|
53
47
|
var OPENAPI_VERSION = "3.1.0";
|
|
54
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @primitive b.openapi.create
|
|
51
|
+
* @signature b.openapi.create(opts)
|
|
52
|
+
* @since 0.6.30
|
|
53
|
+
* @related b.openapi.parse, b.asyncapi.create, b.safeSchema
|
|
54
|
+
*
|
|
55
|
+
* Build a fluent OpenAPI 3.1 document builder. `opts.info` is required
|
|
56
|
+
* (`title` + `version`). Returns a chainable builder; terminal calls
|
|
57
|
+
* are `toJson()`, `toJsonString(indent)`, `toYaml()`, and
|
|
58
|
+
* `middleware(opts)`. `toJson()` cross-checks every doc-level and
|
|
59
|
+
* per-operation security requirement against
|
|
60
|
+
* `components.securitySchemes` and throws
|
|
61
|
+
* `OpenApiError("openapi/dangling-security")` on a missing scheme.
|
|
62
|
+
*
|
|
63
|
+
* @opts
|
|
64
|
+
* info: { title, version, description?, contact?, license? }, // REQUIRED — title + version are non-empty strings
|
|
65
|
+
* servers: array, // [{ url, description?, variables? }, ...]
|
|
66
|
+
* externalDocs: { url, description? },
|
|
67
|
+
* tags: array, // [{ name, description? }, ...] — seed; builder.tag() appends more
|
|
68
|
+
* security: array, // doc-level security requirements [{ schemeName: ["scope"] }, ...]
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* var doc = b.openapi.create({
|
|
72
|
+
* info: { title: "Acme API", version: "1.0.0" },
|
|
73
|
+
* servers: [{ url: "https://api.acme.example.com" }],
|
|
74
|
+
* });
|
|
75
|
+
* doc.security.add("bearerAuth", b.openapi.security.bearer({ bearerFormat: "JWT" }));
|
|
76
|
+
* doc.path("get", "/users/{id}", {
|
|
77
|
+
* summary: "Fetch a user",
|
|
78
|
+
* parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
79
|
+
* responses: { "200": { description: "ok" }, "404": { description: "not found" } },
|
|
80
|
+
* security: [{ bearerAuth: [] }],
|
|
81
|
+
* });
|
|
82
|
+
* var json = doc.toJson();
|
|
83
|
+
* json.openapi; // → "3.1.0"
|
|
84
|
+
* json.paths["/users/{id}"].get.summary; // → "Fetch a user"
|
|
85
|
+
*/
|
|
55
86
|
function create(opts) {
|
|
56
87
|
opts = opts || {};
|
|
57
88
|
validateOpts(opts, [
|
|
@@ -316,17 +347,32 @@ function create(opts) {
|
|
|
316
347
|
return builder;
|
|
317
348
|
}
|
|
318
349
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
/**
|
|
351
|
+
* @primitive b.openapi.parse
|
|
352
|
+
* @signature b.openapi.parse(jsonStringOrObject)
|
|
353
|
+
* @since 0.6.30
|
|
354
|
+
* @related b.openapi.create
|
|
355
|
+
*
|
|
356
|
+
* Parse + validate an external OpenAPI 3.1 document. Operators hand a
|
|
357
|
+
* doc that arrived from a downstream integration (consumer hand-
|
|
358
|
+
* edited, contract-test fixture, third-party publish) and want the
|
|
359
|
+
* framework's gate to enforce the same shape rules `toJson()`
|
|
360
|
+
* enforces on builder output. Throws on invalid JSON or non-object
|
|
361
|
+
* input; otherwise returns `{ doc, errors, valid }`. `errors` is an
|
|
362
|
+
* array of strings — empty on a valid document. Path keys must start
|
|
363
|
+
* with `/`, every operation must declare `responses` with a
|
|
364
|
+
* `description`, path parameters must carry `required: true`, and
|
|
365
|
+
* doc-level security must reference declared schemes.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* var result = b.openapi.parse('{"openapi":"3.1.0","info":{"title":"x","version":"1.0.0"}}');
|
|
369
|
+
* result.valid; // → true
|
|
370
|
+
* result.errors; // → []
|
|
371
|
+
*
|
|
372
|
+
* var bad = b.openapi.parse({ openapi: "3.1.0", info: { title: "x", version: "1.0.0" }, paths: { "users": {} } });
|
|
373
|
+
* bad.valid; // → false
|
|
374
|
+
* bad.errors[0]; // → 'path "users" must start with \'/\''
|
|
375
|
+
*/
|
|
330
376
|
function parse(jsonStringOrObject) {
|
|
331
377
|
var doc;
|
|
332
378
|
if (typeof jsonStringOrObject === "string") {
|
package/lib/outbox.js
CHANGED
|
@@ -102,12 +102,94 @@ function _utcNowExpr(externalDb) {
|
|
|
102
102
|
return new Date();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Debezium-shape change-event envelope. Operators integrating with
|
|
106
|
+
// downstream Kafka Connect / Debezium consumers opt-in via
|
|
107
|
+
// `outbox.create({ envelope: "debezium" })`. The envelope wraps the
|
|
108
|
+
// operator's payload as `payload.after` and carries Debezium
|
|
109
|
+
// connector-shape metadata (`source`, `op`, `ts_ms`).
|
|
110
|
+
//
|
|
111
|
+
// Reference: Debezium 2.x ChangeEvent envelope —
|
|
112
|
+
// { schema: { type, fields, optional, name }, payload: {...} }
|
|
113
|
+
//
|
|
114
|
+
// We don't ship a schema-registry hookup — the payload's schema is
|
|
115
|
+
// "operator-supplied JSON object" by default. Operators integrating
|
|
116
|
+
// with Confluent Schema Registry attach `event.debezium.schema` to
|
|
117
|
+
// override per-event.
|
|
118
|
+
var DEFAULT_DEBEZIUM_CONNECTOR_VERSION = "1.0.0"; // allow:raw-byte-literal — version string
|
|
119
|
+
|
|
120
|
+
function _debeziumSchemaFor(payloadObj) {
|
|
121
|
+
// Best-effort schema synthesis. Debezium consumers expect a JSON
|
|
122
|
+
// schema description of `payload`. We emit a permissive object
|
|
123
|
+
// schema so consumers that don't rely on the schema field still
|
|
124
|
+
// round-trip the payload cleanly.
|
|
125
|
+
return {
|
|
126
|
+
type: "struct",
|
|
127
|
+
optional: false,
|
|
128
|
+
name: "blamejs.outbox.Envelope",
|
|
129
|
+
fields: [
|
|
130
|
+
{ type: "struct", optional: true, field: "before",
|
|
131
|
+
name: "blamejs.outbox.Row" },
|
|
132
|
+
{ type: "struct", optional: true, field: "after",
|
|
133
|
+
name: "blamejs.outbox.Row" },
|
|
134
|
+
{ type: "struct", optional: false, field: "source",
|
|
135
|
+
name: "blamejs.outbox.Source",
|
|
136
|
+
fields: [
|
|
137
|
+
{ type: "string", optional: false, field: "connector" },
|
|
138
|
+
{ type: "string", optional: false, field: "version" },
|
|
139
|
+
{ type: "string", optional: true, field: "db" },
|
|
140
|
+
{ type: "string", optional: false, field: "table" },
|
|
141
|
+
{ type: "int64", optional: false, field: "ts_ms" },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{ type: "string", optional: false, field: "op" },
|
|
145
|
+
{ type: "int64", optional: false, field: "ts_ms" },
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _toDebeziumEnvelope(rawEvent, opts) {
|
|
151
|
+
// rawEvent is the operator-shape `{ topic, payload, key, headers,
|
|
152
|
+
// attempts, id }` we already pass to plain publishers. We adapt
|
|
153
|
+
// it here so existing operator schemas work unchanged.
|
|
154
|
+
var payload = rawEvent.payload && typeof rawEvent.payload === "object"
|
|
155
|
+
? rawEvent.payload : { value: rawEvent.payload };
|
|
156
|
+
var op = (rawEvent.headers && typeof rawEvent.headers === "object" &&
|
|
157
|
+
typeof rawEvent.headers["debezium-op"] === "string")
|
|
158
|
+
? rawEvent.headers["debezium-op"]
|
|
159
|
+
: "c"; // default: create. Operators emit u (update) / d (delete) via headers.
|
|
160
|
+
var nowMs = Date.now();
|
|
161
|
+
return {
|
|
162
|
+
schema: _debeziumSchemaFor(payload),
|
|
163
|
+
payload: {
|
|
164
|
+
before: (payload && payload.before) || null,
|
|
165
|
+
after: (payload && payload.after !== undefined) ? payload.after : payload,
|
|
166
|
+
source: {
|
|
167
|
+
connector: opts.connectorName || "blamejs",
|
|
168
|
+
version: opts.connectorVersion || DEFAULT_DEBEZIUM_CONNECTOR_VERSION,
|
|
169
|
+
db: opts.dbName || null,
|
|
170
|
+
table: rawEvent.topic, // topic is the table-shape stable identifier
|
|
171
|
+
ts_ms: nowMs,
|
|
172
|
+
},
|
|
173
|
+
op: op,
|
|
174
|
+
ts_ms: nowMs,
|
|
175
|
+
// Operator-shape passthrough: `key` / `headers` / `attempts`
|
|
176
|
+
// travel as Debezium-extension fields so consumers that need
|
|
177
|
+
// them aren't forced to fabricate.
|
|
178
|
+
key: rawEvent.key || null,
|
|
179
|
+
headers: rawEvent.headers || null,
|
|
180
|
+
attempts: rawEvent.attempts || 0,
|
|
181
|
+
eventId: rawEvent.id || null,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
105
186
|
function create(opts) {
|
|
106
187
|
validateOpts.requireObject(opts, "outbox", OutboxError);
|
|
107
188
|
validateOpts(opts, [
|
|
108
189
|
"externalDb", "table", "publisher",
|
|
109
190
|
"pollIntervalMs", "batchSize", "maxAttempts",
|
|
110
191
|
"retryBackoff", "audit", "name",
|
|
192
|
+
"envelope", "connectorName", "connectorVersion", "dbName",
|
|
111
193
|
], "outbox.create");
|
|
112
194
|
|
|
113
195
|
if (!opts.externalDb || typeof opts.externalDb.transaction !== "function") {
|
|
@@ -148,6 +230,15 @@ function create(opts) {
|
|
|
148
230
|
var auditOn = opts.audit !== false;
|
|
149
231
|
var externalDb = opts.externalDb;
|
|
150
232
|
var publisher = opts.publisher;
|
|
233
|
+
var envelope = opts.envelope || "raw";
|
|
234
|
+
if (envelope !== "raw" && envelope !== "debezium") {
|
|
235
|
+
throw new OutboxError("outbox/bad-envelope",
|
|
236
|
+
"outbox.create: envelope must be 'raw' (default) or 'debezium', got " +
|
|
237
|
+
JSON.stringify(envelope));
|
|
238
|
+
}
|
|
239
|
+
var connectorName = opts.connectorName || "blamejs";
|
|
240
|
+
var connectorVersion = opts.connectorVersion || DEFAULT_DEBEZIUM_CONNECTOR_VERSION;
|
|
241
|
+
var dbName = opts.dbName || null;
|
|
151
242
|
|
|
152
243
|
function _backoffMs(attempts) {
|
|
153
244
|
var ms = backoffInitial * Math.pow(backoffFactor, Math.max(0, attempts - 1));
|
|
@@ -326,7 +417,14 @@ function create(opts) {
|
|
|
326
417
|
headers: row.headers ? safeJson.parse(row.headers, { maxBytes: C.BYTES.mib(1) }) : null,
|
|
327
418
|
attempts: row.attempts,
|
|
328
419
|
};
|
|
329
|
-
|
|
420
|
+
var publishEvent = (envelope === "debezium")
|
|
421
|
+
? _toDebeziumEnvelope(event, {
|
|
422
|
+
connectorName: connectorName,
|
|
423
|
+
connectorVersion: connectorVersion,
|
|
424
|
+
dbName: dbName,
|
|
425
|
+
})
|
|
426
|
+
: event;
|
|
427
|
+
await publisher(publishEvent);
|
|
330
428
|
await _markPublished(row.id);
|
|
331
429
|
_emitMetric("published", 1);
|
|
332
430
|
} catch (e) {
|