@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,50 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * Chain-writer primitive — race-safe append to a hash-chained log table.
3
+ * @module b.chainWriter
4
+ * @nav Observability
5
+ * @title Chain Writer
4
6
  *
5
- * The framework's audit_log AND consent_log have the same shape:
7
+ * @intro
8
+ * Race-safe append to a hash-chained log table. Both `audit_log` and
9
+ * `consent_log` share the same row shape — take next monotonic
10
+ * counter, read previous row's `rowHash`, seal the logical row via
11
+ * field-crypto, materialize null entries for every hashable column
12
+ * so canonicalization sees the same key set at write-time and
13
+ * verify-time, compute `rowHash` over the sealed content, INSERT
14
+ * with `prevHash` / `rowHash` / `nonce` / `fencingToken`.
6
15
  *
7
- * 1. Take the next monotonic counter
8
- * 2. Compute prevHash from the previous row's rowHash
9
- * 3. Seal the logical row via field-crypto (sealedFields → vault.seal,
10
- * derivedHashes computed)
11
- * 4. Materialize null entries for every hashable column (so canonicalize
12
- * at write-time and verify-time agree on the key set)
13
- * 5. Compute rowHash over the sealed content (excluding chain bookkeeping)
14
- * 6. INSERT with prevHash / rowHash / nonce / fencingToken
16
+ * The chain-writer extracts that pattern so every consumer gets the
17
+ * same race protection. Each instance owns a per-chain Mutex
18
+ * serializing read-prev compute-hash insert (without it,
19
+ * concurrent appends hash against the same prev-tip and fork the
20
+ * chain), plus a Once initializing the in-process counter from
21
+ * `MAX(monotonicCounter)` on first use.
15
22
  *
16
- * audit.js and consent.js previously each carried their own copy of
17
- * the chain-write pattern. The duplication produced bugs at the same
18
- * architectural points: a chain-fork race had to be fixed in audit
19
- * via Mutex, and consent had the same race because the fix hadn't
20
- * propagated. Per the framework's "if a task repeats more than once
21
- * it should be a primitive" rule, the pattern lives here and every
22
- * chain-writer consumer gets the same safety guarantees automatically.
23
+ * Writes route through the cluster-storage dispatcher so the same
24
+ * chain definition works on single-node SQLite and on cluster-mode
25
+ * external Postgres. `cluster.requireLeader()` runs before the
26
+ * mutex; followers reject with `NotLeaderError`. Table names are
27
+ * restricted to the `ALLOWED_CHAIN_TABLES` allowlist so a misconfig
28
+ * can't point a writer at a non-chain table and corrupt the chain
29
+ * semantics.
23
30
  *
24
- * Each chain-writer instance owns:
25
- * - The table name (validated via sql-safe.assertOneOf at construction)
26
- * - The full column list (for INSERT)
27
- * - The hashable column list (for canonicalization)
28
- * - A Mutex serializing the chain (read-prev → compute-hash → insert)
29
- * - A Once initializing the in-process counter from MAX(monotonicCounter)
31
+ * Operators usually don't construct chain-writers directly — `b.audit`
32
+ * and `b.consent` each construct one at module load. Direct use is
33
+ * for new chain-backed tables registered in `ALLOWED_CHAIN_TABLES`.
30
34
  *
31
- * Writes go through the cluster-storage dispatcher so the same chain
32
- * definition works in single-node SQLite and cluster-mode external-db.
33
- *
34
- * Public API:
35
- *
36
- * chainWriter.create({
37
- * table: "audit_log" | "consent_log" | …,
38
- * columnsForInsert: [string], order matters; INSERT uses this
39
- * hashableColumns: [string], for canonicalize null-fill
40
- * validateAction: function (event) optional; throws on invalid input
41
- * })
42
- *
43
- * writer.append(logical) async; returns { rowHash, prevHash, …logical }
44
- * writer._resetForTest() re-initializes counter + mutex
45
- *
46
- * Operators usually don't construct chain-writers directly; audit and
47
- * consent each construct one at module load.
35
+ * @card
36
+ * Race-safe append to a hash-chained log table.
48
37
  */
