@blamejs/blamejs-shop 0.4.85 → 0.4.87
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/lib/asset-manifest.json +1 -1
- package/lib/cart.js +75 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/storefront.js +8 -1
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.15.
|
|
4
|
-
"createdAt": "2026-06-
|
|
3
|
+
"frameworkVersion": "0.15.16",
|
|
4
|
+
"createdAt": "2026-06-22T12:04:10.257Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -2346,6 +2346,10 @@
|
|
|
2346
2346
|
"type": "function",
|
|
2347
2347
|
"arity": 3
|
|
2348
2348
|
},
|
|
2349
|
+
"openAppendNoFollowSync": {
|
|
2350
|
+
"type": "function",
|
|
2351
|
+
"arity": 2
|
|
2352
|
+
},
|
|
2349
2353
|
"openNoFollowSync": {
|
|
2350
2354
|
"type": "function",
|
|
2351
2355
|
"arity": 2
|
|
@@ -16060,6 +16064,10 @@
|
|
|
16060
16064
|
"type": "function",
|
|
16061
16065
|
"arity": 0
|
|
16062
16066
|
},
|
|
16067
|
+
"supportsTransactions": {
|
|
16068
|
+
"type": "function",
|
|
16069
|
+
"arity": 1
|
|
16070
|
+
},
|
|
16063
16071
|
"transaction": {
|
|
16064
16072
|
"type": "function",
|
|
16065
16073
|
"arity": 2
|
|
@@ -49213,7 +49221,7 @@
|
|
|
49213
49221
|
},
|
|
49214
49222
|
"ipPrefix": {
|
|
49215
49223
|
"type": "function",
|
|
49216
|
-
"arity":
|
|
49224
|
+
"arity": 2
|
|
49217
49225
|
},
|
|
49218
49226
|
"makeResourceAuditEmitter": {
|
|
49219
49227
|
"type": "function",
|
|
@@ -1336,6 +1336,39 @@ function openNoFollowSync(filepath, mode) {
|
|
|
1336
1336
|
return nodeFs.openSync(filepath, flags, mode === undefined ? 0o600 : mode);
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* @primitive b.atomicFile.openAppendNoFollowSync
|
|
1341
|
+
* @signature b.atomicFile.openAppendNoFollowSync(filepath, mode?)
|
|
1342
|
+
* @since 0.15.16
|
|
1343
|
+
* @status stable
|
|
1344
|
+
* @related b.atomicFile.openNoFollowSync, b.atomicFile.writeSync
|
|
1345
|
+
*
|
|
1346
|
+
* Open a path for append with `O_NOFOLLOW` so a symlink at the final path
|
|
1347
|
+
* component is refused (`ELOOP`) instead of followed — the append-sink
|
|
1348
|
+
* counterpart to `openNoFollowSync` (read) and `_openExclTemp` (exclusive
|
|
1349
|
+
* create). For long-lived append targets a one-shot atomic write can't
|
|
1350
|
+
* model: an active log file kept open across many appends and reopened on
|
|
1351
|
+
* rotation. The flags are `O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW` —
|
|
1352
|
+
* the file is created (mode-applied) if absent and appended to if it is a
|
|
1353
|
+
* regular file, but a symlink at the path fails the open closed rather than
|
|
1354
|
+
* redirecting writes to an attacker-chosen target (CWE-59). A bare
|
|
1355
|
+
* `openSync(path, "a")` instead follows such a symlink, so a caller's
|
|
1356
|
+
* pre-check-then-unlink defense still races a symlink re-planted before the
|
|
1357
|
+
* sink's own open — this primitive makes the symlink refusal atomic with the
|
|
1358
|
+
* open. `O_NOFOLLOW` is POSIX-only; on platforms without it the flag is 0
|
|
1359
|
+
* (a plain append open) — Windows symlink semantics differ and are out of
|
|
1360
|
+
* scope. Throws the raw `openSync` error (caller maps `ELOOP` / `ENOENT`).
|
|
1361
|
+
*
|
|
1362
|
+
* @example
|
|
1363
|
+
* var fd = b.atomicFile.openAppendNoFollowSync(activeLogPath, 0o600);
|
|
1364
|
+
* fs.writeSync(fd, line); // appends; a symlink at activeLogPath → ELOOP
|
|
1365
|
+
*/
|
|
1366
|
+
function openAppendNoFollowSync(filepath, mode) {
|
|
1367
|
+
var flags = nodeFs.constants.O_WRONLY | nodeFs.constants.O_APPEND |
|
|
1368
|
+
nodeFs.constants.O_CREAT | (nodeFs.constants.O_NOFOLLOW || 0);
|
|
1369
|
+
return nodeFs.openSync(filepath, flags, mode === undefined ? 0o600 : mode);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1339
1372
|
module.exports = {
|
|
1340
1373
|
write: write,
|
|
1341
1374
|
writeSync: writeSync,
|
|
@@ -1345,6 +1378,7 @@ module.exports = {
|
|
|
1345
1378
|
readSync: readSync,
|
|
1346
1379
|
fdSafeReadSync: fdSafeReadSync,
|
|
1347
1380
|
openNoFollowSync: openNoFollowSync,
|
|
1381
|
+
openAppendNoFollowSync: openAppendNoFollowSync,
|
|
1348
1382
|
writeJson: writeJson,
|
|
1349
1383
|
readJson: readJson,
|
|
1350
1384
|
copy: copy,
|
|
@@ -214,6 +214,16 @@ function _validateChain(x5c, rootPems) {
|
|
|
214
214
|
for (var v = 0; v < chain.length; v++) {
|
|
215
215
|
var notBefore = Date.parse(chain[v].validFrom);
|
|
216
216
|
var notAfter = Date.parse(chain[v].validTo);
|
|
217
|
+
// Fail closed on an unparseable validity window: a cert whose
|
|
218
|
+
// notBefore/notAfter is present but does not parse (Date.parse ->
|
|
219
|
+
// NaN) must not slip past the window checks below — otherwise a
|
|
220
|
+
// forged / malformed validity date silently disables both the
|
|
221
|
+
// not-yet-valid and the expired guards.
|
|
222
|
+
if (!isFinite(notBefore) || !isFinite(notAfter)) {
|
|
223
|
+
throw new FidoMds3Error("fido-mds3/cert-bad-validity",
|
|
224
|
+
"x5c[" + v + "] has unparseable validity dates (validFrom=" +
|
|
225
|
+
chain[v].validFrom + ", validTo=" + chain[v].validTo + ")");
|
|
226
|
+
}
|
|
217
227
|
if (isFinite(notBefore) && now < notBefore) {
|
|
218
228
|
throw new FidoMds3Error("fido-mds3/cert-not-yet-valid",
|
|
219
229
|
"x5c[" + v + "] is not yet valid (notBefore=" + chain[v].validFrom + ")");
|
|
@@ -692,8 +692,13 @@ function create(opts) {
|
|
|
692
692
|
if (!noaHok) continue; // §3.1 (incorporates §3) — time-bound required
|
|
693
693
|
var noaHokSec = Date.parse(noaHok) / 1000; // ms→s
|
|
694
694
|
if (!isFinite(noaHokSec) || noaHokSec < nowSec - clockSkewSec) continue; // unparseable or expired
|
|
695
|
-
if (nbHok
|
|
696
|
-
|
|
695
|
+
if (nbHok) {
|
|
696
|
+
// Fail CLOSED on a present-but-unparseable HoK NotBefore (same shape
|
|
697
|
+
// as the Bearer path + NotOnOrAfter above) — an unparseable bound
|
|
698
|
+
// can't establish the confirmation has begun, so skip this SCD.
|
|
699
|
+
var nbHokSec = Date.parse(nbHok) / 1000; // ms→s
|
|
700
|
+
if (!isFinite(nbHokSec) || nbHokSec > nowSec + clockSkewSec) continue; // unparseable or not-yet-valid
|
|
701
|
+
}
|
|
697
702
|
var recipHok = _attr(scdHok, "Recipient");
|
|
698
703
|
if (!recipHok || recipHok !== opts.assertionConsumerServiceUrl) continue; // §3.1→§4.1.4.2 — Recipient is mandatory; absent fails the endpoint binding
|
|
699
704
|
hokOk = true;
|
|
@@ -709,7 +714,10 @@ function create(opts) {
|
|
|
709
714
|
var notBefore = _attr(scd, "NotBefore");
|
|
710
715
|
if (notBefore) {
|
|
711
716
|
var nb = Date.parse(notBefore) / 1000; // ms→s
|
|
712
|
-
|
|
717
|
+
// Fail CLOSED on a present-but-unparseable NotBefore (mirrors the
|
|
718
|
+
// NotOnOrAfter line above + the Conditions block) — an unparseable
|
|
719
|
+
// bound can't establish the confirmation has begun, so skip this SCD.
|
|
720
|
+
if (!isFinite(nb) || nb > nowSec + clockSkewSec) continue;
|
|
713
721
|
}
|
|
714
722
|
// §4.1.4.2 — a Bearer SubjectConfirmationData delivered to an ACS MUST
|
|
715
723
|
// carry a Recipient equal to this SP's ACS URL. Absent Recipient fails
|
|
@@ -151,7 +151,10 @@ function _maybeReapStale(pidFile) {
|
|
|
151
151
|
function _openLogFd(logFile) {
|
|
152
152
|
if (typeof logFile !== "string" || logFile.length === 0) return null;
|
|
153
153
|
atomicFile.ensureDir(nodePath.dirname(logFile));
|
|
154
|
-
|
|
154
|
+
// O_NOFOLLOW append: refuse (ELOOP) a symlink planted at the daemon log
|
|
155
|
+
// path rather than redirecting the detached process's stdout/stderr to an
|
|
156
|
+
// attacker-chosen file (CWE-59).
|
|
157
|
+
var fd = atomicFile.openAppendNoFollowSync(logFile, DEFAULT_LOG_FILE_MODE);
|
|
155
158
|
return fd;
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -706,6 +706,8 @@ class Pool {
|
|
|
706
706
|
* // close(client): async → void (optional; default no-op)
|
|
707
707
|
* // ping(client): async → void (optional; default `SELECT 1`)
|
|
708
708
|
* // beginTx / commit / rollback(client): async → void (optional; default `BEGIN`/`COMMIT`/`ROLLBACK`)
|
|
709
|
+
* // batch(client, statements): async → void (optional; atomic multi-statement path for batch-only adapters, e.g. D1 db.batch)
|
|
710
|
+
* // supportsTransactions: boolean (interactive-tx capability; set false on a stateless/autocommit-per-statement adapter so transaction()/outbox REFUSE rather than silently run non-atomic — default assumes stateful)
|
|
709
711
|
* // dialect: "postgres" | "mysql" | "sqlite" | "mongodb" | "other" (default "postgres")
|
|
710
712
|
* // requireTls: boolean (opt-in TLS posture gate; default off — see below)
|
|
711
713
|
* // tls / ssl / sslmode: transport-TLS declaration consulted by requireTls (tls:true | ssl:<obj> | sslmode:"require"|"verify-ca"|"verify-full")
|
|
@@ -796,6 +798,60 @@ function init(opts) {
|
|
|
796
798
|
applicationName.length + ")", true);
|
|
797
799
|
}
|
|
798
800
|
}
|
|
801
|
+
// ---- Interactive-transaction capability ----
|
|
802
|
+
//
|
|
803
|
+
// transaction(fn) and the b.outbox dual-write guarantee both require
|
|
804
|
+
// an INTERACTIVE transaction: BEGIN, the body statements, and COMMIT
|
|
805
|
+
// must all run on the SAME session so the body sees isolation and a
|
|
806
|
+
// throw rolls everything back. A stateless / autocommit-per-statement
|
|
807
|
+
// adapter (connect() returns a sentinel, each query() is an
|
|
808
|
+
// independent round-trip — e.g. an HTTP bridge to Cloudflare D1) makes
|
|
809
|
+
// BEGIN / body / COMMIT land on different sessions: no isolation, no
|
|
810
|
+
// rollback, yet every call resolves, so the consumer wrongly believes
|
|
811
|
+
// the block was atomic. Detect that posture up front and REFUSE
|
|
812
|
+
// loudly rather than silently degrade to no-op BEGIN/COMMIT.
|
|
813
|
+
//
|
|
814
|
+
// An INTERACTIVE transaction (transaction(fn) runs arbitrary async
|
|
815
|
+
// operator logic between tx.query calls) requires a stateful session;
|
|
816
|
+
// it cannot be transparently replayed as a static batch, so a backend
|
|
817
|
+
// can provide interactive transactions only when:
|
|
818
|
+
// - it supplies interactive-tx hooks (beginTx / commit / rollback)
|
|
819
|
+
// bound to a stateful client, OR
|
|
820
|
+
// - it explicitly declares supportsTransactions: true (operator
|
|
821
|
+
// asserts the default BEGIN/COMMIT runs on a stateful client).
|
|
822
|
+
// It CANNOT when it explicitly declares supportsTransactions: false
|
|
823
|
+
// (the stateless / autocommit-per-statement declaration). When the
|
|
824
|
+
// flag is omitted and no hooks are present the backend is assumed
|
|
825
|
+
// stateful — the historical default — so existing stateful pg / sqlite
|
|
826
|
+
// / mysql backends are unaffected.
|
|
827
|
+
//
|
|
828
|
+
// A batch(client, statements) hook is a SEPARATE atomic-multi-statement
|
|
829
|
+
// path for batch-only adapters (e.g. D1's db.batch([...])); it does NOT
|
|
830
|
+
// make interactive transaction(fn) safe (the interleaved operator logic
|
|
831
|
+
// can't be captured as a static statement list), so its presence is
|
|
832
|
+
// recorded but does not flip supportsTransactions.
|
|
833
|
+
if (cfg.supportsTransactions !== undefined && cfg.supportsTransactions !== null) {
|
|
834
|
+
validateOpts.optionalBoolean(cfg.supportsTransactions,
|
|
835
|
+
"backend '" + name + "': supportsTransactions", ExternalDbError, "INVALID_CONFIG");
|
|
836
|
+
}
|
|
837
|
+
if (cfg.batch !== undefined && cfg.batch !== null && typeof cfg.batch !== "function") {
|
|
838
|
+
throw _err("INVALID_CONFIG",
|
|
839
|
+
"backend '" + name + "': batch must be a function (client, statements) when supplied", true);
|
|
840
|
+
}
|
|
841
|
+
var hasInteractiveHooks =
|
|
842
|
+
typeof cfg.beginTx === "function" &&
|
|
843
|
+
typeof cfg.commit === "function" &&
|
|
844
|
+
typeof cfg.rollback === "function";
|
|
845
|
+
var hasBatch = typeof cfg.batch === "function";
|
|
846
|
+
var supportsTransactions;
|
|
847
|
+
if (cfg.supportsTransactions === false) {
|
|
848
|
+
supportsTransactions = false;
|
|
849
|
+
} else {
|
|
850
|
+
// true flag, interactive hooks, or (historical default) flag omitted
|
|
851
|
+
// with no hooks → assume a stateful interactive session.
|
|
852
|
+
supportsTransactions = true;
|
|
853
|
+
}
|
|
854
|
+
|
|
799
855
|
var rawConnect = cfg.connect;
|
|
800
856
|
var rawQuery = cfg.query;
|
|
801
857
|
var connectFn = rawConnect;
|
|
@@ -834,6 +890,9 @@ function init(opts) {
|
|
|
834
890
|
beginTx: cfg.beginTx || function (client) { return cfg.query(client, "BEGIN", []); },
|
|
835
891
|
commit: cfg.commit || function (client) { return cfg.query(client, "COMMIT", []); },
|
|
836
892
|
rollback: cfg.rollback || function (client) { return cfg.query(client, "ROLLBACK", []); },
|
|
893
|
+
supportsTransactions: supportsTransactions,
|
|
894
|
+
hasInteractiveHooks: hasInteractiveHooks,
|
|
895
|
+
batch: hasBatch ? cfg.batch : null,
|
|
837
896
|
classifications: Array.isArray(cfg.classifications) && cfg.classifications.length > 0
|
|
838
897
|
? cfg.classifications.slice()
|
|
839
898
|
: ["*"],
|
|
@@ -1015,6 +1074,62 @@ function _servesClassification(b, cls) {
|
|
|
1015
1074
|
return b.classifications.indexOf("*") !== -1 || b.classifications.indexOf(cls) !== -1;
|
|
1016
1075
|
}
|
|
1017
1076
|
|
|
1077
|
+
// Refuse an interactive-transaction request against a backend that cannot
|
|
1078
|
+
// honor it (declared supportsTransactions:false, no beginTx/commit/rollback
|
|
1079
|
+
// hooks, no batch path). Such a backend would run BEGIN / body / COMMIT as
|
|
1080
|
+
// independent autocommit round-trips on different sessions — no isolation,
|
|
1081
|
+
// no rollback — and resolve, leaving the consumer believing the block was
|
|
1082
|
+
// atomic. Fail closed at the gate (CWE-662-shaped: improper synchronization
|
|
1083
|
+
// → lost atomicity) so the operator wires interactive-tx hooks or a batch
|
|
1084
|
+
// adapter rather than shipping a silently non-atomic dual-write.
|
|
1085
|
+
function _assertInteractiveTransactions(b, where) {
|
|
1086
|
+
if (!b) return;
|
|
1087
|
+
if (b.supportsTransactions === false) {
|
|
1088
|
+
throw _err("NON_ATOMIC_BACKEND",
|
|
1089
|
+
"externalDb." + where + " requires an interactive transaction, but backend '" +
|
|
1090
|
+
b.name + "' declares supportsTransactions: false (a stateless / " +
|
|
1091
|
+
"autocommit-per-statement adapter). BEGIN / the body statements / COMMIT " +
|
|
1092
|
+
"would each run on a different session — no isolation, no rollback — so the " +
|
|
1093
|
+
"block would NOT be atomic. Supply interactive beginTx / commit / rollback " +
|
|
1094
|
+
"hooks, or a batch(client, statements) adapter, on this backend before using " +
|
|
1095
|
+
"externalDb." + where + " (and b.outbox, which is built on it).",
|
|
1096
|
+
true);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* @primitive b.externalDb.supportsTransactions
|
|
1102
|
+
* @signature b.externalDb.supportsTransactions(opts?)
|
|
1103
|
+
* @since 0.15.16
|
|
1104
|
+
* @related b.externalDb.transaction, b.externalDb.init
|
|
1105
|
+
*
|
|
1106
|
+
* Report whether the picked (or default) backend can provide an
|
|
1107
|
+
* interactive transaction. Returns `false` only when the backend declares
|
|
1108
|
+
* `supportsTransactions: false` at `init()` — a stateless /
|
|
1109
|
+
* autocommit-per-statement adapter on which `transaction()` would run
|
|
1110
|
+
* `BEGIN` / the body / `COMMIT` on different sessions (no isolation, no
|
|
1111
|
+
* rollback). Consumers built on the dual-write guarantee (`b.outbox`) call
|
|
1112
|
+
* this at construction so a non-atomic backend is refused up front rather
|
|
1113
|
+
* than at the first transaction.
|
|
1114
|
+
*
|
|
1115
|
+
* Same backend-selection `opts` as `b.externalDb.query` (`backend` /
|
|
1116
|
+
* `classification`).
|
|
1117
|
+
*
|
|
1118
|
+
* @opts
|
|
1119
|
+
* backend?: string, // explicit backend name
|
|
1120
|
+
* classification?: string, // route by data class
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* if (!b.externalDb.supportsTransactions()) {
|
|
1124
|
+
* throw new Error("this backend cannot run atomic transactions");
|
|
1125
|
+
* }
|
|
1126
|
+
*/
|
|
1127
|
+
function supportsTransactions(opts) {
|
|
1128
|
+
_requireInit();
|
|
1129
|
+
var b = _pickBackend(opts);
|
|
1130
|
+
return !!(b && b.supportsTransactions !== false);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1018
1133
|
// ---- Public API ----
|
|
1019
1134
|
|
|
1020
1135
|
/**
|
|
@@ -1158,6 +1273,13 @@ async function query(sql, params, opts) {
|
|
|
1158
1273
|
* `BEGIN`, so RLS state set by `sessionGucs` (`SET LOCAL`) applies for
|
|
1159
1274
|
* the duration of the transaction and resets at COMMIT/ROLLBACK.
|
|
1160
1275
|
*
|
|
1276
|
+
* Refuses (`NON_ATOMIC_BACKEND`) when the picked backend declares
|
|
1277
|
+
* `supportsTransactions: false` — a stateless / autocommit-per-statement
|
|
1278
|
+
* adapter on which BEGIN / the body / COMMIT would run on different
|
|
1279
|
+
* sessions (no isolation, no rollback). Supply interactive
|
|
1280
|
+
* `beginTx`/`commit`/`rollback` hooks or a `batch` adapter on the backend
|
|
1281
|
+
* instead of shipping a silently non-atomic block.
|
|
1282
|
+
*
|
|
1161
1283
|
* @opts
|
|
1162
1284
|
* backend?: string, // explicit backend name
|
|
1163
1285
|
* classification?: string, // route by data class
|
|
@@ -1185,6 +1307,14 @@ async function transaction(fn, opts) {
|
|
|
1185
1307
|
if (typeof fn !== "function") throw _err("INVALID_FN", "transaction requires a function", true);
|
|
1186
1308
|
opts = opts || {};
|
|
1187
1309
|
var b = _pickBackend(opts);
|
|
1310
|
+
// Fail closed on a backend that cannot provide interactive transactions
|
|
1311
|
+
// (stateless / autocommit-per-statement adapter declared
|
|
1312
|
+
// supportsTransactions:false with no beginTx/commit/rollback hooks and
|
|
1313
|
+
// no batch path). Running the default BEGIN / body / COMMIT against such
|
|
1314
|
+
// a backend lands each statement on a different session — no isolation,
|
|
1315
|
+
// no rollback — yet resolves, so the consumer wrongly believes the block
|
|
1316
|
+
// was atomic. Refuse instead of silently degrading.
|
|
1317
|
+
_assertInteractiveTransactions(b, "transaction");
|
|
1188
1318
|
var role = dbRoleContext.getRole();
|
|
1189
1319
|
|
|
1190
1320
|
// sessionGucs — per-transaction `SET LOCAL "name" = value` plumbing.
|
|
@@ -2608,6 +2738,7 @@ module.exports = {
|
|
|
2608
2738
|
init: init,
|
|
2609
2739
|
query: query,
|
|
2610
2740
|
transaction: transaction,
|
|
2741
|
+
supportsTransactions: supportsTransactions,
|
|
2611
2742
|
healthCheck: healthCheck,
|
|
2612
2743
|
listBackends: listBackends,
|
|
2613
2744
|
shutdown: shutdown,
|
|
@@ -27,6 +27,7 @@ var numericBounds = require("./numeric-bounds");
|
|
|
27
27
|
var safeJson = require("./safe-json");
|
|
28
28
|
var safeBuffer = require("./safe-buffer");
|
|
29
29
|
var requestHelpers = require("./request-helpers");
|
|
30
|
+
var validateOpts = require("./validate-opts");
|
|
30
31
|
var audit = require("./audit");
|
|
31
32
|
var { GraphqlFederationError } = require("./framework-error");
|
|
32
33
|
|
|
@@ -121,7 +122,7 @@ function _readBody(req, errorClass) {
|
|
|
121
122
|
* @opts
|
|
122
123
|
* publicSchemaOk: boolean, // default false — explicit override to publish the SDL
|
|
123
124
|
* routerToken: string, // required unless publicSchemaOk; 32+ chars
|
|
124
|
-
* nonceStore: {
|
|
125
|
+
* nonceStore: { checkAndInsert(nonce, expireAt): bool }, // optional atomic replay protection (b.nonceStore-shaped)
|
|
125
126
|
* nonceHeader: string, // default "x-apollographql-router-nonce" — request header carrying the replay nonce
|
|
126
127
|
* nonceTtlMs: number, // default 5 minutes
|
|
127
128
|
* errorClass: Function, // default GraphqlFederationError
|
|
@@ -143,8 +144,15 @@ function guardSdl(opts) {
|
|
|
143
144
|
throw errorClass.factory("graphql-federation/bad-opts",
|
|
144
145
|
"graphqlFederation.guardSdl: routerToken (32+ char) required unless publicSchemaOk=true");
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// Replay store speaks the framework's ATOMIC checkAndInsert(nonce, expireAt)
|
|
148
|
+
// contract — the same one b.webhook.verify and b.nonceStore.create use — so
|
|
149
|
+
// the framework's own replay store drops in and concurrent redeliveries of a
|
|
150
|
+
// captured token can't both pass a non-atomic has()-then-remember() race
|
|
151
|
+
// (CWE-367). A store lacking checkAndInsert is refused at config time.
|
|
152
|
+
validateOpts.optionalObjectWithMethod(opts.nonceStore, "checkAndInsert",
|
|
153
|
+
"graphqlFederation.guardSdl: nonceStore", errorClass, "graphql-federation/bad-nonce-store",
|
|
154
|
+
"must implement checkAndInsert(nonce, expireAt) — b.nonceStore.create is the reference store");
|
|
155
|
+
var nonceStore = opts.nonceStore || null;
|
|
148
156
|
numericBounds.requirePositiveFiniteIntIfPresent(opts.nonceTtlMs, "graphqlFederation.guardSdl: opts.nonceTtlMs", errorClass, "BAD_TTL");
|
|
149
157
|
var nonceTtlMs = opts.nonceTtlMs || C.TIME.minutes(5);
|
|
150
158
|
var auditOn = opts.audit !== false;
|
|
@@ -215,21 +223,23 @@ function guardSdl(opts) {
|
|
|
215
223
|
_emitDenied(req, "missing nonce", {});
|
|
216
224
|
return _refuse(res, 401, "graphql federation: nonce required");
|
|
217
225
|
}
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
// Atomic check-and-insert: a redelivered nonce returns false (already
|
|
227
|
+
// present) and is refused; a fresh nonce is inserted with an absolute
|
|
228
|
+
// expiry in one operation, so two concurrent deliveries of the same
|
|
229
|
+
// captured token can't both observe "unseen" and pass.
|
|
230
|
+
return Promise.resolve(nonceStore.checkAndInsert(nonce, Date.now() + nonceTtlMs)).then(function (fresh) {
|
|
231
|
+
if (!fresh) {
|
|
220
232
|
_emitDenied(req, "nonce replay", { nonce: nonce.slice(0, NONCE_PREVIEW_LEN) + "..." });
|
|
221
233
|
return _refuse(res, 401, "graphql federation: nonce replay");
|
|
222
234
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (typeof next === "function") next();
|
|
232
|
-
});
|
|
235
|
+
if (auditOn) {
|
|
236
|
+
audit.safeEmit({
|
|
237
|
+
action: "graphqlfederation.sdl_allowed",
|
|
238
|
+
outcome: "success",
|
|
239
|
+
metadata: {},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (typeof next === "function") next();
|
|
233
243
|
});
|
|
234
244
|
}
|
|
235
245
|
if (auditOn) {
|
|
@@ -57,7 +57,20 @@ function create(config) {
|
|
|
57
57
|
var bytesWritten = 0;
|
|
58
58
|
|
|
59
59
|
function _open() {
|
|
60
|
-
|
|
60
|
+
// O_NOFOLLOW append open: a symlink planted at the active log path is
|
|
61
|
+
// refused (ELOOP) instead of followed, so log writes can't be redirected
|
|
62
|
+
// to an attacker-chosen file (CWE-59). This makes the symlink refusal
|
|
63
|
+
// atomic with the open — a caller's pre-check-then-unlink can't close the
|
|
64
|
+
// race on its own because the sink's own open is what follows the link.
|
|
65
|
+
try {
|
|
66
|
+
fd = atomicFile.openAppendNoFollowSync(activePath, cfg.fileMode);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e && e.code === "ELOOP") {
|
|
69
|
+
throw _err("SYMLINK_REFUSED",
|
|
70
|
+
"refusing to open log at a symlink: " + activePath, true);
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
61
74
|
openedAt = Date.now();
|
|
62
75
|
try {
|
|
63
76
|
var stat = nodeFs.fstatSync(fd);
|
|
@@ -188,6 +188,7 @@ function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
|
|
|
188
188
|
url: url,
|
|
189
189
|
headers: headers,
|
|
190
190
|
body: body,
|
|
191
|
+
timeoutMs: timeoutMs,
|
|
191
192
|
idleTimeoutMs: timeoutMs,
|
|
192
193
|
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
193
194
|
errorClass: LogStreamError,
|
|
@@ -1102,6 +1102,11 @@ async function _fetchDmarcRecord(domain, dnsLookup) {
|
|
|
1102
1102
|
// throw on malformed v= / unrecognized np= or psd= values rather than
|
|
1103
1103
|
// silently dropping — operators with a typo'd record otherwise see the
|
|
1104
1104
|
// fallback policy applied without warning.
|
|
1105
|
+
// RFC 7489 §6.3 — p= (and DMARCbis sp=/np=) take exactly one of
|
|
1106
|
+
// none|quarantine|reject. p= is REQUIRED; a record without a valid p=
|
|
1107
|
+
// carries no usable policy (treat as if no record were published rather
|
|
1108
|
+
// than defaulting any unrecognized value to "deliver").
|
|
1109
|
+
var DMARC_VALID_P = { none: 1, quarantine: 1, reject: 1 };
|
|
1105
1110
|
var DMARCBIS_VALID_NP = { none: 1, quarantine: 1, reject: 1 };
|
|
1106
1111
|
var DMARCBIS_VALID_PSD = { y: 1, n: 1, u: 1 };
|
|
1107
1112
|
|
|
@@ -1115,8 +1120,22 @@ function _parseDmarcRecord(text) {
|
|
|
1115
1120
|
var key = pairs[i][0];
|
|
1116
1121
|
var val = pairs[i][1];
|
|
1117
1122
|
if (key === "v") policy.v = val;
|
|
1118
|
-
else if (key === "p")
|
|
1119
|
-
|
|
1123
|
+
else if (key === "p") {
|
|
1124
|
+
var pVal = val.toLowerCase();
|
|
1125
|
+
if (!Object.prototype.hasOwnProperty.call(DMARC_VALID_P, pVal)) {
|
|
1126
|
+
throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
|
|
1127
|
+
"DMARC p= must be one of none|quarantine|reject, got " + JSON.stringify(val));
|
|
1128
|
+
}
|
|
1129
|
+
policy.p = pVal;
|
|
1130
|
+
}
|
|
1131
|
+
else if (key === "sp") {
|
|
1132
|
+
var spVal = val.toLowerCase();
|
|
1133
|
+
if (!Object.prototype.hasOwnProperty.call(DMARC_VALID_P, spVal)) {
|
|
1134
|
+
throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
|
|
1135
|
+
"DMARC sp= must be one of none|quarantine|reject, got " + JSON.stringify(val));
|
|
1136
|
+
}
|
|
1137
|
+
policy.sp = spVal;
|
|
1138
|
+
}
|
|
1120
1139
|
else if (key === "pct") policy.pct = parseInt(val, 10);
|
|
1121
1140
|
else if (key === "adkim") policy.adkim = val.toLowerCase();
|
|
1122
1141
|
else if (key === "aspf") policy.aspf = val.toLowerCase();
|
|
@@ -1141,6 +1160,16 @@ function _parseDmarcRecord(text) {
|
|
|
1141
1160
|
throw new MailAuthError("mail-auth/dmarc-bad-version",
|
|
1142
1161
|
"DMARC record version must be DMARC1, got " + JSON.stringify(policy.v));
|
|
1143
1162
|
}
|
|
1163
|
+
// RFC 7489 §6.3 — p= is REQUIRED. A record syntactically valid in
|
|
1164
|
+
// every other respect but missing p= publishes no usable policy; the
|
|
1165
|
+
// receiver MUST treat it as if no record were found rather than
|
|
1166
|
+
// synthesizing a permissive default. Fail closed here so the caller
|
|
1167
|
+
// can route the missing-policy case (none/permerror) instead of
|
|
1168
|
+
// delivering an unauthenticated message.
|
|
1169
|
+
if (policy.p === null) {
|
|
1170
|
+
throw new MailAuthError("mail-auth/dmarc-missing-policy",
|
|
1171
|
+
"DMARC record has no required p= tag (RFC 7489 §6.3)");
|
|
1172
|
+
}
|
|
1144
1173
|
return policy;
|
|
1145
1174
|
}
|
|
1146
1175
|
|
|
@@ -1236,7 +1265,18 @@ async function dmarcEvaluate(opts) {
|
|
|
1236
1265
|
}
|
|
1237
1266
|
}
|
|
1238
1267
|
} catch (e) {
|
|
1239
|
-
|
|
1268
|
+
// RFC 7489 §6.6.3 — a syntactically invalid / policy-less record is a
|
|
1269
|
+
// PERMANENT error (permerror); only transient DNS resolution failures
|
|
1270
|
+
// are temperror. The DMARC parser raises typed MailAuthError codes for
|
|
1271
|
+
// the permanent cases (bad version, unrecognized tag value, missing
|
|
1272
|
+
// required p=); anything else (DNS lookup) is transient. Either way the
|
|
1273
|
+
// disposition is fail-closed — neither path yields recommendedAction
|
|
1274
|
+
// "deliver".
|
|
1275
|
+
var permanent = e && typeof e.code === "string" &&
|
|
1276
|
+
(e.code === "mail-auth/dmarc-bad-version" ||
|
|
1277
|
+
e.code === "mail-auth/dmarcbis-bad-tag" ||
|
|
1278
|
+
e.code === "mail-auth/dmarc-missing-policy");
|
|
1279
|
+
return { result: permanent ? "permerror" : "temperror", explanation: e.message,
|
|
1240
1280
|
policy: null, alignment: { spf: false, dkim: false },
|
|
1241
1281
|
orgDomain: orgDomain };
|
|
1242
1282
|
}
|
|
@@ -1312,13 +1352,23 @@ async function dmarcEvaluate(opts) {
|
|
|
1312
1352
|
sampleRoll = bCrypto.randomInt(0, 100); // pct sample roll
|
|
1313
1353
|
}
|
|
1314
1354
|
var sampled = !pass && pct < 100 && sampleRoll >= pct;
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1355
|
+
// Disposition is driven by the validated p= value. policy.p is one of
|
|
1356
|
+
// none|quarantine|reject (the parser fails closed on anything else, so a
|
|
1357
|
+
// missing/typo'd policy never reaches here). "none" is monitor-only →
|
|
1358
|
+
// deliver; reject/quarantine map to their disposition (sampled = the
|
|
1359
|
+
// next-less-strict step per §6.6.4). The explicit "none" arm — rather
|
|
1360
|
+
// than a catch-all "else → deliver" — keeps an unexpected value from
|
|
1361
|
+
// silently delivering a failing message.
|
|
1362
|
+
var recommendedAction;
|
|
1363
|
+
if (pass) {
|
|
1364
|
+
recommendedAction = "deliver";
|
|
1365
|
+
} else if (policy.p === "none") {
|
|
1366
|
+
recommendedAction = "deliver";
|
|
1367
|
+
} else if (sampled) {
|
|
1368
|
+
recommendedAction = policy.p === "reject" ? "quarantine" : "none"; // p is reject|quarantine here
|
|
1369
|
+
} else {
|
|
1370
|
+
recommendedAction = policy.p === "reject" ? "reject" : "quarantine"; // p is reject|quarantine here
|
|
1371
|
+
}
|
|
1322
1372
|
|
|
1323
1373
|
return {
|
|
1324
1374
|
result: pass ? "pass" : "fail",
|
|
@@ -1544,10 +1594,15 @@ async function arcVerify(rfc822, opts) {
|
|
|
1544
1594
|
var asX = asTags.x ? parseInt(asTags.x, 10) : null;
|
|
1545
1595
|
var skewSec = Math.floor(arcClockSkewMs / 1000); // sec divisor
|
|
1546
1596
|
var timeFault = null;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1597
|
+
// RFC 8617 §5.2 — a PRESENT t=/x= that is unparseable (parseInt → NaN) must
|
|
1598
|
+
// FAIL CLOSED (a time fault), not silently skip the future/expiry check: a
|
|
1599
|
+
// bare `isFinite(amsT) &&` lets a malformed timestamp slip the MUST-reject
|
|
1600
|
+
// gate. Gate on the RAW tag's presence (amsTags.t/x) so an unparseable value
|
|
1601
|
+
// becomes a fault, while an absent tag is correctly ignored.
|
|
1602
|
+
if (amsTags.t && (!isFinite(amsT) || amsT - skewSec > nowSec)) timeFault = isFinite(amsT) ? "ams-t-future" : "ams-t-unparseable";
|
|
1603
|
+
if (amsTags.x && (!isFinite(amsX) || amsX + skewSec < nowSec)) timeFault = isFinite(amsX) ? "ams-x-expired" : "ams-x-unparseable";
|
|
1604
|
+
if (asTags.t && (!isFinite(asT) || asT - skewSec > nowSec)) timeFault = isFinite(asT) ? "as-t-future" : "as-t-unparseable";
|
|
1605
|
+
if (asTags.x && (!isFinite(asX) || asX + skewSec < nowSec)) timeFault = isFinite(asX) ? "as-x-expired" : "as-x-unparseable";
|
|
1551
1606
|
|
|
1552
1607
|
// AMS — RFC 8617 §5.1.1. Same shape as a DKIM-Signature; reuses
|
|
1553
1608
|
// the DKIM verifier by injecting a temporary message that has
|
|
@@ -819,6 +819,12 @@ function _verifyCertChain(leaf, intermediates, anchors) {
|
|
|
819
819
|
while (depth < MAX_DEPTH) {
|
|
820
820
|
var notBefore = Date.parse(current.validFrom);
|
|
821
821
|
var notAfter = Date.parse(current.validTo);
|
|
822
|
+
// Fail closed on a present-but-unparseable validity window: a NaN date
|
|
823
|
+
// would otherwise skip both window checks below and let the cert pass
|
|
824
|
+
// validation unchecked.
|
|
825
|
+
if (!isFinite(notBefore) || !isFinite(notAfter)) {
|
|
826
|
+
return { ok: false, reason: "cert validity dates unparseable" };
|
|
827
|
+
}
|
|
822
828
|
if (isFinite(notBefore) && now < notBefore) {
|
|
823
829
|
return { ok: false, reason: "cert not-yet-valid (notBefore=" + current.validFrom + ")" };
|
|
824
830
|
}
|
|
@@ -829,6 +829,16 @@ function checkCert(opts) {
|
|
|
829
829
|
var nowMs = Date.now();
|
|
830
830
|
var notBeforeMs = Date.parse(cert.validFrom);
|
|
831
831
|
var notAfterMs = Date.parse(cert.validTo);
|
|
832
|
+
// Fail closed on a present-but-unparseable validity field — gating
|
|
833
|
+
// each comparison behind isFinite() alone let a NaN notBefore /
|
|
834
|
+
// notAfter skip both checks and pass validity. RFC 5280 §4.1.2.5
|
|
835
|
+
// makes both dates mandatory; a date a peer cannot parse must be
|
|
836
|
+
// refused at preflight, not accepted and left to fail interop later.
|
|
837
|
+
if (!isFinite(notBeforeMs) || !isFinite(notAfterMs)) {
|
|
838
|
+
throw new MailCryptoError("mail-crypto/smime/bad-validity",
|
|
839
|
+
"cert has unparseable validity dates (validFrom=" + cert.validFrom +
|
|
840
|
+
", validTo=" + cert.validTo + ")");
|
|
841
|
+
}
|
|
832
842
|
if (isFinite(notBeforeMs) && nowMs < notBeforeMs) {
|
|
833
843
|
throw new MailCryptoError("mail-crypto/smime/expired-cert",
|
|
834
844
|
"cert is not yet valid (notBefore=" + cert.validFrom + ", now=" +
|