@blamejs/core 0.7.20 → 0.7.22

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.22** (2026-05-05) — DKIM dual-signer + soc2-cc7 → soc2 posture rename. **`b.mail.dkim.dualSigner`** — RFC 8463 §3 transition signer that produces messages with BOTH a legacy RSA-SHA-256 DKIM-Signature AND an Ed25519-SHA-256 DKIM-Signature header. Receivers without Ed25519 support validate the RSA signature; receivers that prefer Ed25519 validate the post-quantum-friendlier signature. Operators rolling off RSA-SHA-256 wire `b.mail.dkim.dualSigner({ domain, rsa: { selector, privateKey }, eddsa: { selector, privateKey } })` and pass the result anywhere a regular DKIM signer is accepted; `sign()` produces a wire with two `DKIM-Signature` headers. Both signers are constructed eagerly at create-time (configuration errors surface at boot, not at first send). **soc2-cc7 → soc2 posture rename** — every guard's compliance posture name changes from `"soc2-cc7"` to `"soc2"`. The CC7-specific scoping was misleading (SOC 2 controls span CC1–CC9; the existing posture wasn't CC7-specific). Operators with `compliancePosture: "soc2-cc7"` MUST update to `compliancePosture: "soc2"` — the old name now throws `unknown compliance posture`. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
12
+
13
+ - **0.7.21** (2026-05-05) — small primitive batch (5 fixes): TLS 1.3 framework-wide minimum, `b.safeRedirect`, `b.pick`, audit-sign legacy compat-shim removed, webhook PQC signatures emit base64url. **Framework-wide TLS 1.3 minimum** — `index.js` sets `tls.DEFAULT_MIN_VERSION = "TLSv1.3"` once at boot, before any framework module loads `node:tls`. Applies to every TLS socket (outbound `https.request` / mail SMTP+STARTTLS / Redis-Postgres-Mongo TLS / `b.httpClient`, AND inbound `https.createServer` when blamejs is the listener). Per-call override still works for legacy peers. **`b.safeRedirect`** — open-redirect (CWE-601) defense. `b.safeRedirect.resolve(rawTarget, { allowedOrigins, allowedHosts, fallback })` returns the safe URL (or fallback). Refuses protocol-relative (`//attacker.com`), backslash variants (`\\\\attacker.com`), control-char-laden (CRLF header injection), and `data:` / `javascript:` schemes; same-origin paths (`/dashboard`) and fragments (`#x`) pass through; full URLs require explicit allowlist. Operator drops the result straight into `res.writeHead(302, { Location: ... })`. **`b.pick`** — mass-assignment (CWE-915 / OWASP API3:2023) defense. `b.pick(req.body, ["a", "b", ["nested", ["sub1"]]])` returns a NEW object with only allowlisted keys; prototype-pollution keys (`__proto__` / `constructor` / `prototype`) ALWAYS stripped even if listed. `opts.onUnknown: "throw"` rejects unknown keys instead of silently dropping. Nested allowlist syntax for object-shaped fields. **Audit-sign legacy compat-shim removed** — `lib/audit-sign.js` no longer falls back to `ml-dsa-87` for key files missing the `algorithm` field. Throws `KEY_FILE_MISSING_ALG` / `UNWRAPPED_MISSING_ALG` at load time; operators with legacy files rotate the key (deletes + regenerates) or hand-edit to add `"algorithm": "slh-dsa-shake-256f"`. Pre-v1 compat-shim sweep per the no-pre-v1-compat rule. **Webhook PQC signatures emit base64url** — `b.webhook` now signs to base64url (was hex). SLH-DSA-SHAKE-256f signatures are ~29.5 KB binary → ~40 KB base64url vs ~59 KB hex; the hex form blew past nginx default 8 KB / Cloudflare default 16 KB / many CDN edge limits. Verification accepts EITHER encoding for a transition window — base64url-shaped sig values decode as base64url; hex-shaped values decode as hex. Smoke 8478 / wiki e2e 178 / Linux container smoke 8478 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
14
+
11
15
  - **0.7.20** (2026-05-05) — browser-hardening batch (5 fixes): CSRF Origin/Referer cross-check, default CSP gains Trusted Types, expanded Permissions-Policy, `__Host-` / `__Secure-` cookie prefix invariants, `b.middleware.fetchMetadata`. **CSRF Origin/Referer cross-check** (`b.middleware.csrfProtect`) — second-line defense alongside the double-submit token. State-changing requests whose Origin (or Referer when Origin is absent) doesn't resolve to the request's own origin are refused before the token check. Defaults ON; operator opt-out via `checkOrigin: false`; operator allowlist via `allowedOrigins: ["https://app.example.com"]`. Missing-Origin-AND-Missing-Referer (curl, server-to-server) defers to the token check. **Trusted Types in default CSP** (`b.middleware.securityHeaders`) — default CSP now includes `require-trusted-types-for 'script'; trusted-types 'allow-duplicates' default;`. Compatible browsers (Chrome 83+, Edge 83+) enforce typed-value DOM-sink writes (defends every untrusted-string-to-DOM XSS vector at runtime); Firefox + Safari ignore (no regression). Operators opt out by passing an explicit `csp:` value. **Expanded Permissions-Policy** — defaults now disable `browsing-topics=()`, `attribution-reporting=()`, `unload=()`, `interest-cohort=()`, `join-ad-interest-group=()`, `run-ad-auction=()`, `private-state-token-issuance=()`, `private-state-token-redemption=()`, `compute-pressure=()`, `hid=()`, `serial=()`, `idle-detection=()` — closes advertising / tracking / load-event-leak / device-API surfaces. Existing operator overrides via `permissionsPolicy: "..."` continue unchanged. **`__Host-` / `__Secure-` cookie prefix invariants** (`b.cookies.serialize`) — RFC 6265bis §4.1.3 enforced at serialize time. `__Secure-*` requires `secure: true`; `__Host-*` requires `secure: true` AND `path: "/"` AND no `domain:`. Each violation throws a typed `CookieError` with operator-actionable code (`cookies/prefix-secure-required`, `cookies/prefix-host-secure-required`, `cookies/prefix-host-path-required`, `cookies/prefix-host-no-domain`) instead of producing a malformed cookie that browsers silently reject. **`b.middleware.fetchMetadata`** — new fetch-metadata isolation primitive. Reads `Sec-Fetch-Site` / `Sec-Fetch-Mode` / `Sec-Fetch-Dest` and refuses cross-site state-changing requests by default. Operators opt in to specific destinations (e.g. `allowedDest: ["empty", "document"]`) or specific origins (`allowCrossSite: true`). Direct navigations (typed URL / bookmark — `Sec-Fetch-Site: none`) pass through; `same-origin` always passes; `same-site` configurable. Missing fetch-metadata (legacy browsers, server-to-server) deferred to other auth/CSRF layers per `allowMissing: true` default. Smoke 8481 / wiki e2e 178 / Linux container smoke 8481 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
