@blamejs/core 0.8.42 → 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 +93 -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/ai-pref.js CHANGED
@@ -1,48 +1,29 @@
1
1
  "use strict";
2
2
  /**
3
- * b.aiPref — IETF AIPREF Working Group Content-Usage HTTP response
4
- * header + robots.txt grammar + Cloudflare Content Signals Policy +
5
- * Pay-Per-Crawl (HTTP 402) coordination.
6
- *
7
- * IETF AIPREF (Authors / Information Providers' Preference for AI
8
- * Use) draft-ietf-aipref-attach-04 (deadline 2026-08) defines a
9
- * machine-readable Content-Usage HTTP response header that signals
10
- * the operator's AI-training / AI-inference / AI-snippet preferences
11
- * to crawlers. Cloudflare's Content Signals Policy + Pay-Per-Crawl
12
- * (HTTP 402) is the de-facto baseline that Cloudflare adopted ahead
13
- * of the IETF spec finalizing.
14
- *
15
- * Public API:
16
- *
17
- * b.aiPref.middleware(opts) -> middleware(req, res, next)
18
- * opts:
19
- * train: "allow" | "deny" | "paid" — default "deny"
20
- * infer: "allow" | "deny" | "paid" default "allow"
21
- * snippet: "allow" | "deny" default "allow"
22
- * price: { amountUsd, perTokens? } when any of
23
- * train/infer is "paid".
24
- * cloudflareSignals: bool, default true — emit the Cloudflare
25
- * Content-Signals header alongside Content-Usage.
26
- * robotsContext: "default" | "<user-agent>" emit
27
- * per-user-agent rules in robots.txt rather
28
- * than the catch-all default.
29
- *
30
- * b.aiPref.robotsBlock(opts) -> string
31
- * Returns a robots.txt block per AIPREF §3 grammar:
32
- *
33
- * User-agent: GPTBot
34
- * Content-Usage: train=deny, infer=allow, snippet=allow
35
- *
36
- * b.aiPref.serializeHeader(opts) -> string
37
- * Returns the Content-Usage HTTP response header value.
38
- *
39
- * b.aiPref.parseHeader(value) -> { train, infer, snippet, price? }
40
- * Parses an inbound Content-Usage header (used when the framework
41
- * plays the role of crawler: respect declared preferences).
42
- *
43
- * b.aiPref.refusePaidCrawl(req, res, opts)
44
- * Convenience: emits HTTP 402 Payment Required with the price
45
- * manifest in the Cloudflare-compatible JSON body.
3
+ * @module b.aiPref
4
+ * @nav AI
5
+ * @title Ai Pref
6
+ *
7
+ * @intro
8
+ * AIPREF (RFC draft) signal operators publish a machine-readable
9
+ * preference about AI training / agent crawling / etc.
10
+ *
11
+ * Wires three coordinating surfaces into one primitive: the IETF
12
+ * AIPREF `Content-Usage` HTTP response header
13
+ * (draft-ietf-aipref-attach-04, deadline 2026-08), the matching
14
+ * robots.txt grammar, and Cloudflare's Content Signals Policy +
15
+ * Pay-Per-Crawl (HTTP 402). Operators declare train / infer /
16
+ * snippet preferences once; the middleware emits both the
17
+ * `Content-Usage` header and Cloudflare's `CF-Content-Signals`
18
+ * alongside.
19
+ *
20
+ * Inbound parsing closes the loop when the framework plays the role
21
+ * of crawler`parseHeader` decodes a peer's preferences so the
22
+ * caller can refuse training / pay the per-crawl price / respect a
23
+ * snippet=deny.
24
+ *
25
+ * @card
26
+ * AIPREF (RFC draft) signal — operators publish a machine-readable preference about AI training / agent crawling / etc.
46
27
  */
47
28
 
48
29
  var audit = require("./audit");
@@ -80,6 +61,38 @@ function _validate(opts) {
80
61
  return { train: train, infer: infer, snippet: snippet, price: opts.price || null };
81
62
  }
82
63
 
