@blamejs/core 0.14.26 → 0.15.0

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 (150) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/agent-envelope-mac.js +104 -0
  5. package/lib/agent-event-bus.js +105 -4
  6. package/lib/agent-posture-chain.js +8 -42
  7. package/lib/ai-content-detect.js +9 -10
  8. package/lib/api-key.js +107 -74
  9. package/lib/atomic-file.js +62 -4
  10. package/lib/audit-chain.js +47 -11
  11. package/lib/audit-sign.js +77 -2
  12. package/lib/audit-tools.js +79 -51
  13. package/lib/audit.js +249 -123
  14. package/lib/auth/openid-federation.js +108 -47
  15. package/lib/backup/index.js +13 -10
  16. package/lib/break-glass.js +202 -144
  17. package/lib/cache.js +174 -105
  18. package/lib/chain-writer.js +38 -16
  19. package/lib/cli.js +19 -14
  20. package/lib/cluster-provider-db.js +130 -104
  21. package/lib/cluster-storage.js +119 -22
  22. package/lib/cluster.js +119 -71
  23. package/lib/compliance.js +169 -4
  24. package/lib/consent.js +73 -24
  25. package/lib/constants.js +16 -11
  26. package/lib/crypto-field.js +474 -92
  27. package/lib/db-declare-row-policy.js +35 -22
  28. package/lib/db-file-lifecycle.js +3 -2
  29. package/lib/db-query.js +497 -255
  30. package/lib/db-schema.js +209 -44
  31. package/lib/db.js +176 -95
  32. package/lib/error-page.js +14 -1
  33. package/lib/external-db-migrate.js +229 -139
  34. package/lib/external-db.js +25 -15
  35. package/lib/file-upload.js +52 -7
  36. package/lib/framework-error.js +14 -1
  37. package/lib/framework-files.js +73 -0
  38. package/lib/framework-schema.js +695 -394
  39. package/lib/gate-contract.js +649 -1
  40. package/lib/guard-agent-registry.js +26 -44
  41. package/lib/guard-all.js +1 -0
  42. package/lib/guard-auth.js +42 -112
  43. package/lib/guard-cidr.js +33 -154
  44. package/lib/guard-csv.js +46 -113
  45. package/lib/guard-domain.js +34 -157
  46. package/lib/guard-dsn.js +27 -43
  47. package/lib/guard-email.js +47 -69
  48. package/lib/guard-envelope.js +19 -32
  49. package/lib/guard-event-bus-payload.js +24 -42
  50. package/lib/guard-event-bus-topic.js +25 -43
  51. package/lib/guard-filename.js +42 -106
  52. package/lib/guard-graphql.js +42 -123
  53. package/lib/guard-html.js +53 -108
  54. package/lib/guard-idempotency-key.js +24 -42
  55. package/lib/guard-image.js +46 -103
  56. package/lib/guard-imap-command.js +18 -32
  57. package/lib/guard-jmap.js +16 -30
  58. package/lib/guard-json.js +38 -108
  59. package/lib/guard-jsonpath.js +38 -171
  60. package/lib/guard-jwt.js +49 -179
  61. package/lib/guard-list-id.js +25 -41
  62. package/lib/guard-list-unsubscribe.js +27 -43
  63. package/lib/guard-mail-compose.js +24 -42
  64. package/lib/guard-mail-move.js +26 -44
  65. package/lib/guard-mail-query.js +28 -46
  66. package/lib/guard-mail-reply.js +24 -42
  67. package/lib/guard-mail-sieve.js +24 -42
  68. package/lib/guard-managesieve-command.js +17 -31
  69. package/lib/guard-markdown.js +37 -104
  70. package/lib/guard-message-id.js +26 -45
  71. package/lib/guard-mime.js +39 -151
  72. package/lib/guard-oauth.js +54 -135
  73. package/lib/guard-pdf.js +45 -101
  74. package/lib/guard-pop3-command.js +21 -31
  75. package/lib/guard-posture-chain.js +24 -42
  76. package/lib/guard-regex.js +33 -107
  77. package/lib/guard-saga-config.js +24 -42
  78. package/lib/guard-shell.js +42 -172
  79. package/lib/guard-smtp-command.js +48 -54
  80. package/lib/guard-snapshot-envelope.js +24 -42
  81. package/lib/guard-sql.js +1491 -0
  82. package/lib/guard-stream-args.js +24 -43
  83. package/lib/guard-svg.js +47 -65
  84. package/lib/guard-template.js +35 -172
  85. package/lib/guard-tenant-id.js +26 -45
  86. package/lib/guard-time.js +32 -154
  87. package/lib/guard-trace-context.js +25 -44
  88. package/lib/guard-uuid.js +32 -153
  89. package/lib/guard-xml.js +38 -113
  90. package/lib/guard-yaml.js +51 -163
  91. package/lib/http-client.js +37 -9
  92. package/lib/inbox.js +120 -107
  93. package/lib/legal-hold.js +107 -50
  94. package/lib/log-stream-cloudwatch.js +47 -31
  95. package/lib/log-stream-otlp.js +32 -18
  96. package/lib/mail-crypto-smime.js +2 -6
  97. package/lib/mail-greylist.js +2 -6
  98. package/lib/mail-helo.js +2 -6
  99. package/lib/mail-journal.js +85 -64
  100. package/lib/mail-rbl.js +2 -6
  101. package/lib/mail-scan.js +2 -6
  102. package/lib/mail-server-jmap.js +117 -12
  103. package/lib/mail-spam-score.js +2 -6
  104. package/lib/mail-store.js +287 -154
  105. package/lib/middleware/body-parser.js +71 -25
  106. package/lib/middleware/csrf-protect.js +19 -8
  107. package/lib/middleware/fetch-metadata.js +17 -7
  108. package/lib/middleware/idempotency-key.js +54 -38
  109. package/lib/middleware/rate-limit.js +102 -32
  110. package/lib/middleware/security-headers.js +21 -5
  111. package/lib/migrations.js +108 -66
  112. package/lib/network-heartbeat.js +7 -0
  113. package/lib/nonce-store.js +31 -9
  114. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  115. package/lib/object-store/azure-blob.js +57 -3
  116. package/lib/object-store/sigv4.js +10 -0
  117. package/lib/observability.js +87 -0
  118. package/lib/otel-export.js +25 -1
  119. package/lib/outbox.js +136 -82
  120. package/lib/parsers/safe-xml.js +47 -7
  121. package/lib/pqc-agent.js +44 -0
  122. package/lib/pubsub-cluster.js +42 -20
  123. package/lib/queue-local.js +202 -139
  124. package/lib/queue-redis.js +9 -1
  125. package/lib/queue-sqs.js +6 -0
  126. package/lib/redact.js +68 -11
  127. package/lib/redis-client.js +160 -31
  128. package/lib/retention.js +82 -39
  129. package/lib/router.js +212 -5
  130. package/lib/safe-dns.js +29 -45
  131. package/lib/safe-ical.js +18 -33
  132. package/lib/safe-icap.js +27 -43
  133. package/lib/safe-sieve.js +21 -40
  134. package/lib/safe-sql.js +124 -3
  135. package/lib/safe-vcard.js +18 -33
  136. package/lib/scheduler.js +35 -12
  137. package/lib/seeders.js +122 -74
  138. package/lib/session-stores.js +42 -14
  139. package/lib/session.js +109 -72
  140. package/lib/sql.js +3885 -0
  141. package/lib/ssrf-guard.js +51 -4
  142. package/lib/static.js +177 -34
  143. package/lib/subject.js +55 -17
  144. package/lib/vault/index.js +3 -2
  145. package/lib/vault/passphrase-ops.js +3 -2
  146. package/lib/vault/rotate.js +104 -64
  147. package/lib/vendor-data.js +2 -0
  148. package/lib/websocket.js +35 -5
  149. package/package.json +1 -1
  150. package/sbom.cdx.json +6 -6