12
16
 
13
17
  - **0.7.19** (2026-05-05) — auth-primitives batch (5 fixes): session idle/absolute timeout, JWT keyResolver for kid rotation, `b.middleware.bearerAuth`, `b.auth.jwt.verifyExternal` (classical-alg JWT verifier with JWKS support), and Argon2id parameter audit visibility. **Session idle + absolute timeouts** (`b.session.verify`) now enforce OWASP ASVS 5.0 §3.3 / NIST SP 800-63B-4 — defaults: 30 min idle, 12 hours absolute. Operators opt out per-call by passing `idleTimeoutMs: 0` / `absoluteTimeoutMs: 0`. Both surface `auth.session.expired_idle` / `auth.session.expired_absolute` audit events on enforcement. **JWT `keyResolver` opt** (`b.auth.jwt.verify`) — operator passes `keyResolver(decodedHeader)` to look up the public key per-token (typically by `kid`). Mutually exclusive with `opts.publicKey`. Async-friendly. Closes the kid-rotation gap where signers carried `kid` but verifiers only accepted a single static key. **`b.middleware.bearerAuth`** — new middleware that extracts `Authorization: Bearer <token>`, runs an operator-supplied `verify(token)` function, and attaches `req.user`. Distinct from cookie-session `attachUser`. Missing-Authorization passes through (so cookie path can take over); invalid/null/throw rejects 401 + `WWW-Authenticate: Bearer error="invalid_token"` per RFC 6750 §3. **`b.auth.jwt.verifyExternal`** — new generic classical-alg JWT verifier (RS256 / RS384 / RS512 / PS256 / PS384 / PS512 / ES256 / ES384 / ES512 / EdDSA) for integration with external IdPs (Auth0 / Okta / Keycloak / Cognito / Azure AD / Google / Apple). `algorithms` is REQUIRED with no default — defends the alg-confusion class (CVE-2024-54150 / CVE-2025-30144 / CVE-2026-22817 Hono). HMAC algs and `none` are explicitly refused (HMAC + JWKS public-key trust source IS the alg-confusion vector). Three key-source options: `jwks` (pre-fetched array), `jwksUri` (auto-fetched + TTL-cached via `b.httpClient` SSRF gate), `keyResolver` (custom). Standard claim checks (`exp` / `nbf` / `iat` / `aud` / `iss` / `sub`) with operator-tunable `clockSkewMs`. **`b.auth.password.params()`** — new accessor returning the active Argon2id params (`memoryCostKib` / `timeCost` / `parallelism`) plus the OWASP 2026 floor (`19 MiB` / `t>=2` / `p>=1`) plus `meetsFloor: bool`. Compliance-audit visibility without parsing PHC strings. Smoke 8458 → 8481 / wiki e2e 178 / Linux container smoke 8481 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
package/README.md CHANGED
@@ -46,7 +46,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
46
46
  - **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto + cryptographic erasure (`b.cryptoField.eraseRow`), signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA that issues clientAuth / serverAuth / dual-EKU certs with SAN entries and auto-detects the highest-PQC signature algorithm the vendored x509 library accepts (today: ECDSA-P384-SHA384 bridge; self-upgrades to SLH-DSA / ML-DSA when the X.509 ecosystem catches up), PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
47
47
  - **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`, in-process CIDR fence via `b.middleware.networkAllowlist`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate (cloud-metadata IPs hard-denied unconditionally; private / loopback / link-local overridable per call), scheme + userinfo + per-host (wildcard / per-method) destination allowlist, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`); operator-tunable network configurability — env-driven NTP / NTS (RFC 8915 authenticated time), IPv4-or-IPv6 NTP servers, DNS with IPv6 / DoH / DoT (private-CA trust pinning via `opts.ca`) / cache / lookup timeout, outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`), runtime DPI trust-store CA additions, application-level heartbeats, TCP socket defaults (`b.network`).
48
48
  - **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env), `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.).
