@blamejs/core 0.14.8 → 0.14.10
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/auth/saml.js +5 -5
- package/lib/backup/index.js +3 -3
- package/lib/compliance-ai-act.js +7 -7
- package/lib/compliance.js +21 -21
- package/lib/crypto-field.js +69 -0
- package/lib/dsr.js +1 -1
- package/lib/guard-archive.js +1 -1
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-store-fts.js +40 -18
- package/lib/mail-store.js +188 -2
- package/lib/parsers/index.js +0 -6
- 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.14.x
|
|
10
10
|
|
|
11
|
+
- v0.14.10 (2026-05-31) — **Full-text-search token hashes move to a keyed MAC; existing mail-store search indexes rebuild automatically on upgrade.** The mail-store full-text-search index hashed its tokens with a hand-rolled salted-SHA3 derived hash. It now routes through the framework's sealed-column hashing primitive in keyed mode (HMAC-SHAKE256 off the per-deployment MAC key), so a search-index token hash is unforgeable and un-correlatable across deployments without that key — the same posture the sealed-column lookup hashes already use. Because the keyed hash changes the stored token values, a mail-store opened after upgrade detects its index as old-format and rebuilds it once from the sealed message rows. The rebuild runs under a format marker: the index is marked `rebuilding` before it is cleared and only marked current after every row is re-hashed inside an explicit transaction, and search falls back to its cursor path (rather than returning partial hits) whenever the marker is not current — so an interrupted rebuild leaves the old index intact and queryable and retries on the next open, never serving a half-built index. A new `b.cryptoField.computeNamespacedHash` primitive backs the keyed hashing for callers that hash outside the registered-column path. **Added:** *`b.cryptoField.computeNamespacedHash`* — A mode-aware namespaced hash for indexed-lookup callers that hash a value outside the registered-column derived-hash path. `computeNamespacedHash(ns, value, { mode, truncateBytes })` routes through the same engine as `computeDerived` — `salted-sha3` (default) or the keyed `hmac-shake256` — with optional hex truncation. The mail-store full-text index is the first consumer. **Changed:** *Mail-store full-text index rehashes to a keyed MAC on upgrade* — The full-text-search token hash now uses `b.cryptoField.computeNamespacedHash` in `hmac-shake256` mode instead of a hand-rolled salted-SHA3. The first time a store is opened after upgrade, its index is detected as old-format and rebuilt once from the sealed message rows; subsequent opens are no-ops. Search is unaffected once the rebuild completes. The rebuild requires the vault to be initialized and fails closed (a clear error) at construction if it is not, rather than leaving a stale searchable index. **Security:** *Keyed, un-correlatable full-text-search token hashes* — A search-index token hash is now a keyed MAC over a per-deployment key, not a static-salted digest — it cannot be forged or correlated across deployments without that key, closing the low-entropy-token correlation gap on the search index. The index remains unrecoverable from a database dump alone, as before. **Detectors:** *Hand-rolled lookup-hash check covers the split form* — The check that requires sealed-column lookup hashes to compose the framework primitive now also catches the across-lines hand-roll (`var salt = getDerivedHashSalt(); var hex = salt.toString(...); sha3(hex + ns + value)`), not only the single-expression form, so the bypass that the mail-store index used can't reappear. **Migration:** *Automatic, one-time full-text index rebuild* — No operator action is required: the rebuild runs automatically and idempotently on first open after upgrade, atomically and crash-safe (an interrupted rebuild keeps the old index and retries). The only requirement is that the vault is initialized before the mail-store is constructed. One caveat for shared stores: do not run a pre-upgrade and post-upgrade node against the same backend file concurrently across this format change — the old node would write old-format hashes the new node cannot match. Roll the deployment fully across the upgrade. This re-open condition is lifted once all nodes are on 0.14.10 or later.
|
|
12
|
+
|
|
13
|
+
- v0.14.9 (2026-05-30) — **Corrects EU AI Act doc paths that named an uncallable namespace, plus source-comment hygiene and two new codebase checks.** A documentation fix and internal hygiene. The `@primitive` / `@signature` / `@example` blocks for the EU AI Act fundamental-rights-impact-assessment and GPAI training-data-summary helpers advertised `b.complianceAiAct.*`, which is undefined — the callable path is `b.compliance.aiAct.*` — so an operator copying the documented call got `undefined is not a function`. The documented paths now match the real surface. Alongside that: a duplicate parser entry in a doc block is removed, version stamps embedded in section-divider comments are stripped, and two codebase checks are added — one that fails the build when a `@primitive` block documents a wholly-unresolvable namespace (the gap that hid the AI Act paths), and one that flags a version stamp left inside a section divider. No exported API, error code, wire format, or runtime behaviour changes. **Changed:** *Source-comment hygiene* — Removed a duplicate `env` entry from the parsers `@module` doc block, and stripped internal version stamps (`vX.Y.Z`) from `// ---- ... ----` section-divider comments across several files, keeping the descriptive label. Comment-only; no behaviour change. **Fixed:** *EU AI Act helper documentation named an uncallable path* — `b.compliance.aiAct.fundamentalRightsImpactAssessment` and `b.compliance.aiAct.gpai.trainingDataSummary` were documented as `b.complianceAiAct.*` in their `@primitive` / `@signature` / `@example` blocks (and one returned reference string). `b.complianceAiAct` is undefined, so the documented call failed; the documented paths now match the callable surface. **Detectors:** *`@primitive` reachability covers wrong-namespace paths* — The reachability check previously only flagged a missing leaf on a resolved namespace; a `@primitive` whose entire dotted prefix is unresolvable (the shape that hid the AI Act doc paths) was silently skipped. It now walks each prefix segment and fails the build on any unresolvable one, while preserving the factory-instance-shorthand exemption. · *Version-stamp-in-divider check* — A new check flags a version stamp (`vX.Y.Z`) left immediately after a section divider's dashes (`// ---- vX.Y.Z ...`) — internal release vocabulary that does not belong in shipped source comments — without matching legitimate `@since` tags or prose version references.
|
|
14
|
+
|
|
11
15
|
- v0.14.8 (2026-05-30) — **Source-comment and codebase-check hygiene, plus a new require-block alignment check; no API or behaviour changes.** Internal lint and comment cleanup with no operator-facing surface change. Several codebase-check comments and one stub helper name that described behaviour the check no longer has are corrected; an unused lint-suppression class and a set of stale duplicate-cluster qualifiers (functions that were since renamed or extracted) are pruned or re-pointed. Fifty-nine `// allow:` markers that named the byte-size or time-literal check on values those checks no longer flag are removed, and twenty self-negating rationales on markers the time check genuinely fires on are rewritten to say why the value coincidentally matches. A new codebase check holds top-of-file require blocks to consistent `=` column alignment, with the files that currently carry drift listed as a migration allowlist. No exported API, error code, wire format, or runtime behaviour changes. **Changed:** *Lint-suppression and codebase-check comment cleanup* — Corrected codebase-check comments that overstated their check's scope (a duplicate-code threshold described as three files when the advisory threshold is two, a narrowed byte-literal check carrying its pre-narrowing description, and a deferred-scan helper named as though it enforced a guarantee it does not yet provide). Removed an unused lint-suppression class and its one dead in-code marker, and pruned or re-pointed stale duplicate-cluster qualifiers that named functions since renamed or extracted into shared helpers. Removed fifty-nine `// allow:` markers that suppressed nothing, and rewrote twenty self-negating marker rationales (which read "not seconds" while sitting on a value the time check fires on) to explain the coincidental match. Source-comment and test hygiene only. **Detectors:** *Require-block `=` alignment check* — A new codebase check flags a top-of-file require block that mixes its `=` column alignment — a fittable line whose `=` drifts off the column the rest of the block shares. Compact single-space blocks are exempt (only blocks that declare alignment intent are checked), as are destructures and long names physically too wide to reach the column, and blank- or comment-separated tiers align independently. The files that currently carry such drift are an explicit migration allowlist, reflowed over time; new code is held to the rule.
|
|
12
16
|
|
|
13
17
|
- v0.14.7 (2026-05-30) — **Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock.** This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: "warn" })` (audited, allowed) or `"off"` while the schema is reconciled. **Added:** *Column-membership gate on every query* — `b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: "reject" | "warn" | "off" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89). · *Keyed mode for sealed-column lookup hashes* — Equality-lookup ("derived") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: "hmac-shake256" })` or per column with `{ from, mode: "hmac-shake256" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically. · *Dual-control gate on audit-chain purge* — `b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties). · *Running clock for breach-notification deadlines* — `b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually. **Changed:** *Queries against undeclared columns now fail closed by default* — The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: "warn" })` to audit and allow, or `"off"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members. **Security:** *Database encryption key bound to its location* — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required. · *`whereRaw` refuses embedded string literals* — `whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89). · *Credential-rejection audits record the attempted relation* — A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778). **Detectors:** *Audit-purge dual-control gate* — A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement. · *Raw-SQL literal/interpolation guard* — A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code. · *Hand-rolled lookup-hash guard* — A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy. · *Auth-audit attempted-relation guard* — A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter.
|
package/lib/auth/saml.js
CHANGED
|
@@ -842,7 +842,7 @@ function create(opts) {
|
|
|
842
842
|
"</md:EntityDescriptor>";
|
|
843
843
|
}
|
|
844
844
|
|
|
845
|
-
// ----
|
|
845
|
+
// ---- Single Logout (RFC SAML Bindings §3.4 HTTP-Redirect) ----
|
|
846
846
|
|
|
847
847
|
/**
|
|
848
848
|
* @primitive b.auth.saml.sp.buildLogoutRequest
|
|
@@ -1250,7 +1250,7 @@ function create(opts) {
|
|
|
1250
1250
|
};
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
-
// ----
|
|
1253
|
+
// ---- SLO HTTP-POST binding (SAML Bindings §3.5) ----
|
|
1254
1254
|
|
|
1255
1255
|
/**
|
|
1256
1256
|
* @primitive b.auth.saml.sp.buildLogoutRequestPost
|
|
@@ -1524,7 +1524,7 @@ function create(opts) {
|
|
|
1524
1524
|
};
|
|
1525
1525
|
}
|
|
1526
1526
|
|
|
1527
|
-
// ----
|
|
1527
|
+
// ---- SAML EncryptedAssertion decrypt (XMLEnc) ----
|
|
1528
1528
|
|
|
1529
1529
|
// XMLEnc Algorithm URIs we support.
|
|
1530
1530
|
//
|
|
@@ -1722,7 +1722,7 @@ function _decryptEncryptedAssertion(encAssertion, spPrivateKeyPem) {
|
|
|
1722
1722
|
return clearBytes.toString("utf8");
|
|
1723
1723
|
}
|
|
1724
1724
|
|
|
1725
|
-
// ----
|
|
1725
|
+
// ---- SAML SLO XMLDSig-Enveloped (HTTP-POST/SOAP) ----
|
|
1726
1726
|
|
|
1727
1727
|
// PQC SignatureMethod URIs used by the embedded XMLDSig signatures.
|
|
1728
1728
|
// Standard XMLDSig vocabulary classical signing URIs (W3C XMLDSig
|
|
@@ -1936,7 +1936,7 @@ function _verifyEmbeddedXmlDsig(xml, idpVerifyKey, idpVerifyAlg, expectedRootLoc
|
|
|
1936
1936
|
}
|
|
1937
1937
|
}
|
|
1938
1938
|
|
|
1939
|
-
// ----
|
|
1939
|
+
// ---- SAML SLO signature-alg dispatch ----
|
|
1940
1940
|
|
|
1941
1941
|
function _sigAlgUrn(alg) {
|
|
1942
1942
|
// PQC signers — framework-private experimental URIs. The `urn:`
|
package/lib/backup/index.js
CHANGED
|
@@ -1015,7 +1015,7 @@ module.exports = {
|
|
|
1015
1015
|
BUNDLE_ID_RE: BUNDLE_ID_RE,
|
|
1016
1016
|
};
|
|
1017
1017
|
|
|
1018
|
-
// ----
|
|
1018
|
+
// ---- bundleAdapterStorage ---------------------------------------
|
|
1019
1019
|
|
|
1020
1020
|
/**
|
|
1021
1021
|
* @primitive b.backup.bundleAdapterStorage
|
|
@@ -2257,7 +2257,7 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
|
|
|
2257
2257
|
};
|
|
2258
2258
|
};
|
|
2259
2259
|
|
|
2260
|
-
// ----
|
|
2260
|
+
// ---- objectStoreAdapter ----------------------------------------
|
|
2261
2261
|
|
|
2262
2262
|
/**
|
|
2263
2263
|
* @primitive b.backup.bundleAdapterStorage.objectStoreAdapter
|
|
@@ -2475,7 +2475,7 @@ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
|
|
|
2475
2475
|
};
|
|
2476
2476
|
};
|
|
2477
2477
|
|
|
2478
|
-
// ----
|
|
2478
|
+
// ---- migrate ----------------------------------------------------
|
|
2479
2479
|
|
|
2480
2480
|
/**
|
|
2481
2481
|
* @primitive b.backup.migrate
|
package/lib/compliance-ai-act.js
CHANGED
|
@@ -538,8 +538,8 @@ function deployerChecklist(assessment) {
|
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
/**
|
|
541
|
-
* @primitive b.
|
|
542
|
-
* @signature b.
|
|
541
|
+
* @primitive b.compliance.aiAct.fundamentalRightsImpactAssessment
|
|
542
|
+
* @signature b.compliance.aiAct.fundamentalRightsImpactAssessment(opts)
|
|
543
543
|
* @since 0.8.77
|
|
544
544
|
*
|
|
545
545
|
* EU AI Act Article 27 — Fundamental Rights Impact Assessment (FRIA).
|
|
@@ -569,7 +569,7 @@ function deployerChecklist(assessment) {
|
|
|
569
569
|
* }
|
|
570
570
|
*
|
|
571
571
|
* @example
|
|
572
|
-
* var fria = b.
|
|
572
|
+
* var fria = b.compliance.aiAct.fundamentalRightsImpactAssessment({
|
|
573
573
|
* systemId: "credit-scoring-v3",
|
|
574
574
|
* deploymentContext: { purpose: "loan approval", sector: "financial",
|
|
575
575
|
* geography: "EU", scale: "1M decisions/year" },
|
|
@@ -603,13 +603,13 @@ function fundamentalRightsImpactAssessment(opts) {
|
|
|
603
603
|
notificationStatus: "operator-must-notify",
|
|
604
604
|
note: "Notify national market-surveillance authority before first use (Art 27(3))",
|
|
605
605
|
auditHook: "b.audit emission action='aiact.fria.completed' recommended",
|
|
606
|
-
annexIVReference: "see b.
|
|
606
|
+
annexIVReference: "see b.compliance.aiAct.annexIVScaffold for technical documentation",
|
|
607
607
|
};
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
/**
|
|
611
|
-
* @primitive b.
|
|
612
|
-
* @signature b.
|
|
611
|
+
* @primitive b.compliance.aiAct.gpai.trainingDataSummary
|
|
612
|
+
* @signature b.compliance.aiAct.gpai.trainingDataSummary(opts)
|
|
613
613
|
* @since 0.8.77
|
|
614
614
|
*
|
|
615
615
|
* EU AI Act Article 53(1)(d) — GPAI training-data summary template
|
|
@@ -634,7 +634,7 @@ function fundamentalRightsImpactAssessment(opts) {
|
|
|
634
634
|
* contentProvenance: object, // { synthIdEmbed, c2paManifestEmbed, watermarkProvider }
|
|
635
635
|
*
|
|
636
636
|
* @example
|
|
637
|
-
* var summary = b.
|
|
637
|
+
* var summary = b.compliance.aiAct.gpai.trainingDataSummary({
|
|
638
638
|
* modelId: "acme-llm-7b",
|
|
639
639
|
* modelVersion: "1.0",
|
|
640
640
|
* provider: { name: "Acme AI", address: "1 St", contact: "ai@acme.example" },
|
package/lib/compliance.js
CHANGED
|
@@ -107,12 +107,12 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
107
107
|
"bsi-c5", // Germany BSI C5
|
|
108
108
|
"ens-es", // Spain Esquema Nacional de Seguridad
|
|
109
109
|
"uk-g-cloud", // UK G-Cloud
|
|
110
|
-
// ----
|
|
110
|
+
// ---- 2026 effective deadlines ----
|
|
111
111
|
"modpa", // Maryland Online Data Privacy Act (effective 2025-10-01) — strict data-min
|
|
112
112
|
"nydfs-500", // NYDFS 23 NYCRR 500 Amendment 2 — financial cybersecurity (multi-factor + asset inventory + governance)
|
|
113
113
|
"hipaa-2026", // HHS HIPAA Security Rule 2026-Q4 final — extends hipaa with mandatory MFA + asset inventory + 72h restoration testing
|
|
114
114
|
"quebec-25", // Quebec Law 25 final phase (effective 2026-09-22) — DPIA + automated-decision opt-out
|
|
115
|
-
// ----
|
|
115
|
+
// ---- US state consumer-privacy postures ----
|
|
116
116
|
// Each posture carries per-state cure-period, profiling opt-out
|
|
117
117
|
// and minor-consent metadata via b.dsr.stateRules(state). The
|
|
118
118
|
// generic DSR primitive (b.dsr.submit) covers ~80% of the surface;
|
|
@@ -139,7 +139,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
139
139
|
"ct-sb3", // Connecticut SB 3 Consumer Health Data
|
|
140
140
|
"tx-cubi", // Texas Capture or Use of Biometric Identifier
|
|
141
141
|
"fl-fdbr", // Florida Digital Bill of Rights (SB 262, effective 2024-07-01) — narrow scope ($1B+ revenue threshold)
|
|
142
|
-
// ----
|
|
142
|
+
// ---- AI-governance postures ----
|
|
143
143
|
// State + sectoral AI regulations crystallizing through 2026. Each
|
|
144
144
|
// posture is a flag that operators pin alongside their base
|
|
145
145
|
// privacy/sectoral posture; the floors enforce audit-chain signing
|
|
@@ -153,20 +153,20 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
153
153
|
"ca-tfaia", // California SB 53 — Transparency in Frontier AI Act (effective 2026-01-01)
|
|
154
154
|
"kr-ai-basic", // South Korea AI Basic Act (effective 2026-01-22)
|
|
155
155
|
"cn-ai-label", // China Measures for Labelling of AI-Generated Content (effective 2025-09-01)
|
|
156
|
-
// ----
|
|
156
|
+
// ---- AI management cross-walks ----
|
|
157
157
|
"iso-42001", // ISO/IEC 42001:2023 — AI Management System
|
|
158
158
|
"iso-23894", // ISO/IEC 23894:2023 — AI Risk Management Guidance
|
|
159
|
-
// ----
|
|
159
|
+
// ---- content-credentials posture flags ----
|
|
160
160
|
"ca-sb942", // California SB-942 (Cal. Bus. & Prof. Code §22757) gen-AI disclosure (effective 2026-08-02) // regulatory identifier + date, not bytes
|
|
161
161
|
"ca-ab853", // California AB-853 platform-side gen-AI detection (effective 2026-08-02) // regulatory identifier + date, not bytes
|
|
162
|
-
// ----
|
|
162
|
+
// ---- substrate-to-posture cleanup ----
|
|
163
163
|
"eaa", // EU Accessibility Act / Directive (EU) 2019/882 (effective 2025-06-28)
|
|
164
164
|
"wcag-2-2", // W3C Web Content Accessibility Guidelines 2.2 (Oct 2023 Recommendation)
|
|
165
165
|
"eu-data-act", // EU Data Act / Regulation (EU) 2023/2854 (effective 2025-09-12)
|
|
166
166
|
"hitech", // Health Information Technology for Economic and Clinical Health Act (2009)
|
|
167
167
|
"ferpa", // Family Educational Rights and Privacy Act (20 U.S.C. §1232g)
|
|
168
168
|
"dpdp", // India Digital Personal Data Protection Act 2023 (rules-pending; cascade tier exists)
|
|
169
|
-
// ----
|
|
169
|
+
// ---- privacy 2026 sweep ----
|
|
170
170
|
// US federal child / financial privacy
|
|
171
171
|
"coppa", // Children's Online Privacy Protection Act (15 U.S.C. §6501)
|
|
172
172
|
"coppa-2025", // COPPA 2025 Amendment (FTC final 2025-04-22; effective 2026-06-23 — biometric expansion + knowing-collection disclosure)
|
|
@@ -203,7 +203,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
203
203
|
"eu-cer", // EU Critical Entities Resilience Directive (2022/2557; transposition 2024-10-17)
|
|
204
204
|
"eu-cyber-sol", // EU Cyber Solidarity Act (Regulation 2025/38; effective 2025-02-04)
|
|
205
205
|
"eidas-2", // eIDAS 2 / EUDI Wallet (Regulation 2024/1183; rollout 2026-2027)
|
|
206
|
-
// ----
|
|
206
|
+
// ---- sectoral + cybersecurity directives ----
|
|
207
207
|
"cmmc-2.0", // US DoD Cybersecurity Maturity Model Certification 2.0 (effective 2025-Q1)
|
|
208
208
|
"cjis-v6", // FBI Criminal Justice Information Services Security Policy v6.0 (Dec 2024)
|
|
209
209
|
"iso-27001-2022", // ISO/IEC 27001:2022 — Information Security Management System
|
|
@@ -214,7 +214,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
214
214
|
"nist-800-66-r2", // NIST SP 800-66 Rev 2 — HIPAA Security Rule implementation guidance // NIST publication number, not bytes
|
|
215
215
|
"ehds", // EU European Health Data Space (Regulation 2025/327; phased 2027-2029)
|
|
216
216
|
"circia", // US Cyber Incident Reporting for Critical Infrastructure Act (final rule pending)
|
|
217
|
-
// ----
|
|
217
|
+
// ---- exceptd framework-control-gap closure ----
|
|
218
218
|
// Postures added to recognise every framework cited in the
|
|
219
219
|
// exceptd 2026-05-11 framework-control-gaps catalog. Each posture
|
|
220
220
|
// either (a) maps to a framework the operator must audit against,
|
|
@@ -248,7 +248,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
248
248
|
"cwe-top-25-2024", // CWE Top 25 Most Dangerous Software Weaknesses (2024)
|
|
249
249
|
"cis-controls-v8", // CIS Controls v8
|
|
250
250
|
"cmmc-2.0-level-2", // CMMC 2.0 Level 2 (Advanced) — 110 NIST 800-171 Rev 2 controls // NIST pub number / level, not bytes
|
|
251
|
-
// ----
|
|
251
|
+
// ---- granular CMMC level distinction ----
|
|
252
252
|
// CMMC 2.0 maturity levels carry distinct control-mapping
|
|
253
253
|
// expectations: Level 1 = 15 controls (FAR 52.204-21), Level 2 =
|
|
254
254
|
// 110 controls (NIST 800-171 Rev 2), Level 3 = additional NIST
|
|
@@ -257,7 +257,7 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
257
257
|
// L1/L2/L3 postures are the recommended pin for new deployments.
|
|
258
258
|
"cmmc-2.0-level-1", // CMMC 2.0 Level 1 (Foundational) — 15 FAR controls; FCI-only data // regulatory identifier, not bytes
|
|
259
259
|
"cmmc-2.0-level-3", // CMMC 2.0 Level 3 (Expert) — NIST 800-172 enhanced controls atop L2 // regulatory identifier, not bytes
|
|
260
|
-
// ----
|
|
260
|
+
// ---- promote POSTURE_DEFAULTS-only entries into the
|
|
261
261
|
// canonical KNOWN_POSTURES surface so operators can actually
|
|
262
262
|
// `b.compliance.set(...)` them. Each entry had cascade
|
|
263
263
|
// configuration wired but couldn't be pinned because set()'s
|
|
@@ -757,7 +757,7 @@ var REGIME_MAP = Object.freeze({
|
|
|
757
757
|
"ct-sb3": { name: "Connecticut SB 3 Consumer Health Data", citation: "Conn. P.A. 23-56 (effective 2023-07-01)", jurisdiction: "US-CT", domain: "health" },
|
|
758
758
|
"tx-cubi": { name: "Texas Capture or Use of Biometric Identifier", citation: "Tex. Bus. & Com. Code §503.001 (effective 2009-09-01)", jurisdiction: "US-TX", domain: "biometric" },
|
|
759
759
|
"fl-fdbr": { name: "Florida Digital Bill of Rights", citation: "Fla. Stat. §501.701 et seq. SB 262 (effective 2024-07-01)", jurisdiction: "US-FL", domain: "privacy" },
|
|
760
|
-
// ----
|
|
760
|
+
// ---- AI governance ----
|
|
761
761
|
"co-ai": { name: "Colorado AI Act", citation: "C.R.S. §6-1-1701 et seq. SB24-205 (postponed to 2026-06-30; enforcement stayed)", jurisdiction: "US-CO", domain: "ai-governance" },
|
|
762
762
|
"il-hb3773": { name: "Illinois HB 3773 — AI in Employment", citation: "775 ILCS 5 IHRA AI amendment (effective 2026-01-01)", jurisdiction: "US-IL", domain: "ai-governance" },
|
|
763
763
|
"tx-traiga": { name: "Texas Responsible AI Governance Act", citation: "Tex. Bus. & Com. Code Ch. 552 HB 149 (effective 2026-01-01)", jurisdiction: "US-TX", domain: "ai-governance" },
|
|
@@ -766,20 +766,20 @@ var REGIME_MAP = Object.freeze({
|
|
|
766
766
|
"ca-tfaia": { name: "California Transparency in Frontier AI Act", citation: "Cal. Bus. & Prof. Code §22757.10 et seq. SB 53 (effective 2026-01-01)", jurisdiction: "US-CA", domain: "ai-governance" },
|
|
767
767
|
"kr-ai-basic": { name: "South Korea AI Basic Act", citation: "Framework Act on Development of AI (effective 2026-01-22)", jurisdiction: "KR", domain: "ai-governance" },
|
|
768
768
|
"cn-ai-label": { name: "China — Measures for Labelling AI-Generated Content", citation: "CAC + MIIT + Ministry of Public Security + NRTA Order (effective 2025-09-01)", jurisdiction: "CN", domain: "ai-governance" },
|
|
769
|
-
// ----
|
|
769
|
+
// ---- AI management cross-walks ----
|
|
770
770
|
"iso-42001": { name: "ISO/IEC 42001 — AI Management System", citation: "ISO/IEC 42001:2023", jurisdiction: "international", domain: "ai-governance" },
|
|
771
771
|
"iso-23894": { name: "ISO/IEC 23894 — AI Risk Management", citation: "ISO/IEC 23894:2023", jurisdiction: "international", domain: "ai-governance" },
|
|
772
|
-
// ----
|
|
772
|
+
// ---- content-credentials posture flags ----
|
|
773
773
|
"ca-sb942": { name: "California Gen-AI Provenance Disclosure", citation: "Cal. Bus. & Prof. Code §22757 SB-942 (effective 2026-08-02)", jurisdiction: "US-CA", domain: "content-credentials" },
|
|
774
774
|
"ca-ab853": { name: "California Platform Gen-AI Detection", citation: "Cal. Bus. & Prof. Code §22757 AB-853 (effective 2026-08-02)", jurisdiction: "US-CA", domain: "content-credentials" },
|
|
775
|
-
// ----
|
|
775
|
+
// ---- substrate-to-posture cleanup ----
|
|
776
776
|
"eaa": { name: "EU Accessibility Act", citation: "Directive (EU) 2019/882 (effective 2025-06-28)", jurisdiction: "EU", domain: "accessibility" },
|
|
777
777
|
"wcag-2-2": { name: "W3C Web Content Accessibility Guidelines 2.2", citation: "W3C Recommendation (Oct 2023)", jurisdiction: "international", domain: "accessibility" },
|
|
778
778
|
"eu-data-act": { name: "EU Data Act", citation: "Regulation (EU) 2023/2854 (effective 2025-09-12)", jurisdiction: "EU", domain: "data-sharing" },
|
|
779
779
|
"hitech": { name: "Health Information Technology for Economic and Clinical Health Act", citation: "Pub. L. 111-5, Title XIII, Subtitle D (2009)", jurisdiction: "US", domain: "health" },
|
|
780
780
|
"ferpa": { name: "Family Educational Rights and Privacy Act", citation: "20 U.S.C. §1232g; 34 CFR Part 99", jurisdiction: "US", domain: "student-records" },
|
|
781
781
|
"dpdp": { name: "Digital Personal Data Protection Act 2023", citation: "Act 22 of 2023 (India; rules pending)", jurisdiction: "IN", domain: "privacy" },
|
|
782
|
-
// ----
|
|
782
|
+
// ---- privacy 2026 sweep ----
|
|
783
783
|
// US federal
|
|
784
784
|
"coppa": { name: "Children's Online Privacy Protection Act", citation: "15 U.S.C. §§6501-6506; 16 CFR Part 312 (effective 2000-04-21)", jurisdiction: "US", domain: "child-privacy" },
|
|
785
785
|
"coppa-2025": { name: "COPPA 2025 Amendment", citation: "FTC final rule (2025-04-22; effective 2026-06-23) — biometric expansion + knowing-collection-13-and-under disclosure", jurisdiction: "US", domain: "child-privacy" },
|
|
@@ -815,7 +815,7 @@ var REGIME_MAP = Object.freeze({
|
|
|
815
815
|
"eu-cer": { name: "EU Critical Entities Resilience Directive", citation: "Directive (EU) 2022/2557 (transposition 2024-10-17)", jurisdiction: "EU", domain: "cybersecurity" },
|
|
816
816
|
"eu-cyber-sol": { name: "EU Cyber Solidarity Act", citation: "Regulation (EU) 2025/38 (effective 2025-02-04)", jurisdiction: "EU", domain: "cybersecurity" },
|
|
817
817
|
"eidas-2": { name: "eIDAS 2 / EUDI Wallet", citation: "Regulation (EU) 2024/1183 (rollout 2026-2027)", jurisdiction: "EU", domain: "identity" },
|
|
818
|
-
// ----
|
|
818
|
+
// ---- sectoral + cybersecurity directives ----
|
|
819
819
|
"cmmc-2.0": { name: "Cybersecurity Maturity Model Certification 2.0", citation: "32 CFR Part 170 (DFARS rule effective 2025-Q1)", jurisdiction: "US", domain: "cybersecurity" },
|
|
820
820
|
"cjis-v6": { name: "FBI CJIS Security Policy v6.0", citation: "CJIS Security Policy v6.0 (effective 2024-12)", jurisdiction: "US", domain: "law-enforcement" },
|
|
821
821
|
"iso-27001-2022": { name: "ISO/IEC 27001:2022 Information Security Management System", citation: "ISO/IEC 27001:2022", jurisdiction: "international", domain: "cybersecurity" },
|
|
@@ -826,7 +826,7 @@ var REGIME_MAP = Object.freeze({
|
|
|
826
826
|
"nist-800-66-r2": { name: "NIST SP 800-66 Rev 2 — HIPAA Security Rule Guidance", citation: "NIST SP 800-66 Rev 2 (Feb 2024)", jurisdiction: "US", domain: "health" },
|
|
827
827
|
"ehds": { name: "European Health Data Space", citation: "Regulation (EU) 2025/327 (phased 2027-2029)", jurisdiction: "EU", domain: "health" },
|
|
828
828
|
"circia": { name: "Cyber Incident Reporting for Critical Infrastructure Act", citation: "6 U.S.C. §681 et seq. (final rule pending)", jurisdiction: "US", domain: "cybersecurity" },
|
|
829
|
-
// ----
|
|
829
|
+
// ---- REGIME_MAP backfill for KNOWN_POSTURES without
|
|
830
830
|
// describe() coverage. Each entry resolves `b.compliance.describe
|
|
831
831
|
// (posture)` → { name, citation, jurisdiction, domain } so admin
|
|
832
832
|
// UI / generated audit reports rendering "running under <name>
|
|
@@ -870,7 +870,7 @@ var REGIME_MAP = Object.freeze({
|
|
|
870
870
|
"bsi-c5": { name: "Germany BSI C5 — Cloud Computing Compliance Catalogue", citation: "BSI Cloud Computing Compliance Criteria Catalogue (C5:2020)", jurisdiction: "DE", domain: "cybersecurity" },
|
|
871
871
|
"ens-es": { name: "Spain Esquema Nacional de Seguridad", citation: "Real Decreto 311/2022", jurisdiction: "ES", domain: "cybersecurity" },
|
|
872
872
|
"uk-g-cloud": { name: "UK G-Cloud Framework", citation: "UK Crown Commercial Service G-Cloud 14", jurisdiction: "UK", domain: "cybersecurity" },
|
|
873
|
-
// ----
|
|
873
|
+
// ---- REGIME_MAP backfill (cybersecurity / AI / supply-chain frameworks) ----
|
|
874
874
|
"nist-800-53": { name: "NIST SP 800-53 Rev 5 — Security & Privacy Controls", citation: "NIST SP 800-53 Rev 5", jurisdiction: "US", domain: "cybersecurity" },
|
|
875
875
|
"nist-ai-rmf-1.0": { name: "NIST AI Risk Management Framework 1.0", citation: "NIST AI 100-1 (Jan 2023)", jurisdiction: "US", domain: "ai" },
|
|
876
876
|
"iso-42001-2023": { name: "ISO/IEC 42001:2023 — AI Management System", citation: "ISO/IEC 42001:2023", jurisdiction: "international", domain: "ai" },
|
|
@@ -1176,7 +1176,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1176
1176
|
"nist-800-66-r2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1177
1177
|
"ehds": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1178
1178
|
"circia": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1179
|
-
// ----
|
|
1179
|
+
// ---- exceptd framework-control-gap closure cascade ----
|
|
1180
1180
|
"nist-800-53": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1181
1181
|
// NIST AI-RMF MANAGE.4.3 / ISO 23894 §6.5 / ISO 42001
|
|
1182
1182
|
// §A.6 require encrypted backups for AI system state (model
|
|
@@ -1242,7 +1242,7 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1242
1242
|
"cmmc-2.0-level-1": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1243
1243
|
"cmmc-2.0-level-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1244
1244
|
"cmmc-2.0-level-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
|
|
1245
|
-
// ----
|
|
1245
|
+
// ---- sectoral catch-up ----
|
|
1246
1246
|
// 42 CFR Part 2 — Substance Use Disorder records confidentiality
|
|
1247
1247
|
// (HHS final rule 2024-04-16 aligns Part 2 with HIPAA but retains
|
|
1248
1248
|
// a stricter consent floor; encrypted backups + signed audit chain
|
package/lib/crypto-field.js
CHANGED
|
@@ -235,6 +235,74 @@ function _computeDerivedHash(spec, tableMode, ns, normalized) {
|
|
|
235
235
|
return sha3Hash(vault.getDerivedHashSalt().toString("hex") + ns + normalized);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @primitive b.cryptoField.computeNamespacedHash
|
|
240
|
+
* @signature b.cryptoField.computeNamespacedHash(ns, value, opts?)
|
|
241
|
+
* @since 0.14.10
|
|
242
|
+
* @compliance gdpr, hipaa
|
|
243
|
+
* @related b.cryptoField.computeDerived, b.cryptoField.lookupHash
|
|
244
|
+
*
|
|
245
|
+
* Computes a namespaced indexed-lookup digest of `value` for a
|
|
246
|
+
* pseudo-field that is NOT backed by a registered derived-hash column
|
|
247
|
+
* (e.g. the sealed-token FTS index in `b.mailStore.fts`). The caller
|
|
248
|
+
* supplies the full namespace string directly — there is no schema
|
|
249
|
+
* lookup — so the same keyed/salted hash machinery that protects
|
|
250
|
+
* registered derived hashes also covers ad-hoc indexed tokens. This is
|
|
251
|
+
* the canonical entry point: hand-rolling
|
|
252
|
+
* `sha3Hash(vault.getDerivedHashSalt() + ns + value)` at a call site
|
|
253
|
+
* bypasses the keyed-MAC mode (`hmac-shake256` off
|
|
254
|
+
* `vault.getDerivedHashMacKey`) and the per-deployment salt policy.
|
|
255
|
+
*
|
|
256
|
+
* `opts.mode` selects the digest:
|
|
257
|
+
* - `"salted-sha3"` (default): SHA3-512 over `<salt-hex> + ns + value`
|
|
258
|
+
* (deterministic per deployment; byte-identical to the legacy
|
|
259
|
+
* hand-rolled scheme).
|
|
260
|
+
* - `"hmac-shake256"`: SHAKE256(`<vault MAC key> || ns + value`) — a
|
|
261
|
+
* keyed MAC so an attacker who recovers the salt alone cannot
|
|
262
|
+
* correlate two low-entropy plaintexts.
|
|
263
|
+
*
|
|
264
|
+
* `opts.truncateBytes` truncates the hex digest to that many BYTES
|
|
265
|
+
* (the hex string is sliced to `truncateBytes * 2` characters). Throws
|
|
266
|
+
* (config-time / entry-point tier) on an unknown `mode` or a
|
|
267
|
+
* non-positive-integer `truncateBytes` so an operator catches the typo
|
|
268
|
+
* at boot rather than silently indexing under a malformed digest.
|
|
269
|
+
*
|
|
270
|
+
* @opts
|
|
271
|
+
* mode: string, // "salted-sha3" (default) | "hmac-shake256"
|
|
272
|
+
* truncateBytes: number, // optional; positive integer byte width to slice to
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* var ns = "bj-mail_messages-body:fts:";
|
|
276
|
+
* var h = b.cryptoField.computeNamespacedHash(ns, "kubernetes", {
|
|
277
|
+
* mode: "hmac-shake256", truncateBytes: 8
|
|
278
|
+
* });
|
|
279
|
+
* /^[0-9a-f]{16}$/.test(h); // → true
|
|
280
|
+
*
|
|
281
|
+
* // Default mode is byte-identical to the legacy salted-sha3 hash.
|
|
282
|
+
* b.cryptoField.computeNamespacedHash(ns, "kubernetes").length; // → 128
|
|
283
|
+
*/
|
|
284
|
+
function computeNamespacedHash(ns, value, opts) {
|
|
285
|
+
opts = opts || {};
|
|
286
|
+
var mode = opts.mode || "salted-sha3";
|
|
287
|
+
if (mode !== "salted-sha3" && mode !== "hmac-shake256") {
|
|
288
|
+
throw new Error("computeNamespacedHash: opts.mode must be 'salted-sha3' " +
|
|
289
|
+
"(default) or 'hmac-shake256', got " + JSON.stringify(mode));
|
|
290
|
+
}
|
|
291
|
+
var truncateBytes = opts.truncateBytes;
|
|
292
|
+
if (truncateBytes !== undefined) {
|
|
293
|
+
if (typeof truncateBytes !== "number" || !isFinite(truncateBytes) ||
|
|
294
|
+
truncateBytes <= 0 || Math.floor(truncateBytes) !== truncateBytes) {
|
|
295
|
+
throw new Error("computeNamespacedHash: opts.truncateBytes must be a " +
|
|
296
|
+
"positive integer (bytes), got " + JSON.stringify(truncateBytes));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
var hex = _computeDerivedHash({ mode: mode }, mode, ns, String(value));
|
|
300
|
+
if (truncateBytes !== undefined) {
|
|
301
|
+
return hex.slice(0, truncateBytes * 2);
|
|
302
|
+
}
|
|
303
|
+
return hex;
|
|
304
|
+
}
|
|
305
|
+
|
|
238
306
|
/**
|
|
239
307
|
* @primitive b.cryptoField.getSchema
|
|
240
308
|
* @signature b.cryptoField.getSchema(table)
|
|
@@ -1046,6 +1114,7 @@ module.exports = {
|
|
|
1046
1114
|
applyPosture: applyPosture,
|
|
1047
1115
|
getActivePosture: getActivePosture,
|
|
1048
1116
|
computeDerived: computeDerived,
|
|
1117
|
+
computeNamespacedHash: computeNamespacedHash,
|
|
1049
1118
|
lookupHash: lookupHash,
|
|
1050
1119
|
clearForTest: clearForTest,
|
|
1051
1120
|
declareColumnResidency: declareColumnResidency,
|
package/lib/dsr.js
CHANGED
|
@@ -1078,7 +1078,7 @@ function dbTicketStore(opts) {
|
|
|
1078
1078
|
};
|
|
1079
1079
|
}
|
|
1080
1080
|
|
|
1081
|
-
// ----
|
|
1081
|
+
// ---- US state-law DSR drift registry -------------------
|
|
1082
1082
|
//
|
|
1083
1083
|
// Each US state consumer-privacy law expresses the same DSR core
|
|
1084
1084
|
// (access / deletion / correction / portability) but with per-state
|
package/lib/guard-archive.js
CHANGED
|
@@ -905,7 +905,7 @@ module.exports = {
|
|
|
905
905
|
tarEntryPolicy: tarEntryPolicy,
|
|
906
906
|
};
|
|
907
907
|
|
|
908
|
-
// ----
|
|
908
|
+
// ---- extensions ---------------------------------------------------
|
|
909
909
|
|
|
910
910
|
/**
|
|
911
911
|
* @primitive b.guardArchive.inspect
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -928,7 +928,7 @@ function _audit(auditHandle, action, outcome, metadata) {
|
|
|
928
928
|
} catch (_e) { /* drop-silent — audit failures must not crash callers */ }
|
|
929
929
|
}
|
|
930
930
|
|
|
931
|
-
// ----
|
|
931
|
+
// ---- experimental encrypt/decrypt + WKD ----
|
|
932
932
|
//
|
|
933
933
|
// PQC PGP encrypt/decrypt for ML-KEM-1024 recipients shipped under
|
|
934
934
|
// `experimental` namespace (RFC 9580bis PKESK ML-KEM codepoints
|
package/lib/mail-store-fts.js
CHANGED
|
@@ -54,10 +54,23 @@
|
|
|
54
54
|
* nothing readable; search works against ciphertext.
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
var
|
|
58
|
-
var vault = require("./vault");
|
|
57
|
+
var cryptoField = require("./crypto-field");
|
|
59
58
|
var C = require("./constants");
|
|
60
59
|
|
|
60
|
+
// Sealed-token FTS on-disk format version. Bumped when the token-hash
|
|
61
|
+
// transform changes so the mail-store reindex path can detect a stale
|
|
62
|
+
// index and rebuild it from the sealed messages table. v1 was the
|
|
63
|
+
// legacy salted-sha3-truncated hand-roll; v2 is the keyed
|
|
64
|
+
// hmac-shake256 digest computed via cryptoField.computeNamespacedHash.
|
|
65
|
+
var FTS_FORMAT_VERSION = 2;
|
|
66
|
+
|
|
67
|
+
// Per-token hash width, in bytes. 8 bytes -> 16 hex chars. Full 64-char
|
|
68
|
+
// SHA3 / SHAKE digest is overkill for the FTS hash space, and the
|
|
69
|
+
// shorter token compresses the FTS5 row 4x without observable collision
|
|
70
|
+
// risk at corpus sizes the framework targets (<= 10^9 unique tokens,
|
|
71
|
+
// where 64-bit collision space leaves the birthday bound > 10^9).
|
|
72
|
+
var FTS_HASH_BYTES = 8;
|
|
73
|
+
|
|
61
74
|
// Stopwords are dropped before hashing — they'd dominate every row's
|
|
62
75
|
// token set without adding query selectivity. Kept conservative to
|
|
63
76
|
// stay locale-neutral for v1.
|
|
@@ -154,26 +167,29 @@ function tokenize(text) {
|
|
|
154
167
|
return out;
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
// Hash one token
|
|
158
|
-
//
|
|
170
|
+
// Hash one token through the canonical cryptoField primitive
|
|
171
|
+
// (computeNamespacedHash) so the FTS index inherits the keyed-MAC
|
|
172
|
+
// digest used for derived-hash mirrors on sealed columns. The
|
|
159
173
|
// namespace is per-table, per-field, per-purpose ("fts") so that
|
|
160
|
-
// rotating an operator's vault
|
|
161
|
-
//
|
|
162
|
-
// — full 64-char
|
|
163
|
-
// tokens compress the FTS5 row 4x without
|
|
164
|
-
// at corpus sizes the framework targets
|
|
165
|
-
// 64-bit collision space leaves the
|
|
174
|
+
// rotating an operator's vault key invalidates every FTS row in the
|
|
175
|
+
// same step as every sealed column. Returns a 16-char hex prefix
|
|
176
|
+
// (FTS_HASH_BYTES bytes) — full 64-char digest is overkill for the
|
|
177
|
+
// FTS hash space, and shorter tokens compress the FTS5 row 4x without
|
|
178
|
+
// observable collision risk at corpus sizes the framework targets
|
|
179
|
+
// (<= 10^9 unique tokens, where 64-bit collision space leaves the
|
|
180
|
+
// birthday bound > 10^9).
|
|
166
181
|
/**
|
|
167
182
|
* @primitive b.mailStore.fts.hashToken
|
|
168
183
|
* @signature b.mailStore.fts.hashToken(table, field, token)
|
|
169
184
|
* @since 0.11.25
|
|
170
185
|
* @status stable
|
|
171
186
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* Returns a 16-char hex
|
|
187
|
+
* Keyed hash of one token under the (table, field) namespace. Routes
|
|
188
|
+
* through `b.cryptoField.computeNamespacedHash` in `hmac-shake256`
|
|
189
|
+
* mode — the same keyed-MAC machinery that protects sealed-column
|
|
190
|
+
* derived hashes — so rotating the vault key invalidates every FTS
|
|
191
|
+
* hash in step with every sealed-column hash. Returns a 16-char hex
|
|
192
|
+
* prefix.
|
|
177
193
|
*
|
|
178
194
|
* @example
|
|
179
195
|
* var h = b.mailStore.fts.hashToken("mail_messages", "body", "kubernetes");
|
|
@@ -185,9 +201,10 @@ function hashToken(table, field, token) {
|
|
|
185
201
|
// fields are pseudo-fields (no sealed-column registration), so the
|
|
186
202
|
// canonical fallback path is always the right answer here.
|
|
187
203
|
var ns = "bj-" + table + "-" + field + ":fts:";
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
return cryptoField.computeNamespacedHash(ns, token, {
|
|
205
|
+
mode: "hmac-shake256",
|
|
206
|
+
truncateBytes: FTS_HASH_BYTES,
|
|
207
|
+
});
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
// Hash a token array → space-separated string suitable for FTS5
|
|
@@ -391,4 +408,9 @@ module.exports = {
|
|
|
391
408
|
MAX_TOKEN_LEN: MAX_TOKEN_LEN,
|
|
392
409
|
MAX_TOKENS_PER_FIELD: MAX_TOKENS_PER_FIELD,
|
|
393
410
|
FTS_FIELDS: FTS_FIELDS,
|
|
411
|
+
FTS_HASH_BYTES: FTS_HASH_BYTES,
|
|
412
|
+
// On-disk format marker — the mail-store reindex path stamps this
|
|
413
|
+
// into `<prefix>_meta` once a full rebuild completes; a stale/missing
|
|
414
|
+
// marker triggers a rebuild from the sealed messages table.
|
|
415
|
+
FTS_FORMAT_VERSION: FTS_FORMAT_VERSION,
|
|
394
416
|
};
|
package/lib/mail-store.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
var C = require("./constants");
|
|
61
61
|
var bCrypto = require("./crypto");
|
|
62
62
|
var cryptoField = require("./crypto-field");
|
|
63
|
+
var vault = require("./vault");
|
|
63
64
|
var safeMime = require("./safe-mime");
|
|
64
65
|
var safeSql = require("./safe-sql");
|
|
65
66
|
var guardMessageId = require("./guard-message-id");
|
|
@@ -72,6 +73,14 @@ var DEFAULT_TABLE_PREFIX = "blamejs_mail";
|
|
|
72
73
|
var DEFAULT_MAX_MESSAGE_BYTES = C.BYTES.mib(50);
|
|
73
74
|
var DEFAULT_MAX_BODY_BYTES = C.BYTES.mib(25);
|
|
74
75
|
|
|
76
|
+
// `<prefix>_meta` marker key carrying the FTS on-disk format version.
|
|
77
|
+
// The reindex path writes a `'rebuilding'` sentinel BEFORE clearing the
|
|
78
|
+
// index and the final format-version string only AFTER every row is
|
|
79
|
+
// reinserted, so a partial / interrupted rebuild is never marked
|
|
80
|
+
// complete and never queried as a finished index.
|
|
81
|
+
var FTS_FORMAT_META_KEY = "fts_format";
|
|
82
|
+
var FTS_REBUILDING_SENTINEL = "rebuilding";
|
|
83
|
+
|
|
75
84
|
// Standard IMAP4rev2 default folders + JMAP role mapping.
|
|
76
85
|
var DEFAULT_FOLDERS = Object.freeze([
|
|
77
86
|
{ name: "INBOX", role: "inbox" },
|
|
@@ -129,6 +138,7 @@ function create(opts) {
|
|
|
129
138
|
var qFlags = safeSql.quoteIdentifier(prefix + "_flags", "sqlite");
|
|
130
139
|
var qQuota = safeSql.quoteIdentifier(prefix + "_quota", "sqlite");
|
|
131
140
|
var qFts = safeSql.quoteIdentifier(prefix + "_messages_fts", "sqlite");
|
|
141
|
+
var qMeta = safeSql.quoteIdentifier(prefix + "_meta", "sqlite");
|
|
132
142
|
var messagesTable = prefix + "_messages";
|
|
133
143
|
|
|
134
144
|
var maxMessageBytes = opts.maxMessageBytes !== undefined ? opts.maxMessageBytes : DEFAULT_MAX_MESSAGE_BYTES;
|
|
@@ -150,7 +160,7 @@ function create(opts) {
|
|
|
150
160
|
});
|
|
151
161
|
|
|
152
162
|
if (doInit) {
|
|
153
|
-
_ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts);
|
|
163
|
+
_ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts, qMeta);
|
|
154
164
|
_ensureDefaultFolders(db, qFolders);
|
|
155
165
|
}
|
|
156
166
|
|
|
@@ -207,6 +217,150 @@ function create(opts) {
|
|
|
207
217
|
"INSERT INTO " + qFts + " (objectid, subject_toks, addr_toks, body_toks) VALUES (?, ?, ?, ?)");
|
|
208
218
|
var stmtDeleteFts = db.prepare("DELETE FROM " + qFts + " WHERE objectid = ?");
|
|
209
219
|
|
|
220
|
+
// `<prefix>_meta` marker read — used by the reindex gate at create()
|
|
221
|
+
// AND by every FTS query path (search) to refuse a half-built index.
|
|
222
|
+
// Guarded behind a closure so a store opened with init:false against a
|
|
223
|
+
// pre-format-version database (no _meta table) reads as "no marker"
|
|
224
|
+
// instead of throwing.
|
|
225
|
+
function _readFtsMarker() {
|
|
226
|
+
try {
|
|
227
|
+
var row = db.prepare("SELECT value FROM " + qMeta + " WHERE key = ?")
|
|
228
|
+
.get(FTS_FORMAT_META_KEY);
|
|
229
|
+
return row ? row.value : null;
|
|
230
|
+
} catch (_e) {
|
|
231
|
+
// _meta table absent (init:false against a legacy store) — treat
|
|
232
|
+
// as no marker so the FTS query path falls back rather than
|
|
233
|
+
// querying a possibly-stale index as if it were current.
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// The FTS index is queryable only when the on-disk marker equals the
|
|
238
|
+
// current format version. A `'rebuilding'` sentinel or any other
|
|
239
|
+
// value (stale format, missing marker) means the index is non-final;
|
|
240
|
+
// search() falls back to the modseq cursor rather than returning
|
|
241
|
+
// partial / mixed-scheme FTS hits.
|
|
242
|
+
var CURRENT_FTS_FMT = String(mailStoreFts.FTS_FORMAT_VERSION);
|
|
243
|
+
function _ftsIndexUsable() {
|
|
244
|
+
return _readFtsMarker() === CURRENT_FTS_FMT;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Reindex-on-upgrade. Runs once at create() (when doInit) AFTER the
|
|
248
|
+
// schema is ensured. Rebuilds the sealed-token FTS index from the
|
|
249
|
+
// canonical (sealed) messages table whenever the on-disk format
|
|
250
|
+
// marker is missing or stale — the token-hash transform changed, so
|
|
251
|
+
// every previously-indexed row hashes to a different value and the
|
|
252
|
+
// old rows are unsearchable under the new scheme.
|
|
253
|
+
//
|
|
254
|
+
// Data-safety contract:
|
|
255
|
+
// - A `'rebuilding'` sentinel is written BEFORE the DELETE, so an
|
|
256
|
+
// interrupted rebuild leaves a non-final marker; search() refuses
|
|
257
|
+
// the index until a later create() completes the rebuild.
|
|
258
|
+
// - The DELETE + every reinsert run inside an explicit
|
|
259
|
+
// BEGIN/COMMIT/ROLLBACK (ordinary SQLite — works on node:sqlite
|
|
260
|
+
// and b.db alike; neither exposes a usable `.transaction(fn)()`
|
|
261
|
+
// for this shape). A throw inside the insert loop rolls the whole
|
|
262
|
+
// rebuild back, leaving the OLD index intact + queryable... except
|
|
263
|
+
// the marker still reads as non-final, so search falls back until
|
|
264
|
+
// the next successful create() — the index is never silently
|
|
265
|
+
// half-built.
|
|
266
|
+
// - The final format-version marker is written ONLY after every row
|
|
267
|
+
// reinserts and the COMMIT lands.
|
|
268
|
+
function _reindexFts() {
|
|
269
|
+
var fmt = _readFtsMarker();
|
|
270
|
+
if (fmt === CURRENT_FTS_FMT) return { reindexed: false, reason: "current" };
|
|
271
|
+
|
|
272
|
+
// Count existing FTS rows AFTER the early-return so the common
|
|
273
|
+
// already-current path pays nothing for the scan.
|
|
274
|
+
var ftsCountRow = db.prepare("SELECT COUNT(*) AS n FROM " + qFts).get();
|
|
275
|
+
var ftsCount = (ftsCountRow && ftsCountRow.n) || 0;
|
|
276
|
+
var msgCountRow = db.prepare("SELECT COUNT(*) AS n FROM " + qMsgs).get();
|
|
277
|
+
var msgCount = (msgCountRow && msgCountRow.n) || 0;
|
|
278
|
+
|
|
279
|
+
// Fresh store — no marker AND nothing indexed AND no messages to
|
|
280
|
+
// index. Just stamp the current format; there is nothing to rebuild.
|
|
281
|
+
if (fmt === null && ftsCount === 0 && msgCount === 0) {
|
|
282
|
+
_writeFtsMarker(CURRENT_FTS_FMT);
|
|
283
|
+
return { reindexed: false, reason: "fresh" };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Reindex unseals EVERY message row — a runtime dependency on the
|
|
287
|
+
// vault that a bare create() otherwise wouldn't have. Fail loud at
|
|
288
|
+
// boot (entry-point tier) rather than silently leaving a stale,
|
|
289
|
+
// wrong-scheme index searchable: an operator who constructs the
|
|
290
|
+
// store before vault.init() must see the misordering immediately.
|
|
291
|
+
if (!vault.isInitialized()) {
|
|
292
|
+
throw new MailStoreError("mail-store/fts-reindex-vault-uninitialized",
|
|
293
|
+
"mailStore.create: FTS index format is stale (marker=" +
|
|
294
|
+
JSON.stringify(fmt) + ", current=" + CURRENT_FTS_FMT + ") and a " +
|
|
295
|
+
"reindex from the sealed messages table requires the vault - call " +
|
|
296
|
+
"b.vault.init(...) BEFORE b.mailStore.create(...). Refusing to leave " +
|
|
297
|
+
"a stale, wrong-scheme search index queryable.");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Sentinel BEFORE any destructive work — a crash between here and
|
|
301
|
+
// the final marker leaves the index marked non-final.
|
|
302
|
+
_writeFtsMarker(FTS_REBUILDING_SENTINEL);
|
|
303
|
+
|
|
304
|
+
// BEGIN IMMEDIATE takes the write lock up front so the message
|
|
305
|
+
// snapshot, the FTS delete, and the reinsert are one isolated window.
|
|
306
|
+
// The snapshot is read INSIDE the lock: a concurrent writer in another
|
|
307
|
+
// process cannot append a message between the snapshot and the delete
|
|
308
|
+
// (which would otherwise drop that row's freshly-inserted FTS entry
|
|
309
|
+
// without re-adding it, silently losing it from search).
|
|
310
|
+
var allRows;
|
|
311
|
+
db.prepare("BEGIN IMMEDIATE").run();
|
|
312
|
+
try {
|
|
313
|
+
allRows = db.prepare("SELECT * FROM " + qMsgs).all();
|
|
314
|
+
db.prepare("DELETE FROM " + qFts).run();
|
|
315
|
+
for (var i = 0; i < allRows.length; i += 1) {
|
|
316
|
+
// unsealRow returns the DB column names (subject / from_addr /
|
|
317
|
+
// to_addrs / body_text); map them into the FTS param shape
|
|
318
|
+
// (subject / from / to / body) rowFromMessage expects.
|
|
319
|
+
var clear = cryptoField.unsealRow(messagesTable, allRows[i]);
|
|
320
|
+
var ftsRow = mailStoreFts.rowFromMessage(messagesTable, {
|
|
321
|
+
objectid: clear.objectid,
|
|
322
|
+
subject: clear.subject || "",
|
|
323
|
+
from: clear.from_addr || "",
|
|
324
|
+
to: clear.to_addrs || "",
|
|
325
|
+
body: clear.body_text || "",
|
|
326
|
+
});
|
|
327
|
+
stmtInsertFts.run(ftsRow.objectid, ftsRow.subject_toks,
|
|
328
|
+
ftsRow.addr_toks, ftsRow.body_toks);
|
|
329
|
+
}
|
|
330
|
+
db.prepare("COMMIT").run();
|
|
331
|
+
} catch (e) {
|
|
332
|
+
// Roll the partial rebuild back — the OLD index rows are restored
|
|
333
|
+
// (the DELETE is undone), so the prior scheme stays intact and
|
|
334
|
+
// queryable. The marker still reads as the sentinel, so search()
|
|
335
|
+
// falls back until a later create() completes the rebuild: a
|
|
336
|
+
// retriable, never-silently-half-built state.
|
|
337
|
+
try { db.prepare("ROLLBACK").run(); } catch (_re) { /* best-effort */ }
|
|
338
|
+
throw new MailStoreError("mail-store/fts-reindex-failed",
|
|
339
|
+
"mailStore.create: FTS reindex from the sealed messages table " +
|
|
340
|
+
"failed and was rolled back (the prior index is intact); retry " +
|
|
341
|
+
"after resolving: " + ((e && e.message) || String(e)));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Full rebuild committed — stamp the current format LAST so the
|
|
345
|
+
// index only reads as final once every row is present.
|
|
346
|
+
_writeFtsMarker(CURRENT_FTS_FMT);
|
|
347
|
+
return { reindexed: true, rows: allRows.length };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function _writeFtsMarker(value) {
|
|
351
|
+
db.prepare(
|
|
352
|
+
"INSERT INTO " + qMeta + " (key, value) VALUES (?, ?) " +
|
|
353
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
|
354
|
+
).run(FTS_FORMAT_META_KEY, value);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Wire the reindex into create() AFTER schema bootstrap. A fresh store
|
|
358
|
+
// (no marker, 0 FTS rows, 0 messages) just stamps the format; a store
|
|
359
|
+
// carrying an old-format index rebuilds from the sealed rows.
|
|
360
|
+
if (doInit) {
|
|
361
|
+
_reindexFts();
|
|
362
|
+
}
|
|
363
|
+
|
|
210
364
|
return {
|
|
211
365
|
appendMessage: function (folderName, rawBytes, appendOpts) {
|
|
212
366
|
// Wrap canonical row insert + FTS row insert in a single backend
|
|
@@ -277,6 +431,27 @@ function create(opts) {
|
|
|
277
431
|
var limit = f.limit || 100;
|
|
278
432
|
if (limit > 1000) limit = 1000; // query row cap, not bytes
|
|
279
433
|
|
|
434
|
+
// The FTS index is queryable only when the on-disk format marker
|
|
435
|
+
// is current. A `'rebuilding'` sentinel (mid-rebuild / interrupted
|
|
436
|
+
// rebuild) or a stale/missing marker means the index is non-final
|
|
437
|
+
// — fall back to the bare modseq cursor rather than returning
|
|
438
|
+
// partial or wrong-scheme FTS hits. Returning a subset of the true
|
|
439
|
+
// matches as if it were complete is the corruption hazard the
|
|
440
|
+
// reindex sentinel exists to prevent.
|
|
441
|
+
if (!_ftsIndexUsable()) {
|
|
442
|
+
var nonFinal = stmtQueryByModseq.all(folder.id, sinceModseq, limit);
|
|
443
|
+
return {
|
|
444
|
+
rows: nonFinal.map(function (r) {
|
|
445
|
+
return {
|
|
446
|
+
objectid: r.objectid, modseq: r.modseq, sizeBytes: r.size_bytes,
|
|
447
|
+
internalDate: r.internal_date, legalHold: r.legal_hold === 1,
|
|
448
|
+
};
|
|
449
|
+
}),
|
|
450
|
+
nextModseq: nonFinal.length > 0 ? nonFinal[nonFinal.length - 1].modseq : sinceModseq,
|
|
451
|
+
ftsUnavailable: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
280
455
|
var matchClauses = [];
|
|
281
456
|
function addMatch(filterKey, term) {
|
|
282
457
|
if (!term) return;
|
|
@@ -834,7 +1009,7 @@ function _normalizeMsgId(s) {
|
|
|
834
1009
|
|
|
835
1010
|
// ---- Schema bootstrap ----------------------------------------------------
|
|
836
1011
|
|
|
837
|
-
function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts) {
|
|
1012
|
+
function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts, qMeta) {
|
|
838
1013
|
// Folders table — created first since messages reference folder_id.
|
|
839
1014
|
db.prepare(
|
|
840
1015
|
"CREATE TABLE IF NOT EXISTS " + qFolders + " (" +
|
|
@@ -910,6 +1085,17 @@ function _ensureSchema(db, qMsgs, qFolders, qFlags, qQuota, qFts) {
|
|
|
910
1085
|
// tokens on whitespace exactly — hashes are ASCII-hex-only, so no
|
|
911
1086
|
// Unicode case-fold runs at MATCH time.
|
|
912
1087
|
db.prepare(mailStoreFts.createSql(qFts)).run();
|
|
1088
|
+
|
|
1089
|
+
// Per-prefix key/value metadata table. Holds the FTS on-disk format
|
|
1090
|
+
// marker (`fts_format`) so the reindex path can detect a stale index
|
|
1091
|
+
// and rebuild it. Scoped per table-prefix (NOT PRAGMA user_version,
|
|
1092
|
+
// which is db-global and would collide across stores sharing one
|
|
1093
|
+
// sqlite file).
|
|
1094
|
+
db.prepare(
|
|
1095
|
+
"CREATE TABLE IF NOT EXISTS " + qMeta + " (" +
|
|
1096
|
+
"key TEXT PRIMARY KEY, " +
|
|
1097
|
+
"value TEXT)"
|
|
1098
|
+
).run();
|
|
913
1099
|
}
|
|
914
1100
|
|
|
915
1101
|
function _ensureDefaultFolders(db, qFolders) {
|
package/lib/parsers/index.js
CHANGED
|
@@ -36,12 +36,6 @@
|
|
|
36
36
|
* parsed as `country: false`). Block + flow style;
|
|
37
37
|
* literal `|` and folded `>` block scalars with chomp
|
|
38
38
|
* indicators.
|
|
39
|
-
* env — .env file loader with size cap + schema validation;
|
|
40
|
-
* refuses to expand $VAR references; refuses to silently
|
|
41
|
-
* overwrite existing process.env values unless explicitly
|
|
42
|
-
* opted in. Dev-tooling — production secrets should still
|
|
43
|
-
* come through the operator's secrets-management; this is
|
|
44
|
-
* the local-development convenience.
|
|
45
39
|
* ini — INI / .gitconfig / systemd-unit / php.ini / tox.ini parser.
|
|
46
40
|
* Sections (incl. [parent.child] / [parent "child"] nesting),
|
|
47
41
|
* ; or # comments (inline + leading), single + double quoting
|
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:a284c3a3-4413-4bda-9a99-ef60b057cc3a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-31T11:30:02.144Z",
|
|
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.14.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.14.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.14.
|
|
25
|
+
"version": "0.14.10",
|
|
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.14.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.14.10",
|
|
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.14.
|
|
57
|
+
"ref": "@blamejs/core@0.14.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|