@blamejs/core 0.13.25 → 0.13.27

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.13.x
10
10
 
11
+ - 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.
12
+
13
+ - 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.
14
+
11
15
  - 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.
12
16
 
13
17
  - v0.13.24 (2026-05-28) — **`b.guard*` docs corrected: the compliance-posture opt key, the gate API, and validate return shapes.** Documentation corrections across the b.guard* family. The most consequential: the posture-selection option was documented as `compliance:` in many guards' @opts, but the working key is `compliancePosture:` — passing the documented `{ compliance: "hipaa" }` was silently ignored, so a compliance posture (e.g. HIPAA PII redaction) never activated. If you select a posture via the gate/validate/sanitize options, use `compliancePosture:`; `compliance:` had no effect. The guard docs now name the correct key uniformly. Also corrected: gate examples and prose that invoked the gate as a callable or via `.run` / `.inspect` (the gate is an object whose method is `.check(ctx)`), and validate() return shapes that listed `severities` / `summary` / `refusal` fields the function never returned (it returns `{ ok, issues }`). **Fixed:** *guard posture option is `compliancePosture:`, not `compliance:`* — Many `b.guard*` primitives documented the compliance-posture selector as `compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2"` in their `@opts`, but the family resolver reads `compliancePosture:`. Passing `{ compliance: "hipaa" }` was accepted and silently ignored — the posture overlay (e.g. CSV `piiPolicy: "redact"` under HIPAA) never applied, leaving the default policy in force. The docs across the guard family now name `compliancePosture:` consistently (the key the resolver and `b.guardX.compliancePosture(name)` already used). Action: if you selected a posture with `compliance:`, switch to `compliancePosture:` — the posture was not taking effect before. · *guard gate is an object with `.check(ctx)`, not a callable* — Several guards' gate `@example`s and prose invoked the gate as a function (`g({...})`), via `.run(...)`, or via `.inspect(...)`, and a few described it as "an async function". `b.guardX.gate(opts)` returns an object whose async method is `.check(ctx)`; the examples and prose now use `.check`, so they run as written. · *guard validate() returns `{ ok, issues }`* — Several guards documented `validate()` as returning `{ ok, issues, severities }`, `{ ok, issues, summary }`, or `{ ok, issues, refusal? }`. The function returns `{ ok, issues }` (each issue carries its own `severity` / `kind`); the documented extra top-level fields were never present. The docs now state the actual shape.
package/lib/audit.js CHANGED
@@ -796,9 +796,10 @@ function _checkpointPayload(atMonotonicCounter, atRowHash, createdAt) {
796
796
  );
797
797
  }
798
798
 
799
- // Anchor the current chain tip with a fresh ML-DSA-87 signature. Inserts
800
- // a row into audit_checkpoints. Updates <dataDir>/audit.tip for boot-time
801
- // rollback detection.
799
+ // Anchor the current chain tip with a fresh post-quantum signature (the
800
+ // configured b.auditSign algorithm SLH-DSA-SHAKE-256f by default).
801
+ // Inserts a row into audit_checkpoints. Updates <dataDir>/audit.tip for
802
+ // boot-time rollback detection.
802
803
  //
803
804
  // opts:
804
805
  // skipIfUnchanged: bool — return null without inserting if the chain tip
