@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/i18n.js CHANGED
@@ -1,62 +1,60 @@
1
1
  "use strict";
2
2
  /**
3
- * b.i18n — translation + locale negotiation primitive.
3
+ * @module b.i18n
4
+ * @nav Tools
5
+ * @title i18n
4
6
  *
5
- * Built on Node 24's bundled `Intl.*` (PluralRules, NumberFormat,
6
- * DateTimeFormat, RelativeTimeFormat, ListFormat, DisplayNames). Zero
7
- * vendoring, zero CLDR data shipped the runtime owns it.
7
+ * @intro
8
+ * ICU MessageFormat + CLDR Plural Rules + locale-aware Intl
9
+ * formatters with translation lookup. Built on Node 24's bundled
10
+ * `Intl.*` (`PluralRules`, `NumberFormat`, `DateTimeFormat`,
11
+ * `RelativeTimeFormat`, `ListFormat`, `DisplayNames`) — zero
12
+ * vendoring, zero CLDR data shipped, the runtime owns it.
8
13
  *
9
- * var i = b.i18n.create({
10
- * defaultLocale: "en",
11
- * locales: ["en", "es", "fr", "ja", "ar"],
12
- * translations: {
13
- * en: { greeting: "Hello, {name}!", items: { one: "{count} item", other: "{count} items" } },
14
- * es: { greeting: "¡Hola, {name}!" },
15
- * },
16
- * });
14
+ * Lookup chain: `t("nav.home", vars, { locale })` walks the
15
+ * subtag-stripped chain (`pt-BR` → `pt`), then falls through to the
16
+ * configured `fallbackLocale` and finally `defaultLocale` unless
17
+ * `fallbackLocale: null` (strict "this locale or miss"). Plural-
18
+ * shaped values use CLDR cardinal keys (`zero` / `one` / `two` /
19
+ * `few` / `many` / `other`); `other` is mandatory and validated at
20
+ * load. Ordinal plurals route through a separate `Intl.PluralRules({
21
+ * type: "ordinal" })` cache via `to(key, count)`.
17
22
  *
18
- * i.t("greeting", { name: "Alice" }); // "Hello, Alice!"
19
- * i.tn("items", 5); // → "5 items"
20
- * i.formatNumber(1234.5, { style: "currency", currency: "USD" });
21
- * i.formatRelative(-5, "minute"); // → "5 minutes ago"
23
+ * Translation file format (JSON loaded eagerly from `opts.dir` or
24
+ * inline via `opts.translations`):
22
25
  *
23
- * router.use(i.middleware());
24
- * // ...later
25
- * res.locals.t("greeting", { name: req.user.name });
26
+ * {
27
+ * "greeting": "Hello, {name}!",
28
+ * "items": { "one": "{count} item", "other": "{count} items" },
29
+ * "nav": { "home": "Home", "about": "About" }
30
+ * }
26
31
  *
27
- * Translation file format (JSON):
32
+ * ICU MessageFormat (`{name, plural, ...}` / `{name, select, ...}` /
33
+ * `{name, selectordinal, ...}`) is auto-detected by `t()`; operators
34
+ * force the path with `t(key, vars, { messageFormat: true })`. The
35
+ * companion `b.i18n.messageFormat` namespace exposes the parser /
36
+ * formatter for use outside an instance.
28
37
  *
29
- * {
30
- * "greeting": "Hello, {name}!",
31
- * "items": { "one": "{count} item", "other": "{count} items" },
32
- * "nav": { "home": "Home", "about": "About" }
33
- * }
38
+ * Validation policy:
39
+ * - `create()` throws on bad opts (boot).
40
+ * - Bad BCP 47 locale at any boundary → throw at call site.
41
+ * - `t(missingKey)` return the key + emit `i18n.missing`
42
+ * observability event (never throws unless `missingKey: "throw"`).
43
+ * - Plural shape missing `other` → throw at load time.
44
+ * - Missing interpolation var renders as literal `{var}` unless
45
+ * `interpolation.strict: true`.
46
+ * - `formatNumber` / `formatDate` / `formatRelative` / `formatList`
47
+ * throw at call site on a non-finite value or unparseable date.
48
+ * - Middleware `Accept-Language` parse error falls back to the
49
+ * default locale; the request never crashes on a bad header.
34
50
  *
35
- * - Plural-shaped values use CLDR cardinal keys (zero/one/two/few/
36
- * many/other). `other` is mandatory caught at load.
37
- * - Nested keys use dotted paths in t() (e.g. "nav.home").
38
- * - {var} interpolation; missing vars render as literal {var} unless
39
- * `interpolation.strict: true`.
51
+ * Security stance: translation values come from operator-controlled
52
+ * files, not user input. `{var}` interpolation does NOT html-escape;
53
+ * `b.template` already escapes at render time. For non-template
54
+ * contexts, pass `interpolation.escape: fn`.
40
55
  *
41
- * Validation policy:
42
- *
43
- * - create() opts → throw at boot
44
- * - bad locale tag at any boundary → throw at call site
45
- * - t(missingKey) → return key + observability event
46
- * - t() with bad locale override → throw at call site (programming bug)
47
- * - plural shape missing 'other' → throw at load time
48
- * - interpolation missing var → render literal {var}
49
- * - format* bad input → throw at call site
50
- * - middleware Accept-Language parse error → fall back to defaultLocale
51
- *
52
- * Security stance: translation values come from operator-controlled
53
- * files, not user input. {var} interpolation does NOT html-escape;
54
- * `b.template` already escapes when rendered. Operators using t() in
55
- * non-template contexts pass `interpolation.escape`.
56
- *
57
- * No audit-chain integration: i18n is not operator-action shaped (no
58
- * state mutation). Routing observability events (missing key, locale
59
- * fallback, formatter cache misses) is enough.
56
+ * @card
57
+ * ICU MessageFormat + CLDR Plural Rules + locale-aware Intl formatters with translation lookup.
60
58
  */
