@blamejs/core 0.13.26 → 0.13.28
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 +4 -0
- package/lib/calendar.js +14 -10
- package/lib/cookies.js +2 -2
- package/lib/file-upload.js +18 -1
- package/lib/mail-bimi.js +3 -2
- package/lib/mail-crypto-pgp.js +7 -11
- package/lib/mail-crypto-smime.js +6 -5
- package/lib/mail-crypto.js +5 -5
- package/lib/mail-store.js +3 -2
- package/lib/middleware/api-encrypt.js +3 -3
- package/lib/middleware/csp-nonce.js +2 -2
- package/lib/middleware/require-auth.js +2 -1
- package/lib/queue-redis.js +9 -0
- package/lib/router.js +4 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.28 (2026-05-28) — **Queue retry backoff now applies on the Redis backend; static-serve path-containment edge closed.** Two behavioral fixes plus doc corrections. The Redis queue backend silently discarded the documented retry backoff: b.queue.consume passes the delay as `{ retryDelayMs }` (the shape the local backend reads), but the Redis backend's fail() accepted only a bare-number third argument, so the object failed its numeric check and the delay was forced to 0 — a failing job re-leased immediately instead of waiting 1s/2s/4s/…, a retry storm under failure. The Redis backend now accepts the object form, so the exponential backoff applies as documented (verified by an integration test against real Redis). Separately, b.router.serveStatic's path-containment check used a bare string prefix, so a sibling directory whose name extends the root (root `/srv/public` vs `/srv/public-evil`) could pass; it now anchors on a path separator. Also: b.fileUpload now surfaces (via an observability counter) when a configured content-safety gate is skipped because an upload streamed past the reassembly cap, and documents that boundary; and b.cookies.parse's example output is corrected. **Fixed:** *Redis queue backend honors the documented retry backoff* — `b.queue.consume` re-pends a failed job with deterministic exponential backoff (1s base, 5min cap) by calling the backend's `fail()` with `{ retryDelayMs }`. The Redis backend's `fail()` accepted only a bare-number third argument, so the object failed its `typeof === "number"` check and the delay was reset to 0 — a failing job became immediately re-leasable, hot-looping instead of backing off. `fail()` now accepts both the object form (as the local backend does) and a bare number, so the backoff applies on Redis. An integration test against real Redis pins it. · *`b.router.serveStatic` path-containment anchors on a separator* — The containment check was `resolvedPath.startsWith(root)`, which a sibling directory sharing the root's name as a prefix (root `/srv/public` vs `/srv/public-evil`) could satisfy. It now requires the resolved path to equal `root` or start with `root + path.sep`, closing the sibling-prefix edge (`b.staticServe.create` remains the hardened serving path, with realpath + filename gating). · *`b.fileUpload` surfaces content-safety gate skips on oversized streamed uploads* — The byte-level content-safety gate inspects the reassembled buffer, so it runs on uploads up to `maxStreamReassemblyBytes` (default 64 MiB); a larger upload is handed to `onFinalize` as a stream and the byte-content gate is skipped (MIME-sniff and filename gates still run). That skip now emits a `fileUpload.content_safety_skipped_streamed` observability counter instead of passing silently, and the limit is documented. To guarantee content-gating of a type, cap `maxFileBytes` at or below `maxStreamReassemblyBytes`. · *`b.cookies.parse` example output corrected* — The example claimed `theme=%22dark%22` parses to `theme: "dark"`, but quote-stripping runs before percent-decoding, so the literal quotes survive. The example now uses `theme=dark%20mode` → `theme: "dark mode"`, which demonstrates percent-decoding without the quote-strip-ordering quirk.
|
|
12
|
+
|
|
13
|
+
- v0.13.27 (2026-05-28) — **Documentation corrected across api-encrypt, mail-crypto, mail-store, and calendar.** A set of JSDoc corrections where the documented contract had drifted from the code. The most actionable: b.middleware.apiEncrypt's @opts named the keypair fields secretKey / ecSecretKey, but the middleware requires privateKey / ecPrivateKey (the shape b.crypto.generateEncryptionKeyPair returns), so a keypair built from the docs threw INVALID_KEYPAIR at construction; the same block documented a wrong custom-nonceStore interface and an example calling a non-existent b.crypto.keypair(). Also corrected: the b.mail.crypto facade and the PGP module described S/MIME sign/verify and PGP encrypt/decrypt/WKD as deferred when they ship and are live; b.mail.crypto.smime.checkCert documented a return shape whose field names did not match what it returns; and b.mailStore.create listed a destroy method it does not expose. No code behavior changed — only the docs were wrong. **Fixed:** *`b.middleware.apiEncrypt` options documented with the correct field names* — The `@opts` listed the keypair as `{ publicKey, secretKey, ecPublicKey, ecSecretKey }`, but the middleware requires `{ publicKey, privateKey, ecPublicKey, ecPrivateKey }` — the shape `b.crypto.generateEncryptionKeyPair()` returns — and threw `INVALID_KEYPAIR` for the documented shape. The custom `nonceStore` interface was documented as `{ has, add, prune }` but the middleware calls `{ checkAndInsert, purgeExpired, close }`, and the example called a non-existent `b.crypto.keypair()`. All three now match the implementation (`b.crypto.generateEncryptionKeyPair()`). · *`b.mail.crypto` S/MIME and PGP availability described accurately* — The `b.mail.crypto` facade said S/MIME `sign()` / `verify()` were deferred, and the PGP module's intro said in-process encrypt / decrypt and WKD discovery would 'ship in v0.10.14'. All of these are implemented and live (PGP encrypt/decrypt/WKD were promoted to the stable surface in v0.11.32; S/MIME sign/verify/verifyAll run on the `b.cms` substrate). The docs now describe them as available; the genuinely-deferred PGP v6-signature-packet support remains noted as deferred. · *`b.mail.crypto.smime.checkCert` return shape documented correctly* — The doc and example described the result as `{ subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }`; the function returns `{ subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }` (full DN strings, no key-size field). The documented shape now matches. · *`b.mailStore.create` method list matches the returned handle* — The doc listed a `destroy` method the handle does not expose and omitted `search` / `moveMessages` / `hardExpunge` that it does. The list now reflects the actual methods. · *Smaller doc corrections* — `b.calendar` fromIcal / toIcal / validate now document that they also handle Task (VTODO), Note (VJOURNAL), and Group, not just Event. `b.middleware.requireAuth`'s `prefersJson` default is documented as Accept / X-Requested-With only (Content-Type is intentionally not a signal, as the module already noted). The default CSP nonce is 24 base64 chars (16 bytes), not 22. `b.mail.bimi`'s returned `evidenceDocument` is noted as echoed from the operator-supplied option rather than pulled from the certificate.
|
|
14
|
+
|
|
11
15
|
- v0.13.26 (2026-05-28) — **`b.cryptoField.unsealRow` nulls a sealed column on unseal failure instead of returning the forged ciphertext.** When a sealed column failed to unseal — a DB-write attacker's forged `vault:<…>` payload, or a valid ciphertext copied into a different row so the AAD no longer matches — unsealRow recorded the failure on the audit chain but then kept the original attacker-crafted string in the field rather than nulling it, despite the documented contract that downstream sees 'no value'. A write-back guard discarded the intended null on the failure path. The column is now nulled on any unseal failure, so a forged or cross-row-copied value never reaches downstream code as if it were a real plaintext. Valid values round-trip unchanged and genuinely-unsealed pass-through values are still kept. This hardens every sealed-column reader, including the agent idempotency / orchestrator / tenant rows sealed in 0.13.25. **Fixed:** *Audit checkpoint docs name the actual signature algorithm* — `b.audit.checkpoint` / `b.audit.verifyCheckpoints` described the anchor signature as ML-DSA-87, but the checkpoint is signed with the configured `b.auditSign` algorithm — SLH-DSA-SHAKE-256f by default (ML-DSA-87 / ML-DSA-65 are opt-in). The docs and the verify-failure reason now refer to the post-quantum signature without naming a specific algorithm the operator may not be using. · *`b.storage.chunkScratch` example and assembly description corrected* — The `assemble()` example omitted the mandatory `chunkEncryptionKeys` argument (one sealed key per chunk, returned by `saveChunk`), so it would have thrown as written; it now collects and passes the keys. The prose no longer claims the primitive writes a final file with an 'atomic finalize' — `assemble()` concatenates the chunks in order and returns the assembled bytes for the caller to persist. **Security:** *Sealed columns are nulled on unseal failure (forged / cross-row ciphertext)* — `b.cryptoField.unsealRow` now nulls a sealed field when its value fails to unseal — a crafted `vault:`/`vault.aad:` payload written by a DB-write attacker, or a valid ciphertext copied to a different row (AAD mismatch). Previously the field kept the attacker-controlled string, so downstream code could read the forged ciphertext as if it were the plaintext. The audit emit (`system.crypto.unseal_failed`) is unchanged. Valid round-trips and not-actually-sealed pass-through values are unaffected. A regression test pins the forged-value, cross-row-copy, and pass-through cases.
|
|
12
16
|
|
|
13
17
|
- v0.13.25 (2026-05-28) — **Agent idempotency results and orchestrator/tenant registry rows are sealed at rest.** The b.agent.idempotency, b.agent.orchestrator, and b.agent.tenant primitives documented their stored rows as sealed at rest, but the values were written as plaintext JSON — a database dump could expose cached result payloads (which can carry mail-move / search data), the tenant ids that own each agent, and operator-supplied endpoint metadata. Those values are now sealed via b.cryptoField (XChaCha20-Poly1305 through the vault) before they reach the backing store and unsealed on read, when a vault is configured — which is the default in a booted app. Each ciphertext is AAD-bound to its row identity so a database-write attacker cannot copy a sealed value between rows. Reads of rows written before this release (plain JSON) continue to work unchanged, and a vault-less deployment stores rows as before. No API or call-site changes are required. **Fixed:** *`b.agent.saga` run() return/throw shape documented correctly* — The doc said `run()` resolves to `{ status: "failed", failedStep, lastCompensationError }` on failure and to a bare final state. In fact it resolves to `{ status: "completed", sagaId, state }` on success and rejects (throws) on step failure with an error carrying `failedStep`, `cause`, `compensationCause`, and `failedCompStepName`. The docs now describe the actual contract. · *`b.agent.idempotency` put() example uses the real option name* — The example passed `{ argsFingerprint: ... }`, which the function does not read; the fingerprint option is `requestFingerprint` (or `args`). The example now uses `requestFingerprint`. · *`b.agent.tenant.derivedKey` @since corrected to 0.9.26* — It was tagged `@since 0.9.25`, a version before the tenant module existed; it ships in 0.9.26 alongside `b.agent.tenant.create`. · *`b.agent.trace.injectIntoEnvelope` documented as single-argument* — The doc listed a second `currentSpan` argument the function ignores — it always injects the currently-active span's trace context. The doc now shows `injectIntoEnvelope(envelope)` and notes it should be called while the intended span is active. **Security:** *Agent idempotency / orchestrator / tenant rows sealed at rest* — `b.agent.idempotency` cached result blobs, `b.agent.orchestrator` registry rows (owning tenant id + endpoint metadata), and `b.agent.tenant` registry-row metadata are now sealed via `b.cryptoField` when a vault is configured (the default in a booted app via `b.start`). The fields were previously stored as plaintext despite the docs describing them as sealed, so a DB dump exposed cached payloads, agent↔tenant ownership, and endpoint detail. Each sealed value is AAD-bound to the row identity (the idempotency key hash / the agent name / the tenant id), so a sealed value cannot be copied between rows. Rows written before this release remain readable (a non-sealed value passes through unseal), and a vault-less deployment behaves as before. No call-site changes.
|
package/lib/calendar.js
CHANGED
|
@@ -107,8 +107,9 @@ var MAX_EXPAND_SPAN_MS = 10 * 365 * 24 * 60 * 60 * 1000;
|
|
|
107
107
|
* @since 0.11.31
|
|
108
108
|
* @status stable
|
|
109
109
|
*
|
|
110
|
-
* Validate a JSCalendar Event / Task object's
|
|
111
|
-
* RFC 8984 §5 (Event)
|
|
110
|
+
* Validate a JSCalendar Event / Task / Note / Group object's
|
|
111
|
+
* required-field shape per RFC 8984 §5 (Event) / §6 (Task) / §7 (Note) /
|
|
112
|
+
* §1.4.4 (Group). Returns the input on success; throws
|
|
112
113
|
* `CalendarError` on refusal with a `.code` naming the specific shape
|
|
113
114
|
* rule that failed.
|
|
114
115
|
*
|
|
@@ -337,10 +338,11 @@ function validate(jsCal) {
|
|
|
337
338
|
* @since 0.11.31
|
|
338
339
|
* @status stable
|
|
339
340
|
*
|
|
340
|
-
* Parse iCalendar text (RFC 5545) via `b.safeIcal.parse` and map
|
|
341
|
-
*
|
|
342
|
-
* a single
|
|
343
|
-
* an array
|
|
341
|
+
* Parse iCalendar text (RFC 5545) via `b.safeIcal.parse` and map each
|
|
342
|
+
* VEVENT → JSCalendar Event, VTODO → Task, and VJOURNAL → Note
|
|
343
|
+
* (RFC 8984 §5 / §6 / §7). Returns a single object when the VCALENDAR
|
|
344
|
+
* holds exactly one component, or an array across all components when
|
|
345
|
+
* there are several.
|
|
344
346
|
*
|
|
345
347
|
* @opts
|
|
346
348
|
* safeIcalOpts: object, // forwarded to b.safeIcal.parse (caps, allowExperimental, etc.)
|
|
@@ -381,10 +383,12 @@ function fromIcal(text, opts) {
|
|
|
381
383
|
* @since 0.11.31
|
|
382
384
|
* @status stable
|
|
383
385
|
*
|
|
384
|
-
* Render a JSCalendar
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
386
|
+
* Render a JSCalendar object back to RFC 5545 iCalendar text — Event →
|
|
387
|
+
* VEVENT, Task → VTODO, Note → VJOURNAL, and a Group to a VCALENDAR
|
|
388
|
+
* carrying each member component. Returns a CRLF-terminated string
|
|
389
|
+
* wrapped in a `BEGIN:VCALENDAR / VERSION:2.0 /
|
|
390
|
+
* PRODID:-//blamejs//Calendar//EN / … / END:VCALENDAR` envelope per
|
|
391
|
+
* RFC 5545 §3.4.
|
|
388
392
|
*
|
|
389
393
|
* @opts
|
|
390
394
|
* prodid: string, // PRODID value to emit; default "-//blamejs//Calendar//EN"
|
package/lib/cookies.js
CHANGED
|
@@ -127,8 +127,8 @@ function _scrubAttr(s) {
|
|
|
127
127
|
* `parseSafe`.
|
|
128
128
|
*
|
|
129
129
|
* @example
|
|
130
|
-
* var jar = b.cookies.parse("session=abc; theme
|
|
131
|
-
* // → { session: "abc", theme: "dark" }
|
|
130
|
+
* var jar = b.cookies.parse("session=abc; theme=dark%20mode");
|
|
131
|
+
* // → { session: "abc", theme: "dark mode" } // percent-decoded
|
|
132
132
|
*/
|
|
133
133
|
function parse(cookieHeader) {
|
|
134
134
|
var out = {};
|
package/lib/file-upload.js
CHANGED
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* for content gating and `b.guardFilename.gate({ profile: "strict" })`
|
|
19
19
|
* for filename gating. Operators opt out via `contentSafety: null`
|
|
20
20
|
* / `filenameSafety: null` (audited at create time so a security
|
|
21
|
-
* review can find the disabled-on-deploy rows).
|
|
21
|
+
* review can find the disabled-on-deploy rows). The byte-level
|
|
22
|
+
* content gate inspects the reassembled buffer, so it runs on uploads
|
|
23
|
+
* up to `maxStreamReassemblyBytes` (default 64 MiB); a larger upload
|
|
24
|
+
* is handed to `onFinalize` as a stream and the byte-content gate is
|
|
25
|
+
* skipped (MIME-sniff + filename gates still run, and the skip emits a
|
|
26
|
+
* `fileUpload.content_safety_skipped` warning audit). To guarantee
|
|
27
|
+
* content-gating of a type, cap `maxFileBytes` at or below
|
|
28
|
+
* `maxStreamReassemblyBytes`. Per-chunk hooks
|
|
22
29
|
* (`onChunk`) are the integration point for virus scanners and
|
|
23
30
|
* schema-shape checks; rejecting from the hook surfaces as a
|
|
24
31
|
* permanent `FileUploadError`.
|
|
@@ -1076,6 +1083,16 @@ function create(opts) {
|
|
|
1076
1083
|
// Clear the streaming alias if present — sanitized fits in memory.
|
|
1077
1084
|
bodyStream = null;
|
|
1078
1085
|
}
|
|
1086
|
+
} else if (safetyGate && typeof safetyGate.check === "function" && !bodyBuffer) {
|
|
1087
|
+
// A content-safety gate is configured for this extension, but the
|
|
1088
|
+
// upload streamed past maxStreamReassemblyBytes and was never
|
|
1089
|
+
// reassembled into a buffer the byte-level gate can inspect. The
|
|
1090
|
+
// MIME-sniff and filename gates still ran; the per-extension
|
|
1091
|
+
// content gate did NOT. Surface it (rather than skipping silently)
|
|
1092
|
+
// via an observability counter so operators can alert, lower
|
|
1093
|
+
// maxStreamReassemblyBytes, or cap maxFileBytes to force
|
|
1094
|
+
// content-gating of this type.
|
|
1095
|
+
_emitObs("fileUpload.content_safety_skipped_streamed", 1, { ext: safetyExt });
|
|
1079
1096
|
}
|
|
1080
1097
|
}
|
|
1081
1098
|
|
package/lib/mail-bimi.js
CHANGED
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
* subjectAltName URI matches the BIMI domain, and confirms the
|
|
35
35
|
* cert carries the BIMI mark-verification policy OID
|
|
36
36
|
* (1.3.6.1.5.5.7.3.31). The verified mark is returned as
|
|
37
|
-
* { svg, evidenceDocument } pulled from RFC 3709
|
|
38
|
-
* when present
|
|
37
|
+
* { svg, evidenceDocument } — `svg` pulled from the RFC 3709
|
|
38
|
+
* logotype extension when present, `evidenceDocument` echoed from the
|
|
39
|
+
* operator-supplied opts.evidenceDocument.
|
|
39
40
|
*
|
|
40
41
|
* `validateTinyPsSvg` enforces the AuthIndicators-WG Tiny PS subset:
|
|
41
42
|
* single root <svg>, version="1.2", baseProfile="tiny-ps", viewBox
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -57,19 +57,15 @@
|
|
|
57
57
|
* to a known operator key rather than trusting any key that
|
|
58
58
|
* happens to match the signature.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
60
|
+
* Now live (promoted to the stable top-level surface in v0.11.32):
|
|
61
61
|
* - In-process encrypt + decrypt (Message Encrypted Session Key +
|
|
62
62
|
* Symmetrically Encrypted Integrity Protected Data packets,
|
|
63
|
-
* RFC 9580 §5.1 / §5.13)
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* substrate
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* on-each-side across two patches. Cheap escape hatch (pre-
|
|
70
|
-
* v0.10.14): operators wire a third-party OpenPGP library in
|
|
71
|
-
* their own consumer code and call this module's sign() /
|
|
72
|
-
* verify() on the resulting cleartext blob.
|
|
63
|
+
* RFC 9580 §5.1 / §5.13) as `b.mail.crypto.pgp.encrypt` /
|
|
64
|
+
* `.decrypt`, and WKD key discovery (draft-koch-openpgp-webkey-
|
|
65
|
+
* service) as `b.mail.crypto.pgp.wkd` — all on the same `b.cms`
|
|
66
|
+
* substrate that backs S/MIME sign/verify.
|
|
67
|
+
*
|
|
68
|
+
* Deferred (with the documented condition for opting in):
|
|
73
69
|
* - v6 signature packets (RFC 9580 §5.2.3, packet version 6 with
|
|
74
70
|
* SHA2-512 fingerprints and salted hashes). Defer condition: v6
|
|
75
71
|
* is not yet emitted by GnuPG 2.4 LTS or by Sequoia stable, so
|
package/lib/mail-crypto-smime.js
CHANGED
|
@@ -670,14 +670,15 @@ function _wrapBase64(s) {
|
|
|
670
670
|
* Operator-side cert preflight that lights up at boot: refuses
|
|
671
671
|
* SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1
|
|
672
672
|
* as the certificate-signature algorithm. Returns the parsed cert
|
|
673
|
-
* shape
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* the cert is outside its
|
|
673
|
+
* shape: the full subject / issuer DN strings, the validity window,
|
|
674
|
+
* the signature algorithm (name + OID), the key type, and the SHA-256
|
|
675
|
+
* fingerprint. Throws `mail-crypto/smime/bad-cert` on any of the above;
|
|
676
|
+
* throws `mail-crypto/smime/expired-cert` if the cert is outside its
|
|
677
|
+
* validity window.
|
|
677
678
|
*
|
|
678
679
|
* @example
|
|
679
680
|
* var info = b.mail.crypto.smime.checkCert({ certPem: pem });
|
|
680
|
-
* // → {
|
|
681
|
+
* // → { subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }
|
|
681
682
|
*/
|
|
682
683
|
function checkCert(opts) {
|
|
683
684
|
opts = validateOpts.requireObject(opts, "mail.crypto.smime.checkCert",
|
package/lib/mail-crypto.js
CHANGED
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
* (pub-alg 1, EMSA-PKCS1-v1_5 + SHA-256, 2048-bit floor per
|
|
23
23
|
* RFC 8301).
|
|
24
24
|
* - `b.mail.crypto.smime` — S/MIME 4.0 per RFC 8551 with CMS
|
|
25
|
-
* SignedData per RFC 5652.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
25
|
+
* SignedData per RFC 5652. Surface: sign() + verify() + verifyAll()
|
|
26
|
+
* (built on the `b.cms` substrate — digest recompute + timing-safe
|
|
27
|
+
* compare + PQC signature verify + X.509 chain walk) plus
|
|
28
|
+
* checkCert(), the operator-side preflight that refuses SHA-1 /
|
|
29
|
+
* MD5 / < 2048-bit RSA certs at boot.
|
|
30
30
|
*
|
|
31
31
|
* Both sub-namespaces share `MailCryptoError` (FrameworkError
|
|
32
32
|
* subclass via defineClass with alwaysPermanent: true) so operator
|
package/lib/mail-store.js
CHANGED
|
@@ -90,8 +90,9 @@ var DEFAULT_FOLDERS = Object.freeze([
|
|
|
90
90
|
* @related b.safeMime, b.guardMessageId, b.cryptoField
|
|
91
91
|
*
|
|
92
92
|
* Build a mail-store handle. Returns an object with `appendMessage` /
|
|
93
|
-
* `fetchByObjectId` / `
|
|
94
|
-
* `
|
|
93
|
+
* `fetchByObjectId` / `search` / `queryByModseq` / `setFlags` /
|
|
94
|
+
* `createFolder` / `listFolders` / `threadFor` / `quota` /
|
|
95
|
+
* `moveMessages` / `setLegalHold` / `hardExpunge`.
|
|
95
96
|
*
|
|
96
97
|
* @opts
|
|
97
98
|
* backend: object, // required — sqlite-shaped { prepare(sql) → { run, get, all }, transaction(fn) }
|
|
@@ -250,11 +250,11 @@ function _writeRejection(res, code, body) {
|
|
|
250
250
|
*
|
|
251
251
|
* @opts
|
|
252
252
|
* {
|
|
253
|
-
* keypair: { publicKey,
|
|
253
|
+
* keypair: { publicKey, privateKey, ecPublicKey, ecPrivateKey },
|
|
254
254
|
* keypairs: [...] // multi-key rotation set; first = active
|
|
255
255
|
* replayWindowMs: number,
|
|
256
256
|
* pruneIntervalMs: number,
|
|
257
|
-
* nonceStore: {
|
|
257
|
+
* nonceStore: { checkAndInsert, purgeExpired, close },
|
|
258
258
|
* exemptPaths: string[],
|
|
259
259
|
* contentTypes: string[], // default ["application/json"]
|
|
260
260
|
* audit: boolean,
|
|
@@ -270,7 +270,7 @@ function _writeRejection(res, code, body) {
|
|
|
270
270
|
* @example
|
|
271
271
|
* var b = require("@blamejs/core");
|
|
272
272
|
* var app = b.router.create();
|
|
273
|
-
* var kp = b.crypto.
|
|
273
|
+
* var kp = b.crypto.generateEncryptionKeyPair();
|
|
274
274
|
* app.use(b.middleware.apiEncrypt({
|
|
275
275
|
* keypair: kp,
|
|
276
276
|
* replayWindowMs: 30000,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* browser refuses to execute them. This middleware closes the gap:
|
|
9
9
|
*
|
|
10
10
|
* 1. Generate a fresh random nonce per request (base64, default
|
|
11
|
-
* 16 bytes /
|
|
11
|
+
* 16 bytes / 24 chars).
|
|
12
12
|
* 2. Attach it to `req.cspNonce` (handler-readable) AND
|
|
13
13
|
* `res.locals.cspNonce` (template-data-readable — render.js
|
|
14
14
|
* auto-merges res.locals into template data).
|
|
@@ -233,7 +233,7 @@ function _injectNonce(cspHeader, nonce, directives, strictDynamic) {
|
|
|
233
233
|
* Per-request CSP nonce + render integration. Constructed via
|
|
234
234
|
* `b.middleware.cspNonce(opts)`; the resulting middleware has the
|
|
235
235
|
* `(req, res, next)` shape shown above. Generates a fresh
|
|
236
|
-
* random nonce (16 bytes /
|
|
236
|
+
* random nonce (16 bytes / 24 chars base64 by default), attaches it
|
|
237
237
|
* to `req.cspNonce` and `res.locals.cspNonce` (auto-merged into
|
|
238
238
|
* template data), and patches the existing Content-Security-Policy
|
|
239
239
|
* header to append `'nonce-XYZ'` to the configured directives
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
* when set, prefersJson()=false rejections
|
|
31
31
|
* produce 302 instead of 401 text/plain)
|
|
32
32
|
* prefersJson: function (optional override; defaults to checking
|
|
33
|
-
* Accept / X-Requested-With
|
|
33
|
+
* Accept / X-Requested-With — NOT
|
|
34
|
+
* Content-Type, see the note above)
|
|
34
35
|
* errorMessage: 'Authentication required.'
|
|
35
36
|
* audit: true (emit auth.required.denied on reject)
|
|
36
37
|
* }
|
package/lib/queue-redis.js
CHANGED
|
@@ -582,6 +582,15 @@ function create(opts) {
|
|
|
582
582
|
async function fail(jobId, errorMessage, retryDelayMs) {
|
|
583
583
|
await _ensureConnected();
|
|
584
584
|
var nowMs = Date.now();
|
|
585
|
+
// b.queue.consume passes the object form `{ retryDelayMs }` (matching
|
|
586
|
+
// the queue-local backend); accept it as well as a bare-number third
|
|
587
|
+
// arg. Without this the object failed the `typeof === "number"` test
|
|
588
|
+
// below and the delay was forced to 0, so the documented exponential
|
|
589
|
+
// backoff was silently discarded and a failing job re-leased
|
|
590
|
+
// immediately on the redis backend (retry storm).
|
|
591
|
+
if (retryDelayMs && typeof retryDelayMs === "object") {
|
|
592
|
+
retryDelayMs = retryDelayMs.retryDelayMs;
|
|
593
|
+
}
|
|
585
594
|
if (typeof retryDelayMs !== "number" || !isFinite(retryDelayMs) || retryDelayMs < 0) {
|
|
586
595
|
retryDelayMs = 0;
|
|
587
596
|
}
|
package/lib/router.js
CHANGED
|
@@ -1229,7 +1229,10 @@ function serveStatic(dir) {
|
|
|
1229
1229
|
var rel = req.pathname;
|
|
1230
1230
|
if (rel.includes("\0")) return next();
|
|
1231
1231
|
var filePath = nodePath.resolve(nodePath.join(root, rel));
|
|
1232
|
-
|
|
1232
|
+
// Anchor on `root + sep` (not a bare prefix) so a sibling directory
|
|
1233
|
+
// that shares the root's name as a prefix — e.g. root `/srv/public`
|
|
1234
|
+
// vs `/srv/public-evil` — cannot satisfy the containment check.
|
|
1235
|
+
if (filePath !== root && !filePath.startsWith(root + nodePath.sep)) return next();
|
|
1233
1236
|
if (!nodeFs.existsSync(filePath) || nodeFs.statSync(filePath).isDirectory()) return next();
|
|
1234
1237
|
|
|
1235
1238
|
var ext = nodePath.extname(filePath).toLowerCase();
|
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:23932d34-331e-44c1-9a55-5d959a713e9c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-28T23:40:53.813Z",
|
|
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.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.28",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.28",
|
|
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.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.28",
|
|
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.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.28",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|