@blamejs/core 0.18.46 → 0.18.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,20 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.18.x
10
10
 
11
+ - v0.18.47 (2026-08-22) — **A misspelled character policy was accepted at boot and quietly meant something else.** `bidiPolicy: "rejct"` was accepted by every door the framework offers — the guard's own option resolver and the gate built from it — and then read leniently at runtime: the scan runs because the value is not `allow`, an issue is raised, and the disposition falls through to refuse. Nothing is let through, so it was never a hole; the operator simply asked for one disposition, got another, and was told nothing at the moment a boot error would have cost them a restart. Ninety-three settings across the guard family behaved that way. **Fixed:** *A character policy refuses a value outside its vocabulary* — The five character-threat policies every guard shares — `bidiPolicy`, `nullBytePolicy`, `controlPolicy`, `zeroWidthPolicy`, `tagsPolicy` — now accept only `allow`, `audit`, `audit-only`, `reject`, and `strip` where the guard can carry a repair out. Anything else is refused where options are resolved, which is boot, not the first request that trips the scan. Every entry point takes the check: the generated resolver, and the `gate()` and `validate()` of the guards that bind their own.
12
+
13
+ The check is derived from each guard's own declared defaults rather than hand-listed per guard, for the same reason the size caps beside it are. Hand-listing is what produced the gap: `b.guardCountry` added this for its own three options after `{ reservedPolicy: "rejcet" }` served a reserved country code and reported success, and the other two hundred and fifty-nine policy options across the family never followed. A guard that declares its own vocabulary explicitly still wins, so that one is unchanged.
14
+
15
+ Whether a guard can perform a repair is now DECLARED rather than inferred from whether it exports a `sanitize`. An identifier guard exports one that validates and throws — a UUID with a zero-width character in it is not a UUID, and there is nothing to repair — so inferring the capability let `b.guardRegex`, `b.guardJwt`, `b.guardShell` and `b.guardTemplate` accept `strip` at construction and then refuse at runtime, which is the same silent substitution. Guards that repair say so, and the claim is held from both ends: `b.gateContract.defineGuard` refuses a spec that declares repair without a `sanitize` — or a `sanitizeTransform` for one to be built from — and the family sweep separately checks that a guard claiming it actually removes the character, while one that does not claim it refuses the instruction.
16
+
17
+ What this does not yet cover: the guard-specific policies — formula injection, homoglyphs, DOCTYPE, aliases, and the rest — still accept anything. Their vocabularies genuinely differ from one another, and the set a profile happens to select is not the set the code accepts, so inferring them would refuse values that work today. Each needs its own list, read from the guard rather than guessed. · *`audit-only` did not mean what `audit` means* — The two have been synonyms wherever a policy is turned into a disposition. The severity calculation recognised only the literal `audit`, so on the public validation path they disagreed: `zeroWidthPolicy: "audit"` returned a pass with a warning, while `audit-only` returned a failure with a high-severity issue. The same split affected the control-character and Unicode Tags policies.
18
+
19
+ One predicate decides it now, in the module that owns the character scanning, and the disposition mapper asks the same one. Two spellings of a single setting is how they come to disagree; asking one question is how they stop. · *The DNSSEC algorithm table called RSASHA512 current* — RFC 9904 replaced RFC 8624 as the DNSSEC algorithm requirements, and it reads RSASHA512 as NOT RECOMMENDED for signing while still MUST for validation. `b.network.dns.classifyDnskeyAlgorithm(10)` reported it as `current`, so an operator asking which algorithm to sign a zone with got the opposite of what the current document says.
20
+
21
+ It is not marked deprecated, and deliberately: validation remains mandatory, and a caller using that flag to decide whether to ACCEPT a zone must keep accepting it. One boolean cannot carry a requirement that differs between signing and validation, so the reason names both levels and the flag follows the side an operator can still choose.
22
+
23
+ The citations moved with it — DSA, DSA-NSEC3-SHA1 and ECC-GOST to RFC 9904's Table 2, the DS digest vocabulary and GOST R 34.11-94 to Table 3, and RSAMD5 off RFC 6944, which had been obsoleted twice over, onto RFC 6725. Nothing asserted on algorithm 10 before, which is how a stale classification sat in a table everything else in the file kept current.
24
+
11
25
  - v0.18.46 (2026-08-22) — **A failing test reported the handle it leaked instead of the check that failed.** Thirty test files ran their assertions inside a `try` whose `finally` drained open handles. A throw from a `finally` replaces the error the body raised, so when a test failed and its skipped teardown left a socket behind, what surfaced was "a handle leaked" — the consequence — while the check that actually failed was discarded. That is the shape of failure that reads as an unexplained flake, and it was reachable in every suite that opens a server. **Fixed:** *A drain no longer replaces the failure it followed* — `try { ...tests... } finally { await drain() }` reads as correct. It is not. When the body throws, every teardown after the throw is skipped, so the drain finds the servers those teardowns would have closed and throws too — and a throw from a `finally` replaces the body's error. The run reports a leaked handle, which is true and useless: the check that failed is gone, and the leak looks like the bug.