61
59
 
62
60
  var fs = require("node:fs");
@@ -371,6 +369,61 @@ function _makeFormatterCache(make, kind, emitObs) {
371
369
 
372
370
  // ---- Public create ----
373
371
 
372
+ /**
373
+ * @primitive b.i18n.create
374
+ * @signature b.i18n.create(opts)
375
+ * @since 0.6.0
376
+ * @status stable
377
+ * @related b.template.render
378
+ *
379
+ * Build an i18n instance bound to a fixed `locales` set. The returned
380
+ * object exposes translation (`t` / `tn` / `to` / `has`), Intl
381
+ * formatters (`formatNumber` / `formatDate` / `formatRelative` /
382
+ * `formatList` / `displayName`), locale state (`setLocale` /
383
+ * `locale` / `locales()` / `dir()`), translation introspection
384
+ * (`translations(locale)`), and an Express-shaped `middleware()` that
385
+ * negotiates the request locale (resolver → query → cookie →
386
+ * `Accept-Language`) and binds `req.t` / `req.tn` / `req.to` /
387
+ * `req.dir` / `res.locals.t` etc. for handlers.
388
+ *
389
+ * Throws `I18nError` at boot on a malformed locale tag, a
390
+ * `defaultLocale` not present in `locales`, a plural-shaped entry
391
+ * missing `other`, an unknown CLDR plural key, or a missing
392
+ * translation file when `dir` is supplied without `lazyLoad`.
393
+ *
394
+ * @opts
395
+ * defaultLocale: string, // BCP 47 tag; required, must appear in locales
396
+ * locales: [string], // BCP 47 tags; required, non-empty
397
+ * fallbackLocale: string | null, // null = strict; default = defaultLocale
398
+ * translations: { [locale: string]: object }, // inline trees (mutually exclusive with dir)
399
+ * dir: string, // load <dir>/<locale>.json (mutually exclusive with translations)
400
+ * eagerLocales: [string], // with lazyLoad: which locales to load at create
401
+ * lazyLoad: boolean, // with dir: load other locales on first lookup; default false
402
+ * interpolation: { start?: string, end?: string, escape?: fn, strict?: boolean },
403
+ * missingKey: "return-key" | "throw" | (key, locale) => string,
404
+ * onMissingKey: (key, locale) => void, // observability hook (best-effort)
405
+ * rtlLanguages: [string], // override the framework default RTL list
406
+ * observability: { event: (name, value, labels) => void },
407
+ * clock: () => number, // ms-since-epoch override (testing)
408
+ *
409
+ * @example
410
+ * var i = b.i18n.create({
411
+ * defaultLocale: "en",
412
+ * locales: ["en", "es", "fr", "ja", "ar"],
413
+ * translations: {
414
+ * en: { greeting: "Hello, {name}!", items: { one: "{count} item", other: "{count} items" } },
415
+ * es: { greeting: "Hola, {name}!" },
416
+ * },
417
+ * });
418
+ *
419
+ * i.t("greeting", { name: "Alice" }); // → "Hello, Alice!"
420
+ * i.tn("items", 5); // → "5 items"
421
+ * i.t("greeting", { name: "Ana" }, { locale: "es" }); // → "Hola, Ana!"
422
+ * i.formatNumber(1234.5, { style: "currency", currency: "USD" }); // → "$1,234.50"
423
+ * i.formatRelative(-5, "minute"); // → "5 minutes ago"
424
+ * i.dir({ locale: "ar" }); // → "rtl"
425
+ * i.has("nav.missing"); // → false
426
+ */
374
427
  function create(opts) {
375
428
  opts = opts || {};
376
429
  validateOpts(opts, [
package/lib/iab-mspa.js CHANGED
@@ -1,41 +1,30 @@
1
1
  "use strict";
2
2
  /**
3
- * b.iabMspa — IAB Multi-State Privacy Agreement / Global Privacy
4
- * Platform (GPP) universal opt-out signal codec.
3
+ * @module b.iabMspa
4
+ * @nav Compliance
5
+ * @title IAB MSPA
5
6
  *
6
- * GPP (https://github.com/InteractiveAdvertisingBureau/Global-
7
- * Privacy-Platform) is the IAB's successor to the patchwork of
8
- * per-state US privacy strings. A GPP string carries multiple
9
- * sections separated by `~`, each tagged with a section ID. The
10
- * MSPA-relevant sections are the US national + state sections
11
- * (USNAT, USCA, USVA, USCO, USCT, USUT) carrying:
7
+ * @intro
8
+ * IAB Multi-State Privacy Agreement signal encode/decode opt-out
9
+ * preferences for state privacy laws (CCPA, CPA, etc.).
12
10
  *
13
- * - SaleOptOut, SharingOptOut, TargetedAdvertisingOptOut, Sensitive-
14
- * DataProcessingOptOuts, KnownChildSensitiveData
15
- * - GPC (Global Privacy Control browser signal mirror)
16
- * - MSPA service-provider / opted-in flags
11
+ * The IAB Global Privacy Platform (GPP) is the successor to the
12
+ * patchwork of per-state US privacy strings. A GPP string carries
13
+ * multiple sections separated by `~`, each tagged with a section
14
+ * ID. The MSPA-relevant sections cover the US national + state
15
+ * regimes (USNAT, USCA, USVA, USCO, USCT, USUT, plus 2025-26
16
+ * additions) and carry sale / sharing / targeted-ads /
17
+ * sensitive-data / child-data opt-out flags alongside the W3C
18
+ * `Sec-GPC` browser-signal mirror.
17
19
  *
18
- * Public API:
20
+ * The framework ships a partial-correct decoder (the binary tag
21
+ * layout is operator-side via the IAB's gpp-cmp libraries), an
22
+ * opt-out evaluator that returns `mustHonor` across in-scope
23
+ * sections, a throw-on-must-honor refusal helper, and a header
24
+ * reader for the `Sec-GPC: 1` universal opt-out signal.
19
25
  *
20
- * b.iabMspa.parseGpp(gppString) -> { header, sections }
21
- * header: { version, sectionIds[], gpcSignal? }
22
- * sections: [{ id, optOuts: { sale, sharing, targetedAds, ... } }]
23
- *
24
- * b.iabMspa.checkOptOut(parsed, opts) -> { mustHonor, signals }
25
- * opts: { dataUse: "sale" | "sharing" | "targeted-ads" |
26
- * "sensitive" | "child-data", state? }
27
- * Returns mustHonor=true when ANY in-scope section signals an
28
- * opt-out for the requested use; signals lists which section IDs
29
- * produced the verdict.
30
- *
31
- * b.iabMspa.refuseProcessing(parsed, opts)
32
- * Throws IabMspaError when mustHonor → operator's data-flow code
33
- * halts at the same point a CCPA "do-not-sell" header would.
34
- *
35
- * b.iabMspa.gpcFromHeaders(req) -> bool
36
- * Reads the W3C `Sec-GPC: 1` browser header (RFC draft-davidson-
37
- * httpbis-gpc-00). Universal opt-out per California CCPA / CPRA
38
- * §1798.135(b)(1) and Colorado, Connecticut, etc.
26
+ * @card
27
+ * IAB Multi-State Privacy Agreement signal — encode/decode opt-out preferences for state privacy laws (CCPA, CPA, etc.).
39
28
  */
40
29
 
41
30
  var audit = require("./audit");
@@ -63,6 +52,26 @@ var SECTION_IDS = {
63
52
  var ALL_SECTIONS = Object.keys(SECTION_IDS).map(Number);
64
53
  var DATA_USES = ["sale", "sharing", "targeted-ads", "sensitive", "child-data"];
65
54
 
55
+ /**
56
+ * @primitive b.iabMspa.parseGpp
57
+ * @signature b.iabMspa.parseGpp(gppString)
58
+ * @since 0.8.44
59
+ * @related b.iabMspa.checkOptOut, b.iabMspa.refuseProcessing
60
+ *
61
+ * Parse the framing of a GPP string into `{ header, sections }`. The
62
+ * decoder splits on `~`, identifies each section by its positional
63
+ * claim in the header's section-ID list, and exposes the per-section
64
+ * raw payloads. The framework deliberately does not decode the
65
+ * binary section layout — operator-side libraries
66
+ * (`@iabtechlab/gpp-cmp`) own that surface and populate
67
+ * `section.optOuts`. Throws on missing input or strings exceeding
68
+ * the 8192-char defensive cap.
69
+ *
70
+ * @example
71
+ * var parsed = b.iabMspa.parseGpp("DBABBg.7.8");
72
+ * parsed.header.sectionIds; // → [7, 8]
73
+ * parsed.sections.length; // → 0 (no payload segments yet)
74
+ */
66
75
  function parseGpp(gppString) {
67
76
  if (typeof gppString !== "string" || gppString.length === 0) {
68
77
  throw IabMspaError.factory("BAD_INPUT",
@@ -115,6 +124,37 @@ function parseGpp(gppString) {
115
124
  return { header: header, sections: sections };
116
125
  }
117
126
 
127
+ /**
128
+ * @primitive b.iabMspa.checkOptOut
129
+ * @signature b.iabMspa.checkOptOut(parsed, opts)
130
+ * @since 0.8.44
131
+ * @related b.iabMspa.parseGpp, b.iabMspa.refuseProcessing
132
+ *
133
+ * Walk the parsed GPP sections and return `{ mustHonor, signals }`
134
+ * for the requested data-use category. `mustHonor` is `true` when
135
+ * ANY in-scope section signals an opt-out for that use; `signals`
136
+ * lists the section labels that produced the verdict. Operators
137
+ * narrow the search to a specific state by passing `opts.state`.
138
+ * Sections whose `optOuts` field hasn't been populated by an
139
+ * operator-side decoder are skipped (no false positives from
140
+ * missing data).
141
+ *
142
+ * @opts
143
+ * dataUse: "sale" | "sharing" | "targeted-ads" | "sensitive" | "child-data",
144
+ * state: string, // optional GPP section label
145
+ *
146
+ * @example
147
+ * var parsed = {
148
+ * header: { sectionIds: [8] },
149
+ * sections: [
150
+ * { id: 8, idLabel: "usca", raw: "",
151
+ * optOuts: { sale: true, sharing: false, targetedAds: true } },
152
+ * ],
153
+ * };
154
+ * var verdict = b.iabMspa.checkOptOut(parsed, { dataUse: "sale" });
155
+ * verdict.mustHonor; // → true
156
+ * verdict.signals; // → ["usca"]
157
+ */
118
158
  function checkOptOut(parsed, opts) {
119
159
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.sections)) {
120
160
  throw IabMspaError.factory("BAD_PARSED",
@@ -140,6 +180,28 @@ function checkOptOut(parsed, opts) {
140
180
  return { mustHonor: signals.length > 0, signals: signals };
141
181
  }
142
182
 
183
+ /**
184
+ * @primitive b.iabMspa.refuseProcessing
185
+ * @signature b.iabMspa.refuseProcessing(parsed, opts)
186
+ * @since 0.8.44
187
+ * @related b.iabMspa.checkOptOut, b.iabMspa.parseGpp
188
+ *
189
+ * Throw `IabMspaError` when `checkOptOut` returns `mustHonor:true`
190
+ * — wires the framework's opt-out signal into the operator's
191
+ * data-flow code at the same point a CCPA do-not-sell header would
192
+ * halt processing. Audits the refusal under
193
+ * `iabmspa.processing_refused` before throwing. Returns the verdict
194
+ * object on the no-opt-out path so the caller can inspect signals.
195
+ *
196
+ * @opts
197
+ * dataUse: "sale" | "sharing" | "targeted-ads" | "sensitive" | "child-data",
198
+ * state: string, // optional GPP section label
199
+ *
200
+ * @example
201
+ * var parsed = { header: { sectionIds: [] }, sections: [] };
202
+ * var verdict = b.iabMspa.refuseProcessing(parsed, { dataUse: "sale" });
203
+ * verdict.mustHonor; // → false (no signals → no throw)
204
+ */
143
205
  function refuseProcessing(parsed, opts) {
144
206
  var rv = checkOptOut(parsed, opts);
145
207
  if (rv.mustHonor) {
@@ -159,6 +221,24 @@ function refuseProcessing(parsed, opts) {
159
221
  return rv;
160
222
  }
161
223
 
224
+ /**
225
+ * @primitive b.iabMspa.gpcFromHeaders
226
+ * @signature b.iabMspa.gpcFromHeaders(req)
227
+ * @since 0.8.44
228
+ * @related b.iabMspa.checkOptOut, b.iabMspa.refuseProcessing
229
+ *
230
+ * Read the W3C `Sec-GPC: 1` browser header from an inbound request.
231
+ * Returns `true` when the user's browser is asserting the universal
232
+ * opt-out signal (mandatory under California CCPA / CPRA
233
+ * §1798.135(b)(1) and Colorado, Connecticut, etc.). Defensive
234
+ * against missing `req`/`headers` shapes — never throws.
235
+ *
236
+ * @example
237
+ * var req = { headers: { "sec-gpc": "1" } };
238
+ * b.iabMspa.gpcFromHeaders(req); // → true
239
+ * b.iabMspa.gpcFromHeaders({ headers: {} }); // → false
240
+ * b.iabMspa.gpcFromHeaders(null); // → false
241
+ */
162
242
  function gpcFromHeaders(req) {
163
243
  if (!req || !req.headers) return false;
164
244
  var h = req.headers["sec-gpc"];
package/lib/iab-tcf.js CHANGED
@@ -1,7 +1,40 @@
1
1
  "use strict";
2
2
  /**
3
- * b.iabTcf — IAB Europe Transparency & Consent Framework v2.3 consent
4
- * string parser + disclosedVendors validator.
3
+ * @module b.iabTcf
4
+ * @nav Compliance
5
+ * @title IAB TCF
6
+ *
7
+ * @intro
8
+ * IAB Transparency & Consent Framework v2.3 — TCF string
9
+ * parse/encode, vendor list lookup, purpose & special-feature
10
+ * checks.
11
+ *
12
+ * Required by TCF Policy v2.3 §III.B.5 (CMP MUST signal which
13
+ * vendors received disclosure regardless of consent state).
14
+ * Deadline 2026-02-28 is past — Google Ads + every major DSP
15
+ * rejects v2.2-shaped strings since that date. EU/UK adtech
16
+ * operators that didn't migrate are losing inventory.
17
+ *
18
+ * Consent-string format (TCF v2.3 spec, §A): base64url-no-pad of
19
+ * segments separated by `.`:
20
+ * `Core | DisclosedVendors | (AllowedVendors) | PublisherTC`.
21
+ * Core carries cmpVersion=2, version=4 (TCF v2.3),
22
+ * created/lastUpdated, cmpId, vendorListVersion,
23
+ * policyVersion=4, special-feature-opts-in, purpose-consents,
24
+ * purpose-LIs, vendor-consents bitmap, vendor-LIs bitmap,
25
+ * publisher restrictions. DisclosedVendors is REQUIRED in v2.3.
26
+ *
27
+ * The framework does NOT bundle the IAB Global Vendor List —
28
+ * operators fetch the versioned JSON from
29
+ * https://vendor-list.consensu.org/v3/vendor-list.json and use
30
+ * `parsed.core.vendorListVersion` to load the matching cache
31
+ * entry.
32
+ *
33
+ * @card
34
+ * IAB Transparency & Consent Framework v2.3 — TCF string parse/encode, vendor list lookup, purpose & special-feature checks.
35
+ */
36
+ /*
37
+ * Original prose retained:
5
38
  *
6
39
  * Required by TCF Policy v2.3 §III.B.5 (CMP MUST signal which vendors
7
40
  * received disclosure regardless of consent state). Deadline 2026-02-28
@@ -205,6 +238,28 @@ function _parseSecondaryVendorSegment(buf, expectedType) {
205
238
  return _parseVendorSection(r);
206
239
  }
207
240
 
241
+ /**
242
+ * @primitive b.iabTcf.parseString
243
+ * @signature b.iabTcf.parseString(tcString)
244
+ * @since 0.8.0
245
+ * @status stable
246
+ * @compliance iab-tcf
247
+ * @related b.iabTcf.requireV23Disclosed, b.iabTcf.checkVendor
248
+ *
249
+ * Defensively parse a TCF v2.3 consent string (Core + optional
250
+ * DisclosedVendors / AllowedVendors / PublisherTC segments).
251
+ * Refuses non-string input, refuses payloads above 64 KiB, and
252
+ * caps every bit-field to spec-declared widths. Returns a
253
+ * structured object; per-segment decode failures land in
254
+ * `errors[]` instead of throwing so a partial parse still serves.
255
+ *
256
+ * @example
257
+ * var parsed = b.iabTcf.parseString("CPXxRfAPXxRfAAfKABENB-CgAP_AAH_AAA");
258
+ * parsed.core.version;
259
+ * // → 4
260
+ * parsed.errors;
261
+ * // → []
262
+ */
208
263
  function parseString(tcString) {
209
264
  if (typeof tcString !== "string" || tcString.length === 0) {
210
265
  throw IabTcfError.factory("BAD_INPUT",
@@ -261,6 +316,34 @@ function parseString(tcString) {
261
316
  };
262
317
  }
263
318
 
319
+ /**
320
+ * @primitive b.iabTcf.requireV23Disclosed
321
+ * @signature b.iabTcf.requireV23Disclosed(tcString, opts)
322
+ * @since 0.8.0
323
+ * @status stable
324
+ * @compliance iab-tcf
325
+ * @related b.iabTcf.parseString, b.iabTcf.checkVendor
326
+ *
327
+ * Hard gate the operator wires upstream of every ad-bidder
328
+ * forward. Throws `IabTcfError` when the core/policy version is
329
+ * not 4 (i.e. a v2.2 string), when the DisclosedVendors segment
330
+ * is absent (mandatory since 2026-02-28 per TCF Policy v2.3
331
+ * §III.B.5), or when base64url decoding fails. Emits
332
+ * `iabtcf.refused` / `iabtcf.accepted` to the audit chain so the
333
+ * regulator-facing record exists per request.
334
+ *
335
+ * @opts
336
+ * audit: boolean, // default true — emit accept/refuse audit events
337
+ *
338
+ * @example
339
+ * try {
340
+ * var parsed = b.iabTcf.requireV23Disclosed("CPXxRfAPXxRfAAfKABENB-CgAP_AAH_AAA");
341
+ * parsed.disclosedVendors.present;
342
+ * // → true
343
+ * } catch (e) {
344
+ * // refuse the ad request
345
+ * }
346
+ */
264
347
  function requireV23Disclosed(tcString, opts) {
265
348
  opts = opts || {};
266
349
  var auditOn = opts.audit !== false;
@@ -329,6 +412,28 @@ function requireV23Disclosed(tcString, opts) {
329
412
  return parsed;
330
413
  }
331
414
 
415
+ /**
416
+ * @primitive b.iabTcf.checkVendor
417
+ * @signature b.iabTcf.checkVendor(parsed, vendorId)
418
+ * @since 0.8.0
419
+ * @status stable
420
+ * @compliance iab-tcf
421
+ * @related b.iabTcf.parseString, b.iabTcf.requireV23Disclosed
422
+ *
423
+ * Lookup a vendor id in a parsed TCF object. Returns three flags:
424
+ * `consented` (vendor in `vendorConsents`), `legitimate` (vendor
425
+ * in `vendorLIs`), `disclosed` (vendor in DisclosedVendors).
426
+ * Throws `IabTcfError` for malformed `parsed` or non-positive
427
+ * vendorId.
428
+ *
429
+ * @example
430
+ * var parsed = b.iabTcf.parseString("CPXxRfAPXxRfAAfKABENB-CgAP_AAH_AAA");
431
+ * var verdict = b.iabTcf.checkVendor(parsed, 755);
432
+ * verdict.consented;
433
+ * // → false
434
+ * verdict.disclosed;
435
+ * // → false
436
+ */
332
437
  function checkVendor(parsed, vendorId) {
333
438
  if (!parsed || !parsed.core) {
334
439
  throw IabTcfError.factory("BAD_PARSED",
package/lib/inbox.js CHANGED
@@ -1,62 +1,45 @@
1
1
  "use strict";
2
2
  /**
3
- * b.inbox — transactional dedupe-on-receive.
3
+ * @module b.inbox
4
+ * @nav Production
5
+ * @title Inbox
4
6
  *
5
- * Companion to `b.outbox`. Where outbox guarantees at-least-once
6
- * delivery, inbox lets the receiver guarantee exactly-once handling
7
- * by recording every (source, messageId) pair in the same transaction
8
- * as the business state change. If the same event is delivered twice
9
- * (network retry, replay, broker re-dispatch on consumer failure),
10
- * the second handler refuses with a duplicate-key constraint and the
11
- * application sees a clean short-circuit.
7
+ * @intro
8
+ * Transactional dedupe-on-receive for inbound message handlers.
9
+ * Companion to `b.outbox`: where outbox guarantees at-least-once
10
+ * delivery, inbox lets the receiver guarantee exactly-once handling
11
+ * by recording every `(source, messageId)` pair in the same database
12
+ * transaction as the business state change. A duplicate redelivery
13
+ * (network retry, replay, broker re-dispatch on consumer failure)
14
+ * collides with the primary-key constraint and the second handler
15
+ * short-circuits cleanly.
12
16
  *
13
- * var inbox = b.inbox.create({
14
- * externalDb: b.externalDb,
15
- * table: "inbox_events",
16
- * retentionDays: 30, // sweep older rows
17
- * audit: true,
18
- * });
19
- *
20
- * // High-level API — recommended for most callers:
21
- * await inbox.handle({
22
- * messageId: kafkaEvent.headers["x-event-id"],
23
- * source: "kafka:orders.created.v1",
24
- * payload: kafkaEvent.payload, // optional, audit only
25
- * }, async function (xdb) {
26
- * // Business state change runs exactly once per (source, messageId).
27
- * await xdb.query("INSERT INTO orders ...", [...]);
28
- * });
17
+ * Schema (declared via `declareSchema(externalDb)`): `message_id
18
+ * TEXT`, `source TEXT`, `received_at TIMESTAMP`, `processed_at
19
+ * TIMESTAMP NULL`, `metadata_json JSONB|TEXT`, with `PRIMARY KEY
20
+ * (source, message_id)`. Postgres uses `ON CONFLICT … DO NOTHING
21
+ * RETURNING` to decide fresh-vs-duplicate in one round-trip; SQLite
22
+ * 3.35+ uses `INSERT OR IGNORE … RETURNING 1` to avoid the
23
+ * `changes()` race when callers issue intervening statements on the
24
+ * same transaction handle.
29
25
  *
30
- * // Low-level API operator manages the transaction directly:
31
- * await b.externalDb.transaction(async function (xdb) {
32
- * var fresh = await inbox.recordReceive({
33
- * messageId: id, source: "kafka:orders.created",
34
- * }, xdb);
35
- * if (!fresh) return; // duplicate; skip
36
- * await xdb.query("INSERT INTO orders ...", [...]);
37
- * });
38
- *
39
- * // Schema:
40
- * await inbox.declareSchema(b.externalDb);
41
- *
42
- * // Periodic retention sweep (operator wires their scheduler):
43
- * await inbox.sweep();
44
- *
45
- * Schema columns:
26
+ * Defenses on the input side: `messageId` and `source` are bounded
27
+ * in length (default 256 chars each) and rejected for NUL / C0 /
28
+ * DEL control characters before they reach the primary key — Postgres
29
+ * TEXT may truncate at NUL, opening a dedupe-collision attack where
30
+ * `"abc\\0attacker"` and `"abc"` collide. `metadata` is JSON-
31
+ * serialized through `safeJson` and capped at `maxPayloadBytes`
32
+ * (default 64 KiB).
46
33
  *
47
- * message_id TEXT — primary part of the dedupe tuple
48
- * source TEXT — namespace (kafka topic, queue name, ...)
49
- * received_at TIMESTAMP
50
- * processed_at TIMESTAMP NULL — set when handle() commits
51
- * metadata_json JSONB / TEXT (operator-supplied audit blob)
34
+ * Two APIs: high-level `handle(opts, handler)` opens a transaction,
35
+ * records receive, runs the handler exactly once when fresh, marks
36
+ * processed, commits — recommended for most callers. Low-level
37
+ * `recordReceive(opts, txn)` lets operators manage the transaction
38
+ * directly when they need fine-grained control over what runs in
39
+ * the dedupe envelope.
52
40
  *
53
- * PRIMARY KEY (source, message_id) — enforces idempotence.
54
- *
55
- * Picking semantics:
56
- * - Postgres backends: ON CONFLICT (source, message_id) DO NOTHING
57
- * RETURNING * lets `recordReceive` decide fresh vs duplicate in
58
- * a single round-trip.
59
- * - SQLite: INSERT OR IGNORE + SELECT changes() to test fresh-ness.
41
+ * @card
42
+ * Transactional dedupe-on-receive for inbound message handlers.
60
43
  */
61
44
 
62
45
  var C = require("./constants");
@@ -90,6 +73,61 @@ function _utcNowExpr(externalDb) {
90
73
  return "CURRENT_TIMESTAMP";
91
74
  }
92
75
 
76
+ /**
77
+ * @primitive b.inbox.create
78
+ * @signature b.inbox.create(opts)
79
+ * @since 0.8.48
80
+ * @status stable
81
+ * @related b.outbox, b.externalDb, b.audit
82
+ *
83
+ * Build an inbox dedupe-store. Returns
84
+ * `{ declareSchema, recordReceive, markProcessed, handle, sweep,
85
+ * isFresh, getStats, table, retentionDays }`. Operators call
86
+ * `declareSchema` once at boot, `handle` per inbound message, and
87
+ * `sweep` periodically (under their own scheduler) to age out
88
+ * processed rows past retention.
89
+ *
90
+ * @opts
91
+ * externalDb: Object, // b.externalDb instance (transaction()-shaped)
92
+ * table: string, // SQL identifier; required
93
+ * retentionDays: number, // sweep horizon (default 30); unprocessed rows kept 2x as long
94
+ * audit: boolean, // emit inbox.* audit events (default true)
95
+ * maxPayloadBytes: number, // metadata serialized cap (default 64 KiB)
96
+ * messageIdMaxLen: number, // chars (default 256)
97
+ * sourceMaxLen: number, // chars (default 256)
98
+ *
99
+ * @example
100
+ * var inbox = b.inbox.create({
101
+ * externalDb: externalDbInstance,
102
+ * table: "inbox_events",
103
+ * retentionDays: 30,
104
+ * });
105
+ *
106
+ * await inbox.declareSchema(externalDbInstance);
107
+ *
108
+ * var outcome = await inbox.handle({
109
+ * messageId: "evt-9f3c4d",
110
+ * source: "kafka:orders.created.v1",
111
+ * }, async function (xdb) {
112
+ * await xdb.query("INSERT INTO orders (id) VALUES ($1)", ["o-42"]);
113
+ * return { orderId: "o-42" };
114
+ * });
115
+ * outcome.fresh; // → true on first delivery, false on replay
116
+ * outcome.result.orderId; // → "o-42"
117
+ *
118
+ * // Replay short-circuits:
119
+ * var replay = await inbox.handle({
120
+ * messageId: "evt-9f3c4d", source: "kafka:orders.created.v1",
121
+ * }, async function () { return { orderId: "should-not-run" }; });
122
+ * replay.fresh; // → false
123
+ * replay.result; // → null
124
+ *
125
+ * var stats = await inbox.getStats({ source: "kafka:orders.created.v1" });
126
+ * stats.total; // → 1
127
+ * stats.processed; // → 1
128
+ *
129
+ * var deleted = await inbox.sweep(); // age out beyond retention
130
+ */
93
131
  function create(opts) {
94
132
  opts = opts || {};
95
133
  validateOpts(opts, [