@blamejs/core 0.8.67 → 0.8.69
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/watcher.js +155 -32
- package/package.json +1 -1
- package/sbom.cyclonedx.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.69 (2026-05-10) — test-side `waitUntil` helper + CLAUDE.md §11b convention. Every recurring "test passes alone, fails under SMOKE_PARALLEL=64 / macOS GitHub-Actions runner" flake we've fought across v0.8.55 (rate-limit-cluster), v0.8.60 (watcher), v0.8.63 / v0.8.65 / v0.8.68 (log-stream-otlp / sandbox) was the same root cause: a fixed-budget `setTimeout(r, N)` sleep too short for runner-contention reality. New `test/helpers/wait.js` ships `waitUntil(predicate, opts?)` (polls every `intervalMs` default 25ms up to `timeoutMs` default 5000ms, exits early when predicate truthy, throws labeled error on timeout) + `waitUntilEqual(getter, expected)` convenience wrapper. `test/helpers/index.js` re-exports both. Refactored `test/layer-0-primitives/log-stream-otlp.test.js`'s "collector saw retries" gate (the most-recently-flaked one) to use `waitUntil({ failCount >= 2, dropEvents.length === 1 })` instead of `_sleep(200)` — fast platforms exit in ~30ms, contended platforms get the full budget. CLAUDE.md §11b documents the convention: when you find yourself bumping a hand-tuned sleep to fix a CI flake, that's the smell — convert to `waitUntil`. Future flake fixes update one timeout ceiling instead of N inline budgets.
|
|
12
|
+
- v0.8.68 (2026-05-10) — `b.watcher` polling backend for environments where `fs.watch` doesn't deliver events. Pre-v0.8.68 the watcher used `fs.watch(root, { recursive: true })` exclusively — works on real Linux/macOS/Windows kernels but silent on filesystems where the kernel→userspace event bridge doesn't exist: Docker Desktop bind-mounts on Windows / macOS hosts (gRPC-FUSE / VirtioFS doesn't propagate inotify events from the Linux container's mount through to the host fs the operator runs node on), NFS / SMB mounts, some FUSE filesystems. Add `mode: "fs" | "poll"` (default `"fs"`) — when `mode: "poll"`, the watcher walks the tree on a fixed interval and diffs against the previous snapshot. New file / mtime-change / size-change → `onChange` via the existing debounce + ignore + lstat dispatch; missing path → `onDelete`. `pollIntervalMs` (default 1s) sets cadence; `pollMaxFiles` (default 50000) caps the per-tick walk so a misconfigured root can't stall the event loop stat'ing 100k files every second — overflow refuses with `watcher/poll-overflow`. Symlinks skipped (matches fs.watch path). The initial walk happens synchronously in `create()` so the first event fires only on real post-start changes (not on pre-existing files). `_flushForTest()` runs one synchronous tick + drains pending debounces so polling tests don't have to sleep `pollIntervalMs`. Returned handle gains `.mode` for operator introspection. fs.watch-backend error messages now suggest `mode: "poll"` as the fallback. New Layer 0 polling tests cover create / modify / delete detection across nested directories + mode validation + pollMaxFiles overflow.
|
|
11
13
|
- v0.8.67 (2026-05-10) — SAML XMLDSig Reference Transforms (`enveloped-signature` + per-Reference c14n) + full IdP-emitted SAML round-trip in the federation-auth integration test. Pre-v0.8.67 `b.auth.saml.sp.verifyResponse` only honored the SignedInfo's `CanonicalizationMethod`; it didn't process the `<ds:Transforms>` block on the Reference. Real-world IdP-signed responses (Keycloak, ADFS, Okta) attach `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (strip the `<Signature>` child of the referenced element before c14n) and a per-Reference `xml-exc-c14n#` Transform — without them, the digest computed over the assertion-including-signature never matches the signed-then-signature-injected reality and verifyResponse rejected legitimate IdP responses. `_verifyXmldsig` now reads the Transforms list, applies enveloped-signature by filtering the parsed-tree's `<Signature>` element children before canonicalization, and honors the per-Reference c14n choice (with vs without comments). The single-match-by-ID invariant + signature-wrapping defense moves into the saml.js path directly so the modified subtree (signature stripped) is the one that gets canonicalized + digested. `test/integration/federation-auth.test.js` now drives Keycloak's HTML login form via cookie-jar curl-equivalent (no headless browser needed), captures the IdP-signed SAMLResponse, fetches the IdP signing certificate from `/protocol/saml/descriptor`, hands the response to `sp.verifyResponse(b64, { expectedInResponseTo })`, and asserts the extracted `nameId` / `issuer` / `audience` / `inResponseTo` match the realm's signed claims. Verified end-to-end: Keycloak alice user → SAML AuthnRequest → login form POST → signed Response → `sp.verifyResponse()` → `{ nameId: "alice", issuer: <realm>, audience: <SP entityID>, attributes: { Role: "default-roles-..." } }`. Unsupported Transform algorithms refuse loudly via `auth-saml/unsupported-transform`. No primitive surface change versus v0.8.66 (the Transforms processing is internal to `_verifyXmldsig`).
|
|
12
14
|
- v0.8.66 (2026-05-10) — `b.session.updateData(token, data, opts?)`. Update the sealed `data` payload on a session WITHOUT rotating the sid. Pre-v0.8.66 the only path to mutate session data was `b.session.rotate(token, { data })` which forces an sid rotation — appropriate for security-boundary transitions (login, MFA, role escalation) but heavyweight for cart-state writes / preference flips / step-up-completion flags. Default semantics: full payload replace, `lastActivity` bumped (idle-timeout reset), reserved `__bj_fingerprint` binding preserved automatically so verify() still surfaces drift correctly. `opts.merge: true` does a one-level deep merge into the existing payload; `opts.touchLastActivity: false` skips the idle-timeout bump. Returns `false` for unknown / expired / pre-v0.8.61-raw-format tokens (no throw). Anonymous-session userIds work the same as named userIds. Leader-only. New Layer 0 tests: 13 checks covering replace / merge / null-clear / fingerprint preservation / unknown-token returns / array refusal.
|
|
13
15
|
- v0.8.65 (2026-05-10) — federated-authentication integration test fixture (Keycloak as OIDC OP + SAML IdP). Adds `quay.io/keycloak/keycloak:26.0` to `docker-compose.test.yml` running with realm-import on port `:18080` (HTTP) + `:18081` (Quarkus health). Realm `blamejs-test` boots with one OIDC client (`blamejs-rp-oidc`, secret `blamejs-test-rp-secret`, frontchannel + backchannel logout enabled), one SAML SP client (entityID `https://sp.blamejs-test.example`, RSA-SHA256 assertion signature), and a test user (`alice` / `blamejs-test-password`). New `test/integration/federation-auth.test.js` exercises end-to-end against the live Keycloak: OIDC discovery, `b.auth.oauth.authorizationUrl` (state + nonce + PKCE), password-grant token retrieval, `verifyIdToken` against the realm JWKS, `fetchUserInfo` with `idTokenSub` cross-check, RP-Initiated Logout URL build, `parseFrontchannelLogoutRequest` iss-mismatch refusal, `verifyBackchannelLogoutToken` JWKS-lookup + signature + typ-check failure paths, SAML `buildAuthnRequest` POST to the IdP's `/protocol/saml` endpoint, SP `metadata()` XML emit, and CIBA `startAuthentication` wire-format check (Keycloak's `backchannel_authentication_endpoint` is at `/protocol/openid-connect/ext/ciba/auth`). `b.auth.ciba._postForm` now sets `responseMode: "always-resolve"` so deterministic OAuth-shape error JSON (`{ error, error_description }`) reaches the AuthError-mapping path instead of being rejected as a generic HTTP error. `b.auth.ciba._postForm` URL validation switched from a non-existent `safeUrl.assertHttpUrl` to `safeUrl.parse({ allowedProtocols })`. `scripts/check-services.js` registers `keycloak` + `keycloak-health` (both v4 + v6); `test/helpers/services.js` exposes `URLS.keycloak`. CLAUDE.md release-workflow §6 documents the federation-auth integration test path. OID4VCI / OID4VP / OpenID Federation deferred — Keycloak's `oid4vc-issuer` SPI is preview-only and there's no entity-statement publisher in the base image.
|
package/lib/watcher.js
CHANGED
|
@@ -55,6 +55,17 @@ var audit = lazyRequire(function () { return require("./audit"); });
|
|
|
55
55
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
56
56
|
|
|
57
57
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
58
|
+
// Polling-mode defaults. The polling backend exists for environments
|
|
59
|
+
// where fs.watch's native events don't reach userspace — most commonly
|
|
60
|
+
// Docker Desktop bind-mounts on Windows / macOS hosts (where the
|
|
61
|
+
// inotify events from the Linux container's mount don't propagate
|
|
62
|
+
// through the gRPC-FUSE / VirtioFS bridge to the host fs), or NFS /
|
|
63
|
+
// SMB mounts that don't fire change notifications. Operators opt in
|
|
64
|
+
// explicitly via `mode: "poll"`. Default cadence is 1s per tick;
|
|
65
|
+
// pollMaxFiles caps the per-tick walk so a misconfigured root can't
|
|
66
|
+
// stall the event loop by stat'ing 100k files every second.
|
|
67
|
+
var DEFAULT_POLL_INTERVAL_MS = 1000; // allow:raw-byte-literal — 1-second poll cadence
|
|
68
|
+
var DEFAULT_POLL_MAX_FILES = 50000; // allow:raw-byte-literal — per-tick stat cap
|
|
58
69
|
// Per-watcher event count cap before we self-terminate as a safety net
|
|
59
70
|
// against runaway directories that emit millions of events per minute.
|
|
60
71
|
// Operators with legitimate high-churn directories raise this via opts.
|
|
@@ -167,10 +178,23 @@ function _compileIgnore(patterns) {
|
|
|
167
178
|
};
|
|
168
179
|
}
|
|
169
180
|
|
|
181
|
+
var ALLOWED_MODES = ["fs", "poll"];
|
|
182
|
+
|
|
170
183
|
function _validateOpts(opts) {
|
|
171
184
|
validateOpts.requireObject(opts, "watcher.create", WatcherError, "watcher/bad-opts");
|
|
172
185
|
validateOpts.requireNonEmptyString(opts.root, "root", WatcherError, "watcher/bad-root");
|
|
173
186
|
validateOpts.optionalFiniteNonNegative(opts.debounceMs, "debounceMs", WatcherError, "watcher/bad-debounce-ms");
|
|
187
|
+
if (opts.mode !== undefined && ALLOWED_MODES.indexOf(opts.mode) === -1) {
|
|
188
|
+
throw new WatcherError("watcher/bad-mode",
|
|
189
|
+
"watcher.create: mode must be one of " + ALLOWED_MODES.join(", ") +
|
|
190
|
+
", got " + JSON.stringify(opts.mode));
|
|
191
|
+
}
|
|
192
|
+
validateOpts.optionalPositiveFinite(opts.pollIntervalMs, "pollIntervalMs", WatcherError, "watcher/bad-poll-interval-ms");
|
|
193
|
+
if (opts.pollMaxFiles !== undefined &&
|
|
194
|
+
(typeof opts.pollMaxFiles !== "number" || !isFinite(opts.pollMaxFiles) || opts.pollMaxFiles < 1)) {
|
|
195
|
+
throw new WatcherError("watcher/bad-poll-max-files",
|
|
196
|
+
"watcher.create: pollMaxFiles must be a positive finite integer");
|
|
197
|
+
}
|
|
174
198
|
if (opts.maxPending !== undefined &&
|
|
175
199
|
(typeof opts.maxPending !== "number" || !isFinite(opts.maxPending) || opts.maxPending < 1)) {
|
|
176
200
|
throw new WatcherError("watcher/bad-max-pending",
|
|
@@ -191,6 +215,9 @@ function create(opts) {
|
|
|
191
215
|
var root = path.resolve(opts.root);
|
|
192
216
|
var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
193
217
|
var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
|
|
218
|
+
var mode = opts.mode || "fs";
|
|
219
|
+
var pollIntervalMs = opts.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
|
|
220
|
+
var pollMaxFiles = opts.pollMaxFiles || DEFAULT_POLL_MAX_FILES;
|
|
194
221
|
var onChange = opts.onChange || function () {};
|
|
195
222
|
var onDelete = opts.onDelete || function () {};
|
|
196
223
|
var onError = opts.onError || function () {};
|
|
@@ -292,41 +319,126 @@ function create(opts) {
|
|
|
292
319
|
pending.set(relPath, entry);
|
|
293
320
|
}
|
|
294
321
|
|
|
295
|
-
// ---- start the underlying
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
322
|
+
// ---- start the underlying backend ----
|
|
323
|
+
// pollSnapshot lives at function scope so stop() and _flushForTest()
|
|
324
|
+
// can reach the polling tick state.
|
|
325
|
+
var pollTimer = null;
|
|
326
|
+
var pollSnapshot = null; // Map<relPath, { type, size, mtimeMs }>
|
|
327
|
+
|
|
328
|
+
// Walk the tree honoring `ignore` patterns + the pollMaxFiles cap.
|
|
329
|
+
// Returns the new snapshot Map, OR throws watcher/poll-overflow when
|
|
330
|
+
// the cap is hit (that's an operator-misconfigured root signal — a
|
|
331
|
+
// 100k-file tree under a 1s polling cadence stalls the event loop).
|
|
332
|
+
function _walkPollTree() {
|
|
333
|
+
var snapshot = new Map();
|
|
334
|
+
var fileCount = 0;
|
|
335
|
+
var stack = [""];
|
|
336
|
+
while (stack.length > 0) {
|
|
337
|
+
var relDir = stack.pop();
|
|
338
|
+
var absDir = relDir === "" ? root : path.join(root, relDir);
|
|
339
|
+
var entries;
|
|
340
|
+
try { entries = fs.readdirSync(absDir, { withFileTypes: true }); }
|
|
341
|
+
catch (_e) {
|
|
342
|
+
// Root vanished mid-walk OR an inner dir got deleted between
|
|
343
|
+
// the parent listing and the descent. Skip — the next tick's
|
|
344
|
+
// walk surfaces the deletion via the snapshot diff.
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
348
|
+
var entry = entries[i];
|
|
349
|
+
var relPath = relDir === "" ? entry.name : (relDir + "/" + entry.name);
|
|
350
|
+
// Normalize to forward-slash so glob ignore-matching is
|
|
351
|
+
// consistent with the fs.watch path the operator's hooks see.
|
|
352
|
+
relPath = relPath.split(path.sep).join("/");
|
|
353
|
+
if (isIgnored(relPath)) continue;
|
|
354
|
+
if (entry.isSymbolicLink()) continue; // never follow symlinks
|
|
355
|
+
fileCount += 1;
|
|
356
|
+
if (fileCount > pollMaxFiles) {
|
|
357
|
+
throw new WatcherError("watcher/poll-overflow",
|
|
358
|
+
"watcher.poll: tree exceeds pollMaxFiles=" + pollMaxFiles +
|
|
359
|
+
" — narrow `ignore` patterns OR raise pollMaxFiles, OR switch to mode: \"fs\"");
|
|
360
|
+
}
|
|
361
|
+
var absPath = path.join(absDir, entry.name);
|
|
362
|
+
var st;
|
|
363
|
+
try { st = fs.statSync(absPath); }
|
|
364
|
+
catch (_e) { continue; } // race — entry vanished
|
|
365
|
+
if (entry.isDirectory()) {
|
|
366
|
+
snapshot.set(relPath, { type: "dir", size: 0, mtimeMs: st.mtimeMs });
|
|
367
|
+
stack.push(relPath);
|
|
368
|
+
} else if (entry.isFile()) {
|
|
369
|
+
snapshot.set(relPath, { type: "file", size: st.size, mtimeMs: st.mtimeMs });
|
|
370
|
+
}
|
|
371
|
+
// Other kinds (sockets, FIFOs, devices) — skip.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return snapshot;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function _pollTick() {
|
|
378
|
+
if (stopped) return;
|
|
379
|
+
var next;
|
|
380
|
+
try { next = _walkPollTree(); }
|
|
381
|
+
catch (e) { _safeError(e); return; }
|
|
382
|
+
if (pollSnapshot === null) {
|
|
383
|
+
// First tick — establish the baseline without firing events.
|
|
384
|
+
// Operators get add events on file CREATION after start, not on
|
|
385
|
+
// pre-existing files (matches fs.watch semantics).
|
|
386
|
+
pollSnapshot = next;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Diff: anything in `next` not in `pollSnapshot`, OR with size /
|
|
390
|
+
// mtimeMs different, fires onChange via the same _enqueue path the
|
|
391
|
+
// fs.watch backend uses (so debounce + ignore + lstat dispatch
|
|
392
|
+
// stay uniform). Anything in `pollSnapshot` missing from `next`
|
|
393
|
+
// fires onDelete (via _normalizeAndDispatch's ENOENT branch).
|
|
394
|
+
next.forEach(function (info, relPath) {
|
|
395
|
+
var prev = pollSnapshot.get(relPath);
|
|
396
|
+
if (!prev) { _enqueue(relPath); return; }
|
|
397
|
+
if (prev.size !== info.size || prev.mtimeMs !== info.mtimeMs || prev.type !== info.type) {
|
|
398
|
+
_enqueue(relPath);
|
|
309
399
|
}
|
|
310
|
-
// Both inotify and ReadDirectoryChangesW occasionally fire with
|
|
311
|
-
// an empty filename for the root directory itself — ignore.
|
|
312
|
-
if (rel === "" || rel === ".") return;
|
|
313
|
-
_enqueue(rel);
|
|
314
400
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
401
|
+
pollSnapshot.forEach(function (_info, relPath) {
|
|
402
|
+
if (!next.has(relPath)) _enqueue(relPath);
|
|
403
|
+
});
|
|
404
|
+
pollSnapshot = next;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (mode === "poll") {
|
|
408
|
+
// Establish the initial snapshot synchronously so the first
|
|
409
|
+
// operator-side onChange fires only on real post-start changes.
|
|
410
|
+
try { pollSnapshot = _walkPollTree(); }
|
|
411
|
+
catch (e) {
|
|
412
|
+
throw new WatcherError("watcher/start-failed",
|
|
413
|
+
"watcher.create: initial poll walk failed: " + ((e && e.message) || String(e)));
|
|
414
|
+
}
|
|
415
|
+
pollTimer = setInterval(_pollTick, pollIntervalMs); // allow:setinterval-unref — .unref() called immediately below; timer doesn't pin the event loop
|
|
416
|
+
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
417
|
+
} else {
|
|
418
|
+
try {
|
|
419
|
+
watcherHandle = fs.watch(root, { recursive: true, persistent: true }, function (eventType, filename) {
|
|
420
|
+
if (stopped) return;
|
|
421
|
+
if (!filename) return;
|
|
422
|
+
var rel = filename;
|
|
423
|
+
if (path.isAbsolute(rel) && rel.indexOf(root) === 0) {
|
|
424
|
+
rel = path.relative(root, rel);
|
|
425
|
+
}
|
|
426
|
+
if (rel === "" || rel === ".") return;
|
|
427
|
+
_enqueue(rel);
|
|
428
|
+
});
|
|
429
|
+
watcherHandle.on("error", function (err) { _safeError(err); });
|
|
430
|
+
} catch (e) {
|
|
431
|
+
if (e && (e.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM" || e.code === "ENOSYS")) {
|
|
432
|
+
throw new WatcherError("watcher/recursive-unsupported",
|
|
433
|
+
"watcher.create: recursive watch not supported on this platform/kernel: " +
|
|
434
|
+
((e && e.message) || String(e)) + " — pass mode: \"poll\" to fall back to interval polling");
|
|
435
|
+
}
|
|
436
|
+
throw new WatcherError("watcher/start-failed",
|
|
437
|
+
"watcher.create: fs.watch failed: " + ((e && e.message) || String(e)));
|
|
324
438
|
}
|
|
325
|
-
throw new WatcherError("watcher/start-failed",
|
|
326
|
-
"watcher.create: fs.watch failed: " + ((e && e.message) || String(e)));
|
|
327
439
|
}
|
|
328
440
|
|
|
329
|
-
_safeEmitAudit("watcher.started", { root: root });
|
|
441
|
+
_safeEmitAudit("watcher.started", { root: root, mode: mode });
|
|
330
442
|
|
|
331
443
|
function stop() {
|
|
332
444
|
if (stopped) return;
|
|
@@ -340,13 +452,23 @@ function create(opts) {
|
|
|
340
452
|
try { watcherHandle.close(); } catch (_e) { /* best-effort */ }
|
|
341
453
|
watcherHandle = null;
|
|
342
454
|
}
|
|
343
|
-
|
|
455
|
+
if (pollTimer) {
|
|
456
|
+
try { clearInterval(pollTimer); } catch (_e) { /* best-effort */ }
|
|
457
|
+
pollTimer = null;
|
|
458
|
+
}
|
|
459
|
+
_safeEmitAudit("watcher.stopped", { root: root, mode: mode, eventCount: eventCount });
|
|
344
460
|
}
|
|
345
461
|
|
|
346
462
|
// Test seam — flushes all pending debounce timers immediately so
|
|
347
463
|
// tests don't have to await debounceMs. Not part of the operator
|
|
348
|
-
// contract.
|
|
464
|
+
// contract. In poll mode, also synchronously runs one tick so a
|
|
465
|
+
// test can write a file, call _flushForTest(), and observe the
|
|
466
|
+
// resulting onChange without sleeping for pollIntervalMs.
|
|
349
467
|
function _flushForTest() {
|
|
468
|
+
if (mode === "poll" && !stopped) {
|
|
469
|
+
try { _pollTick(); }
|
|
470
|
+
catch (_e) { /* tests assert via the operator's onChange callback */ }
|
|
471
|
+
}
|
|
350
472
|
var snapshot = Array.from(pending.entries());
|
|
351
473
|
pending.clear();
|
|
352
474
|
for (var i = 0; i < snapshot.length; i += 1) {
|
|
@@ -358,6 +480,7 @@ function create(opts) {
|
|
|
358
480
|
return {
|
|
359
481
|
stop: stop,
|
|
360
482
|
root: root,
|
|
483
|
+
mode: mode,
|
|
361
484
|
_flushForTest: _flushForTest,
|
|
362
485
|
};
|
|
363
486
|
}
|
package/package.json
CHANGED
package/sbom.cyclonedx.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:cf3aa594-6a94-4be5-9952-a87c315ec5eb",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-10T15:19:22.438Z",
|
|
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.69",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.69",
|
|
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.69",
|
|
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.69",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|