49
- - **Content-safety gates** — `b.gateContract` uniform composition contract (mode posture / hooks / forensic snapshot / decision cache / runtime cap). Family members: `b.guardCsv` (formula injection ASCII + full-width prefixes, dangerous-function denylist, bidi / homoglyph / control / null / BOM / zero-width detection, dialect ambiguity, CSV-bombs, numeric precision, schema-bound serializer); `b.guardHtml` (XSS / mXSS / DOM-clobbering / dangerous-tag / event-handler-family / dangerous-URL-scheme with entity-decode / CSS-injection in style attribute / IE conditional comments + token-level sanitize + always-correct `escapeText` / `escapeAttr` entity encoders); `b.guardSvg` (script / foreignObject / animation-element href hijack / DOCTYPE billion-laughs / XXE / SVGZ / cross-origin `<use>` SSRF / event-handlers + token-level sanitize). `b.guardArchive` (zip-slip / symlink + hardlink escape / decompression-ratio bombs (per-entry + aggregate) / nested-archive depth / duplicate-entry / case-insensitive collision / encryption-claim mismatch / format-claim mismatch via magic-byte detection — composes `b.guardFilename` for per-entry-name validation); `b.guardJson` (source-level prototype-pollution detection, duplicate-key, NaN/Infinity, JSON5 syntax, BOM, bidi, numeric-precision-loss, top-level-key allowlist, depth/breadth/array/string/node-count caps); `b.guardYaml` (deserialization-tag RCE via language-specific tag prefixes, billion-laughs alias recursion, Norway-problem implicit booleans, leading-zero octals, multi-document streams, duplicate keys, merge-key chains, depth+anchor+node caps); `b.guardXml` (XXE / billion-laughs / external-entity / parameter-entity / XInclude / xsi:schemaLocation / processing-instruction / CDATA / XML-signature-wrapping detection — DOCTYPE refused at all profile levels); `b.guardMarkdown` (source-level scan run BEFORE any markdown renderer sees the input — dangerous URL schemes in inline links + images + autolinks + reference-link definitions with HTML-entity decode bypass; whitespace-tolerant dangerous-tag matching per CVE-2026-30838; front-matter; HTML comments; code-fence language injection; catastrophic emphasis-run ReDoS per CVE-2025-6493 class; inline DOCTYPE; depth + link + image + autolink + ref-def caps); `b.guardEmail` (single-address + full RFC 822/5322 message validation — SMTP smuggling per CVE-2023-51764 / 51765 / 51766 / CVE-2026-32178 class via bare-CR + bare-LF + smuggled SMTP verbs; CRLF header injection; IDN homograph mixed-script domains with operator-opt-in `allowedScripts`; Punycode flag; display-name spoofing; IP-literal addresses; RFC 5322 comment syntax; multi-@; RFC 5321 length caps + RFC 5322 line cap; BOM injection). Filename safety: `b.guardFilename` (path traversal raw + percent-encoded + overlong-UTF-8 + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi spoofing + shell-exec / double-extension detection — standalone, wires into `b.fileUpload` via `filenameSafety`). All members ship strict / balanced / permissive profiles plus hipaa / pci-dss / gdpr / soc2-cc7 compliance postures. `b.guardAll` is the registry + aggregator: every shipped guard ON by default; opt-out per guard with audited reason via `exceptFor: { name: { reason } }`. **As of v0.7.12, `b.fileUpload` and `b.staticServe` wire `b.guardAll.byExtension({ profile: "strict" })` automatically + `b.fileUpload` also wires `b.guardFilename.gate({ profile: "strict" })` as `filenameSafety`** — defense-in-depth applied without any explicit operator wiring. Operators opt out per host-primitive via `contentSafety: null` / `filenameSafety: null` (audited at create() with operator-supplied reason).
49
+ - **Content-safety gates** — `b.gateContract` uniform composition contract (mode posture / hooks / forensic snapshot / decision cache / runtime cap). Family members: `b.guardCsv` (formula injection ASCII + full-width prefixes, dangerous-function denylist, bidi / homoglyph / control / null / BOM / zero-width detection, dialect ambiguity, CSV-bombs, numeric precision, schema-bound serializer); `b.guardHtml` (XSS / mXSS / DOM-clobbering / dangerous-tag / event-handler-family / dangerous-URL-scheme with entity-decode / CSS-injection in style attribute / IE conditional comments + token-level sanitize + always-correct `escapeText` / `escapeAttr` entity encoders); `b.guardSvg` (script / foreignObject / animation-element href hijack / DOCTYPE billion-laughs / XXE / SVGZ / cross-origin `<use>` SSRF / event-handlers + token-level sanitize). `b.guardArchive` (zip-slip / symlink + hardlink escape / decompression-ratio bombs (per-entry + aggregate) / nested-archive depth / duplicate-entry / case-insensitive collision / encryption-claim mismatch / format-claim mismatch via magic-byte detection — composes `b.guardFilename` for per-entry-name validation); `b.guardJson` (source-level prototype-pollution detection, duplicate-key, NaN/Infinity, JSON5 syntax, BOM, bidi, numeric-precision-loss, top-level-key allowlist, depth/breadth/array/string/node-count caps); `b.guardYaml` (deserialization-tag RCE via language-specific tag prefixes, billion-laughs alias recursion, Norway-problem implicit booleans, leading-zero octals, multi-document streams, duplicate keys, merge-key chains, depth+anchor+node caps); `b.guardXml` (XXE / billion-laughs / external-entity / parameter-entity / XInclude / xsi:schemaLocation / processing-instruction / CDATA / XML-signature-wrapping detection — DOCTYPE refused at all profile levels); `b.guardMarkdown` (source-level scan run BEFORE any markdown renderer sees the input — dangerous URL schemes in inline links + images + autolinks + reference-link definitions with HTML-entity decode bypass; whitespace-tolerant dangerous-tag matching per CVE-2026-30838; front-matter; HTML comments; code-fence language injection; catastrophic emphasis-run ReDoS per CVE-2025-6493 class; inline DOCTYPE; depth + link + image + autolink + ref-def caps); `b.guardEmail` (single-address + full RFC 822/5322 message validation — SMTP smuggling per CVE-2023-51764 / 51765 / 51766 / CVE-2026-32178 class via bare-CR + bare-LF + smuggled SMTP verbs; CRLF header injection; IDN homograph mixed-script domains with operator-opt-in `allowedScripts`; Punycode flag; display-name spoofing; IP-literal addresses; RFC 5322 comment syntax; multi-@; RFC 5321 length caps + RFC 5322 line cap; BOM injection). Filename safety: `b.guardFilename` (path traversal raw + percent-encoded + overlong-UTF-8 + null-byte truncation + Windows reserved names + NTFS ADS + RTLO bidi spoofing + shell-exec / double-extension detection — standalone, wires into `b.fileUpload` via `filenameSafety`). All members ship strict / balanced / permissive profiles plus hipaa / pci-dss / gdpr / soc2 compliance postures. `b.guardAll` is the registry + aggregator: every shipped guard ON by default; opt-out per guard with audited reason via `exceptFor: { name: { reason } }`. **As of v0.7.12, `b.fileUpload` and `b.staticServe` wire `b.guardAll.byExtension({ profile: "strict" })` automatically + `b.fileUpload` also wires `b.guardFilename.gate({ profile: "strict" })` as `filenameSafety`** — defense-in-depth applied without any explicit operator wiring. Operators opt out per host-primitive via `contentSafety: null` / `filenameSafety: null` (audited at create() with operator-supplied reason).
50
50
  - **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); generic distributed pub/sub with cluster-table / Redis PUB/SUB / custom backends (`b.pubsub`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`); chunked file uploads with per-chunk SHA3-512 verification + atomic finalize + tombstone cleanup (`b.fileUpload`).
51
51
  - **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks (local file rotation, generic webhook, OTLP/HTTP-JSON OR OTLP/gRPC to an OTel collector, AWS CloudWatch Logs via SigV4 with optional autoCreate, RFC 5424 syslog over UDP/TCP/TLS), OTLP/HTTP-JSON exporter for traces + metrics (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`); operator-callable boot-time security policy assertions (`b.security.assertProduction`) and tamper-evident config-baseline drift detection signed with the audit-signing key (`b.configDrift`).
