@blamejs/core 0.11.28 → 0.11.29

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.11.x
10
10
 
11
+ - v0.11.29 (2026-05-21) — **JMAP Push — EventSource SSE handler on `b.mail.server.jmap` (RFC 8620 §7.3).** The JMAP listener now exposes a real EventSource handler at `/jmap/eventsource`. The session-resource's `eventSourceUrl` becomes a live push channel: clients connect with `?types=*|<csv>&closeafter=no|state&ping=<seconds>`, the listener subscribes via the operator backend's `mailStore.subscribePush(actor, types, emitFn)` hook, and StateChange events arrive as `event: state` SSE frames with the `{ "@type": "StateChange", changed: {...} }` payload per RFC 8620 §7.4. Keepalive `event: ping` frames default to 30 s (operator-tunable 5-900 s), `closeafter=state` closes after one event for poll-like clients, and the SSE response carries the headers proxies expect (`Cache-Control: no-cache`, `Connection: keep-alive`, `X-Accel-Buffering: no`) so nginx-fronted deployments don't buffer the stream. Without the backend subscribe hook the handler refuses with `503 serverUnavailable`, never silent OK. **Added:** *`eventSourceHandler(req, res)` exposed on the JMAP listener handle* — `b.mail.server.jmap.create(opts).eventSourceHandler` mounts on the operator's HTTP router at whatever path the session-resource's `eventSourceUrl` points to (default `/jmap/eventsource`). Authentication is delegated to the surrounding HTTP middleware (the handler expects `req.user` / `req.actor` to be set); unauthenticated requests return `401 forbidden` per RFC 8620 §1.5. Query-string params parse `types=` (CSV or `*` wildcard), `closeafter=` (`no` | `state`), `ping=` (seconds, clamped 5..900) per §7.3. · *Operator backend hook — `mailStore.subscribePush(actor, types, emitFn)`* — Backends opt in by exposing a `subscribePush` method that accepts (1) the authenticated actor, (2) a parsed `types` list (or `null` for the wildcard `*`), and (3) an `emitFn(event)` callback the backend calls when a state change occurs. The expected event shape is `{ kind: 'StateChange', changed: { <accountId>: { <typeName>: <stateString>, ... }, ... }, pushed?: {...} }`; the listener formats it into the SSE `event: state\ndata: <JSON>\n\n` wire shape. The subscribe call may return either `void` or an `unsubscribe()` function the listener invokes on disconnect. · *SSE wire-correct headers + initial-state hint* — Response headers: `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache`, `Connection: keep-alive`, `X-Accel-Buffering: no` (nginx-specific — disables response buffering on the stream so per-event frames flush immediately). The initial bytes carry `retry: 5000\n\n` (HTML5 SSE reconnect hint — 5 s) + a `: connected\n\n` comment so clients can confirm the channel is alive before the first state event. **Security:** *Push backend missing returns `503 serverUnavailable` (no silent accept)* — If the operator wired the listener without `mailStore.subscribePush`, the handler returns `503 urn:ietf:params:jmap:error:serverUnavailable` with a `description` pointing at the missing hook. RFC 8620 §7.3 expects the server to honour subscriptions or refuse explicitly — silently accepting would let a client believe events will arrive when the server cannot deliver. · *`closeafter` accepts only `no` | `state` (RFC 8620 §7.3)* — Any other value returns `400 invalidArguments` before the subscribe hook fires. Prevents an operator-supplied query string from steering the handler into an undocumented mode. · *Ping interval clamped 5..900 seconds* — `ping=<seconds>` below 5 s or above 900 s clamps to the bounds rather than refusing — a misconfigured client cannot DoS the listener via 1 ms ping floods, and a 24 h ping won't outlast intermediate proxy idle timeouts (typical 60-120 s). The clamped default (30 s) matches the RFC 8620 §7.3 example. · *Ping timer is `.unref()`'d* — Background timers without `.unref()` pin the Node process — graceful shutdown waits indefinitely. The interval is unref'd immediately after creation; per-connection cleanup (`req.on('close')` / `req.on('error')`) clears the timer + invokes the backend's optional `unsubscribe()` + ends the response. **References:** [RFC 8620 (JMAP Core — §7 Push / §7.3 EventSource / §7.4 StateChange)](https://www.rfc-editor.org/rfc/rfc8620.html) · [RFC 8621 (JMAP Mail)](https://www.rfc-editor.org/rfc/rfc8621.html) · [RFC 8887 (JMAP WebSocket transport — opt-in, deferred to a later slice)](https://www.rfc-editor.org/rfc/rfc8887.html) · [HTML5 Server-Sent Events (EventSource)](https://html.spec.whatwg.org/multipage/server-sent-events.html)
12
+
11
13
  - v0.11.28 (2026-05-21) — **IMAP opt-in extensions: NOTIFY (RFC 5465), METADATA (RFC 5464), CATENATE (RFC 4469).** Three IMAP extensions advertised in CAPABILITY and dispatched through the existing per-method registry. NOTIFY accepts a client subscription spec and hands it to the operator's `mailStore.subscribeNotify(actor, spec, emitFn)` hook — actual event emission stays operator-side. METADATA exposes GETMETADATA and SETMETADATA per-mailbox + server-wide annotations through `mailStore.getMetadata` / `setMetadata`. CATENATE extends APPEND to compose a message from existing parts (`TEXT {N}` literals + `URL "imap://..."`) via `mailStore.appendCatenate`. Each handler refuses gracefully (`NO ... backend not configured`) when the operator backend doesn't supply the hook. COMPRESS=DEFLATE (RFC 4978) intentionally NOT advertised — CRIME-class compression-oracle threat on the encrypted IMAP stream. **Added:** *CAPABILITY advertises `NOTIFY`, `METADATA`, `METADATA-SERVER`, `CATENATE`* — All four added unconditionally so capable clients can exercise the extension regardless of authentication state. Each handler is registered in the protocol verb catalogue (`b.mail.serverRegistry`) + the wire-level guard verb list (`b.guardImapCommand.KNOWN_VERBS`) so the existing dispatch + audit + ratelimit gates apply uniformly. · *`NOTIFY SET ...` / `NOTIFY NONE` — RFC 5465* — The handler parses `NOTIFY SET [STATUS] (<filter-set> (<event>...))*` and `NOTIFY NONE` and stores the filter-set verbatim on `state.notifySpec`. When the operator backend exposes `mailStore.subscribeNotify(actor, spec, emitFn)`, the listener wires an `emitFn` that translates backend events (`{ kind: 'STATUS' | 'LIST' | 'FETCH', payload, seq? }`) into untagged IMAP responses on the same connection — drop-silent if the socket has already closed. Without the backend hook, the wire command refuses with `NO NOTIFY backend not configured` rather than silently accepting subscriptions the server can't fulfil. · *`GETMETADATA` / `SETMETADATA` — RFC 5464* — Both verbs parse the per-mailbox + server-wide annotation forms. GETMETADATA accepts optional `(MAXSIZE N)` / `(DEPTH ...)` options before the mailbox + entry list, walks the entries through `mailStore.getMetadata(actor, mailbox, names, opts) → [{ entry, value }]`, and renders an untagged `* METADATA <mailbox> (<entry> <value>...)` response. SETMETADATA tokenises the entry/value pairs (quoted-strings + NIL for clearing), validates the mailbox name, and forwards to `mailStore.setMetadata(actor, mailbox, entries)`. Without the backend hooks, both return `NO ... backend not configured`. · *APPEND `CATENATE` modifier — RFC 4469* — `APPEND mailbox [flags] [date-time] CATENATE (...)` is recognised before the legacy literal-required APPEND path. The parts list mixes `TEXT {N}` literal-bytes parts (handed in via the literal-aware parser) and `URL "imap://..."` reference parts; the listener bundles them into `parts: [{ kind: 'TEXT', bytes } | { kind: 'URL', url }]` and forwards to `mailStore.appendCatenate(mailbox, parts, { actor, flags, internalDate }) → { uid, uidValidity }`. When the backend returns the APPENDUID metadata the response carries `OK [APPENDUID <validity> <uid>] APPEND completed` (RFC 4315). Without the backend hook, refuses with `NO CATENATE backend not configured`. **Security:** *COMPRESS=DEFLATE intentionally NOT advertised (CRIME-class)* — RFC 4978 IMAP COMPRESS=DEFLATE enables stream compression that interacts badly with TLS — the CRIME attack class (CVE-2012-4929, BREACH, et al.) recovers plaintext via chosen-plaintext compression-ratio analysis. The framework default is OFF; operators with explicit threat models accept the downgrade via `opts.compress = true` (no opt-in path landed in v1, intentionally — defer-with-condition: open when an operator surfaces a deployment that needs it AND can document the chosen-plaintext threat model is mitigated). · *Mailbox-name validation reused for both METADATA verbs* — Both GETMETADATA and SETMETADATA run `_validateMailboxName` on the parsed mailbox argument (except for the empty-string `""` server-wide-metadata special case per RFC 5464 §3.1). Operators with the existing `allowLegacyMUtf7` opt see the same mailbox-name policy as the rest of the listener; injection-shape mailbox names are refused identically. · *NOTIFY backend-missing returns NO (not silent accept)* — If the operator wired the listener without `mailStore.subscribeNotify`, `NOTIFY SET ...` returns `NO NOTIFY backend not configured` — never a silent `OK`. RFC 5465 §6 specifies NO as the correct refusal shape; silent acceptance would let a client believe events will arrive when the server cannot fulfil the subscription. **References:** [RFC 5465 (IMAP NOTIFY)](https://www.rfc-editor.org/rfc/rfc5465.html) · [RFC 5464 (IMAP METADATA)](https://www.rfc-editor.org/rfc/rfc5464.html) · [RFC 4469 (IMAP CATENATE)](https://www.rfc-editor.org/rfc/rfc4469.html) · [RFC 4315 (IMAP UIDPLUS — APPENDUID response)](https://www.rfc-editor.org/rfc/rfc4315.html) · [RFC 4978 (IMAP COMPRESS — NOT enabled; CRIME-class threat)](https://www.rfc-editor.org/rfc/rfc4978.html) · [CVE-2012-4929 (CRIME — compression-oracle attack on TLS)](https://nvd.nist.gov/vuln/detail/CVE-2012-4929)
