@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
@@ -1,63 +1,28 @@
1
1
  "use strict";
2
2
  /**
3
- * FTC Dark-Patterns / Click-to-Cancel UX-parity attestation.
3
+ * @module b.darkPatterns
4
+ * @nav Compliance
5
+ * @title Dark Patterns
4
6
  *
5
- * The FTC's Negative Option Rule (effective 2024; expanded 2025-2026
6
- * via state click-to-cancel laws) requires that the steps to cancel a
7
- * subscription / withdraw consent be no more burdensome than the
8
- * steps to subscribe / grant consent. The standard breakdown:
7
+ * @intro
8
+ * FTC dark-patterns compliance refusal helpers for fake-urgency,
9
+ * confirm-shaming, drip-pricing, hidden-cost, sneak-into-basket
10
+ * patterns.
9
11
  *
10
- * - prominence parity — same call-to-action visibility
11
- * - click-count parity — cancel <= signup
12
- * - contrast / font parity — accessible-text contrast and font
13
- * weight match
14
- * - method parity — operator that signed up over the web
15
- * must let the subject cancel over the
16
- * web (not phone-only)
17
- * - confirmation parity — single-confirmation if signup was
18
- * single-confirmation
12
+ * The FTC's Negative Option Rule (effective 2024; expanded 2025-26
13
+ * via state click-to-cancel laws) requires that the steps to cancel
14
+ * a subscription be no more burdensome than the steps to subscribe.
15
+ * The framework can't measure pixel-level UI parity from server
16
+ * code; what it ships is an attestation primitive: operators record
17
+ * a signup-flow snapshot (clicks, CTA text + font weight + contrast
18
+ * ratio, confirmations, channel, login requirement) and a matching
19
+ * cancel-flow snapshot. The framework computes the parity verdict
20
+ * against a posture (`ftc-2024` / `ca-sb942` / `strict`), audits
21
+ * the result, and ships a middleware that refuses cancel-route
22
+ * traffic with HTTP 451 when no passing attestation is on file.
19
23
  *
20
- * The framework can't measure pixel-level UI parity from server code.
21
- * What it CAN do is provide a primitive that:
22
- *
23
- * 1. Records an operator-attested signup-flow snapshot (clicks,
24
- * visible call-to-action text, font weight, contrast ratio).
25
- * 2. Records an attested cancel-flow snapshot.
26
- * 3. Computes the parity verdict and emits an audit trail.
27
- * 4. Refuses to emit a "consent-withdrawn" event in postures that
28
- * require parity if the snapshots show degradation.
29
- *
30
- * Public API:
31
- *
32
- * darkPatterns.recordSignupFlow(opts) -> snapshot
33
- * darkPatterns.recordCancelFlow(opts) -> snapshot
34
- * opts: {
35
- * channel: "web" | "mobile" | "phone" | "email" | "in-person",
36
- * clickCount: integer 1..50,
37
- * cta: { text, fontWeight, contrastRatio },
38
- * confirmations: integer 0..10,
39
- * requiresLogin: bool,
40
- * resourceId: operator-supplied id linking signup<->cancel,
41
- * }
42
- *
43
- * darkPatterns.assertParity(signup, cancel, opts) -> { ok, breaches }
44
- * opts:
45
- * toleranceClicks — how many extra cancel clicks tolerated
46
- * (default 0).
47
- * toleranceContrast — minimum contrast ratio absolute value
48
- * required of cancel (default 4.5 — AA).
49
- * posture — "ftc-2024" | "ca-sb942" | "strict".
50
- * errorClass — DarkPatternsError (mapped to McpError
51
- * namespace? no — uses a dedicated class).
52
- *
53
- * darkPatterns.attest(opts) -> { id, signupFlow, cancelFlow, verdict, signedAt }
54
- * One-shot composer used by operators that capture both flows
55
- * during a regression test of their UI.
56
- *
57
- * darkPatterns.middleware(opts) -> middleware(req, res, next)
58
- * Attached to the cancel-flow endpoint. Verifies the operator has
59
- * a parity attestation on file (via opts.lookupAttestation) and
60
- * refuses with 451 (legal reasons) if missing.
24
+ * @card
25
+ * FTC dark-patterns compliance refusal helpers for fake-urgency, confirm-shaming, drip-pricing, hidden-cost, sneak-into-basket patterns.
61
26
  */
