@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
package/lib/budr.js CHANGED
@@ -1,38 +1,27 @@
1
1
  "use strict";
2
2
  /**
3
- * b.budr — backup, disaster-recovery, RTO/RPO declaration primitive.
3
+ * @module b.budr
4
+ * @nav Production
5
+ * @title BC/DR
4
6
  *
5
- * Operators in regulated environments (HIPAA / DORA / ISO 22301:2019 /
6
- * NIST SP 800-34) must declare their Recovery Time Objective (RTO,
7
- * how long systems can be down before unacceptable impact) and
8
- * Recovery Point Objective (RPO, max acceptable data loss). The
9
- * declaration is auditor-facing regulators want it on file as part
10
- * of business-continuity / disaster-recovery documentation.
7
+ * @intro
8
+ * Backup / Disaster-Recovery RTO/RPO declaration primitive for
9
+ * regulated workloads (HIPAA / DORA / ISO 22301:2019 / NIST
10
+ * SP 800-34). Operators declare their Recovery Time Objective (max
11
+ * acceptable downtime) and Recovery Point Objective (max acceptable
12
+ * data loss) per service; the framework captures the targets in a
13
+ * tamper-evident audit row and exposes them via `list()` / `get()`
14
+ * for dashboard / regulator-export use.
11
15
  *
12
- * The framework can't enforce RTO/RPO end-to-end (those depend on
13
- * downstream backup cadence, replication topology, restore testing).
14
- * What it can do: capture the operator's declared targets in a
15
- * tamper-evident audit row + expose them to dashboards.
16
+ * The framework cannot enforce end-to-end RTO/RPO — those depend on
17
+ * downstream backup cadence, replication topology, and restore
18
+ * testing the operator owns. What it does enforce is the shape of
19
+ * the declaration (typed targets, BCDR tier vocabulary, regulator
20
+ * citation list) so the auditor-facing record stays consistent
21
+ * across services.
16
22
  *
17
- * Public API:
18
- *
19
- * b.budr.declare(opts) -> declaration
20
- * opts:
21
- * service: operator-named service identifier (string).
22
- * rtoMs: Recovery Time Objective in milliseconds.
23
- * rpoMs: Recovery Point Objective in milliseconds.
24
- * tier: "platinum" / "gold" / "silver" / "bronze"
25
- * (BCDR criticality classification — platinum
26
- * most-critical).
27
- * criticality: "critical" / "high" / "medium" / "low".
28
- * owner: operator-named accountable owner (team / role).
29
- * reviewedAt: timestamp of the most recent operator review.
30
- * citations: array of regulatory citations (e.g. ["dora-art-11", "iso-22301:2019"]).
31
- * audit: bool, default true.
32
- *
33
- * b.budr.list() -> Array<declaration>
34
- *
35
- * b.budr.get(service) -> declaration | null
23
+ * @card
24
+ * Backup / Disaster-Recovery RTO/RPO declaration primitive for regulated workloads (HIPAA / DORA / ISO 22301:2019 / NIST SP 800-34).
36
25
  */
37
26
 
38
27
  var nb = require("./numeric-bounds");
@@ -48,6 +37,45 @@ var CRITICALITIES = ["critical", "high", "medium", "low"];
48
37
 
49
38
  var declarations = new Map();
50
39
 