12
14
 
13
15
  - v0.11.27 (2026-05-20) — **IMAP CONDSTORE (RFC 7162) — modseq-aware FETCH + STORE on `b.mail.server.imap`.** The IMAP listener advertises and honours the CONDSTORE extension. Clients that issue `ENABLE CONDSTORE` get MODSEQ attributes in every untagged FETCH response; FETCH parses the `(CHANGEDSINCE <modseq>)` modifier and forwards it to the operator's backend so the backend can prune unchanged rows server-side; STORE parses the `(UNCHANGEDSINCE <modseq>)` conditional-update modifier and surfaces the backend's MODIFIED conflict set in the tagged OK response (`OK [MODIFIED <set>] STORE completed`). The backend interface picks up four new opts on the existing `fetchRange` / `storeFlags` calls: `changedSince`, `includeVanished`, `includeModseq`, `unchangedSince`. Backends MAY return modseq on each row; the listener injects `MODSEQ (<n>)` into the payload when present and CONDSTORE is enabled. QRESYNC (RFC 7162 §3.2) is deferred — accepted in ENABLE but no vanished-set surface is exposed yet. **Added:** *CAPABILITY advertises `CONDSTORE` unconditionally* — Per RFC 7162 §3 servers advertise CONDSTORE; clients ENABLE before relying on MODSEQ in untagged FETCH responses. The advertisement is unconditional (state-independent) so clients that issue CAPABILITY pre-LOGIN see CONDSTORE in the same untagged-response shape they'll see post-LOGIN. The old SELECT-side `HIGHESTMODSEQ` emission keeps working. · *`ENABLE CONDSTORE` handler flips `state.enabledCondStore`* — Replaces the no-op `OK ENABLED` shortcut with a real handler that parses the requested capability set, flips `state.enabledCondStore = true` on CONDSTORE, and replies with `ENABLED CONDSTORE` + `OK ENABLE completed`. Unknown extensions are silently ignored per RFC 5161 §3.1. QRESYNC is recognised but accepted only when a v1+ backend exposes the vanished-set surface. · *FETCH parses `(CHANGEDSINCE <modseq>)` + injects MODSEQ in responses* — When the FETCH args carry a trailing `(CHANGEDSINCE <n>)` modifier (RFC 7162 §3.1.4) the listener strips it from the fetch-att spec and forwards `opts.changedSince` to `mailStore.fetchRange`. The backend can prune unchanged messages server-side. When CONDSTORE is enabled (or the client explicitly requested MODSEQ as a fetch-att), each untagged FETCH response includes `MODSEQ (<n>)` — synthesised from `row.modseq` if the backend supplies it and the payload doesn't already contain it. Also recognises the QRESYNC `VANISHED` modifier (flag forwarded as `opts.includeVanished`); the vanished-set emission is the backend's responsibility for now. · *STORE parses `(UNCHANGEDSINCE <modseq>)` + emits `[MODIFIED <set>]` on conflict* — Per RFC 7162 §3.1.3 the conditional STORE refuses to update messages whose modseq advanced past `unchangedSince` since the client last fetched. The listener parses the modifier between the seq-set and the FLAGS op, forwards `opts.unchangedSince` to `mailStore.storeFlags`, and accepts either the legacy `rows: [...]` shape or a structured `{ rows, modified }` shape. When `modified` is non-empty, the tagged response carries `OK [MODIFIED <set>] STORE completed` so the client knows which messages need re-fetching before retry. Untagged FETCH responses also include `MODSEQ (<n>)` when STORE accepted updates under CONDSTORE. **Security:** *Modifier parsing is bounded + non-greedy* — The CHANGEDSINCE / UNCHANGEDSINCE matchers use `[^)]*` rather than `.*` so a malformed modifier can't consume the entire fetch-att spec. Both modifiers parse `\d+` only — non-integer / negative / Infinity values are silently dropped (the modifier becomes a no-op), so a client cannot ride the modifier to inject arbitrary fragments into the backend opts. · *Modseq attribute is opt-in* — MODSEQ injection into untagged FETCH responses ONLY happens when (a) CONDSTORE is enabled OR (b) the client's fetch-att spec contains the `MODSEQ` keyword. Pre-CONDSTORE clients see exactly the responses they saw before this release. The IMAP wire-format compatibility line is unchanged for the IMAP4rev1 / IMAP4rev2 cohorts that never issue `ENABLE CONDSTORE`. **References:** [RFC 7162 (IMAP4 CONDSTORE / QRESYNC)](https://www.rfc-editor.org/rfc/rfc7162.html) · [RFC 9051 (IMAP4rev2)](https://www.rfc-editor.org/rfc/rfc9051.html) · [RFC 5161 (IMAP ENABLE Extension)](https://www.rfc-editor.org/rfc/rfc5161.html) · [RFC 4315 (IMAP UIDPLUS Extension)](https://www.rfc-editor.org/rfc/rfc4315.html)
