@blamejs/core 0.8.43 → 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.
- package/CHANGELOG.md +92 -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/config-drift.js
CHANGED
|
@@ -1,49 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* @module b.configDrift
|
|
4
|
+
* @nav Observability
|
|
5
|
+
* @title Config Drift
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* signal). config-drift fills that gap: at every boot, the operator
|
|
10
|
-
* passes the baseline config snapshot they want tracked. The
|
|
11
|
-
* primitive hashes it with SHA3-512, signs the digest with the audit-
|
|
12
|
-
* signing key, and writes the result to a sidecar at
|
|
13
|
-
* `<dataDir>/config-baseline.sig`. On the next boot, the sidecar is
|
|
14
|
-
* loaded + verified + diffed against the new snapshot. Drift surfaces
|
|
15
|
-
* as an audit event (`config.drift.detected`); no boot block — the
|
|
16
|
-
* operator may have a legitimate reason to change config and the
|
|
17
|
-
* framework's job is to make the change auditable, not to refuse to
|
|
18
|
-
* start.
|
|
7
|
+
* @intro
|
|
8
|
+
* Monitor + alert when runtime config diverges from a declared
|
|
9
|
+
* baseline.
|
|
19
10
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
11
|
+
* The framework's audit chain captures DATA writes (every row, every
|
|
12
|
+
* key change). It does NOT capture RUNTIME CONFIG — an operator who
|
|
13
|
+
* silently changes `allowedOrigins` three weeks ago leaves no signal.
|
|
14
|
+
* `b.configDrift` fills that gap: at every boot the operator passes
|
|
15
|
+
* the baseline snapshot they want tracked. The primitive hashes it
|
|
16
|
+
* with SHA3-512, signs the digest with the audit-signing key, and
|
|
17
|
+
* writes the result to a sidecar at `<dataDir>/config-baseline.sig`.
|
|
18
|
+
* On the next boot the sidecar is loaded, verified, and diffed
|
|
19
|
+
* against the new snapshot. Drift surfaces as an audit event
|
|
20
|
+
* (`config.drift.detected`); no boot block — the operator may have a
|
|
21
|
+
* legitimate reason to change config, and the framework's job is to
|
|
22
|
+
* make the change auditable, not to refuse to start.
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* auditMode: b.audit.getMode(),
|
|
30
|
-
* vaultMode: b.vault.getMode(),
|
|
31
|
-
* dbAtRest: b.db.getAtRestMode(),
|
|
32
|
-
* });
|
|
33
|
-
* // → { signed: true, drifted: false, previousAt: 1730000000000 }
|
|
24
|
+
* The signed sidecar uses `b.auditSign` — same SLH-DSA-SHAKE-256f
|
|
25
|
+
* keypair the audit chain anchors on. An attacker who flips a config
|
|
26
|
+
* value would also need to forge the signing key to update the
|
|
27
|
+
* sidecar cleanly; otherwise next-boot verify catches the tamper.
|
|
34
28
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
29
|
+
* `b.configDrift.verifyVendorIntegrity` is a sibling primitive that
|
|
30
|
+
* re-hashes every file under `lib/vendor/` against the manifest's
|
|
31
|
+
* sha256 digests at boot — catches a half-applied vendor refresh,
|
|
32
|
+
* a corrupted install, or a vendored cjs modified without a manifest
|
|
33
|
+
* update.
|
|
39
34
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - checkpoint() snapshot: must be a JSON-serialisable object
|
|
43
|
-
* - sidecar verify failure (tampered, key rotated, missing pubkey)
|
|
44
|
-
* surfaces as `config.baseline.tamper` audit event AND the call
|
|
45
|
-
* returns { tamper: true } so the operator can decide whether
|
|
46
|
-
* to refuse boot
|
|
35
|
+
* @card
|
|
36
|
+
* Monitor + alert when runtime config diverges from a declared baseline.
|
|
47
37
|
*/
|
|
48
38
|
var fs = require("node:fs");
|
|
49
39
|
var path = require("node:path");
|
|
@@ -96,6 +86,52 @@ function _diffShallow(prev, next) {
|
|
|
96
86
|
return { changed: changed, added: added, removed: removed };
|
|
97
87
|
}
|
|
98
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @primitive b.configDrift.create
|
|
91
|
+
* @signature b.configDrift.create(opts)
|
|
92
|
+
* @since 0.6.0
|
|
93
|
+
* @status stable
|
|
94
|
+
* @related b.configDrift.verifyVendorIntegrity, b.auditSign.sign, b.audit.safeEmit
|
|
95
|
+
*
|
|
96
|
+
* Build a per-baseline drift detector bound to a `dataDir`. Returns a
|
|
97
|
+
* handle exposing `.checkpoint(snapshot)` (write or compare against the
|
|
98
|
+
* signed sidecar), `.read()` (load + verify the sidecar without
|
|
99
|
+
* mutating it), and `.sidecarPath` (absolute path of the sidecar file).
|
|
100
|
+
*
|
|
101
|
+
* Drift on a key listed in `opts.criticalKeys` surfaces as
|
|
102
|
+
* `config.drift.detected` with `severity: "high"` and outcome
|
|
103
|
+
* `failure`; drift in any other key surfaces as `severity: "low"` and
|
|
104
|
+
* outcome `success`. When `criticalKeys` is omitted every key is
|
|
105
|
+
* treated as high severity. Keys listed in `opts.ignoreKeys` are
|
|
106
|
+
* captured in the snapshot but never raise a drift event.
|
|
107
|
+
*
|
|
108
|
+
* @opts
|
|
109
|
+
* dataDir: string, // directory holding the baseline sidecar (required)
|
|
110
|
+
* audit: object, // b.audit instance; pass false to disable
|
|
111
|
+
* baseline: string, // baseline name — sidecar is `config-baseline-<name>.sig`
|
|
112
|
+
* criticalKeys: string[], // keys whose drift raises severity to "high"
|
|
113
|
+
* ignoreKeys: string[], // keys excluded from drift detection
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* var fakeAudit = { safeEmit: function () {} };
|
|
117
|
+
* var detector = b.configDrift.create({
|
|
118
|
+
* dataDir: "/tmp/blamejs-drift-demo",
|
|
119
|
+
* audit: fakeAudit,
|
|
120
|
+
* criticalKeys: ["allowedOrigins", "csp"],
|
|
121
|
+
* ignoreKeys: ["bootCount"],
|
|
122
|
+
* });
|
|
123
|
+
* detector.sidecarPath;
|
|
124
|
+
* // → "/tmp/blamejs-drift-demo/config-baseline.sig"
|
|
125
|
+
*
|
|
126
|
+
* var first = await detector.checkpoint({
|
|
127
|
+
* allowedOrigins: ["https://app.example.com"],
|
|
128
|
+
* csp: "default-src 'self'",
|
|
129
|
+
* bootCount: 1,
|
|
130
|
+
* });
|
|
131
|
+
* first.signed; // → true
|
|
132
|
+
* first.drifted; // → false
|
|
133
|
+
* first.previousAt; // → null
|
|
134
|
+
*/
|
|
99
135
|
function create(opts) {
|
|
100
136
|
opts = opts || {};
|
|
101
137
|
validateOpts(opts, [
|
|
@@ -292,13 +328,42 @@ function create(opts) {
|
|
|
292
328
|
};
|
|
293
329
|
}
|
|
294
330
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
331
|
+
/**
|
|
332
|
+
* @primitive b.configDrift.verifyVendorIntegrity
|
|
333
|
+
* @signature b.configDrift.verifyVendorIntegrity(opts)
|
|
334
|
+
* @since 0.7.39
|
|
335
|
+
* @status stable
|
|
336
|
+
* @related b.configDrift.create, b.audit.safeEmit
|
|
337
|
+
*
|
|
338
|
+
* At-boot integrity check over `lib/vendor/*`. MANIFEST.json carries a
|
|
339
|
+
* sha256 digest per bundled file; the call re-hashes each one and
|
|
340
|
+
* surfaces mismatches without throwing. Returns
|
|
341
|
+
* `{ ok, checkedCount, mismatches }` and emits
|
|
342
|
+
* `vendor.integrity.verified` (success) or `vendor.integrity.tampered`
|
|
343
|
+
* (failure) on every invocation so a corrupted install lands in the
|
|
344
|
+
* audit chain at boot, not later.
|
|
345
|
+
*
|
|
346
|
+
* Throws `ConfigDriftError("VENDOR_MANIFEST_MISSING")` when MANIFEST.json
|
|
347
|
+
* is absent and `ConfigDriftError("VENDOR_MANIFEST_SHAPE")` when its
|
|
348
|
+
* top-level `packages` map is missing — operators see a hard fail
|
|
349
|
+
* instead of a silent zero-files-checked pass.
|
|
350
|
+
*
|
|
351
|
+
* @opts
|
|
352
|
+
* libVendorDir: string, // absolute path to lib/vendor (defaults to cwd-relative)
|
|
353
|
+
* manifestPath: string, // absolute path to MANIFEST.json (defaults under libVendorDir)
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* try {
|
|
357
|
+
* var result = b.configDrift.verifyVendorIntegrity({
|
|
358
|
+
* libVendorDir: "/srv/app/lib/vendor",
|
|
359
|
+
* });
|
|
360
|
+
* result.ok; // → true
|
|
361
|
+
* result.checkedCount; // → 42
|
|
362
|
+
* result.mismatches; // → []
|
|
363
|
+
* } catch (e) {
|
|
364
|
+
* e.code; // → "VENDOR_MANIFEST_MISSING"
|
|
365
|
+
* }
|
|
366
|
+
*/
|
|
302
367
|
function verifyVendorIntegrity(opts) {
|
|
303
368
|
opts = opts || {};
|
|
304
369
|
var libVendorDir = opts.libVendorDir || path.join(process.cwd(), "lib", "vendor");
|
package/lib/config.js
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.config
|
|
4
|
+
* @nav Tools
|
|
5
|
+
* @title Config
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* config.
|
|
7
|
+
* @intro
|
|
8
|
+
* Schema-validated environment configuration. Operators read
|
|
9
|
+
* `process.env` throughout their app; a typo in the key name OR a
|
|
10
|
+
* value in the wrong shape (`port="abc"`, `flag="yas"`) surfaces
|
|
11
|
+
* three days later as a mysterious 500. `b.config.create` validates
|
|
12
|
+
* env at boot through `b.safeSchema` so the app refuses to start
|
|
13
|
+
* with broken config — the throw happens at `create()` time, not
|
|
14
|
+
* at the first request that touches the broken value.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* DATABASE_URL: b.safeSchema.string().url(),
|
|
18
|
-
* REDIS_URL: b.safeSchema.string().url().optional(),
|
|
19
|
-
* FEATURE_X: b.config.coerce.boolean().default(false),
|
|
20
|
-
* }),
|
|
21
|
-
* // env: process.env (default) — operators in tests pass a fake object
|
|
22
|
-
* // redactKeys: ["SESSION_SECRET", "DATABASE_URL"] — never log these
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* config.value → the validated, typed value object
|
|
26
|
-
* config.value.PORT → 3000 (Number, not "3000")
|
|
27
|
-
* config.boot() → throws ConfigError on validation failure;
|
|
28
|
-
* returns the validated value otherwise
|
|
16
|
+
* `b.config.coerce.number()` and `b.config.coerce.boolean()` wrap
|
|
17
|
+
* schema leaves with the env-friendly preprocessors most operators
|
|
18
|
+
* want (env values are always strings at the source). `loadDbBacked`
|
|
19
|
+
* composes `create` with periodic DB-row polling so a row update in
|
|
20
|
+
* `_blamejs_config_overrides` surfaces without restart, and falls
|
|
21
|
+
* back to the last-good value on validation failure.
|
|
29
22
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the first request that touches the broken value.
|
|
23
|
+
* @card
|
|
24
|
+
* Schema-validated environment configuration.
|
|
33
25
|
*/
|
|
34
26
|
var safeSchema = require("./safe-schema");
|
|
35
27
|
var validateOpts = require("./validate-opts");
|
|
@@ -64,6 +56,41 @@ var coerce = {
|
|
|
64
56
|
},
|
|
65
57
|
};
|
|
66
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @primitive b.config.create
|
|
61
|
+
* @signature b.config.create(opts)
|
|
62
|
+
* @since 0.8.0
|
|
63
|
+
* @status stable
|
|
64
|
+
* @related b.config.loadDbBacked, b.safeSchema.object
|
|
65
|
+
*
|
|
66
|
+
* Validate env against a `b.safeSchema` shape and return a frozen
|
|
67
|
+
* config handle (`value` / `get` / `has` / `redacted` / `subscribe` /
|
|
68
|
+
* `reload`). Throws `ConfigError` synchronously when validation fails
|
|
69
|
+
* — the operator sees broken config at boot rather than at the first
|
|
70
|
+
* request that touches the value. The handle's `reload(overlay)`
|
|
71
|
+
* applies a new env-shaped overlay on top of the validated baseline,
|
|
72
|
+
* notifies subscribers on success, and falls back to the prior value
|
|
73
|
+
* on failure.
|
|
74
|
+
*
|
|
75
|
+
* @opts
|
|
76
|
+
* schema: b.safeSchema instance (required; built via b.safeSchema.object({...})),
|
|
77
|
+
* env: object (env bag; default process.env),
|
|
78
|
+
* redactKeys: Array<string> (keys masked by `.redacted()` for log output),
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* var s = b.safeSchema;
|
|
82
|
+
* var cfg = b.config.create({
|
|
83
|
+
* schema: s.object({
|
|
84
|
+
* NODE_ENV: s.enum_(["development", "test", "production"]),
|
|
85
|
+
* PORT: b.config.coerce.number().default(3000),
|
|
86
|
+
* }),
|
|
87
|
+
* env: { NODE_ENV: "production", PORT: "8080" },
|
|
88
|
+
* redactKeys: [],
|
|
89
|
+
* });
|
|
90
|
+
* cfg.value.NODE_ENV; // → "production"
|
|
91
|
+
* cfg.value.PORT; // → 8080 (Number, not "8080")
|
|
92
|
+
* cfg.has("PORT"); // → true
|
|
93
|
+
*/
|
|
67
94
|
function create(opts) {
|
|
68
95
|
if (!opts || typeof opts !== "object") {
|
|
69
96
|
throw new ConfigError("config/bad-opts",
|
|
@@ -168,19 +195,46 @@ function create(opts) {
|
|
|
168
195
|
};
|
|
169
196
|
}
|
|
170
197
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
198
|
+
/**
|
|
199
|
+
* @primitive b.config.loadDbBacked
|
|
200
|
+
* @signature b.config.loadDbBacked(opts)
|
|
201
|
+
* @since 0.8.0
|
|
202
|
+
* @status stable
|
|
203
|
+
* @related b.config.create, b.safeAsync.repeating
|
|
204
|
+
*
|
|
205
|
+
* Compose `b.config.create` with a periodic DB-row fetch. Operators
|
|
206
|
+
* keep canonical config values in
|
|
207
|
+
* `_blamejs_config_overrides(key TEXT PRIMARY KEY, value TEXT)`; this
|
|
208
|
+
* helper polls every `intervalMs`, applies the rows as an overlay
|
|
209
|
+
* via the underlying handle's `reload`, and re-validates. Reload
|
|
210
|
+
* failures emit a `config.reload.failed` audit row but DO NOT
|
|
211
|
+
* clobber the previous value — the running app stays on the
|
|
212
|
+
* last-good config. The returned handle is the same shape as
|
|
213
|
+
* `create()` plus a `.stop()` method that halts the poller.
|
|
214
|
+
*
|
|
215
|
+
* @opts
|
|
216
|
+
* schema: b.safeSchema instance (required),
|
|
217
|
+
* env: object (env baseline; default process.env),
|
|
218
|
+
* redactKeys: Array<string>,
|
|
219
|
+
* fetchRows: async () => Array<{ key: string, value: string }> (required),
|
|
220
|
+
* intervalMs: number (positive finite poll interval),
|
|
221
|
+
* audit: boolean (default true; reserved for future per-poll audit),
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* var s = b.safeSchema;
|
|
225
|
+
* var cfg = b.config.loadDbBacked({
|
|
226
|
+
* schema: s.object({
|
|
227
|
+
* FEATURE_X: b.config.coerce.boolean().default(false),
|
|
228
|
+
* }),
|
|
229
|
+
* env: { FEATURE_X: "false" },
|
|
230
|
+
* fetchRows: async function () {
|
|
231
|
+
* return [{ key: "FEATURE_X", value: "true" }];
|
|
232
|
+
* },
|
|
233
|
+
* intervalMs: 60 * 1000,
|
|
234
|
+
* });
|
|
235
|
+
* cfg.value.FEATURE_X; // → false (until first poll tick lands)
|
|
236
|
+
* cfg.stop(); // halt the poller on shutdown
|
|
237
|
+
*/
|
|
184
238
|
function loadDbBacked(opts) {
|
|
185
239
|
opts = opts || {};
|
|
186
240
|
validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "audit"],
|
package/lib/consent.js
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
3
|
+
* @module b.consent
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title Consent
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Consent-record chain — every grant / withdrawal / expiry / supersede
|
|
9
|
+
* for a (subjectId, purpose) pair lands in `consent_log` as one
|
|
10
|
+
* append-only, hash-chained row. Same tamper-evidence design as
|
|
11
|
+
* `audit_log`: per-row SHA3-512 hash chain over the sealed payload,
|
|
12
|
+
* verified at boot, refuse-to-boot on a break.
|
|
13
|
+
*
|
|
14
|
+
* GDPR Art. 7 demands controllers be able to demonstrate the data
|
|
15
|
+
* subject consented; CCPA / CPRA Title 1.81.5 requires evidence the
|
|
16
|
+
* consumer exercised opt-out. consent_log carries `subjectId` (sealed)
|
|
17
|
+
* + `purpose` + `lawfulBasis` + `channel` + operator-supplied
|
|
18
|
+
* `evidenceRef` so a regulator request resolves to a specific row,
|
|
19
|
+
* tied to the audit chain via shared chain-writer primitives.
|
|
20
|
+
*
|
|
21
|
+
* Lawful-basis vocabulary tracks the GDPR Art. 6(1) enumeration:
|
|
22
|
+
* `consent`, `contract`, `legal_obligation`, `vital_interests`,
|
|
23
|
+
* `public_task`, `legitimate_interests`. Any audit event declaring
|
|
24
|
+
* `lawfulBasis: 'consent'` should reference a current consent_log
|
|
25
|
+
* entry — the framework records the grants and withdrawals here;
|
|
26
|
+
* enforcement at the trust boundary is the app's call (typical shape:
|
|
27
|
+
* `if (!b.consent.isGranted({ subjectId, purpose })) return 403`).
|
|
28
|
+
*
|
|
29
|
+
* Cluster mode keeps `_blamejs_consent_tip` current with a fenced
|
|
30
|
+
* `INSERT … ON CONFLICT DO UPDATE … WHERE fencingToken <= EXCLUDED`
|
|
31
|
+
* so a partitioned old leader cannot rewrite the tip even if its
|
|
32
|
+
* application-layer leader gate let the call through. Followers
|
|
33
|
+
* refuse `grant` / `withdraw` with `NotLeaderError`.
|
|
34
|
+
*
|
|
35
|
+
* @card
|
|
36
|
+
* Consent-record chain — every grant / withdrawal / expiry / supersede for a (subjectId, purpose) pair lands in `consent_log` as one append-only, hash-chained row.
|
|
21
37
|
*/
|
|
22
38
|
var auditChain = require("./audit-chain");
|
|
23
39
|
var cluster = require("./cluster");
|
|
@@ -64,6 +80,40 @@ var _chainWriter = chainWriter.create({
|
|
|
64
80
|
|
|
65
81
|
// ---- Public API ----
|
|
66
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @primitive b.consent.grant
|
|
85
|
+
* @signature b.consent.grant(opts)
|
|
86
|
+
* @since 0.1.0
|
|
87
|
+
* @status stable
|
|
88
|
+
* @compliance gdpr, ccpa, hipaa
|
|
89
|
+
* @related b.consent.withdraw, b.consent.isGranted, b.consent.history
|
|
90
|
+
*
|
|
91
|
+
* Append a "granted" row to consent_log for a (subjectId, purpose) pair.
|
|
92
|
+
* Refuses on a follower (cluster mode). Lawful-basis must come from the
|
|
93
|
+
* GDPR Art. 6(1) enumeration; an unknown value throws synchronously
|
|
94
|
+
* before touching the chain.
|
|
95
|
+
*
|
|
96
|
+
* @opts
|
|
97
|
+
* subjectId: string, // sealed at rest
|
|
98
|
+
* purpose: string, // e.g. "marketing"
|
|
99
|
+
* lawfulBasis: "consent" | "contract" | "legal_obligation"
|
|
100
|
+
* | "vital_interests" | "public_task"
|
|
101
|
+
* | "legitimate_interests",
|
|
102
|
+
* scope: object, // optional, JSON-serialized
|
|
103
|
+
* channel: string, // "web-banner" / "api" / ...
|
|
104
|
+
* evidenceRef: string, // optional pointer to UI snapshot
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* await b.consent.grant({
|
|
108
|
+
* subjectId: "u-42",
|
|
109
|
+
* purpose: "marketing",
|
|
110
|
+
* lawfulBasis: "consent",
|
|
111
|
+
* scope: { channels: ["email", "sms"] },
|
|
112
|
+
* channel: "web-banner-v3",
|
|
113
|
+
* evidenceRef: "snapshot-2026-05-09T14:00:00Z",
|
|
114
|
+
* });
|
|
115
|
+
* // → { _id, monotonicCounter, rowHash, prevHash, ... }
|
|
116
|
+
*/
|
|
67
117
|
function grant(opts) {
|
|
68
118
|
cluster.requireLeader();
|
|
69
119
|
if (!opts || !opts.subjectId || !opts.purpose || !opts.lawfulBasis || !opts.channel) {
|
|
@@ -83,6 +133,34 @@ function grant(opts) {
|
|
|
83
133
|
});
|
|
84
134
|
}
|
|
85
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @primitive b.consent.withdraw
|
|
138
|
+
* @signature b.consent.withdraw(opts)
|
|
139
|
+
* @since 0.1.0
|
|
140
|
+
* @status stable
|
|
141
|
+
* @compliance gdpr, ccpa
|
|
142
|
+
* @related b.consent.grant, b.consent.isGranted
|
|
143
|
+
*
|
|
144
|
+
* Append a "withdrawn" row to consent_log. After this lands,
|
|
145
|
+
* `isGranted` returns `false` for the same (subjectId, purpose). Pair
|
|
146
|
+
* with a downstream sweep over data-classes that depended on the
|
|
147
|
+
* lawful basis (typical pattern: cascade into `b.retention` or
|
|
148
|
+
* `b.subject.erase`).
|
|
149
|
+
*
|
|
150
|
+
* @opts
|
|
151
|
+
* subjectId: string,
|
|
152
|
+
* purpose: string,
|
|
153
|
+
* reason: string, // optional, recorded as evidenceRef
|
|
154
|
+
* channel: string, // optional, defaults to "api"
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* await b.consent.withdraw({
|
|
158
|
+
* subjectId: "u-42",
|
|
159
|
+
* purpose: "marketing",
|
|
160
|
+
* reason: "user-self-service-portal",
|
|
161
|
+
* });
|
|
162
|
+
* // → { _id, monotonicCounter, rowHash, ... }
|
|
163
|
+
*/
|
|
86
164
|
function withdraw(opts) {
|
|
87
165
|
cluster.requireLeader();
|
|
88
166
|
if (!opts || !opts.subjectId || !opts.purpose) {
|
|
@@ -99,6 +177,30 @@ function withdraw(opts) {
|
|
|
99
177
|
});
|
|
100
178
|
}
|
|
101
179
|
|
|
180
|
+
/**
|
|
181
|
+
* @primitive b.consent.isGranted
|
|
182
|
+
* @signature b.consent.isGranted(opts)
|
|
183
|
+
* @since 0.1.0
|
|
184
|
+
* @status stable
|
|
185
|
+
* @compliance gdpr, ccpa
|
|
186
|
+
* @related b.consent.grant, b.consent.withdraw, b.consent.history
|
|
187
|
+
*
|
|
188
|
+
* Returns `true` when the most recent consent_log row for the
|
|
189
|
+
* (subjectId, purpose) pair has action `granted`. Lookups go through
|
|
190
|
+
* the derived `subjectIdHash` so the sealed `subjectId` column never
|
|
191
|
+
* needs to be unsealed for the query. Safe on followers (read-only).
|
|
192
|
+
*
|
|
193
|
+
* @opts
|
|
194
|
+
* subjectId: string,
|
|
195
|
+
* purpose: string,
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* if (!b.consent.isGranted({ subjectId: "u-42", purpose: "marketing" })) {
|
|
199
|
+
* res.statusCode = 403;
|
|
200
|
+
* return res.end("consent required");
|
|
201
|
+
* }
|
|
202
|
+
* // → true / false
|
|
203
|
+
*/
|
|
102
204
|
function isGranted(opts) {
|
|
103
205
|
if (!opts || !opts.subjectId || !opts.purpose) {
|
|
104
206
|
throw new Error("consent.isGranted requires { subjectId, purpose }");
|
|
@@ -117,6 +219,26 @@ function isGranted(opts) {
|
|
|
117
219
|
return row.action === "granted";
|
|
118
220
|
}
|
|
119
221
|
|
|
222
|
+
/**
|
|
223
|
+
* @primitive b.consent.history
|
|
224
|
+
* @signature b.consent.history(subjectId)
|
|
225
|
+
* @since 0.1.0
|
|
226
|
+
* @status stable
|
|
227
|
+
* @compliance gdpr, ccpa
|
|
228
|
+
* @related b.consent.grant, b.consent.withdraw, b.subject.export
|
|
229
|
+
*
|
|
230
|
+
* Returns every consent_log row for `subjectId`, oldest first, decrypted
|
|
231
|
+
* by the framework's row reader. Composes into `b.subject.export` for
|
|
232
|
+
* GDPR Art. 15 / CCPA §1798.110 right-of-access responses without the
|
|
233
|
+
* caller having to walk the chain manually.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* var rows = b.consent.history("u-42");
|
|
237
|
+
* rows.forEach(function (r) {
|
|
238
|
+
* console.log(r.recordedAt, r.purpose, r.action, r.lawfulBasis);
|
|
239
|
+
* });
|
|
240
|
+
* // → [{ recordedAt, purpose, action, lawfulBasis, channel, ... }]
|
|
241
|
+
*/
|
|
120
242
|
function history(subjectId) {
|
|
121
243
|
if (!subjectId) throw new Error("consent.history requires a subjectId");
|
|
122
244
|
var hash = db().hashFor("consent_log", "subjectId", subjectId);
|
|
@@ -130,6 +252,31 @@ function history(subjectId) {
|
|
|
130
252
|
return rows;
|
|
131
253
|
}
|
|
132
254
|
|
|
255
|
+
/**
|
|
256
|
+
* @primitive b.consent.verify
|
|
257
|
+
* @signature b.consent.verify(opts)
|
|
258
|
+
* @since 0.1.0
|
|
259
|
+
* @status stable
|
|
260
|
+
* @compliance gdpr, soc2
|
|
261
|
+
* @related b.audit.verify, b.consent.grant
|
|
262
|
+
*
|
|
263
|
+
* Verify the consent_log hash chain end-to-end. Recomputes each row's
|
|
264
|
+
* `rowHash` from the sealed-form columns + nonce + `prevHash`, walking
|
|
265
|
+
* from genesis to tip. Returns `{ ok, rowsVerified, breakAt? }` — a
|
|
266
|
+
* regulator-ready integrity check that auditors can run without
|
|
267
|
+
* holding the vault key.
|
|
268
|
+
*
|
|
269
|
+
* @opts
|
|
270
|
+
* from: number, // optional monotonicCounter floor
|
|
271
|
+
* to: number, // optional monotonicCounter ceiling
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* var report = await b.consent.verify();
|
|
275
|
+
* if (!report.ok) {
|
|
276
|
+
* console.error("consent chain break at row", report.breakAt);
|
|
277
|
+
* }
|
|
278
|
+
* // → { ok: true, rowsVerified: 1024 }
|
|
279
|
+
*/
|
|
133
280
|
async function verify(opts) {
|
|
134
281
|
return await auditChain.verifyChain(
|
|
135
282
|
function (sql, params) { return clusterStorage.executeAll(sql, params || []); },
|
package/lib/constants.js
CHANGED