@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -1,58 +1,49 @@
1
1
  "use strict";
2
2
  /**
3
- * observability — combined metrics + tracing tap surface.
3
+ * @module b.observability
4
+ * @nav Observability
5
+ * @title Observability
4
6
  *
5
- * Framework hot paths previously called metrics.tap + tracing.tap
6
- * separately, with each module repeating the lazy-require + try/catch
7
- * boilerplate. This primitive folds the two into one helper:
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
- * var obs = require("./observability");
10
- * return obs.tap("audit.record",
11
- * { action: event.action, outcome: event.outcome },
12
- * async function (span) {
13
- * // ... operation body ...
14
- * return result;
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
- * For fire-and-forget value-noting where wrapping fn doesn't fit —
36
- * incrementing a counter on a side-effect deep inside an existing
37
- * function use `event(name, value, labels)`. Same shape as the
38
- * legacy metrics.tap call; routes through metrics only (no span).
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
- * Public API:
41
- * observability.tap(name, attrs, fn) → fn's return value
42
- * observability.tap(name, fn) → fn's return value (no attrs)
43
- * observability.event(name, value, labels) undefined
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
- * Tests live in test/layer-0-primitives/observability.test.js.
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
- * Parameters:
48
- * name: stringused as both the span name AND the metrics tap
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 — OpenAPI 3.1 schema-document builder.
3
+ * @module b.openapi
4
+ * @nav Other
5
+ * @title Openapi
4
6
  *
5
- * Operators describe their public HTTP surface as an OpenAPI 3.1
6
- * document the framework can serve at /openapi.json (or any path of
7
- * their choice) for downstream tooling: API consumers, Postman, code-
8
- * generators, contract-testing rigs.
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
- * The builder is FRAMEWORK-FACING: it produces a valid OpenAPI 3.1
11
- * document, but the operator's hand-written contract is the source of
12
- * truth — it does NOT auto-walk b.router routes (operators frequently
13
- * want a smaller / different surface published than what the router
14
- * exposes internally). Future patch may add `fromRouter()` once the
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
- * Public surface:
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
- * b.openapi.create({ info, servers, externalDocs, tags })
20
- * -> builder
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
- // Parse + validate an external OpenAPI 3.1 JSON document. Operators
320
- // hand a doc that arrived from a downstream integration (consumer
321
- // hand-edited, contract-test fixture, third-party publish) and want
322
- // the framework's gate to enforce the same shape rules `toJson` does
323
- // on builder output.
324
- //
325
- // Returns `{ doc, errors[] }`. `doc` is the parsed object (whether
326
- // valid or not, so the operator can inspect what they got);
327
- // `errors` is an array of strings empty on a valid document.
328
- //
329
- // Throws (config-time entry-point) on invalid JSON / wrong type.
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
- await publisher(event);
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) {