@blamejs/core 0.18.45 → 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 +38 -0
- package/lib/codepoint-class.js +31 -3
- package/lib/gate-contract.js +110 -2
- package/lib/guard-email.js +1 -0
- package/lib/guard-filename.js +1 -0
- package/lib/guard-html.js +4 -0
- package/lib/guard-json.js +1 -0
- package/lib/guard-markdown.js +1 -0
- package/lib/guard-svg.js +1 -0
- package/lib/guard-text.js +6 -1
- package/lib/guard-xml.js +1 -0
- package/lib/guard-yaml.js +9 -2
- package/lib/network-dns.js +25 -12
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,44 @@ 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
|
+
|
|
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.
|
|
26
|
+
|
|
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.
|
|
28
|
+
|
|
29
|
+
Six of those thirty had their own cleanup — destroying a keep-alive agent, closing websocket clients and the sockets an upgrade detached from the server, resetting module state — sitting in the same `finally`. They were the worse instance rather than the exception: more throwable work in the place that replaces the failure. The helper took an optional teardown so their work runs inside the structure that preserves the error, instead of those six taking an exemption.
|
|
30
|
+
|
|
31
|
+
One file drained fifty-six times, once per section. A drain is a leak VERDICT rather than cleanup, so that was fifty-six places a real assertion failure could surface as a leak. Its sections now clean up and the verdict is taken once, at the end, where nothing is left to mask. The trade is that a leak there is reported against the file rather than the section; what the drain names is the surviving handles with their addresses and states, which is the attribution that matters. · *The helper that makes that promise is now tested* — Thirty files rely on "the body's error wins", and nothing checked it. A helper can stop doing the one thing it exists for while every suite using it still passes, because the suites only exercise it on the path where nothing goes wrong.
|
|
32
|
+
|
|
33
|
+
It is now driven directly: a body that throws, a teardown that throws after it, both throwing at once, a teardown that throws when the body passed, and a body that throws a string rather than an Error — which used to lose the appended note entirely, since assigning to a property of a primitive is a silent no-op outside strict mode.
|
|
34
|
+
|
|
35
|
+
The suite was confirmed to fail against the behaviour it forbids before it was kept. · *Two ways the failure-preserving helper could still lose the failure* — A thrown `null` was not a failure. Both helpers decided whether something had failed by asking whether the value it threw was truthy, so `throw null`, `throw ""` and `throw 0` read as passes: the drain helper returned normally, and the polling helper dropped "last predicate threw" from its timeout message, leaving "the condition never came true" where the truth was "the condition could not be evaluated". Both now track whether a throw happened as its own flag, separate from what was thrown.
|
|
36
|
+
|
|
37
|
+
And annotating an error could destroy it, or be invisible. When a run fails and its teardown fails after, the second failure is appended to the first — by mutating the error's message. An error can refuse to be mutated: a frozen one, or a `message` defined non-writable. That assignment throws, and the throw escapes from the annotator and replaces the failure it was called to annotate. Appending is now attempted and, when refused, the same information is carried in a new error that keeps the original as its cause.
|
|
38
|
+
|
|
39
|
+
The note also has to reach the place anybody reads. A runner prints `.stack`, not `.message`, and a stack string is formatted once on first read and then cached — so an error that some assertion or logger had already touched carried a stack headed by the original message, and the appended note never appeared. The note now goes on the stack as well, guarded on whether it is already there rather than on whether the stack was cached, so it lands exactly once.
|
|
40
|
+
|
|
41
|
+
All of it is the swallowing this release is about, one level further in: the helper that exists to stop a failure being lost had several ways of its own to lose one. · *A polling timeout blamed an exception the predicate had stopped raising* — The polling helper names the last exception in its timeout message, which is the difference between "the condition never came true" and "the condition could not be evaluated". It reported whatever had most recently been thrown, including from a poll many rounds earlier — so a predicate that raised while the thing it watches was still settling, and then answered cleanly, blamed the timeout on an exception it had stopped raising.
|
|
42
|
+
|
|
43
|
+
A throw during polling is not the verdict. The predicate is being asked repeatedly precisely because the state is still settling, so the loop now treats a throw as "not yet" and keeps nothing from it. The diagnostic speaks for one attempt — the one at the deadline — and answers the question worth asking: was the predicate still failing to evaluate when the wait gave up?
|
|
44
|
+
|
|
45
|
+
The timeout diagnostic now has its own tests, which is what surfaced this: whether the helper eventually returns is exercised by every suite that polls, and the message it produces when it does not was exercised by none of them. · *Two test files said they were outside the smoke run* — Both carried a header stating they were not part of the smoke surface. The runner takes every `*.test.js` under the layer directory, and the only opt-out is a `STANDALONE_ONLY` marker in the first two kilobytes, which neither file carries — so both have been running all along. A comment that says a failure here will not block is the kind that gets believed. **Detectors:** *A handle drain may not sit in a `finally`* — The shape is refused wherever it reappears. It matches both spellings — a direct call and a local wrapper — because a word boundary alone misses the underscore-prefixed form, which is exactly how six of the thirty files went uncounted when the population was first measured.
|
|
46
|
+
|
|
47
|
+
Anchored on the `finally` and the drain together and tempered so it cannot cross a function-closing brace; the quantifier is a backstop far above any real `finally` body rather than the precision mechanism. It was confirmed firing on all thirty before any of them were converted, and silent after the last one.
|
|
48
|
+
|
|
11
49
|
- v0.18.45 (2026-08-21) — **`b.publicSuffix` returned names that cannot exist — a 64-octet label, and `ex*ample.com`.** `canonicalDomain` is where the framework decides what a domain IS, and two kinds of non-domain got through it. A label longer than the 63 octets RFC 1035 allows came back as an organizational domain, and 17 of 31 non-LDH characters survived because `domainToASCII` maps a name without validating it as a host. Both answers then travelled: into DMARC alignment, cookie-domain comparisons and residency routing, where a name no resolver will ever return is compared against one that exists. **Changed:** *Vendored `@blamejs/pki` 0.5.23 to 0.5.25* — The toolkit behind `b.mtlsCa` and `b.auth.passkey`. Two releases land together.
|
|
12
50
|
|
|
13
51
|
0.5.24 binds an ACME certificate download to the order that asked for it. The end-entity certificate that comes back must carry the public key the order's CSR asked to have certified, and its identifier set must equal the order's — its dNSName and iPAddress subject alternative names, with the subject common name read only where it asserts none. `downloadCertificate` now requires `expectedSpki`, `identifiers`, or both, and refuses a call supplying neither rather than returning a certificate nothing looked at. An identifier that maps to no certificate name is refused rather than dropped from the comparison, which is what let an order for a name plus one other identifier be satisfied by a certificate covering only the name.
|
package/lib/codepoint-class.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
package/lib/gate-contract.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
package/lib/guard-email.js
CHANGED
package/lib/guard-filename.js
CHANGED
|
@@ -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
package/lib/guard-markdown.js
CHANGED
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
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,
|
package/lib/network-dns.js
CHANGED
|
@@ -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
|
|
2090
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|
|
2177
|
-
//
|
|
2178
|
-
// §4
|
|
2179
|
-
//
|
|
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: "
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:2b85824f-8c87-4b4e-9ee7-abfe05e1b852",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.47",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.18.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.18.47",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|