@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/cart.js +75 -0
  4. package/lib/order.js +4 -2
  5. package/lib/security-middleware.js +1 -0
  6. package/lib/storefront.js +8 -1
  7. package/lib/vendor/MANIFEST.json +71 -39
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  10. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  11. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  12. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  13. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  14. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  15. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  16. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  17. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  19. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  20. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  21. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  22. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  23. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  24. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  25. package/lib/vendor/blamejs/lib/mail.js +39 -0
  26. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  27. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  28. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  29. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  30. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  31. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  32. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  33. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  34. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  35. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  36. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  37. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  38. package/lib/vendor/blamejs/lib/session.js +85 -28
  39. package/lib/vendor/blamejs/package.json +1 -1
  40. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  59. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  60. package/lib/webhooks.js +38 -9
  61. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.15.15",
4
- "createdAt": "2026-06-22T00:16:09.227Z",
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": 1
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 + ")");
@@ -434,6 +434,7 @@ function policy(opts) {
434
434
  method: "GET",
435
435
  url: url,
436
436
  headers: { "User-Agent": "blamejs-password-policy/1" },
437
+ timeoutMs: p.hibpTimeoutMs,
437
438
  idleTimeoutMs: p.hibpTimeoutMs,
438
439
  errorClass: AuthError,
439
440
  });
@@ -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 && isFinite(Date.parse(nbHok) / 1000) && // ms→s
696
- Date.parse(nbHok) / 1000 > nowSec + clockSkewSec) continue; // ms→s
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
- if (isFinite(nb) && nb > nowSec + clockSkewSec) continue;
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
- var fd = nodeFs.openSync(logFile, "a", DEFAULT_LOG_FILE_MODE);
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: { has(nonce): bool, remember(nonce, ttlMs) }, // optional replay protection
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
- var nonceStore = opts.nonceStore && typeof opts.nonceStore.has === "function" &&
147
- typeof opts.nonceStore.remember === "function" ? opts.nonceStore : null;
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
- return Promise.resolve(nonceStore.has(nonce)).then(function (seen) {
219
- if (seen) {
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
- return Promise.resolve(nonceStore.remember(nonce, nonceTtlMs)).then(function () {
224
- if (auditOn) {
225
- audit.safeEmit({
226
- action: "graphqlfederation.sdl_allowed",
227
- outcome: "success",
228
- metadata: {},
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) {
@@ -161,6 +161,7 @@ function _post(cfg, body, headers) {
161
161
  url: _resolveEndpoint(cfg),
162
162
  headers: headers,
163
163
  body: body,
164
+ timeoutMs: cfg.timeoutMs,
164
165
  idleTimeoutMs: cfg.timeoutMs,
165
166
  maxResponseBytes: MAX_RESPONSE_BYTES,
166
167
  errorClass: LogStreamError,
@@ -57,7 +57,20 @@ function create(config) {
57
57
  var bytesWritten = 0;
58
58
 
59
59
  function _open() {
60
- fd = nodeFs.openSync(activePath, "a", cfg.fileMode);
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,
@@ -67,6 +67,7 @@ function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
67
67
  url: url,
68
68
  headers: headers,
69
69
  body: body,
70
+ timeoutMs: timeoutMs,
70
71
  idleTimeoutMs: timeoutMs,
71
72
  maxResponseBytes: MAX_RESPONSE_BYTES,
72
73
  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") policy.p = val.toLowerCase();
1119
- else if (key === "sp") policy.sp = val.toLowerCase();
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
- return { result: "temperror", explanation: e.message,
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
- var recommendedAction = pass ? "deliver" :
1316
- sampled
1317
- ? (policy.p === "reject" ? "quarantine" :
1318
- policy.p === "quarantine" ? "none" : "deliver")
1319
- : (policy.p === "reject" ? "reject" :
1320
- policy.p === "quarantine" ? "quarantine" :
1321
- "deliver");
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
- if (amsT && isFinite(amsT) && amsT - skewSec > nowSec) timeFault = "ams-t-future";
1548
- if (amsX && isFinite(amsX) && amsX + skewSec < nowSec) timeFault = "ams-x-expired";
1549
- if (asT && isFinite(asT) && asT - skewSec > nowSec) timeFault = "as-t-future";
1550
- if (asX && isFinite(asX) && asX + skewSec < nowSec) timeFault = "as-x-expired";
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=" +