@blamejs/core 0.8.77 → 0.8.80
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 +2 -0
- package/lib/config.js +78 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- v0.8.80 (2026-05-10) — **Bug fix — `b.config.loadDbBacked` overlapping-tick race**. `cfg.refresh()` calls `_tick()` directly and the periodic poller also invokes `_tick()` independently. When two ticks overlap (two `refresh()`es back-to-back, or `refresh()` racing a poll), the older read could resolve LAST and overwrite a newer config write — so `admin-save → await cfg.refresh()` was not guaranteed to leave the latest value active when `fetchRows` latency varied across calls. Reproducible by serving a 200ms read followed by a 20ms read; without the fix, the slower (older) result clobbered the faster (newer) one. Fix: every tick claims a monotonic sequence number at start; at apply-time, ticks whose sequence is older than the last-applied sequence drop with a `config.reload.skipped` audit emission (phase `stale-tick`). The high-water mark advances ONLY after `cfg.reload` succeeds — a newer tick whose validation fails must not suppress an older in-flight tick that still has valid data (otherwise `refresh(valid)` followed by `refresh(invalid)` could silently keep stale config active even though the valid update was about to land). Fetch / transform failures short-circuit before the apply path and likewise do NOT advance the watermark.
|
|
12
|
+
- v0.8.78 (2026-05-10) — save-triggered reload for `b.config.loadDbBacked`. Admin save handlers / settings-management UIs that write a row in `_blamejs_config_overrides` now call `await cfg.refresh()` immediately after the write, so the new value is active without waiting for the poll's `intervalMs` tick. The poll stays in place as a safety-net for drift (e.g., direct DB writes outside the admin path). `refresh()` returns a `Promise<void>` of identical shape to `cfg.hydrated`: resolves after the tick settles (success OR audit-on-failure), NEVER rejects so save handlers don't deadlock on a flaky DB. The existing `cfg.subscribe(fn)` continues to fire synchronously inside every successful reload — operators reach for it to invalidate caches / recompute derived state / hot-rebuild middleware that closed over the previous config. Three-tier precedence is documented explicitly in the `@primitive` block: DB-row overlay > `opts.env` baseline > schema `default(...)`.
|
|
11
13
|
- v0.8.77 (2026-05-10) — substantive additive release closing 10 audit clusters surfaced by the 8-agent compliance audit. **OAuth resource-server completeness**: `b.auth.oauth.introspectToken` (RFC 7662), `registerClient` (RFC 7591 — refuses empty redirect_uris), `deviceAuthorization` + `pollDeviceCode` (RFC 8628 with slow_down/authorization_pending handling), `exchangeToken` (RFC 8693 subject+actor delegation), new `b.middleware.protectedResourceMetadata` serving `.well-known/oauth-protected-resource` (draft-ietf-oauth-resource-metadata). **Vendored-deps SBOM**: new `scripts/build-vendored-sbom.js` emits `sbom.vendored.cdx.json` (CycloneDX 1.6) covering every `lib/vendor/*` bundle with per-file SHA-256 + purl + license metadata; wired into `npm-publish.yml` so OSV-Scanner now scans it alongside the primary `sbom.cdx.json` — closes the gap where downstream scanners couldn't see what was actually shipping. **MCP endpoint coverage**: `b.mcp.assertProtocolVersion` (MCP 2025-11-25 §4.1 header), `b.mcp.sampling.guard({ maxRequestsPerSession, maxMessagesPerRequest, maxTokensPerRequest, allowedModelHints })` (HIGH-RISK endpoint — confused-deputy class), `b.mcp.elicitation.guard` (prompt-injection scan + schema-type allowlist + size cap). **ACME completeness**: `revokeCert` (RFC 8555 §7.6), `accountKeyRollover` (§7.3.5), `deactivateAccount` (§7.3.6), `tlsAlpn01KeyAuthorization` (RFC 8737), External Account Binding opt on `newAccount` (§7.3.4 — required by ZeroSSL/Buypass/Google CA) — closes 47-day CA/B forum surface before Mar 2026 effective date. **Permissions-Policy denylist** expanded with `identity-credentials-get`, `attribution-reporting-cross-site`, `publickey-credentials-create`, `join-ad-interest-group`, `run-ad-auction`, `shared-storage`, `shared-storage-select-url`, `smartcard`, `all-screens-capture`, `deferred-fetch` (10 directives — single-file fix). **NIST control crosswalk**: new `b.nistCrosswalk` catalog mapping `800-53r5` (~50 controls), `csf-2.0` (~22 functions), `800-171r3` (~25 requirements), `800-218` (SSDF tasks) to framework primitives — used by operators producing SSPs, POAMs, ATO packages, CMMC self-assessments. **SCIM 2.0 server**: new `b.middleware.scimServer` implementing RFC 7642/7643/7644 — Users + Groups + ServiceProviderConfig + ResourceTypes + Schemas + filter parser (eq/ne/co/sw/ew/pr/gt/ge/lt/le) + GET/POST/PUT/PATCH/DELETE dispatch + bearer-auth callback hook + 1 MiB body cap; the most operator-visible federation gap before this — Okta/Entra/etc. couldn't push users without an external adapter. **CRA + EU AI Act forward-deadline templates**: `b.cra.conformityAssessment` Annex VIII technical dossier scaffold (CE marking, Module routing, vuln-handling auto-fill), `b.complianceAiAct.fundamentalRightsImpactAssessment` (Article 27 FRIA template — mandatory for Annex III §5-8 deployers), `b.complianceAiAct.gpai.trainingDataSummary` (Article 53(1)(d) AI Office template — mandatory 2026-08-02). **C2PA COSE_Sign1 wrap**: new `b.contentCredentials.signCose` produces RFC 9052 COSE_Sign1 CBOR envelope with x5chain header + ML-DSA-87 / ed25519 / es256/384/512 / SLH-DSA-SHAKE-256f algorithms — interops with c2patool / JPEG Trust / Adobe verifiers (current `sign()` ships a blamejs-internal envelope; the new `signCose()` ships the canonical wire format). **US state-law backlog**: 22 new compliance postures (`vcdpa`, `co-cpa`, `ctdpa`, `ucpa`, `tdpsa`, `or-cpa`, `mt-cdpa`, `ia-icdpa`, `in-indpa`, `de-dpdpa`, `nh-nhpa`, `nj-njdpa`, `ky-kcdpa`, `tn-tipa`, `mn-mncdpa`, `ri-ricpa`, `ne-dpa`, `nv-sb370`, `ca-aadc`, `ct-sb3`, `tx-cubi`, plus existing `modpa` + `quebec-25`) registered in `b.compliance` + per-state DSR rules via `b.dsr.stateRules(state)` / `b.dsr.listStateRules()` returning `{ responseDays, extensionDays, cureDays, profilingOptOut, minorOptIn, notes }`. **Operator hook**: `b.middleware.rateLimit` instance gains `.resetAll()` for clean-slate flushing during incident-response (in-memory backends only; cluster backend no-ops per multi-replica race-safety). Cluster backend correctly refuses lest one replica's flush race another's in-flight `take()`. **`b.config.loadDbBacked` gains `transformValue: (row) => string | Promise<string>`** — per-row transform applied between `fetchRows` and schema validation; common shape is unsealing a `b.vault`-sealed ciphertext column so canonical secrets live encrypted-at-rest in `_blamejs_config_overrides`. Per-row failures (transform throws OR returns non-string) emit `config.reload.failed` and skip the row so a single bad row can't crash the poller. **`b.cryptoField` gains `sealDoc` / `unsealDoc` doc-shaped aliases** of the existing `sealRow` / `unsealRow` — same identity, lets downstream tests reach for the document-naming convention when preparing seed objects via raw `INSERT`. **Bug fix — `b.config` reactive `value`**: `cfg.value.X` now reflects the latest validated state after every `reload()` (and every `loadDbBacked` poll). Before this fix, `cfg.value` was a captured property pinned to the create-time object, so `cfg.value.FEATURE_X` stayed stale forever and only `cfg.get("FEATURE_X")` saw updates — the published example in `@primitive b.config.loadDbBacked` was wrong against the implementation. Now backed by a `Object.defineProperty` getter; `cfg.get()` / `cfg.has()` semantics unchanged. **Bug fix — `b.config.loadDbBacked` startup hydration window**: `loadDbBacked` returned a config handle that stayed at env-only defaults for the first `intervalMs` because `safeAsync.repeating` is `setInterval`-shaped (no t=0 fire). The handle now kicks off one immediate hydration `_tick()` on construction and exposes `cfg.hydrated` — a Promise that resolves after the first tick settles. Callers awaiting it before serving traffic get a fully-hydrated config; the Promise NEVER rejects (per-tick failures route through audit, last-good value stays). **`b.middleware._modules.rateLimit.instances()` + module-level `.resetAll()`** — module now keeps a registry of every rate-limit middleware created in the process. Incident-response scripts can enumerate every limiter and flush state across the whole process without threading references through the app code. `create()` registers; `middleware.close()` deregisters. Top-level `resetAll()` returns the count of instances it walked.
|
|
12
14
|
- v0.8.76 (2026-05-10) — CI green-up for v0.8.75. The OSV-Scanner v2 binary refuses to parse SBOMs whose filename doesn't match the CycloneDX recognized-pattern spec — `sbom.cyclonedx.json` is NOT recognized; only `bom.json` / `*.cdx.json` / `*.spdx.json` etc. are. v0.8.75's npm-publish workflow failed with `Failed to parse SBOM "sbom.cyclonedx.json": Invalid SBOM filename`. Renamed the artifact to `sbom.cdx.json` everywhere (workflow generation step, post-process script, OSV scan target, cosign sign target, GH release asset upload, `package.json` `files` array, `scripts/check-pack-against-gitignore.js` allowlist, `.gitignore` allowlist). No primitive surface change versus v0.8.75; published-tarball asset filename changes from `sbom.cyclonedx.json` to `sbom.cdx.json` (consumers reading the SBOM out of the install tree should update the path).
|
|
13
15
|
- v0.8.75 (2026-05-10) — CI green-up for v0.8.73 + v0.8.74. The OSV-Scanner action's v2.3.5 binary removed the `--fail-on-vuln=<severity>` flag; passing it now errors with `flag provided but not defined: -fail-on-vuln` and the entire npm-publish workflow exits 1 before `npm publish` ever runs. v0.8.73 + v0.8.74's npm-publish workflows both failed for this reason (Dependabot bumped osv-scanner-action 2.0.2 → 2.3.5 in PR #8 alongside the v2 flag removal; the workflow was never re-tested under the new binary). v2's default behavior is exit-1-on-ANY-finding — stricter than the v1 `--fail-on-vuln=HIGH` floor, and appropriate for a zero-npm-runtime-dep framework where any surfaced vuln means a vendor refresh is overdue. The framework currently has no findings, so the stricter floor is a no-op at HEAD. No primitive surface change versus v0.8.74.
|
package/lib/config.js
CHANGED
|
@@ -232,8 +232,18 @@ function create(opts) {
|
|
|
232
232
|
*
|
|
233
233
|
* The returned handle is the same shape as `create()` plus:
|
|
234
234
|
* - `.hydrated` — Promise<void> for the first tick
|
|
235
|
+
* - `.refresh()`— run one tick on demand (save-triggered reload);
|
|
236
|
+
* returns Promise<void> that never rejects
|
|
235
237
|
* - `.stop()` — halts the poller
|
|
236
238
|
*
|
|
239
|
+
* Three tiers of precedence (highest wins): the DB-row overlay
|
|
240
|
+
* resolved at each `_tick` > the `opts.env` baseline > defaults
|
|
241
|
+
* declared on the schema (`s.string().default(...)` and friends).
|
|
242
|
+
* The `.subscribe(fn)` callback registered through `create()` fires
|
|
243
|
+
* synchronously inside every successful reload — operators reach for
|
|
244
|
+
* it to invalidate caches, recompute derived state, or hot-rebuild
|
|
245
|
+
* middleware that closed over the previous config value.
|
|
246
|
+
*
|
|
237
247
|
* @opts
|
|
238
248
|
* schema: b.safeSchema instance (required),
|
|
239
249
|
* env: object (env baseline; default process.env),
|
|
@@ -277,6 +287,25 @@ function create(opts) {
|
|
|
277
287
|
* },
|
|
278
288
|
* intervalMs: 30 * 1000,
|
|
279
289
|
* });
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* // Save-triggered reload — admin UI writes a row, fires refresh()
|
|
293
|
+
* // so the new value is active immediately without waiting for
|
|
294
|
+
* // intervalMs. cfg.subscribe(...) sees the change inline.
|
|
295
|
+
* var cfg = b.config.loadDbBacked({
|
|
296
|
+
* schema: s.object({ FEATURE_X: b.config.coerce.boolean().default(false) }),
|
|
297
|
+
* fetchRows: async function () { return await db.all("SELECT key, value FROM _config"); },
|
|
298
|
+
* intervalMs: 5 * 60 * 1000, // safety-net interval
|
|
299
|
+
* });
|
|
300
|
+
* await cfg.hydrated; // boot path waits
|
|
301
|
+
* cfg.subscribe(function (next) { cache.invalidate(); });
|
|
302
|
+
*
|
|
303
|
+
* adminApp.post("/settings", async function (req, res) {
|
|
304
|
+
* await db.run("INSERT OR REPLACE INTO _config(key,value) VALUES (?,?)",
|
|
305
|
+
* req.body.key, req.body.value);
|
|
306
|
+
* await cfg.refresh(); // active immediately
|
|
307
|
+
* res.json({ ok: true });
|
|
308
|
+
* });
|
|
280
309
|
*/
|
|
281
310
|
function loadDbBacked(opts) {
|
|
282
311
|
opts = opts || {};
|
|
@@ -295,8 +324,27 @@ function loadDbBacked(opts) {
|
|
|
295
324
|
ConfigError, "config/bad-transform-value") || null;
|
|
296
325
|
var cfg = create({ schema: opts.schema, env: opts.env, redactKeys: opts.redactKeys });
|
|
297
326
|
var stopped = false;
|
|
327
|
+
// Concurrency guard. _tick() runs `await opts.fetchRows()` + per-row
|
|
328
|
+
// `await transformValue(row)`, so multiple ticks (poll firing while
|
|
329
|
+
// refresh() is in-flight, or two refresh()es back-to-back) can
|
|
330
|
+
// overlap. Without coordination, whichever tick FINISHES last applies
|
|
331
|
+
// its overlay last — and "finishes last" is not "started last" when
|
|
332
|
+
// fetchRows latency varies. The result: an admin save followed by
|
|
333
|
+
// await refresh() can be silently rolled back by an older in-flight
|
|
334
|
+
// tick whose fetchRows started before the save.
|
|
335
|
+
//
|
|
336
|
+
// Fix: every tick claims a monotonic seq at start. At apply time, if
|
|
337
|
+
// a newer tick has already applied (ticksAppliedMax >= my seq), drop
|
|
338
|
+
// — its data is more recent than mine. The seq check + reload are
|
|
339
|
+
// both synchronous (no awaits between them) so the check-and-apply
|
|
340
|
+
// is atomic on Node's single thread. fetch / transform failures do
|
|
341
|
+
// NOT advance ticksAppliedMax: they short-circuit before the apply
|
|
342
|
+
// path, leaving newer ticks free to apply later.
|
|
343
|
+
var ticksStarted = 0;
|
|
344
|
+
var ticksAppliedMax = -1;
|
|
298
345
|
async function _tick() {
|
|
299
346
|
if (stopped) return;
|
|
347
|
+
var mySeq = ++ticksStarted;
|
|
300
348
|
var rows;
|
|
301
349
|
try { rows = await opts.fetchRows(); }
|
|
302
350
|
catch (e) {
|
|
@@ -338,7 +386,26 @@ function loadDbBacked(opts) {
|
|
|
338
386
|
}
|
|
339
387
|
overlay[row.key] = value;
|
|
340
388
|
}
|
|
341
|
-
|
|
389
|
+
// Drop-stale: a tick that started after me has already finished and
|
|
390
|
+
// applied its newer fetch — my overlay would clobber fresher data.
|
|
391
|
+
if (mySeq <= ticksAppliedMax) {
|
|
392
|
+
try {
|
|
393
|
+
lazyAudit().safeEmit({
|
|
394
|
+
action: "config.reload.skipped", outcome: "success",
|
|
395
|
+
metadata: { phase: "stale-tick", mySeq: mySeq, appliedMax: ticksAppliedMax },
|
|
396
|
+
});
|
|
397
|
+
} catch (_e) { /* audit best-effort */ }
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// Advance the watermark ONLY after a successful reload. A newer
|
|
401
|
+
// tick whose validation fails must not suppress an older in-flight
|
|
402
|
+
// tick that still has valid data — otherwise refresh(valid)
|
|
403
|
+
// followed by refresh(invalid) could silently keep the previous
|
|
404
|
+
// config active even though the valid update is about to land.
|
|
405
|
+
try {
|
|
406
|
+
cfg.reload(overlay);
|
|
407
|
+
ticksAppliedMax = mySeq;
|
|
408
|
+
}
|
|
342
409
|
catch (e) {
|
|
343
410
|
try {
|
|
344
411
|
lazyAudit().safeEmit({
|
|
@@ -357,6 +424,16 @@ function loadDbBacked(opts) {
|
|
|
357
424
|
// the established "last-good config stays in place" contract.
|
|
358
425
|
cfg.hydrated = _tick();
|
|
359
426
|
var handle = safeAsync.repeating(_tick, opts.intervalMs, { name: "config-db-reload" });
|
|
427
|
+
// Save-triggered reload — admin save handlers / settings-management
|
|
428
|
+
// UIs invoke cfg.refresh() right after writing a row to drop the
|
|
429
|
+
// intervalMs-worth of staleness latency between save and active.
|
|
430
|
+
// Returns the same Promise<void> shape as cfg.hydrated: resolves
|
|
431
|
+
// after the tick settles (success OR audit-on-failure), never
|
|
432
|
+
// rejects so the save handler never deadlocks on a flaky DB.
|
|
433
|
+
// Subscribers fire synchronously inside cfg.reload() within the
|
|
434
|
+
// tick, matching the save-then-invalidate-cache pattern operators
|
|
435
|
+
// expect when an admin flips a feature flag.
|
|
436
|
+
cfg.refresh = function () { return _tick(); };
|
|
360
437
|
cfg.stop = function () { stopped = true; if (handle) { handle.stop(); handle = null; } };
|
|
361
438
|
return cfg;
|
|
362
439
|
}
|
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.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:34412b93-1297-4f78-8272-084d2928e90a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-11T05:57:08.754Z",
|
|
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.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.80",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.80",
|
|
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.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.80",
|
|
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.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.80",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|