52
52
  - **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`).
package/index.js CHANGED
@@ -1,4 +1,16 @@
1
1
  "use strict";
2
+
3
+ // TLS 1.3 minimum, framework-wide. Sets the default for every TLS
4
+ // socket the process opens — outbound (https.request, mail SMTP+
5
+ // STARTTLS, redis/postgres/mongo with TLS, http-client) AND inbound
6
+ // (https.createServer when blamejs is the listener). Per-call override
7
+ // still works when an operator with a legacy peer needs TLSv1.2.
8
+ // node:tls reads `DEFAULT_MIN_VERSION` once at first TLS use; setting
9
+ // it here, before any framework module loads node:tls, makes the
10
+ // default sticky for the entire process.
11
+ var _tls = require("node:tls");
12
+ _tls.DEFAULT_MIN_VERSION = "TLSv1.3";
13
+
2
14
  /**
3
15
  * blamejs — public API entry point.
4
16
  *
@@ -83,6 +95,8 @@ httpClient.encrypted = require("./lib/middleware/api-encrypt").httpClient;
83
95
  httpClient.cookieJar = require("./lib/http-client-cookie-jar");
84
96
  var websocket = require("./lib/websocket");
85
97
  var safeUrl = require("./lib/safe-url");
98
+ var safeRedirect = require("./lib/safe-redirect");
99
+ var pick = require("./lib/pick");
86
100
  var gateContract = require("./lib/gate-contract");
87
101
  var guardCsv = require("./lib/guard-csv");
88
102
  var guardHtml = require("./lib/guard-html");
@@ -220,6 +234,8 @@ module.exports = {
220
234
  httpClient: httpClient,
221
235
  websocket: websocket,
222
236
  safeUrl: safeUrl,
237
+ safeRedirect: safeRedirect,
238
+ pick: pick,
223
239
  gateContract: gateContract,
224
240
  guardCsv: guardCsv,
225
241
  guardHtml: guardHtml,
package/lib/audit-sign.js CHANGED
@@ -77,12 +77,11 @@ var _err = AuditSignError.factory;
77
77
 
78
78
  // Default for newly-generated keys. Operators can override at init
79
79
  // via opts.algorithm — e.g. `auditSigning: { algorithm: "ml-dsa-87" }`
80
- // for throughput-sensitive deployments. Existing key files determine
81
- // their own algorithm via the on-disk `algorithm` field; legacy key
82
- // files that predate the algorithm field load as "ml-dsa-87" (the
83
- // previous implicit default).
80
+ // for throughput-sensitive deployments. Every key file MUST carry the
81
+ // `algorithm` field on disk the framework refuses to load a key file
82
+ // that lacks it. The legacy implicit-default-to-ml-dsa-87 fallback was
83
+ // removed as part of the pre-v1 compat-shim sweep.
84
84
  var DEFAULT_SIGNING_ALG = "slh-dsa-shake-256f";
85
- var LEGACY_DEFAULT_ALG = "ml-dsa-87";
86
85
  var SUPPORTED_SIGNING_ALGS = Object.freeze(["slh-dsa-shake-256f", "ml-dsa-87"]);
87
86
 
88
87
  var SIGNING_KEY_SCHEMA = {
@@ -202,11 +201,16 @@ function _initPlaintext() {
202
201
  throw _err("KEY_FILE_CORRUPT",
203
202
  "audit-sign.key corrupted or schema-invalid at " + paths.plaintext + " - " + e.message);
204
203
  }
205
- var loadedAlg = loaded.algorithm || LEGACY_DEFAULT_ALG;
204
+ if (typeof loaded.algorithm !== "string" || loaded.algorithm.length === 0) {
205
+ throw _err("KEY_FILE_MISSING_ALG",
206
+ "audit-sign.key at " + paths.plaintext + " is missing the required " +
207
+ "`algorithm` field. Regenerate the keypair (deletes the file and " +
208
+ "boots fresh) or hand-edit to add `\"algorithm\": \"slh-dsa-shake-256f\"`.");
209
+ }
206
210
  keys = {
207
211
  publicKey: loaded.publicKey,
208
212
  privateKey: loaded.privateKey,
209
- algorithm: loadedAlg,
213
+ algorithm: loaded.algorithm,
210
214
  fingerprint: _computeFingerprint(loaded.publicKey),
211
215
  };
212
216
  return;
@@ -248,14 +252,17 @@ async function _initWrapped() {
248
252
  throw _err("UNWRAPPED_INVALID",
249
253
  "unwrapped audit-sign.key invalid: " + e.message);
250
254
  }
251
- var loadedAlg = loaded.algorithm || LEGACY_DEFAULT_ALG;
255
+ if (typeof loaded.algorithm !== "string" || loaded.algorithm.length === 0) {
256
+ throw _err("UNWRAPPED_MISSING_ALG",
257
+ "unwrapped audit-sign.key is missing the required `algorithm` field.");
258
+ }
252
259
  keys = {
253
260
  publicKey: loaded.publicKey,
254
261
  privateKey: loaded.privateKey,
255
- algorithm: loadedAlg,
262
+ algorithm: loaded.algorithm,
256
263
  fingerprint: _computeFingerprint(loaded.publicKey),
257
264
  };