64
+ /**
65
+ * @primitive b.aiPref.serializeHeader
66
+ * @signature b.aiPref.serializeHeader(opts)
67
+ * @since 0.8.44
68
+ * @related b.aiPref.middleware, b.aiPref.parseHeader, b.aiPref.robotsBlock
69
+ *
70
+ * Render the AIPREF `Content-Usage` HTTP response header value from
71
+ * an operator preference object. Output is an RFC 8941 structured-
72
+ * fields list of `train=...`, `infer=...`, `snippet=...` pairs, plus
73
+ * `price-usd` / `per-tokens` when any axis is `paid`. Throws when the
74
+ * preferences are inconsistent (e.g. `train=paid` with no price).
75
+ *
76
+ * @opts
77
+ * train: "allow" | "deny" | "paid", // default "deny"
78
+ * infer: "allow" | "deny" | "paid", // default "allow"
79
+ * snippet: "allow" | "deny", // default "allow"
80
+ * price: { amountUsd: number, perTokens?: number },
81
+ *
82
+ * @example
83
+ * var v = b.aiPref.serializeHeader({
84
+ * train: "deny",
85
+ * infer: "allow",
86
+ * snippet: "allow",
87
+ * });
88
+ * // → "train=deny, infer=allow, snippet=allow"
89
+ *
90
+ * var paid = b.aiPref.serializeHeader({
91
+ * train: "paid", infer: "paid", snippet: "allow",
92
+ * price: { amountUsd: 0.001, perTokens: 1000 },
93
+ * });
94
+ * // → "train=paid, infer=paid, snippet=allow, price-usd=0.001000, per-tokens=1000"
95
+ */
83
96
  function serializeHeader(opts) {
84
97
  var v = _validate(opts);
85
98
  // RFC 8941 structured-fields list of token=token pairs. AIPREF §4.2.
@@ -97,6 +110,33 @@ function serializeHeader(opts) {
97
110
  return parts.join(", ");
98
111
  }
99
112
 
113
+ /**
114
+ * @primitive b.aiPref.parseHeader
115
+ * @signature b.aiPref.parseHeader(value)
116
+ * @since 0.8.44
117
+ * @related b.aiPref.serializeHeader, b.aiPref.middleware
118
+ *
119
+ * Parse an inbound `Content-Usage` header value into the typed
120
+ * preference shape. Used when the framework acts as a crawler and
121
+ * must respect a publisher's declared preferences. Unknown axes are
122
+ * dropped silently so a forward-compatible publisher can advertise
123
+ * future fields without breaking older clients. Throws when the
124
+ * value is missing or exceeds the 1024-char defensive cap.
125
+ *
126
+ * @example
127
+ * var p = b.aiPref.parseHeader(
128
+ * "train=deny, infer=allow, snippet=allow"
129
+ * );
130
+ * p.train; // → "deny"
131
+ * p.infer; // → "allow"
132
+ * p.snippet; // → "allow"
133
+ *
134
+ * var paid = b.aiPref.parseHeader(
135
+ * "train=paid, infer=allow, snippet=allow, price-usd=0.001000, per-tokens=1000"
136
+ * );
137
+ * paid.price.amountUsd; // → 0.001
138
+ * paid.price.perTokens; // → 1000
139
+ */
100
140
  function parseHeader(value) {
101
141
  if (typeof value !== "string" || value.length === 0) {
102
142
  throw AiPrefError.factory("BAD_HEADER", "aiPref.parseHeader: value required");
@@ -127,6 +167,35 @@ function parseHeader(value) {
127
167
  return out;
128
168
  }
129
169
 
170
+ /**
171
+ * @primitive b.aiPref.robotsBlock
172
+ * @signature b.aiPref.robotsBlock(opts)
173
+ * @since 0.8.44
174
+ * @related b.aiPref.serializeHeader, b.aiPref.middleware
175
+ *
176
+ * Render an AIPREF §3 robots.txt block: a `User-agent:` line followed
177
+ * by a `Content-Usage:` line carrying the same grammar as the HTTP
178
+ * header. Authors who serve robots.txt as a static file paste the
179
+ * output verbatim. The `userAgent` opt defaults to the catch-all `*`;
180
+ * pass `"GPTBot"` / `"ClaudeBot"` / etc. for per-crawler rules. UA
181
+ * strings are capped at 256 chars.
182
+ *
183
+ * @opts
184
+ * train: "allow" | "deny" | "paid",
185
+ * infer: "allow" | "deny" | "paid",
186
+ * snippet: "allow" | "deny",
187
+ * price: { amountUsd: number, perTokens?: number },
188
+ * userAgent: string, // default "*"
189
+ *
190
+ * @example
191
+ * var block = b.aiPref.robotsBlock({
192
+ * userAgent: "GPTBot",
193
+ * train: "deny",
194
+ * infer: "allow",
195
+ * snippet: "allow",
196
+ * });
197
+ * // → "User-agent: GPTBot\nContent-Usage: train=deny, infer=allow, snippet=allow\n"
198
+ */
130
199
  function robotsBlock(opts) {
131
200
  var v = _validate(opts);
132
201
  var ua = opts.userAgent || "*";
@@ -152,6 +221,34 @@ function _cfSignalsHeader(v) {
152
221
  return parts.join("; ");
153
222
  }
154
223
 
224
+ /**
225
+ * @primitive b.aiPref.middleware
226
+ * @signature b.aiPref.middleware(opts)
227
+ * @since 0.8.44
228
+ * @related b.aiPref.serializeHeader, b.aiPref.refusePaidCrawl, b.aiPref.robotsBlock
229
+ *
230
+ * Build an HTTP middleware that emits `Content-Usage` (and, by
231
+ * default, the Cloudflare `CF-Content-Signals` mirror) on every
232
+ * response. Wires the operator's AI-training / inference / snippet
233
+ * preferences into the request lifecycle so every page advertises
234
+ * the same posture without per-route plumbing.
235
+ *
236
+ * @opts
237
+ * train: "allow" | "deny" | "paid",
238
+ * infer: "allow" | "deny" | "paid",
239
+ * snippet: "allow" | "deny",
240
+ * price: { amountUsd: number, perTokens?: number },
241
+ * cloudflareSignals: boolean, // default true
242
+ *
243
+ * @example
244
+ * var aiPrefMw = b.aiPref.middleware({
245
+ * train: "deny",
246
+ * infer: "allow",
247
+ * snippet: "allow",
248
+ * });
249
+ * // mount aiPrefMw on every public route — emits Content-Usage +
250
+ * // CF-Content-Signals headers on each response.
251
+ */
155
252
  function middleware(opts) {
156
253
  var v = _validate(opts);
157
254
  var emitCf = opts.cloudflareSignals !== false;
@@ -167,6 +264,32 @@ function middleware(opts) {
167
264
  };
168
265
  }
169
266
 
267
+ /**
268
+ * @primitive b.aiPref.refusePaidCrawl
269
+ * @signature b.aiPref.refusePaidCrawl(req, res, opts)
270
+ * @since 0.8.44
271
+ * @related b.aiPref.middleware, b.aiPref.serializeHeader
272
+ *
273
+ * Emit HTTP 402 Payment Required with the price manifest in the
274
+ * Cloudflare-compatible JSON body. Operator route handlers detect
275
+ * an unmonetized AI crawler (via UA / signed-token absence / etc.)
276
+ * and call this helper to surface the price + contact channel
277
+ * uniformly. Audits the refusal under
278
+ * `aipref.paid_crawl_refused`.
279
+ *
280
+ * @opts
281
+ * price: { amountUsd: number, perTokens?: number },
282
+ * contact: string, // optional pricing contact
283
+ *
284
+ * @example
285
+ * function handler(req, res) {
286
+ * b.aiPref.refusePaidCrawl(req, res, {
287
+ * price: { amountUsd: 0.005, perTokens: 1000 },
288
+ * contact: "https://example.test/ai-licensing",
289
+ * });
290
+ * }
291
+ * // → res.statusCode === 402; body is JSON { error: "payment_required", ... }
292
+ */
170
293
  function refusePaidCrawl(req, res, opts) {
171
294
  if (!opts || !opts.price || typeof opts.price.amountUsd !== "number") {
172
295
  throw AiPrefError.factory("BAD_PRICE",
package/lib/api-key.js CHANGED
@@ -1,56 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * b.apiKey — operator-facing API-key issuance, verification, revocation,
4
- * and rotation.
3
+ * @module b.apiKey
4
+ * @nav Identity
5
+ * @title API Keys
5
6
  *
6
- * var keys = b.apiKey.create({
7
- * namespace: "live",
8
- * audit: b.audit, // optional
9
- * trackLastUsedAt: false, // default
10
- * });
11
- *
12
- * var issued = await keys.issue({
13
- * ownerId: "user-42",
14
- * scopes: ["read:users", "write:posts"],
15
- * metadata: { name: "Mobile app v3" },
16
- * expiresAt: Date.now() + b.constants.TIME.days(90),
17
- * });
18
- * // issued.key — "bk_live_<idHex>_<secretHex>" (returned ONCE)
19
- * // issued.id — "<idHex>"
20
- *
21
- * var record = await keys.verify(req.headers["x-api-key"]);
22
- * // → { id, ownerId, scopes, metadata, ... } or null
23
- *
24
- * await keys.revoke(id);
25
- * var rotated = await keys.rotate(id); // new secret; old stops working
26
- * var owned = await keys.listForOwner("user-42");
27
- *
28
- * Token format (Stripe-style, prefix-recognizable):
29
- *
30
- * <prefix>_<namespace>_<idHex>_<secretHex>
31
- *
32
- * Example: `bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d`
7
+ * @intro
8
+ * Long-lived API token primitives — generate / verify / revoke /
9
+ * rotate; sealed at rest; per-key scope + rate-limit. Tokens are
10
+ * Stripe-style prefix-recognizable strings of the form
11
+ * `<prefix>_<namespace>_<idHex>_<secretHex>` so a leaked credential
12
+ * is identifiable on sight (secret-scanner allowlists, log-grep
13
+ * for `bk_live_`).
33
14
  *
34
- * - prefix operator-supplied; default "bk". Visual marker.
35
- * - namespace operator-supplied; lets multiple key registries coexist
36
- * (e.g. "live"/"test", "v1"/"v2") without collision.
37
- * - idHex opaque random hex; PRIMARY KEY component (DB lookup).
38
- * - secretHex opaque random hex; never re-derivable. Stored as
39
- * SHA3-512 hash, constant-time-compared on verify.
15
+ * Storage: framework table `_blamejs_api_keys` with sealed columns
16
+ * (ownerId / scopes / metadata via cryptoField), `ownerIdHash` for
17
+ * indexed `listForOwner`. Same dual-storage pattern as sessions —
18
+ * local SQLite in single-node mode, external-db in cluster mode,
19
+ * dispatched via cluster-storage. Hash algorithm is operator-
20
+ * selectable (SHAKE256 default for high-entropy random secrets;
21
+ * Argon2id available for low-entropy deployments). Visibility
22
+ * defaults are ON: `auditFailures`, `auditSuccess`, and
23
+ * `trackLastUsedAt` all default true so HIPAA §164.312(b) /
24
+ * PCI-DSS 10.2.1 / GDPR Art. 32 trails are complete out of the
25
+ * box. Operators with extreme verify-rate volume opt OUT
26
+ * explicitly.
40
27
  *
41
- * Storage: framework table `_blamejs_api_keys` (sealed columns:
42
- * ownerId/scopes/metadata; ownerIdHash for indexed listForOwner).
43
- * Same dual-storage pattern as sessions local SQLite in single-node
44
- * mode, external-db in cluster mode, dispatched via cluster-storage.
28
+ * Graceful rotation moves the prior secret hash into a
29
+ * `secondarySecretHash` slot with a TTL (default 7 days) so
30
+ * in-flight clients survive the rotation window without coordinated
31
+ * redeploy.
45
32
  *
46
- * Validation policy:
47
- *
48
- * - apiKey.create opts → throw at config time
49
- * - registry.issue opts → throw ApiKeyError at call site
50
- * - registry.rotate(id) on missing/revoked → throw ApiKeyError at call site
51
- * - registry.verify(token) on any failure → return null (tolerant read)
52
- * - registry.revoke(id) on missing → return false (tolerant read)
53
- * - registry.getById(id) on missing → return null (tolerant read)
33
+ * @card
34
+ * Long-lived API token primitives — generate / verify / revoke / rotate; sealed at rest; per-key scope + rate-limit.
54
35
  */
55
36
 
56
37
  var crypto = require("./crypto");
@@ -179,6 +160,30 @@ function _validateIssueOpts(opts) {
179
160
  // Each part is alphanumeric so split-by-underscore is unambiguous as long
180
161
  // as prefix/namespace are validated to contain no underscores. We verify
181
162
  // that during create.
163
+
164
+ /**
165
+ * @primitive b.apiKey.parseFormat
166
+ * @signature b.apiKey.parseFormat(token)
167
+ * @since 0.4.9
168
+ * @status stable
169
+ * @related b.apiKey.create
170
+ *
171
+ * Pure parser for the framework's `<prefix>_<namespace>_<idHex>_<secretHex>`
172
+ * token format. Returns `{ prefix, namespace, idHex, secretHex }` on
173
+ * a structurally-valid token, `null` otherwise. Never touches the
174
+ * registry — used by routing code that wants to dispatch a request
175
+ * to the correct registry (multi-namespace deployments) before
176
+ * calling `verify()`. Hex parts are not constant-time-compared here;
177
+ * that happens inside `verify()` against the stored hash.
178
+ *
179
+ * @example
180
+ * var parts = b.apiKey.parseFormat("bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b");
181
+ * // → { prefix: "bk", namespace: "live", idHex: "5b9e7c8a4f2d1e3a",
182
+ * // secretHex: "8a7b6c5d4e3f2a1b" }
183
+ *
184
+ * b.apiKey.parseFormat("not-a-token"); // → null
185
+ * b.apiKey.parseFormat("bk_live_xyz_zzz"); // → null (non-hex)
186
+ */
182
187
  function parseFormat(token) {
183
188
  if (typeof token !== "string" || token.length === 0) return null;
184
189
  var parts = token.split("_");
@@ -209,6 +214,62 @@ function _sealForInsert(row) {
209
214
 
210
215
  // ---- Registry factory ----
211
216
 
217
+ /**
218
+ * @primitive b.apiKey.create
219
+ * @signature b.apiKey.create(opts)
220
+ * @since 0.4.9
221
+ * @status stable
222
+ * @compliance hipaa, pci-dss, gdpr, soc2
223
+ * @related b.apiKey.parseFormat, b.permissions.create, b.session
224
+ *
225
+ * Build an API-key registry bound to a single `namespace`. Returns a
226
+ * handle exposing async `issue` / `verify` / `revoke` / `rotate` /
227
+ * `listForOwner` / `getById` / `purgeExpired`. State changes
228
+ * (`issue` / `revoke` / `rotate` / `purgeExpired`) require leader in
229
+ * cluster mode; reads (`verify` / `getById` / `listForOwner`) run on
230
+ * any node. Issued tokens contain the secret material exactly once —
231
+ * the registry persists only the SHAKE256 / Argon2id hash and a
232
+ * scrub-safe record without secrets. Operators with multiple key
233
+ * lifecycles (e.g. `live` / `test`) instantiate one registry per
234
+ * namespace.
235
+ *
236
+ * @opts
237
+ * namespace: string, // registry namespace (required, no underscores / whitespace)
238
+ * prefix: string, // token prefix (default "bk", no underscores)
239
+ * idBytes: number, // bytes of id randomness (default 8 → 16 hex chars)
240
+ * secretBytes: number, // bytes of secret randomness (default 16 → 32 hex chars)
241
+ * trackLastUsedAt: boolean, // update lastUsedAt on verify success (default true)
242
+ * auditFailures: boolean, // emit verify-failure audits (default true)
243
+ * auditSuccess: boolean, // emit verify/list/get-success audits (default true)
244
+ * purgeAfterMs: number, // age threshold for purgeExpired (default 90 days)
245
+ * hashAlgo: string, // "shake256" (default) or "argon2id"
246
+ * audit: b.audit, // optional audit sink
247
+ * clock: function, // () → unix ms (test override)
248
+ *
249
+ * @example
250
+ * var keys = b.apiKey.create({
251
+ * namespace: "live",
252
+ * audit: b.audit,
253
+ * });
254
+ *
255
+ * var issued = await keys.issue({
256
+ * ownerId: "user-42",
257
+ * scopes: ["read:users", "write:posts"],
258
+ * metadata: { name: "Mobile app v3" },
259
+ * expiresAt: Date.now() + b.constants.TIME.days(90),
260
+ * });
261
+ * // issued.key — "bk_live_5b9e7c8a4f2d1e3a_8a7b6c5d4e3f2a1b" (returned ONCE)
262
+ *
263
+ * var record = await keys.verify(req.headers["x-api-key"]);
264
+ * if (!record) return res.writeHead(401).end();
265
+ * // → { id, ownerId, scopes, metadata, lastUsedAt, ... }
266
+ *
267
+ * // Graceful rotation — old secret keeps working for 7 days:
268
+ * var rotated = await keys.rotate(issued.id, { graceful: true });
269
+ *
270
+ * await keys.revoke(issued.id); // immediate cutover
271
+ * var owned = await keys.listForOwner("user-42");
272
+ */
212
273
  function create(opts) {
213
274
  opts = opts || {};
214
275
  validateOpts(opts, [
@@ -1,52 +1,53 @@
1
1
  "use strict";
2
2
  /**
3
- * api-snapshot — public API surface walker + breaking-change detector.
3
+ * @module b.apiSnapshot
4
+ * @nav Other
5
+ * @title API Snapshot
4
6
  *
5
- * The framework's LTS-contract enforcement at the type level. Walks
6
- * the framework's module exports recursively, records every member's
7
- * type, and compares two snapshots to find:
7
+ * @intro
8
+ * Public-API surface walker plus breaking-change detector the
9
+ * framework's LTS-contract enforcement at the type level. Operators
10
+ * capture a snapshot of the framework's `module.exports` tree,
11
+ * commit it alongside the version bump, and the release workflow's
12
+ * `check-api-snapshot.js` gate fails CI when any subsequent change
13
+ * removes or retypes a previously-shipped public member.
8
14
  *
9
- * - removed a member present in the old snapshot but not the new
10
- * (BREAKING — fails CI)
11
- * - typeChanged a member's category flipped (function → object, etc.)
12
- * (BREAKING — fails CI)
13
- * - added a new member that wasn't in the old snapshot
14
- * (ADDITIVE — does not fail; signals the snapshot is
15
- * out-of-date and the operator should rerun capture)
15
+ * Three diff classes:
16
16
  *
17
- * var snap = b.apiSnapshot.capture(require("@blamejs/core"));
18
- * // { version, frameworkVersion, createdAt,
19
- * // exports: { ... nested tree ... } }
17
+ * - `removed` a member present in the old snapshot but not
18
+ * the new (BREAKING — fails CI)
19
+ * - `typeChanged` a member's category flipped, e.g. function
20
+ * object, primitive → instance (BREAKING — fails
21
+ * CI)
22
+ * - `additive` a new member that wasn't in the old snapshot
23
+ * (informational — signals the snapshot is out
24
+ * of date and the operator should rerun capture)
20
25
  *
21
- * b.apiSnapshot.write(snap, "./api-snapshot.json");
22
- * var loaded = b.apiSnapshot.read("./api-snapshot.json");
26
+ * Walker rules: functions record as
27
+ * `{ type: "function", arity: fn.length }`; plain objects recurse
28
+ * into enumerable string keys; primitives record as
29
+ * `{ type: "primitive", valueType }` without capturing the literal
30
+ * value (so a version-string change in `b.version` doesn't fail
31
+ * CI); non-plain objects (Map, Set, Buffer, Date, RegExp, Error
32
+ * instances) record as `{ type: "instance", ctorName }` without
33
+ * recursion; cycles short-circuit as `{ type: "cycle" }`; depth is
34
+ * capped at `opts.maxDepth` (default 8). Members whose key starts
35
+ * with `_` are skipped — the framework convention for test seams
36
+ * and internal helpers.
23
37
  *
24
- * var diff = b.apiSnapshot.compare(loaded, snap);
25
- * // { breaking: [{ path, kind, was?, is? }],
26
- * // typeChanged: [{ path, was, is }],
27
- * // additive: [{ path, type }] }
38
+ * Function-arity changes: a DECREASE in `fn.length` is breaking
39
+ * (the operator removed a required parameter). An INCREASE is not
40
+ * flagged because adding an optional trailing parameter is additive
41
+ * to existing callers.
28
42
  *
29
- * if (diff.breaking.length > 0 || diff.typeChanged.length > 0) {
30
- * console.error(b.apiSnapshot.formatDiff(diff));
31
- * process.exit(1);
32
- * }
43
+ * On-disk format: stable canonical JSON ordered as
44
+ * `{ version, frameworkVersion, createdAt, exports }`. The format
45
+ * version (`b.apiSnapshot.SNAPSHOT_FORMAT_VERSION`) is checked on
46
+ * read so a future schema bump can't silently mis-compare against
47
+ * an older baseline.
33
48
  *
34
- * Walker rules:
35
- * - Functions record as { type: 'function', arity: fn.length }.
36
- * Class constructors are still 'function' — recursive scope walks
37
- * prototype only when the operator explicitly opts in via
38
- * opts.includeClassPrototypes.
39
- * - Plain objects recurse into their own enumerable string keys.
40
- * - Primitives (string, number, boolean, null, undefined) record as
41
- * { type: 'primitive', valueType: typeof v }. Specific values are
42
- * NOT captured — only the type — so a version-string change in
43
- * constants doesn't fail CI.
44
- * - Members whose key starts with '_' are skipped (test seams,
45
- * internal helpers).
46
- * - Cycles are detected and short-circuit as { type: 'cycle' }.
47
- * - Non-plain objects (Map, Set, Buffer, Date, RegExp, Error, etc.)
48
- * are recorded as { type: 'instance', constructor: name } without
49
- * recursion — they're terminal nodes.
49
+ * @card
50
+ * Public-API surface walker plus breaking-change detector — the framework's LTS-contract enforcement at the type level.
50
51
  */
51
52
 
52
53
  var fs = require("fs");
@@ -112,6 +113,34 @@ function _walkNode(value, depth, maxDepth, seen, skipUnderscore) {
112
113
  return { type: "object", members: members };
113
114
  }
114
115
 
116
+ /**
117
+ * @primitive b.apiSnapshot.capture
118
+ * @signature b.apiSnapshot.capture(target, opts)
119
+ * @since 0.1.91
120
+ * @status stable
121
+ * @related b.apiSnapshot.compare, b.apiSnapshot.write
122
+ *
123
+ * Walk a module's exports tree and produce a snapshot object
124
+ * `{ version, frameworkVersion, createdAt, exports }` suitable for
125
+ * round-tripping through `write` / `read`. The walk is recursive
126
+ * with cycle detection and a depth cap; underscore-prefixed keys
127
+ * are skipped by default (override with `skipUnderscore: false`).
128
+ * Throws `ApiSnapshotError` when the top-level target is not a plain
129
+ * object — class instances and runtime-built exports can't be walked
130
+ * by category.
131
+ *
132
+ * @opts
133
+ * maxDepth: 8, // recursion ceiling
134
+ * skipUnderscore: true, // skip `_internal` keys
135
+ * frameworkVersion: "0.8.48", // override target.version
136
+ * createdAt: "2026-05-09T...", // pin for deterministic snapshots
137
+ *
138
+ * @example
139
+ * var snap = b.apiSnapshot.capture(require("@blamejs/core"));
140
+ * // → { version: 1, frameworkVersion: "0.8.48",
141
+ * // createdAt: "2026-05-09T12:00:00.000Z",
142
+ * // exports: { uuid: { type: "object", members: {...} }, ... } }
143
+ */
115
144
  function capture(target, opts) {
116
145
  opts = opts || {};
117
146
  if (!target || typeof target !== "object") {
@@ -135,6 +164,25 @@ function capture(target, opts) {
135
164
  };
136
165
  }
137
166
 
167
+ /**
168
+ * @primitive b.apiSnapshot.write
169
+ * @signature b.apiSnapshot.write(snapshot, filePath)
170
+ * @since 0.1.91
171
+ * @status stable
172
+ * @related b.apiSnapshot.read, b.apiSnapshot.capture
173
+ *
174
+ * Serialize a snapshot to disk in canonical JSON form (stable
175
+ * `{ version, frameworkVersion, createdAt, exports }` ordering, mode
176
+ * 0o644). Returns the filePath written. Throws `ApiSnapshotError`
177
+ * when the snapshot or path is missing — the release workflow
178
+ * surfaces typos at commit time instead of writing to an unintended
179
+ * location.
180
+ *
181
+ * @example
182
+ * var snap = b.apiSnapshot.capture(require("@blamejs/core"));
183
+ * var written = b.apiSnapshot.write(snap, "./api-snapshot.json");
184
+ * // → "./api-snapshot.json"
185
+ */
138
186
  function write(snapshot, filePath) {
139
187
  if (!snapshot || typeof snapshot !== "object") {
140
188
  throw new ApiSnapshotError("api-snapshot/bad-snapshot",
@@ -155,6 +203,28 @@ function write(snapshot, filePath) {
155
203
  return filePath;
156
204
  }
157
205
 
206
+ /**
207
+ * @primitive b.apiSnapshot.read
208
+ * @signature b.apiSnapshot.read(filePath)
209
+ * @since 0.1.91
210
+ * @status stable
211
+ * @related b.apiSnapshot.write, b.apiSnapshot.compare
212
+ *
213
+ * Load a snapshot from disk and validate its envelope. Throws
214
+ * `ApiSnapshotError` with a specific code on each failure mode —
215
+ * `api-snapshot/missing` (no file), `api-snapshot/read-failed`
216
+ * (I/O error), `api-snapshot/bad-json` (parse failure), or
217
+ * `api-snapshot/bad-version` (format-version mismatch — the
218
+ * baseline was written by a different snapshot major) — so the
219
+ * release workflow can surface a precise reason instead of a
220
+ * generic "snapshot broken" message.
221
+ *
222
+ * @example
223
+ * var loaded = b.apiSnapshot.read("./api-snapshot.json");
224
+ * var current = b.apiSnapshot.capture(require("@blamejs/core"));
225
+ * var diff = b.apiSnapshot.compare(loaded, current);
226
+ * // → { breaking: [], additive: [], typeChanged: [] }
227
+ */
158
228
  function read(filePath) {
159
229
  if (typeof filePath !== "string" || filePath.length === 0) {
160
230
  throw new ApiSnapshotError("api-snapshot/bad-path",
@@ -277,6 +347,31 @@ function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged)
277
347
  // cycle / deep — terminal, nothing more to compare
278
348
  }
279
349
 
350
+ /**
351
+ * @primitive b.apiSnapshot.compare
352
+ * @signature b.apiSnapshot.compare(oldSnapshot, newSnapshot)
353
+ * @since 0.1.91
354
+ * @status stable
355
+ * @related b.apiSnapshot.formatDiff, b.apiSnapshot.capture
356
+ *
357
+ * Diff two snapshots and return
358
+ * `{ breaking, additive, typeChanged }`. `breaking` carries every
359
+ * member that was removed, retyped, lost arity, swapped its
360
+ * constructor name, or changed primitive `valueType`; the release
361
+ * workflow exits non-zero when this list is non-empty. `additive`
362
+ * lists new members (informational — operator should rerun
363
+ * `capture` and commit the refreshed baseline). `typeChanged` is a
364
+ * subset of `breaking` surfaced separately for easier triage.
365
+ *
366
+ * @example
367
+ * var loaded = b.apiSnapshot.read("./api-snapshot.json");
368
+ * var current = b.apiSnapshot.capture(require("@blamejs/core"));
369
+ * var diff = b.apiSnapshot.compare(loaded, current);
370
+ * if (diff.breaking.length > 0) {
371
+ * console.error(b.apiSnapshot.formatDiff(diff));
372
+ * process.exit(1);
373
+ * }
374
+ */
280
375
  function compare(oldSnapshot, newSnapshot) {
281
376
  if (!oldSnapshot || !oldSnapshot.exports) {
282
377
  throw new ApiSnapshotError("api-snapshot/bad-snapshot",
@@ -298,6 +393,28 @@ function compare(oldSnapshot, newSnapshot) {
298
393
  return { breaking: breaking, additive: additive, typeChanged: typeChanged };
299
394
  }
300
395
 
396
+ /**
397
+ * @primitive b.apiSnapshot.formatDiff
398
+ * @signature b.apiSnapshot.formatDiff(diff)
399
+ * @since 0.1.91
400
+ * @status stable
401
+ * @related b.apiSnapshot.compare
402
+ *
403
+ * Render a diff result from `compare` into a human-readable
404
+ * multi-line string suitable for `console.error` in a CI script.
405
+ * Breaking entries are flagged with `-`, additive entries with `+`,
406
+ * and the `was` / `is` types are JSON-quoted so the operator can
407
+ * paste the line verbatim into the migration notes.
408
+ *
409
+ * @example
410
+ * var diff = {
411
+ * breaking: [{ path: "uuid.v3", kind: "removed", was: "function" }],
412
+ * additive: [{ path: "uuid.v8", type: "function" }],
413
+ * typeChanged: [],
414
+ * };
415
+ * var rendered = b.apiSnapshot.formatDiff(diff);
416
+ * // → "[api-snapshot] BREAKING (1):\n - uuid.v3 (removed) was=\"function\"\n..."
417
+ */
301
418
  function formatDiff(diff) {
302
419
  if (!diff || typeof diff !== "object") {
303
420
  throw new ApiSnapshotError("api-snapshot/bad-diff",