@@ -810,8 +811,10 @@ function _checkpointPayload(atMonotonicCounter, atRowHash, createdAt) {
810
811
  * @compliance soc2, pci-dss, sox-404
811
812
  * @related b.audit.verifyCheckpoints, b.audit.verify
812
813
  *
813
- * Anchor the current chain tip with a fresh ML-DSA-87 (post-quantum)
814
- * signature. Inserts a row into `audit_checkpoints` and updates the
814
+ * Anchor the current chain tip with a fresh post-quantum signature (the
815
+ * configured `b.auditSign` algorithm SLH-DSA-SHAKE-256f by default,
816
+ * ML-DSA-87 / ML-DSA-65 optional). Inserts a row into `audit_checkpoints`
817
+ * and updates the
815
818
  * boot-time rollback-detection sidecar (single-node) or the cluster
816
819
  * audit-tip row (cluster mode, fencing-token guarded). Cluster mode
817
820
  * requires the caller hold leader status — `cluster.requireLeader()`
@@ -917,8 +920,8 @@ async function checkpoint(opts) {
917
920
  * @related b.audit.checkpoint, b.audit.verify
918
921
  *
919
922
  * Walk every checkpoint and verify (a) the public-key fingerprint
920
- * matches the current signing key, (b) the ML-DSA-87 signature over the
921
- * payload still verifies, (c) the audit_log row at the anchored counter
923
+ * matches the current signing key, (b) the post-quantum signature over
924
+ * the payload still verifies, (c) the audit_log row at the anchored counter
922
925
  * still has the recorded rowHash. Catches tampering that recomputed
923
926
  * chain hashes after holding the vault key, because the off-chain
924
927
  * signature anchor is unforgeable without the signing key.
@@ -967,7 +970,7 @@ async function verifyCheckpoints() {
967
970
  checkpointsVerified: i,
968
971
  breakAt: i,
969
972
  checkpointId: c._id,
970
- reason: "ML-DSA-87 signature failed",
973
+ reason: "post-quantum signature failed",
971
974
  };
972
975
  }
973
976
  // Also confirm the audit row at atMonotonicCounter still matches the
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 required-field shape per
111
- * RFC 8984 §5 (Event) + §6 (Task). Returns the input on success; throws
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 the
341
- * first VEVENT into a JSCalendar Event object (RFC 8984 §5). Returns
342
- * a single Event when the VCALENDAR contains exactly one VEVENT, or
343
- * an array when multiple VEVENTs are present.
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 Event back to RFC 5545 iCalendar text. Returns a
385
- * CRLF-terminated string wrapped in a `BEGIN:VCALENDAR / VERSION:2.0 /
386
- * PRODID:-//blamejs//Calendar//EN / BEGIN:VEVENT ... END:VEVENT /
387
- * END:VCALENDAR` envelope per RFC 5545 §3.4.
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"
@@ -495,9 +495,14 @@ function unsealRow(table, row) {
495
495
  } catch (_e) { /* drop-silent */ }
496
496
  unsealed = null;
497
497
  }
498
- // If the value wasn't actually sealed, vault.unseal returns the input
499
- // unchanged keep the original.
500
- out[field] = unsealed !== undefined && unsealed !== null ? unsealed : out[field];
498
+ // Assign unconditionally. `unsealed` already carries the right value
499
+ // for every branch: the plaintext on success, the original value on
500
+ // the not-actually-sealed pass-through (set above), and `null` on an
501
+ // unseal failure. The failure case MUST null the column so downstream
502
+ // sees "no value" rather than the attacker-crafted `vault:<…>` string
503
+ // (a prior `... ? unsealed : out[field]` guard silently kept the
504
+ // forged ciphertext on failure, defeating the documented defense).
505
+ out[field] = unsealed;
501
506
  }
502
507
  }