62
27
 
63
28
  var audit = require("./audit");
@@ -144,6 +109,39 @@ function _validateFlowOpts(opts, label, errorClass) {
144
109
  }
145
110
  }
146
111
 
112
+ /**
113
+ * @primitive b.darkPatterns.recordSignupFlow
114
+ * @signature b.darkPatterns.recordSignupFlow(opts)
115
+ * @since 0.8.44
116
+ * @related b.darkPatterns.recordCancelFlow, b.darkPatterns.assertParity, b.darkPatterns.attest
117
+ *
118
+ * Capture a frozen snapshot of an operator-attested signup flow.
119
+ * Validates every input strictly: channel must be one of the allowed
120
+ * channels, click count is an integer 1..50, CTA carries a non-empty
121
+ * label plus CSS font weight 100..1000 and WCAG contrast 1..21,
122
+ * confirmations are an integer 0..10. The frozen result feeds
123
+ * `assertParity` paired with the matching cancel-flow snapshot.
124
+ *
125
+ * @opts
126
+ * channel: "web" | "mobile" | "phone" | "email" | "in-person" | "mail",
127
+ * clickCount: number, // integer 1..50
128
+ * cta: { text: string, fontWeight: number, contrastRatio: number },
129
+ * confirmations: number, // integer 0..10
130
+ * requiresLogin: boolean,
131
+ * resourceId: string, // links signup<->cancel
132
+ *
133
+ * @example
134
+ * var signup = b.darkPatterns.recordSignupFlow({
135
+ * channel: "web",
136
+ * clickCount: 2,
137
+ * cta: { text: "Subscribe", fontWeight: 700, contrastRatio: 7.2 },
138
+ * confirmations: 1,
139
+ * requiresLogin: false,
140
+ * resourceId: "plan-pro-2026",
141
+ * });
142
+ * signup.kind; // → "signup"
143
+ * signup.clickCount; // → 2
144
+ */
147
145
  function recordSignupFlow(opts) {
148
146
  _validateFlowOpts(opts, "SignupFlow", DarkPatternsError);
149
147
  return Object.freeze({
@@ -162,6 +160,37 @@ function recordSignupFlow(opts) {
162
160
  });
163
161
  }
164
162
 
163
+ /**
164
+ * @primitive b.darkPatterns.recordCancelFlow
165
+ * @signature b.darkPatterns.recordCancelFlow(opts)
166
+ * @since 0.8.44
167
+ * @related b.darkPatterns.recordSignupFlow, b.darkPatterns.assertParity, b.darkPatterns.attest
168
+ *
169
+ * Capture a frozen snapshot of the cancel-flow counterpart. Same
170
+ * validation discipline and field shape as `recordSignupFlow` so the
171
+ * two snapshots are directly comparable. The `resourceId` MUST match
172
+ * the signup snapshot's `resourceId`; `assertParity` enforces this.
173
+ *
174
+ * @opts
175
+ * channel: "web" | "mobile" | "phone" | "email" | "in-person" | "mail",
176
+ * clickCount: number, // integer 1..50
177
+ * cta: { text: string, fontWeight: number, contrastRatio: number },
178
+ * confirmations: number, // integer 0..10
179
+ * requiresLogin: boolean,
180
+ * resourceId: string, // must match signup
181
+ *
182
+ * @example
183
+ * var cancel = b.darkPatterns.recordCancelFlow({
184
+ * channel: "web",
185
+ * clickCount: 2,
186
+ * cta: { text: "Cancel subscription", fontWeight: 700, contrastRatio: 7.2 },
187
+ * confirmations: 1,
188
+ * requiresLogin: false,
189
+ * resourceId: "plan-pro-2026",
190
+ * });
191
+ * cancel.kind; // → "cancel"
192
+ * cancel.resourceId; // → "plan-pro-2026"
193
+ */
165
194
  function recordCancelFlow(opts) {
166
195
  _validateFlowOpts(opts, "CancelFlow", DarkPatternsError);
167
196
  return Object.freeze({
@@ -180,6 +209,45 @@ function recordCancelFlow(opts) {
180
209
  });
181
210
  }
182
211
 
212
+ /**
213
+ * @primitive b.darkPatterns.assertParity
214
+ * @signature b.darkPatterns.assertParity(signup, cancel, opts)
215
+ * @since 0.8.44
216
+ * @related b.darkPatterns.recordSignupFlow, b.darkPatterns.recordCancelFlow, b.darkPatterns.attest
217
+ *
218
+ * Compare a signup snapshot against a cancel snapshot under a named
219
+ * posture. Reports every parity breach: extra clicks beyond
220
+ * `toleranceClicks`, channel mismatch, contrast below the posture
221
+ * floor or degraded by more than 0.5 vs signup, font-weight
222
+ * regression, added confirmations, login required only on cancel.
223
+ * Returns `{ ok, breaches, posture }`. Postures: `ftc-2024` (FTC
224
+ * baseline), `ca-sb942` (California stricter), `strict` (contrast
225
+ * floor 7.0).
226
+ *
227
+ * @opts
228
+ * posture: "ftc-2024" | "ca-sb942" | "strict",
229
+ * toleranceClicks: number, // override posture default
230
+ * toleranceContrast: number, // override posture default
231
+ * errorClass: Error, // override DarkPatternsError
232
+ *
233
+ * @example
234
+ * var signup = b.darkPatterns.recordSignupFlow({
235
+ * channel: "web", clickCount: 2,
236
+ * cta: { text: "Subscribe", fontWeight: 700, contrastRatio: 7.2 },
237
+ * confirmations: 1, requiresLogin: false, resourceId: "plan-pro-2026",
238
+ * });
239
+ * var cancel = b.darkPatterns.recordCancelFlow({
240
+ * channel: "web", clickCount: 5,
241
+ * cta: { text: "Cancel", fontWeight: 400, contrastRatio: 3.0 },
242
+ * confirmations: 3, requiresLogin: true, resourceId: "plan-pro-2026",
243
+ * });
244
+ * var verdict = b.darkPatterns.assertParity(signup, cancel, { posture: "ftc-2024" });
245
+ * verdict.ok; // → false
246
+ * verdict.breaches.map(function (b2) { return b2.kind; });
247
+ * // → ["click-count", "contrast-below-floor", "contrast-degradation",
248
+ * // "font-weight-degradation", "confirmation-step-added",
249
+ * // "login-required-only-for-cancel"]
250
+ */
183
251
  function assertParity(signup, cancel, opts) {
184
252
  opts = opts || {};
185
253
  var errorClass = opts.errorClass || DarkPatternsError;
@@ -261,6 +329,42 @@ function assertParity(signup, cancel, opts) {
261
329
  return { ok: breaches.length === 0, breaches: breaches, posture: postureName };
262
330
  }
263
331
 
332
+ /**
333
+ * @primitive b.darkPatterns.attest
334
+ * @signature b.darkPatterns.attest(opts)
335
+ * @since 0.8.44
336
+ * @related b.darkPatterns.recordSignupFlow, b.darkPatterns.recordCancelFlow, b.darkPatterns.assertParity, b.darkPatterns.middleware
337
+ *
338
+ * One-shot composer used by operators that capture both flows during
339
+ * a UI regression test: builds the two snapshots, runs `assertParity`,
340
+ * and emits an audit row keyed `darkpatterns.attest` whose outcome is
341
+ * `success` on parity-clean or `denied` on any breach. Returns the
342
+ * full attestation envelope (id, both snapshots, verdict, signedAt)
343
+ * suitable for persistence and lookup by the cancel-route middleware.
344
+ *
345
+ * @opts
346
+ * signup: recordSignupFlow opts shape,
347
+ * cancel: recordCancelFlow opts shape,
348
+ * posture: "ftc-2024" | "ca-sb942" | "strict",
349
+ * audit: boolean, // default true
350
+ *
351
+ * @example
352
+ * var att = b.darkPatterns.attest({
353
+ * signup: {
354
+ * channel: "web", clickCount: 2,
355
+ * cta: { text: "Subscribe", fontWeight: 700, contrastRatio: 7.2 },
356
+ * confirmations: 1, requiresLogin: false, resourceId: "plan-pro-2026",
357
+ * },
358
+ * cancel: {
359
+ * channel: "web", clickCount: 2,
360
+ * cta: { text: "Cancel subscription", fontWeight: 700, contrastRatio: 7.2 },
361
+ * confirmations: 1, requiresLogin: false, resourceId: "plan-pro-2026",
362
+ * },
363
+ * posture: "ftc-2024",
364
+ * });
365
+ * att.verdict.ok; // → true
366
+ * att.id; // → "plan-pro-2026"
367
+ */
264
368
  function attest(opts) {
265
369
  opts = opts || {};
266
370
  var errorClass = opts.errorClass || DarkPatternsError;
@@ -292,6 +396,33 @@ function attest(opts) {
292
396
  };
293
397
  }
294
398
 
399
+ /**
400
+ * @primitive b.darkPatterns.middleware
401
+ * @signature b.darkPatterns.middleware(opts)
402
+ * @since 0.8.44
403
+ * @related b.darkPatterns.attest, b.darkPatterns.assertParity
404
+ *
405
+ * Mount on the cancel-route handler. Resolves a `resourceId` from
406
+ * the inbound request via the operator's `resourceIdFromReq`, looks
407
+ * up the corresponding attestation via `lookupAttestation`, and
408
+ * refuses with HTTP 451 (Unavailable for Legal Reasons) when no
409
+ * attestation exists or the on-file verdict shows a parity breach.
410
+ * Audits the refusal under `darkpatterns.cancel_blocked`.
411
+ *
412
+ * @opts
413
+ * lookupAttestation: function (resourceId) -> attestation | Promise,
414
+ * resourceIdFromReq: function (req) -> string,
415
+ * errorClass: Error, // override DarkPatternsError
416
+ *
417
+ * @example
418
+ * var attestations = new Map();
419
+ * var mw = b.darkPatterns.middleware({
420
+ * resourceIdFromReq: function (req) { return req.headers["x-plan-id"]; },
421
+ * lookupAttestation: function (id) { return attestations.get(id); },
422
+ * });
423
+ * // mount mw on the DELETE /subscription handler — refuses with 451
424
+ * // when the operator has no passing parity attestation on file.
425
+ */
295
426
  function middleware(opts) {
296
427
  opts = opts || {};
297
428
  var errorClass = opts.errorClass || DarkPatternsError;
package/lib/db-query.js CHANGED
@@ -30,9 +30,20 @@ var { Readable } = require("node:stream");
30
30
  var C = require("./constants");
31
31
  var cryptoField = require("./crypto-field");
32
32
  var { generateToken } = require("./crypto");
33
+ var safeJson = require("./safe-json");
34
+ var safeJsonPath = require("./safe-jsonpath");
33
35
  var safeSql = require("./safe-sql");
34
36
 
35
- var ALLOWED_OPS = new Set(["=", "!=", "<>", "<", "<=", ">", ">=", "IS", "IS NOT", "LIKE", "IN"]);
37
+ // "@>" / "?" / "?|" / "?&" are JSONB containment + key-existence
38
+ // operators. Routed through safeJsonPath validation before binding so
39
+ // operator-supplied values can't smuggle NUL / control / bidi
40
+ // characters into the JSON-shape comparison.
41
+ var ALLOWED_OPS = new Set([
42
+ "=", "!=", "<>", "<", "<=", ">", ">=", "IS", "IS NOT", "LIKE", "IN",
43
+ "@>", "?", "?|", "?&",
44
+ ]);
45
+ var JSONB_CONTAINMENT_OPS = new Set(["@>"]);
46
+ var JSONB_KEY_OPS = new Set(["?", "?|", "?&"]);
36
47
 
37
48
  class Query {
38
49
  constructor(database, tableName) {
@@ -103,6 +114,49 @@ class Query {
103
114
  if (!ALLOWED_OPS.has(op)) {
104
115
  throw new Error("invalid where operator: " + op);
105
116
  }
117
+ // D-M4 — JSONB / JSON-path injection guard. Routes operator-
118
+ // supplied JSONB containment + key-existence values through
119
+ // safe-jsonpath before they reach the engine. Bound via `?`
120
+ // placeholder so the value still doesn't interpolate; this is
121
+ // the second line of defense — refuses NUL / control / bidi /
122
+ // zero-width that some drivers silently strip out of JSON
123
+ // round-trip but the engine processes verbatim.
124
+ if (JSONB_CONTAINMENT_OPS.has(op)) {
125
+ if (typeof value === "string") {
126
+ // Operator passed pre-stringified JSON; parse + validate the
127
+ // shape, refuse on bad shape / control chars / depth bomb.
128
+ var parsed;
129
+ try { parsed = safeJson.parse(value); }
130
+ catch (e) {
131
+ throw new Error("where '" + op + "' value: invalid JSON string: " +
132
+ ((e && e.message) || String(e)));
133
+ }
134
+ safeJsonPath.validateContainment(parsed);
135
+ } else {
136
+ safeJsonPath.validateContainment(value);
137
+ // Bind the canonical-shape JSON so the driver sees the same
138
+ // bytes we validated. JSON.stringify here is safe — the
139
+ // shape was just walked end-to-end.
140
+ value = JSON.stringify(value);
141
+ }
142
+ }
143
+ if (JSONB_KEY_OPS.has(op)) {
144
+ if (op === "?") {
145
+ if (typeof value !== "string") {
146
+ throw new Error("where '?' requires a string key (got " + (typeof value) + ")");
147
+ }
148
+ safeJsonPath.validateKey(value);
149
+ } else {
150
+ // ?| / ?& take a Postgres text[] of keys. Caller passes a JS
151
+ // array; each element validated as a single key.
152
+ if (!Array.isArray(value) || value.length === 0) {
153
+ throw new Error("where '" + op + "' requires a non-empty array of string keys");
154
+ }
155
+ for (var ki = 0; ki < value.length; ki++) {
156
+ safeJsonPath.validateKey(value[ki]);
157
+ }
158
+ }
159
+ }
106
160
  // Sealed-field translation: rewrite predicate to use derived hash if available
107
161
  if (this._isSealedField(field)) {
108
162
  var lookup = cryptoField.lookupHash(this._cryptoFieldKey(), field, value);
@@ -294,9 +348,24 @@ class Query {
294
348
  // the bound table's sealedFields registration before it lands in the
295
349
  // operator's pipeline. For large result sets (audit exports, backup
296
350
  // table dumps) this avoids materializing the full rowset in memory.
297
- stream() {
351
+ // D-M5 — streamLimit ceiling enforced from the module-level db
352
+ // config; per-call opts.streamLimit overrides for one-off bumps.
353
+ stream(opts) {
298
354
  var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
299
355
  this._whereClause() + this._orderLimitOffset();
356
+ var perCallLimit;
357
+ // db.js exports getStreamLimit so this module reads the live
358
+ // ceiling without bouncing through the lib's circular load.
359
+ var dbModule = require("./db"); // allow:inline-require — circular-load defense (db imports db-query)
360
+ perCallLimit = dbModule.getStreamLimit();
361
+ if (opts && opts.streamLimit !== undefined) {
362
+ if (typeof opts.streamLimit !== "number" || !isFinite(opts.streamLimit) ||
363
+ opts.streamLimit <= 0 || Math.floor(opts.streamLimit) !== opts.streamLimit) {
364
+ throw new Error("Query.stream: opts.streamLimit must be a positive finite integer; got " +
365
+ JSON.stringify(opts.streamLimit));
366
+ }
367
+ perCallLimit = opts.streamLimit;
368
+ }
300
369
  var stmt = this._db.prepare(sql);
301
370
  var key = this._cryptoFieldKey();
302
371
  var iter;
@@ -306,12 +375,20 @@ class Query {
306
375
  setImmediate(function () { r.destroy(e); });
307
376
  return r;
308
377
  }
378
+ var emitted = 0;
309
379
  return new Readable({
310
380
  objectMode: true,
311
381
  read: function () {
312
382
  try {
383
+ if (emitted >= perCallLimit) {
384
+ this.destroy(new Error("Query.stream: emitted " + emitted +
385
+ " rows, exceeding streamLimit " + perCallLimit +
386
+ ". Pass opts.streamLimit higher OR raise via db.init({ streamLimit })."));
387
+ return;
388
+ }
313
389
  var step = iter.next();
314
390
  if (step.done) { this.push(null); return; }
391
+ emitted += 1;
315
392
  this.push(cryptoField.unsealRow(key, step.value));
316
393
  } catch (e) {
317
394
  this.destroy(e);