258
- log("audit-signing keypair unsealed (alg=" + loadedAlg + ").");
265
+ log("audit-signing keypair unsealed (alg=" + loaded.algorithm + ").");
259
266
  } finally {
260
267
  // The audit-signing passphrase is single-use at boot — no re-wrap path
261
268
  // keeps it alive (unlike vault.currentPassphrase). Zero on the way out.
package/lib/csv.js CHANGED
@@ -39,7 +39,7 @@
39
39
  * `b.guardCsv` — its `serialize` / `validate` / `sanitize` / `gate`
40
40
  * surface handles every documented threat with a single profile choice
41
41
  * (strict / balanced / permissive / email-attachment) or compliance
42
- * posture (hipaa / pci-dss / gdpr / soc2-cc7).
42
+ * posture (hipaa / pci-dss / gdpr / soc2).
43
43
  *
44
44
  * Throws CsvError (FrameworkError, permanent) on shape violations.
45
45
  */
package/lib/guard-all.js CHANGED
@@ -43,7 +43,7 @@
43
43
  * - PROFILES — object map; must include the SHARED_PROFILES
44
44
  * vocabulary (strict / balanced / permissive)
45
45
  * - COMPLIANCE_POSTURES — object map; must include the SHARED_POSTURES
46
- * vocabulary (hipaa / pci-dss / gdpr / soc2-cc7)
46
+ * vocabulary (hipaa / pci-dss / gdpr / soc2)
47
47
  * - gate(opts) — returns a b.gateContract-shaped gate
48
48
  *
49
49
  * The parity check at module load throws GuardAllError if a registered
@@ -95,7 +95,7 @@ var STANDALONE_GUARDS = [
95
95
  // support. Adding a new shared profile / posture is a coordinated
96
96
  // cross-guard change — every member must implement it.
97
97
  var SHARED_PROFILES = Object.freeze(["strict", "balanced", "permissive"]);
98
- var SHARED_POSTURES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2-cc7"]);
98
+ var SHARED_POSTURES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
99
99
 
100
100
  // ---- Registry parity check (runs at module load) ----
101
101
 
@@ -117,7 +117,7 @@
117
117
  * permissive — symlinks + hardlinks within root allowed; nested-depth
118
118
  * 4; 100000 entries; 10 GiB total; 1000:1 ratio.
119
119
  *
120
- * Compliance postures: hipaa / pci-dss / gdpr / soc2-cc7 — strict
120
+ * Compliance postures: hipaa / pci-dss / gdpr / soc2 — strict
121
121
  * overlay + forensic snapshots.
122
122
  */
123
123
 
@@ -248,7 +248,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
248
248
  "gdpr": Object.assign({}, PROFILES["balanced"], {
249
249
  forensicSnippetBytes: C.BYTES.bytes(128),
250
250
  }),
251
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
251
+ "soc2": Object.assign({}, PROFILES["strict"], {
252
252
  forensicSnippetBytes: C.BYTES.bytes(512),
253
253
  }),
254
254
  });
package/lib/guard-csv.js CHANGED
@@ -47,7 +47,7 @@
47
47
  * validation.
48
48
  * - Profiles: strict (OWASP-aligned, prefix-tab default per OWASP) /
49
49
  * balanced / permissive / email-attachment.
50
- * - Compliance postures: hipaa / pci-dss / gdpr / soc2-cc7.
50
+ * - Compliance postures: hipaa / pci-dss / gdpr / soc2.
51
51
  * - Operator extensibility: profile composition, custom rules, hooks
52
52
  * (beforeCheck / afterCheck / onIssue / onSanitize / onRefuse /
53
53
  * onAudit), threat-intel feeds, sandbox isolation, snapshot tests.
@@ -275,7 +275,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
275
275
  piiPolicy: "redact",
276
276
  forensicSnippetBytes: FORENSIC_SNIPPET_GDPR,
277
277
  },
278
- "soc2-cc7": {
278
+ "soc2": {
279
279
  formulaInjectionPolicy: "prefix-tab",
280
280
  bidiCharPolicy: "reject",
281
281
  controlCharPolicy: "reject",
@@ -265,7 +265,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
265
265
  "gdpr": Object.assign({}, PROFILES["balanced"], {
266
266
  forensicSnippetBytes: C.BYTES.bytes(128),
267
267
  }),
268
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
268
+ "soc2": Object.assign({}, PROFILES["strict"], {
269
269
  forensicSnippetBytes: C.BYTES.bytes(512),
270
270
  }),
271
271
  });
@@ -234,7 +234,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
234
234
  leadingTrailingPolicy: "strip",
235
235
  forensicSnippetBytes: C.BYTES.bytes(128),
236
236
  },
237
- "soc2-cc7": {
237
+ "soc2": {
238
238
  bidiPolicy: "reject", controlPolicy: "reject", nullBytePolicy: "reject",
239
239
  zeroWidthPolicy: "reject", traversalPolicy: "reject",
240
240
  reservedCharPolicy: "reject", reservedNamePolicy: "reject",
package/lib/guard-html.js CHANGED
@@ -351,7 +351,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
351
351
  mxssHintPolicy: "audit",
352
352
  forensicSnippetBytes: C.BYTES.bytes(128),
353
353
  },
354
- "soc2-cc7": {
354
+ "soc2": {
355
355
  allowedTags: STRICT_ALLOWED_TAGS,
356
356
  bidiPolicy: "reject",
357
357
  controlPolicy: "reject",
package/lib/guard-json.js CHANGED
@@ -204,7 +204,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
204
204
  "gdpr": Object.assign({}, PROFILES["balanced"], {
205
205
  forensicSnippetBytes: C.BYTES.bytes(128),
206
206
  }),
207
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
207
+ "soc2": Object.assign({}, PROFILES["strict"], {
208
208
  forensicSnippetBytes: C.BYTES.bytes(512),
209
209
  }),
210
210
  });
@@ -213,7 +213,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
213
213
  "gdpr": Object.assign({}, PROFILES["balanced"], {
214
214
  forensicSnippetBytes: C.BYTES.bytes(128),
215
215
  }),
216
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
216
+ "soc2": Object.assign({}, PROFILES["strict"], {
217
217
  forensicSnippetBytes: C.BYTES.bytes(512),
218
218
  }),
219
219
  });