503
508
 
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 logotype extension
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
@@ -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
- * Deferred from v1 (each with the documented condition for opting in):
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) and WKD key discovery (draft-koch-
64
- * openpgp-webkey-service). Defer condition: ships in v0.10.14
65
- * alongside `b.mail.crypto.smime` sign + verify the CMS
66
- * substrate `b.cms` landed in v0.10.13 unblocked the S/MIME
67
- * side, and OpenPGP encrypt rides the same release so the
68
- * mail-crypto surface lights up coherently rather than half-
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
@@ -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 (subject CN, issuer CN, validFrom / validTo, key algorithm
674
- * + size, signature algorithm). Throws `mail-crypto/smime/bad-cert`
675
- * on any of the above; throws `mail-crypto/smime/expired-cert` if
676
- * the cert is outside its validity window.
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
- * // → { subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }
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",
@@ -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. v1 surface: checkCert() the
26
- * operator-side preflight that refuses SHA-1 / MD5 / < 2048-bit
27
- * RSA certs at boot. sign() + verify() are DEFERRED in v1; see
28
- * the @intro block in lib/mail-crypto-smime.js for the deferral
29
- * conditions and the operator escape hatch.
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` / `queryByModseq` / `setFlags` / `createFolder` /
94
- * `listFolders` / `threadFor` / `quota` / `setLegalHold` / `destroy`.
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, secretKey, ecPublicKey, ecSecretKey },
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: { has, add, prune },
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.keypair();
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 / 22 chars).
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 / 22 chars base64 by default), attaches it
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 / Content-Type)
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/storage.js CHANGED
@@ -833,10 +833,10 @@ function _requireInit() {
833
833
  //
834
834
  // Resumable-chunked-upload primitive. Operators handling large file
835
835
  // uploads (multipart form / tus / S3-multipart-style flow) need to
836
- // persist incoming chunks during the upload window, then assemble
837
- // them into a final file when the upload completes. Without a
838
- // framework primitive every consumer ended up reinventing the
839
- // per-assembly directory layout + atomic finalize + GC of partial
836
+ // persist incoming chunks during the upload window, then concatenate
837
+ // them in order when the upload completes. Without a framework
838
+ // primitive every consumer ended up reinventing the per-assembly
839
+ // directory layout + ordered gap-checked assembly + GC of partial
840
840
  // assemblies that never completed.
841
841
  //
842
842
  // chunkScratch owns:
@@ -844,8 +844,8 @@ function _requireInit() {
844
844
  // storage backend just like saveFile)
845
845
  // - chunk persistence + retrieval with the framework envelope
846
846
  // - assembly metadata tracking createdAt/totalChunks/chunkHashes
847
- // - atomic concat into the final file (no consumer ever sees a
848
- // half-assembled file)
847
+ // - ordered, gap-checked concat returning the assembled bytes for
848
+ // the caller to persist (the primitive does not write a final file)
849
849
  // - GC of stale partial assemblies (operator opts in via gc())
850
850
  //
851
851
  // Backend is the same `b.storage` backend the operator already
@@ -919,10 +919,11 @@ function _validateChunkIndex(idx) {
919
919
  * @related b.storage.saveFile, b.storage.getFileBuffer
920
920
  *
921
921
  * Resumable-chunked-upload primitive. Persists incoming upload chunks
922
- * during the upload window + atomically assembles them into the
923
- * final file on completion. Owns per-assembly directory layout,
924
- * envelope-encrypted chunk persistence, atomic finalize, and GC of
925
- * partial assemblies.
922
+ * during the upload window, then concatenates them in order on
923
+ * completion and returns the assembled bytes for the caller to persist
924
+ * (the primitive does not itself write a final file). Owns per-assembly
925
+ * directory layout, envelope-encrypted chunk persistence, ordered
926
+ * gap-checked assembly, and GC of partial assemblies.
926
927
  *
927
928
  * Composes existing primitives: each chunk routes through
928
929
  * `b.storage.saveFile` (same XChaCha20-Poly1305 envelope as the
@@ -974,13 +975,16 @@ function _validateChunkIndex(idx) {
974
975
  * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
975
976
  * var cs = b.storage.chunkScratch({ rootKeyPrefix: "uploads/scratch" });
976
977
  *
977
- * // During upload — each PUT lands one chunk
978
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 0, data: chunk0 });
979
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 1, data: chunk1 });
980
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 2, data: chunk2 });
978
+ * // During upload — each PUT lands one chunk. saveChunk returns the
979
+ * // chunk's sealed encryptionKey; collect them in order for assemble.
980
+ * var keys = [];
981
+ * keys[0] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 0, data: chunk0 })).encryptionKey;
982
+ * keys[1] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 1, data: chunk1 })).encryptionKey;
983
+ * keys[2] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 2, data: chunk2 })).encryptionKey;
981
984
  *
982
- * // On completion — atomic assemble + cleanup
983
- * var assembled = await cs.assemble({ assemblyId: "upload-abc", expectedTotal: 3 });
985
+ * // On completion — concat the chunks (in order) into the assembled
986
+ * // buffer, then clean up. chunkEncryptionKeys is one key per chunk.
987
+ * var assembled = await cs.assemble({ assemblyId: "upload-abc", expectedTotal: 3, chunkEncryptionKeys: keys });
984
988
  * await cs.removeAssembly("upload-abc");
985
989
  *
986
990
  * // Periodic GC of partial uploads abandoned mid-stream
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.25",
3
+ "version": "0.13.27",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:47754ab4-658d-4a84-ac31-adda479b6281",
5
+ "serialNumber": "urn:uuid:ffa5e6b8-d243-4ea1-813c-7fa23f7cf2ae",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-28T12:35:12.333Z",
8
+ "timestamp": "2026-05-28T23:00:13.047Z",
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.25",
22
+ "bom-ref": "@blamejs/core@0.13.27",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.25",
25
+ "version": "0.13.27",
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.25",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.27",
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.25",
57
+ "ref": "@blamejs/core@0.13.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]