@blamejs/core 0.15.13 → 0.15.15

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 (208) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +38 -6
  4. package/lib/agent-event-bus.js +13 -0
  5. package/lib/agent-idempotency.js +5 -1
  6. package/lib/agent-snapshot.js +32 -2
  7. package/lib/ai-aedt-bias-audit.js +2 -1
  8. package/lib/ai-content-detect.js +1 -3
  9. package/lib/ai-frontier-protocol.js +1 -1
  10. package/lib/ai-model-manifest.js +1 -1
  11. package/lib/ai-output.js +16 -7
  12. package/lib/api-snapshot.js +4 -1
  13. package/lib/app-shutdown.js +7 -1
  14. package/lib/archive-gz.js +9 -0
  15. package/lib/archive-read.js +9 -7
  16. package/lib/archive-tar-read.js +51 -8
  17. package/lib/archive.js +4 -2
  18. package/lib/asn1-der.js +70 -22
  19. package/lib/atomic-file.js +204 -2
  20. package/lib/audit-chain.js +54 -5
  21. package/lib/audit-daily-review.js +12 -2
  22. package/lib/audit-sign.js +2 -1
  23. package/lib/audit-tools.js +108 -23
  24. package/lib/audit.js +7 -2
  25. package/lib/auth/access-lock.js +2 -2
  26. package/lib/auth/bot-challenge.js +1 -1
  27. package/lib/auth/ciba.js +71 -8
  28. package/lib/auth/dpop.js +15 -1
  29. package/lib/auth/fido-mds3.js +30 -13
  30. package/lib/auth/jwt.js +21 -5
  31. package/lib/auth/lockout.js +18 -2
  32. package/lib/auth/oauth.js +8 -2
  33. package/lib/auth/passkey.js +1 -1
  34. package/lib/auth/password.js +1 -1
  35. package/lib/auth/saml.js +42 -12
  36. package/lib/auth/sd-jwt-vc.js +24 -4
  37. package/lib/auth/status-list.js +14 -2
  38. package/lib/auth/step-up.js +9 -1
  39. package/lib/auth-bot-challenge.js +21 -2
  40. package/lib/backup/bundle.js +7 -2
  41. package/lib/backup/crypto.js +23 -8
  42. package/lib/backup/index.js +41 -20
  43. package/lib/backup/manifest.js +7 -1
  44. package/lib/break-glass.js +41 -22
  45. package/lib/cbor.js +34 -11
  46. package/lib/cdn-cache-control.js +7 -3
  47. package/lib/cert.js +5 -3
  48. package/lib/cli.js +5 -1
  49. package/lib/cloud-events.js +3 -2
  50. package/lib/cluster-storage.js +7 -3
  51. package/lib/codepoint-class.js +17 -0
  52. package/lib/compliance-eaa.js +1 -1
  53. package/lib/compliance-sanctions.js +9 -7
  54. package/lib/compliance.js +1 -1
  55. package/lib/config-drift.js +22 -8
  56. package/lib/content-credentials.js +11 -8
  57. package/lib/content-digest.js +11 -4
  58. package/lib/cookies.js +10 -2
  59. package/lib/cose.js +20 -0
  60. package/lib/crdt.js +2 -1
  61. package/lib/crypto-field.js +48 -24
  62. package/lib/crypto.js +18 -4
  63. package/lib/csp.js +13 -0
  64. package/lib/daemon.js +4 -1
  65. package/lib/data-act.js +27 -4
  66. package/lib/db-file-lifecycle.js +14 -6
  67. package/lib/db-query.js +102 -11
  68. package/lib/db.js +32 -15
  69. package/lib/dora.js +43 -13
  70. package/lib/dr-runbook.js +1 -1
  71. package/lib/dsa.js +2 -1
  72. package/lib/dsr.js +22 -8
  73. package/lib/early-hints.js +19 -0
  74. package/lib/eat.js +5 -1
  75. package/lib/external-db.js +60 -4
  76. package/lib/fda-21cfr11.js +30 -5
  77. package/lib/flag-providers.js +6 -2
  78. package/lib/forms.js +1 -1
  79. package/lib/gate-contract.js +46 -5
  80. package/lib/gdpr-ropa.js +18 -9
  81. package/lib/graphql-federation.js +17 -4
  82. package/lib/guard-all.js +2 -2
  83. package/lib/guard-dsn.js +1 -1
  84. package/lib/guard-envelope.js +1 -1
  85. package/lib/guard-html.js +9 -11
  86. package/lib/guard-imap-command.js +1 -1
  87. package/lib/guard-jmap.js +1 -1
  88. package/lib/guard-json.js +14 -6
  89. package/lib/guard-mail-move.js +1 -1
  90. package/lib/guard-managesieve-command.js +1 -1
  91. package/lib/guard-pop3-command.js +1 -1
  92. package/lib/guard-smtp-command.js +1 -1
  93. package/lib/guard-svg.js +8 -9
  94. package/lib/html-balance.js +7 -3
  95. package/lib/http-client-cookie-jar.js +33 -12
  96. package/lib/http-client.js +225 -53
  97. package/lib/iab-tcf.js +3 -2
  98. package/lib/importmap-integrity.js +41 -1
  99. package/lib/incident-report.js +9 -6
  100. package/lib/json-patch.js +1 -1
  101. package/lib/json-path.js +24 -3
  102. package/lib/jtd.js +2 -2
  103. package/lib/legal-hold.js +24 -8
  104. package/lib/log.js +2 -2
  105. package/lib/mail-agent.js +2 -2
  106. package/lib/mail-arf.js +1 -1
  107. package/lib/mail-auth.js +27 -4
  108. package/lib/mail-bimi.js +16 -16
  109. package/lib/mail-bounce.js +3 -3
  110. package/lib/mail-crypto-smime.js +71 -6
  111. package/lib/mail-deploy.js +9 -5
  112. package/lib/mail-dkim.js +20 -7
  113. package/lib/mail-greylist.js +2 -4
  114. package/lib/mail-helo.js +2 -4
  115. package/lib/mail-journal.js +11 -8
  116. package/lib/mail-mdn.js +8 -4
  117. package/lib/mail-rbl.js +2 -4
  118. package/lib/mail-scan.js +3 -5
  119. package/lib/mail-server-jmap.js +4 -1
  120. package/lib/mail-server-registry.js +1 -1
  121. package/lib/mail-server-tls.js +9 -2
  122. package/lib/mail-spam-score.js +2 -4
  123. package/lib/mail-store-fts.js +1 -1
  124. package/lib/mail.js +22 -2
  125. package/lib/markup-tokenizer.js +24 -0
  126. package/lib/mcp.js +6 -4
  127. package/lib/mdoc.js +26 -3
  128. package/lib/metrics.js +14 -3
  129. package/lib/middleware/api-encrypt.js +2 -2
  130. package/lib/middleware/body-parser.js +10 -4
  131. package/lib/middleware/bot-guard.js +26 -18
  132. package/lib/middleware/clear-site-data.js +5 -1
  133. package/lib/middleware/compose-pipeline.js +39 -5
  134. package/lib/middleware/compression.js +9 -0
  135. package/lib/middleware/cors.js +32 -23
  136. package/lib/middleware/csrf-protect.js +60 -21
  137. package/lib/middleware/daily-byte-quota.js +6 -4
  138. package/lib/middleware/fetch-metadata.js +28 -4
  139. package/lib/middleware/network-allowlist.js +61 -30
  140. package/lib/middleware/rate-limit.js +25 -16
  141. package/lib/middleware/scim-server.js +2 -1
  142. package/lib/middleware/security-headers.js +24 -6
  143. package/lib/middleware/speculation-rules.js +6 -3
  144. package/lib/middleware/tus-upload.js +2 -2
  145. package/lib/money.js +1 -1
  146. package/lib/mtls-ca.js +10 -6
  147. package/lib/network-dns-resolver.js +1 -1
  148. package/lib/network-dns.js +9 -2
  149. package/lib/network-dnssec.js +2 -1
  150. package/lib/network-smtp-policy.js +23 -5
  151. package/lib/network-tls.js +27 -5
  152. package/lib/network-tsig.js +2 -2
  153. package/lib/nis2-report.js +1 -1
  154. package/lib/nist-crosswalk.js +1 -1
  155. package/lib/ntp-check.js +28 -0
  156. package/lib/numeric-bounds.js +9 -0
  157. package/lib/object-store/azure-blob.js +1 -2
  158. package/lib/object-store/gcs-bucket-ops.js +4 -2
  159. package/lib/object-store/gcs.js +6 -4
  160. package/lib/object-store/http-put.js +1 -2
  161. package/lib/object-store/http-request.js +30 -1
  162. package/lib/object-store/local.js +37 -17
  163. package/lib/object-store/sigv4.js +1 -2
  164. package/lib/observability-otlp-exporter.js +20 -4
  165. package/lib/outbox.js +11 -4
  166. package/lib/parsers/safe-xml.js +1 -1
  167. package/lib/parsers/safe-yaml.js +21 -3
  168. package/lib/pipl-cn.js +11 -8
  169. package/lib/queue-local.js +10 -3
  170. package/lib/redact.js +7 -3
  171. package/lib/request-helpers.js +347 -36
  172. package/lib/resource-access-lock.js +3 -3
  173. package/lib/restore-bundle.js +46 -18
  174. package/lib/restore-rollback.js +10 -4
  175. package/lib/restore.js +19 -0
  176. package/lib/retention.js +20 -4
  177. package/lib/router.js +17 -4
  178. package/lib/safe-ical.js +2 -2
  179. package/lib/safe-icap.js +1 -1
  180. package/lib/safe-json.js +70 -0
  181. package/lib/safe-sieve.js +1 -1
  182. package/lib/safe-vcard.js +1 -1
  183. package/lib/sandbox-worker.js +6 -0
  184. package/lib/sandbox.js +1 -1
  185. package/lib/scheduler.js +17 -1
  186. package/lib/self-update-standalone-verifier.js +16 -0
  187. package/lib/session.js +62 -120
  188. package/lib/sql.js +25 -3
  189. package/lib/static.js +65 -13
  190. package/lib/template.js +7 -5
  191. package/lib/tenant-quota.js +52 -19
  192. package/lib/tsa.js +5 -2
  193. package/lib/vault/index.js +5 -0
  194. package/lib/vault/passphrase-ops.js +22 -26
  195. package/lib/vault/passphrase-source.js +8 -3
  196. package/lib/vault/rotate.js +13 -18
  197. package/lib/vault/seal-pem-file.js +4 -1
  198. package/lib/vc.js +1 -1
  199. package/lib/vendor/MANIFEST.json +10 -10
  200. package/lib/vendor/public-suffix-list.dat +23 -10
  201. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  202. package/lib/webhook.js +16 -1
  203. package/lib/websocket.js +1 -1
  204. package/lib/worm.js +1 -1
  205. package/lib/ws-client.js +83 -46
  206. package/lib/x509-chain.js +91 -0
  207. package/package.json +1 -1
  208. package/sbom.cdx.json +6 -6
