@blamejs/core 0.13.33 → 0.13.34
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/app-shutdown.js +32 -0
- package/lib/cert.js +45 -6
- package/lib/db.js +11 -3
- 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.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.34 (2026-05-29) — **Corrupt TLS certs self-heal at boot, and graceful shutdown no longer loses the final DB flush.** Two failure-mode fixes in the same family as the encrypted-DB recovery in 0.13.33. The cert manager treated a corrupt sealed cert or key worse than a missing one: a missing file re-issues via ACME, but a corrupt one let a raw decrypt error escape out of start(), so the same bad file was read on every boot — an unrecoverable crash loop. A corrupt sealed cert/key is now treated like an absent one and re-issued, and a corrupt derived meta.json is re-derived rather than fatal; the ACME account key (which binds order history) instead fails with an actionable error rather than a raw throw. On the shutdown side, an encrypted database that failed its final re-encrypt used to delete its plaintext working copy anyway, discarding every write since the last periodic flush; it now keeps the working copy so the next boot recovers it. The shutdown orchestrator also gains a hard-deadline watchdog: when the operator delegates signal handling to it, a phase that never settles can no longer hold the process open until the supervisor SIGKILLs it (which would skip the final DB re-encrypt) — the watchdog forces a clean exit, so exit handlers still flush. The wiki production and base compose files set a stop_grace_period above that budget so a docker stop or rolling redeploy lets the re-encrypt finish. **Changed:** *Shutdown watchdog forces a clean, DB-flushing exit if a phase hangs* — The graceful-shutdown orchestrator uses soft per-phase timeouts — on expiry the underlying work keeps running — so a phase that never settles could hold the event loop open past the grace window, after which a container supervisor SIGKILLs the process and skips the final DB re-encrypt. When the operator opts into signal handling, a watchdog now forces `process.exit` `graceMs + forceExitMarginMs` after the signal; exit runs the registered handlers (the DB re-encrypt), so the last flush still happens. A new `forceExitMarginMs` option (default 5000) tunes the headroom; set the container stop grace above `graceMs + forceExitMarginMs`. · *Wiki compose sets stop_grace_period above the shutdown budget* — `examples/wiki/docker-compose.yml` and `docker-compose.prod.yml` now set `stop_grace_period: 40s`. Docker's 10s default would SIGKILL the container before the 30s shutdown budget reaches the DB re-encrypt phase, losing the final flush on a `docker stop` or rolling redeploy. The production note also reminds PaaS platforms that regenerate the compose (Coolify, Dokku, CapRover) to set the stop grace via the platform UI alongside the persistent-storage mount and `--shm-size`. **Fixed:** *A corrupt sealed TLS cert or key re-issues instead of crash-looping at boot* — `b.cert`'s start path read the sealed `cert.pem`/`key.pem` and let a raw unseal/decrypt error escape if the file was truncated or corrupt, so a managed restart read the same bad file on every boot — a crash loop, and worse handling than an absent file (which already re-issues). A corrupt sealed cert/key is now treated like a missing one: it is logged, an audit event is emitted, and the certificate is re-issued via ACME. A corrupt derived `meta.json` is likewise re-derived rather than throwing `cert/bad-meta`. · *Unreadable ACME account key fails with an actionable error, not a raw decrypt throw* — Unlike a re-issuable certificate, the ACME account key binds existing order and authorization history, so it is not silently regenerated on corruption. An unreadable `account/jwk.json.sealed` now raises `cert/account-key-unreadable` naming the file and the recovery (restore from backup, or delete to register a fresh account) instead of letting a raw decrypt/parse error escape out of start(). · *Encrypted DB keeps its working copy when the final shutdown re-encrypt fails* — `db.close()` re-encrypts the tmpfs working copy to `db.enc`, then deletes the working copy. If that final re-encrypt failed (a full `/dev/shm`, a full disk), the delete still ran, discarding every write since the last periodic flush and leaving only the older `db.enc`. The working copy is now kept whenever the re-encrypt fails, so the next boot's integrity-probed recovery picks up the latest writes (and still falls back to `db.enc` if the working copy is itself corrupt). `db.enc` is never modified by this path. **Detectors:** *Cross-artifact guard that stop_grace_period covers the shutdown budget* — A new codebase check fails if either wiki compose file omits `stop_grace_period` or sets it below the orchestrator's `graceMs` plus the watchdog margin read from `lib/app-shutdown.js`, so raising the shutdown budget without bumping the compose — or dropping the setting — cannot silently reopen the SIGKILL-before-re-encrypt data-loss window.
|
|
12
|
+
|
|
11
13
|
- v0.13.33 (2026-05-28) — **Encrypted-mode DB recovers from a corrupt tmpfs working copy instead of crash-looping.** In encrypted-at-rest mode the live SQLite copy is decrypted into a tmpfs working file and re-encrypted to db.enc periodically. If that working copy was corrupted (an unclean shutdown, or a full tmpfs — Docker's /dev/shm defaults to 64 MiB), the boot path trusted it because its mtime was newer than db.enc, so db.init failed its integrity gate with "database disk image is malformed" identically on every boot — an unrecoverable crash loop. db now integrity-probes the newer working copy before trusting it: if it is unreadable, the working copy is discarded and db.enc (the last-good encrypted snapshot) is re-decrypted, so the next boot self-heals. db.enc is never modified by this path, and a genuinely-corrupt db.enc still fails loudly rather than wiping data. The boot error on an unreadable database is now actionable (it names the tmpfs-size cause and the recovery). The wiki production compose also gains the storage settings encrypted mode needs. **Fixed:** *Corrupt tmpfs working copy no longer causes a boot crash loop (encrypted-at-rest mode)* — `db.init`'s crash-recovery path preferred a newer tmpfs working copy over `db.enc` unconditionally. When that copy was corrupt (truncated by an unclean shutdown or a full `/dev/shm`), every boot trusted it and failed the integrity gate the same way — an unrecoverable loop. The newer working copy is now integrity-probed (`PRAGMA quick_check`); if it is unreadable it is discarded and `db.enc` — the last-good encrypted snapshot — is re-decrypted, so boot self-heals. `db.enc` is never modified, so this only ever rolls back to the persistent copy; if `db.enc` is also corrupt, boot still fails loudly (no silent data loss). A regression test pins the recovery. · *Actionable boot error when the database is unreadable* — When SQLite reports a database too corrupt to even run an integrity check, the boot error now names the likely cause and recovery instead of surfacing the raw "database disk image is malformed": in encrypted mode it points at the tmpfs working copy and the most common operational cause (Docker's 64 MiB `/dev/shm` default — raise it via `shm_size` / `--shm-size`), or restoring `db.enc` / the DB file from backup. · *Wiki production compose ships the storage encrypted mode needs* — `examples/wiki/docker-compose.prod.yml` now sets `shm_size: '512m'` (so the encrypted-mode tmpfs working copy has headroom above Docker's 64 MiB default) and mounts a persistent `wiki-data` volume at `/data` (so `db.enc` + sealed keys survive container recreate, host reboot, and image redeploys, and give a restore point). A note flags that PaaS platforms which regenerate the compose on deploy (Coolify, Dokku, CapRover, …) must set both via the platform UI — a persistent-storage mount for `/data` and a `--shm-size 512m` custom option.
|
|
12
14
|
|
|
13
15
|
- v0.13.32 (2026-05-28) — **`b.auditDailyReview` enforces notify under the sox-404 posture; compliance doc corrections.** b.auditDailyReview documented `sox-404` (SOX §404 ICFR — the internal-controls regime this primitive serves) as one of the postures that make a `notify` callback mandatory at construction, but the enforcement set used only `sox`, so pinning `posture: "sox-404"` without a notify channel was silently accepted. `sox-404` is now in the mandatory-notify set, so the advertised guarantee holds (a regression test pins it). The rest are documentation corrections with no behavior change: b.compliance.posturesByDomain / posturesByJurisdiction examples showed small fixed arrays where the functions return every matching posture (the catalog has grown); b.dataAct's surface list named two methods that do not exist (the real surface is declareProduct / recordUserAccess / shareWithThirdParty / recordSwitchRequest, with gatekeeper refusal folded into shareWithThirdParty); b.secCyber.eightKArtifact's documented return key `audit` is actually `deadlineBusinessDays`; and b.compliance.aiAct.transparency's helper summary named `cspMetaTag` / a `watermark({ kind })` argument that are really `metaTags` / `watermark({ mediaKind })`. **Fixed:** *`b.auditDailyReview` requires a notify channel under the `sox-404` posture* — The docs listed `sox-404` among the postures that make a `notify` callback mandatory at create-time, but the enforcement set contained only `sox` — so `posture: "sox-404"` without `notify` was accepted instead of refused. `sox-404` (SOX §404 ICFR) is now in the mandatory-notify set, matching the documented guarantee; constructing without a notify channel under it throws `auditDailyReview/notify-required-under-posture`. · *`b.compliance` jurisdiction/domain lister examples no longer enumerate a stale fixed set* — `posturesByDomain` and `posturesByJurisdiction` return every posture matching the domain/jurisdiction, but their `@example`s showed small fixed arrays from before the posture catalog grew. The examples now show a representative prefix with `...` and note they return the full matching set. · *`b.dataAct` surface list matches the real methods* — The module surface listed `userAccessible(...)` and `refuseGatekeeper(...)`, neither of which exists. The real surface is `declareProduct` / `recordUserAccess` / `shareWithThirdParty` / `recordSwitchRequest`, and DMA-gatekeeper refusal (Art 32 §1) is enforced inside `shareWithThirdParty`. The doc now reflects that. · *`b.secCyber.eightKArtifact` documented return shape corrected* — The signature line showed `{ artifact, deadline, audit }`; the function returns `{ artifact, deadline, deadlineBusinessDays }` (there is no `audit` key). The doc now matches. · *`b.compliance.aiAct.transparency` helper names corrected* — The helper summary named a `cspMetaTag(...)` function and a `watermark({ kind })` argument; the real names are `metaTags(...)` and `watermark({ mediaKind })`. Calling the documented names threw. Also corrected: a `b.aiAdverseDecision` illustration showed an ECOA `statutoryDeadlines` shape that didn't match the regime's actual deadlines.
|
package/lib/app-shutdown.js
CHANGED
|
@@ -53,6 +53,10 @@ var AppShutdownError = defineClass("AppShutdownError", { alwaysPermanent: true }
|
|
|
53
53
|
var log = boot("app-shutdown");
|
|
54
54
|
|
|
55
55
|
var DEFAULT_GRACE_MS = C.TIME.seconds(30);
|
|
56
|
+
// Headroom between the shutdown grace budget and the hard forced-exit
|
|
57
|
+
// watchdog, so the watchdog fires before a container supervisor's own
|
|
58
|
+
// stop-grace SIGKILL (set stop_grace_period > graceMs + this margin).
|
|
59
|
+
var FORCE_EXIT_MARGIN_MS = C.TIME.seconds(5);
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
62
|
* @primitive b.appShutdown.create
|
|
@@ -71,6 +75,7 @@ var DEFAULT_GRACE_MS = C.TIME.seconds(30);
|
|
|
71
75
|
*
|
|
72
76
|
* @opts
|
|
73
77
|
* graceMs: number, // total budget across all phases (default 30000)
|
|
78
|
+
* forceExitMarginMs: number, // headroom after graceMs before the signal-handler watchdog forces exit (default 5000); set the container stop grace above graceMs + this
|
|
74
79
|
* phases: array, // [{ name, run: async fn, timeoutMs? }]
|
|
75
80
|
* installSignalHandlers: boolean, // wire SIGTERM/SIGINT (default false)
|
|
76
81
|
* signals: array, // signal names (default ["SIGTERM","SIGINT"])
|
|
@@ -94,6 +99,9 @@ function create(opts) {
|
|
|
94
99
|
numericBounds.requirePositiveFiniteIntIfPresent(opts.graceMs,
|
|
95
100
|
"app-shutdown.create: opts.graceMs", AppShutdownError, "app-shutdown/bad-grace-ms");
|
|
96
101
|
var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
|
|
102
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.forceExitMarginMs,
|
|
103
|
+
"app-shutdown.create: opts.forceExitMarginMs", AppShutdownError, "app-shutdown/bad-force-exit-margin-ms");
|
|
104
|
+
var forceExitMarginMs = opts.forceExitMarginMs !== undefined ? opts.forceExitMarginMs : FORCE_EXIT_MARGIN_MS;
|
|
97
105
|
var phases = Array.isArray(opts.phases) ? opts.phases.slice() : [];
|
|
98
106
|
var installSignalHandlers = !!opts.installSignalHandlers;
|
|
99
107
|
for (var i = 0; i < phases.length; i++) {
|
|
@@ -224,6 +232,30 @@ function create(opts) {
|
|
|
224
232
|
function _signalCallback(sig) {
|
|
225
233
|
return function () {
|
|
226
234
|
log("received " + sig + " — initiating graceful shutdown");
|
|
235
|
+
// Hard-deadline safety net. Per-phase budgets use a SOFT timeout
|
|
236
|
+
// (withTimeout lets the underlying work keep running on expiry), so
|
|
237
|
+
// shutdown() RESOLVING does not guarantee the process EXITS: a hung
|
|
238
|
+
// phase's leaked handle (a socket that won't close, a timer that keeps
|
|
239
|
+
// firing) can hold the event loop alive past the grace window, after
|
|
240
|
+
// which the supervisor SIGKILLs us and the final DB re-encrypt is lost.
|
|
241
|
+
// Arm an unref'd watchdog that forces a clean exit at the deadline.
|
|
242
|
+
// It is deliberately NOT cleared when shutdown() resolves — the whole
|
|
243
|
+
// point is to catch the case where the orchestration finished but the
|
|
244
|
+
// process won't die. unref() so it never itself keeps us alive: a clean
|
|
245
|
+
// shutdown with no leaked handles exits naturally well before it fires.
|
|
246
|
+
// process.exit() runs the registered exit handlers (db re-encrypts
|
|
247
|
+
// there), so the last flush still happens.
|
|
248
|
+
var watchdog = setTimeout(function () {
|
|
249
|
+
log.error("shutdown exceeded " + (graceMs + forceExitMarginMs) +
|
|
250
|
+
"ms without the process exiting — forcing exit (exit handlers run " +
|
|
251
|
+
"the final DB flush) before the supervisor SIGKILLs");
|
|
252
|
+
// Bounded forced exit after the grace deadline, armed ONLY inside the
|
|
253
|
+
// signal handler (operator opted into installSignalHandlers,
|
|
254
|
+
// delegating process lifecycle to the orchestrator).
|
|
255
|
+
// allow:process-exit — operator-delegated lifecycle, watchdog only
|
|
256
|
+
process.exit(process.exitCode || 1);
|
|
257
|
+
}, graceMs + forceExitMarginMs);
|
|
258
|
+
if (typeof watchdog.unref === "function") watchdog.unref();
|
|
227
259
|
shutdown().then(function (result) {
|
|
228
260
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
229
261
|
process.exitCode = result.ok ? 0 : 1;
|
package/lib/cert.js
CHANGED
|
@@ -54,6 +54,7 @@ var atomicFile = require("./atomic-file");
|
|
|
54
54
|
var safeJson = require("./safe-json");
|
|
55
55
|
var { defineClass } = require("./framework-error");
|
|
56
56
|
var C = require("./constants");
|
|
57
|
+
var { boot } = require("./log");
|
|
57
58
|
|
|
58
59
|
var acme = lazyRequire(function () { return require("./acme"); });
|
|
59
60
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
@@ -62,6 +63,7 @@ var networkTls = lazyRequire(function () { return require("./network-tls"); }
|
|
|
62
63
|
var bCrypto = lazyRequire(function () { return require("./crypto"); });
|
|
63
64
|
|
|
64
65
|
var CertError = defineClass("CertError");
|
|
66
|
+
var log = boot("cert");
|
|
65
67
|
|
|
66
68
|
var DEFAULT_RENEW_INTERVAL_MS = C.TIME.hours(6);
|
|
67
69
|
var DEFAULT_MIN_DAYS_BEFORE_EXPIRY = 14;
|
|
@@ -140,8 +142,13 @@ function _createSealedDiskStorage(opts) {
|
|
|
140
142
|
if (!nodeFs.existsSync(p)) return null;
|
|
141
143
|
try { return safeJson.parse(nodeFs.readFileSync(p, "utf8"), { maxBytes: C.BYTES.kib(16) }); }
|
|
142
144
|
catch (e) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
// meta.json is a derived index (expiry + fingerprint), not a
|
|
146
|
+
// source of truth — the sealed cert is. A corrupt meta must not
|
|
147
|
+
// block renewal: treat it as absent so _ensureCert re-derives it
|
|
148
|
+
// from a fresh issue rather than throwing out of start().
|
|
149
|
+
log.warn("cert: meta.json for '" + certName + "' unreadable (" +
|
|
150
|
+
e.message + ") — treating as absent, will re-derive");
|
|
151
|
+
return null;
|
|
145
152
|
}
|
|
146
153
|
},
|
|
147
154
|
|
|
@@ -413,8 +420,21 @@ function create(opts) {
|
|
|
413
420
|
? nodeFs.readFileSync(nodePath.join(storage.rootDir, "account/jwk.json.sealed"))
|
|
414
421
|
: null;
|
|
415
422
|
if (sealedBuf) {
|
|
416
|
-
var
|
|
417
|
-
|
|
423
|
+
var jwk;
|
|
424
|
+
try {
|
|
425
|
+
var plain = (opts.storage.vault || vault().getDefaultStore()).unseal(sealedBuf);
|
|
426
|
+
jwk = safeJson.parse(plain.toString("utf8"), { maxBytes: C.BYTES.kib(64) });
|
|
427
|
+
} catch (e) {
|
|
428
|
+
// The ACME account key binds existing order + authorization
|
|
429
|
+
// history, so it is NOT auto-regenerated on corruption (unlike a
|
|
430
|
+
// re-issuable cert) — that would silently abandon the account.
|
|
431
|
+
// Fail with an actionable error naming the file + recovery instead
|
|
432
|
+
// of letting a raw decrypt/parse error escape out of start().
|
|
433
|
+
throw new CertError("cert/account-key-unreadable",
|
|
434
|
+
"cert: ACME account key 'account/jwk.json.sealed' is unreadable (" +
|
|
435
|
+
e.message + "). Restore it from backup, or delete it to register " +
|
|
436
|
+
"a fresh ACME account (this abandons prior order history).");
|
|
437
|
+
}
|
|
418
438
|
return _accountKeyFromJwk(jwk);
|
|
419
439
|
}
|
|
420
440
|
var pair = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
@@ -555,10 +575,29 @@ function create(opts) {
|
|
|
555
575
|
// for emergency reissue / key rollover when the existing cert is
|
|
556
576
|
// structurally fine but operationally compromised (suspected key
|
|
557
577
|
// disclosure, CA misissuance investigation, posture-driven rotation).
|
|
578
|
+
// A corrupt sealed cert/key is RECOVERABLE state — the CA re-issues on
|
|
579
|
+
// demand. Treat an unreadable sealed file like a missing one (log +
|
|
580
|
+
// re-issue) rather than letting a raw unseal/decrypt error escape out of
|
|
581
|
+
// start(): on a managed restart the same corrupt file is read on every
|
|
582
|
+
// boot, so throwing here is an unrecoverable crash loop, and a corrupt
|
|
583
|
+
// file must never be handled worse than an absent one (which already
|
|
584
|
+
// falls through to issue). The ACME account key is the one piece NOT
|
|
585
|
+
// auto-recovered this way — see _loadOrGenerateAccountKey.
|
|
586
|
+
async function _readSealedOrReissue(relPath, certName) {
|
|
587
|
+
try {
|
|
588
|
+
return await storage.readSealed(relPath);
|
|
589
|
+
} catch (e) {
|
|
590
|
+
log.warn("cert: sealed '" + relPath + "' unreadable (" + e.message +
|
|
591
|
+
") — re-issuing as if absent");
|
|
592
|
+
_emitAudit("cert.sealed.corrupt", "recovered", { path: relPath, name: certName });
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
558
597
|
async function _ensureCert(certManifest, forceIssue) {
|
|
559
598
|
var meta = await storage.readMeta(certManifest.name);
|
|
560
|
-
var certBuf = await
|
|
561
|
-
var keyBuf = await
|
|
599
|
+
var certBuf = await _readSealedOrReissue(certManifest.name + "/cert.pem", certManifest.name);
|
|
600
|
+
var keyBuf = await _readSealedOrReissue(certManifest.name + "/key.pem", certManifest.name);
|
|
562
601
|
if (!forceIssue && meta && certBuf && keyBuf &&
|
|
563
602
|
meta.expiresAt > Date.now() + minDaysBeforeExpiry * C.TIME.days(1)) {
|
|
564
603
|
// Cached, not due for renewal yet.
|
package/lib/db.js
CHANGED
|
@@ -1997,11 +1997,19 @@ function close() {
|
|
|
1997
1997
|
// Order: encrypt while the DB is still open (so the file is consistent),
|
|
1998
1998
|
// then close the SQLite handle (releases the file lock on Windows),
|
|
1999
1999
|
// THEN unlink the plaintext sidecar files.
|
|
2000
|
-
|
|
2001
|
-
|
|
2000
|
+
var encryptOk = false;
|
|
2001
|
+
try { encryptToDisk(); encryptOk = true; } catch (e) {
|
|
2002
|
+
log.error("close: final encrypt failed: " + e.message +
|
|
2003
|
+
" — keeping the plaintext working copy so the next boot can recover " +
|
|
2004
|
+
"the latest writes (db.enc still holds the prior snapshot)");
|
|
2002
2005
|
}
|
|
2003
2006
|
try { database.close(); } catch (_e) { /* already closed */ }
|
|
2004
|
-
|
|
2007
|
+
// Only discard the plaintext working copy once it has been safely
|
|
2008
|
+
// re-encrypted. If the final encrypt failed (full /dev/shm, disk-full),
|
|
2009
|
+
// the working copy is the ONLY carrier of writes since the last periodic
|
|
2010
|
+
// flush — keep it so decryptToTmp's newer-mtime recovery picks it up next
|
|
2011
|
+
// boot (integrity-probed, falling back to db.enc if it is itself corrupt).
|
|
2012
|
+
if (atRest === "encrypted" && encryptOk) removePlaintextFiles();
|
|
2005
2013
|
database = null;
|
|
2006
2014
|
initialized = false;
|
|
2007
2015
|
}
|
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:315508ca-8141-48dc-aa50-76d4a8892704",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-29T12:27:08.508Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.34",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.34",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.34",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.34",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|