@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/fdx.js CHANGED
@@ -1,7 +1,36 @@
1
1
  "use strict";
2
2
  /**
3
- * b.fdx — CFPB §1033 / Financial Data Exchange (FDX) consumer-
4
- * financial-data sharing wrapper.
3
+ * @module b.fdx
4
+ * @nav Compliance
5
+ * @title FDX
6
+ *
7
+ * @intro
8
+ * Financial Data Exchange (FDX) US open-banking standard —
9
+ * consent records, SHA3 transaction hashes, data-recipient
10
+ * registry.
11
+ *
12
+ * CFPB §1033 (12 CFR §1033.121-461, final rule 2024-10-22) gives
13
+ * US consumers the right to authorize a third party to access
14
+ * their financial data through a covered data provider's
15
+ * developer interface. FDX (https://financialdataexchange.org)
16
+ * is the industry-standard schema + protocol the CFPB rule
17
+ * effectively codifies (FDX 6.0+ aligns with the §1033 final
18
+ * rule). Compliance deadline 2026-04-01 already past for $250B+
19
+ * asset-size banks.
20
+ *
21
+ * The framework can't be the operator's authorization server,
22
+ * resource server, or FDX-data origin — those are the operator's
23
+ * core banking system. What it can do: bind the operator's auth
24
+ * server to the FAPI 2.0 profile (which §1033.351 effectively
25
+ * requires), validate FDX response shapes, emit §1033-shape
26
+ * audit events on every authorized data access, and generate
27
+ * the §1033.401(b) consent receipt.
28
+ *
29
+ * @card
30
+ * Financial Data Exchange (FDX) US open-banking standard — consent records, SHA3 transaction hashes, data-recipient registry.
31
+ */
32
+ /*
33
+ * Original prose retained:
5
34
  *
6
35
  * CFPB §1033 (12 CFR §1033.121-461, final rule 2024-10-22) gives US
7
36
  * consumers the right to authorize a third party to access their
@@ -95,6 +124,42 @@ var FDX_SCHEMAS = {
95
124
  },
96
125
  };
97
126
 
127
+ /**
128
+ * @primitive b.fdx.bind
129
+ * @signature b.fdx.bind(opts)
130
+ * @since 0.8.0
131
+ * @status stable
132
+ * @compliance fdx, fapi2
133
+ * @related b.fdx.validateResponse, b.fdx.consentReceipt, b.fapi2.assertOAuthConfig
134
+ *
135
+ * Bind an operator's authorization-server config to the FDX 6.0
136
+ * data-sharing surface and the FAPI 2.0 security profile. Refuses
137
+ * unknown FDX resources, refuses missing issuer/jwksUri, and runs
138
+ * `b.fapi2.assertOAuthConfig` on the supplied FAPI opts so a
139
+ * non-conformant deployment fails at boot. Returns a handle with
140
+ * `schemaValidator(resource, body)` and `consent.receipt(opts)`.
141
+ *
142
+ * @opts
143
+ * authServer: {
144
+ * issuer: string, // required, non-empty
145
+ * jwksUri: string, // required, non-empty
146
+ * fapi2: { pkce, dpop?, mtls?, par },
147
+ * },
148
+ * resources: ["accounts" | "transactions" | "statements" |
149
+ * "payment-networks" | "rewards" | "tax-forms"],
150
+ *
151
+ * @example
152
+ * var fdx = b.fdx.bind({
153
+ * authServer: {
154
+ * issuer: "https://auth.example-bank.com",
155
+ * jwksUri: "https://auth.example-bank.com/.well-known/jwks.json",
156
+ * fapi2: { pkce: true, mtls: true, par: true },
157
+ * },
158
+ * resources: ["accounts", "transactions"],
159
+ * });
160
+ * fdx.fapi2Posture;
161
+ * // → "fapi-2.0"
162
+ */
98
163
  function bind(opts) {
99
164
  if (!opts || typeof opts !== "object") {
100
165
  throw FdxError.factory("BAD_OPTS", "fdx.bind: opts required");
@@ -150,6 +215,36 @@ function bind(opts) {
150
215
  };
151
216
  }
152
217
 
218
+ /**
219
+ * @primitive b.fdx.validateResponse
220
+ * @signature b.fdx.validateResponse(resourceType, body)
221
+ * @since 0.8.0
222
+ * @status stable
223
+ * @compliance fdx
224
+ * @related b.fdx.bind
225
+ *
226
+ * Validate an FDX response payload against the framework's FDX 6.0
227
+ * minimum-required-field schema. Accepts both envelope form
228
+ * (`{ accounts: [...] }`) and bare-array / single-record form.
229
+ * Returns `{ valid, errors }` so the operator can decide whether
230
+ * to refuse, redact, or pass through. Throws `FdxError` only when
231
+ * `resourceType` is unknown.
232
+ *
233
+ * @example
234
+ * var result = b.fdx.validateResponse("accounts", {
235
+ * accounts: [{
236
+ * accountId: "acct-1",
237
+ * accountType: "DEPOSIT",
238
+ * accountNumberDisplay: "****1234",
239
+ * currency: "USD",
240
+ * currentBalance: 1234.56,
241
+ * }],
242
+ * });
243
+ * result.valid;
244
+ * // → true
245
+ * result.errors.length;
246
+ * // → 0
247
+ */
153
248
  function validateResponse(resourceType, body) {
154
249
  var schema = FDX_SCHEMAS[resourceType];
155
250
  if (!schema) {
@@ -181,6 +276,41 @@ function validateResponse(resourceType, body) {
181
276
  return { valid: errors.length === 0, errors: errors };
182
277
  }
183
278
 
279
+ /**
280
+ * @primitive b.fdx.consentReceipt
281
+ * @signature b.fdx.consentReceipt(opts)
282
+ * @since 0.8.0
283
+ * @status stable
284
+ * @compliance fdx
285
+ * @related b.fdx.bind
286
+ *
287
+ * Mint the §1033.401(b) consent receipt the authorization server
288
+ * gives the consumer at authorization time. Required fields:
289
+ * dataProvider, consumerRef, thirdParty, revocationUrl, scopes.
290
+ * Optional `durationMs` defaults to 52 weeks. Emits
291
+ * `fdx.consent_receipt_issued` to the audit chain so the regulator
292
+ * sees a record per receipt.
293
+ *
294
+ * @opts
295
+ * dataProvider: string, // issuer / data provider name
296
+ * consumerRef: string, // operator-side consumer identifier
297
+ * thirdParty: string, // recipient name
298
+ * revocationUrl: string, // public revocation endpoint
299
+ * scopes: [string, ...], // data scopes (account ids, resources)
300
+ * durationMs: number, // optional; defaults to 52 weeks
301
+ *
302
+ * @example
303
+ * var receipt = b.fdx.consentReceipt({
304
+ * dataProvider: "https://auth.example-bank.com",
305
+ * consumerRef: "consumer-42",
306
+ * thirdParty: "Aggregator Inc.",
307
+ * revocationUrl: "https://example-bank.com/consent/revoke/abc",
308
+ * scopes: ["accounts", "transactions"],
309
+ * durationMs: 365 * 86400 * 1000,
310
+ * });
311
+ * receipt.type;
312
+ * // → "fdx.consent-receipt"
313
+ */
184
314
  function consentReceipt(opts) {
185
315
  if (!opts || typeof opts !== "object") {
186
316
  throw FdxError.factory("BAD_OPTS", "fdx.consentReceipt: opts required");
package/lib/file-type.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ /**
3
+ * @module b.fileType
4
+ * @nav Validation
5
+ * @title File Type
6
+ *
7
+ * @intro
8
+ * Magic-byte content-type detection independent of the declared
9
+ * MIME. The Content-Type header on a multipart upload is supplied
10
+ * by the client; a hostile uploader can label a polyglot HTML
11
+ * payload as `image/png` and the header alone won't catch it.
12
+ * This primitive inspects the leading bytes against a hardcoded
13
+ * signature registry (PNG / JPEG / GIF / WEBP / AVIF / HEIC / PDF /
14
+ * OOXML / CFB / ZIP / RAR / 7Z / TAR / GZIP / BZ2 / XZ / MP3 / MP4
15
+ * / WEBM / PE / ELF / Mach-O) and returns the actual format.
16
+ *
17
+ * `detect(buf)` returns `null` rather than throwing on bad input
18
+ * (saved-for-later analysis often runs against partial reads);
19
+ * `assertOneOf(buf, allowlist)` throws `FileTypeError` when the
20
+ * detected format is not in the operator-supplied allowlist.
21
+ * Allowlist entries match against `mime` ("image/png"), short
22
+ * `name` ("png"), or `category` ("image") — operators pin the
23
+ * tightest level the use case allows.
24
+ *
25
+ * Out of scope: content disarm (CDR), polyglot detection, and
26
+ * filename-extension validation. Operators with disarm requirements
27
+ * reach for a sandbox like dangerzone or vmray; filename extensions
28
+ * live behind `b.guardFilename`. Operators extending the registry
29
+ * pass `opts.extra` to `detect` — extras come first, letting an
30
+ * operator override a built-in entry without forking.
31
+ *
32
+ * @card
33
+ * Magic-byte content-type detection independent of the declared MIME.
34
+ */
2
35
  /**
3
36
  * file-type — magic-byte content detection.
4
37
  *
@@ -206,6 +239,32 @@ function _entryMatches(entry, buf) {
206
239
  return true;
207
240
  }
208
241
 
242
+ /**
243
+ * @primitive b.fileType.detect
244
+ * @signature b.fileType.detect(buf, opts?)
245
+ * @since 0.1.0
246
+ * @related b.fileType.assertOneOf, b.fileUpload.create
247
+ *
248
+ * Inspects the leading bytes of `buf` against the signature registry
249
+ * and returns `{ mime, extension, category, name }` for the first
250
+ * matching entry, or `null` when no signature matches (or `buf` is
251
+ * empty, or not a Buffer/Uint8Array). OOXML entries (`docx`/`xlsx`/
252
+ * `pptx`) sit before generic ZIP so well-formed Office files win
253
+ * before the bare ZIP shape; operators extending the registry via
254
+ * `opts.extra` get their entries scanned first so they can override
255
+ * built-ins without forking.
256
+ *
257
+ * @opts
258
+ * extra: Array<{ name, mime, extension, category, offset, magic, extra? }>,
259
+ *
260
+ * @example
261
+ * var fs = require("node:fs");
262
+ * var buf = fs.readFileSync("photo.png");
263
+ * b.fileType.detect(buf);
264
+ * // → { mime: "image/png", extension: "png", category: "image", name: "png" }
265
+ *
266
+ * b.fileType.detect(Buffer.from("")); // → null
267
+ */
209
268
  function detect(buf, opts) {
210
269
  if (!Buffer.isBuffer(buf)) {
211
270
  if (buf instanceof Uint8Array) buf = Buffer.from(buf);
@@ -227,6 +286,34 @@ function detect(buf, opts) {
227
286
  return null;
228
287
  }
229
288
 
289
+ /**
290
+ * @primitive b.fileType.assertOneOf
291
+ * @signature b.fileType.assertOneOf(buf, allowlist, opts?)
292
+ * @since 0.1.0
293
+ * @related b.fileType.detect, b.fileUpload.create
294
+ *
295
+ * Detects the format of `buf` and throws `FileTypeError` when the
296
+ * result is not in `allowlist`. Allowlist entries match by `mime`
297
+ * ("image/png"), short `name` ("png"), or `category` ("image") so
298
+ * operators pin the tightest level the use case allows. Empty
299
+ * buffers throw `EMPTY` unless `opts.allowEmpty: true`. Unrecognized
300
+ * magic bytes throw `UNKNOWN_TYPE` — the framework refuses to fall
301
+ * back to the advertised header MIME because the entire purpose of
302
+ * this primitive is mistrusting that header.
303
+ *
304
+ * @opts
305
+ * allowEmpty: boolean, // default false
306
+ * extra: Array<{ name, mime, extension, category, offset, magic, extra? }>,
307
+ *
308
+ * @example
309
+ * var fs = require("node:fs");
310
+ * var buf = fs.readFileSync("photo.png");
311
+ * var detected = b.fileType.assertOneOf(buf, ["image/png", "image/jpeg"]);
312
+ * // → { mime: "image/png", extension: "png", category: "image", name: "png" }
313
+ *
314
+ * // Category-level allowlist (any image format)
315
+ * b.fileType.assertOneOf(buf, ["image"]);
316
+ */
230
317
  function assertOneOf(buf, allowlist, opts) {
231
318
  opts = opts || {};
232
319
  if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
@@ -1,4 +1,39 @@
1
1
  "use strict";
2
+ /**
3
+ * @module b.fileUpload
4
+ * @nav HTTP
5
+ * @title File Upload
6
+ *
7
+ * @intro
8
+ * Streaming multipart upload with content-safety guards wired on
9
+ * by default. Init / acceptChunk / finalize lifecycle: operator
10
+ * calls `init` to allocate per-upload staging, streams chunks via
11
+ * `acceptChunk` (each carrying its own SHA3-512 hex), then calls
12
+ * `finalize` with a manifest so the framework can verify per-chunk
13
+ * + total hash, sniff magic bytes against an `allowedFileTypes`
14
+ * allowlist, and hand off to the operator's `onFinalize` (buffer
15
+ * for small uploads, Readable stream above `maxStreamReassemblyBytes`).
16
+ *
17
+ * Default-on safety: `b.guardAll.byExtension({ profile: "strict" })`
18
+ * for content gating and `b.guardFilename.gate({ profile: "strict" })`
19
+ * for filename gating. Operators opt out via `contentSafety: null`
20
+ * / `filenameSafety: null` (audited at create time so a security
21
+ * review can find the disabled-on-deploy rows). Per-chunk hooks
22
+ * (`onChunk`) are the integration point for virus scanners and
23
+ * schema-shape checks; rejecting from the hook surfaces as a
24
+ * permanent `FileUploadError`.
25
+ *
26
+ * Quotas: `maxFileBytes`, `maxChunkBytes`, `maxStagingBytes`,
27
+ * `maxActiveUploadsPerActor`, `maxChunks`, `incompleteTtlMs`,
28
+ * `maxIdleMs`. `purgeIncomplete()` reclaims TTL'd / idle staging
29
+ * directories — operators wire it to `b.scheduler` for a cron-shaped
30
+ * sweep. Permission scopes (`fileUpload.init` / `accept` / `finalize`
31
+ * / `status` / `list` / `cancel`) are checked through `b.permissions`
32
+ * when wired.
33
+ *
34
+ * @card
35
+ * Streaming multipart upload with content-safety guards wired on by default.
36
+ */
2
37
  /**
3
38
  * b.fileUpload — chunked file upload primitive.
4
39
  *
@@ -282,6 +317,64 @@ function _validateCreateOpts(opts) {
282
317
  }
283
318
  }
284
319
 
320
+ /**
321
+ * @primitive b.fileUpload.create
322
+ * @signature b.fileUpload.create(opts)
323
+ * @since 0.7.2
324
+ * @related b.fileType.detect, b.fileType.assertOneOf
325
+ *
326
+ * Builds an upload manager bound to `opts.stagingDir`. The returned
327
+ * object exposes `init`, `acceptChunk`, `finalize`, `status`, `list`,
328
+ * `cancelUpload`, `purgeIncomplete`, and `close`. Uploads are written
329
+ * chunk-per-file under a per-upload directory (mode 0o700); finalize
330
+ * walks the manifest in order, verifies per-chunk + total SHA3-512,
331
+ * runs the magic-byte allowlist (when `allowedFileTypes` is set), and
332
+ * hands the assembled buffer (or a stream above `maxStreamReassemblyBytes`)
333
+ * to the operator's `onFinalize`.
334
+ *
335
+ * Per-chunk and per-upload audits flow through the wired `audit` and
336
+ * `observability` instances. Quota refusals, hash mismatches, MIME-claim
337
+ * disagreement, filename-safety refusal, and content-safety refusal all
338
+ * throw `FileUploadError` with `permanent: true` — no retry succeeds.
339
+ *
340
+ * @opts
341
+ * stagingDir: string, // absolute path; created mode 0o700 if missing
342
+ * maxFileBytes: number, // default 2 GiB
343
+ * maxChunkBytes: number, // default 8 MiB
344
+ * maxStreamReassemblyBytes: number, // above this finalize streams; default 64 MiB
345
+ * maxStagingBytes: number, // default 50 GiB
346
+ * maxActiveUploadsPerActor: number, // default 16
347
+ * maxChunks: number, // default 16384
348
+ * incompleteTtlMs: number, // since createdAt; default 24h
349
+ * maxIdleMs: number, // since lastChunkAt; default 30m
350
+ * allowedFileTypes: string[], // MIME allowlist; "image/*" wildcard supported
351
+ * audit: b.audit,
352
+ * observability: b.observability,
353
+ * permissions: b.permissions, // optional; gates init/accept/finalize/status/list/cancel
354
+ * fileType: b.fileType, // required when allowedFileTypes is non-empty
355
+ * contentSafety: Object | null, // ext→gate map; null = audited opt-out; undefined = b.guardAll.byExtension({ profile: "strict" })
356
+ * filenameSafety: Object | null, // gate; null = audited opt-out; undefined = b.guardFilename.gate({ profile: "strict" })
357
+ * onChunk: async function (info), // optional per-chunk hook
358
+ * onFinalize: async function (info), // operator decides final storage
359
+ * clock: function () → number, // test-fixture clock; default Date.now
360
+ *
361
+ * @example
362
+ * var uploads = b.fileUpload.create({
363
+ * stagingDir: "/var/lib/myapp/uploads",
364
+ * maxFileBytes: C.BYTES.gib(2),
365
+ * allowedFileTypes: ["image/png", "image/jpeg", "application/pdf"],
366
+ * fileType: b.fileType,
367
+ * audit: b.audit,
368
+ * observability: b.observability,
369
+ * onFinalize: async function (info) {
370
+ * // → info.body / info.stream → operator's storage layer
371
+ * return { ok: true, sha3: info.sha3, size: info.size };
372
+ * },
373
+ * });
374
+ *
375
+ * await uploads.init({ uploadId: "u-1", actor: { id: "ada" }, metadata: { filename: "photo.png" } });
376
+ * // → { uploadId: "u-1", createdAt: 1762560000000, expiresAt: 1762646400000 }
377
+ */
285
378
  function create(opts) {
286
379
  _validateCreateOpts(opts);
287
380
  var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
package/lib/flag.js CHANGED
@@ -1,29 +1,40 @@
1
1
  "use strict";
2
2
  /**
3
- * b.flag — feature-flag client per the OpenFeature specification
4
- * (https://openfeature.dev/specification/).
3
+ * @module b.flag
4
+ * @nav Tools
5
+ * @title Flag
5
6
  *
6
- * var flag = b.flag.create({
7
- * provider: b.flag.providers.localFile({ path: "./flags.json" }),
8
- * defaultEvaluationContext: { environment: "production" },
9
- * });
10
- *
11
- * var enabled = flag.getBoolean("new-checkout-flow", { targetingKey: req.user.id });
12
- * var sample = flag.getString ("greeting-banner", { targetingKey: req.user.id }, "default-text");
13
- * var rate = flag.getNumber ("upsell-rate", { targetingKey: req.user.id }, 0);
14
- * var cfg = flag.getObject ("checkout-config", { targetingKey: req.user.id }, {});
7
+ * @intro
8
+ * Feature-flag primitive shaped to the OpenFeature specification
9
+ * (https://openfeature.dev/specification/). Per-tenant overrides
10
+ * ride on the targeting context (`{ targetingKey, environment,
11
+ * ...attrs }`); every flip of a flag value at the provider layer
12
+ * audits via `flag.evaluated` / `flag.evaluation.error` on the
13
+ * framework audit chain so operators see the change without scraping
14
+ * provider state. Kill-switch semantics fall out of the same
15
+ * resolution chain: a provider that returns `value: false` for a
16
+ * targeting key disables the gated path immediately — no client
17
+ * restart, no cache flush.
15
18
  *
16
- * var details = flag.getDetails("new-checkout-flow", ctx);
17
- * // → { value, variant, reason, metadata }
19
+ * Validation policy:
20
+ * - `create()` throws on bad opts (provider missing
21
+ * `.evaluate(flagKey, ctx)`, hooks not function-shaped, etc.).
22
+ * - The hot path (`getBoolean` / `getString` / `getNumber` /
23
+ * `getObject` / `getValue`) NEVER throws — a provider that
24
+ * errors emits `flag.evaluation.error` and the call returns
25
+ * the operator-supplied default. A request can't be taken down
26
+ * by a misconfigured flag.
18
27
  *
19
- * flag.middleware() → request-time middleware that attaches a
20
- * per-request `flag` accessor onto req.
28
+ * Composability: `flag.middleware({ userKey })` attaches a
29
+ * per-request `req.flag` accessor that bakes the request's
30
+ * targeting context in, so handlers call `req.flag.getBoolean(key)`
31
+ * without threading context through every site. Multiple providers
32
+ * can be stacked via `opts.providers` or `addProvider`/`removeProvider`
33
+ * — first non-`flag_not_found` response wins, so a local-file
34
+ * override beats the central provider for break-glass scenarios.
21
35
  *
22
- * Per the validation-tier policy: create() throws on bad opts; the
23
- * hot-path getValue / getBoolean / etc. NEVER throw — they return the
24
- * operator-supplied default + emit `flag.evaluation.error` on the
25
- * audit chain so the operator sees the problem without taking down
26
- * the request.
36
+ * @card
37
+ * Feature-flag primitive shaped to the OpenFeature specification (https://openfeature.dev/specification/).
27
38
  */
28
39
 
29
40
  var validateOpts = require("./validate-opts");
@@ -60,6 +71,57 @@ function _validateHooks(rawHooks) {
60
71
  return out;
61
72
  }
62
73
 
74
+ /**
75
+ * @primitive b.flag.create
76
+ * @signature b.flag.create(opts)
77
+ * @since 0.6.0
78
+ * @status stable
79
+ * @related b.audit.safeEmit
80
+ *
81
+ * Build an OpenFeature-shaped flag client backed by one or more
82
+ * providers. The returned object exposes typed getters
83
+ * (`getBoolean` / `getString` / `getNumber` / `getObject` / `getValue`),
84
+ * a richer `getDetails(flagKey, ctx)` (returns
85
+ * `{ value, variant, reason, metadata }`), batch helpers (`getValues` /
86
+ * `getDetailsAll`), provider mutation (`addProvider` / `removeProvider`),
87
+ * and `middleware({ userKey })` for per-request binding.
88
+ *
89
+ * Throws `FlagError` at config time on a missing provider, hooks not
90
+ * shaped as `{ before?, after?, error?, finally? }` functions, or a
91
+ * provider lacking `.evaluate(flagKey, ctx)`.
92
+ *
93
+ * @opts
94
+ * provider: { evaluate: (flagKey, ctx) => result, kind?: string, list?: () => [string] },
95
+ * providers: [provider], // additional providers (first non-not-found wins)
96
+ * defaultEvaluationContext: { targetingKey?: string, [attr: string]: any },
97
+ * audit: boolean, // emit flag.evaluated / flag.evaluation.error; default true
98
+ * errorHandler: ({ flagKey, err, ctx }) => void,
99
+ * hooks: { before?: fn, after?: fn, error?: fn, finally?: fn }, // OpenFeature hook stages
100
+ *
101
+ * @example
102
+ * var provider = {
103
+ * kind: "memory",
104
+ * evaluate: function (flagKey, ctx) {
105
+ * if (flagKey === "new-checkout-flow") {
106
+ * return { value: ctx.targetingKey === "user-42", variant: "on", reason: "TARGETING_MATCH" };
107
+ * }
108
+ * return { reason: "flag_not_found" };
109
+ * },
110
+ * };
111
+ *
112
+ * var flag = b.flag.create({
113
+ * provider: provider,
114
+ * defaultEvaluationContext: { environment: "production" },
115
+ * });
116
+ *
117
+ * flag.getBoolean("new-checkout-flow", { targetingKey: "user-42" }); // → true
118
+ * flag.getBoolean("new-checkout-flow", { targetingKey: "user-99" }); // → false
119
+ * flag.getString ("missing-key", { targetingKey: "u" }, "fallback"); // → "fallback"
120
+ *
121
+ * // Per-request binding via middleware:
122
+ * // app.use(flag.middleware({ userKey: "id" }));
123
+ * // // handler: if (req.flag.getBoolean("new-checkout-flow")) { ... }
124
+ */
63
125
  function create(opts) {
64
126
  opts = opts || {};
65
127
  validateOpts(opts, [