40
+ /**
41
+ * @primitive b.budr.declare
42
+ * @signature b.budr.declare(opts)
43
+ * @since 0.8.0
44
+ * @status stable
45
+ * @compliance hipaa, dora, soc2
46
+ * @related b.budr.get, b.budr.list, b.dora.create
47
+ *
48
+ * Register a service's Recovery Time Objective + Recovery Point
49
+ * Objective targets. Each call replaces the previous declaration for
50
+ * the same `service` and emits a `budr.declared` audit row carrying
51
+ * the typed targets, BCDR tier, criticality, owner, and regulator
52
+ * citations. Throws `BudrError` on bad opts (unknown tier, missing
53
+ * targets, citation list not an array).
54
+ *
55
+ * @opts
56
+ * service: string (1..128 chars, [A-Za-z0-9._:/-]),
57
+ * rtoMs: number (positive finite integer milliseconds),
58
+ * rpoMs: number (positive finite integer milliseconds),
59
+ * tier: "platinum" | "gold" | "silver" | "bronze",
60
+ * criticality: "critical" | "high" | "medium" | "low",
61
+ * owner: string (team / role accountable for restore),
62
+ * reviewedAt: number (ms-since-epoch of last operator review),
63
+ * citations: Array<string> (e.g. ["dora-art-11", "iso-22301:2019"]),
64
+ * audit: boolean (default true; set false to skip audit emit),
65
+ *
66
+ * @example
67
+ * var dec = b.budr.declare({
68
+ * service: "payments-gateway",
69
+ * rtoMs: 60 * 60 * 1000,
70
+ * rpoMs: 5 * 60 * 1000,
71
+ * tier: "platinum",
72
+ * criticality: "critical",
73
+ * owner: "team-payments",
74
+ * citations: ["dora-art-11", "iso-22301:2019"],
75
+ * });
76
+ * dec.tier; // → "platinum"
77
+ * dec.rtoMs; // → 3600000
78
+ */
51
79
  function declare(opts) {
52
80
  if (!opts || typeof opts !== "object") {
53
81
  throw BudrError.factory("BAD_OPTS", "budr.declare: opts required");
@@ -109,12 +137,57 @@ function declare(opts) {
109
137
  return declaration;
110
138
  }
111
139
 
140
+ /**
141
+ * @primitive b.budr.get
142
+ * @signature b.budr.get(service)
143
+ * @since 0.8.0
144
+ * @status stable
145
+ * @related b.budr.declare, b.budr.list
146
+ *
147
+ * Look up the registered declaration for `service`. Returns the
148
+ * frozen declaration object on hit, `null` on miss or non-string
149
+ * input. Cheap — purely an in-memory Map lookup.
150
+ *
151
+ * @example
152
+ * b.budr.declare({
153
+ * service: "core-ledger", rtoMs: 1800000, rpoMs: 60000,
154
+ * tier: "platinum", criticality: "critical",
155
+ * });
156
+ * var dec = b.budr.get("core-ledger");
157
+ * dec.rpoMs; // → 60000
158
+ * b.budr.get("not-registered"); // → null
159
+ */
112
160
  function get(service) {
113
161
  if (typeof service !== "string") return null;
114
162
  var rec = declarations.get(service);
115
163
  return rec === undefined ? null : rec;
116
164
  }
117
165
 
166
+ /**
167
+ * @primitive b.budr.list
168
+ * @signature b.budr.list()
169
+ * @since 0.8.0
170
+ * @status stable
171
+ * @related b.budr.declare, b.budr.get
172
+ *
173
+ * Snapshot of every registered declaration in insertion order.
174
+ * Returns a fresh array — mutating it does not affect the registry.
175
+ * Use this to drive a dashboard table or to export the operator's
176
+ * BCDR posture for an auditor.
177
+ *
178
+ * @example
179
+ * b.budr.declare({
180
+ * service: "payments-gateway", rtoMs: 3600000, rpoMs: 300000,
181
+ * tier: "platinum", criticality: "critical",
182
+ * });
183
+ * b.budr.declare({
184
+ * service: "reporting", rtoMs: 14400000, rpoMs: 3600000,
185
+ * tier: "silver", criticality: "medium",
186
+ * });
187
+ * var all = b.budr.list();
188
+ * all.length; // → 2
189
+ * all[0].service; // → "payments-gateway"
190
+ */
118
191
  function list() {
119
192
  return Array.from(declarations.values());
120
193
  }
package/lib/bundler.js CHANGED
@@ -1,80 +1,46 @@
1
1
  "use strict";
2
2
  /**
3
- * bundler — content-hashed asset pipeline + manifest, with optional
4
- * operator-supplied ESM engine for module-graph bundling, tree-shaking,
5
- * minification, and source maps.
3
+ * @module b.bundler
4
+ * @nav Tools
5
+ * @title Bundler
6
6
  *
7
- * What this primitive does:
8
- * - Reads each named entry from disk (or runs it through an
9
- * operator-supplied engine first for module-graph builds)
10
- * - Computes a content hash (SHA3-512, first 16 hex chars)
11
- * - Writes the entry to outdir/<name>.<hash>.<ext> for cache-busting
12
- * - Emits manifest.json mapping logical name → hashed filename
13
- * - Optionally watches entries and rebuilds on change
7
+ * @intro
8
+ * Client-side asset bundler produces content-hashed
9
+ * `dist/<name>.<hash>.<ext>` files plus a `manifest.json` mapping
10
+ * logical name to hashed filename. Designed to drop into a static
11
+ * server (`b.static`) so cache-busting lives at the filename layer
12
+ * and HTML can long-cache hashed paths.
14
13
  *
15
- * Engine surface (new in v0.6.44):
14
+ * No-build-step fallback: the default `engine.passthrough` reads
15
+ * each entry from disk verbatim, hashes it, and writes the hashed
16
+ * copy. Operators with no module-graph need ship their source files
17
+ * directly through the bundler and skip the toolchain entirely.
16
18
  *
17
- * var bundler = b.bundler.create({
18
- * entries: { app: "./src/app.js" },
19
- * outdir: "./public/dist",
20
- * engine: engineInstance, // optional defaults to passthrough
21
- * });
22
- *
23
- * `engineInstance` implements:
24
- *
25
- * {
26
- * name: string, // logged on build
27
- * transform: async (entryPath, contentBuf) => {
28
- * content: Buffer | string, // post-transform output
29
- * sourceMap: string | Buffer | null, // optional .map sibling
30
- * imports?: [string], // optional — for graph-aware watch
31
- * },
32
- * }
33
- *
34
- * The default engine is `b.bundler.engine.passthrough` — reads the file
35
- * verbatim, no transform. This is what every existing `b.bundler.create`
36
- * call gets without setting `engine`.
19
+ * Module-graph / tree-shake / minify / sourcemaps: operators supply
20
+ * esbuild (devDependency, never vendored) and adapt it via
21
+ * `engine.fromEsbuild(esbuild, opts)` — the framework owns the
22
+ * integration seam, the operator brings the heavy machinery. The
23
+ * ~10 MB esbuild-wasm blob is intentionally not vendored.
37
24
  *
38
- * For ESM module-graph bundling + tree-shake + minify + sourcemaps,
39
- * operators supply esbuild themselves (devDependency or operator-side
40
- * vendored) and adapt it via:
25
+ * Hashes are SHA3-512, first 16 hex chars by default (operators
26
+ * override via `opts.hashLen` between 4 and 64). Source maps written
27
+ * by an engine land as `<hashed>.<ext>.map` siblings.
41
28
  *
42
- * var esbuild = require("esbuild");
43
- * var bundler = b.bundler.create({
44
- * entries: { app: "./src/app.js" },
45
- * outdir: "./public/dist",
46
- * engine: b.bundler.engine.fromEsbuild(esbuild, {
47
- * bundle: true,
48
- * format: "esm",
49
- * target: ["chrome120", "firefox120", "safari17"],
50
- * minify: true,
51
- * sourcemap: true,
52
- * }),
53
- * });
29
+ * Watch mode: `bundler.watch(callback)` arms `fs.watch` on each
30
+ * entry's directory, debounces bursts via `opts.graceMs` (default
31
+ * 100 ms), and rebuilds the entire entry set on change.
54
32
  *
55
- * The framework intentionally does NOT vendor `esbuild-wasm` itself
56
- * (the wasm blob is ~10 MB — bigger than every other vendored dep
57
- * combined; it would 3-5x the @blamejs/core npm tarball for a build-
58
- * time tool most operators only need at deploy time anyway). Treating
59
- * esbuild as an operator-supplied driver follows the same pattern as
60
- * `b.externalDb` and `b.mtlsCa.create({ engine })`: the framework owns
61
- * the integration seam, the operator brings the heavy machinery.
33
+ * Manifest format:
62
34
  *
63
- * Out of scope (true even with the engine surface):
64
- * - A bespoke bundler bundled INSIDE the framework. The engine
65
- * surface IS the answer; operators wanting esbuild / rollup /
66
- * swc / vite wire them through it.
35
+ * { "app": "app.4a8c2f1d9e3b7062.js", "styles": "styles.b29f1e7c.css" }
67
36
  *
68
- * Operator with no engine + multi-file ESM source: pre-concat manually
69
- * and point bundler at the result.
37
+ * Integrates with `lib/static.js`: serve `outdir` as a static
38
+ * directory; `b.static`'s hashed-path detection sets long-cache
39
+ * headers on files that look hashed, and `integrity()` reads the
40
+ * manifest to emit Subresource Integrity attributes.
70
41
  *
71
- * Manifest format (manifest.json under outdir):
72
- * { "app": "app.4a8c2f1d9e3b7062.js", "styles": "styles.b29f1e7c.css" }
73
- *
74
- * Integrates with lib/static.js: serve `outdir` as a static directory;
75
- * lib/static.js's hashed-path detection sets long-cache headers on
76
- * files that look hashed, and integrity() reads the manifest to
77
- * generate Subresource Integrity attributes.
42
+ * @card
43
+ * Client-side asset bundler — produces content-hashed `dist/<name>.<hash>.<ext>` files plus a `manifest.json` mapping logical name to hashed filename.
78
44
  */
79
45
 
80
46
  var path = require("path");
@@ -224,6 +190,60 @@ function _validateEngine(eng) {
224
190
  return eng;
225
191
  }
226
192
 
193
+ /**
194
+ * @primitive b.bundler.create
195
+ * @signature b.bundler.create(opts)
196
+ * @since 0.4.0
197
+ * @status stable
198
+ * @related b.static.serve
199
+ *
200
+ * Build a content-hashed asset pipeline for a fixed set of named
201
+ * entries. The returned object exposes `build()` (one-shot rebuild,
202
+ * resolves to `{ outputs, manifestPath, manifest, durationMs }`),
203
+ * `watch(callback)` (arm `fs.watch` and debounce-rebuild on change),
204
+ * and `close()` (drop watchers and pending timers).
205
+ *
206
+ * Throws `BundlerError` at config time on missing / malformed entries,
207
+ * missing `outdir`, an out-of-range `hashLen`, or an engine that does
208
+ * not implement `{ name, transform }`.
209
+ *
210
+ * @opts
211
+ * entries: { [name: string]: string }, // logical name → source path
212
+ * outdir: string, // dist directory (created if missing, mode 0o755)
213
+ * cwd: string, // resolves relative entries / outdir; defaults to process.cwd()
214
+ * engine: { name: string, transform: async (entryPath, contentBuf) => { content, sourceMap?, imports? } },
215
+ * // defaults to engine.passthrough
216
+ * manifest: string | false, // manifest filename ("manifest.json"), or false to skip
217
+ * hash: boolean, // emit <name>.<hash>.<ext>; default true
218
+ * hashLen: number, // hex chars in the hash, 4..64; default 16
219
+ * graceMs: number, // watch-mode debounce ms; default 100
220
+ * log: object, // structured logger ({ info, warn, error })
221
+ *
222
+ * @example
223
+ * var bundler = b.bundler.create({
224
+ * entries: { app: "./src/app.js", styles: "./src/styles.css" },
225
+ * outdir: "./public/dist",
226
+ * hashLen: 16,
227
+ * });
228
+ *
229
+ * // bundler.build() returns a Promise resolving to:
230
+ * // { outputs: [...], manifest: { app: "app.<hash>.js", styles: "styles.<hash>.css" },
231
+ * // manifestPath: ".../public/dist/manifest.json", durationMs: <number> }
232
+ *
233
+ * // Watch mode — rebuild on edits.
234
+ * bundler.watch(function (err, result) {
235
+ * if (err) return;
236
+ * // result.manifest is the freshly-written name→hashed-filename map
237
+ * });
238
+ *
239
+ * // Operator-supplied esbuild for module-graph + tree-shake + minify.
240
+ * // var esbuild = require("esbuild");
241
+ * // var modGraph = b.bundler.create({
242
+ * // entries: { app: "./src/app.js" },
243
+ * // outdir: "./public/dist",
244
+ * // engine: b.bundler.engine.fromEsbuild(esbuild, { minify: true, sourcemap: true }),
245
+ * // });
246
+ */
227
247
  function create(opts) {
228
248
  opts = opts || {};
229
249
  validateOpts(opts, [
package/lib/cache.js CHANGED
@@ -1,88 +1,73 @@
1
1
  "use strict";
2
2
  /**
3
- * b.cache — operator-facing cache primitive.
3
+ * @module b.cache
4
+ * @nav Data
5
+ * @title Cache
4
6
  *
5
- * var cache = b.cache.create({
6
- * namespace: "session.user",
7
- * backend: "memory",
8
- * ttlMs: C.TIME.minutes(5),
9
- * maxEntries: 10000,
10
- * maxBytes: C.BYTES.mib(100), // memory backend only
11
- * sizeOf: function (v) { return v.byteLength; }, // optional override
12
- * slidingTtl: true, // bump expiresAt on hit
13
- * audit: b.audit, // optional
14
- * });
15
- *
16
- * await cache.set("u-42", record, { ttlMs: C.TIME.minutes(10), tags: ["user:42", "session"] });
17
- * var hit = await cache.get("u-42");
18
- *
19
- * // Memoize / read-through:
20
- * var profile = await cache.wrap("u-42", function () {
21
- * return db.users.findOne({ _id: "u-42" });
22
- * });
23
- *
24
- * // Bulk invalidate (memory backend):
25
- * await cache.invalidateTag("user:42"); // purges every entry tagged user:42
26
- *
27
- * Surface (returned by create):
7
+ * @intro
8
+ * LRU + TTL cache with operator-supplied namespacing, drop-silent
9
+ * key validation on hot-path observability, and pluggable backends
10
+ * that share semantics across single-process and clustered nodes.
28
11
  *
29
- * get(key) → value | undefined
30
- * set(key, value, opts?) → void (opts: { ttlMs, tags })
31
- * del(key) → boolean (existed)
32
- * has(key) → boolean (does NOT bump LRU recency)
33
- * clear(opts?) → number (purged) (opts: { req, context })
34
- * size() → number
35
- * bytes() → number (memory backend only — total stored bytes)
36
- * wrap(key, fn, opts?) → fn's return value (opts: { ttlMs, singleFlight })
37
- * invalidateTag(tag, opts?) → number (purged) (opts: { req, context })
38
- * getTags(key) → string[] | null
39
- * close() → void
12
+ * Three first-class backends ship in the box:
40
13
  *
41
- * Backends:
14
+ * - "memory" (default) — Map + LRU eviction (maxEntries) + bytes
15
+ * eviction (maxBytes) + periodic sweep. Single-process accuracy.
16
+ * - "cluster" — _blamejs_cache table via cluster-storage. One
17
+ * table serves every CacheInstance via "<namespace>:<key>"
18
+ * composite key; ON CONFLICT UPSERT for atomic set.
19
+ * - "redis" — cache-redis client; sliding TTL via EXPIRE; tag
20
+ * wipes via SCAN+DEL on a per-namespace prefix.
42
21
  *
43
- * "memory" (default) Map + LRU eviction (maxEntries) + periodic
44
- * sweep timer (sweepIntervalMs). Single-process accuracy only.
22
+ * A `{ get, set, del, clear, size, close }` operator-supplied
23
+ * object is the custom-backend escape hatch (Memcached, in-memory
24
+ * harnesses, anything else with the same async surface).
45
25
  *
46
- * "cluster" — _blamejs_cache table via cluster-storage. PRIMARY KEY
47
- * is "<namespace>:<key>" so one table serves every CacheInstance.
48
- * UPSERT via ON CONFLICT for atomic set; DELETE WHERE expiresAt
49
- * for sweep. JSON-only value serialization.
26
+ * Hot-path validation policy:
50
27
  *
51
- * { get, set, del, clear, size, close } — operator-supplied custom
52
- * backend (Redis, Memcached, …). All methods async.
28
+ * - create() opts → throw at boot (config-time)
29
+ * - key arg on get/set/del → throw at call site (programming bug)
30
+ * - per-call ttlMs override → throw at call site (silent footgun
31
+ * if accepted)
32
+ * - audit / observability → drop silent (hot-path sink)
33
+ * - method-after-close → throw BAD_STATE
53
34
  *
54
- * Validation policy:
35
+ * Security defaults that are NOT opt-in:
55
36
  *
56
- * - create() opts → throw at boot
57
- * - get/set/del/has/wrap key arg type → throw at call site (programming bug)
58
- * - set value type tolerant (operator decides what to store)
59
- * - per-call ttlMs override → throw at call site (bad ttl is silent footgun)
60
- * - audit / observability emit failures → drop silent (hot-path sink)
61
- * - method called after close() → throw BAD_STATE at call site
37
+ * - auditClear: true mass purges are operator-action shaped
38
+ * - auditFailures: true backend errors are signal
39
+ * - hot-path get/set/hit/miss/evictionobservability only
40
+ * (the audit chain would drown at any reasonable QPS)
62
41
  *
63
- * Security defaults:
42
+ * Returned `CacheInstance` shape:
64
43
  *
65
- * - auditClear: true mass purge is operator-action shaped (can hide forensics)
66
- * - auditFailures: true — backend errors are signal
67
- * - hot-path get/set/hit/miss/eviction observability only (audit chain
68
- * would drown at any reasonable QPS)
44
+ * get(key) value | undefined
45
+ * set(key, value, opts?) → void (opts: { ttlMs, tags, seal })
46
+ * del(key)boolean
47
+ * has(key) boolean (does NOT bump LRU recency)
48
+ * clear(opts?) → number (opts: { req, context })
49
+ * size() → number
50
+ * bytes() → number (memory backend only)
51
+ * wrap(key, fn, opts?) → fn's return (opts: { ttlMs, singleFlight })
52
+ * invalidateTag(tag, opts?) → number (opts: { req, context })
53
+ * getTags(key) → string[] | null
54
+ * close() → void
69
55
  *
70
- * The cache supports single-flight wrap (concurrent calls collapse),
71
- * stale-while-revalidate, LRU + bytes eviction on the memory backend,
72
- * sliding TTL on hit, tag-based bulk invalidation (memory backend), a
73
- * shared cluster backend, and a custom-backend escape hatch.
56
+ * Stale-while-revalidate, single-flight wrap (concurrent calls
57
+ * collapse to one compute), tag-based bulk invalidation (memory +
58
+ * cluster), and cross-node invalidation via b.pubsub are all built
59
+ * in operator opts in via the `staleWhileRevalidate` /
60
+ * `invalidationPubsub` opts.
74
61
  *
75
- * What is NOT in the box:
62
+ * What is NOT in the box: maxBytes on the cluster backend (would
63
+ * require an aggregate query per set; operator prunes the shared
64
+ * table on their own schedule) and per-entry exact slidingTtl on
65
+ * the cluster backend (sliding extends by the cache's defaultTtlMs;
66
+ * operators with mixed-TTL writes wanting strict per-entry sliding
67
+ * use the memory backend or extend at the application layer).
76
68
  *
77
- * - maxBytes on the cluster backend — per-row size accounting against
78
- * a shared table would mean an aggregate query on every set. The
79
- * operator controls cluster-table size with their own pruning if
80
- * bytes pressure surfaces.
81
- * - Per-entry exact slidingTtl on the cluster backend — sliding works
82
- * on cluster but extends by the cache's defaultTtlMs (we don't
83
- * store per-row ttl). Operators with mixed-TTL writes wanting
84
- * strict per-entry sliding use the memory backend or extend at
85
- * the application layer.
69
+ * @card
70
+ * LRU + TTL cache with operator-supplied namespacing, drop-silent key validation on hot-path observability, and pluggable backends that share semantics across single-process and clustered nodes.
86
71
  */
87
72
 
88
73
  var cacheRedis = require("./cache-redis");
@@ -100,6 +85,15 @@ var { CacheError } = require("./framework-error");
100
85
 
101
86
  var log = boot("cache");
102
87
  var observability = lazyRequire(function () { return require("./observability"); });
88
+ // D-L5 — opt-in vault seal for cluster-backend cache values. Lazy so
89
+ // vault-not-initialized in tests with a memory cache doesn't crash
90
+ // at module load.
91
+ var vault = lazyRequire(function () { return require("./vault"); });
92
+ // Marker bytes that prefix sealed values in _blamejs_cache.valueJson —
93
+ // "blamejs:cache.sealed:" + base64-of-vault-sealed-payload. The
94
+ // non-prefixed cache rows pass straight through JSON.parse as before
95
+ // so the seal opt is a strict per-call upgrade with no migration.
96
+ var CACHE_SEAL_PREFIX = "blamejs:cache.sealed:";
103
97
 
104
98
  var _err = CacheError.factory;
105
99
 
@@ -486,7 +480,17 @@ function _clusterBackend(cfg) {
486
480
  [newExpires, now, _composedKey(key), now]
487
481
  ).catch(function () { /* best-effort */ });
488
482
  }
489
- try { return safeJson.parse(row.valueJson, { maxBytes: C.BYTES.mib(64) }); }
483
+ var stored = row.valueJson;
484
+ // D-L5 — sealed-row decode. Sealed entries are prefixed at write
485
+ // time so the unseal-on-read path is a strict opt-in: rows
486
+ // written without seal:true continue parsing as before.
487
+ if (typeof stored === "string" && stored.indexOf(CACHE_SEAL_PREFIX) === 0) {
488
+ try {
489
+ var unsealed = vault().unseal(stored.substring(CACHE_SEAL_PREFIX.length));
490
+ return safeJson.parse(unsealed, { maxBytes: C.BYTES.mib(64) });
491
+ } catch (_e) { return undefined; }
492
+ }
493
+ try { return safeJson.parse(stored, { maxBytes: C.BYTES.mib(64) }); }
490
494
  catch (_e) { return undefined; }
491
495
  }
492
496
 
@@ -497,6 +501,13 @@ function _clusterBackend(cfg) {
497
501
  // changed value, app code treats it as the original" — a subtle
498
502
  // freshness bug that's hard to debug.
499
503
  var json = safeJson.stringify(value);
504
+ // D-L5 — opt-in vault seal. When the caller passes seal: true,
505
+ // wrap the JSON via b.vault.seal (XChaCha20-Poly1305) before
506
+ // landing in _blamejs_cache.valueJson. The marker prefix is what
507
+ // get() looks for to know it must unseal on read.
508
+ if (meta && meta.seal === true) {
509
+ json = CACHE_SEAL_PREFIX + vault().seal(json);
510
+ }
500
511
  var storedExpires = (expiresAt === Infinity) ? Number.MAX_SAFE_INTEGER : expiresAt;
501
512
  var now = clock();
502
513
  var ck = _composedKey(key);
@@ -713,6 +724,103 @@ function _customBackend(operatorBackend, cfg) {
713
724
 
714
725
  // ---- Public create ----
715
726
 
727
+ /**
728
+ * @primitive b.cache.create
729
+ * @signature b.cache.create(opts)
730
+ * @since 0.1.0
731
+ * @status stable
732
+ * @related b.pubsub.create, b.audit
733
+ *
734
+ * Build a `CacheInstance` bound to a `namespace`. The instance owns
735
+ * its sweep timer, its backend connection, its single-flight inflight
736
+ * map, and (when `invalidationPubsub` is supplied) a pubsub
737
+ * subscription that mirrors `del` / `clear` / `invalidateTag` events
738
+ * across nodes. Multiple instances coexist — a "session.user" memory
739
+ * cache and a "billing.invoice" cluster cache share neither keys nor
740
+ * tags. `close()` releases everything.
741
+ *
742
+ * The `backend` opt picks the storage tier: `"memory"` (default,
743
+ * single-process LRU+TTL), `"cluster"` (shared SQL table for
744
+ * multi-node coherence), `"redis"` (when `redisUrl` is supplied;
745
+ * native EXPIRE-based TTL), or an operator-supplied object with
746
+ * `{ get, set, del, clear, size, close }` for any other store.
747
+ * Backends are interchangeable from the caller's perspective —
748
+ * `await cache.get(key)` returns the same shape regardless.
749
+ *
750
+ * @opts
751
+ * namespace: string, // required; collision domain; must not contain ':'
752
+ * backend: "memory" | "cluster" | "redis" | object, // default "memory"
753
+ * ttlMs: number | Infinity, // default C.TIME.minutes(5)
754
+ * maxEntries: number | Infinity, // memory backend cap; default 10000
755
+ * maxBytes: number | Infinity, // memory backend cap; default Infinity
756
+ * sizeOf: function(value) -> number, // memory bytes accounting override
757
+ * sweepIntervalMs: number, // expired-entry sweep cadence; default C.TIME.minutes(1); minimum 1000
758
+ * staleWhileRevalidate: boolean, // wrap() serves stale + refreshes in background; default false
759
+ * slidingTtl: boolean, // bump expiresAt on hit; default false
760
+ * auditFailures: boolean, // emit audit on backend errors; default true
761
+ * auditClear: boolean, // emit audit on clear / invalidateTag; default true
762
+ * audit: { emit } | b.audit, // audit sink override
763
+ * observability: { event } | b.observability, // metrics sink override
764
+ * clock: function() -> number, // Date.now() override (testing)
765
+ * invalidationPubsub: b.pubsub instance, // cross-node del/clear/tag mirroring
766
+ * redisUrl: string, // backend === "redis" only; required there
767
+ * redisPassword: string, // backend === "redis" only
768
+ * redisUsername: string, // backend === "redis" only
769
+ * redisTls: boolean, // backend === "redis" only
770
+ * redisCa: string | Buffer, // backend === "redis" only; PEM CA bundle
771
+ * redisServername: string, // backend === "redis" only; SNI override
772
+ * redisConnectTimeoutMs: number, // backend === "redis" only
773
+ * redisCommandTimeoutMs: number, // backend === "redis" only
774
+ * redisMaxReconnectAttempts: number, // backend === "redis" only
775
+ *
776
+ * @example
777
+ * var b = require("@blamejs/core");
778
+ * var C = b.constants;
779
+ *
780
+ * // Simple set/get against the default memory backend.
781
+ * var sessions = b.cache.create({
782
+ * namespace: "session.user",
783
+ * ttlMs: C.TIME.minutes(5),
784
+ * maxEntries: 10000,
785
+ * });
786
+ * await sessions.set("u-42", { uid: "u-42", role: "admin" });
787
+ * var hit = await sessions.get("u-42");
788
+ * // → { uid: "u-42", role: "admin" }
789
+ *
790
+ * @example
791
+ * var b = require("@blamejs/core");
792
+ * var C = b.constants;
793
+ *
794
+ * // wrap() pattern with per-call TTL override + single-flight.
795
+ * // Concurrent callers collapse to one DB read; subsequent reads
796
+ * // serve from cache for 10 minutes.
797
+ * var profiles = b.cache.create({
798
+ * namespace: "billing.profile",
799
+ * ttlMs: C.TIME.minutes(2),
800
+ * });
801
+ * var profile = await profiles.wrap(
802
+ * "u-42",
803
+ * function () { return { uid: "u-42", plan: "pro" }; },
804
+ * { ttlMs: C.TIME.minutes(10) }
805
+ * );
806
+ * // → { uid: "u-42", plan: "pro" }
807
+ *
808
+ * @example
809
+ * var b = require("@blamejs/core");
810
+ * var C = b.constants;
811
+ *
812
+ * // Cluster-shared cache: every node sees the same entries via the
813
+ * // _blamejs_cache table. Tag-based bulk invalidation purges across
814
+ * // every namespace member in one call.
815
+ * var inventory = b.cache.create({
816
+ * namespace: "catalog.item",
817
+ * backend: "cluster",
818
+ * ttlMs: C.TIME.minutes(15),
819
+ * });
820
+ * await inventory.set("sku-1001", { qty: 42 }, { tags: ["warehouse:east"] });
821
+ * var purged = await inventory.invalidateTag("warehouse:east");
822
+ * // → 1
823
+ */
716
824
  function create(opts) {
717
825
  opts = opts || {};
718
826
  validateOpts(opts, [
@@ -885,7 +993,19 @@ function create(opts) {
885
993
  }
886
994
  }
887
995
  }
888
- try { await backend.set(key, value, expiresAt, { ttlMs: ttlMs, tags: tags }); }
996
+ // D-L5 opt-in vault seal. Strict-shape check: must be the literal
997
+ // boolean true, not just truthy. Backends that don't support seal
998
+ // (memory, custom) ignore the flag transparently; cluster backend
999
+ // wraps valueJson via b.vault.seal before INSERT.
1000
+ var seal = !!(callerOpts && callerOpts.seal === true);
1001
+ if (seal && backend.name !== "cluster") {
1002
+ throw _err("BAD_OPT",
1003
+ "cache.set: seal: true is only supported on the cluster backend " +
1004
+ "(this cache instance uses '" + (backend.name || "custom") + "'). " +
1005
+ "Memory-backed caches do not need seal because the value never reaches disk; " +
1006
+ "custom backends wrap their own at-rest encryption.");
1007
+ }
1008
+ try { await backend.set(key, value, expiresAt, { ttlMs: ttlMs, tags: tags, seal: seal }); }
889
1009
  catch (e) {
890
1010
  emitObs("cache.backend.failed", { namespace: namespace, op: "set" });
891
1011
  _backendFailedAudit("set", e);