package/lib/dora.js CHANGED
@@ -62,12 +62,19 @@ var SIGNIFICANT_INCIDENT_THRESHOLDS = Object.freeze({
62
62
  durationCriticalProcessMs: C.TIME.hours(2), // 2h
63
63
  });
64
64
 
65
- // Article 19 initial report deadline: 24h from "first awareness".
66
- // Article 19(4) intermediate update: within 72h of initial.
67
- // Article 19(6) final report: within 1 month of initial.
68
- var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(24);
69
- var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
70
- var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
65
+ // DORA major-incident reporting timeline (Art. 19 + RTS (EU) 2024/1772 Art. 5):
66
+ // - Initial notification: as early as possible and WITHIN 4 HOURS of
67
+ // classifying the incident as major (the outer bound is 24h from first
68
+ // awareness). classify() returns mustReportInitialByMs as a duration FROM
69
+ // classification, so the operative window encoded here is 4h — the
70
+ // 24h-from-awareness outer bound is enforced separately via
71
+ // INITIAL_REPORT_OUTER_DEADLINE_MS below.
72
+ // - Intermediate update: within 72h of the INITIAL notification's submission.
73
+ // - Final report: within 1 month of the intermediate report's submission.
74
+ var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(4);
75
+ var INITIAL_REPORT_OUTER_DEADLINE_MS = C.TIME.hours(24);
76
+ var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
77
+ var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
71
78
 