12
26
 
13
27
  The drain now runs through one helper that keeps the body's error and appends the others to it, so a run that failed for a reason reports that reason, and a run that only leaked still reports the leak. Thirty files moved onto it.
@@ -207,6 +207,33 @@ function firstInRanges(text, ranges, from) {
207
207
  // The shape the detectors want: `{ index, char, codePoint }` for the first
208
208
  // codepoint of `text` in `ranges`, or null. `char` is the WHOLE codepoint,
209
209
  // which for an astral hit is a surrogate pair.
210
+ /**
211
+ * @primitive b.codepointClass.isAuditPolicy
212
+ * @signature b.codepointClass.isAuditPolicy(policy)
213
+ * @since 0.18.47
214
+ * @status stable
215
+ * @related b.gateContract.policyDisposition, b.gateContract.charPolicyEnums
216
+ *
217
+ * Whether a policy value asks for an AUDIT — reporting the finding rather than
218
+ * refusing or repairing. `audit-only` is the framework's own synonym for
219
+ * `audit`, and this is the one place that decides so.
220
+ *
221
+ * It exists because the two spellings disagreed. The disposition mapper treated
222
+ * them as the same thing while the severity calculation recognised only the
223
+ * literal `audit`, so `zeroWidthPolicy: "audit-only"` produced a high-severity
224
+ * issue and a failed validation where `audit` produced a warning and a pass.
225
+ * Two spellings of one setting is how they come to disagree; asking one
226
+ * predicate is how they stop.
227
+ *
228
+ * @example
229
+ * b.codepointClass.isAuditPolicy("audit"); // → true
230
+ * b.codepointClass.isAuditPolicy("audit-only"); // → true
231
+ * b.codepointClass.isAuditPolicy("reject"); // → false
232
+ */
233
+ function isAuditPolicy(policy) {
234
+ return policy === "audit" || policy === "audit-only";
235
+ }
236
+
210
237
  function _firstHit(text, ranges) {
211
238
  var i = firstInRanges(text, ranges);
212
239
  if (i === -1) return null;
@@ -817,7 +844,7 @@ function detectCharThreats(text, opts, codePrefix) {
817
844
  // reported, so the audit setting did nothing an operator could
818
845
  // observe except the thing they asked it not to do.
819
846
  severity: opts.controlPolicy === "reject" ? "high"
820
- : opts.controlPolicy === "audit" ? "warn" : "high",
847
+ : isAuditPolicy(opts.controlPolicy) ? "warn" : "high",
821
848
  ruleId: codePrefix + ".control",
822
849
  location: ctrlMatch.index,
823
850
  // DEL is a control character here but is not in the C0 block, so the
@@ -849,7 +876,7 @@ function detectCharThreats(text, opts, codePrefix) {
849
876
  issues.push({
850
877
  kind: "zero-width",
851
878
  severity: opts.zeroWidthPolicy === "reject" ? "critical"
852
- : opts.zeroWidthPolicy === "audit" ? "warn" : "high",
879
+ : isAuditPolicy(opts.zeroWidthPolicy) ? "warn" : "high",
853
880
  ruleId: codePrefix + ".zero-width",
854
881
  location: zwMatch.index,
855
882
  snippet: "zero-width / invisible-formatting char U+" +
@@ -877,7 +904,7 @@ function detectCharThreats(text, opts, codePrefix) {
877
904
  issues.push({
878
905
  kind: "unicode-tags",
879
906
  severity: tagsPolicy === "reject" ? "critical"
880
- : tagsPolicy === "audit" ? "warn" : "high",
907
+ : isAuditPolicy(tagsPolicy) ? "warn" : "high",
881
908
  ruleId: codePrefix + ".unicode-tags",
882
909
  location: tagMatch.index,
883
910
  snippet: "Unicode Tags block char U+" +
@@ -1724,6 +1751,7 @@ module.exports = {
1724
1751
  hex4: hex4,
1725
1752
  charClass: charClass,
1726
1753
  inRanges: inRanges,
1754
+ isAuditPolicy: isAuditPolicy,
1727
1755
  firstInRanges: firstInRanges,
1728
1756
  stripRanges: stripRanges,
1729
1757
  replaceRanges: replaceRanges,
@@ -2143,13 +2143,97 @@ function buildContentGate(spec) {
2143
2143
  * b.gateContract.policyDisposition("audit-only"); // → "audit"
2144
2144
  * b.gateContract.policyDisposition("rejet"); // → "refuse" (fail closed)
2145
2145
  */
2146
+ // The character-threat policies every guard in the family shares, and the
2147
+ // vocabulary each accepts. A value outside it is a typo, and a typo is a
2148
+ // CONFIG-TIME error: read leniently at runtime it takes whichever branch is not
2149
+ // the strict one — the scan runs because the value is not "allow", an issue is
2150
+ // raised, and policyDisposition falls through to "refuse". That fails closed,
2151
+ // so it is not a hole, but the operator asked for one disposition and silently
2152
+ // got another. This turns it into a boot error instead.
2153
+ //
2154
+ // `strip` is an instruction to repair, so it is legal only where the guard has
2155
+ // a `sanitize` to repair with — the same rule the family's performable-actions
2156
+ // invariant asserts. An `entries` guard has none by design: a hostile archive
2157
+ // member cannot be made safe.
2158
+ var CHAR_POLICY_KEYS = Object.freeze([
2159
+ "bidiPolicy", "nullBytePolicy", "controlPolicy", "zeroWidthPolicy", "tagsPolicy",
2160
+ ]);
2161
+ var CHAR_POLICY_BASE = Object.freeze(["allow", "audit", "audit-only", "reject"]);
2162
+
2163
+ /**
2164
+ * @primitive b.gateContract.charPolicyEnums
2165
+ * @signature b.gateContract.charPolicyEnums(defaults, opts?)
2166
+ * @since 0.18.47
2167
+ * @status stable
2168
+ * @related b.gateContract.resolveProfileAndPosture, b.gateContract.defineGuard
2169
+ *
2170
+ * The `enumOpts` entry for every character-threat policy present in `defaults`,
2171
+ * so a guard binding its own resolver holds callers to the same vocabulary the
2172
+ * generated one does. `opts.canRepair` adds `strip`, which is an instruction to
2173
+ * repair and so legal only where the guard has a `sanitize` to perform it.
2174
+ *
2175
+ * Pass the result as `enumOpts` to `resolveProfileAndPosture`. A guard whose
2176
+ * gate or validate binds its own resolver and omits this accepts a misspelled
2177
+ * policy on that entry point while the generated path refuses it — the two
2178
+ * doors disagreeing is how the check came to cover only one of them.
2179
+ *
2180
+ * @opts
2181
+ * canRepair: boolean, // default: false — adds `strip` to every entry
2182
+ *
2183
+ * @example
2184
+ * gateContract.resolveProfileAndPosture(opts, {
2185
+ * profiles: PROFILES,
2186
+ * defaults: DEFAULTS,
2187
+ * enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
2188
+ * });
2189
+ */
2190
+ function charPolicyEnums(defaults, opts) {
2191
+ var canRepair = !!(opts && opts.canRepair);
2192
+ var out = null;
2193
+ for (var i = 0; i < CHAR_POLICY_KEYS.length; i += 1) {
2194
+ var key = CHAR_POLICY_KEYS[i];
2195
+ // `tagsPolicy` is usually ABSENT from a guard's defaults because it
2196
+ // INHERITS from zeroWidthPolicy (resolveTagsPolicy reads one when the other
2197
+ // is unset). It is still a supported inline override, so a guard exposing
2198
+ // the inherited behaviour must hold a typo in it to the same vocabulary —
2199
+ // keying only on presence left `{ tagsPolicy: "strp" }` unchecked on every
2200
+ // guard that inherits, and the Tags detector then read the typo as a
2201
+ // high-severity refusal instead of the promised boot error.
2202
+ var governs = defaults && (typeof defaults[key] === "string" ||
2203
+ (key === "tagsPolicy" && typeof defaults.zeroWidthPolicy === "string"));
2204
+ if (!governs) continue;
2205
+ if (!out) out = {};
2206
+ out[key] = canRepair ? CHAR_POLICY_BASE.concat(["strip"]) : CHAR_POLICY_BASE.slice();
2207
+ }
2208
+ return out;
2209
+ }
2210
+
2211
+ function _withCharPolicyEnums(defaults, spec) {
2212
+ var declared = spec.enumOpts && typeof spec.enumOpts === "object" ? spec.enumOpts : null;
2213
+ // DECLARED, not inferred from the presence of a `sanitize`. An identifier
2214
+ // guard exports one that validates and throws — there is nothing to repair,
2215
+ // the value either is a UUID or is not — so inferring the capability let
2216
+ // guardRegex, guardJwt, guardShell and guardTemplate accept `strip` at
2217
+ // construction and refuse at runtime, which is the silent substitution this
2218
+ // check exists to stop. A guard says whether it can repair a character, and
2219
+ // the family sweep holds it to the claim.
2220
+ var derived = charPolicyEnums(defaults, { canRepair: spec.charRepair === true });
2221
+ if (!derived) return declared || null;
2222
+ var out = declared ? Object.assign({}, declared) : {};
2223
+ Object.keys(derived).forEach(function (key) {
2224
+ if (declared && declared[key]) return; // an explicit declaration wins
2225
+ out[key] = derived[key];
2226
+ });
2227
+ return out;
2228
+ }
2229
+
2146
2230
  var MITIGATION_POLICIES = Object.freeze({
2147
2231
  strip: true, "prefix-tab": true, "prefix-quote": true,
2148
2232
  "wrap-with-quotes-and-prefix": true, allowlist: true, redact: true, trim: true,
2149
2233
  });
2150
2234
  function policyDisposition(policy) {
2151
2235
  if (policy === "reject") return "refuse";
2152
- if (policy === "audit" || policy === "audit-only") return "audit";
2236
+ if (codepointClass.isAuditPolicy(policy)) return "audit";
2153
2237
  if (MITIGATION_POLICIES[policy] === true) return "sanitize";
2154
2238
  return "refuse";
2155
2239
  }
@@ -3168,7 +3252,12 @@ function defineGuard(spec) {
3168
3252
  // validate() below.
3169
3253
  nonNegativeOpts: _capKeys(defaults, spec.intOpts),
3170
3254
  // Options restricted to a fixed vocabulary, checked at the same funnel.
3171
- enumOpts: spec.enumOpts || null,
3255
+ // The character policies are derived rather than hand-listed, for the
3256
+ // reason the caps above are: declared per guard, they were declared by
3257
+ // one. guard-country wrote `enumOpts` for its own three opts after
3258
+ // `{ reservedPolicy: "rejcet" }` served a reserved code and reported ok,
3259
+ // and the other 259 policy opts across the family never followed.
3260
+ enumOpts: _withCharPolicyEnums(defaults, spec),
3172
3261
  });
3173
3262
  };
3174
3263
  if (typeof spec.detect === "function") {
@@ -3360,6 +3449,10 @@ function defineGuard(spec) {
3360
3449
  var out = {
3361
3450
  NAME: spec.name,
3362
3451
  KIND: spec.kind,
3452
+ // Surfaced so a caller — and the family sweep — can ask whether `strip` is
3453
+ // an instruction this guard can carry out, rather than inferring it from
3454
+ // the presence of a `sanitize` that may only validate and throw.
3455
+ CHAR_REPAIR: spec.charRepair === true,
3363
3456
  // The options this guard DECLARES to be caps, where zero is refused. Every
3364
3457
  // other numeric option is derived from the defaults and held only to
3365
3458
  // "non-negative integer", because derivation cannot tell a cap from a
@@ -3379,6 +3472,20 @@ function defineGuard(spec) {
3379
3472
  out.EXTENSIONS = Object.freeze((spec.extensions || []).slice());
3380
3473
  }
3381
3474
  if (spec.integrationFixtures) out.INTEGRATION_FIXTURES = spec.integrationFixtures;
3475
+ // A declaration has to be backed by something. `charRepair: true` says the
3476
+ // guard can carry out `strip`, which puts that value in its character-policy
3477
+ // vocabulary — so a spec that declares it without a `sanitize` (or a
3478
+ // `sanitizeTransform` for one to be built from) would accept the instruction
3479
+ // at boot and refuse it at runtime, which is the substitution the declaration
3480
+ // was introduced to stop. Checked HERE rather than earlier because
3481
+ // sanitizeTransform generates the sanitize further up, and checking before
3482
+ // that would refuse a spec that does supply a repair path.
3483
+ if (spec.charRepair === true && typeof spec.sanitize !== "function") {
3484
+ throw ErrorClass.factory(prefix + ".bad-spec",
3485
+ prefix + ": charRepair is declared but the guard has no sanitize — " +
3486
+ "`strip` on a character policy is an instruction to repair, so declare a " +
3487
+ "sanitize or a sanitizeTransform, or drop charRepair.");
3488
+ }
3382
3489
  if (typeof spec.sanitize === "function") out.sanitize = spec.sanitize;
3383
3490
  out.gate = gateFn;
3384
3491
  // Error class exported under its own constructor name (GuardCsvError etc.)
@@ -3713,6 +3820,7 @@ module.exports = {
3713
3820
  // defineGuard: the cap keys to declare, derived from its defaults so the
3714
3821
  // list cannot drift away from them.
3715
3822
  capKeysOf: capKeysOf,
3823
+ charPolicyEnums: charPolicyEnums,
3716
3824
  identitySanitize: identitySanitize,
3717
3825
  ctxValueFrom: ctxValueFrom,
3718
3826
  runIssueValidator: runIssueValidator,
@@ -1118,6 +1118,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
1118
1118
  module.exports = gateContract.defineGuard({
1119
1119
  name: "email",
1120
1120
  kind: "content",
1121
+ charRepair: true,
1121
1122
  errorClass: GuardEmailError,
1122
1123
  profiles: PROFILES,
1123
1124
  defaults: DEFAULTS,
@@ -1306,6 +1306,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
1306
1306
  module.exports = gateContract.defineGuard({
1307
1307
  name: "filename",
1308
1308
  kind: "filename",
1309
+ charRepair: true,
1309
1310
  errorClass: GuardFilenameError,
1310
1311
  profiles: PROFILES,
1311
1312
  defaults: DEFAULTS,
package/lib/guard-html.js CHANGED
@@ -983,6 +983,10 @@ var INTEGRATION_FIXTURES = Object.freeze({
983
983
  module.exports = gateContract.defineGuard({
984
984
  name: "html",
985
985
  kind: "content",
986
+ // sanitize rewrites the document, so `strip` on a character policy is an
987
+ // instruction this guard can carry out. Declared rather than inferred from
988
+ // the presence of a sanitize — an identifier guard has one that only throws.
989
+ charRepair: true,
986
990
  errorClass: GuardHtmlError,
987
991
  profiles: PROFILES,
988
992
  defaults: DEFAULTS,
package/lib/guard-json.js CHANGED
@@ -1086,6 +1086,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
1086
1086
  module.exports = gateContract.defineGuard({
1087
1087
  name: "json",
1088
1088
  kind: "content",
1089
+ charRepair: true,
1089
1090
  errorClass: GuardJsonError,
1090
1091
  profiles: PROFILES,
1091
1092
  defaults: DEFAULTS,
@@ -1905,6 +1905,7 @@ function render(source, opts) {
1905
1905
  module.exports = gateContract.defineGuard({
1906
1906
  name: "markdown",
1907
1907
  kind: "content",
1908
+ charRepair: true,
1908
1909
  errorClass: GuardMarkdownError,
1909
1910
  profiles: PROFILES,
1910
1911
  base: 256,
package/lib/guard-svg.js CHANGED
@@ -1008,6 +1008,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
1008
1008
  var _guard = module.exports = gateContract.defineGuard({
1009
1009
  name: "svg",
1010
1010
  kind: "content",
1011
+ charRepair: true,
1011
1012
  errorClass: GuardSvgError,
1012
1013
  profiles: PROFILES,
1013
1014
  defaults: DEFAULTS,
package/lib/guard-text.js CHANGED
@@ -129,9 +129,13 @@ function _resolveOpts(opts) {
129
129
  defaults: DEFAULTS,
130
130
  errorClass: GuardTextError,
131
131
  errCodePrefix: "text",
132
- // Own resolver, own cap declaration — see guard-image.gate.
132
+ // Own resolver, own cap declaration — see guard-image.gate. The policy
133
+ // vocabulary comes with it for the same reason: a hand-bound resolver that
134
+ // omits it accepts `bidiPolicy: "rejct"` on gate() and validate() while the
135
+ // generated path refuses it.
133
136
  intOpts: INT_OPTS,
134
137
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
138
+ enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
135
139
  });
136
140
  }
137
141
 
@@ -573,6 +577,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
573
577
  module.exports = gateContract.defineGuard({
574
578
  name: "text",
575
579
  kind: "content",
580
+ charRepair: true,
576
581
  errorClass: GuardTextError,
577
582
  profiles: PROFILES,
578
583
  defaults: DEFAULTS,
package/lib/guard-xml.js CHANGED
@@ -736,6 +736,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
736
736
  var _guard = module.exports = gateContract.defineGuard({
737
737
  name: "xml",
738
738
  kind: "content",
739
+ charRepair: true,
739
740
  errorClass: GuardXmlError,
740
741
  profiles: PROFILES,
741
742
  defaults: DEFAULTS,
package/lib/guard-yaml.js CHANGED
@@ -648,9 +648,12 @@ function parse(input, opts) {
648
648
  defaults: DEFAULTS,
649
649
  errorClass: GuardYamlError,
650
650
  errCodePrefix: "yaml",
651
- // Own resolver, own cap declaration — see guard-image.gate.
651
+ // Own resolver, own cap declaration — see guard-image.gate. The policy
652
+ // vocabulary comes with it: a hand-bound resolver that omits it accepts
653
+ // `bidiPolicy: "rejct"` here while the generated path refuses it.
652
654
  intOpts: INT_OPTS,
653
655
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
656
+ enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
654
657
  });
655
658
  if (typeof input !== "string") {
656
659
  throw _err("yaml.bad-input", "parse requires string input");
@@ -797,9 +800,12 @@ function gate(opts) {
797
800
  defaults: DEFAULTS,
798
801
  errorClass: GuardYamlError,
799
802
  errCodePrefix: "yaml",
800
- // Own resolver, own cap declaration — see guard-image.gate.
803
+ // Own resolver, own cap declaration — see guard-image.gate. The policy
804
+ // vocabulary comes with it: a hand-bound resolver that omits it accepts
805
+ // `bidiPolicy: "rejct"` here while the generated path refuses it.
801
806
  intOpts: INT_OPTS,
802
807
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
808
+ enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
803
809
  });
804
810
  return gateContract.buildContentGate({
805
811
  name: opts.name || "guardYaml:" + (opts.profile || "default"),
@@ -813,6 +819,7 @@ function gate(opts) {
813
819
  module.exports = gateContract.defineGuard({
814
820
  name: "yaml",
815
821
  kind: "content",
822
+ charRepair: true,
816
823
  errorClass: GuardYamlError,
817
824
  profiles: PROFILES,
818
825
  defaults: DEFAULTS,
@@ -2086,8 +2086,13 @@ function isNullMx(mxRecords) {
2086
2086
  * Classify a DNSKEY / RRSIG algorithm number against the IANA DNS
2087
2087
  * Security Algorithm Numbers registry, flagging SHA-1-based and
2088
2088
  * other deprecated algorithms per RFC 9905 (Deprecating DNSSEC
2089
- * SHA-1 Usage), RFC 8624 (Algorithm Implementation Requirements),
2090
- * and RFC 6944 / RFC 6725 (RSAMD5 deprecation).
2089
+ * SHA-1 Usage), RFC 9904 (Algorithm Implementation Requirements,
2090
+ * which obsoletes RFC 8624), and RFC 6725 (RSAMD5 deprecation).
2091
+ *
2092
+ * `deprecated` answers "should this still be accepted", so it follows
2093
+ * the VALIDATION requirement. Where a spelling differs between signing
2094
+ * and validation — RSASHA512 is NOT RECOMMENDED to sign with but MUST
2095
+ * still validate — the `reason` names both, since one boolean cannot.
2091
2096
  *
2092
2097
  * Returns `{ algorithm, name, deprecated, reason, known }` for any
2093
2098
  * IANA-assigned number; `known: false` for unassigned numbers
@@ -2123,18 +2128,22 @@ function isNullMx(mxRecords) {
2123
2128
  // auditing DNSSEC chain-of-trust evidence know they cannot validate
2124
2129
  // the entry against a public algorithm registry.
2125
2130
  var DNSKEY_ALGORITHMS = Object.freeze({
2126
- 1: { name: "RSAMD5", deprecated: true, reason: "MD5 broken (RFC 6944 §2.1, RFC 6725)" },
2131
+ 1: { name: "RSAMD5", deprecated: true, reason: "MD5 broken; deprecated for zone signing (RFC 6725 §2.2)" },
2127
2132
  2: { name: "DH", deprecated: true, reason: "Diffie-Hellman key (RFC 2539) — never widely deployed; superseded by signature algorithms" },
2128
- 3: { name: "DSA", deprecated: true, reason: "DSA deprecated (RFC 8624 §3.1)" },
2133
+ 3: { name: "DSA", deprecated: true, reason: "DSA MUST NOT be used for signing or validation (RFC 9904 Table 2)" },
2129
2134
  4: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
2130
2135
  5: { name: "RSASHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
2131
- 6: { name: "DSA-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3); DSA deprecated (RFC 8624 §3.1)" },
2136
+ 6: { name: "DSA-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3); DSA MUST NOT be used (RFC 9904 Table 2)" },
2132
2137
  7: { name: "RSASHA1-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
2133
2138
  8: { name: "RSASHA256", deprecated: false, reason: "current — RFC 5702" }, // IANA DNSKEY algorithm number
2134
2139
  9: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
2135
- 10: { name: "RSASHA512", deprecated: false, reason: "current RFC 5702" },
2140
+ // Not deprecated — validation is still MUST, so a caller deciding whether to
2141
+ // ACCEPT a zone must keep accepting it. But it is NOT RECOMMENDED to sign
2142
+ // with, and reporting it as plainly "current" answered a signing question
2143
+ // with the opposite of what the current RFC says.
2144
+ 10: { name: "RSASHA512", deprecated: false, reason: "defined in RFC 5702; RFC 9904 Table 2 — signing NOT RECOMMENDED, validation MUST" },
2136
2145
  11: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
2137
- 12: { name: "ECC-GOST", deprecated: true, reason: "deprecated (RFC 8624 §3.1)" },
2146
+ 12: { name: "ECC-GOST", deprecated: true, reason: "MUST NOT be used for signing (RFC 9904 Table 2)" },
2138
2147
  13: { name: "ECDSAP256SHA256", deprecated: false, reason: "current — RFC 6605" },
2139
2148
  14: { name: "ECDSAP384SHA384", deprecated: false, reason: "current — RFC 6605" },
2140
2149
  15: { name: "ED25519", deprecated: false, reason: "current — RFC 8080" },
@@ -2173,15 +2182,19 @@ var DNSKEY_ALGORITHMS = Object.freeze({
2173
2182
  * // → { digestType: 2, name: "SHA-256", deprecated: false, ... }
2174
2183
  */
2175
2184
 
2176
- // DS digest-type vocabulary (RFC 4034 §5.1 + RFC 6605 §6 + RFC 8624
2177
- // §3.2 + RFC 9558). Digest type 1 = SHA-1 is deprecated per RFC 9905
2178
- // §4. Digest types 5 (GOST R 34.11-2012) and 6 (SM3) added by RFC
2179
- // 9558. Reserved value 0 surfaced for completeness.
2185
+ // DS digest-type vocabulary (RFC 4034 §5.1 + RFC 6605 §6 + RFC 9904
2186
+ // Table 3 + RFC 9558). Digest type 1 = SHA-1 is deprecated per RFC 9905
2187
+ // §4 RFC 9904 Table 3 spells it MUST NOT for delegation and MUST for
2188
+ // validation, so `deprecated` follows the delegation side here for the
2189
+ // same reason it follows validation in the DNSKEY table: that is the
2190
+ // side an operator can still choose. Digest types 5 (GOST R 34.11-2012)
2191
+ // and 6 (SM3) added by RFC 9558. Reserved value 0 surfaced for
2192
+ // completeness.
2180
2193
  var DS_DIGEST_TYPES = Object.freeze({
2181
2194
  0: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 3658) — not for production use" },
2182
2195
  1: { name: "SHA-1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §4)" },
2183
2196
  2: { name: "SHA-256", deprecated: false, reason: "current — RFC 4509" },
2184
- 3: { name: "GOST R 34.11-94", deprecated: true, reason: "deprecated (RFC 8624 §3.2; superseded by GOST 2012 in RFC 9558)" },
2197
+ 3: { name: "GOST R 34.11-94", deprecated: true, reason: "MUST NOT be used for delegation (RFC 9904 Table 3); superseded by GOST 2012 in RFC 9558" },
2185
2198
  4: { name: "SHA-384", deprecated: false, reason: "current — RFC 6605 §6" },
2186
2199
  5: { name: "GOST R 34.11-2012", deprecated: false, reason: "current — RFC 9558 §3" },
2187
2200
  6: { name: "SM3", deprecated: false, reason: "current — RFC 9558 §3 (Chinese national standard)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.46",
3
+ "version": "0.18.47",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:7d5783b2-ffef-415c-b303-2b4cc46bc73c",
5
+ "serialNumber": "urn:uuid:2b85824f-8c87-4b4e-9ee7-abfe05e1b852",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-22T08:31:57.142Z",
8
+ "timestamp": "2026-08-22T12:32:06.262Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.46",
22
+ "bom-ref": "@blamejs/core@0.18.47",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.46",
25
+ "version": "0.18.47",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.46",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.47",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.46",
57
+ "ref": "@blamejs/core@0.18.47",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]