@blamejs/core 0.15.12 → 0.15.14
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 +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +45 -29
- package/lib/acme.js +6 -5
- package/lib/agent-event-bus.js +18 -4
- package/lib/agent-idempotency.js +7 -6
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-snapshot.js +32 -2
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-capability.js +1 -6
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-dp.js +1 -5
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-input.js +2 -2
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +14 -24
- package/lib/archive-tar-read.js +56 -24
- package/lib/archive.js +6 -12
- package/lib/arg-parser.js +7 -6
- package/lib/asn1-der.js +70 -22
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +312 -33
- package/lib/audit-chain.js +183 -54
- package/lib/audit-daily-review.js +36 -16
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +258 -0
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +91 -2
- package/lib/auth/access-lock.js +7 -29
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +29 -36
- package/lib/auth/fido-mds3.js +14 -16
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +15 -17
- package/lib/auth/lockout.js +24 -27
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +4 -4
- package/lib/auth/password.js +8 -2
- package/lib/auth/saml.js +70 -40
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +25 -5
- package/lib/auth/status-list.js +21 -9
- package/lib/auth/step-up.js +15 -13
- package/lib/auth-bot-challenge.js +27 -39
- package/lib/backup/bundle.js +18 -20
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +55 -67
- package/lib/backup/manifest.js +7 -1
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/break-glass.js +41 -22
- package/lib/cache.js +7 -18
- package/lib/cbor.js +36 -12
- package/lib/cdn-cache-control.js +15 -12
- package/lib/cert.js +8 -13
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +10 -2
- package/lib/client-hints.js +7 -9
- package/lib/cloud-events.js +43 -33
- package/lib/cluster-storage.js +9 -41
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +84 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +2 -12
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +11 -21
- package/lib/compliance.js +3 -10
- package/lib/config-drift.js +24 -20
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +21 -12
- package/lib/cookies.js +15 -13
- package/lib/cose.js +39 -11
- package/lib/cra-report.js +1 -11
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +34 -33
- package/lib/crypto.js +138 -7
- package/lib/csp.js +239 -3
- package/lib/daemon.js +46 -42
- package/lib/data-act.js +46 -13
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +75 -49
- package/lib/db.js +66 -27
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +30 -20
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +74 -30
- package/lib/fda-21cfr11.js +42 -13
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +911 -39
- package/lib/gdpr-ropa.js +22 -22
- package/lib/graphql-federation.js +18 -5
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +3 -2
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +17 -14
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +2 -2
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +12 -60
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +74 -128
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +9 -10
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +101 -109
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +3 -2
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +6 -5
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +13 -14
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +9 -12
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +103 -149
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cache.js +5 -12
- package/lib/http-client-cookie-jar.js +35 -16
- package/lib/http-client.js +233 -74
- package/lib/http-message-signature.js +3 -2
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +12 -27
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +30 -23
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +4 -3
- package/lib/mail-auth.js +43 -69
- package/lib/mail-bimi.js +35 -55
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +15 -14
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +17 -30
- package/lib/mail-helo.js +4 -7
- package/lib/mail-journal.js +13 -9
- package/lib/mail-mdn.js +12 -7
- package/lib/mail-rbl.js +7 -12
- package/lib/mail-scan.js +5 -7
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +39 -52
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +98 -2
- package/lib/mail-spam-score.js +9 -12
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +28 -13
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +78 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +7 -5
- package/lib/mdoc.js +36 -14
- package/lib/metrics.js +46 -33
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +13 -24
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +41 -52
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +28 -28
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +73 -23
- package/lib/middleware/daily-byte-quota.js +12 -30
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +35 -4
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +45 -44
- package/lib/middleware/scim-server.js +5 -3
- package/lib/middleware/security-headers.js +33 -7
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +14 -29
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +125 -41
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +98 -7
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +18 -17
- package/lib/network-heartbeat.js +3 -2
- package/lib/network-smtp-policy.js +52 -46
- package/lib/network-tls.js +67 -47
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +2 -12
- package/lib/nist-crosswalk.js +1 -1
- package/lib/nonce-store.js +81 -3
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +31 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +6 -47
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +14 -75
- package/lib/object-store/http-put.js +2 -7
- package/lib/object-store/http-request.js +157 -0
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +10 -79
- package/lib/observability-otlp-exporter.js +29 -29
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +54 -41
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/parsers/safe-yaml.js +28 -9
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +9 -4
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +334 -29
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +24 -22
- package/lib/retention.js +23 -9
- package/lib/router.js +17 -4
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +14 -28
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +51 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +13 -27
- package/lib/sandbox-worker.js +15 -2
- package/lib/sandbox.js +4 -3
- package/lib/scheduler.js +22 -13
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +4 -11
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +219 -7
- package/lib/sql.js +228 -81
- package/lib/sse.js +6 -10
- package/lib/static.js +201 -111
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +275 -16
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +53 -38
- package/lib/tsa.js +16 -17
- package/lib/validate-opts.js +154 -8
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +34 -49
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +3 -8
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +59 -19
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +8 -4
- package/lib/worm.js +3 -4
- package/lib/ws-client.js +65 -56
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/csp.js
CHANGED
|
@@ -52,7 +52,12 @@
|
|
|
52
52
|
var nodeCrypto = require("node:crypto");
|
|
53
53
|
var validateOpts = require("./validate-opts");
|
|
54
54
|
var bCrypto = require("./crypto");
|
|
55
|
+
var pick = require("./pick");
|
|
55
56
|
var { defineClass } = require("./framework-error");
|
|
57
|
+
// The strict framework defaults the per-route merge derives from. One-
|
|
58
|
+
// directional edge: security-headers requires only request-helpers +
|
|
59
|
+
// validate-opts, never csp, so this is not a cycle (§9 top-of-file require).
|
|
60
|
+
var securityHeaderDefaults = require("./middleware/security-headers");
|
|
56
61
|
|
|
57
62
|
var CspError = defineClass("CspError", { alwaysPermanent: true });
|
|
58
63
|
|
|
@@ -78,6 +83,61 @@ var ALL_DIRECTIVES = [
|
|
|
78
83
|
var UNSAFE_KEYWORDS = ["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'"];
|
|
79
84
|
var CATCH_ALL_SOURCES = ["*", "https:"];
|
|
80
85
|
|
|
86
|
+
// _parseCspString — split a CSP header value into an ordered
|
|
87
|
+
// { directive: [sources] } map. Tolerant reader (defensive tier): a
|
|
88
|
+
// malformed base yields whatever directives DO parse. The merge never
|
|
89
|
+
// re-validates these base sources (they are the trusted default), so the
|
|
90
|
+
// parser only needs to be faithful, not strict. First write wins on a
|
|
91
|
+
// duplicate directive (matches UA "honor the first").
|
|
92
|
+
function _parseCspString(value) {
|
|
93
|
+
var out = {};
|
|
94
|
+
if (typeof value !== "string") return out;
|
|
95
|
+
var segments = value.split(";");
|
|
96
|
+
for (var i = 0; i < segments.length; i += 1) {
|
|
97
|
+
var tokens = segments[i].trim().split(/\s+/);
|
|
98
|
+
var name = tokens[0];
|
|
99
|
+
if (!name) continue;
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(out, name)) continue;
|
|
101
|
+
out[name] = tokens.slice(1);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// _parsePermissionsPolicyString — split a Permissions-Policy header value
|
|
107
|
+
// into a { feature: "allowlist" } map plus the feature order, preserving
|
|
108
|
+
// each feature's RFC-9651 value-list verbatim ("()" / "*" / "(self ...)").
|
|
109
|
+
function _parsePermissionsPolicyString(value) {
|
|
110
|
+
var out = {};
|
|
111
|
+
var order = [];
|
|
112
|
+
if (typeof value !== "string") return { map: out, order: order };
|
|
113
|
+
// Split on the literal comma (linear) — the per-item .trim() below handles
|
|
114
|
+
// the surrounding whitespace, so the prior `/\s*,\s*/` was redundant and
|
|
115
|
+
// ran in O(n^2) on a comma-less run of whitespace (js/polynomial-redos).
|
|
116
|
+
var parts = value.split(",");
|
|
117
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
118
|
+
var p = parts[i].trim();
|
|
119
|
+
if (!p) continue;
|
|
120
|
+
var eq = p.indexOf("=");
|
|
121
|
+
if (eq === -1) continue;
|
|
122
|
+
var feature = p.slice(0, eq).trim();
|
|
123
|
+
var allow = p.slice(eq + 1).trim();
|
|
124
|
+
if (!feature) continue;
|
|
125
|
+
if (!Object.prototype.hasOwnProperty.call(out, feature)) order.push(feature);
|
|
126
|
+
out[feature] = allow;
|
|
127
|
+
}
|
|
128
|
+
return { map: out, order: order };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// RFC-9651 value-list shape for ONE Permissions-Policy feature value (no
|
|
132
|
+
// leading "feature=" — just the allowlist): "*", "()", "self", or a
|
|
133
|
+
// parenthesised origin list "(self \"https://x\")". A hostile value with a
|
|
134
|
+
// comma / CRLF can't pass (no comma inside, the parens are balanced-only).
|
|
135
|
+
var PP_VALUE_RE = /^(?:\*|self|\([^),\r\n]*\))$/;
|
|
136
|
+
// Permissions-Policy feature names are RFC-9651 structured-field tokens —
|
|
137
|
+
// ASCII lowercase, hyphenated. Reject anything else so a hostile key can't
|
|
138
|
+
// inject a comma / CRLF into the header.
|
|
139
|
+
var PP_FEATURE_RE = /^[a-z][a-z0-9-]*$/; // allow:duplicate-regex — generic ASCII lowercase-hyphen token shape; shared shape, distinct domain (Permissions-Policy feature name)
|
|
140
|
+
|
|
81
141
|
/**
|
|
82
142
|
* @primitive b.csp.build
|
|
83
143
|
* @signature b.csp.build(directives, opts?)
|
|
@@ -184,9 +244,15 @@ function build(directives, opts) {
|
|
|
184
244
|
directives["trusted-types"] = opts.trustedTypesPolicies;
|
|
185
245
|
}
|
|
186
246
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
return _emitCanonical(directives);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Emit a directive map as a CSP header string in canonical (ALL_DIRECTIVES)
|
|
251
|
+
// order so operators diffing two policies see structural diffs rather than
|
|
252
|
+
// ordering noise. Pure serializer — it does NOT validate sources (build()
|
|
253
|
+
// and mergeDirectives() validate before calling it), so a trusted base
|
|
254
|
+
// policy round-trips through it untouched.
|
|
255
|
+
function _emitCanonical(directives) {
|
|
190
256
|
var out = [];
|
|
191
257
|
for (var di = 0; di < ALL_DIRECTIVES.length; di += 1) {
|
|
192
258
|
var d = ALL_DIRECTIVES[di];
|
|
@@ -264,8 +330,178 @@ function hash(scriptBody, alg) {
|
|
|
264
330
|
return "'" + algName + "-" + digest + "'";
|
|
265
331
|
}
|
|
266
332
|
|
|
333
|
+
/**
|
|
334
|
+
* @primitive b.csp.mergeDirectives
|
|
335
|
+
* @signature b.csp.mergeDirectives(base, additions, opts?)
|
|
336
|
+
* @since 0.15.13
|
|
337
|
+
* @status stable
|
|
338
|
+
* @related b.csp.build, b.csp.mergePermissionsPolicy
|
|
339
|
+
*
|
|
340
|
+
* Derive a per-route CSP from a strict base by ADDING hosts to named
|
|
341
|
+
* directives, leaving every other directive exactly as the base. The fix for
|
|
342
|
+
* the "load a third-party SDK on one route" case: take the framework's strict
|
|
343
|
+
* default (pass `base` omitted / `undefined`) and add `https://js.stripe.com`
|
|
344
|
+
* to `script-src` + `frame-src` for the checkout route only, without
|
|
345
|
+
* re-typing the whole policy or relaxing `frame-ancestors` / `object-src` /
|
|
346
|
+
* `base-uri`.
|
|
347
|
+
*
|
|
348
|
+
* Additive only: each `additions[directive]` array is APPENDED (de-duped) to
|
|
349
|
+
* that directive's existing sources; a directive absent from the base is
|
|
350
|
+
* seeded from `default-src` (or `'self'`) first so it never lands wide-open.
|
|
351
|
+
* Only the ADDED sources are validated (CR/LF/NUL, catch-all `*`/`https:`,
|
|
352
|
+
* `data:` in img/media/font, unsafe-* in script directives) — the trusted
|
|
353
|
+
* base round-trips untouched. Returns a policy string for the middleware
|
|
354
|
+
* `csp:` opt. Throws `CspError` on an unknown directive, a hostile directive
|
|
355
|
+
* name, or a rejected added source.
|
|
356
|
+
*
|
|
357
|
+
* @opts
|
|
358
|
+
* acknowledgeUnsafe: boolean, // allow an added 'unsafe-*' in a script directive
|
|
359
|
+
* allowDataImages: boolean, // allow an added data: in img-src/media-src/font-src
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* // Admit Stripe on the checkout route's script + frame directives only.
|
|
363
|
+
* var routeCsp = b.csp.mergeDirectives(undefined, {
|
|
364
|
+
* "script-src": ["https://js.stripe.com"],
|
|
365
|
+
* "frame-src": ["https://js.stripe.com"],
|
|
366
|
+
* });
|
|
367
|
+
* // -> the strict default with those two hosts appended; everything else unchanged
|
|
368
|
+
*/
|
|
369
|
+
function mergeDirectives(base, additions, opts) {
|
|
370
|
+
var baseString = base === undefined ? securityHeaderDefaults.DEFAULT_CSP : base;
|
|
371
|
+
if (typeof baseString !== "string" || baseString.length === 0) {
|
|
372
|
+
throw new CspError("csp/bad-base",
|
|
373
|
+
"csp.mergeDirectives: base must be a non-empty CSP string (or undefined to " +
|
|
374
|
+
"derive from the framework DEFAULT_CSP)");
|
|
375
|
+
}
|
|
376
|
+
if (!additions || typeof additions !== "object" || Array.isArray(additions)) {
|
|
377
|
+
throw new CspError("csp/bad-additions",
|
|
378
|
+
"csp.mergeDirectives: additions must be an object keyed by CSP directive name, " +
|
|
379
|
+
"each value a non-empty array of source strings to ADD");
|
|
380
|
+
}
|
|
381
|
+
opts = opts || {};
|
|
382
|
+
validateOpts(opts, ["acknowledgeUnsafe", "allowDataImages"], "csp.mergeDirectives");
|
|
383
|
+
|
|
384
|
+
var addKeys = Object.keys(additions);
|
|
385
|
+
// Validate directive names (proto-safe) + that each addition is a
|
|
386
|
+
// non-empty array, before touching the base.
|
|
387
|
+
for (var ki = 0; ki < addKeys.length; ki += 1) {
|
|
388
|
+
var nm = addKeys[ki];
|
|
389
|
+
if (pick.isPoisonedKey(nm)) {
|
|
390
|
+
throw new CspError("csp/bad-directive-name",
|
|
391
|
+
"csp.mergeDirectives: '" + nm + "' is not a valid directive name");
|
|
392
|
+
}
|
|
393
|
+
if (ALL_DIRECTIVES.indexOf(nm) === -1) {
|
|
394
|
+
throw new CspError("csp/unknown-directive",
|
|
395
|
+
"csp.mergeDirectives: '" + nm + "' is not a recognized CSP3 directive");
|
|
396
|
+
}
|
|
397
|
+
if (!Array.isArray(additions[nm]) || additions[nm].length === 0) {
|
|
398
|
+
throw new CspError("csp/bad-directive-value",
|
|
399
|
+
"csp.mergeDirectives: additions['" + nm + "'] must be a non-empty array of sources");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Validate ONLY the added sources by routing a throwaway additions-only
|
|
403
|
+
// map through build() (reuses every per-source rule). build() may seed
|
|
404
|
+
// Trusted-Types into the copy; we discard its output and keep the original
|
|
405
|
+
// additions for the merge.
|
|
406
|
+
var validationMap = {};
|
|
407
|
+
for (var vk = 0; vk < addKeys.length; vk += 1) validationMap[addKeys[vk]] = additions[addKeys[vk]].slice();
|
|
408
|
+
build(validationMap, { acknowledgeUnsafe: opts.acknowledgeUnsafe === true,
|
|
409
|
+
allowDataImages: opts.allowDataImages === true,
|
|
410
|
+
requireTrustedTypes: false });
|
|
411
|
+
|
|
412
|
+
var directives = _parseCspString(baseString);
|
|
413
|
+
for (var ai = 0; ai < addKeys.length; ai += 1) {
|
|
414
|
+
var name = addKeys[ai];
|
|
415
|
+
var existing = Object.prototype.hasOwnProperty.call(directives, name)
|
|
416
|
+
? directives[name].slice()
|
|
417
|
+
: (directives["default-src"] ? directives["default-src"].slice() : ["'self'"]);
|
|
418
|
+
var added = additions[name];
|
|
419
|
+
// A directive value of 'none' MUST stand alone (CSP3 §2.3.1) — appending a
|
|
420
|
+
// host to it emits the malformed "'none' https://x". When merging real
|
|
421
|
+
// sources in, the added sources supersede 'none', so drop it first.
|
|
422
|
+
if (added.length && existing.length === 1 && existing[0] === "'none'") existing = [];
|
|
423
|
+
for (var si = 0; si < added.length; si += 1) {
|
|
424
|
+
if (existing.indexOf(added[si]) === -1) existing.push(added[si]);
|
|
425
|
+
}
|
|
426
|
+
directives[name] = existing;
|
|
427
|
+
}
|
|
428
|
+
return _emitCanonical(directives);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @primitive b.csp.mergePermissionsPolicy
|
|
433
|
+
* @signature b.csp.mergePermissionsPolicy(base, overrides, opts?)
|
|
434
|
+
* @since 0.15.13
|
|
435
|
+
* @status stable
|
|
436
|
+
* @related b.csp.mergeDirectives, b.csp.build
|
|
437
|
+
*
|
|
438
|
+
* Derive a per-route Permissions-Policy from a strict base by replacing the
|
|
439
|
+
* allowlist of NAMED features only, leaving every other feature at its `()`
|
|
440
|
+
* deny default. The companion to `mergeDirectives` for the Permissions-Policy
|
|
441
|
+
* header: re-enable `payment` to `(self "https://js.stripe.com")` on the
|
|
442
|
+
* checkout route while `camera` / `microphone` / `geolocation` stay denied.
|
|
443
|
+
*
|
|
444
|
+
* Each override value is validated as an RFC-9651 feature value-list (`*`,
|
|
445
|
+
* `()`, `self`, or a parenthesised origin list) — a value carrying a comma or
|
|
446
|
+
* CR/LF is refused so it can't inject a second feature or a header break. A
|
|
447
|
+
* feature not present in the base is added (opting it in); one present is
|
|
448
|
+
* replaced. Returns a header string for the middleware `permissionsPolicy:`
|
|
449
|
+
* opt. Throws `CspError` on a hostile feature name or a malformed value.
|
|
450
|
+
*
|
|
451
|
+
* @opts
|
|
452
|
+
* (none)
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* var routePp = b.csp.mergePermissionsPolicy(undefined, {
|
|
456
|
+
* payment: '(self "https://js.stripe.com")',
|
|
457
|
+
* });
|
|
458
|
+
* // -> the strict default with payment re-enabled to that allowlist; all else denied
|
|
459
|
+
*/
|
|
460
|
+
function mergePermissionsPolicy(base, overrides, opts) {
|
|
461
|
+
var baseString = base === undefined
|
|
462
|
+
? securityHeaderDefaults.DEFAULT_PERMISSIONS.join(", ")
|
|
463
|
+
: base;
|
|
464
|
+
if (typeof baseString !== "string" || baseString.length === 0) {
|
|
465
|
+
throw new CspError("csp/bad-base",
|
|
466
|
+
"csp.mergePermissionsPolicy: base must be a non-empty Permissions-Policy string " +
|
|
467
|
+
"(or undefined to derive from the framework default)");
|
|
468
|
+
}
|
|
469
|
+
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
|
|
470
|
+
throw new CspError("csp/bad-overrides",
|
|
471
|
+
"csp.mergePermissionsPolicy: overrides must be an object keyed by feature name, " +
|
|
472
|
+
"each value an RFC-9651 allowlist string");
|
|
473
|
+
}
|
|
474
|
+
if (opts !== undefined) validateOpts(opts || {}, [], "csp.mergePermissionsPolicy");
|
|
475
|
+
|
|
476
|
+
var parsed = _parsePermissionsPolicyString(baseString);
|
|
477
|
+
var map = parsed.map;
|
|
478
|
+
var order = parsed.order;
|
|
479
|
+
var keys = Object.keys(overrides);
|
|
480
|
+
for (var ki = 0; ki < keys.length; ki += 1) {
|
|
481
|
+
var feature = keys[ki];
|
|
482
|
+
if (pick.isPoisonedKey(feature) ||
|
|
483
|
+
!PP_FEATURE_RE.test(feature)) { // allow:regex-no-length-cap — anchored token shape (no backtracking); a feature name is a short RFC-9651 token
|
|
484
|
+
throw new CspError("csp/bad-feature-name",
|
|
485
|
+
"csp.mergePermissionsPolicy: '" + feature + "' is not a valid feature name");
|
|
486
|
+
}
|
|
487
|
+
var value = overrides[feature];
|
|
488
|
+
if (typeof value !== "string" || !PP_VALUE_RE.test(value)) { // allow:regex-no-length-cap — anchored alternation (no backtracking); a feature value is a short RFC-9651 value-list
|
|
489
|
+
throw new CspError("csp/bad-feature-value",
|
|
490
|
+
"csp.mergePermissionsPolicy: " + feature + " value must be one of *, (), self, or a " +
|
|
491
|
+
"parenthesised origin list (got " + JSON.stringify(value) + ")");
|
|
492
|
+
}
|
|
493
|
+
if (!Object.prototype.hasOwnProperty.call(map, feature)) order.push(feature);
|
|
494
|
+
map[feature] = value;
|
|
495
|
+
}
|
|
496
|
+
var out = [];
|
|
497
|
+
for (var oi = 0; oi < order.length; oi += 1) out.push(order[oi] + "=" + map[order[oi]]);
|
|
498
|
+
return out.join(", ");
|
|
499
|
+
}
|
|
500
|
+
|
|
267
501
|
module.exports = {
|
|
268
502
|
build: build,
|
|
503
|
+
mergeDirectives: mergeDirectives,
|
|
504
|
+
mergePermissionsPolicy: mergePermissionsPolicy,
|
|
269
505
|
nonce: nonce,
|
|
270
506
|
hash: hash,
|
|
271
507
|
CspError: CspError,
|
package/lib/daemon.js
CHANGED
|
@@ -42,7 +42,6 @@ var nodePath = require("node:path");
|
|
|
42
42
|
var numericBounds = require("./numeric-bounds");
|
|
43
43
|
var appShutdown = require("./app-shutdown");
|
|
44
44
|
var processSpawn = require("./process-spawn");
|
|
45
|
-
var lazyRequire = require("./lazy-require");
|
|
46
45
|
var safeAsync = require("./safe-async");
|
|
47
46
|
var atomicFile = require("./atomic-file");
|
|
48
47
|
var validateOpts = require("./validate-opts");
|
|
@@ -50,7 +49,7 @@ var C = require("./constants");
|
|
|
50
49
|
var { boot } = require("./log");
|
|
51
50
|
var { defineClass } = require("./framework-error");
|
|
52
51
|
|
|
53
|
-
var
|
|
52
|
+
var auditEmit = require("./audit-emit");
|
|
54
53
|
|
|
55
54
|
var DaemonError = defineClass("DaemonError", { alwaysPermanent: true });
|
|
56
55
|
var log = boot("daemon");
|
|
@@ -63,13 +62,7 @@ var DEFAULT_POLL_MS = 100;
|
|
|
63
62
|
var DEFAULT_LOG_FILE_MODE = 0o600;
|
|
64
63
|
|
|
65
64
|
function _safeAuditEmit(action, outcome, metadata) {
|
|
66
|
-
|
|
67
|
-
audit().safeEmit({
|
|
68
|
-
action: action,
|
|
69
|
-
outcome: outcome || "success",
|
|
70
|
-
metadata: metadata || {},
|
|
71
|
-
});
|
|
72
|
-
} catch (_e) { /* drop-silent — by design */ }
|
|
65
|
+
auditEmit.emit(action, metadata, outcome);
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
function _isLivePid(pid) {
|
|
@@ -80,48 +73,59 @@ function _isLivePid(pid) {
|
|
|
80
73
|
|
|
81
74
|
function _readPidFile(pidFile) {
|
|
82
75
|
try {
|
|
83
|
-
|
|
76
|
+
// Same fd-safe + capped + symlink-refusing read as app-shutdown's lockfile
|
|
77
|
+
// reader (one shape): a PID file is never a legit symlink mount, so
|
|
78
|
+
// refuseSymlink is safe; any throw → null ("nothing live there").
|
|
79
|
+
var raw = atomicFile.fdSafeReadSync(pidFile, { maxBytes: C.BYTES.kib(1), refuseSymlink: true, encoding: "utf8" });
|
|
84
80
|
var pid = parseInt(String(raw).trim(), 10);
|
|
85
81
|
return isFinite(pid) && pid > 0 ? pid : null;
|
|
86
82
|
} catch (_e) { return null; }
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
function _validateStartOpts(opts) {
|
|
90
|
-
validateOpts.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
86
|
+
validateOpts.shape(opts, {
|
|
87
|
+
pidFile: { rule: "required-string", code: "daemon/bad-pid-file",
|
|
88
|
+
label: "daemon.start: opts.pidFile (absolute path recommended)" },
|
|
89
|
+
logFile: { rule: "optional-string", code: "daemon/bad-log-file",
|
|
90
|
+
label: "daemon.start: opts.logFile" },
|
|
91
|
+
signals: function (value) {
|
|
92
|
+
validateOpts.optionalNonEmptyStringArray(value,
|
|
93
|
+
"daemon.start: opts.signals", DaemonError, "daemon/bad-signals");
|
|
94
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
95
|
+
throw new DaemonError("daemon/bad-signals",
|
|
96
|
+
"daemon.start: opts.signals must be a non-empty array of POSIX signal names");
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
command: { rule: "optional-string", code: "daemon/bad-command",
|
|
100
|
+
label: "daemon.start: opts.command (path to executable)" },
|
|
101
|
+
args: function (value) {
|
|
102
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
103
|
+
throw new DaemonError("daemon/bad-args",
|
|
104
|
+
"daemon.start: opts.args must be an array of strings when present");
|
|
105
|
+
}
|
|
106
|
+
if (opts.command === undefined && value !== undefined) {
|
|
107
|
+
throw new DaemonError("daemon/bad-args",
|
|
108
|
+
"daemon.start: opts.args requires opts.command");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
}, "daemon.start", DaemonError, "daemon/bad-opts");
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
function _validateStopOpts(opts) {
|
|
116
|
-
validateOpts.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
validateOpts.shape(opts, {
|
|
116
|
+
pidFile: { rule: "required-string", code: "daemon/bad-pid-file",
|
|
117
|
+
label: "daemon.stop: opts.pidFile" },
|
|
118
|
+
signal: { rule: "optional-string", code: "daemon/bad-signal",
|
|
119
|
+
label: "daemon.stop: opts.signal" },
|
|
120
|
+
timeoutMs: function (value) {
|
|
121
|
+
numericBounds.requirePositiveFiniteIntIfPresent(value,
|
|
122
|
+
"daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
|
|
123
|
+
},
|
|
124
|
+
pollMs: function (value) {
|
|
125
|
+
numericBounds.requirePositiveFiniteIntIfPresent(value,
|
|
126
|
+
"daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
|
|
127
|
+
},
|
|
128
|
+
}, "daemon.stop", DaemonError, "daemon/bad-opts");
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
function _maybeReapStale(pidFile) {
|
package/lib/data-act.js
CHANGED
|
@@ -89,6 +89,32 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
|
|
|
89
89
|
"booking",
|
|
90
90
|
]);
|
|
91
91
|
|
|
92
|
+
// Is `recipient` a designated DMA gatekeeper? Match the gatekeeper token as a
|
|
93
|
+
// DNS LABEL of the recipient's host authority — any position, including the
|
|
94
|
+
// FINAL label. A substring `indexOf(g + ".")` test missed a gatekeeper that is
|
|
95
|
+
// the last label (e.g. "blog.google" / "drive.google" on the real ".google"
|
|
96
|
+
// TLD, or "data.amazon"), letting an Art 32 §1 third-party share through — and
|
|
97
|
+
// also false-flagged a non-gatekeeper whose name merely contained the token
|
|
98
|
+
// ("notgoogle.com"). Label matching closes the bypass and drops the false
|
|
99
|
+
// positive. A bare name ("Google") still matches exactly. URL / userinfo / port
|
|
100
|
+
// forms are normalized to the host authority first.
|
|
101
|
+
function _isGatekeeperRecipient(recipient) {
|
|
102
|
+
var s = String(recipient).toLowerCase().trim();
|
|
103
|
+
var scheme = s.indexOf("://");
|
|
104
|
+
if (scheme !== -1) s = s.slice(scheme + 3);
|
|
105
|
+
s = s.split("/")[0].split("?")[0].split("#")[0]; // drop path / query / fragment
|
|
106
|
+
var at = s.lastIndexOf("@");
|
|
107
|
+
if (at !== -1) s = s.slice(at + 1); // drop userinfo
|
|
108
|
+
s = s.split(":")[0]; // drop port
|
|
109
|
+
if (s.length === 0) return false;
|
|
110
|
+
var labels = s.split(".");
|
|
111
|
+
return DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
112
|
+
if (s === g) return true;
|
|
113
|
+
for (var i = 0; i < labels.length; i++) { if (labels[i] === g) return true; }
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* @primitive b.dataAct.declareProduct
|
|
94
120
|
* @signature b.dataAct.declareProduct(opts)
|
|
@@ -116,9 +142,12 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
|
|
|
116
142
|
* });
|
|
117
143
|
*/
|
|
118
144
|
function declareProduct(opts) {
|
|
119
|
-
validateOpts.
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
validateOpts.shape(opts, {
|
|
146
|
+
productId: { rule: "required-string", label: "productId", code: "dataact/no-product-id" },
|
|
147
|
+
dataHolder: { rule: "required-string", label: "dataHolder", code: "dataact/no-data-holder" },
|
|
148
|
+
productKind: "optional-string",
|
|
149
|
+
dataScope: "optional-string",
|
|
150
|
+
}, "dataAct.declareProduct", DataActError, "dataact/bad-opts");
|
|
122
151
|
var kind = opts.productKind || "connected-product";
|
|
123
152
|
if (kind !== "connected-product" && kind !== "related-service") {
|
|
124
153
|
throw new DataActError("dataact/bad-kind",
|
|
@@ -218,14 +247,15 @@ function recordUserAccess(opts) {
|
|
|
218
247
|
function shareWithThirdParty(opts) {
|
|
219
248
|
validateOpts.requireObject(opts, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
|
|
220
249
|
_requireProduct(opts.productId, "shareWithThirdParty");
|
|
221
|
-
validateOpts.
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
validateOpts.shape(opts, {
|
|
251
|
+
productId: { rule: "required-string", label: "productId", code: "dataact/no-product-id" },
|
|
252
|
+
userId: { rule: "required-string", label: "userId", code: "dataact/no-user-id" },
|
|
253
|
+
recipient: { rule: "required-string", label: "recipient", code: "dataact/no-recipient" },
|
|
254
|
+
scope: { rule: "required-string", label: "scope", code: "dataact/no-scope" },
|
|
255
|
+
acceptGatekeeper: "optional-plain-object",
|
|
256
|
+
}, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
|
|
224
257
|
|
|
225
|
-
var
|
|
226
|
-
var isGatekeeper = DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
227
|
-
return recipientLower === g || recipientLower.indexOf(g + ".") !== -1;
|
|
228
|
-
});
|
|
258
|
+
var isGatekeeper = _isGatekeeperRecipient(opts.recipient);
|
|
229
259
|
if (isGatekeeper) {
|
|
230
260
|
if (!opts.acceptGatekeeper || typeof opts.acceptGatekeeper !== "object" ||
|
|
231
261
|
typeof opts.acceptGatekeeper.reason !== "string" ||
|
|
@@ -291,9 +321,12 @@ function shareWithThirdParty(opts) {
|
|
|
291
321
|
* });
|
|
292
322
|
*/
|
|
293
323
|
function recordSwitchRequest(opts) {
|
|
294
|
-
validateOpts.
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
validateOpts.shape(opts, {
|
|
325
|
+
customerId: { rule: "required-string", label: "customerId", code: "dataact/no-customer-id" },
|
|
326
|
+
targetProvider: { rule: "required-string", label: "targetProvider", code: "dataact/no-target" },
|
|
327
|
+
dataSlices: "optional-string-array",
|
|
328
|
+
noticePeriodDays: "optional-non-negative",
|
|
329
|
+
}, "dataAct.recordSwitchRequest", DataActError, "dataact/bad-opts");
|
|
297
330
|
if (!Array.isArray(opts.dataSlices) || opts.dataSlices.length === 0) {
|
|
298
331
|
throw new DataActError("dataact/no-data-slices",
|
|
299
332
|
"recordSwitchRequest: dataSlices must be a non-empty array");
|
package/lib/db-file-lifecycle.js
CHANGED
|
@@ -172,8 +172,11 @@ function fileLifecycle(opts) {
|
|
|
172
172
|
: nodePath.join(opts.dataDir, frameworkFiles.fileName("dbKeyEnc"));
|
|
173
173
|
var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
174
174
|
var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// 0o700 — these hold the DB encryption tmpfs scratch + the data dir
|
|
176
|
+
// (db.key.enc, the decrypted DB while open); a default-mode mkdir leaves
|
|
177
|
+
// them group/other-traversable.
|
|
178
|
+
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
179
|
+
if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
|
|
177
180
|
|
|
178
181
|
var dbPath = null;
|
|
179
182
|
var encKey = null;
|
|
@@ -183,7 +186,9 @@ function fileLifecycle(opts) {
|
|
|
183
186
|
function _loadOrGenerateKey() {
|
|
184
187
|
if (encKey) return encKey;
|
|
185
188
|
if (nodeFs.existsSync(keyPath)) {
|
|
186
|
-
|
|
189
|
+
// Cap + fd-bound. NO refuseSymlink: dbKeyPath may be an operator-absolute
|
|
190
|
+
// KMS-fronted volume that legitimately symlinks the sealed key.
|
|
191
|
+
var sealedKey = atomicFile.fdSafeReadSync(keyPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
|
|
187
192
|
var keyB64;
|
|
188
193
|
try { keyB64 = opts.vault.unseal(sealedKey); }
|
|
189
194
|
catch (e) {
|
|
@@ -212,7 +217,10 @@ function fileLifecycle(opts) {
|
|
|
212
217
|
dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
|
|
213
218
|
generateToken(TMP_NAME_BYTES) + ".db");
|
|
214
219
|
if (nodeFs.existsSync(encPath)) {
|
|
215
|
-
|
|
220
|
+
// Cap + fd-bound (deployment-tunable ceiling). NO refuseSymlink: the data
|
|
221
|
+
// dir may be a symlink-mounted volume (k8s PVC). db.enc is AEAD-checked
|
|
222
|
+
// downstream, so the cap is the OOM-before-cap defense.
|
|
223
|
+
var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
216
224
|
if (packed.length < 26) { // minimum envelope length
|
|
217
225
|
throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
|
|
218
226
|
"fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
|
|
@@ -248,7 +256,7 @@ function fileLifecycle(opts) {
|
|
|
248
256
|
catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
|
|
249
257
|
}
|
|
250
258
|
if (!nodeFs.existsSync(dbPath)) return null;
|
|
251
|
-
var plain =
|
|
259
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
252
260
|
var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
253
261
|
atomicFile.writeSync(encPath, packed);
|
|
254
262
|
_emitAudit("flushed", "success", { label: label, bytes: plain.length });
|
|
@@ -269,7 +277,7 @@ function fileLifecycle(opts) {
|
|
|
269
277
|
throw new DbFileLifecycleError("db-file-lifecycle/no-source",
|
|
270
278
|
"fileLifecycle.snapshot: " + dbPath + " is missing");
|
|
271
279
|
}
|
|
272
|
-
var plain =
|
|
280
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
273
281
|
return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
274
282
|
}
|
|
275
283
|
|