72
79
  // Adjacent-regulation incident-reporting deadlines — operators wiring
73
80
  // NIS2 / CRA / HIPAA breach notification reach for these constants
@@ -124,14 +131,26 @@ function _classifyImpl(input) {
124
131
  reasons.push("severity-high");
125
132
  }
126
133
 
127
- // 2. Affected clients (absolute).
134
+ // 2. Affected clients absolute count OR share of the client base. The
135
+ // percentile thresholds (RTS 2024/1772 Art. 1(1)(a): >10% major, ESA-
136
+ // guideline 1% significant) were declared but never evaluated, so a
137
+ // large-share-but-small-absolute incident (e.g. 16% of a 50k client base)
138
+ // under-classified. Operators pass clientBase (total clients) to enable
139
+ // the percentage criterion; absolute-only behavior is unchanged when it
140
+ // is omitted.
128
141
  if (typeof input.affectedClients === "number" && input.affectedClients > 0) {
129
- if (input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
142
+ var clientBase = (typeof input.clientBase === "number" && input.clientBase > 0) ? input.clientBase : null;
143
+ var clientPct = clientBase ? (input.affectedClients / clientBase) : 0;
144
+ var majorByClients = input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
145
+ (clientBase !== null && clientPct >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsPercentile);
146
+ var significantByClients = input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute ||
147
+ (clientBase !== null && clientPct >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsPercentile);
148
+ if (majorByClients) {
130
149
  hitsMajor += 1;
131
- reasons.push("clients-major-absolute");
132
- } else if (input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
150
+ reasons.push("clients-major");
151
+ } else if (significantByClients) {
133
152
  hitsSignificant += 1;
134
- reasons.push("clients-significant-absolute");
153
+ reasons.push("clients-significant");
135
154
  }
136
155
  }
137
156
 
@@ -188,7 +207,11 @@ function _classifyImpl(input) {
188
207
  return {
189
208
  classification: classification,
190
209
  mustReport: mustReport,
210
+ // Operative initial-notification window: 4h from this classification.
191
211
  mustReportInitialByMs: mustReport ? INITIAL_REPORT_DEADLINE_MS : null,
212
+ // Outer bound: no later than 24h from first awareness — surfaced so an
213
+ // operator scheduling from `detectedAt` can enforce min(class+4h, aware+24h).
214
+ mustReportInitialOuterByMs: mustReport ? INITIAL_REPORT_OUTER_DEADLINE_MS : null,
192
215
  reasons: reasons,
193
216
  };
194
217
  }
@@ -343,10 +366,16 @@ function create(opts) {
343
366
  // Article 19 deadline — operator-side scheduler uses this.
344
367
  nextStageDueAt: null,
345
368
  };
369
+ // Anchor each downstream deadline on the SUBMISSION time of the report
370
+ // being filed (record.reportedAt), not on detectedAt: the intermediate is
371
+ // due 72h after the initial is *submitted*, and the final one month after
372
+ // the intermediate is *submitted* (DORA RTS 2024/1772 reporting timeline).
373
+ // Anchoring on detectedAt understates the deadline whenever a report is
374
+ // filed later than detection.
346
375
  if (input.stage === "initial") {
347
- record.nextStageDueAt = input.detectedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
376
+ record.nextStageDueAt = record.reportedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
348
377
  } else if (input.stage === "intermediate") {
349
- record.nextStageDueAt = input.detectedAt + FINAL_REPORT_DEADLINE_MS;
378
+ record.nextStageDueAt = record.reportedAt + FINAL_REPORT_DEADLINE_MS;
350
379
  }
351
380
  _emit("dora.incident.reported", {
352
381
  metadata: {
@@ -396,6 +425,7 @@ module.exports = {
396
425
  MAJOR_INCIDENT_THRESHOLDS: MAJOR_INCIDENT_THRESHOLDS,
397
426
  SIGNIFICANT_INCIDENT_THRESHOLDS: SIGNIFICANT_INCIDENT_THRESHOLDS,
398
427
  INITIAL_REPORT_DEADLINE_MS: INITIAL_REPORT_DEADLINE_MS,
428
+ INITIAL_REPORT_OUTER_DEADLINE_MS: INITIAL_REPORT_OUTER_DEADLINE_MS,
399
429
  INTERMEDIATE_REPORT_DEADLINE_MS: INTERMEDIATE_REPORT_DEADLINE_MS,
400
430
  FINAL_REPORT_DEADLINE_MS: FINAL_REPORT_DEADLINE_MS,
401
431
  DEADLINES_NIS2: DEADLINES_NIS2,
package/lib/dr-runbook.js CHANGED
@@ -275,7 +275,7 @@ async function emit(opts) {
275
275
  "drRunbook.emit: outDir", DrRunbookError, "dr-runbook/no-outdir");
276
276
  validateOpts.requireNonEmptyString(opts.posture,
277
277
  "drRunbook.emit: posture", DrRunbookError, "dr-runbook/no-posture");
278
- if (!POSTURE_BLOCKS[opts.posture]) {
278
+ if (!Object.prototype.hasOwnProperty.call(POSTURE_BLOCKS, opts.posture)) {
279
279
  throw new DrRunbookError("dr-runbook/unknown-posture",
280
280
  "drRunbook.emit: posture '" + opts.posture + "' not in supported list (" +
281
281
  Object.keys(POSTURE_BLOCKS).join(", ") + ")");
package/lib/dsa.js CHANGED
@@ -36,6 +36,7 @@
36
36
 
37
37
  var validateOpts = require("./validate-opts");
38
38
  var lazyRequire = require("./lazy-require");
39
+ var numericBounds = require("./numeric-bounds");
39
40
  var C = require("./constants");
40
41
  var { DsaError } = require("./framework-error");
41
42
 
@@ -414,7 +415,7 @@ function transparencyReport(opts) {
414
415
  METRIC_FIELDS.forEach(function (field) {
415
416
  var v = supplied[field];
416
417
  if (v === undefined || v === null) { metrics[field] = 0; return; }
417
- if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) {
418
+ if (!numericBounds.isNonNegativeFiniteInt(v)) {
418
419
  throw new DsaError("dsa/bad-metric-value",
419
420
  "b.dsa.transparencyReport: metrics." + field +
420
421
  " must be a non-negative integer, got " +
package/lib/dsr.js CHANGED
@@ -210,7 +210,7 @@ var POSTURE_DEADLINE_MS = Object.freeze({
210
210
  // + extension). Read-only at module scope.
211
211
  function _deadlineForPosture(posture) {
212
212
  if (typeof posture !== "string") return POSTURE_DEADLINE_MS["default"];
213
- return POSTURE_DEADLINE_MS[posture] || POSTURE_DEADLINE_MS["default"];
213
+ return Object.prototype.hasOwnProperty.call(POSTURE_DEADLINE_MS, posture) ? POSTURE_DEADLINE_MS[posture] : POSTURE_DEADLINE_MS["default"];
214
214
  }
215
215
 
216
216
  function _now() { return Date.now(); }
@@ -1198,12 +1198,26 @@ function dbTicketStore(opts) {
1198
1198
  SUBJECT_FILTER_SPEC.forEach(function (spec) {
1199
1199
  var supplied = filter.subject[spec.key];
1200
1200
  if (!supplied) return;
1201
- var column = vaulted ? spec.hashCol : spec.plainCol;
1202
- var match = vaulted
1203
- ? (function () { var d = cryptoField().computeDerived(DSR_SEAL_TABLE, spec.sealField, supplied); return d ? d.value : null; })()
1204
- : supplied;
1205
- conds.push(column + " = " + spec.param);
1206
- params[spec.param] = match;
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(", ") + ")");
1207
1221
  });
1208
1222
  }
1209
1223
 
@@ -1455,7 +1469,7 @@ var STATE_RULES = Object.freeze({
1455
1469
  function stateRules(state) {
1456
1470
  if (typeof state !== "string" || state.length === 0) return null;
1457
1471
  // Direct posture-name lookup first
1458
- if (STATE_RULES[state]) return Object.assign({}, STATE_RULES[state]);
1472
+ if (Object.prototype.hasOwnProperty.call(STATE_RULES, state)) return Object.assign({}, STATE_RULES[state]);
1459
1473
  // 2-letter state abbreviation lookup (case-insensitive)
1460
1474
  var u = state.toUpperCase();
1461
1475
  var keys = Object.keys(STATE_RULES);
@@ -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
- if (typeof ds !== "number" || ds < DBGSTAT.disabled) {
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");
@@ -339,6 +339,50 @@ function _classifyStatement(sql) {
339
339
  return _STATEMENT_CLASS_MAP[kw] || "OTHER";
340
340
  }
341
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
+
342
386
  // Statement classes that place no rows on the backend, so the cross-
343
387
  // border residency write gate lets them pass without a row tag. Every
344
388
  // other class — DML, ROUTINE (CALL / EXECUTE / DO), a COPY ... FROM
@@ -1810,12 +1854,19 @@ async function _readQuery(sql, params, opts) {
1810
1854
  // was explicitly configured allowCrossBorder (which is audited).
1811
1855
  var _readPosture = _activePosture();
1812
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";
1813
1864
  // Fail CLOSED when the row's region is not identified: a regulated read to a
1814
- // residency-tagged replica without opts.rowResidencyTag would otherwise route
1815
- // residency-restricted rows to an arbitrary-region replica with no check at
1816
- // 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).
1817
1868
  if (!_tagPresent && _crossBorderRegulated(_readPosture) &&
1818
- replica.residencyTag && !replica.allowCrossBorder) {
1869
+ _replicaConstrained && !replica.allowCrossBorder) {
1819
1870
  _emit("db.residency.replica.tag_required", "denied", {
1820
1871
  backend: b.name, replicaIdx: replica.index,
1821
1872
  replicaTag: replica.residencyTag, posture: _readPosture,
@@ -2574,6 +2625,11 @@ module.exports = {
2574
2625
  // lock tables live on the externalDb side. See lib/external-db-migrate.js.
2575
2626
  migrate: externalDbMigrate,
2576
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,
2577
2633
  _resetForTest: _resetForTest,
2578
2634
  _extractTargetRelation: _extractTargetRelation,
2579
2635
  };
@@ -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
- // Modification-shaped events (verbs containing "update" / "modif" /
118
- // "delete" / "rectif" / "erase" / "set") must carry before/after.
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 modShape = /\.(update|updated|modif|modified|delete|deleted|rectif|rectified|erase|erased|set|setrole|put|patched)\b/.test(verb) ||
121
- /\.(update|delete|modif|set|put|patch|rectif|erase)/.test(verb);
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 —
@@ -263,7 +279,16 @@ function posture(opts) {
263
279
  }, "denied");
264
280
  return { ok: false, reason: "record-hash-mismatch" };
265
281
  }
266
- if (verifyWith && signed.signature) {
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
+ }
267
292
  var sigBuf = Buffer.from(signed.signature, "base64");
268
293
  var payload = JSON.stringify({
269
294
  printedName: signed.printedName,
@@ -41,6 +41,7 @@ var validateOpts = require("./validate-opts");
41
41
  var lazyRequire = require("./lazy-require");
42
42
  var safeJson = require("./safe-json");
43
43
  var C = require("./constants");
44
+ var atomicFile = require("./atomic-file");
44
45
  var { defineClass } = require("./framework-error");
45
46
  var FlagError = defineClass("FlagError", { alwaysPermanent: true });
46
47
 
@@ -128,7 +129,10 @@ function localFile(opts) {
128
129
  validateOpts.requireNonEmptyString(opts.path,
129
130
  "providers.localFile: path", FlagError, "flag/bad-provider");
130
131
  var raw;
131
- try { raw = nodeFs.readFileSync(opts.path, "utf8"); }
132
+ // Capped fd-bound read; the cap precedes the alloc so an oversized flag file
133
+ // can't OOM boot. refuseSymlink stays OFF: a flag file is commonly a k8s
134
+ // ConfigMap mount, which is a symlink chain — refusing it would break that.
135
+ try { raw = atomicFile.fdSafeReadSync(opts.path, { maxBytes: C.BYTES.mib(1), encoding: "utf8" }); }
132
136
  catch (e) {
133
137
  throw new FlagError("flag/bad-provider",
134
138
  "providers.localFile: cannot read file " + JSON.stringify(opts.path) +
@@ -154,7 +158,7 @@ function localFile(opts) {
154
158
  try {
155
159
  nodeFs.watch(opts.path, { persistent: false }, function () {
156
160
  try {
157
- var nextRaw = nodeFs.readFileSync(opts.path, "utf8");
161
+ var nextRaw = atomicFile.fdSafeReadSync(opts.path, { maxBytes: C.BYTES.mib(1), encoding: "utf8" });
158
162
  var nextParsed = safeJson.parse(nextRaw, { maxBytes: C.BYTES.mib(1) });
159
163
  if (nextParsed && nextParsed.flags) {
160
164
  for (var k in nextParsed.flags) {
package/lib/forms.js CHANGED
@@ -252,7 +252,7 @@ function _renderField(field) {
252
252
  var control;
253
253
  if (type === "textarea") control = _renderTextarea(field);
254
254
  else if (type === "select") control = _renderSelect(field);
255
- else if (INPUT_TYPES[type]) control = _renderInput(field);
255
+ else if (Object.prototype.hasOwnProperty.call(INPUT_TYPES, type)) control = _renderInput(field);
256
256
  else throw new Error("forms.render: unsupported field type: " + type);
257
257
 
258
258
  // Hidden + submit fields don't need a label wrapper
@@ -1095,13 +1095,49 @@ function makeProfileBuilder(profiles) {
1095
1095
  * posture.piiPolicy; // → "redact"
1096
1096
  */
1097
1097
  function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
1098
- if (!postures || !postures[name]) {
1098
+ // hasOwnProperty: `name` is caller input; a bracket lookup would let a
1099
+ // prototype key like "constructor" pass the presence check and reach
1100
+ // Object.assign (proto shadowing).
1101
+ if (!postures || !Object.prototype.hasOwnProperty.call(postures, name)) {
1099
1102
  throw errorFactory(codePrefix + ".bad-posture",
1100
1103
  "unknown compliancePosture " + JSON.stringify(name));
1101
1104
  }
1102
1105
  return Object.assign({}, postures[name]);
1103
1106
  }
1104
1107
 
1108
+ /**
1109
+ * @primitive b.gateContract.makePostureAccessor
1110
+ * @signature b.gateContract.makePostureAccessor(postures, opts?)
1111
+ * @since 0.15.14
1112
+ * @status stable
1113
+ * @related b.gateContract.lookupCompliancePosture, b.gateContract.resolveProfileName
1114
+ *
1115
+ * Build the public `compliancePosture(name)` accessor a guard exposes — maps a
1116
+ * compliance-posture name through the guard's own `postures` table to the
1117
+ * profile it selects, returning `opts.fallback` (default `null`) for an
1118
+ * unknown name. Folds the one-line lookup the mail-scanner / content-detect
1119
+ * factories each redefined verbatim (`return POSTURES[name] || null`) into one
1120
+ * proto-shadow-safe helper: the membership test is `hasOwnProperty.call` so a
1121
+ * prototype key (constructor / __proto__ / toString) resolves to the fallback
1122
+ * rather than an inherited Function. Unlike `lookupCompliancePosture` it does
1123
+ * NOT throw and returns the raw mapped value (a profile name string), not an
1124
+ * object copy.
1125
+ *
1126
+ * @opts
1127
+ * fallback: any, // value returned for an unknown / proto-key name (default null)
1128
+ *
1129
+ * @example
1130
+ * var compliancePosture = b.gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
1131
+ * compliancePosture("hipaa"); // → "strict"
1132
+ * compliancePosture("constructor"); // → null (proto key, not an own posture)
1133
+ */
1134
+ function makePostureAccessor(postures, opts) {
1135
+ var fallback = (opts && Object.prototype.hasOwnProperty.call(opts, "fallback")) ? opts.fallback : null;
1136
+ return function (name) {
1137
+ return Object.prototype.hasOwnProperty.call(postures, name) ? postures[name] : fallback;
1138
+ };
1139
+ }
1140
+
1105
1141
  // "GuardCidrError" -> "guardCidr" — the guard's audit/message identity, derived
1106
1142
  // once from its error class name. Used for the default gate's audit/metric
1107
1143
  // prefix AND the profile resolver's error message, so neither re-cases the name.
@@ -1199,9 +1235,13 @@ function makeProfileResolver(cfg) {
1199
1235
  */
1200
1236
  function resolveProfileName(opts, postures, defaultProfile) {
1201
1237
  opts = opts || {};
1202
- return opts.profile ||
1203
- (opts.posture && postures && postures[opts.posture]) ||
1204
- defaultProfile;
1238
+ // hasOwnProperty: opts.posture is caller/operator input; a bracket lookup
1239
+ // would let a prototype key like "constructor" resolve to an inherited
1240
+ // truthy value and become the profile name (proto shadowing).
1241
+ var postureProfile = (opts.posture && postures &&
1242
+ Object.prototype.hasOwnProperty.call(postures, opts.posture))
1243
+ ? postures[opts.posture] : null;
1244
+ return opts.profile || postureProfile || defaultProfile;
1205
1245
  }
1206
1246
 
1207
1247
  /**
@@ -2072,7 +2112,7 @@ Object.freeze(INPUT_CONTRACTS);
2072
2112
 
2073
2113
  function resolveInputContract(contract) {
2074
2114
  if (typeof contract === "function") return contract;
2075
- return INPUT_CONTRACTS[contract] || INPUT_CONTRACTS.text;
2115
+ return Object.prototype.hasOwnProperty.call(INPUT_CONTRACTS, contract) ? INPUT_CONTRACTS[contract] : INPUT_CONTRACTS.text;
2076
2116
  }
2077
2117
 
2078
2118
  /**
@@ -3125,6 +3165,7 @@ module.exports = {
3125
3165
  charThreatDisposition: charThreatDisposition,
3126
3166
  extractBytesAsText: extractBytesAsText,
3127
3167
  lookupCompliancePosture: lookupCompliancePosture,
3168
+ makePostureAccessor: makePostureAccessor,
3128
3169
  ALL_STRICT_POSTURES: ALL_STRICT_POSTURES,
3129
3170
  CHAR_THREATS_REJECT_ALL: CHAR_THREATS_REJECT_ALL,
3130
3171
  DANGEROUS_URL_SCHEMES: DANGEROUS_URL_SCHEMES,
package/lib/gdpr-ropa.js CHANGED
@@ -91,7 +91,7 @@ function create(opts) {
91
91
  throw new GdprRopaError("gdpr-ropa/bad-id",
92
92
  "gdpr.ropa." + op + ": activity.id must be a non-empty string");
93
93
  }
94
- if (!VALID_LEGAL_BASES[activity.legalBasis]) {
94
+ if (!Object.prototype.hasOwnProperty.call(VALID_LEGAL_BASES, activity.legalBasis)) {
95
95
  throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
96
96
  "gdpr.ropa." + op + ": activity.legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
97
97
  }
@@ -127,7 +127,7 @@ function create(opts) {
127
127
  registeredAt: existing.registeredAt,
128
128
  lastUpdatedAt: now(),
129
129
  });
130
- if (patch.legalBasis && !VALID_LEGAL_BASES[merged.legalBasis]) {
130
+ if (patch.legalBasis && !Object.prototype.hasOwnProperty.call(VALID_LEGAL_BASES, merged.legalBasis)) {
131
131
  throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
132
132
  "gdpr.ropa.update: legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
133
133
  }
@@ -173,6 +173,21 @@ function create(opts) {
173
173
  activities: list(),
174
174
  };
175
175
  }
176
+ // Quote a CSV cell AND neutralize spreadsheet formula injection (CWE-1236):
177
+ // a value beginning with = + - @ TAB or CR is evaluated as a formula by
178
+ // Excel / Google Sheets when the export is opened, so a RoPA field like
179
+ // "=HYPERLINK(...)" or "=cmd|..." would execute. Prefix such a value with a
180
+ // single quote so the spreadsheet renders it as literal text; then RFC-4180
181
+ // quote (double internal quotes).
182
+ function _csvCell(v) {
183
+ var s = (v === undefined || v === null) ? ""
184
+ : (Array.isArray(v) ? JSON.stringify(v) : String(v));
185
+ var c0 = s.charCodeAt(0);
186
+ if (c0 === 0x3d || c0 === 0x2b || c0 === 0x2d || c0 === 0x40 || c0 === 0x09 || c0 === 0x0d) {
187
+ s = "'" + s;
188
+ }
189
+ return '"' + s.replace(/"/g, '""') + '"';
190
+ }
176
191
  function _exportCsv() {
177
192
  var headers = [
178
193
  "id", "name", "purposes", "legalBasis", "dataCategories",
@@ -183,13 +198,7 @@ function create(opts) {
183
198
  var entries = list();
184
199
  for (var i = 0; i < entries.length; i++) {
185
200
  var e = entries[i];
186
- var row = headers.map(function (h) {
187
- var v = e[h];
188
- if (v === undefined || v === null) return "";
189
- if (Array.isArray(v)) return JSON.stringify(v).replace(/"/g, "\"\"");
190
- return String(v).replace(/"/g, "\"\"");
191
- });
192
- rows.push(row.map(function (c) { return '"' + c + '"'; }).join(","));
201
+ rows.push(headers.map(function (h) { return _csvCell(e[h]); }).join(","));
193
202
  }
194
203
  return rows.join("\n");
195
204
  }
@@ -35,7 +35,13 @@ var ROUTER_TOKEN_MIN_LEN = 32;
35
35
  var NONCE_MIN_LEN = 16; // string-length floor for nonce entropy, not bytes
36
36
  var NONCE_MAX_LEN = 256; // string-length cap, not bytes
37
37
  var NONCE_PREVIEW_LEN = 8; // log-preview slice length, not bytes
38
- var SDL_PROBE_RE = /(^|[\s,{])_service\b|_entities\b/;
38
+ // Word-boundary anchored on BOTH probes. A prefix char-class ([\s,{]) on
39
+ // `_service` was bypassable by an alias with no leading space — `query{x:_service{sdl}}`
40
+ // has `:` before `_service`, which is not in the class, so the SDL trust gate
41
+ // was skipped and the subgraph leaked its full SDL. `\b` already prevents
42
+ // matching substrings like `my_service` (no boundary between `y` and `_`), so
43
+ // the prefix class only added the bypass.
44
+ var SDL_PROBE_RE = /\b_service\b|\b_entities\b/;
39
45
 
40
46
  /**
41
47
  * @primitive b.graphqlFederation.queryProbesSdl
@@ -173,14 +179,21 @@ function guardSdl(opts) {
173
179
  return function graphqlFedGuard(req, res, next) {
174
180
  Promise.resolve().then(function () {
175
181
  return _readBody(req, errorClass).then(function (rawBody) {
176
- var query = null;
182
+ var probesSdl = false;
177
183
  try {
178
184
  var parsed = typeof rawBody === "string" ? safeJson.parse(rawBody, { maxBytes: C.BYTES.mib(1) }) : rawBody; // routed via safeJson.parse
179
- query = parsed && typeof parsed === "object" ? parsed.query : null;
185
+ // A GraphQL HTTP batch is a JSON ARRAY of operations; probe EVERY
186
+ // element's query, not just `parsed.query` (which is undefined for an
187
+ // array → the SDL gate was skipped and a batched `_service{sdl}`
188
+ // operation leaked the SDL on batching-enabled subgraphs).
189
+ var candidates = Array.isArray(parsed)
190
+ ? parsed.map(function (o) { return o && typeof o === "object" ? o.query : null; })
191
+ : [parsed && typeof parsed === "object" ? parsed.query : null];
192
+ probesSdl = candidates.some(queryProbesSdl);
180
193
  } catch (_e) { /* not JSON; pass through */ }
181
194
  if (req.body === undefined) req.body = rawBody;
182
195
 
183
- if (!queryProbesSdl(query)) {
196
+ if (!probesSdl) {
184
197
  if (typeof next === "function") next();
185
198
  return;
186
199
  }
package/lib/guard-all.js CHANGED
@@ -128,12 +128,12 @@ function _verifyParity() {
128
128
  failures.push(g.NAME + ": missing gate(opts) function");
129
129
  }
130
130
  SHARED_PROFILES.forEach(function (p) {
131
- if (!g.PROFILES || !g.PROFILES[p]) {
131
+ if (!g.PROFILES || !Object.prototype.hasOwnProperty.call(g.PROFILES, p)) {
132
132
  failures.push(g.NAME + ": does not declare shared profile " + JSON.stringify(p));
133
133
  }
134
134
  });
135
135
  SHARED_POSTURES.forEach(function (p) {
136
- if (!g.COMPLIANCE_POSTURES || !g.COMPLIANCE_POSTURES[p]) {
136
+ if (!g.COMPLIANCE_POSTURES || !Object.prototype.hasOwnProperty.call(g.COMPLIANCE_POSTURES, p)) {
137
137
  failures.push(g.NAME + ": does not declare shared compliance posture " + JSON.stringify(p));
138
138
  }
139
139
  });
package/lib/guard-dsn.js CHANGED
@@ -210,7 +210,7 @@ function parse(deliveryStatusBody, opts) {
210
210
  "parse: per-recipient block missing Action (RFC 3464 §2.3.3)");
211
211
  }
212
212
  var action = fields["action"].toLowerCase();
213
- if (!KNOWN_ACTIONS[action]) {
213
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_ACTIONS, action)) {
214
214
  throw new GuardDsnError("guard-dsn/bad-action",
215
215
  "parse: Action '" + action + "' not in RFC 3464 §2.3.3 vocabulary");
216
216
  }
@@ -148,7 +148,7 @@ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
148
148
  function check(ctx, opts) {
149
149
  opts = opts || {};
150
150
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
151
- if (!PROFILES[profile]) {
151
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
152
152
  throw new GuardEnvelopeError("guard-envelope/bad-profile",
153
153
  "check: unknown profile '" + profile + "'");
154
154
  }