@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/dsr.js
CHANGED
|
@@ -114,6 +114,7 @@ var bCrypto = require("./crypto");
|
|
|
114
114
|
var lazyRequire = require("./lazy-require");
|
|
115
115
|
var validateOpts = require("./validate-opts");
|
|
116
116
|
var safeSql = require("./safe-sql");
|
|
117
|
+
var boundedMap = require("./bounded-map");
|
|
117
118
|
var { defineClass } = require("./framework-error");
|
|
118
119
|
|
|
119
120
|
var DsrError = defineClass("DsrError", { alwaysPermanent: true });
|
|
@@ -209,7 +210,7 @@ var POSTURE_DEADLINE_MS = Object.freeze({
|
|
|
209
210
|
// + extension). Read-only at module scope.
|
|
210
211
|
function _deadlineForPosture(posture) {
|
|
211
212
|
if (typeof posture !== "string") return POSTURE_DEADLINE_MS["default"];
|
|
212
|
-
return POSTURE_DEADLINE_MS[posture]
|
|
213
|
+
return Object.prototype.hasOwnProperty.call(POSTURE_DEADLINE_MS, posture) ? POSTURE_DEADLINE_MS[posture] : POSTURE_DEADLINE_MS["default"];
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
function _now() { return Date.now(); }
|
|
@@ -372,10 +373,7 @@ function create(opts) {
|
|
|
372
373
|
} catch (_e) { /* drop-silent — audit sink */ }
|
|
373
374
|
}
|
|
374
375
|
|
|
375
|
-
|
|
376
|
-
try { observability().safeEvent("dsr." + verb, n || 1, labels || {}); }
|
|
377
|
-
catch (_e) { /* drop-silent */ }
|
|
378
|
-
}
|
|
376
|
+
var _emitMetric = observability().namespaced("dsr");
|
|
379
377
|
|
|
380
378
|
function _newTicketId() {
|
|
381
379
|
var ts = String(Date.now()).slice(-7); // last 7 chars of unix-ms timestamp; collision-resistant when paired with the random suffix
|
|
@@ -894,10 +892,10 @@ function memoryTicketStore() {
|
|
|
894
892
|
var byId = new Map();
|
|
895
893
|
return {
|
|
896
894
|
insert: async function (ticket) {
|
|
897
|
-
|
|
895
|
+
boundedMap.requireAbsent(byId, ticket.id, function () {
|
|
898
896
|
throw new DsrError("dsr/duplicate-ticket-id",
|
|
899
897
|
"memoryTicketStore: duplicate ticket id " + ticket.id);
|
|
900
|
-
}
|
|
898
|
+
});
|
|
901
899
|
byId.set(ticket.id, Object.assign({}, ticket));
|
|
902
900
|
},
|
|
903
901
|
get: async function (id) {
|
|
@@ -919,10 +917,10 @@ function memoryTicketStore() {
|
|
|
919
917
|
return out;
|
|
920
918
|
},
|
|
921
919
|
update: async function (id, ticket) {
|
|
922
|
-
|
|
920
|
+
boundedMap.requirePresent(byId, id, function () {
|
|
923
921
|
throw new DsrError("dsr/ticket-not-found",
|
|
924
922
|
"memoryTicketStore: ticket " + id + " not found for update");
|
|
925
|
-
}
|
|
923
|
+
});
|
|
926
924
|
byId.set(id, Object.assign({}, ticket));
|
|
927
925
|
},
|
|
928
926
|
delete: async function (id) {
|
|
@@ -1023,10 +1021,8 @@ function _ensureDsrSealTable() {
|
|
|
1023
1021
|
function dbTicketStore(opts) {
|
|
1024
1022
|
opts = opts || {};
|
|
1025
1023
|
var db = opts.db;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
"dbTicketStore: opts.db must be a b.db-shaped handle (with runSql + prepare)");
|
|
1029
|
-
}
|
|
1024
|
+
validateOpts.requireMethods(db, ["runSql", "prepare"],
|
|
1025
|
+
"dbTicketStore: opts.db (b.db-shaped handle)", DsrError, "dsr/bad-db");
|
|
1030
1026
|
var tableRaw = opts.table || "dsr_tickets";
|
|
1031
1027
|
var qTable, qEmailIdx, qStatusIdx;
|
|
1032
1028
|
try {
|
|
@@ -1202,12 +1198,26 @@ function dbTicketStore(opts) {
|
|
|
1202
1198
|
SUBJECT_FILTER_SPEC.forEach(function (spec) {
|
|
1203
1199
|
var supplied = filter.subject[spec.key];
|
|
1204
1200
|
if (!supplied) return;
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1201
|
+
if (!vaulted) {
|
|
1202
|
+
conds.push(spec.plainCol + " = " + spec.param);
|
|
1203
|
+
params[spec.param] = supplied;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
// Vaulted: match BOTH the active keyed-MAC digest AND the legacy
|
|
1207
|
+
// salted-sha3 digest via the dual-read, so a ticket written before the
|
|
1208
|
+
// keyed-MAC default flip is still FOUND. A single-value equality on the
|
|
1209
|
+
// keyed-MAC digest silently skips those pre-flip rows, so an Art.17
|
|
1210
|
+
// erasure purge (list-by-subject → delete) would leave the subject's
|
|
1211
|
+
// legacy-hashed PII-bearing tickets behind. Mirrors subject.eraseHard.
|
|
1212
|
+
var cand = cryptoField().lookupHashCandidates(DSR_SEAL_TABLE, spec.sealField, supplied);
|
|
1213
|
+
var values = cand && cand.values ? cand.values : [];
|
|
1214
|
+
if (values.length === 0) return;
|
|
1215
|
+
var placeholders = values.map(function (v, i) {
|
|
1216
|
+
var p = spec.param + "_" + i;
|
|
1217
|
+
params[p] = v;
|
|
1218
|
+
return p;
|
|
1219
|
+
});
|
|
1220
|
+
conds.push(spec.hashCol + " IN (" + placeholders.join(", ") + ")");
|
|
1211
1221
|
});
|
|
1212
1222
|
}
|
|
1213
1223
|
|
|
@@ -1459,7 +1469,7 @@ var STATE_RULES = Object.freeze({
|
|
|
1459
1469
|
function stateRules(state) {
|
|
1460
1470
|
if (typeof state !== "string" || state.length === 0) return null;
|
|
1461
1471
|
// Direct posture-name lookup first
|
|
1462
|
-
if (STATE_RULES
|
|
1472
|
+
if (Object.prototype.hasOwnProperty.call(STATE_RULES, state)) return Object.assign({}, STATE_RULES[state]);
|
|
1463
1473
|
// 2-letter state abbreviation lookup (case-insensitive)
|
|
1464
1474
|
var u = state.toUpperCase();
|
|
1465
1475
|
var keys = Object.keys(STATE_RULES);
|
package/lib/early-hints.js
CHANGED
|
@@ -170,6 +170,13 @@ function send(res, opts) {
|
|
|
170
170
|
throw new EarlyHintsError("early-hints/bad-header-value",
|
|
171
171
|
"earlyHints.send: header '" + name + "' must be a string or string[]", true);
|
|
172
172
|
}
|
|
173
|
+
var vals = Array.isArray(canonical[name]) ? canonical[name] : [canonical[name]];
|
|
174
|
+
for (var vi = 0; vi < vals.length; vi += 1) {
|
|
175
|
+
if (typeof vals[vi] === "string" && _hasHeaderInjection(vals[vi])) {
|
|
176
|
+
throw new EarlyHintsError("early-hints/bad-header-value",
|
|
177
|
+
"earlyHints.send: header '" + name + "' value contains a CR/LF/NUL header-injection character", true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
173
180
|
headers[name] = canonical[name];
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -182,9 +189,21 @@ function send(res, opts) {
|
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
|
|
192
|
+
// A header value reaching res.writeEarlyHints must carry no CR/LF/NUL — those
|
|
193
|
+
// split the interim response into attacker-chosen headers. Node rejects them
|
|
194
|
+
// too, but screen defensively (matching the cookies / security-headers
|
|
195
|
+
// boundary) so the framework surfaces a typed error, not a raw Node throw.
|
|
196
|
+
function _hasHeaderInjection(s) {
|
|
197
|
+
return s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1 || s.indexOf("\0") !== -1;
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
function _validateLink(linkValue, idx) {
|
|
186
201
|
validateOpts.requireNonEmptyString(linkValue, "earlyHints.send.link[" + idx + "]",
|
|
187
202
|
EarlyHintsError, "early-hints/bad-link");
|
|
203
|
+
if (_hasHeaderInjection(linkValue)) {
|
|
204
|
+
throw new EarlyHintsError("early-hints/bad-link",
|
|
205
|
+
"link[" + idx + "] contains a CR/LF/NUL header-injection character");
|
|
206
|
+
}
|
|
188
207
|
if (linkValue.length > LINK_MAX_BYTES) {
|
|
189
208
|
throw new EarlyHintsError("early-hints/bad-link",
|
|
190
209
|
"link[" + idx + "] exceeds " + LINK_MAX_BYTES + " bytes");
|
package/lib/eat.js
CHANGED
|
@@ -205,7 +205,11 @@ async function verify(eat, opts) {
|
|
|
205
205
|
// Debug status — refuse a token that can't prove debug is disabled.
|
|
206
206
|
if (opts.requireDebugDisabled === true) {
|
|
207
207
|
var ds = raw.get(EAT.dbgstat);
|
|
208
|
-
|
|
208
|
+
// Accept ONLY a defined disabled enum value (1..4). `ds < disabled` alone
|
|
209
|
+
// let any out-of-range value >= 1 (5, 99, non-integers) pass as "disabled"
|
|
210
|
+
// — a fail-open on an undefined dbgstat (RFC 9711 §3.3 defines 0..4 only).
|
|
211
|
+
if (typeof ds !== "number" || !Number.isInteger(ds) ||
|
|
212
|
+
ds < DBGSTAT.disabled || ds > DBGSTAT["disabled-fully-and-permanently"]) {
|
|
209
213
|
throw new EatError("eat/debug-not-disabled",
|
|
210
214
|
"eat.verify: requireDebugDisabled — dbgstat is " +
|
|
211
215
|
(ds === undefined ? "absent" : (DBGSTAT_BY_VALUE[ds] || ds)) + ", not a disabled state");
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* - externaldb.migrate.lock.released { holder }
|
|
50
50
|
*/
|
|
51
51
|
var nodePath = require("node:path");
|
|
52
|
+
var moduleLoader = require("./module-loader");
|
|
52
53
|
var atomicFile = require("./atomic-file");
|
|
53
54
|
var canonicalJson = require("./canonical-json");
|
|
54
55
|
var { sha3Hash } = require("./crypto");
|
|
@@ -379,19 +380,10 @@ function _list(dir) {
|
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
function _loadMigration(file, dir) {
|
|
382
|
-
var
|
|
383
|
-
|
|
384
|
-
// new content. Matches lib/migrations.js semantics.
|
|
385
|
-
try { delete require.cache[require.resolve(fullPath)]; } catch (_e) { /* not yet cached */ }
|
|
386
|
-
var mod;
|
|
387
|
-
// Operator-supplied migration file — dynamic by design, won't survive
|
|
388
|
-
// SEA / pkg bundling. External DB migration tooling is host-CLI scope,
|
|
389
|
-
// not framework-internal scope.
|
|
390
|
-
try { mod = require(fullPath); } // allow:dynamic-require — operator-supplied migration
|
|
391
|
-
catch (e) {
|
|
392
|
-
throw _err("externaldb-migrate/load-failed",
|
|
383
|
+
var mod = moduleLoader.requireFresh(nodePath.join(dir, file), function (e) {
|
|
384
|
+
return _err("externaldb-migrate/load-failed",
|
|
393
385
|
"migration '" + file + "' failed to load: " + ((e && e.message) || String(e)));
|
|
394
|
-
}
|
|
386
|
+
});
|
|
395
387
|
if (!mod || typeof mod.up !== "function") {
|
|
396
388
|
throw _err("externaldb-migrate/missing-up",
|
|
397
389
|
"migration '" + file + "' must export an `up(xdb, ctx)` function");
|
|
@@ -437,16 +429,23 @@ function _resolveBackendName(opts) {
|
|
|
437
429
|
|
|
438
430
|
function create(opts) {
|
|
439
431
|
opts = opts || {};
|
|
440
|
-
validateOpts(opts,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
432
|
+
validateOpts.shape(opts, {
|
|
433
|
+
dir: { rule: "required-string", code: "externaldb-migrate/no-dir",
|
|
434
|
+
label: "externalDb.migrate.create: opts.dir (path to migrations directory)" },
|
|
435
|
+
staleAfterMs: { rule: "optional-non-negative", code: "externaldb-migrate/bad-stale",
|
|
436
|
+
label: "externalDb.migrate: staleAfterMs" },
|
|
437
|
+
audit: function (value) {
|
|
438
|
+
validateOpts.auditShape(value, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-audit");
|
|
439
|
+
},
|
|
440
|
+
schemaIntrospect: { rule: "optional-function", code: "externaldb-migrate/bad-introspect",
|
|
441
|
+
label: "externalDb.migrate: schemaIntrospect" },
|
|
442
|
+
backend: { rule: "optional-string", code: "externaldb-migrate/bad-backend",
|
|
443
|
+
label: "externalDb.migrate: backend (externalDb backend name)" },
|
|
444
|
+
ranBy: { rule: "optional-string", code: "externaldb-migrate/bad-ran-by",
|
|
445
|
+
label: "externalDb.migrate: ranBy (schema-history actor)" },
|
|
446
|
+
signHistory: { rule: "optional-boolean", code: "externaldb-migrate/bad-sign-history",
|
|
447
|
+
label: "externalDb.migrate: signHistory" },
|
|
448
|
+
}, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-opt");
|
|
450
449
|
var dir = opts.dir;
|
|
451
450
|
var audit = opts.audit || null;
|
|
452
451
|
var schemaIntrospect = typeof opts.schemaIntrospect === "function"
|
package/lib/external-db.js
CHANGED
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
* External-database integration for app data — Postgres / MySQL / SQLite / MongoDB connection pooling, retry, circuit breaker, classification routing, residency enforcement, and audit hooks.
|
|
37
37
|
*/
|
|
38
38
|
var retryHelper = require("./retry");
|
|
39
|
+
var safeBuffer = require("./safe-buffer");
|
|
39
40
|
var bCrypto = require("./crypto");
|
|
41
|
+
var numericBounds = require("./numeric-bounds");
|
|
40
42
|
var C = require("./constants");
|
|
41
43
|
var dbRoleContext = require("./db-role-context");
|
|
42
44
|
var externalDbMigrate = require("./external-db-migrate");
|
|
@@ -45,6 +47,7 @@ var { boot } = require("./log");
|
|
|
45
47
|
var safeAsync = require("./safe-async");
|
|
46
48
|
var safeSql = require("./safe-sql");
|
|
47
49
|
var validateOpts = require("./validate-opts");
|
|
50
|
+
var codepointClass = require("./codepoint-class");
|
|
48
51
|
var { ExternalDbError } = require("./framework-error");
|
|
49
52
|
|
|
50
53
|
var log = boot("external-db");
|
|
@@ -336,6 +339,50 @@ function _classifyStatement(sql) {
|
|
|
336
339
|
return _STATEMENT_CLASS_MAP[kw] || "OTHER";
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
// True when a top-level (paren-depth 0, outside any string / comment / quoted-
|
|
343
|
+
// identifier / dollar-quoted span) RETURNING keyword is present — a DML
|
|
344
|
+
// statement with RETURNING yields rows to the caller. Quote-aware so the literal
|
|
345
|
+
// word inside a value ('see RETURNING docs') is not mistaken for a clause, and
|
|
346
|
+
// depth-aware so a RETURNING buried in a CTE / subquery paren does not count for
|
|
347
|
+
// the OUTER statement. Reuses the shared opaque-span scanner; single linear pass.
|
|
348
|
+
function _hasTopLevelReturning(sql) {
|
|
349
|
+
var n = sql.length, i = 0, depth = 0;
|
|
350
|
+
while (i < n) {
|
|
351
|
+
var skipped = _skipOpaqueSpan(sql, i);
|
|
352
|
+
if (skipped === -1) return false; // unterminated span — nothing top-level beyond
|
|
353
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
354
|
+
var ch = sql.charAt(i);
|
|
355
|
+
if (ch === "(") { depth += 1; i += 1; continue; }
|
|
356
|
+
if (ch === ")") { depth -= 1; i += 1; continue; }
|
|
357
|
+
if (_isIdentStart(ch)) {
|
|
358
|
+
var we = i + 1;
|
|
359
|
+
while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
|
|
360
|
+
if (depth === 0 && sql.slice(i, we).toUpperCase() === "RETURNING") return true;
|
|
361
|
+
i = we;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
i += 1;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Does this statement return a row set to the caller? Distinct from the
|
|
370
|
+
// residency gate's read/write split: an `EXPLAIN ANALYZE INSERT` is a WRITE for
|
|
371
|
+
// the gate yet returns plan rows; a DML `... RETURNING` is a write that yields
|
|
372
|
+
// rows. Consumers (e.g. cluster-storage's local exec choosing .all() vs .run())
|
|
373
|
+
// must use .all() for any row-returning statement, including a WITH (CTE) read
|
|
374
|
+
// that the leading keyword alone would mis-route to .run() and SILENTLY DROP.
|
|
375
|
+
// Statement classes that produce a row set for the caller (SELECT and the
|
|
376
|
+
// READ_INFO family — SHOW / DESCRIBE / PRAGMA). Keyed by class name, mirroring
|
|
377
|
+
// _STATEMENT_CLASS_MAP / _RESIDENCY_READ_CLASS.
|
|
378
|
+
var _ROW_RETURNING_CLASS = Object.freeze({ SELECT: true, READ_INFO: true });
|
|
379
|
+
|
|
380
|
+
function statementReturnsRows(sql) {
|
|
381
|
+
if (typeof sql !== "string" || sql.length === 0) return false;
|
|
382
|
+
if (_ROW_RETURNING_CLASS[_classifyStatement(sql)] === true) return true;
|
|
383
|
+
return _hasTopLevelReturning(sql);
|
|
384
|
+
}
|
|
385
|
+
|
|
339
386
|
// Statement classes that place no rows on the backend, so the cross-
|
|
340
387
|
// border residency write gate lets them pass without a row tag. Every
|
|
341
388
|
// other class — DML, ROUTINE (CALL / EXECUTE / DO), a COPY ... FROM
|
|
@@ -417,11 +464,7 @@ function _otelDbAttributes(b, sql, includeStatement) {
|
|
|
417
464
|
// legitimately-quoted relation name still surfaces in the audit row.
|
|
418
465
|
var _RELATION_RE = /\b(?:FROM|INTO|UPDATE|JOIN|TABLE|COPY)\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)/ig;
|
|
419
466
|
function _hasControlChar(s) {
|
|
420
|
-
|
|
421
|
-
var c = s.charCodeAt(i);
|
|
422
|
-
if (c < 0x20 || c === 0x7f) return true;
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
467
|
+
return codepointClass.firstControlCharOffset(s, { forbidTab: true }) !== -1;
|
|
425
468
|
}
|
|
426
469
|
function _extractTargetRelation(sql) {
|
|
427
470
|
if (typeof sql !== "string" || sql.length === 0) return null;
|
|
@@ -747,7 +790,7 @@ function init(opts) {
|
|
|
747
790
|
throw _err("INVALID_CONFIG",
|
|
748
791
|
"backend '" + name + "': applicationName must not contain CR, LF, or NUL characters", true);
|
|
749
792
|
}
|
|
750
|
-
if (applicationName
|
|
793
|
+
if (safeBuffer.byteLengthOf(applicationName) > C.BYTES.bytes(63)) {
|
|
751
794
|
throw _err("INVALID_CONFIG",
|
|
752
795
|
"backend '" + name + "': applicationName exceeds Postgres 63-byte limit (got " +
|
|
753
796
|
applicationName.length + ")", true);
|
|
@@ -1440,7 +1483,7 @@ function _buildSessionGucsStatements(sessionGucs) {
|
|
|
1440
1483
|
// that bloats query logs and consumes max_stack_depth. The cap
|
|
1441
1484
|
// is generous for legitimate tenant identifiers but rejects
|
|
1442
1485
|
// amplification.
|
|
1443
|
-
if (value
|
|
1486
|
+
if (safeBuffer.byteLengthOf(value) > C.BYTES.kib(4)) {
|
|
1444
1487
|
throw _err("INVALID_SESSION_GUCS",
|
|
1445
1488
|
"sessionGucs['" + name + "']: value exceeds 4 KiB cap (got " +
|
|
1446
1489
|
value.length + " chars)", true);
|
|
@@ -1725,11 +1768,9 @@ function _buildReplicas(backendName, cfg) {
|
|
|
1725
1768
|
"backend '" + backendName + "': replicas[" + i + "].query must be a function", true);
|
|
1726
1769
|
}
|
|
1727
1770
|
var weight = r.weight !== undefined ? r.weight : 1;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
"backend '" + backendName + "': replicas[" + i + "].weight must be a positive integer", true);
|
|
1732
|
-
}
|
|
1771
|
+
numericBounds.requirePositiveFiniteInt(weight,
|
|
1772
|
+
"backend '" + backendName + "': replicas[" + i + "].weight", ExternalDbError,
|
|
1773
|
+
"INVALID_CONFIG", null, { permanent: true });
|
|
1733
1774
|
var replicaTag = r.residencyTag || "unrestricted";
|
|
1734
1775
|
var allowCrossBorder = r.allowCrossBorder === true;
|
|
1735
1776
|
if (!_residencyCompatible(primaryTag, replicaTag) && !allowCrossBorder) {
|
|
@@ -1813,12 +1854,19 @@ async function _readQuery(sql, params, opts) {
|
|
|
1813
1854
|
// was explicitly configured allowCrossBorder (which is audited).
|
|
1814
1855
|
var _readPosture = _activePosture();
|
|
1815
1856
|
var _tagPresent = opts.rowResidencyTag && typeof opts.rowResidencyTag === "string";
|
|
1857
|
+
// A replica only carries a real residency CONSTRAINT when its tag names a
|
|
1858
|
+
// specific region. "unrestricted" / "global" mean "no constraint" (the tag
|
|
1859
|
+
// defaults to "unrestricted" when none is configured), so such a replica may
|
|
1860
|
+
// serve any region's rows and must NOT require opts.rowResidencyTag — gating
|
|
1861
|
+
// it would over-reject every regulated read to an untagged replica.
|
|
1862
|
+
var _replicaConstrained = replica.residencyTag &&
|
|
1863
|
+
replica.residencyTag !== "unrestricted" && replica.residencyTag !== "global";
|
|
1816
1864
|
// Fail CLOSED when the row's region is not identified: a regulated read to a
|
|
1817
|
-
// residency-
|
|
1818
|
-
// residency-restricted rows to an arbitrary-region replica with no check
|
|
1819
|
-
// all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
|
|
1865
|
+
// residency-CONSTRAINED replica without opts.rowResidencyTag would otherwise
|
|
1866
|
+
// route residency-restricted rows to an arbitrary-region replica with no check
|
|
1867
|
+
// at all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
|
|
1820
1868
|
if (!_tagPresent && _crossBorderRegulated(_readPosture) &&
|
|
1821
|
-
|
|
1869
|
+
_replicaConstrained && !replica.allowCrossBorder) {
|
|
1822
1870
|
_emit("db.residency.replica.tag_required", "denied", {
|
|
1823
1871
|
backend: b.name, replicaIdx: replica.index,
|
|
1824
1872
|
replicaTag: replica.residencyTag, posture: _readPosture,
|
|
@@ -2066,15 +2114,9 @@ function configurePool(backendName, opts) {
|
|
|
2066
2114
|
"configurePool: unknown option '" + k + "'. Allowed: " + allowed.join(", "), true);
|
|
2067
2115
|
}
|
|
2068
2116
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
"configurePool: " + name + " must be a positive integer, got " + JSON.stringify(value), true);
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
if (opts.min !== undefined) _requirePosInt("min", opts.min);
|
|
2076
|
-
if (opts.max !== undefined) _requirePosInt("max", opts.max);
|
|
2077
|
-
if (opts.idleTimeoutMs !== undefined) _requirePosInt("idleTimeoutMs", opts.idleTimeoutMs);
|
|
2117
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
2118
|
+
["min", "max", "idleTimeoutMs"], "configurePool", ExternalDbError,
|
|
2119
|
+
"INVALID_CONFIG", { permanent: true });
|
|
2078
2120
|
if (opts.min !== undefined && opts.max !== undefined && opts.min > opts.max) {
|
|
2079
2121
|
throw _err("INVALID_CONFIG", "configurePool: min must be <= max", true);
|
|
2080
2122
|
}
|
|
@@ -2146,11 +2188,8 @@ function _connectAs(rawConnect, query, opts) {
|
|
|
2146
2188
|
throw _err("INVALID_CONFIG", "connectAs: applicationName must be a string", true);
|
|
2147
2189
|
}
|
|
2148
2190
|
if (opts.statementTimeoutMs !== undefined) {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
throw _err("INVALID_CONFIG",
|
|
2152
|
-
"connectAs: statementTimeoutMs must be a positive integer", true);
|
|
2153
|
-
}
|
|
2191
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.statementTimeoutMs,
|
|
2192
|
+
"connectAs: statementTimeoutMs", ExternalDbError, "INVALID_CONFIG", { permanent: true });
|
|
2154
2193
|
}
|
|
2155
2194
|
if (opts.gucs !== undefined && (typeof opts.gucs !== "object" || opts.gucs === null)) {
|
|
2156
2195
|
throw _err("INVALID_CONFIG", "connectAs: gucs must be an object", true);
|
|
@@ -2586,6 +2625,11 @@ module.exports = {
|
|
|
2586
2625
|
// lock tables live on the externalDb side. See lib/external-db-migrate.js.
|
|
2587
2626
|
migrate: externalDbMigrate,
|
|
2588
2627
|
Pool: Pool,
|
|
2628
|
+
// Shared SQL statement classifier (CTE / EXPLAIN aware): does the statement
|
|
2629
|
+
// return a row set? Internal — consumed by cluster-storage's local exec to
|
|
2630
|
+
// choose .all() vs .run() so a WITH (CTE) read is not mis-routed and its rows
|
|
2631
|
+
// lost. Underscore-prefixed: framework-internal, not an operator API surface.
|
|
2632
|
+
_statementReturnsRows: statementReturnsRows,
|
|
2589
2633
|
_resetForTest: _resetForTest,
|
|
2590
2634
|
_extractTargetRelation: _extractTargetRelation,
|
|
2591
2635
|
};
|
package/lib/fda-21cfr11.js
CHANGED
|
@@ -91,6 +91,18 @@ var DEFAULT_SIGNATURE_MEANINGS = Object.freeze([
|
|
|
91
91
|
// must carry §11.10(e) shape. Operators add more via opts.gxpNamespaces.
|
|
92
92
|
var DEFAULT_GXP_NAMESPACES = Object.freeze(["subject", "consent", "db", "breakglass"]);
|
|
93
93
|
|
|
94
|
+
// Non-mutating verb segments. A GxP-namespace event whose final action segment
|
|
95
|
+
// is NOT one of these is treated as a record MODIFICATION and must carry the
|
|
96
|
+
// §11.10(e) before/after/reason shape (fail-closed — an unknown verb is assumed
|
|
97
|
+
// to mutate). hasOwnProperty-guarded lookup (untrusted action key).
|
|
98
|
+
var READ_VERBS = Object.freeze({
|
|
99
|
+
read: 1, viewed: 1, view: 1, get: 1, got: 1, list: 1, listed: 1, query: 1,
|
|
100
|
+
queried: 1, access: 1, accessed: 1, export: 1, exported: 1, history: 1,
|
|
101
|
+
isgranted: 1, check: 1, checked: 1, verify: 1, verified: 1, lookup: 1,
|
|
102
|
+
search: 1, searched: 1, fetch: 1, fetched: 1, render: 1, rendered: 1,
|
|
103
|
+
download: 1, downloaded: 1,
|
|
104
|
+
});
|
|
105
|
+
|
|
94
106
|
// §11.10(e) — every modification audit must carry timestamp, actor,
|
|
95
107
|
// before/after pair, and a reason. The framework's audit row shape
|
|
96
108
|
// already carries `recordedAt` (timestamp) + `actorUserId` + `reason`;
|
|
@@ -114,11 +126,15 @@ function _hasRequiredAuditShape(row) {
|
|
|
114
126
|
if (!row.action || typeof row.action !== "string") {
|
|
115
127
|
return { ok: false, reason: "row missing action verb (§11.10(e))" };
|
|
116
128
|
}
|
|
117
|
-
//
|
|
118
|
-
// "
|
|
129
|
+
// §11.10(e) requires before/after on every record MODIFICATION. Inferring
|
|
130
|
+
// "is this a modification" from a denylist of mutating verbs silently let
|
|
131
|
+
// off-list verbs (anonymize / revoke / overwrite / replace / merge /
|
|
132
|
+
// withdraw / restrict / objection …) bypass the requirement. Fail CLOSED
|
|
133
|
+
// instead: a GxP-namespace event is treated as a modification UNLESS its
|
|
134
|
+
// final verb segment is on an explicit non-mutating READ allowlist.
|
|
119
135
|
var verb = row.action.toLowerCase();
|
|
120
|
-
var
|
|
121
|
-
|
|
136
|
+
var lastSeg = verb.indexOf(".") === -1 ? verb : verb.slice(verb.lastIndexOf(".") + 1);
|
|
137
|
+
var modShape = !Object.prototype.hasOwnProperty.call(READ_VERBS, lastSeg);
|
|
122
138
|
if (modShape) {
|
|
123
139
|
var meta = row.metadata;
|
|
124
140
|
// Audit chain stores metadata as a JSON string when read back —
|
|
@@ -181,14 +197,18 @@ function posture(opts) {
|
|
|
181
197
|
"audit", "signWith", "verifyWith", "meanings", "gxpNamespaces",
|
|
182
198
|
"interceptAudit", "now",
|
|
183
199
|
], "fda21cfr11.posture");
|
|
184
|
-
validateOpts.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
validateOpts.shape(opts, {
|
|
201
|
+
audit: function (value) {
|
|
202
|
+
validateOpts.auditShape(value, "fda21cfr11.posture",
|
|
203
|
+
Fda21Cfr11Error, "fda21cfr11/bad-audit");
|
|
204
|
+
},
|
|
205
|
+
signWith: { rule: "optional-function", code: "fda21cfr11/bad-signer" },
|
|
206
|
+
verifyWith: { rule: "optional-function", code: "fda21cfr11/bad-verifier" },
|
|
207
|
+
now: { rule: "optional-function", code: "fda21cfr11/bad-now" },
|
|
208
|
+
meanings: { rule: "optional-string-array", code: "fda21cfr11/bad-meanings" },
|
|
209
|
+
gxpNamespaces: { rule: "optional-string-array", code: "fda21cfr11/bad-gxp-namespaces" },
|
|
210
|
+
interceptAudit: { rule: "optional-boolean", code: "fda21cfr11/bad-intercept-audit" },
|
|
211
|
+
}, "fda21cfr11.posture", Fda21Cfr11Error, "fda21cfr11/bad-opts");
|
|
192
212
|
|
|
193
213
|
var auditMod = opts.audit && typeof opts.audit.safeEmit === "function" ? opts.audit : null;
|
|
194
214
|
var signWith = typeof opts.signWith === "function" ? opts.signWith : null;
|
|
@@ -259,7 +279,16 @@ function posture(opts) {
|
|
|
259
279
|
}, "denied");
|
|
260
280
|
return { ok: false, reason: "record-hash-mismatch" };
|
|
261
281
|
}
|
|
262
|
-
if (verifyWith
|
|
282
|
+
if (verifyWith) {
|
|
283
|
+
// A configured verifier means a signature is REQUIRED — a null/empty
|
|
284
|
+
// signature must fail closed (recordHash alone is self-consistency, not
|
|
285
|
+
// authentication; accepting it is alg:none-style signature stripping).
|
|
286
|
+
if (!signed.signature) {
|
|
287
|
+
_emit("fda21cfr11.signature.verified", {
|
|
288
|
+
printedName: signed.printedName, ok: false, reason: "signature-required",
|
|
289
|
+
}, "denied");
|
|
290
|
+
return { ok: false, reason: "signature-required" };
|
|
291
|
+
}
|
|
263
292
|
var sigBuf = Buffer.from(signed.signature, "base64");
|
|
264
293
|
var payload = JSON.stringify({
|
|
265
294
|
printedName: signed.printedName,
|
package/lib/fdx.js
CHANGED
|
@@ -168,10 +168,13 @@ function bind(opts) {
|
|
|
168
168
|
throw FdxError.factory("fdx/bad-auth-server",
|
|
169
169
|
"fdx.bind: authServer object required");
|
|
170
170
|
}
|
|
171
|
-
validateOpts.
|
|
172
|
-
"fdx.bind: authServer.issuer"
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
validateOpts.shape(opts.authServer, {
|
|
172
|
+
issuer: { rule: "required-string", code: "BAD_ISSUER", label: "fdx.bind: authServer.issuer" },
|
|
173
|
+
jwksUri: { rule: "required-string", code: "BAD_JWKS_URI", label: "fdx.bind: authServer.jwksUri" },
|
|
174
|
+
}, "fdx.bind: authServer", FdxError, "fdx/bad-auth-server",
|
|
175
|
+
// fapi2 is forwarded verbatim to fapi2.assertOAuthConfig, which owns
|
|
176
|
+
// its shape ({ pkce, dpop?, mtls?, par }) — validated there, not here.
|
|
177
|
+
{ allow: ["fapi2"] });
|
|
175
178
|
|
|
176
179
|
if (!Array.isArray(opts.resources) || opts.resources.length === 0) {
|
|
177
180
|
throw FdxError.factory("fdx/bad-resources",
|
|
@@ -315,20 +318,21 @@ function consentReceipt(opts) {
|
|
|
315
318
|
if (!opts || typeof opts !== "object") {
|
|
316
319
|
throw FdxError.factory("fdx/bad-opts", "fdx.consentReceipt: opts required");
|
|
317
320
|
}
|
|
318
|
-
validateOpts.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
321
|
+
validateOpts.shape(opts, {
|
|
322
|
+
dataProvider: { rule: "required-string", code: "BAD_DATA_PROVIDER" },
|
|
323
|
+
consumerRef: { rule: "required-string", code: "BAD_CONSUMER_REF" },
|
|
324
|
+
thirdParty: { rule: "required-string", code: "BAD_THIRD_PARTY" },
|
|
325
|
+
revocationUrl: { rule: "required-string", code: "BAD_REVOCATION_URL" },
|
|
326
|
+
scopes: function (value) {
|
|
327
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
328
|
+
throw FdxError.factory("fdx/bad-scopes",
|
|
329
|
+
"fdx.consentReceipt: scopes must be a non-empty array");
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
durationMs: function (value, label) {
|
|
333
|
+
numericBounds.requirePositiveFiniteIntIfPresent(value, label, FdxError, "BAD_DURATION");
|
|
334
|
+
},
|
|
335
|
+
}, "fdx.consentReceipt", FdxError, "fdx/bad-opts");
|
|
332
336
|
|
|
333
337
|
var issuedAt = Date.now();
|
|
334
338
|
var expiresAt = issuedAt + (opts.durationMs || C.TIME.weeks(52));
|