@@ -502,6 +502,174 @@ function create(opts) {
502
502
  });
503
503
  }
504
504
 
505
+ // RFC 8620 §7.3 — EventSource (Server-Sent Events) push channel.
506
+ // Clients connect to `/jmap/eventsource?types=...&closeafter=...&ping=N`.
507
+ // Server holds the connection open and writes `event: state` + JSON
508
+ // payloads when the operator backend reports a state change.
509
+ // Periodic `event: ping` keeps intermediate proxies / load-balancers
510
+ // from closing the idle connection.
511
+ //
512
+ // closeafter=state — close after first state event (poll-like).
513
+ // closeafter=no (default) — keep open until disconnect.
514
+ // ping=<seconds> — keepalive interval (default 30s, min 5s, max 900s).
515
+ function eventSourceHandler(req, res) {
516
+ var actor = req.user || (req.actor || null);
517
+ if (!actor) {
518
+ res.statusCode = 401;
519
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
520
+ res.end(JSON.stringify({
521
+ type: "urn:ietf:params:jmap:error:forbidden",
522
+ description: "Authentication required",
523
+ }));
524
+ return;
525
+ }
526
+ if (typeof opts.mailStore.subscribePush !== "function") {
527
+ res.statusCode = 503;
528
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
529
+ res.end(JSON.stringify({
530
+ type: "urn:ietf:params:jmap:error:serverUnavailable",
531
+ description: "Push subscribe backend not configured (mailStore.subscribePush)",
532
+ }));
533
+ return;
534
+ }
535
+ // Parse query params from the URL. The HTTP server hands `req.url`
536
+ // with the query intact; we don't depend on Node's URL constructor
537
+ // for the query-string parse — small inline scan is enough.
538
+ var url = String(req.url || "");
539
+ var qIdx = url.indexOf("?");
540
+ var query = qIdx === -1 ? "" : url.slice(qIdx + 1);
541
+ var params = Object.create(null);
542
+ query.split("&").forEach(function (pair) {
543
+ if (!pair) return;
544
+ var eq = pair.indexOf("=");
545
+ var k = eq === -1 ? pair : pair.slice(0, eq);
546
+ var v = eq === -1 ? "" : pair.slice(eq + 1);
547
+ try { params[decodeURIComponent(k)] = decodeURIComponent(v); }
548
+ catch (_e) { /* drop-silent — malformed % encoding */ }
549
+ });
550
+ var typesStr = params.types || "*";
551
+ var types = typesStr === "*"
552
+ ? null
553
+ : typesStr.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
554
+ var closeAfter = (params.closeafter || "no").toLowerCase();
555
+ if (closeAfter !== "no" && closeAfter !== "state") {
556
+ res.statusCode = 400;
557
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
558
+ res.end(JSON.stringify({
559
+ type: "urn:ietf:params:jmap:error:invalidArguments",
560
+ description: "closeafter must be 'no' or 'state' (RFC 8620 §7.3)",
561
+ }));
562
+ return;
563
+ }
564
+ // RFC 8620 §7.3 — `ping=0` is the EXPLICIT opt-out for the
565
+ // keepalive event channel. Treat it as "no ping" rather than
566
+ // clamping to the default. Any other non-finite / out-of-band
567
+ // value falls back to the 30 s default; in-band values
568
+ // (5..900 s) pass through unchanged so clients see the same
569
+ // negotiated interval they requested.
570
+ var pingN;
571
+ var pingDisabled = false;
572
+ if (params.ping === "0") {
573
+ pingDisabled = true;
574
+ pingN = 0;
575
+ } else {
576
+ pingN = parseInt(params.ping, 10);
577
+ if (!isFinite(pingN) || pingN < 5) pingN = 30; // allow:raw-byte-literal — RFC 8620 §7.3 default ping seconds
578
+ if (pingN > 900) pingN = 900; // allow:raw-byte-literal — operator-supplied ping seconds, not bytes // allow:raw-time-literal — explicit max-ping cap (15 minutes)
579
+ }
580
+
581
+ // SSE wire headers per the HTML5 spec § "Server-sent events"
582
+ // and RFC 8620 §7.3 — Content-Type MUST be `text/event-stream`,
583
+ // intermediates MUST NOT cache (`Cache-Control: no-cache`),
584
+ // `Connection: keep-alive` instructs proxies to leave it open.
585
+ res.statusCode = 200;
586
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
587
+ res.setHeader("Cache-Control", "no-cache");
588
+ res.setHeader("Connection", "keep-alive");
589
+ res.setHeader("X-Accel-Buffering", "no"); // disables nginx response buffering on the EventSource stream
590
+ // Initial event tells the client the stream is alive + carries
591
+ // the current session state so a fresh subscriber can compare
592
+ // against its cached `state` to know whether a missed update
593
+ // happened during the (re)connect.
594
+ res.write("retry: 5000\n\n"); // allow:raw-byte-literal — SSE reconnect-after hint (5s)
595
+ res.write(": connected\n\n");
596
+
597
+ var closed = false;
598
+ var pingTimer = null;
599
+ var unsubscribe = null;
600
+
601
+ function _send(eventName, data) {
602
+ if (closed) return;
603
+ try {
604
+ res.write("event: " + eventName + "\n");
605
+ res.write("data: " + (typeof data === "string" ? data : JSON.stringify(data)) + "\n\n");
606
+ } catch (_e) {
607
+ // Socket already torn down — clean up.
608
+ _cleanup();
609
+ }
610
+ }
611
+
612
+ function _cleanup() {
613
+ if (closed) return;
614
+ closed = true;
615
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
616
+ if (typeof unsubscribe === "function") {
617
+ try { unsubscribe(); } catch (_e) { /* silent-catch: drop-silent — unsubscribe is best-effort cleanup */ }
618
+ }
619
+ try { res.end(); } catch (_e) { /* silent-catch: drop-silent — socket already torn down */ }
620
+ }
621
+
622
+ function _pingTick() {
623
+ if (closed) return;
624
+ // RFC 8620 §7.3 — ping payload carries `{ "interval": <N> }` so
625
+ // clients can detect stale connections via interval drift and
626
+ // tell whether the server clamped their requested value.
627
+ var pingPayload = JSON.stringify({ interval: pingN });
628
+ try { res.write("event: ping\ndata: " + pingPayload + "\n\n"); }
629
+ catch (_e) { _cleanup(); }
630
+ }
631
+
632
+ // Operator-supplied emit-fn — the backend pushes
633
+ // { kind: "StateChange", changed: { <accountId>: { <type>: <state> } } }
634
+ // events into the SSE stream. The listener formats per RFC 8620
635
+ // §7.4 — `event: state` carries the StateChange object body.
636
+ var emitFn = function (event) {
637
+ if (!event || closed) return;
638
+ if (event.kind === "StateChange") {
639
+ _send("state", {
640
+ "@type": "StateChange",
641
+ changed: event.changed || {},
642
+ pushed: event.pushed || undefined,
643
+ });
644
+ if (closeAfter === "state") {
645
+ _cleanup();
646
+ }
647
+ }
648
+ };
649
+
650
+ Promise.resolve()
651
+ .then(function () { return opts.mailStore.subscribePush(actor, types, emitFn); })
652
+ .then(function (unsub) {
653
+ if (closed) {
654
+ if (typeof unsub === "function") { try { unsub(); } catch (_e) { /* silent-catch: drop-silent — unsubscribe is best-effort cleanup */ } }
655
+ return;
656
+ }
657
+ unsubscribe = typeof unsub === "function" ? unsub : null;
658
+ if (!pingDisabled) {
659
+ pingTimer = setInterval(_pingTick, pingN * 1000); // allow:raw-time-literal — seconds → ms conversion // allow:raw-byte-literal — not bytes, time conversion
660
+ if (pingTimer && typeof pingTimer.unref === "function") pingTimer.unref();
661
+ }
662
+ })
663
+ .catch(function (err) {
664
+ _emit("mail.server.jmap.push_subscribe_threw",
665
+ { error: (err && err.message) || String(err) }, "failure");
666
+ _cleanup();
667
+ });
668
+
669
+ req.on("close", _cleanup);
670
+ req.on("error", _cleanup);
671
+ }
672
+
505
673
  function discoveryHandler(req, res) {
506
674
  // RFC 8620 §2.2 — well-known endpoint redirects (or directly returns)
507
675
  // the session URL. We redirect to /jmap/session per the most common
@@ -518,6 +686,7 @@ function create(opts) {
518
686
  apiHandler: apiHandler,
519
687
  sessionHandler: sessionHandler,
520
688
  discoveryHandler: discoveryHandler,
689
+ eventSourceHandler: eventSourceHandler,
521
690
  MailServerJmapError: MailServerJmapError,
522
691
  };
523
692
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.11.28",
3
+ "version": "0.11.29",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:e497b463-3374-4f8a-84c2-9686c0567a23",
5
+ "serialNumber": "urn:uuid:bb88c938-c54a-4094-afe0-0fa2adc94cbd",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-21T14:49:20.894Z",
8
+ "timestamp": "2026-05-21T15:36:40.156Z",
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.11.28",
22
+ "bom-ref": "@blamejs/core@0.11.29",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.11.28",
25
+ "version": "0.11.29",
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.11.28",
29
+ "purl": "pkg:npm/%40blamejs/core@0.11.29",
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.11.28",
57
+ "ref": "@blamejs/core@0.11.29",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]