49
38
 
50
39
  var { generateToken, generateBytes } = require("./crypto");
@@ -74,6 +63,42 @@ class ChainWriterError extends FrameworkError {
74
63
  }
75
64
  }
76
65
 
66
+ /**
67
+ * @primitive b.chainWriter.create
68
+ * @signature b.chainWriter.create(opts)
69
+ * @since 0.8.48
70
+ * @status stable
71
+ * @related b.audit, b.consent, b.auditChain
72
+ *
73
+ * Build a chain-writer bound to a single hash-chained table. Returns
74
+ * `{ table, append, _resetForTest, _getMutexForTest }`. `append(logical)`
75
+ * is the public surface — async, leader-gated, mutex-serialized; on
76
+ * success it returns the logical row decorated with the computed
77
+ * `rowHash` and `prevHash`.
78
+ *
79
+ * @opts
80
+ * table: string, // one of ALLOWED_CHAIN_TABLES (audit_log | consent_log)
81
+ * columnsForInsert: string[], // INSERT column order (every name is identifier-validated)
82
+ * hashableColumns: string[], // columns that participate in the rowHash canonicalization
83
+ * validateInput: Function, // optional; (logical) → throws on invalid shape
84
+ *
85
+ * @example
86
+ * var writer = b.chainWriter.create({
87
+ * table: "audit_log",
88
+ * columnsForInsert: ["_id", "monotonicCounter", "recordedAt",
89
+ * "action", "outcome",
90
+ * "prevHash", "rowHash", "nonce", "fencingToken"],
91
+ * hashableColumns: ["_id", "monotonicCounter", "recordedAt",
92
+ * "action", "outcome"],
93
+ * });
94
+ *
95
+ * var row = await writer.append({
96
+ * action: "user.login",
97
+ * outcome: "success",
98
+ * });
99
+ * row.rowHash; // → "<hex sha3-512 digest>"
100
+ * row.prevHash; // → "<previous tip rowHash, or zero-hash on first row>"
101
+ */
77
102
  function create(opts) {
78
103
  if (!opts || !opts.table || !Array.isArray(opts.columnsForInsert) ||
79
104
  !Array.isArray(opts.hashableColumns)) {
@@ -1,46 +1,69 @@
1
1
  "use strict";
2
2
  /**
3
- * b.circuitBreaker — top-level circuit-breaker primitive.
3
+ * @module b.circuitBreaker
4
+ * @nav Production
5
+ * @title Circuit Breaker
4
6
  *
5
- * Re-exports the CircuitBreaker class previously only reachable as
6
- * b.retry.CircuitBreaker, plus a `create(opts)` factory that matches
7
- * every other framework primitive's `create()` shape. The
8
- * implementation lives in lib/retry.js to keep the retry-classifier
9
- * + CircuitBreaker close to each other (they share the
10
- * isRetryable / observability emit conventions). This module is the
11
- * top-level surface so operators don't have to know that retry
12
- * happens to be the home of the circuit-breaker class.
7
+ * @intro
8
+ * Top-level circuit-breaker primitive. Re-exports the CircuitBreaker
9
+ * class otherwise reachable as `b.retry.CircuitBreaker`, plus a
10
+ * `create(opts)` factory matching every other framework primitive's
11
+ * `create()` shape. The implementation lives in `lib/retry.js` so the
12
+ * retry classifier and the breaker share the `isRetryable` /
13
+ * observability emit conventions; this module is the operator-facing
14
+ * surface so callers don't have to know retry is the breaker's home.
13
15
  *
14
- * State machine:
15
- * closed — normal flow; failures count up to failureThreshold
16
- * open — fast-fail every call for cooldownMs
17
- * half — first probe succeeds close; first probe fails → re-open
16
+ * State machine: `closed` (normal flow; failures count up to
17
+ * `failureThreshold`), `open` (every call fast-fails for `cooldownMs`),
18
+ * `half` (first probe closes the breaker on success or re-opens it
19
+ * on failure). Intended for per-target use one instance per
20
+ * upstream service. Sharing a breaker across unrelated targets
21
+ * defeats the failure-threshold semantic.
18
22
  *
23
+ * @card
24
+ * Top-level circuit-breaker primitive.
25
+ */
26
+
27
+ var retry = require("./retry");
28
+
29
+ /**
30
+ * @primitive b.circuitBreaker.create
31
+ * @signature b.circuitBreaker.create(opts)
32
+ * @since 0.8.48
33
+ * @status stable
34
+ * @related b.retry, b.httpClient
35
+ *
36
+ * Build a circuit-breaker. Returns a CircuitBreaker instance with
37
+ * `wrap(fn)` (executes `fn` if the breaker is closed; throws RetryError
38
+ * with `code: "retry/circuit-open"` when open), `state()`, `reset()`,
39
+ * and `onStateChange(handler)` listener registration. Pass-through
40
+ * factory: identical instance shape to `b.retry.CircuitBreaker`, with
41
+ * the framework's `create(opts)` vocabulary.
42
+ *
43
+ * @opts
44
+ * name: string, // identifier used in audit + state-change events
45
+ * failureThreshold: number, // failures in the closed state before opening
46
+ * cooldownMs: number, // milliseconds the breaker stays open before probing
47
+ * successThreshold: number, // probe successes required to close from half-open
48
+ * audit: Object, // optional b.audit instance for state-change emission
49
+ * onStateChange: Function, // ({ name, from, to, at }) → void
50
+ *
51
+ * @example
19
52
  * var cb = b.circuitBreaker.create({
20
- * name: "upstream-billing",
21
- * failureThreshold: 5,
22
- * cooldownMs: b.constants.TIME.seconds(30),
23
- * successThreshold: 2,
24
- * audit: b.audit,
25
- * onStateChange: function (event) {
26
- * // event = { name, from, to, at }
27
- * log("breaker " + event.name + " " + event.from + " -> " + event.to);
53
+ * name: "upstream-billing",
54
+ * failureThreshold: 5,
55
+ * cooldownMs: 30000,
56
+ * successThreshold: 2,
57
+ * onStateChange: function (e) {
58
+ * // e = { name, from: "closed", to: "open", at: <ms> }
28
59
  * },
29
60
  * });
30
- * await cb.wrap(function () { return upstream.callRiskyOp(); });
31
61
  *
32
- * The circuit-breaker is intended for per-target use (one instance
33
- * per upstream service); operators sharing a breaker across
34
- * unrelated targets defeat the failure-threshold semantic.
62
+ * var result = await cb.wrap(async function () {
63
+ * return { ok: true, value: 42 };
64
+ * });
65
+ * result.value; // → 42
35
66
  */
36
-
37
- var retry = require("./retry");
38
-
39
- // Pass-through factory — operators get the same instance shape as
40
- // b.retry.CircuitBreaker but with the framework's `create(opts)`
41
- // vocabulary. The breaker class is unchanged; this is a thin
42
- // surface re-export so b.circuitBreaker is operator-discoverable
43
- // alongside b.retry.
44
67
  function create(opts) {
45
68
  return new retry.CircuitBreaker(opts || {});
46
69
  }
@@ -1,50 +1,28 @@
1
1
  "use strict";
2
2
  /**
3
- * cli-helpers — shared shape for blamejs CLI subcommands AND for
4
- * operators writing their own one-shot CLI scripts on top of the
5
- * framework.
3
+ * @module b.cliHelpers
4
+ * @nav Production
5
+ * @title CLI Helpers
6
6
  *
7
- * Three patterns recur across every CLI command (`migrate`, `seed`,
8
- * `audit`, `vault`, `backup`, `api-key`):
7
+ * @intro
8
+ * Shared shape for blamejs CLI subcommands AND for operators
9
+ * writing their own one-shot CLI scripts on top of the framework.
10
+ * Three patterns recur across every CLI command (`migrate`, `seed`,
11
+ * `audit`, `vault`, `backup`, `api-key`): bootstrap a headless
12
+ * `b.createApp` instance from `--data-dir` and shut it down cleanly;
13
+ * report success / error / usage with a consistent
14
+ * `blamejs <verb> <sub>: <message>` prefix on stderr plus canonical
15
+ * exit codes (0 ok, 1 runtime failure, 2 arg error); resolve a
16
+ * passphrase from a `--<flag>` or env var into the Buffer shape
17
+ * the underlying crypto primitives accept.
9
18
  *
10
- * 1. Bootstrap a headless `b.createApp` instance from `--data-dir`,
11
- * operate against vault + DB + audit chain, shut down cleanly.
12
- * 2. Report success / error / usage with a consistent
13
- * `blamejs <verb> <sub>: <message>` prefix on stderr + canonical
14
- * exit codes (0 ok, 1 runtime failure, 2 arg error).
15
- * 3. Resolve a passphrase from `--<flag>` or an env var, encode to
16
- * the Buffer the underlying crypto primitive needs.
19
+ * The reporter writes through whichever stream `ctx.stdout` /
20
+ * `ctx.stderr` point at `process.stdout` / `process.stderr` in
21
+ * production, captured stream stubs in tests so the same handler
22
+ * is testable without spawning a child process.
17
23
  *
18
- * var cli = b.cliHelpers;
19
- *
20
- * var report = cli.makeReporter(ctx, "blamejs my-tool issue");
21
- * if (!args.flags["data-dir"]) return report.usage(MY_USAGE);
22
- *
23
- * var pp = cli.resolvePassphrase(args, ctx, {
24
- * flag: "passphrase",
25
- * envVar: "MY_TOOL_PASSPHRASE",
26
- * });
27
- * if (!pp) return report.error("--passphrase or MY_TOOL_PASSPHRASE is required", 2);
28
- *
29
- * var booted;
30
- * try {
31
- * booted = await cli.bootApp({
32
- * dataDir: args.flags["data-dir"],
33
- * vaultMode: "wrapped",
34
- * env: ctx.env,
35
- * });
36
- * // do the op against booted.b.X / booted.app.db / etc.
37
- * return report.ok("done");
38
- * } catch (e) {
39
- * return report.error(e.message);
40
- * } finally {
41
- * if (booted) await booted.app.shutdown();
42
- * }
43
- *
44
- * The reporter writes through whichever stream `ctx.stdout` / `ctx.stderr`
45
- * point at — `process.stdout` / `process.stderr` in production, captured
46
- * stream stubs in tests — so the same handler is testable without
47
- * spawning a child process.
24
+ * @card
25
+ * Shared shape for blamejs CLI subcommands AND for operators writing their own one-shot CLI scripts on top of the framework.
48
26
  */
49
27
 
50
28
  var lazyRequire = require("./lazy-require");
@@ -66,15 +44,26 @@ function _writeLine(stream, line) {
66
44
  }
67
45
 
68
46
  /**
69
- * Make a reporter bound to a CLI context + a verb prefix. Every
70
- * stderr message produced by .error / .usage gets the prefix.
47
+ * @primitive b.cliHelpers.makeReporter
48
+ * @signature b.cliHelpers.makeReporter(ctx, prefix)
49
+ * @since 0.8.0
50
+ * @status stable
51
+ * @related b.cliHelpers.resolvePassphrase, b.cliHelpers.bootApp
71
52
  *
72
- * var report = cliHelpers.makeReporter(ctx, "blamejs vault seal");
73
- * return report.ok("sealed: " + path); // stdout, returns 0
74
- * return report.error("decrypt failed"); // stderr "blamejs vault seal: decrypt failed", returns 1
75
- * return report.error("missing arg", 2); // returns 2 (arg error vs runtime)
76
- * return report.usage(VAULT_USAGE); // stderr USAGE, returns 2
77
- * return report.helpStdout(VAULT_USAGE); // stdout USAGE, returns 0 (for `help <verb>`)
53
+ * Build a reporter bound to a CLI context (`ctx.stdout` /
54
+ * `ctx.stderr`) and a verb prefix. Every stderr message produced by
55
+ * `.error` / `.usage` gets the prefix. Methods return canonical
56
+ * Unix exit codes `0` for `ok` / `helpStdout`, `1` for `error`
57
+ * (override via second arg), `2` for `usage` (argument-error
58
+ * convention).
59
+ *
60
+ * @example
61
+ * var ctx = { stdout: process.stdout, stderr: process.stderr };
62
+ * var report = b.cliHelpers.makeReporter(ctx, "blamejs vault seal");
63
+ * var ok = report.ok("sealed: /data/vault"); // → 0
64
+ * var fail = report.error("decrypt failed"); // → 1
65
+ * var arg = report.error("missing --data-dir", 2); // → 2
66
+ * var help = report.usage("Usage: blamejs vault ..."); // → 2
78
67
  */
79
68
  function makeReporter(ctx, prefix) {
80
69
  if (!ctx || typeof ctx !== "object") {
@@ -111,15 +100,35 @@ function makeReporter(ctx, prefix) {
111
100
  // ---- Passphrase resolution -----------------------------------------------
112
101
 
113
102
  /**
114
- * Resolve a passphrase from a flag or env var into a UTF-8 Buffer
115
- * (the shape the underlying crypto primitives accept).
103
+ * @primitive b.cliHelpers.resolvePassphrase
104
+ * @signature b.cliHelpers.resolvePassphrase(args, ctx, opts)
105
+ * @since 0.8.0
106
+ * @status stable
107
+ * @related b.cliHelpers.makeReporter, b.cliHelpers.bootApp
108
+ *
109
+ * Resolve a passphrase from a CLI flag (preferred) or an env var
110
+ * (fallback) into a UTF-8 Buffer — the shape vault / crypto
111
+ * primitives accept. Returns `null` when neither source produced a
112
+ * non-empty string; the caller decides whether absence is a hard
113
+ * error (vault seal) or a soft default (plaintext-mode dev data dir).
116
114
  *
117
- * cli.resolvePassphrase(args, ctx, { flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE" })
118
- * Buffer | null
115
+ * @opts
116
+ * flag: string (CLI flag name, e.g. "passphrase" reads args.flags.passphrase),
117
+ * envVar: string (env var fallback, e.g. "BLAMEJS_VAULT_PASSPHRASE"),
119
118
  *
120
- * Returns `null` when neither source produced a non-empty string. The
121
- * caller decides whether absence is a hard error (vault seal) or a
122
- * soft default (operator chose plaintext-mode).
119
+ * @example
120
+ * var args = { flags: { passphrase: "hunter2" } };
121
+ * var ctx = { env: { BLAMEJS_VAULT_PASSPHRASE: "envval" } };
122
+ * var pp = b.cliHelpers.resolvePassphrase(args, ctx, {
123
+ * flag: "passphrase",
124
+ * envVar: "BLAMEJS_VAULT_PASSPHRASE",
125
+ * });
126
+ * pp.toString("utf8"); // → "hunter2" (flag wins over env)
127
+ *
128
+ * var none = b.cliHelpers.resolvePassphrase({ flags: {} }, { env: {} }, {
129
+ * flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE",
130
+ * });
131
+ * none; // → null
123
132
  */
124
133
  function resolvePassphrase(args, ctx, opts) {
125
134
  opts = opts || {};
@@ -143,26 +152,48 @@ function resolvePassphrase(args, ctx, opts) {
143
152
  // ---- Headless app bootstrap ----------------------------------------------
144
153
 
145
154
  /**
146
- * Boot a serverless `b.createApp` instance from a data dir so a CLI
147
- * script (the framework's own subcommands or operator-written tools)
148
- * can operate against the same vault + DB + audit chain the live app
149
- * uses, without standing up an HTTP listener.
155
+ * @primitive b.cliHelpers.bootApp
156
+ * @signature b.cliHelpers.bootApp(opts)
157
+ * @since 0.8.0
158
+ * @status stable
159
+ * @related b.cliHelpers.makeReporter, b.cliHelpers.resolvePassphrase
150
160
  *
151
- * var booted = await cli.bootApp({
152
- * dataDir: "./data",
153
- * vaultMode: "wrapped", // or "plaintext"; default "wrapped"
154
- * env: process.env, // BLAMEJS_VAULT_PASSPHRASE read from here
155
- * });
156
- * // booted.b the framework module
157
- * // booted.app the headless app instance (call .shutdown() to clean up)
161
+ * Boot a headless `b.createApp` instance from a data dir so a CLI
162
+ * script (framework subcommand or operator-written tool) can operate
163
+ * against the same vault + DB + audit chain the live app uses, with
164
+ * no HTTP listener attached. Returns `{ b, app }` where `b` is the
165
+ * framework module and `app` is the headless instance — caller MUST
166
+ * `await booted.app.shutdown()` in a `finally` so SQLite file handles
167
+ * and the cluster lease release.
168
+ *
169
+ * The default DB at-rest mode is `plain` because CLI runs are
170
+ * short-lived ops that never serve requests; encrypted-at-rest needs
171
+ * a tmpfs handle that wouldn't survive CLI exit anyway. Operators
172
+ * running against a production data dir whose DB is encrypted-at-rest
173
+ * pass `dbAtRest: "encrypted"` and ensure `BLAMEJS_TMPDIR` is set.
158
174
  *
159
- * Caller MUST call `await booted.app.shutdown()` in a `finally` so the
160
- * SQLite file handles + cluster lease release. The default DB at-rest
161
- * mode is `plain` because CLI runs are short-lived ops that never
162
- * serve requests; the encrypted-at-rest mode needs a tmpfs handle that
163
- * wouldn't survive the CLI exit anyway. Operators running against a
164
- * production data dir whose DB is encrypted-at-rest set
165
- * `dbAtRest: "encrypted"` and ensure `BLAMEJS_TMPDIR` is set.
175
+ * @opts
176
+ * dataDir: string (filesystem path to the data dir; required),
177
+ * vaultMode: "wrapped" | "plaintext" (default "wrapped" wrapped
178
+ * reads BLAMEJS_VAULT_PASSPHRASE from `opts.env`),
179
+ * dbAtRest: "plain" | "encrypted" (default "plain"),
180
+ * env: object (env-var bag; default process.env),
181
+ *
182
+ * @example
183
+ * async function run() {
184
+ * var booted;
185
+ * try {
186
+ * booted = await b.cliHelpers.bootApp({
187
+ * dataDir: "./data",
188
+ * vaultMode: "plaintext",
189
+ * env: process.env,
190
+ * });
191
+ * var rows = await booted.app.db.all("SELECT count(*) AS n FROM _blamejs_audit_log");
192
+ * return rows[0].n;
193
+ * } finally {
194
+ * if (booted) await booted.app.shutdown();
195
+ * }
196
+ * }
166
197
  */
167
198
  async function bootApp(opts) {
168
199
  opts = opts || {};
package/lib/cli.js CHANGED
@@ -38,6 +38,7 @@ var fs = require("node:fs");
38
38
  var os = require("node:os");
39
39
  var path = require("path");
40
40
  var apiSnapshot = require("./api-snapshot");
41
+ var argParser = require("./arg-parser");
41
42
  var auditChain = require("./audit-chain");
42
43
  var auditTools = require("./audit-tools");
43
44
  var backup = require("./backup");
@@ -75,37 +76,12 @@ function _writeLine(stream, line) {
75
76
 
76
77
  // Minimal argv parser: positional args + flag map. Supports both
77
78
  // `--flag value` and `--flag=value`. Single-dash forms (-v) treated
78
- // as long aliases to keep the surface predictable.
79
+ // as long aliases to keep the surface predictable. Routed through the
80
+ // reusable b.argParser.parseRaw primitive — every subcommand's hand-
81
+ // written flag validation continues to read the same { pos, flags }
82
+ // shape the cli has always exposed.
79
83
  function _parseArgs(argv) {
80
- var pos = [];
81
- var flags = {};
82
- for (var i = 0; i < argv.length; i++) {
83
- var tok = argv[i];
84
- if (tok === "--") {
85
- for (var j = i + 1; j < argv.length; j++) pos.push(argv[j]);
86
- break;
87
- }
88
- if (tok.indexOf("--") === 0) {
89
- var name = tok.slice(2);
90
- var eq = name.indexOf("=");
91
- var val;
92
- if (eq !== -1) {
93
- val = name.slice(eq + 1);
94
- name = name.slice(0, eq);
95
- } else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0) {
96
- val = argv[++i];
97
- } else {
98
- val = true; // boolean flag
99
- }
100
- flags[name] = val;
101
- } else if (tok.indexOf("-") === 0 && tok.length === 2) {
102
- // -v, -h
103
- flags[tok.slice(1)] = true;
104
- } else {
105
- pos.push(tok);
106
- }
107
- }
108
- return { pos: pos, flags: flags };
84
+ return argParser.parseRaw(argv);
109
85
  }
110
86
 
111
87
  function _resolvePath(p, cwd) {
@@ -1,41 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * CloudEvents 1.0 envelope (cloudevents.io/spec/v1.0).
3
+ * @module b.cloudEvents
4
+ * @nav Communication
5
+ * @title CloudEvents
4
6
  *
5
- * A vendor-neutral event-format spec adopted by AWS EventBridge,
6
- * Knative, Azure Event Grid, Google Eventarc, Datadog, and the
7
- * CNCF event ecosystem. Operators wrap outbound events from
8
- * webhook / pubsub / queue boundaries to interop with these
9
- * consumers without each consumer having to learn a bespoke shape.
7
+ * @intro
8
+ * Produce and consume webhook / pubsub / queue payloads in the
9
+ * framework-neutral CNCF CloudEvents v1.0 schema
10
+ * (cloudevents.io/spec/v1.0). The spec is adopted by AWS
11
+ * EventBridge, Azure Event Grid, Google Eventarc, Knative, Datadog,
12
+ * and the wider CNCF ecosystem — wrapping outbound events at
13
+ * `b.webhook` / `b.pubsub` / `b.queue` boundaries lets operators
14
+ * interop with these consumers without each consumer learning a
15
+ * bespoke shape.
10
16
  *
11
- * var ce = b.cloudEvents.wrap({
12
- * source: "/services/orders",
13
- * type: "com.example.order.created",
14
- * subject: "order/o-1234",
15
- * data: { id: "o-1234", total: 4250 },
16
- * });
17
- * // {
18
- * // specversion: "1.0",
19
- * // id: "<auto-uuid-v4>",
20
- * // source: "/services/orders",
21
- * // type: "com.example.order.created",
22
- * // time: "2026-05-06T...",
23
- * // subject: "order/o-1234",
24
- * // datacontenttype: "application/json",
25
- * // data: { id: "o-1234", total: 4250 },
26
- * // }
27
- *
28
- * var ce = b.cloudEvents.parse(envelope); // throws on shape violation
29
- *
30
- * Spec compliance — REQUIRED attributes (CloudEvents §3.1):
31
- * id, source, specversion, type
17
+ * `wrap` produces a structured-mode envelope from operator-supplied
18
+ * `source` / `type` / `subject` / `data` (and optional `extensions`),
19
+ * auto-filling `id` (UUID v4) and `time` (ISO 8601). Buffer payloads
20
+ * are routed to the `data_base64` field with a default
21
+ * `application/octet-stream` content-type; non-Buffer payloads land
22
+ * in `data` with `application/json`. `parse` validates a received
23
+ * envelope against the §3.1 required-attribute set, refuses unknown
24
+ * `specversion` values and the illegal `data` + `data_base64`
25
+ * simultaneous form, decodes base64-mode payloads back to a Buffer,
26
+ * and surfaces operator-defined extension attributes separately so
27
+ * consumers can route on them without grepping the envelope.
32
28
  *
33
- * OPTIONAL attributes:
34
- * datacontenttype, dataschema, subject, time, data, data_base64
29
+ * Extension-attribute names follow the §3.1 naming rules
30
+ * (lowercase ASCII alnum, 1..20 chars). Names that collide with a
31
+ * spec attribute are refused.
35
32
  *
36
- * Operator-defined extension attributes are passed through unchanged
37
- * if they conform to the spec's naming rules (lowercase ASCII letters
38
- * + digits, length 1–20).
33
+ * @card
34
+ * Produce and consume webhook / pubsub / queue payloads in the framework-neutral CNCF CloudEvents v1.0 schema (cloudevents.io/spec/v1.0).
39
35
  */
40
36
 
41
37
  var nodeCrypto = require("crypto");
@@ -74,6 +70,47 @@ function _genId() {
74
70
 
75
71
  // ---- wrap ----
76
72
 
73
+ /**
74
+ * @primitive b.cloudEvents.wrap
75
+ * @signature b.cloudEvents.wrap(opts)
76
+ * @since 0.7.45
77
+ * @status stable
78
+ * @related b.cloudEvents.parse, b.webhook.signer, b.pubsub.create
79
+ *
80
+ * Produces a CloudEvents v1.0 structured-mode envelope from
81
+ * `opts.source` + `opts.type` (the only required inputs). `id` is
82
+ * auto-filled with a UUID v4 when absent; `time` is auto-filled with
83
+ * the current ISO 8601 timestamp. Buffer `data` is base64-encoded
84
+ * into `data_base64` with `application/octet-stream`; non-Buffer
85
+ * `data` lands in the `data` attribute with `application/json`.
86
+ * Extension keys must match `[a-z0-9]{1,20}` and must not collide
87
+ * with a spec attribute — both refusals throw `CloudEventsError` at
88
+ * config time.
89
+ *
90
+ * @opts
91
+ * {
92
+ * source: string, // required; URI-reference per §3.1
93
+ * type: string, // required; reverse-DNS recommended
94
+ * id?: string, // default UUID v4
95
+ * time?: string, // default new Date().toISOString()
96
+ * subject?: string,
97
+ * datacontenttype?: string, // auto: application/json | application/octet-stream
98
+ * dataschema?: string, // URI of payload schema
99
+ * data?: object|Buffer, // Buffer routes to data_base64
100
+ * extensions?: object // keys [a-z0-9]{1,20}, no spec collisions
101
+ * }
102
+ *
103
+ * @example
104
+ * var b = require("blamejs").create();
105
+ * var ce = b.cloudEvents.wrap({
106
+ * source: "/services/orders",
107
+ * type: "com.example.order.created",
108
+ * subject: "order/o-1234",
109
+ * data: { id: "o-1234", total: 4250 }
110
+ * });
111
+ * ce.specversion;
112
+ * // → "1.0"
113
+ */
77
114
  function wrap(opts) {
78
115
  validateOpts.requireObject(opts, "cloudEvents.wrap", CloudEventsError);
79
116
  validateOpts.requireNonEmptyString(opts.source,
@@ -140,6 +177,36 @@ function wrap(opts) {
140
177
 
141
178
  // ---- parse ----
142
179
 
180
+ /**
181
+ * @primitive b.cloudEvents.parse
182
+ * @signature b.cloudEvents.parse(envelope)
183
+ * @since 0.7.45
184
+ * @status stable
185
+ * @related b.cloudEvents.wrap, b.webhook.verifier
186
+ *
187
+ * Validates a received CloudEvents v1.0 envelope and returns a
188
+ * normalized record `{ specversion, id, source, type, time, subject,
189
+ * datacontenttype, dataschema, data, extensions }`. Throws
190
+ * `CloudEventsError` for missing required attributes (§3.1),
191
+ * unsupported `specversion`, the illegal simultaneous `data` +
192
+ * `data_base64` form (§3.1.1), and base64-decoding failures.
193
+ * Buffer-mode payloads (`data_base64`) are decoded back to a
194
+ * `Buffer`; operator-defined extension attributes are surfaced under
195
+ * `.extensions` so routing can branch on them without scanning the
196
+ * envelope.
197
+ *
198
+ * @example
199
+ * var b = require("blamejs").create();
200
+ * var record = b.cloudEvents.parse({
201
+ * specversion: "1.0",
202
+ * id: "evt-1",
203
+ * source: "/services/orders",
204
+ * type: "com.example.order.created",
205
+ * data: { id: "o-1234", total: 4250 }
206
+ * });
207
+ * record.type;
208
+ * // → "com.example.order.created"
209
+ */
143
210
  function parse(envelope) {
144
211
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
145
212
  throw new CloudEventsError("cloud-events/bad-envelope",