@blamejs/core 0.16.13 → 0.16.15
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 +8 -1
- package/lib/cert.js +29 -11
- package/lib/security-assert.js +6 -6
- package/lib/storage.js +8 -2
- package/lib/tenant-quota.js +52 -13
- 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.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.15 (2026-07-11) — **Restore break-glass certificate key escrow, hand a failed production-security assertion its real diagnostic message, and make tenant storage-byte quotas actually enforce — three defects surfaced by broadening test coverage and fixed at the root.** Three primitives had defects that only a hostile or previously-untested path reached. b.cert key escrow — the optional break-glass path that seals a renewed private key to an operator's offline recipient — never worked: writeEscrow called a b.crypto method that does not exist, so any certificate configured with keyEscrow threw the moment renewal tried to seal the key. It now seals via b.crypto.encrypt (ML-KEM-1024, plus the P-384 hybrid leg when the recipient supplies an ecPublicKey) and the operator recovers the key offline with b.crypto.decrypt; the recipient accepts an ML-KEM-1024 public-key PEM string or a { publicKey, ecPublicKey } pair from b.crypto.generateEncryptionKeyPair(). b.security.assertProduction constructed its error with the code and message transposed, so a failed production-security assertion threw with a bare token (BAD_OPT / ASSERT_FAILED) as its .message and buried the human-readable explanation in .code — operators now get the full diagnostic where they read it. And b.tenant.quota storage-byte accounting was broken several ways: the per-tenant byte sum issued a query the builder rejects, so snapshot / assert / list always threw once a storage cap was set; it read rows through the auto-unsealing ORM, so a sealed column was measured as its small decrypted plaintext rather than the larger on-disk vault envelope (letting sealed-column tenants slip under the cap); when the tenant identifier itself was a sealed column, the plaintext lookup matched no rows at all and the cap silently counted zero; and BLOB columns (handed back as Uint8Array by node:sqlite) were stringified before measuring, roughly tripling their counted size and refusing writes far below the real cap. All are fixed — the sum now filters a sealed tenant id by its derived-hash blind index, reads the raw stored rows, and measures true on-disk byte lengths — so storage quotas enforce at the configured limit. **Fixed:** *b.cert break-glass key escrow seals the renewed key instead of throwing* — A certificate configured with keyEscrow forwarded the private key to writeEscrow, which called a b.crypto.encryptEnvelope method that does not exist — so escrow threw on every renewal and the break-glass recovery path was unusable. It now seals the key to the operator's offline recipient with b.crypto.encrypt: ML-KEM-1024 always, plus a P-384 hybrid leg when the recipient carries an ecPublicKey. The recipient accepts an ML-KEM-1024 public-key PEM string or a { publicKey, ecPublicKey } pair from b.crypto.generateEncryptionKeyPair(); the sealed key is never decrypted by the framework and is recovered offline with b.crypto.decrypt and the matching private key(s). · *b.security.assertProduction throws with the diagnostic in .message* — SecurityAssertError was constructed with its code and message arguments transposed, so a failed production-security assertion surfaced a bare token (BAD_OPT / ASSERT_FAILED) as its .message while the explanatory text — including the per-assertion failure list — landed in .code. Operators catching the error now read the full diagnostic in .message and the stable token in .code, as documented. · *b.tenant.quota enforces storage-byte caps at the configured limit* — The per-tenant storage-bytes accounting had several defects. It issued a query the query builder rejects (a literal '*' column), so snapshot / assert / list threw as soon as a storage cap was configured — the storage half of tenant quotas never ran against a real database. It read rows through the ORM, which auto-unseals sealed columns, so a sealed cell was measured as its small decrypted plaintext rather than the much larger vault envelope actually on disk — a tenant whose data lives in sealed columns could sail under the cap. When the tenant identifier column itself was sealed, the plaintext lookup compared against the on-disk envelope and matched no rows, so the cap silently counted zero for those tenants. And BLOB columns, which node:sqlite returns as a Uint8Array rather than a Node Buffer, were stringified before measuring: String(uint8array) is the decimal-joined bytes, roughly a 3x overcount that refused writes well below the real cap. The sum now filters a sealed tenant identifier by its derived-hash blind index (as the query builder does), reads the raw stored rows (no unseal), and counts text as its UTF-8 byte length and typed-array views by their true byte length, so a storage cap — including data in sealed columns — enforces at the limit operators set.
|
|
12
|
+
|
|
13
|
+
- v0.16.14 (2026-07-11) — **Make the object-store single-backend shorthand work for remote backends, and return a string time zone (not an array) when importing an iCalendar event — two defects found by covering previously-untested configuration and import branches.** Covering more configuration and import branches surfaced two genuine defects, now fixed at the root. The documented object-store single-backend shorthand — b.storage.init({ backend: 'sigv4' | 'gcs' | 'azure-blob' | 'http-put', ... }) — never worked for a remote backend: it forwarded the caller's options with the backend key intact, but the object-store backend builder resolves protocol, so the backend was constructed with no protocol and initialization threw a missing-protocol error. Only the local shorthand (which happens to name the key correctly) worked. The shorthand now translates backend to protocol, so all four remote backends construct as documented. And b.calendar.fromIcal mapped a DTSTART;TZID=<zone> parameter to a JSCalendar timeZone that was an array (['America/New_York']) instead of the string RFC 8984 §4.7.1 requires — it only round-tripped by accident because a single-element array coerces to a string; the parameter is now unwrapped to a scalar string. **Fixed:** *b.storage remote single-backend shorthand constructs the backend* — b.storage.init({ backend: 'sigv4' | 'gcs' | 'azure-blob' | 'http-put', ... }) forwarded the options with the backend key, but the object-store backend builder reads protocol — so the default backend had no protocol and initialization threw a missing-protocol ObjectStoreError. The remote shorthand never worked (only the { backend: 'local' } form, which names protocol correctly under the hood, did). The shorthand now maps backend to protocol and drops the backend key, so all four remote backends build as documented. · *b.calendar.fromIcal returns a string time zone for DTSTART/DUE;TZID* — An imported event's DTSTART;TZID=<zone> (or a task's DUE;TZID) mapped to a JSCalendar timeZone that was a single-element array rather than the string RFC 8984 §4.7.1 requires. It happened to round-trip back through toIcal because a one-element array coerces to a string, but consumers reading timeZone as a string saw an array. The property parameter is now unwrapped to its scalar first value.
|
|
14
|
+
|
|
11
15
|
- v0.16.13 (2026-07-11) — **Make SD-JWT VC verification accept a raw JWK issuer key (its own documented common path), and emit the SMTP command-smuggling audit on a NUL-byte injection — two defects found by covering previously-untested verifier and inbound-server branches.** Covering the uncovered verifier and adversarial-input branches of two more subsystems surfaced two genuine defects, now fixed at the root. b.auth.sdJwtVc.verify rejected a valid credential when the issuerKeyResolver returned a raw JWK object — the very path the code's own comment calls the common one — because the JWK was handed straight to node:crypto.verify, which cannot consume a bare JWK, so verification threw a low-level type error instead of validating; the resolver's JWK is now imported to a key object before verification, matching the holder key-binding path. And the inbound SMTP server refused a command line containing a NUL byte (correct) but never emitted the command-smuggling audit event it emits for bare-CR / bare-LF injection, because it checked for the wrong error code; the audit now fires so a NUL-injection attempt is recorded for forensic triage. A misleading comment in the permissions MFA gate that advertised a non-existent no-freshness-window escape hatch is corrected — the freshness window is always enforced by design. **Fixed:** *b.auth.sdJwtVc.verify accepts a raw JWK from issuerKeyResolver* — When the issuerKeyResolver returned a JWK object — described in the code as the common path — the JWK was passed directly to node:crypto.verify, which requires a key object, so a valid credential failed verification with a raw ERR_INVALID_ARG_TYPE rather than validating. The resolver's JWK is now imported to a public key object (with the existing algorithm/key-type cross-check preserved) before verification, mirroring the holder key-binding JWT path. Verification succeeds for EC and Ed25519 JWK resolvers. · *Corrected a misleading comment in the permissions MFA freshness gate* — A comment in the requireMfa gate described mfaWindowMs: Infinity as an operator escape hatch for a no-freshness-window pass-through. No such escape hatch exists — both the role- and route-level validators reject a non-finite mfaWindowMs, so MFA freshness is always enforced (defaulting to 15 minutes). Enabling it would let a stolen long-lived cookie with a stale mfaAt bypass the gate. The comment now states the freshness window is always enforced; behavior is unchanged. **Security:** *SMTP inbound server records a command-smuggling audit on NUL-byte injection* — The inbound MX server's command handler refused a command line containing a NUL byte with a 500, but the branch meant to emit the mail.server.mx.smtp_smuggling_detected audit checked for the error code guard-smtp-command/nul-byte while the guard actually raises guard-smtp-command/nul. As a result a NUL-injection command was rejected but not recorded, unlike bare-CR / bare-LF smuggling which was audited. The code match is corrected, so a NUL-byte command-smuggling attempt now produces the forensic audit event.
|
|
12
16
|
|
|
13
17
|
- v0.16.12 (2026-07-11) — **Restore the static server's drive-by-execution defense, stop the retention sweep from aborting on subject-scoped rules, make wss:// connections to IP addresses work, and return a verdict instead of throwing on a malformed OpenPGP signature — four defects found by covering previously-untested error and adversarial branches.** Writing behavioral tests for the uncovered error and adversarial branches of the framework's most under-tested files surfaced four genuine defects, now fixed at the root. The static file server's safeAttachmentForRiskyMimes option — the drive-by-execution defense that forces Content-Disposition: attachment for risky inline types (text/html, image/svg+xml, application/javascript) — was silently inert: it was read from a value bag that only carries known-default keys, and this option has no default entry, so it always evaluated to false and the whole defense was dead code. The retention sweep threw and aborted entirely whenever a retention rule with a subjectField met a row with a subject value, because it called a lazily-required module as if it were already resolved. A wss:// connection to an IP-address host threw synchronously and was unusable, because the client set the TLS SNI to the IP literal, which the TLS stack forbids. And b.mail.crypto.pgp.verify threw on a truncated or malformed signature integer instead of returning its documented { ok: false } verdict, so a consumer iterating over untrusted signatures crashed rather than getting a negative result. A static gate now flags any option read from a defaults-applied value bag when that option has no default entry, so the static-server class cannot recur. **Fixed:** *Retention sweep no longer aborts on subject-scoped rules* — A retention rule declared with a subjectField consults the subject-level legal-hold registry when a candidate row carries a subject value. That path called the legal-hold module as though it were already resolved, but it is a lazily-required getter that must be invoked to load the module — so the call threw a TypeError, which the sweep converted into a SWEEP_FAILED and aborted the entire run. The getter is now invoked correctly, so subject-scoped retention sweeps proceed. · *wss:// connections to an IP-address host work* — Connecting the WebSocket client to a wss:// URL whose host is an IP literal threw ERR_INVALID_ARG_VALUE synchronously, because the client set the TLS SNI servername to the IP literal, which RFC 6066 and the Node TLS stack forbid. SNI is now sent only for hostname targets and omitted for IP literals; certificate identity is still verified against the address. **Security:** *Static server's safeAttachmentForRiskyMimes drive-by-execution defense works* — b.staticServe's documented safeAttachmentForRiskyMimes option forces Content-Disposition: attachment for risky inline MIME types (text/html, image/svg+xml, application/javascript) so a browser can't execute an operator-hosted file inline. It was read from the option bag produced by applyDefaults, which strips every key absent from the defaults table — and this option is not a defaults key — so it was always undefined and the defense never engaged. It is now read directly from the caller's options, so enabling it forces the attachment disposition as documented. Operators who set it were relying on a defense that did nothing; it is now active. · *b.mail.crypto.pgp.verify returns a verdict on a malformed signature instead of throwing* — verify() threw MailCryptoError('mail-crypto/pgp/bad-mpi') on a truncated or malformed signature integer, unlike every other malformed-signature path (bad armor, packet parse, hash mismatch), which returns { ok: false, code, reason }. A consumer looping over untrusted signatures and branching on .ok would crash on a crafted input rather than receive a negative verdict. Malformed signature integers now route through the same failure path and return { ok: false }. **Detectors:** *applydefaults-dropped-opt* — A static gate flags any option read from the result of applyDefaults(opts, DEFAULTS) when that option is absent from the DEFAULTS table — the exact shape that silently disabled the static server's attachment defense. The result of applyDefaults carries only the defaults' keys, so such a read is always undefined. The fix is to read the option from the caller's opts directly, or to add it to the defaults table.
|
package/lib/calendar.js
CHANGED
|
@@ -1116,7 +1116,14 @@ function _firstParamValue(prop, paramName) {
|
|
|
1116
1116
|
if (!prop) return null;
|
|
1117
1117
|
var first = Array.isArray(prop) ? prop[0] : prop;
|
|
1118
1118
|
if (!first || !first.params) return null;
|
|
1119
|
-
|
|
1119
|
+
// b.safeIcal exposes each property parameter as a (possibly
|
|
1120
|
+
// multi-valued) array. Consumers here want a scalar — a TZID maps to
|
|
1121
|
+
// the JSCalendar `timeZone` String (RFC 8984 §4.7.1), not an array.
|
|
1122
|
+
// Unwrap to the first element so `timeZone` is a plain string that
|
|
1123
|
+
// round-trips through toIcal's `DTSTART;TZID=<zone>` emission.
|
|
1124
|
+
var v = first.params[paramName];
|
|
1125
|
+
if (v === undefined || v === null) return null;
|
|
1126
|
+
return Array.isArray(v) ? (v.length > 0 ? v[0] : null) : v;
|
|
1120
1127
|
}
|
|
1121
1128
|
|
|
1122
1129
|
function _icalRruleToJscal(rrule) {
|
package/lib/cert.js
CHANGED
|
@@ -155,14 +155,16 @@ function _createSealedDiskStorage(opts) {
|
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
|
|
158
|
-
async writeEscrow(relPath, plaintextKeyPem,
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
158
|
+
async writeEscrow(relPath, plaintextKeyPem, recipient) {
|
|
159
|
+
// Seal the private key to the operator's offline break-glass
|
|
160
|
+
// recipient via b.crypto.encrypt (ML-KEM-1024 KEM, plus the P-384
|
|
161
|
+
// hybrid leg when the recipient supplies an ecPublicKey). The
|
|
162
|
+
// operator recovers it offline with b.crypto.decrypt and the
|
|
163
|
+
// matching private key(s) — it is never decrypted by the framework.
|
|
164
|
+
var envelope = bCrypto().encrypt(plaintextKeyPem, recipient);
|
|
163
165
|
var p = nodePath.join(rootDir, relPath);
|
|
164
166
|
_ensureDir(nodePath.dirname(p));
|
|
165
|
-
atomicFile.writeSync(p,
|
|
167
|
+
atomicFile.writeSync(p, envelope + "\n", { mode: 0o600 });
|
|
166
168
|
},
|
|
167
169
|
};
|
|
168
170
|
}
|
|
@@ -212,7 +214,10 @@ function _createSealedDiskStorage(opts) {
|
|
|
212
214
|
* cleanup: async function (params) { ... }, // required — runs after authorization completes
|
|
213
215
|
* },
|
|
214
216
|
* keyEscrow: { // optional — break-glass-only key recovery
|
|
215
|
-
* recipient:
|
|
217
|
+
* recipient: string | { publicKey, ecPublicKey }, // ML-KEM-1024 pubkey PEM, or a
|
|
218
|
+
* // b.crypto.generateEncryptionKeyPair() hybrid pair; the
|
|
219
|
+
* // renewed key is sealed to it via b.crypto.encrypt and
|
|
220
|
+
* // recovered offline with b.crypto.decrypt
|
|
216
221
|
* },
|
|
217
222
|
* }>,
|
|
218
223
|
* renew: {
|
|
@@ -350,10 +355,23 @@ function create(opts) {
|
|
|
350
355
|
throw new CertError("cert/bad-key-alg",
|
|
351
356
|
"cert.create.certs[" + i + "].keyAlg must be ecdsa-p256 / ecdsa-p384 / rsa-2048 / rsa-3072 / rsa-4096");
|
|
352
357
|
}
|
|
353
|
-
if (c.keyEscrow
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
if (c.keyEscrow) {
|
|
359
|
+
var rec = c.keyEscrow.recipient;
|
|
360
|
+
var okStr = typeof rec === "string" && rec.length > 0;
|
|
361
|
+
// Object form: publicKey (ML-KEM PEM) is required non-empty, matching the
|
|
362
|
+
// string path; ecPublicKey (P-384 hybrid leg) is optional, but if present
|
|
363
|
+
// must itself be a non-empty string — an empty key would otherwise reach
|
|
364
|
+
// b.crypto.encrypt and either fail deeper or silently drop the hybrid leg.
|
|
365
|
+
var okObj = rec && typeof rec === "object" &&
|
|
366
|
+
typeof rec.publicKey === "string" && rec.publicKey.length > 0 &&
|
|
367
|
+
(rec.ecPublicKey == null ||
|
|
368
|
+
(typeof rec.ecPublicKey === "string" && rec.ecPublicKey.length > 0));
|
|
369
|
+
if (!okStr && !okObj) {
|
|
370
|
+
throw new CertError("cert/bad-key-escrow",
|
|
371
|
+
"cert.create.certs[" + i + "].keyEscrow.recipient must be an ML-KEM-1024 " +
|
|
372
|
+
"public-key PEM string, or a { publicKey, ecPublicKey } object from " +
|
|
373
|
+
"b.crypto.generateEncryptionKeyPair() for the P-384 hybrid path");
|
|
374
|
+
}
|
|
357
375
|
}
|
|
358
376
|
certsByName[c.name] = {
|
|
359
377
|
name: c.name,
|
package/lib/security-assert.js
CHANGED
|
@@ -340,14 +340,14 @@ async function assertProduction(opts) {
|
|
|
340
340
|
if (opts.extra !== undefined) {
|
|
341
341
|
if (!Array.isArray(opts.extra)) {
|
|
342
342
|
throw new SecurityAssertError(
|
|
343
|
-
"
|
|
344
|
-
"
|
|
343
|
+
"BAD_OPT",
|
|
344
|
+
"security.assertProduction: opts.extra must be an array of functions, got " + typeof opts.extra);
|
|
345
345
|
}
|
|
346
346
|
for (var ei = 0; ei < opts.extra.length; ei++) {
|
|
347
347
|
if (typeof opts.extra[ei] !== "function") {
|
|
348
348
|
throw new SecurityAssertError(
|
|
349
|
-
"
|
|
350
|
-
"
|
|
349
|
+
"BAD_OPT",
|
|
350
|
+
"security.assertProduction: opts.extra[" + ei + "] must be a function");
|
|
351
351
|
}
|
|
352
352
|
var verdict;
|
|
353
353
|
try { verdict = await opts.extra[ei](); }
|
|
@@ -383,8 +383,8 @@ async function assertProduction(opts) {
|
|
|
383
383
|
if (failures.length > 0) {
|
|
384
384
|
var summary = failures.map(function (f) { return " - " + f.code + ": " + f.message; }).join("\n");
|
|
385
385
|
var err = new SecurityAssertError(
|
|
386
|
-
"
|
|
387
|
-
"
|
|
386
|
+
"ASSERT_FAILED",
|
|
387
|
+
"production security policy failed (" + failures.length + " assertion(s)):\n" + summary);
|
|
388
388
|
err.failures = failures;
|
|
389
389
|
throw err;
|
|
390
390
|
}
|
package/lib/storage.js
CHANGED
|
@@ -162,9 +162,15 @@ function _normalizeConfig(opts) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
if (opts.backend === "http-put" || opts.backend === "sigv4" || opts.backend === "gcs" || opts.backend === "azure-blob") {
|
|
165
|
-
// Forward
|
|
165
|
+
// Forward the single-backend spec, translating the `backend` shorthand
|
|
166
|
+
// into the `protocol` key the object-store adapter builds against — the
|
|
167
|
+
// local branch above already spells it `protocol`, and buildBackend keys
|
|
168
|
+
// off `protocol`, so forwarding `backend` verbatim left the default
|
|
169
|
+
// backend without a protocol and failed to build.
|
|
170
|
+
var remoteCfg = Object.assign({}, opts, { protocol: opts.backend, name: undefined });
|
|
171
|
+
delete remoteCfg.backend;
|
|
166
172
|
return {
|
|
167
|
-
backends: { "default":
|
|
173
|
+
backends: { "default": remoteCfg },
|
|
168
174
|
defaultClassification: null,
|
|
169
175
|
refuseUnclassified: false,
|
|
170
176
|
};
|
package/lib/tenant-quota.js
CHANGED
|
@@ -55,12 +55,14 @@
|
|
|
55
55
|
var C = require("./constants");
|
|
56
56
|
var lazyRequire = require("./lazy-require");
|
|
57
57
|
var boundedMap = require("./bounded-map");
|
|
58
|
+
var sql = require("./sql");
|
|
58
59
|
var validateOpts = require("./validate-opts");
|
|
59
60
|
var { defineClass } = require("./framework-error");
|
|
60
61
|
|
|
61
62
|
var TenantQuotaError = defineClass("TenantQuotaError", { alwaysPermanent: true });
|
|
62
63
|
|
|
63
64
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
65
|
+
var cryptoField = lazyRequire(function () { return require("./crypto-field"); });
|
|
64
66
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
65
67
|
|
|
66
68
|
var DEFAULT_CACHE_TTL_MS = C.TIME.seconds(30);
|
|
@@ -115,6 +117,7 @@ function create(opts) {
|
|
|
115
117
|
], "tenantQuota.create");
|
|
116
118
|
|
|
117
119
|
if (!opts.db || typeof opts.db.from !== "function" ||
|
|
120
|
+
typeof opts.db.prepare !== "function" ||
|
|
118
121
|
typeof opts.db.getTableMetadata !== "function") {
|
|
119
122
|
throw new TenantQuotaError("tenant-quota/bad-db",
|
|
120
123
|
"tenantQuota.create: opts.db must be the framework's b.db namespace");
|
|
@@ -197,25 +200,61 @@ function create(opts) {
|
|
|
197
200
|
var tables = _resolveTables();
|
|
198
201
|
var total = 0;
|
|
199
202
|
for (var i = 0; i < tables.length; i++) {
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
203
|
+
var table = tables[i];
|
|
204
|
+
// Resolve the tenant predicate. When tenantField is itself a SEALED
|
|
205
|
+
// column, the plaintext tenantId never equals the on-disk vault
|
|
206
|
+
// envelope, so the framework filters it by its derived-hash blind index.
|
|
207
|
+
// Reuse cryptoField.lookupHash — the same rewrite db.from().where()
|
|
208
|
+
// applies — so a sealed tenantField resolves correctly (including the
|
|
209
|
+
// legacy dual-read across the keyed-MAC flip). A plaintext tenantField
|
|
210
|
+
// compares directly. Without this, a schema that seals the tenant id
|
|
211
|
+
// matches zero rows and the cap silently never fires.
|
|
212
|
+
var whereField = tenantField;
|
|
213
|
+
var whereVals = [tenantId];
|
|
214
|
+
var sealed = cryptoField().getSealedFields(table) || [];
|
|
215
|
+
if (sealed.indexOf(tenantField) !== -1) {
|
|
216
|
+
var lk = cryptoField().lookupHash(table, tenantField, tenantId);
|
|
217
|
+
if (!lk) {
|
|
218
|
+
throw new TenantQuotaError("tenant-quota/sealed-tenant-no-hash",
|
|
219
|
+
"tenantQuota: tenantField '" + tenantField + "' on table '" + table +
|
|
220
|
+
"' is a sealed column without a derived hash; declare " +
|
|
221
|
+
"derivedHashes: { <name>: { from: '" + tenantField + "' } } so it can be queried");
|
|
222
|
+
}
|
|
223
|
+
whereField = lk.field;
|
|
224
|
+
whereVals = (lk.legacyValue != null && lk.legacyValue !== lk.value)
|
|
225
|
+
? [lk.value, lk.legacyValue]
|
|
226
|
+
: [lk.value];
|
|
227
|
+
}
|
|
228
|
+
// Build the read with b.sql — the same builder db.from() uses — so the
|
|
229
|
+
// table and column identifiers get identical handling (schema-qualified
|
|
230
|
+
// "schema.table" names, reserved-word names, dialect quoting) without
|
|
231
|
+
// re-implementing any of it here. Run it raw via db.prepare and, unlike
|
|
232
|
+
// db.from().all(), do NOT route rows through cryptoField.unsealRow: a
|
|
233
|
+
// storage cap must count what is actually on disk — the (much larger)
|
|
234
|
+
// vault envelope of a sealed column, not the plaintext it unseals to —
|
|
235
|
+
// or a tenant whose data lives in sealed columns sails under the cap.
|
|
236
|
+
// The tenant value(s) are bound parameters.
|
|
237
|
+
var built = sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
238
|
+
.whereIn(whereField, whereVals)
|
|
239
|
+
.toSql();
|
|
240
|
+
var stmt = db.prepare(built.sql);
|
|
241
|
+
var rows = stmt.all.apply(stmt, built.params);
|
|
211
242
|
for (var r = 0; r < rows.length; r++) {
|
|
212
243
|
var row = rows[r];
|
|
213
244
|
var keys = Object.keys(row);
|
|
214
245
|
for (var k = 0; k < keys.length; k++) {
|
|
215
246
|
var v = row[keys[k]];
|
|
216
247
|
if (v == null) continue;
|
|
217
|
-
|
|
218
|
-
|
|
248
|
+
// BLOB columns round-trip as a typed-array view (node:sqlite hands
|
|
249
|
+
// them back as Uint8Array, not a Node Buffer), so count the true
|
|
250
|
+
// byte length off any ArrayBuffer view rather than stringifying it
|
|
251
|
+
// — String(Uint8Array) is the decimal-joined bytes, a ~3x overcount
|
|
252
|
+
// that would refuse inserts well below the real storage cap. Text
|
|
253
|
+
// (including a sealed column's "vault:" envelope) is counted as its
|
|
254
|
+
// UTF-8 byte length, not the JS string .length — a multi-byte
|
|
255
|
+
// character occupies more than one byte on disk.
|
|
256
|
+
if (ArrayBuffer.isView(v)) total += v.byteLength;
|
|
257
|
+
else total += Buffer.byteLength(String(v), "utf8");
|
|
219
258
|
}
|
|
220
259
|
}
|
|
221
260
|
}
|
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:1618a7b7-f477-422c-99bb-f941a8827acf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-12T01:36:29.373Z",
|
|
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.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.15",
|
|
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.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.15",
|
|
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.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|