package/lib/guard-svg.js CHANGED
@@ -327,7 +327,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
327
327
  allowAnimation: false,
328
328
  forensicSnippetBytes: C.BYTES.bytes(128),
329
329
  },
330
- "soc2-cc7": {
330
+ "soc2": {
331
331
  allowedTags: STRICT_ALLOWED_TAGS,
332
332
  bidiPolicy: "reject",
333
333
  controlPolicy: "reject",
package/lib/guard-xml.js CHANGED
@@ -143,7 +143,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
143
143
  "gdpr": Object.assign({}, PROFILES["balanced"], {
144
144
  forensicSnippetBytes: C.BYTES.bytes(128),
145
145
  }),
146
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
146
+ "soc2": Object.assign({}, PROFILES["strict"], {
147
147
  forensicSnippetBytes: C.BYTES.bytes(512),
148
148
  }),
149
149
  });
package/lib/guard-yaml.js CHANGED
@@ -183,7 +183,7 @@ var COMPLIANCE_POSTURES = Object.freeze({
183
183
  "gdpr": Object.assign({}, PROFILES["balanced"], {
184
184
  forensicSnippetBytes: C.BYTES.bytes(128),
185
185
  }),
186
- "soc2-cc7": Object.assign({}, PROFILES["strict"], {
186
+ "soc2": Object.assign({}, PROFILES["strict"], {
187
187
  forensicSnippetBytes: C.BYTES.bytes(512),
188
188
  }),
189
189
  });
package/lib/mail-dkim.js CHANGED
@@ -358,11 +358,62 @@ function create(opts) {
358
358
  };
359
359
  }
360
360
 
361
+ // dualSigner — RFC 8463 §3 transition signer. Produces messages with
362
+ // BOTH a legacy RSA-SHA-256 DKIM-Signature AND an Ed25519-SHA-256
363
+ // DKIM-Signature header. Receivers that don't yet support Ed25519
364
+ // validate the RSA signature; receivers that prefer Ed25519 validate
365
+ // the post-quantum-friendlier signature. The transition pattern is
366
+ // the recommended path for moving the operator's domain off RSA-SHA-
367
+ // 256 without breaking older verifiers.
368
+ //
369
+ // var dual = b.mail.dkim.dualSigner({
370
+ // domain: "example.com",
371
+ // rsa: { selector: "rsa1", privateKey: rsaPemKey },
372
+ // eddsa: { selector: "eddsa1", privateKey: ed25519PemKey },
373
+ // // every other create() opt is shared (canonicalization,
374
+ // // headersToSign, audit) but can be overridden per algorithm.
375
+ // });
376
+ // var signed = dual.sign(rfc822Wire);
377
+ // // → wire with two DKIM-Signature: headers (RSA first, Ed25519 second)
378
+ //
379
+ // Both signers are constructed eagerly at create-time (configuration
380
+ // errors surface at boot, not at first send). The combined sign()
381
+ // applies the RSA signer first, then the Ed25519 signer on top.
382
+ function dualSigner(opts) {
383
+ if (!opts || !opts.rsa || !opts.eddsa) {
384
+ throw new DkimError("dkim/dual-signer-missing",
385
+ "dualSigner requires both opts.rsa and opts.eddsa");
386
+ }
387
+ if (!opts.domain) {
388
+ throw new DkimError("dkim/dual-signer-missing-domain",
389
+ "dualSigner requires opts.domain");
390
+ }
391
+ function _merge(base, alg, override) {
392
+ return Object.assign({}, base, { algorithm: alg }, override);
393
+ }
394
+ var sharedBase = {};
395
+ var commonKeys = ["domain", "headersToSign", "canonicalization", "audit"];
396
+ for (var i = 0; i < commonKeys.length; i += 1) {
397
+ if (opts[commonKeys[i]] !== undefined) sharedBase[commonKeys[i]] = opts[commonKeys[i]];
398
+ }
399
+ var rsaSigner = create(_merge(sharedBase, "rsa-sha256", opts.rsa));
400
+ var eddsaSigner = create(_merge(sharedBase, "ed25519-sha256", opts.eddsa));
401
+ return {
402
+ sign: function (rfc822) {
403
+ var afterRsa = rsaSigner.sign(rfc822);
404
+ return eddsaSigner.sign(afterRsa);
405
+ },
406
+ rsa: rsaSigner,
407
+ eddsa: eddsaSigner,
408
+ };
409
+ }
410
+
361
411
  // Test-only exports for unit testing the canonicalization primitives
362
412
  // directly without going through a full sign() round.
363
413
  module.exports = {
364
- create: create,
365
- DkimError: DkimError,
414
+ create: create,
415
+ dualSigner: dualSigner,
416
+ DkimError: DkimError,
366
417
  _canonHeaderRelaxedForTest: _canonHeaderRelaxed,
367
418
  _canonBodyRelaxedForTest: _canonBodyRelaxed,
368
419
  _canonBodySimpleForTest: _canonBodySimple,
package/lib/pick.js ADDED
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * b.pick — mass-assignment (CWE-915 / OWASP API3:2023) defense.
4
+ *
5
+ * The vulnerability: a route accepting `JSON` body and passing
6
+ * `req.body` straight to a DB write lets an attacker include fields
7
+ * the operator never intended (`isAdmin`, `passwordHash`, `userId`).
8
+ * This primitive is the operator's allowlist of acceptable fields —
9
+ * pass req.body through it before persisting.
10
+ *
11
+ * var safeUserUpdate = b.pick(req.body, [
12
+ * "displayName", "bio", "avatarUrl",
13
+ * ]);
14
+ * await db.users.update(userId, safeUserUpdate);
15
+ *
16
+ * Returns a NEW object containing only the keys in the allowlist.
17
+ * Keys not present in the input are simply absent from the output —
18
+ * no defaults filled in, no `undefined` values. Prototype-pollution
19
+ * keys (`__proto__` / `constructor` / `prototype`) are ALWAYS
20
+ * stripped, even if the operator accidentally lists them.
21
+ *
22
+ * var partial = b.pick(req.body, ["a", "b"], { onUnknown: "throw" });
23
+ * // throws if req.body has any key NOT in ["a", "b"]
24
+ *
25
+ * var nested = b.pick(req.body, [
26
+ * "name",
27
+ * ["profile", ["bio", "url"]], // nested allowlist for `profile.*`
28
+ * ]);
29
+ */
30
+
31
+ var POISONED_KEYS = ["__proto__", "constructor", "prototype"];
32
+
33
+ function _isPlainObject(o) {
34
+ return o !== null && typeof o === "object" && !Array.isArray(o) &&
35
+ (Object.getPrototypeOf(o) === Object.prototype ||
36
+ Object.getPrototypeOf(o) === null);
37
+ }
38
+
39
+ function _normalizeAllowList(list) {
40
+ // Accept either ["a","b"] or [["nested",["sub1","sub2"]]] — return
41
+ // a Map<key, allowList | true>.
42
+ var out = Object.create(null);
43
+ for (var i = 0; i < list.length; i += 1) {
44
+ var entry = list[i];
45
+ if (typeof entry === "string") {
46
+ if (POISONED_KEYS.indexOf(entry) !== -1) continue;
47
+ out[entry] = true;
48
+ } else if (Array.isArray(entry) && entry.length === 2 &&
49
+ typeof entry[0] === "string" && Array.isArray(entry[1])) {
50
+ if (POISONED_KEYS.indexOf(entry[0]) !== -1) continue;
51
+ out[entry[0]] = _normalizeAllowList(entry[1]);
52
+ } else {
53
+ throw new TypeError(
54
+ "b.pick: allowlist entry must be a string or [name, [...]]; got " +
55
+ JSON.stringify(entry));
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function _pickInner(input, normalized, onUnknown, path) {
62
+ if (!_isPlainObject(input)) {
63
+ return _isPlainObject(input) ? {} : input;
64
+ }
65
+ var output = Object.create(null);
66
+ var keys = Object.keys(input);
67
+ for (var i = 0; i < keys.length; i += 1) {
68
+ var k = keys[i];
69
+ if (POISONED_KEYS.indexOf(k) !== -1) continue;
70
+ if (!Object.prototype.hasOwnProperty.call(normalized, k)) {
71
+ if (onUnknown === "throw") {
72
+ throw new TypeError(
73
+ "b.pick: unknown key '" + (path ? path + "." : "") + k +
74
+ "' not in allowlist");
75
+ }
76
+ continue;
77
+ }
78
+ var rule = normalized[k];
79
+ if (rule === true) {
80
+ output[k] = input[k];
81
+ } else {
82
+ // Nested allowlist.
83
+ output[k] = _isPlainObject(input[k])
84
+ ? _pickInner(input[k], rule, onUnknown, (path ? path + "." : "") + k)
85
+ : input[k];
86
+ }
87
+ }
88
+ // Convert to a plain Object (output is currently null-prototype) so
89
+ // downstream JSON serializers / DB drivers see a normal-shape object.
90
+ return Object.assign({}, output);
91
+ }
92
+
93
+ function pick(input, allowList, opts) {
94
+ opts = opts || {};
95
+ if (!Array.isArray(allowList)) {
96
+ throw new TypeError("b.pick: second argument must be an array of allowed keys");
97
+ }
98
+ var onUnknown = opts.onUnknown === "throw" ? "throw" : "drop";
99
+ var normalized = _normalizeAllowList(allowList);
100
+ return _pickInner(input, normalized, onUnknown, "");
101
+ }
102
+
103
+ module.exports = pick;
104
+ module.exports.pick = pick;
105
+ module.exports.POISONED_KEYS = Object.freeze(POISONED_KEYS.slice());
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * safe-redirect — open-redirect (CWE-601) defense for operator-supplied
4
+ * post-login `?next=` / `?return_to=` parameters and similar redirect
5
+ * targets.
6
+ *
7
+ * The vulnerability: an attacker phishes a victim with a link like
8
+ * `https://app.example.com/login?next=https://attacker.example.com`.
9
+ * After login, a naive `res.writeHead(302, { Location: req.query.next })`
10
+ * sends the user to attacker.example.com under the trust of app.example.com.
11
+ *
12
+ * var safe = b.safeRedirect.resolve(rawNext, {
13
+ * base: "https://app.example.com",
14
+ * allowedOrigins: ["https://app.example.com"],
15
+ * allowedHosts: ["app.example.com"],
16
+ * fallback: "/dashboard",
17
+ * });
18
+ * // → safe path or fallback (never attacker.example.com)
19
+ *
20
+ * Decision rules (in order):
21
+ *
22
+ * 1. rawTarget is null / empty / non-string → fallback
23
+ * 2. rawTarget starts with "//" or "\\" → fallback (protocol-relative
24
+ * open redirect — `//attacker.com/path` interpreted as
25
+ * `https://attacker.com/path` by browsers)
26
+ * 3. rawTarget contains a control char / null / CR / LF → fallback
27
+ * (header-injection vector)
28
+ * 4. rawTarget is a relative path starting with "/" → safe (same-
29
+ * origin by definition)
30
+ * 5. rawTarget is a fragment / search-only ("#x" / "?q=1") → safe
31
+ * 6. rawTarget is a full URL → parse + check origin against
32
+ * allowedOrigins (or host against allowedHosts when the operator
33
+ * doesn't care about scheme/port match)
34
+ * 7. anything else (data:, javascript:, malformed) → fallback
35
+ *
36
+ * Returns the safe URL string (path + query + fragment for relative;
37
+ * full URL for allowed full URLs; fallback otherwise). Operators
38
+ * pass the result directly to `res.writeHead(302, { Location: ... })`.
39
+ */
40
+
41
+ var safeUrl = require("./safe-url");
42
+ var validateOpts = require("./validate-opts");
43
+
44
+ var DEFAULT_FALLBACK = "/";
45
+
46
+ function _hasControlChar(s) {
47
+ for (var i = 0; i < s.length; i += 1) {
48
+ var c = s.charCodeAt(i);
49
+ if (c < 0x20 || c === 0x7f) return true; // allow:raw-byte-literal — ASCII control range thresholds
50
+ }
51
+ return false;
52
+ }
53
+
54
+ function resolve(rawTarget, opts) {
55
+ opts = opts || {};
56
+ validateOpts(opts, ["base", "allowedOrigins", "allowedHosts", "fallback"], "safeRedirect.resolve");
57
+
58
+ var fallback = typeof opts.fallback === "string" ? opts.fallback : DEFAULT_FALLBACK;
59
+ if (typeof rawTarget !== "string" || rawTarget.length === 0) return fallback;
60
+ if (_hasControlChar(rawTarget)) return fallback;
61
+
62
+ // Reject protocol-relative ("//host/...") and back-slash variant
63
+ // ("\\host\..." — IE / older browsers may interpret as auth).
64
+ if (rawTarget.length >= 2) {
65
+ var p0 = rawTarget.charAt(0);
66
+ var p1 = rawTarget.charAt(1);
67
+ if ((p0 === "/" || p0 === "\\") && (p1 === "/" || p1 === "\\")) return fallback;
68
+ }
69
+
70
+ // Same-origin relative (path / query / fragment) — safe by definition.
71
+ if (rawTarget.charAt(0) === "/" || rawTarget.charAt(0) === "?" ||
72
+ rawTarget.charAt(0) === "#") {
73
+ return rawTarget;
74
+ }
75
+
76
+ // Full URL — parse and check against allowlist.
77
+ var allowedOrigins = Array.isArray(opts.allowedOrigins) ? opts.allowedOrigins : null;
78
+ var allowedHosts = Array.isArray(opts.allowedHosts) ? opts.allowedHosts : null;
79
+ if (!allowedOrigins && !allowedHosts) {
80
+ // Operator gave no allowlist — refuse all full URLs (the safe default).
81
+ return fallback;
82
+ }
83
+
84
+ var parsed;
85
+ try { parsed = safeUrl.parse(rawTarget, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS }); }
86
+ catch (_e) { return fallback; }
87
+
88
+ if (allowedOrigins) {
89
+ for (var i = 0; i < allowedOrigins.length; i += 1) {
90
+ if (parsed.origin === allowedOrigins[i]) return rawTarget;
91
+ }
92
+ }
93
+ if (allowedHosts) {
94
+ for (var j = 0; j < allowedHosts.length; j += 1) {
95
+ if (parsed.host === allowedHosts[j] || parsed.hostname === allowedHosts[j]) {
96
+ return rawTarget;
97
+ }
98
+ }
99
+ }
100
+ return fallback;
101
+ }
102
+
103
+ module.exports = {
104
+ resolve: resolve,
105
+ DEFAULT_FALLBACK: DEFAULT_FALLBACK,
106
+ };
package/lib/webhook.js CHANGED
@@ -198,17 +198,37 @@ function _hmacVerify(key, data, expectedHex) {
198
198
  return crypto.timingSafeEqual(actualHex, expectedHex);
199
199
  }
200
200
 
201
+ // PQC signatures encode as base64url. SLH-DSA-SHAKE-256f signatures
202
+ // are ~29.5 KB binary → ~59 KB hex but only ~40 KB base64url. The hex
203
+ // form blew past common front-end limits (nginx default 8 KB / Cloudflare
204
+ // default 16 KB / many CDN edge limits 32 KB). base64url keeps the
205
+ // signature in-header for the bulk of operators while still allowing
206
+ // body-bound signatures (operator passes the wire-encoded sig in body
207
+ // when even base64url is too large for their topology).
208
+ //
209
+ // Verification accepts EITHER encoding for a transition window: a
210
+ // base64url-shaped value is decoded as base64url; otherwise a hex-
211
+ // shaped value is decoded as hex. New signatures are emitted as
212
+ // base64url; old hex-encoded signatures still verify.
201
213
  function _pqcSign(privateKeyPem, data) {
202
- return crypto.sign(data, privateKeyPem).toString("hex");
214
+ return crypto.sign(data, privateKeyPem).toString("base64url");
203
215
  }
204
216
 
205
- function _pqcVerify(publicKeyPem, data, expectedHex) {
206
- if (!safeBuffer.isHex(expectedHex) || (expectedHex.length % 2) !== 0) {
207
- return false;
208
- }
217
+ var _BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
218
+
219
+ function _pqcVerify(publicKeyPem, data, expectedSig) {
220
+ if (typeof expectedSig !== "string" || expectedSig.length === 0) return false;
209
221
  var sigBuf;
210
- try { sigBuf = Buffer.from(expectedHex, "hex"); }
211
- catch (_e) { return false; }
222
+ try {
223
+ if (_BASE64URL_RE.test(expectedSig) && // allow:regex-no-length-cap sig length bounded by header parser cap
224
+ !/^[0-9a-f]+$/.test(expectedSig)) { // allow:regex-no-length-cap — same
225
+ sigBuf = Buffer.from(expectedSig, "base64url");
226
+ } else if (safeBuffer.isHex(expectedSig) && (expectedSig.length % 2) === 0) {
227
+ sigBuf = Buffer.from(expectedSig, "hex");
228
+ } else {
229
+ return false;
230
+ }
231
+ } catch (_e) { return false; }
212
232
  try { return crypto.verify(data, sigBuf, publicKeyPem); }
213
233
  catch (_e) { return false; }
214
234
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.20",
3
+ "version": "0.7.22",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:be5934ef-c4b9-4516-a8ee-b8bb2c31ee26",
5
+ "serialNumber": "urn:uuid:30b4f7c6-d648-478a-aa77-dbbfb61c7951",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T05:52:29.844Z",
8
+ "timestamp": "2026-05-05T06:18:09.475Z",
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.7.20",
22
+ "bom-ref": "@blamejs/core@0.7.22",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.20",
25
+ "version": "0.7.22",
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.7.20",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.22",
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.7.20",
57
+ "ref": "@blamejs/core@0.7.22",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]