@@ -50,10 +50,16 @@ var bCrypto = require("./crypto");
50
50
  var lazyRequire = require("./lazy-require");
51
51
  var safeAsync = require("./safe-async");
52
52
  var validateOpts = require("./validate-opts");
53
- var { GateContractError } = require("./framework-error");
53
+ var { GateContractError, defineClass } = require("./framework-error");
54
54
 
55
55
  var observability = lazyRequire(function () { return require("./observability"); });
56
56
  var compliance = lazyRequire(function () { return require("./compliance"); });
57
+ var audit = lazyRequire(function () { return require("./audit"); });
58
+
59
+ // One-time dedupe for the "global posture pinned but this guard maps no
60
+ // overlay" warning. Keyed `<posture>::<errCodePrefix>` so each guard
61
+ // family surfaces the gap once instead of on every gate construction.
62
+ var _unmappedPostureWarned = Object.create(null);
57
63
 
58
64
  // Forensic-id token width (bytes); 64 bits is enough for cross-gate
59
65
  // correlation in a single request scope.
@@ -1096,6 +1102,153 @@ function lookupCompliancePosture(name, postures, errorFactory, codePrefix) {
1096
1102
  return Object.assign({}, postures[name]);
1097
1103
  }
1098
1104
 
1105
+ // "GuardCidrError" -> "guardCidr" — the guard's audit/message identity, derived
1106
+ // once from its error class name. Used for the default gate's audit/metric
1107
+ // prefix AND the profile resolver's error message, so neither re-cases the name.
1108
+ function _guardLabelFromError(ErrorClass) {
1109
+ var n = String(ErrorClass.name).replace(/Error$/, "");
1110
+ return n.charAt(0).toLowerCase() + n.slice(1);
1111
+ }
1112
+
1113
+ /**
1114
+ * @primitive b.gateContract.makeProfileResolver
1115
+ * @signature b.gateContract.makeProfileResolver(cfg)
1116
+ * @since 0.15.0
1117
+ * @status stable
1118
+ * @related b.gateContract.makeProfileBuilder, b.gateContract.lookupCompliancePosture
1119
+ *
1120
+ * Closes over a guard's profile config and returns a `resolveProfile(opts)`
1121
+ * function: maps `opts.posture` through the compliance-posture table, else
1122
+ * falls back to `opts.profile || cfg.defaults`, validates the name against
1123
+ * `cfg.profiles`, and throws `cfg.errorClass.factory(cfg.codePrefix +
1124
+ * "/bad-profile")` on an unknown name. The sibling of `makeProfileBuilder` /
1125
+ * `makeRulePackLoader` / `lookupCompliancePosture` for the resolution step —
1126
+ * every `defineParser`-shaped line-protocol / mail / agent guard reuses it
1127
+ * instead of re-declaring an identical `_resolveProfile`.
1128
+ *
1129
+ * @opts
1130
+ * profiles: object, // the guard's PROFILES map; required
1131
+ * postures: object, // COMPLIANCE_POSTURES (posture -> profile name)
1132
+ * defaults: string, // fallback profile name when no posture/profile given
1133
+ * errorClass: function, // the guard's FrameworkError subclass
1134
+ * codePrefix: string, // error-code namespace (e.g. "mail-compose")
1135
+ * byObject: boolean, // true -> return the profile config object, not its name
1136
+ *
1137
+ * @example
1138
+ * var resolveProfile = b.gateContract.makeProfileResolver({
1139
+ * profiles: PROFILES, postures: COMPLIANCE_POSTURES,
1140
+ * defaults: "strict", errorClass: GuardMailComposeError,
1141
+ * codePrefix: "mail-compose",
1142
+ * });
1143
+ * resolveProfile({ posture: "hipaa" }); // → "strict"
1144
+ */
1145
+ function makeProfileResolver(cfg) {
1146
+ var profiles = cfg.profiles;
1147
+ var postures = cfg.postures;
1148
+ var dft = cfg.defaults;
1149
+ var ErrorClass = cfg.errorClass;
1150
+ var codePrefix = cfg.codePrefix;
1151
+ var byObject = cfg.byObject === true;
1152
+ var label = _guardLabelFromError(ErrorClass);
1153
+ return function resolveProfile(opts) {
1154
+ opts = opts || {};
1155
+ if (opts.posture && postures && postures[opts.posture]) {
1156
+ var pn = postures[opts.posture];
1157
+ return byObject ? profiles[pn] : pn;
1158
+ }
1159
+ var p = opts.profile || dft;
1160
+ if (!profiles[p]) {
1161
+ throw ErrorClass.factory(codePrefix + "/bad-profile",
1162
+ label + ": unknown profile '" + p + "' (use " +
1163
+ Object.keys(profiles).join(" / ") + ")");
1164
+ }
1165
+ return byObject ? profiles[p] : p;
1166
+ };
1167
+ }
1168
+
1169
+ /**
1170
+ * @primitive b.gateContract.throwOnRefusalSeverity
1171
+ * @signature b.gateContract.throwOnRefusalSeverity(issues, cfg)
1172
+ * @since 0.15.0
1173
+ * @status stable
1174
+ * @related b.gateContract.aggregateIssues, b.gateContract.makeProfileResolver
1175
+ *
1176
+ * Throw on the first critical/high-severity issue in a detector's issue
1177
+ * list — the refusal step every guard `sanitize` runs after detection
1178
+ * (sanitize can serve a clean value but never repair a critical/high
1179
+ * finding). Builds the guard's error via `cfg.errorClass.factory` with code
1180
+ * `issue.ruleId || (cfg.codePrefix + ".refused")` and message
1181
+ * `guard<Name>.<op>: <issue.snippet>` (op default `"sanitize"`; the guard
1182
+ * identity derives from the error class name). The throw sibling of
1183
+ * `aggregateIssues` (which returns `{ ok, issues }` instead of throwing) —
1184
+ * replaces the per-guard hand-rolled severity-gating loop.
1185
+ *
1186
+ * @opts
1187
+ * errorClass: function, // the guard's FrameworkError subclass; required
1188
+ * codePrefix: string, // error-code namespace; the `.refused` fallback code
1189
+ * op: string, // operation name in the message (default "sanitize")
1190
+ * severities: string[], // refusal severities (default ["critical","high"])
1191
+ *
1192
+ * @example
1193
+ * var issues = detect(input, opts);
1194
+ * b.gateContract.throwOnRefusalSeverity(issues, {
1195
+ * errorClass: GuardCidrError, codePrefix: "cidr",
1196
+ * });
1197
+ * // throws GuardCidrError(ruleId || "cidr.refused", "guardCidr.sanitize: " + snippet)
1198
+ * // on the first critical/high issue
1199
+ */
1200
+ function throwOnRefusalSeverity(issues, cfg) {
1201
+ var errFactory = cfg.errorClass.factory;
1202
+ var prefix = _guardLabelFromError(cfg.errorClass) + "." + (cfg.op || "sanitize");
1203
+ var fallback = cfg.codePrefix + ".refused";
1204
+ // Default refuses critical + high; cfg.severities narrows it (e.g.
1205
+ // ["critical"] for guards that strip high-severity findings but refuse
1206
+ // only unrepairable critical shapes — email / markdown / xml / yaml).
1207
+ var severities = cfg.severities || ["critical", "high"];
1208
+ for (var i = 0; i < issues.length; i += 1) {
1209
+ var iss = issues[i];
1210
+ if (severities.indexOf(iss.severity) !== -1) {
1211
+ throw errFactory(iss.ruleId || fallback, prefix + ": " + iss.snippet);
1212
+ }
1213
+ }
1214
+ }
1215
+
1216
+ /**
1217
+ * @primitive b.gateContract.ALL_STRICT_POSTURES
1218
+ * @signature b.gateContract.ALL_STRICT_POSTURES
1219
+ * @since 0.15.0
1220
+ * @status stable
1221
+ * @compliance hipaa, pci-dss, gdpr, soc2
1222
+ * @related b.gateContract.lookupCompliancePosture, b.gateContract.makeProfileBuilder
1223
+ *
1224
+ * Canonical strict-all `COMPLIANCE_POSTURES` map every command/parser
1225
+ * guard composes. Maps each of the four baseline regulatory postures —
1226
+ * `hipaa` / `pci-dss` / `gdpr` / `soc2` — onto the guard's `strict`
1227
+ * profile name. Guards whose four postures all resolve to `strict`
1228
+ * (the command/protocol validators: POP3 / IMAP / SMTP / ManageSieve
1229
+ * commands, mail-compose / query / sieve / move / reply, the envelope
1230
+ * and event-bus shapes, the mail pipeline scorers, and the
1231
+ * `safe-*` line-protocol parsers) reference this single frozen object
1232
+ * instead of re-declaring it. Guards that overlay per-posture
1233
+ * byte-limits or redaction flags (the content guards: CSV / HTML /
1234
+ * JSON / XML / YAML / JWT / OAuth / template, etc.) keep their own
1235
+ * posture map and do not compose this.
1236
+ *
1237
+ * Frozen once and shared by reference: every consumer reads it through
1238
+ * its own `COMPLIANCE_POSTURES` binding and never mutates it.
1239
+ *
1240
+ * @example
1241
+ * var COMPLIANCE_POSTURES = b.gateContract.ALL_STRICT_POSTURES;
1242
+ * COMPLIANCE_POSTURES.hipaa; // → "strict"
1243
+ * Object.isFrozen(COMPLIANCE_POSTURES); // → true
1244
+ */
1245
+ var ALL_STRICT_POSTURES = Object.freeze({
1246
+ hipaa: "strict",
1247
+ "pci-dss": "strict",
1248
+ gdpr: "strict",
1249
+ soc2: "strict",
1250
+ });
1251
+
1099
1252
  /**
1100
1253
  * @primitive b.gateContract.makeRulePackLoader
1101
1254
  * @signature b.gateContract.makeRulePackLoader(errorClass, codePrefix)
@@ -1415,6 +1568,16 @@ function resolveProfileAndPosture(opts, cfg) {
1415
1568
  if (typeof globalPosture === "string" &&
1416
1569
  cfg.compliancePostures && cfg.compliancePostures[globalPosture]) {
1417
1570
  posture = globalPosture;
1571
+ } else if (typeof globalPosture === "string" && globalPosture.length > 0) {
1572
+ // A global posture IS pinned, but this guard family ships no
1573
+ // COMPLIANCE_POSTURES overlay for it (e.g. fedramp-rev5-moderate
1574
+ // against a guard whose table only covers hipaa/pci-dss/gdpr/soc2).
1575
+ // Falling through to the unposture-d default is the SAFE behavior,
1576
+ // but operators must know the posture is a no-op for THIS guard —
1577
+ // silently no-oping reads as "enforced" (compliance theater).
1578
+ // Emit a one-time, grep-able audit warning per (posture, guard)
1579
+ // and keep the safe default.
1580
+ _warnUnmappedPosture(globalPosture, prefix);
1418
1581
  }
1419
1582
  }
1420
1583
  if (typeof posture === "string") {
@@ -1427,6 +1590,42 @@ function resolveProfileAndPosture(opts, cfg) {
1427
1590
  return Object.assign({}, cfg.defaults || {}, overlay, opts);
1428
1591
  }
1429
1592
 
1593
+ // _warnUnmappedPosture — emit a one-time, grep-able audit warning that a
1594
+ // globally-pinned posture has no overlay in THIS guard family's
1595
+ // COMPLIANCE_POSTURES table, so the operator doesn't read the
1596
+ // safe-default fall-through as "the posture is enforced here." Drop-
1597
+ // silent (hot-path observability sink): a warning emit must never throw
1598
+ // past the guard-gate construction that triggered it.
1599
+ function _warnUnmappedPosture(posture, prefix) {
1600
+ var dedupeKey = posture + "::" + (prefix || "guard");
1601
+ if (_unmappedPostureWarned[dedupeKey]) return;
1602
+ _unmappedPostureWarned[dedupeKey] = true;
1603
+ try {
1604
+ // Canonical audit outcome triple is success/failure/denied; a
1605
+ // posture that maps no overlay is an advisory NOTICE, not a failure
1606
+ // of this construction — the severity rides in metadata.severity so
1607
+ // the audit row carries the warning intent without abusing outcome.
1608
+ audit().safeEmit({
1609
+ action: "gateContract.posture.unmapped",
1610
+ outcome: "success",
1611
+ metadata: {
1612
+ severity: "warning",
1613
+ posture: posture,
1614
+ guard: prefix || "guard",
1615
+ recommendation: "The pinned compliance posture '" + posture +
1616
+ "' has no overlay in this guard's COMPLIANCE_POSTURES table, so " +
1617
+ "its gate runs the unposture-d default. Pass an explicit " +
1618
+ "compliancePosture this guard maps, or add the overlay, if the " +
1619
+ "posture is meant to tighten this surface.",
1620
+ },
1621
+ });
1622
+ } catch (_e) { /* drop-silent — warning must not break gate construction */ }
1623
+ }
1624
+
1625
+ function _resetForTest() {
1626
+ for (var k in _unmappedPostureWarned) delete _unmappedPostureWarned[k];
1627
+ }
1628
+
1430
1629
  /**
1431
1630
  * @primitive b.gateContract.buildProfile
1432
1631
  * @signature b.gateContract.buildProfile(opts)
@@ -1628,8 +1827,453 @@ function composeHooks(hooks) {
1628
1827
  };
1629
1828
  }
1630
1829
 
1830
+ // ---- Guard-module factories ----
1831
+ //
1832
+ // Every b.guard* primitive of the gate-bearing kinds (content / filename
1833
+ // / identifier) hand-wires the SAME export surface: an error class, a
1834
+ // resolveProfileAndPosture-backed _resolveOpts, a buildGuardGate-backed
1835
+ // gate, a makeProfileBuilder-backed buildProfile, a
1836
+ // lookupCompliancePosture-backed compliancePosture, a makeRulePackLoader-
1837
+ // backed loadRulePack, and a frozen module.exports carrying the
1838
+ // guard-* registry fields (NAME / KIND / MIME_TYPES / EXTENSIONS /
1839
+ // PROFILES / DEFAULTS / COMPLIANCE_POSTURES / INTEGRATION_FIXTURES) plus
1840
+ // the per-guard inspection surface (validate / sanitize / gate). They
1841
+ // differ only in the per-guard inspection LOGIC + the PROFILES /
1842
+ // COMPLIANCE_POSTURES / DEFAULTS tables. `defineGuard` assembles the
1843
+ // boilerplate; the spec injects the logic and the tables.
1844
+ //
1845
+ // `defineParser` is the sibling for the minimal command / line-protocol
1846
+ // / safe-* parser shape — the guards whose four postures all resolve to
1847
+ // `strict` (ALL_STRICT_POSTURES) and whose surface is a self-contained
1848
+ // `validate` / `parse` plus a `compliancePosture(name)` that returns the
1849
+ // effective PROFILE NAME (or null) rather than an overlay clone. Those
1850
+ // guards carry no gate / buildProfile / loadRulePack, so forcing them
1851
+ // through `defineGuard` would be a leaky abstraction.
1852
+
1853
+ // _KIND_CTX_FIELDS — per-KIND ordered list of ctx field names a
1854
+ // buildGuardGate-backed default gate reads, mirroring the hand-written
1855
+ // gate bodies: filename reads ctx.filename || ctx.name, identifier reads
1856
+ // ctx.identifier || ctx.token || ctx.jwt, command reads ctx.line ||
1857
+ // ctx.command. content has no entry — it falls through to
1858
+ // extractBytesAsText (the ctx.bytes string/Buffer normalizer).
1859
+ var _KIND_CTX_FIELDS = Object.freeze({
1860
+ filename: ["filename", "name"],
1861
+ identifier: ["identifier", "token", "jwt"],
1862
+ command: ["line", "command"],
1863
+ });
1864
+
1865
+ // override (when given) replaces the per-KIND field table — lets a guard whose
1866
+ // gate is the standard chain but reads a custom ctx field take the default gate.
1867
+ function _ctxValueForKind(kind, ctx, override) {
1868
+ ctx = ctx || {};
1869
+ var fields = override || _KIND_CTX_FIELDS[kind];
1870
+ if (!fields) return extractBytesAsText(ctx); // content (default)
1871
+ for (var i = 0; i < fields.length; i += 1) {
1872
+ if (ctx[fields[i]]) return ctx[fields[i]];
1873
+ }
1874
+ return "";
1875
+ }
1876
+
1877
+ /**
1878
+ * @primitive b.gateContract.defineGuard
1879
+ * @signature b.gateContract.defineGuard(spec)
1880
+ * @since 0.15.0
1881
+ * @status stable
1882
+ * @related b.gateContract.defineParser, b.gateContract.buildGuardGate, b.gateContract.resolveProfileAndPosture
1883
+ *
1884
+ * Assemble a complete `b.guard*` module from a spec. Mints the per-guard
1885
+ * error class (via `framework-error.defineClass`, or accepts a supplied
1886
+ * `errorClass`), wires `resolveProfileAndPosture` / `buildGuardGate` /
1887
+ * `makeProfileBuilder` / `lookupCompliancePosture` / `makeRulePackLoader`,
1888
+ * and returns the frozen module.exports object every guard ships —
1889
+ * `NAME` / `KIND` / `PROFILES` / `DEFAULTS` / `COMPLIANCE_POSTURES` /
1890
+ * `INTEGRATION_FIXTURES` / `validate` / `sanitize?` / `gate?` /
1891
+ * `buildProfile` / `compliancePosture` / `loadRulePack` plus the spec's
1892
+ * `extra` exports (verb tables, `escapeCell`, `schema`, `kidSafe`, …) and
1893
+ * the error class under its own name.
1894
+ *
1895
+ * The per-guard inspection logic is INJECTED, not abstracted: `validate`
1896
+ * / `sanitize` / `gate` are spec functions that close over the resolved
1897
+ * opts. A guard whose `gate` body is the standard
1898
+ * serve→audit-only→sanitize→refuse chain can omit `spec.gate` and take
1899
+ * the factory default (built from `spec.validate` + `spec.sanitize` per
1900
+ * KIND); a guard with a bespoke gate (CSV's sanitize-reparse-reserialize,
1901
+ * filename's per-policy canSanitize matrix) passes its own. Behavior is
1902
+ * preserved byte-for-byte because the genuinely-divergent code stays
1903
+ * verbatim in the spec — the factory only removes the wiring every guard
1904
+ * copies.
1905
+ *
1906
+ * @opts
1907
+ * name: string, // NAME (e.g. "csv"); required
1908
+ * kind: string, // "content"|"filename"|"identifier"|"command" for the default gate; any non-empty label with a bespoke spec.gate; required
1909
+ * errCodePrefix: string, // error-code namespace (default name)
1910
+ * errorName: string, // defineClass name (mutually exclusive with errorClass)
1911
+ * errorClass: function, // pre-built FrameworkError subclass
1912
+ * profiles: object, // PROFILES (must include strict/balanced/permissive); required
1913
+ * defaults: object, // DEFAULTS baseline (default profiles.strict)
1914
+ * postures: object, // COMPLIANCE_POSTURES (default ALL_STRICT_POSTURES)
1915
+ * mimeTypes: string[], // content guards only
1916
+ * extensions: string[], // content guards only
1917
+ * integrationFixtures: object, // INTEGRATION_FIXTURES (consumed by host harness)
1918
+ * validate: function, // (input, resolvedOpts) -> { ok, issues }; required
1919
+ * sanitize: function, // (input, resolvedOpts) -> cleaned (optional)
1920
+ * gate: function, // (resolvedOpts) -> async (ctx) -> decision (optional; default built per kind)
1921
+ * ctxFields: string[], // ordered ctx field names the default gate reads (overrides the per-KIND table; e.g. ["identifier","cidr"])
1922
+ * defaultGateCheck: function, // override the default gate's per-ctx check
1923
+ * extra: object, // additional exports merged verbatim into module.exports
1924
+ *
1925
+ * @example
1926
+ * module.exports = b.gateContract.defineGuard({
1927
+ * name: "csv", kind: "content", errorClass: GuardCsvError,
1928
+ * profiles: PROFILES, defaults: DEFAULTS, postures: COMPLIANCE_POSTURES,
1929
+ * mimeTypes: ["text/csv"], extensions: [".csv"],
1930
+ * integrationFixtures: INTEGRATION_FIXTURES,
1931
+ * validate: validate, sanitize: sanitize, gate: gate,
1932
+ * extra: { serialize: serialize, escapeCell: escapeCell, schema: schema },
1933
+ * });
1934
+ */
1935
+ function defineGuard(spec) {
1936
+ validateOpts.requireObject(spec, "gateContract.defineGuard", GateContractError);
1937
+ validateOpts.requireNonEmptyString(spec.name, "gateContract.defineGuard: name",
1938
+ GateContractError, "gate-contract/bad-opt");
1939
+ validateOpts.requireNonEmptyString(spec.kind, "gateContract.defineGuard: kind",
1940
+ GateContractError, "gate-contract/bad-opt");
1941
+ // The four known kinds drive the default gate's ctx-field dispatch
1942
+ // (_ctxValueForKind). A guard with a bespoke spec.gate reads its own ctx
1943
+ // fields, so any non-empty kind is allowed there — the kind is then just
1944
+ // the KIND export label (e.g. "oauth-flow" / "graphql-request" / "sql" /
1945
+ // "metadata"). A custom kind WITHOUT a bespoke gate is refused, because
1946
+ // the default gate could not dispatch it to the right ctx field.
1947
+ if (["content", "filename", "identifier", "command"].indexOf(spec.kind) === -1 &&
1948
+ typeof spec.gate !== "function") {
1949
+ throw _err("gate-contract/bad-opt",
1950
+ "defineGuard: kind must be content|filename|identifier|command for the " +
1951
+ "default gate, got " + JSON.stringify(spec.kind) +
1952
+ " — pass spec.gate for a custom kind (the bespoke gate reads its own ctx fields)");
1953
+ }
1954
+ validateOpts.requireObject(spec.profiles, "gateContract.defineGuard: profiles",
1955
+ GateContractError);
1956
+ if (typeof spec.validate !== "function") {
1957
+ throw _err("gate-contract/bad-opt", "defineGuard: validate must be a function");
1958
+ }
1959
+ if (spec.errorClass && spec.errorName) {
1960
+ throw _err("gate-contract/bad-opt",
1961
+ "defineGuard: pass errorClass OR errorName, not both");
1962
+ }
1963
+
1964
+ var prefix = spec.errCodePrefix || spec.name;
1965
+ var ErrorClass = spec.errorClass ||
1966
+ defineClass(spec.errorName || ("Guard" +
1967
+ spec.name.charAt(0).toUpperCase() + spec.name.slice(1) + "Error"),
1968
+ { alwaysPermanent: true });
1969
+ var profiles = spec.profiles;
1970
+ var defaults = spec.defaults || profiles.strict || {};
1971
+ var postures = spec.postures || ALL_STRICT_POSTURES;
1972
+
1973
+ var buildProfileFn = makeProfileBuilder(profiles);
1974
+ function compliancePostureFn(name) {
1975
+ return lookupCompliancePosture(name, postures, ErrorClass.factory, prefix);
1976
+ }
1977
+ var rulePacks = makeRulePackLoader(ErrorClass, prefix);
1978
+
1979
+ // spec.ctxFields (ordered field names) overrides the per-KIND table that
1980
+ // the default gate's _ctxValueForKind reads — lets a guard whose gate is the
1981
+ // standard chain but reads a custom ctx field (e.g. ctx.cidr) drop its
1982
+ // bespoke gate and take the default. null -> _ctxValueForKind uses the
1983
+ // per-KIND table.
1984
+ var ctxFields = Array.isArray(spec.ctxFields) ? spec.ctxFields.slice() : null;
1985
+ // Gate identity is surfaced in audit events / metric counters / cache keys.
1986
+ // Preserve the "guard<Name>:profile" naming the hand-written gates used so
1987
+ // moving a guard onto the default gate does not rename its audit/metric
1988
+ // stream (e.g. "guardCidr:strict"), via the shared error-name derivation.
1989
+ var gateNamePrefix = _guardLabelFromError(ErrorClass);
1990
+
1991
+ // Default gate — the standard serve→audit-only→refuse chain, dispatched
1992
+ // to the right ctx field by KIND (or spec.ctxFields). Guards with a bespoke
1993
+ // gate pass spec.gate; guards whose gate is the standard chain take this
1994
+ // default.
1995
+ // Raw opts pass straight through to spec.validate and buildGuardGate —
1996
+ // matching the hand-written gates, whose validate resolves profile/posture
1997
+ // internally (validate calls its own _resolveOpts). Pre-resolving here
1998
+ // would double-resolve.
1999
+ function defaultGate(opts) {
2000
+ opts = opts || {};
2001
+ var perCtx = spec.defaultGateCheck || function (ctx) {
2002
+ var value = _ctxValueForKind(spec.kind, ctx, ctxFields);
2003
+ if (!value) return { ok: true, action: "serve" };
2004
+ var rv = spec.validate(value, opts);
2005
+ if (!rv.issues || rv.issues.length === 0) return { ok: true, action: "serve" };
2006
+ var hasBlocking = rv.issues.some(function (i) {
2007
+ return i.severity === "critical" || i.severity === "high";
2008
+ });
2009
+ if (!hasBlocking) return { ok: true, action: "audit-only", issues: rv.issues };
2010
+ return { ok: false, action: "refuse", issues: rv.issues };
2011
+ };
2012
+ return buildGuardGate(
2013
+ opts.name || (gateNamePrefix + ":" + (opts.profile || "default")),
2014
+ opts,
2015
+ async function (ctx) { return perCtx(ctx, opts); });
2016
+ }
2017
+
2018
+ var gateFn = spec.gate || defaultGate;
2019
+
2020
+ var out = {
2021
+ NAME: spec.name,
2022
+ KIND: spec.kind,
2023
+ validate: spec.validate,
2024
+ buildProfile: buildProfileFn,
2025
+ compliancePosture: compliancePostureFn,
2026
+ loadRulePack: rulePacks.load,
2027
+ PROFILES: profiles,
2028
+ DEFAULTS: defaults,
2029
+ COMPLIANCE_POSTURES: postures,
2030
+ };
2031
+ if (spec.kind === "content") {
2032
+ out.MIME_TYPES = Object.freeze((spec.mimeTypes || []).slice());
2033
+ out.EXTENSIONS = Object.freeze((spec.extensions || []).slice());
2034
+ }
2035
+ if (spec.integrationFixtures) out.INTEGRATION_FIXTURES = spec.integrationFixtures;
2036
+ if (typeof spec.sanitize === "function") out.sanitize = spec.sanitize;
2037
+ out.gate = gateFn;
2038
+ // Error class exported under its own constructor name (GuardCsvError etc.)
2039
+ out[ErrorClass.name] = ErrorClass;
2040
+ // Per-guard extras (verb tables, escapeCell, schema, kidSafe, …) merged
2041
+ // verbatim via the prototype-safe own-enumerable copy (no computed-name
2042
+ // write; __proto__/constructor/prototype are skipped). Extras win over
2043
+ // factory defaults only when the guard explicitly re-exports a shared
2044
+ // name (rare; documented per guard).
2045
+ if (spec.extra) validateOpts.assignOwnEnumerable(out, spec.extra);
2046
+ return out;
2047
+ }
2048
+
2049
+ /**
2050
+ * @primitive b.gateContract.defineParser
2051
+ * @signature b.gateContract.defineParser(spec)
2052
+ * @since 0.15.0
2053
+ * @status stable
2054
+ * @related b.gateContract.defineGuard, b.gateContract.ALL_STRICT_POSTURES
2055
+ *
2056
+ * Assemble the minimal command / line-protocol / `safe-*` parser module
2057
+ * shape — guards whose four compliance postures all resolve to `strict`
2058
+ * (composing `ALL_STRICT_POSTURES`) and whose surface is a single
2059
+ * self-contained `validate` / `parse` entry point plus a
2060
+ * `compliancePosture(name)` that returns the effective PROFILE NAME (or
2061
+ * `null` for unknown names) rather than an overlay clone. These guards
2062
+ * carry no `gate` / `buildProfile` / `loadRulePack`, so `defineGuard`'s
2063
+ * full assembly would be wrong for them.
2064
+ *
2065
+ * Mints the error class (or accepts one), exposes the spec's primary
2066
+ * entry point under `spec.entryName` (default `"validate"`), and returns
2067
+ * the frozen module.exports with `PROFILES` / `COMPLIANCE_POSTURES` /
2068
+ * `compliancePosture` plus the spec's `extra` exports and the error
2069
+ * class.
2070
+ *
2071
+ * @opts
2072
+ * name: string, // module identity / error-name stem; required
2073
+ * entry: function, // the validate/parse entry point; required
2074
+ * entryName: string, // export key for the entry (default "validate")
2075
+ * profiles: object, // PROFILES; required
2076
+ * postures: object, // COMPLIANCE_POSTURES (default ALL_STRICT_POSTURES)
2077
+ * errorClass: function, // pre-built FrameworkError subclass
2078
+ * errorName: string, // defineClass name (mutually exclusive with errorClass)
2079
+ * extra: object, // additional exports (verb tables, KNOWN_*, …)
2080
+ *
2081
+ * @example
2082
+ * module.exports = b.gateContract.defineParser({
2083
+ * name: "pop3-command", entry: validate,
2084
+ * errorClass: GuardPop3CommandError,
2085
+ * profiles: PROFILES, postures: COMPLIANCE_POSTURES,
2086
+ * extra: { KNOWN_VERBS: KNOWN_VERBS, ZERO_ARG_VERBS: ZERO_ARG_VERBS },
2087
+ * });
2088
+ */
2089
+ function defineParser(spec) {
2090
+ validateOpts.requireObject(spec, "gateContract.defineParser", GateContractError);
2091
+ validateOpts.requireNonEmptyString(spec.name, "gateContract.defineParser: name",
2092
+ GateContractError, "gate-contract/bad-opt");
2093
+ if (typeof spec.entry !== "function") {
2094
+ throw _err("gate-contract/bad-opt", "defineParser: entry must be a function");
2095
+ }
2096
+ validateOpts.requireObject(spec.profiles, "gateContract.defineParser: profiles",
2097
+ GateContractError);
2098
+ if (spec.errorClass && spec.errorName) {
2099
+ throw _err("gate-contract/bad-opt",
2100
+ "defineParser: pass errorClass OR errorName, not both");
2101
+ }
2102
+ var ErrorClass = spec.errorClass ||
2103
+ defineClass(spec.errorName || ("Guard" +
2104
+ spec.name.charAt(0).toUpperCase() + spec.name.slice(1) + "Error"),
2105
+ { alwaysPermanent: true });
2106
+ var postures = spec.postures || ALL_STRICT_POSTURES;
2107
+
2108
+ function compliancePostureFn(name) {
2109
+ return postures[name] || null;
2110
+ }
2111
+
2112
+ var out = {
2113
+ compliancePosture: compliancePostureFn,
2114
+ PROFILES: spec.profiles,
2115
+ COMPLIANCE_POSTURES: postures,
2116
+ };
2117
+ out[spec.entryName || "validate"] = spec.entry;
2118
+ out[ErrorClass.name] = ErrorClass;
2119
+ if (spec.extra) validateOpts.assignOwnEnumerable(out, spec.extra);
2120
+ return out;
2121
+ }
2122
+
2123
+ // ---- ABI doc templates (single-sourced; rendered per guard) ----
2124
+ //
2125
+ // Every guard built through `defineGuard` / `defineParser` exposes the
2126
+ // SAME factory-generated ABI methods (`compliancePosture` and, for
2127
+ // `defineGuard`, `buildProfile` / `loadRulePack` / a default `gate`).
2128
+ // Those methods have no per-guard `function` declaration — the factory
2129
+ // wires them — so a refactored guard that wants its wiki page to keep
2130
+ // listing them used to carry a floating `@primitive` block per method,
2131
+ // duplicating the same prose across every member of the family.
2132
+ //
2133
+ // The `@abiTemplate` blocks below are the ONE copy of that prose. The
2134
+ // wiki parser (`examples/wiki/lib/source-doc-parser.js`) collects them
2135
+ // into a per-factory template bucket (keyed `defineGuard` / `defineParser`)
2136
+ // instead of the gateContract primitive list, and the page generator
2137
+ // (`examples/wiki/lib/page-generator.js`) instantiates them per guard —
2138
+ // substituting `{NS}` (the guard namespace, e.g. `guardCsv`) and `{ERR}`
2139
+ // (its error class, e.g. `GuardCsvError`) and filling `@since` from the
2140
+ // guard's own `@module` / first-primitive metadata — so each guard's page
2141
+ // renders every ABI method with usage correct for THAT guard. The
2142
+ // duplicated prose collapses to a single source; the rendered surface is
2143
+ // unchanged. A guard that keeps a bespoke per-method block (a custom
2144
+ // `gate`, or a guard that documents its own `compliancePosture`) wins —
2145
+ // the page generator skips the template for any method already present.
2146
+ //
2147
+ // These blocks intentionally carry the placeholder primitive form
2148
+ // `b.{NS}.<method>` and placeholder-bearing `@example` bodies; the
2149
+ // validator routes them through its template-shape pass, not the
2150
+ // resolvable-primitive pass.
2151
+
2152
+ /**
2153
+ * @abiTemplate defineGuard
2154
+ * @method compliancePosture
2155
+ * @signature b.{NS}.compliancePosture(name)
2156
+ * @status stable
2157
+ * @compliance hipaa, pci-dss, gdpr, soc2
2158
+ * @related b.{NS}.gate, b.{NS}.buildProfile
2159
+ *
2160
+ * Look up a compliance-posture overlay by name (one of `"hipaa"` /
2161
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a fresh clone of the
2162
+ * posture overlay so the caller may mutate it freely without disturbing
2163
+ * the shared table. Throws `{ERR}` with code `"{CODE}.bad-posture"` when
2164
+ * the name is not one this guard maps. Wired by `gateContract.defineGuard`
2165
+ * through `gateContract.lookupCompliancePosture`, so the clone semantics
2166
+ * and error code are identical across every guard in the family.
2167
+ *
2168
+ * @example
2169
+ * var posture = b.{NS}.compliancePosture("hipaa");
2170
+ * posture; // → overlay clone (mutable)
2171
+ *
2172
+ * try {
2173
+ * b.{NS}.compliancePosture("not-a-regime");
2174
+ * } catch (e) {
2175
+ * e.code; // → "{CODE}.bad-posture"
2176
+ * }
2177
+ */
2178
+
2179
+ /**
2180
+ * @abiTemplate defineGuard
2181
+ * @method buildProfile
2182
+ * @signature b.{NS}.buildProfile(opts)
2183
+ * @status stable
2184
+ * @related b.{NS}.gate, b.{NS}.compliancePosture
2185
+ *
2186
+ * Compose a derived profile from one or more named bases plus inline
2187
+ * overrides, resolving names through this guard's own `PROFILES` table.
2188
+ * `opts.extends` is a base profile name (`"strict"` / `"balanced"` /
2189
+ * `"permissive"`) or an array of names — later entries shadow earlier
2190
+ * ones, and inline `opts` keys win last. Wired by
2191
+ * `gateContract.defineGuard` through `gateContract.makeProfileBuilder`,
2192
+ * so operator-defined profiles stay traceable to a baseline instead of a
2193
+ * hand-typed dictionary.
2194
+ *
2195
+ * @opts
2196
+ * extends: string|string[], // base profile name(s) to compose
2197
+ * ...: any guard key, // inline override of resolved keys
2198
+ *
2199
+ * @example
2200
+ * var custom = b.{NS}.buildProfile({ extends: "strict" });
2201
+ * custom; // → composed profile object
2202
+ */
2203
+
2204
+ /**
2205
+ * @abiTemplate defineGuard
2206
+ * @method loadRulePack
2207
+ * @signature b.{NS}.loadRulePack(pack)
2208
+ * @status stable
2209
+ * @related b.{NS}.gate
2210
+ *
2211
+ * Register an operator-supplied rule pack with this guard's rule-pack
2212
+ * registry. The pack is identified by `pack.id` (a non-empty string) and
2213
+ * stored for later dispatch by gates that opt in via `opts.rulePackId`.
2214
+ * Returns the pack unchanged on success; throws `{ERR}` with code
2215
+ * `"{CODE}.bad-opt"` when `pack` is missing or `pack.id` is not a non-empty
2216
+ * string. Wired by `gateContract.defineGuard` through
2217
+ * `gateContract.makeRulePackLoader`, so storage shape and validation are
2218
+ * identical across the family.
2219
+ *
2220
+ * @example
2221
+ * var pack = b.{NS}.loadRulePack({ id: "tenant-policy", rules: [] });
2222
+ * pack.id; // → "tenant-policy"
2223
+ */
2224
+
2225
+ /**
2226
+ * @abiTemplate defineGuard
2227
+ * @method gate
2228
+ * @signature b.{NS}.gate(opts?)
2229
+ * @status stable
2230
+ * @related b.{NS}.validate, b.gateContract.buildGuardGate
2231
+ *
2232
+ * Build the guard's request-boundary gate — a contract-shaped object
2233
+ * exposing `check(ctx)` that host primitives call at their byte moment.
2234
+ * This is the factory default chain: `serve` when no issue, `audit-only`
2235
+ * for `info` / `warn` issues, and `refuse` for any `high` / `critical`
2236
+ * issue, dispatched to the right `ctx` field by the guard's KIND. Wired
2237
+ * by `gateContract.defineGuard` through `gateContract.buildGuardGate`; a
2238
+ * guard whose gate diverges (a bespoke sanitize-and-reserialize chain,
2239
+ * for example) ships its own `gate` block instead of this template.
2240
+ *
2241
+ * @opts
2242
+ * profile: string, // one of PROFILES; default this guard's default
2243
+ * compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
2244
+ * mode: string, // one of gateContract MODES; default "enforce"
2245
+ *
2246
+ * @example
2247
+ * var gate = b.{NS}.gate({ profile: "strict" });
2248
+ * var decision = await gate.check({ bytes: Buffer.from("...") });
2249
+ * decision.action; // → "serve" | "refuse" | …
2250
+ */
2251
+
2252
+ /**
2253
+ * @abiTemplate defineParser
2254
+ * @method compliancePosture
2255
+ * @signature b.{NS}.compliancePosture(name)
2256
+ * @status stable
2257
+ * @compliance hipaa, pci-dss, gdpr, soc2
2258
+ * @related b.{NS}.validate, b.gateContract.ALL_STRICT_POSTURES
2259
+ *
2260
+ * Return the effective profile NAME for a compliance posture, or `null`
2261
+ * for a name this parser does not map. Unlike the content-guard variant
2262
+ * this returns the resolved profile string (every line-protocol parser
2263
+ * composes `gateContract.ALL_STRICT_POSTURES`, so `"hipaa"` / `"pci-dss"`
2264
+ * / `"gdpr"` / `"soc2"` all resolve to `"strict"`) and never throws —
2265
+ * the parser shape carries no overlay-clone, no `buildProfile`, and no
2266
+ * `loadRulePack`. Wired by `gateContract.defineParser`.
2267
+ *
2268
+ * @example
2269
+ * b.{NS}.compliancePosture("hipaa"); // → "strict"
2270
+ * b.{NS}.compliancePosture("not-a-regime"); // → null
2271
+ */
2272
+
1631
2273
  module.exports = {
1632
2274
  defineGate: defineGate,
2275
+ defineGuard: defineGuard,
2276
+ defineParser: defineParser,
1633
2277
  validateGateShape: validateGateShape,
1634
2278
  runGate: runGate,
1635
2279
  composeGates: composeGates,
@@ -1648,8 +2292,11 @@ module.exports = {
1648
2292
  buildGuardGate: buildGuardGate,
1649
2293
  extractBytesAsText: extractBytesAsText,
1650
2294
  lookupCompliancePosture: lookupCompliancePosture,
2295
+ ALL_STRICT_POSTURES: ALL_STRICT_POSTURES,
1651
2296
  makeRulePackLoader: makeRulePackLoader,
1652
2297
  makeProfileBuilder: makeProfileBuilder,
2298
+ makeProfileResolver: makeProfileResolver,
2299
+ throwOnRefusalSeverity: throwOnRefusalSeverity,
1653
2300
  badInputResultIfNotStringOrBuffer: badInputResultIfNotStringOrBuffer,
1654
2301
  aggregateIssues: aggregateIssues,
1655
2302
  composeHooks: composeHooks,
@@ -1658,4 +2305,5 @@ module.exports = {
1658
2305
  MODES: MODES,
1659
2306
  ISSUE_SEVERITIES: ISSUE_SEVERITIES,
1660
2307
  GateContractError: GateContractError,
2308
+ _resetForTest: _resetForTest,
1661
2309
  };