@blamejs/core 0.8.43 → 0.8.50
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.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/dark-patterns.js
CHANGED
|
@@ -1,63 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.darkPatterns
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title Dark Patterns
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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);
|