@blamejs/core 0.6.27 → 0.6.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.6.x
10
10
 
11
+ - **0.6.29** (2026-05-02) — CI cleanup. **shellcheck gate green**: `docker/init/generate-certs.sh`'s footer line counted output via `ls "$CERT_DIR" | wc -l` (SC2012 — fragile on filenames with newlines or quoting metacharacters); replaced with `find "$CERT_DIR" -maxdepth 1 -mindepth 1 | wc -l`. Same semantics, robust to any cert filename pki-init might end up writing. **shellcheck added to the documented pre-push gate list** in CONTRIBUTING.md alongside smoke / wiki e2e / eslint / api-snapshot / primitive-section validators — CI's `Lint summary` job has always run it, but the local-dev recipe didn't, so contributors hit it post-push instead of catching it locally. **eslint pin alignment**: `.github/PULL_REQUEST_TEMPLATE.md` and CONTRIBUTING.md still pinned `eslint@10` even though v0.6.16 swapped CI runners to `eslint@latest`; both bumped to `@latest` to match the forward-track posture of every other CI tool. No framework code changes; the script + doc updates never ship in the npm package (excluded by `files` array).
12
+ - **0.6.28** (2026-05-02) — bug-finding pass: live integration test suite + every framework bug it caught. **`b.ssrfGuard.checkUrl` cloud-metadata hard-deny** — `allowInternal: true` no longer permits 169.254.169.254 (AWS / GCP / Azure metadata). Loopback / private / link-local / reserved stay allowInternal-overridable per existing semantics; cloud-metadata is unconditional because a blanket override would let any compromised request exfiltrate instance credentials. **`b.mtlsCa` issues server certs and dual-EKU certs** — `generateClientCert({ usage: "client" | "server" | "both", sans: [...] })`. `usage` controls EKU (`client` = clientAuth, `server` = serverAuth, `both` = both); `sans` accepts `DNS:` / `IP:` / bare-DNS entries; serverAuth without an explicit SAN auto-adds the CN. Operators wiring inbound mTLS reverse-proxy fronts no longer hit "unsuitable certificate purpose" on the handshake. **`b.mtlsCa` algorithm auto-detect** — the engine probes webcrypto + the vendored x509 library at first cert issuance and picks the highest-PQC option that round-trips: SLH-DSA-SHAKE-256f → SLH-DSA-SHAKE-128f → ML-DSA-87 → ML-DSA-65 → ECDSA-P384-SHA384 (current bridge — what the X.509 ecosystem accepts today). When the bridge condition lifts and the vendor refresh ships, the same `b.mtlsCa.create(...)` call self-upgrades; `b.mtlsCa.status().cert.label` and `.posture` surface the chosen algorithm. **`b.redisClient.create({ ca, servername })`** — managed-Redis / on-prem-cluster operators connecting over `rediss://` against a private CA can pin trust roots; `servername` auto-suppresses for IP literals so `rediss://127.0.0.1:6380` no longer trips node:tls's IP-as-SNI rule. **`b.network.dns.useDnsOverTls` / `useDnsOverHttps` accept `ca`** — same trust-pinning surface for self-signed / private-PKI DoT and DoH endpoints. **DoT TLS handshake errors** now route as DnsError on the lookup promise rather than leaking as `unhandledRejection` — the secureConnect / error event-listener race had let cert-verification failures escape the per-query Promise wrapping. **DoT socket lifecycle** — was unconditionally `sock.unref()`'d after construct, causing node to exit during in-flight lookups when no other I/O kept the loop alive; now ref'd while a query is in flight and unref'd when idle. **`b.ntpCheck.querySingle` honours IPv6 servers** — was hardcoded to `dgram.createSocket("udp4")`, so `::1` / `fd00::…` queries failed with EINVAL; now auto-detects family from the server string. **`b.logStream.shutdown` drains in-flight emit microtasks** before closing sink fds — fire-and-forget emits queued just before shutdown were silently dropped because `close()` ran ahead of the microtask. **`b.logStream` webhook-sink `close()` flushes BEFORE setting `closed = true`** — `_flush`'s while loop bailed on `!closed`, leaving any records buffered just before shutdown stranded. **Integration test suite** (`test/integration/`, runner at `scripts/test-integration.js`) — 13 live test files across `b.redisClient` plain + TLS, `b.queue` Redis backend lifecycle, `b.mail` SMTP + STARTTLS + multi-rcpt + dot-stuffing transparency, `b.mail.dkim` rsa-sha256 + ed25519-sha256 + bad-algorithm reject, `b.ntpCheck` v4 + v6 + bootCheck + bad-host + bad-port, `b.network.dns` plain + DoT + DoH with strict CA pinning + bad-servername + cache, `b.network.heartbeat` http + tcp + state-change callbacks, `b.objectStore` sigv4 PUT / GET / list / delete on plain HTTP + TLS variants, `b.cache` memory + Redis-backed cluster, `b.httpClient` direct + Squid forward proxy + TLS pin, `b.ssrfGuard` classify + checkUrl + cloud-metadata block, `b.logStream` local + webhook + deferred-syslog error path, `b.mtlsCa` CA bootstrap + clientAuth + serverAuth + dual-EKU live mTLS handshakes. Companion `docker-compose.test.yml` stands up redis (plain + TLS) / postgres / mysql / mongo / minio (HTTP + HTTPS variants) / rabbitmq (plain + TLS) / nats / syslog / ntp / mailpit / coredns (plain + DoT + DoH) / haproxy / caddy / mitmproxy / squid / pki-init (auto-generates Ed25519 CA + per-service leaf certs into a docker volume); host port bindings dual-stack on `127.0.0.1` AND `[::1]`. `scripts/check-services.js` does host-side TCP / TLS handshake / SNTPv4 / DNS-A / Redis-PING / SMTP-banner / HTTP probes across every endpoint; `scripts/test-integration.js` exports the test CA via `docker cp` and sets `NODE_EXTRA_CA_CERTS` per-test child process so the framework's TLS verification stays strict (no `rejectUnauthorized: false` bypass anywhere in the test surface). **Test architecture rule** — live tests go in `test/integration/`, NEVER in `test/layer-N-*/`: smoke must remain pure (no docker dependency, runs in CI / on a developer laptop / inside prepack-guard) and a "skip silently when service is down" branch in a layer-N test makes the gate's pass count misleading and masks bugs that only surface against a live backend. The `0.6.27` `queue-redis.test.js` was moved to `test/integration/queue-redis.test.js` to enforce this. Smoke and wiki e2e green; integration suite ships green against the docker-compose stack.
11
13
  - **0.6.27** (2026-05-02) — Redis backend for `b.queue` so multi-replica apps can share a single queue without each needing to be cluster leader. **Bespoke RESP2 client** (`lib/redis-client.js`) — zero npm runtime deps. TCP via `node:net` + TLS via `node:tls` (`rediss://` auto-detected), legacy single-arg AUTH + ACL `AUTH user pass`, `SELECT db`, pipelining, exponential-backoff reconnect, EVAL helper. **`b.queue` protocol "redis"** (`lib/queue-redis.js`) — full enqueue/lease/extendLease/complete/fail/sweepExpired/size/purge/dlqList/dlqRetry/dlqSize parity with the local backend. Atomicity comes from server-side Lua scripts so concurrent consumers can't double-lease and a sweep can't race a complete. Storage layout: per-job HASH (sealed payload + lastError via `cryptoField.sealRow("_blamejs_jobs", row)` — same crypto config as the local backend), per-queue ready ZSET scored by availableAt, per-queue inflight ZSET scored by leaseExpiresAt, per-queue dlq ZSET scored by finishedAt, plus a queues SET so sweepExpired walks every known queue without a global secondary index. Cron-repeat handled in `complete()` JS — re-enqueues the next firing as a fresh jobId with availableAt=next-cron-fire. **`b.queue.bootFromEnv({ env })`** — env-driven init mirroring `b.network.bootFromEnv` and `b.logStream.bootFromEnv`. Reads `BLAMEJS_QUEUE_PROTOCOL` (`local`|`redis`), `BLAMEJS_QUEUE_REDIS_URL`, `BLAMEJS_QUEUE_REDIS_PASSWORD`, `BLAMEJS_QUEUE_REDIS_USERNAME`, `BLAMEJS_QUEUE_REDIS_TLS`, `BLAMEJS_QUEUE_REDIS_KEY_PREFIX`. Operators flip from local to Redis without a code change; both wiki docker-compose configs declare the new env knobs. Removed `redis` from `DEFERRED_PROTOCOLS`. **Out of scope for v1** (deferred to follow-up patches with explicit re-open conditions): Redis Cluster (slot-routing), Sentinel (managed primary failover), priority ordering on the Redis backend (queue-local supports `priority` opt; Redis backend orders strictly by availableAt for v1), flow children with `dependsOn` cascade. **Wiki**: queue-cache page documents the Redis backend opts schema, bootFromEnv, and the layout. **Tests**: 24 RESP2 protocol parser unit tests (`test/layer-0-primitives/redis-client.test.js`) cover URL parsing, command encoding (binary-safe), every reply type (simple string / error / integer / bulk / nil bulk / nested array / pipelined / incomplete-mid-frame). Live Redis round-trip tests (`test/layer-0-primitives/queue-redis.test.js`) cover enqueue+lease, availableAt scheduling, visibility-timeout sweep, fail+retry path, DLQ list/retry/size, extendLease, purge, and concurrent-leaser no-double-lease — skip cleanly when `BLAMEJS_TEST_REDIS_URL` is not set so the smoke suite passes on dev boxes without a Redis container. Smoke 6780 OK.
12
14
  - **0.6.26** (2026-05-02) — `blamejs restore` and `blamejs audit verify-chain` subcommands wrap the existing `b.restore` and `b.audit.verifyChain` primitives so operators can drive them from runbooks without writing app code. **`blamejs restore`**: `list` (enumerate bundles in storage), `inspect` (manifest summary without touching live data), `apply` (live in-place restore with rollback preserved), `rollback` (revert to most-recent OR named restore point), `list-rollbacks` (enumerate preserved rollback points). Two ways to identify a bundle — `--bundle <dir>` matches the shape `blamejs backup extract` produces (parent dir is treated as storage root, basename as bundle id), or `--storage-root <root> --bundle-id <id>` for multi-bundle stores. `apply` honors `--max-pulled-bytes` / `--max-pulled-files` (defaults 4 GiB / 100K), `--rollback-root` (default `<data-dir>.rollbacks`), `--no-audit`, and `BLAMEJS_BACKUP_PASSPHRASE` env. **`blamejs audit verify-chain`**: walks the live audit chain end-to-end, reports tampering with `breakAt` / `breakRowId` / expected-vs-actual prevHash; honours `--max-rows` to bound long walks; default table is `audit_log`. **Wiki CLI snapshot test now validates subcommand pairs**, not just top commands. Walks every wiki + README invocation of the form `blamejs <cmd> <sub>` and verifies that <sub> exists in the perCommand[<cmd>].subcommands list parsed from `lib/cli.js`. Surfaced one real drift on first run: `examples/wiki/seeders/prod/pages/backup-restore.js` documented `blamejs audit verify-signing` after vault rotation, but no such subcommand existed (only `verify-bundle`); fixed by shipping the new `verify-chain` subcommand and updating the wiki to reference it. Two top-level command gaps surfaced and fixed: `blamejs restore` (now real) and `blamejs network status` (was prose-promised in `network-config.js`; reworded to point at `b.network.snapshot()` for /healthz / custom diagnostics routes since wiring a CLI command for it is operator-side work). README CLI table updated with `restore` row and the two new audit subcommands.
13
15
  - **0.6.25** (2026-05-02) — `b.logStream` gains an AWS CloudWatch Logs sink AND framework-level env wiring. **CloudWatch sink**: `protocol: "cloudwatch"` POSTs `PutLogEvents` over HTTPS with SigV4 signing (service `logs`); operator pre-creates the log group + log stream (the framework does NOT auto-create). Honors IAM role + STS session-token credentials. Respects all three CloudWatch caps automatically: 10,000 events / 1 MiB total payload / 256 KiB-per-event. Per-event oversize dropped at `emit()`-time with `onDrop` fired (truncated message in the drop notification). Per-batch oversize split mid-flush. Permanent AWS errors (`ResourceNotFoundException` / `AccessDeniedException` / `InvalidParameterException` / `UnrecognizedClientException` / `SerializationException`) skip the retry budget. `InvalidSequenceTokenException` (legacy CW accounts) extracts the expected token from the error message and retries with it once. `lib/object-store/sigv4.js` `signRequest()` is now service-agnostic — accepts `opts.service` (default still `"s3"` for back-compat). Removed `cloudwatch` from `DEFERRED_PROTOCOLS`. **`b.logStream.bootFromEnv({ env })`**: framework-level env-driven init mirroring `b.network.bootFromEnv`. Reads `BLAMEJS_LOG_STREAM_PROTOCOL` (`local`/`webhook`/`otlp`/`cloudwatch`), `BLAMEJS_LOG_STREAM_URL`, `BLAMEJS_LOG_STREAM_TOKEN`, `BLAMEJS_LOG_STREAM_SERVICE_NAME`, `BLAMEJS_LOG_STREAM_SERVICE_VERSION`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_GROUP`, `BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_STREAM`, `BLAMEJS_LOG_STREAM_PATH`, plus standard AWS_*. Operators get a working log-stream sink without writing build-app code. Wiki app's `build-app.js` replaced its inline env-reading with one `b.logStream.bootFromEnv()` call; both docker-compose configs declare the new env knobs. **Wiki env-snapshot test**: parallel to api-snapshot.json — walks `process.env.X` / `env.X` / `safeEnv.readVar("X")` reads in the wiki app + framework `lib/`, walks docker-compose env declarations, captures the union as `examples/wiki/env-snapshot.json`, fails the e2e gate when env vars are added/removed without updating the snapshot OR when source-only / compose-only gaps appear (env knob declared but unread, env read but undocumented). The validator immediately surfaced 13 real gaps in the wiki app: 5 `WIKI_*` env vars read by source but missing from docker-compose (`WIKI_VAULT_MODE`, `WIKI_DB_AT_REST`, `WIKI_AUDIT_SIGNING_MODE`, `WIKI_BIND`, `WIKI_SITE_URL`), 2 framework env vars (`BLAMEJS_AUDIT_SIGNING_MODE`, `BLAMEJS_TMPDIR`) read by `lib/db.js` via `safeEnv.readVar` but never declared in the wiki's compose configs, and 6 dead env knobs in compose that nothing read. All 13 fixed. Update workflow: `BLAMEJS_UPDATE_ENV_SNAPSHOT=1 node examples/wiki/test/validate-env-snapshot.js` (mirrors the api-snapshot UX). Wiki observability page documents the new sink alongside webhook + otlp; README "What ships in the box" calls out all four log-stream sinks. Tests cover endpoint resolution, event-byte accounting, batch sorting + sequence-token round-trip, permanent-error classifier, validation, round-trip via mock CloudWatch, STS session-token propagation, ResourceNotFoundException no-retry path, 256 KiB per-event hard cap, dispatcher integration, AND batch splitting on the 1-MiB cap (5 quarter-MB events POST as 4 + 1 batches).
package/README.md CHANGED
@@ -43,8 +43,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
43
43
 
44
44
  - **Data layer** — SQLite with sealed-by-default columns (`b.db`), migrations, seeders, atomic-file writes; bring-your-own external Postgres / MySQL / etc. with pool tuning + role-aware connect + read-replica routing (`b.externalDb`); declarative role-narrowed views and Postgres row-level-security migrations (`b.db.declareView`, `b.db.declareRowPolicy`); S3 / R2 / B2 / GCS / Azure object store with multipart upload + SSE + bucket ops (`b.storage`, `b.objectStore`); durable queue with priority + cron + flows on the local SQLite backend OR a shared Redis backend for multi-replica deploys (`b.queue`, `b.jobs`); cluster-shared cache (`b.cache`).
45
45
  - **Identity & access** — passwords (Argon2id) + policy primitive (NIST 800-63B / PCI-DSS 4.0 / HIPAA-AAL2 profiles, HaveIBeenPwned k-anonymity breach check, length / context / dictionary / complexity rules, rotation + history) (`b.auth.password`); passkeys (WebAuthn), TOTP, JWT (PQ-default), OAuth, sessions with optional IP / UA fingerprint drift detection + anomaly scoring, brute-force lockout (`b.auth.*`, `b.session`); RBAC + optional per-role DB binding + role-spec `requireMfa` + per-route MFA freshness window + ABAC predicate registry (`b.permissions`); API keys with rotation (`b.apiKey`); break-glass column gates with second-factor + audit (`b.breakGlass`); two-person-rule approval workflow with m-of-n quorum + cooling-off lock + approver-role gate + cancellation (`b.dualControl`).
46
- - **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto + cryptographic erasure (`b.cryptoField.eraseRow`), signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA, PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
47
- - **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`, in-process CIDR fence via `b.middleware.networkAllowlist`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate, scheme + userinfo + per-host (wildcard / per-method) destination allowlist, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`); operator-tunable network configurability — env-driven NTP / NTS (RFC 8915 authenticated time), DNS with IPv6 / DoH / DoT / cache / lookup timeout, outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`), runtime DPI trust-store CA additions, application-level heartbeats, TCP socket defaults (`b.network`).
46
+ - **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto + cryptographic erasure (`b.cryptoField.eraseRow`), signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA that issues clientAuth / serverAuth / dual-EKU certs with SAN entries and auto-detects the highest-PQC signature algorithm the vendored x509 library accepts (today: ECDSA-P384-SHA384 bridge; self-upgrades to SLH-DSA / ML-DSA when the X.509 ecosystem catches up), PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
47
+ - **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`, in-process CIDR fence via `b.middleware.networkAllowlist`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate (cloud-metadata IPs hard-denied unconditionally; private / loopback / link-local overridable per call), scheme + userinfo + per-host (wildcard / per-method) destination allowlist, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`); operator-tunable network configurability — env-driven NTP / NTS (RFC 8915 authenticated time), IPv4-or-IPv6 NTP servers, DNS with IPv6 / DoH / DoT (private-CA trust pinning via `opts.ca`) / cache / lookup timeout, outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`), runtime DPI trust-store CA additions, application-level heartbeats, TCP socket defaults (`b.network`).
48
48
  - **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env), `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.).
49
49
  - **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`).
50
50
  - **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks (local file rotation, generic webhook, OTLP/HTTP-JSON to an OTel collector, AWS CloudWatch Logs via SigV4), OTLP/HTTP-JSON exporter for traces + metrics (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`); operator-callable boot-time security policy assertions (`b.security.assertProduction`) and tamper-evident config-baseline drift detection signed with the audit-signing key (`b.configDrift`).
@@ -168,9 +168,14 @@ function create(config) {
168
168
  }
169
169
 
170
170
  async function close() {
171
- closed = true;
171
+ // Drain BEFORE flipping closed=true. _flush()'s while loop bails on
172
+ // !closed, so flipping the flag first leaves any buffered records
173
+ // stranded — the very records the operator queued just before
174
+ // calling shutdown(). Order: stop the timer, drain, then refuse
175
+ // any new enqueues by setting closed.
172
176
  if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
173
177
  await _flush();
178
+ closed = true;
174
179
  }
175
180
 
176
181
  function stats() {
package/lib/log-stream.js CHANGED
@@ -79,6 +79,10 @@ var audit = lazyRequire(function () { return require("./audit"); });
79
79
 
80
80
  var initialized = false;
81
81
  var sinks = {};
82
+ // Pending emit promises, tracked so shutdown can drain them before
83
+ // closing sink fds. Without this, fire-and-forget emits queued just
84
+ // before shutdown raced with close() and silently dropped records.
85
+ var _inflight = new Set();
82
86
  var minLevel = "info";
83
87
  var incomingHandlers = [];
84
88
 
@@ -124,11 +128,14 @@ function emit(level, message, meta) {
124
128
 
125
129
  // Fire-and-forget to all sinks. Sink errors don't bubble — they're
126
130
  // captured by audit (system.log.sink-failure) so an external sink
127
- // outage doesn't take down the app's request handlers.
131
+ // outage doesn't take down the app's request handlers. Pending
132
+ // emits are tracked in _inflight so shutdown() can drain them
133
+ // before closing fds (otherwise records queued just before
134
+ // shutdown would be lost when close() ran ahead of the microtask).
128
135
  Object.keys(sinks).forEach(function (name) {
129
136
  var sink = sinks[name];
130
137
  if (!_shouldEmit(level, sink.levelFilter)) return;
131
- Promise.resolve()
138
+ var p = Promise.resolve()
132
139
  .then(function () { return sink.raw.emit(record); })
133
140
  .catch(function (e) {
134
141
  audit().safeEmit({
@@ -138,6 +145,8 @@ function emit(level, message, meta) {
138
145
  metadata: { sink: name, level: level },
139
146
  });
140
147
  });
148
+ _inflight.add(p);
149
+ p.then(function () { _inflight.delete(p); }, function () { _inflight.delete(p); });
141
150
  });
142
151
  }
143
152
 
@@ -183,6 +192,12 @@ async function deliverIncoming(payload, opts) {
183
192
 
184
193
  async function shutdown() {
185
194
  if (!initialized) return;
195
+ // Drain any in-flight emits before closing sink fds so records
196
+ // queued just before shutdown actually reach disk / the wire.
197
+ if (_inflight.size > 0) {
198
+ try { await Promise.all(Array.from(_inflight)); }
199
+ catch (_e) { /* sink errors already audited via the catch above */ }
200
+ }
186
201
  for (var name in sinks) {
187
202
  try {
188
203
  if (typeof sinks[name].raw.close === "function") await sinks[name].raw.close();
@@ -48,10 +48,103 @@ class MtlsEngineError extends FrameworkError {
48
48
  }
49
49
  }
50
50
 
51
- var CA_KEY_ALG = { name: "ECDSA", namedCurve: "P-384" };
52
- var CA_SIG_ALG = { name: "ECDSA", hash: "SHA-384" };
53
51
  var CA_KEY_USAGES = ["sign", "verify"];
54
52
 
53
+ // Algorithm priority — each entry probed at first use; the first one
54
+ // the vendored x509 library AND webcrypto can both honour wins.
55
+ // Ordered highest-PQC-posture first so the engine self-upgrades the
56
+ // moment the vendor bundle gains PQ-sig X.509 support.
57
+ //
58
+ // keyAlg: passed to webcrypto.subtle.generateKey + import
59
+ // sigAlg: passed to x509.X509CertificateGenerator.create
60
+ // label : surfaced via b.mtlsCa.status() so operators can audit
61
+ // which algorithm the in-flight CA generation is using
62
+ var ALG_CANDIDATES = [
63
+ // Pure-PQC stateless hash-based — matches lib/audit-sign's posture.
64
+ // FIPS 205 (SPHINCS+ family). Awaiting node:tls + browser cert-store
65
+ // verification support; currently issuance-only on most stacks.
66
+ {
67
+ label: "SLH-DSA-SHAKE-256f",
68
+ keyAlg: { name: "SLH-DSA-SHAKE-256f" },
69
+ sigAlg: { name: "SLH-DSA-SHAKE-256f" },
70
+ posture: "pqc-pure",
71
+ },
72
+ {
73
+ label: "SLH-DSA-SHAKE-128f",
74
+ keyAlg: { name: "SLH-DSA-SHAKE-128f" },
75
+ sigAlg: { name: "SLH-DSA-SHAKE-128f" },
76
+ posture: "pqc-pure",
77
+ },
78
+ // Pure-PQC lattice — FIPS 204 (Dilithium family). Smaller than SLH-DSA,
79
+ // accepted by the same emerging cert-store deployments.
80
+ {
81
+ label: "ML-DSA-87",
82
+ keyAlg: { name: "ML-DSA-87" },
83
+ sigAlg: { name: "ML-DSA-87" },
84
+ posture: "pqc-pure",
85
+ },
86
+ {
87
+ label: "ML-DSA-65",
88
+ keyAlg: { name: "ML-DSA-65" },
89
+ sigAlg: { name: "ML-DSA-65" },
90
+ posture: "pqc-pure",
91
+ },
92
+ // Documented bridge — used until cert ecosystems verify the above.
93
+ // The framework's hybrid KEM posture (X25519MLKEM768) covers handshake
94
+ // KEX; these certs sign with ECDSA P-384 + SHA-384.
95
+ {
96
+ label: "ECDSA-P384-SHA384",
97
+ keyAlg: { name: "ECDSA", namedCurve: "P-384" },
98
+ sigAlg: { name: "ECDSA", hash: "SHA-384" },
99
+ posture: "classical",
100
+ },
101
+ ];
102
+
103
+ // First-call probe cache. Re-runs after engine reload (test reset path).
104
+ var _selectedAlg = null;
105
+
106
+ async function _probeCandidate(c) {
107
+ try {
108
+ var pair = await webcrypto.subtle.generateKey(c.keyAlg, true, CA_KEY_USAGES);
109
+ if (!pair || !pair.publicKey) return false;
110
+ // Also confirm the x509 generator accepts the sigAlg by issuing a
111
+ // throwaway self-signed cert. Some keyAlgs work in webcrypto but
112
+ // aren't yet wired through @peculiar/x509's encoder — without this
113
+ // round-trip we'd select an algorithm we can't actually mint certs
114
+ // with and hit a confusing failure on first issuance.
115
+ await x509.X509CertificateGenerator.create({
116
+ serialNumber: "01",
117
+ subject: "CN=probe",
118
+ issuer: "CN=probe",
119
+ notBefore: new Date(),
120
+ notAfter: new Date(Date.now() + 1000),
121
+ signingAlgorithm: c.sigAlg,
122
+ publicKey: pair.publicKey,
123
+ signingKey: pair.privateKey,
124
+ });
125
+ return true;
126
+ } catch (_e) {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ async function _selectAlgorithm() {
132
+ if (_selectedAlg) return _selectedAlg;
133
+ for (var i = 0; i < ALG_CANDIDATES.length; i++) {
134
+ var c = ALG_CANDIDATES[i];
135
+ var ok = await _probeCandidate(c);
136
+ if (ok) { _selectedAlg = c; return c; }
137
+ }
138
+ // Should never happen — ECDSA-P384-SHA384 is universal.
139
+ throw new MtlsEngineError("mtls-engine/no-algorithm",
140
+ "no candidate algorithm passed the webcrypto + x509 probe");
141
+ }
142
+
143
+ // Backwards-compat shape for callers that read these directly. Resolved
144
+ // lazily so the algorithm choice is the first-probe result.
145
+ var CA_KEY_ALG = null;
146
+ var CA_SIG_ALG = null;
147
+
55
148
  var P12_CONTENT_ENC = { name: "AES-CBC", length: 256 };
56
149
  var P12_KDF_HASH = "SHA-512";
57
150
  var P12_MAC_HASH = "SHA-512";
@@ -63,6 +156,7 @@ var DEFAULT_CA_NAME = "blamejs CA";
63
156
  var BAG_ID_KEY = "1.2.840.113549.1.12.10.1.2"; // pkcs-12-pkcs-8ShroudedKeyBag
64
157
  var BAG_ID_CERT = "1.2.840.113549.1.12.10.1.3"; // pkcs-12-certBag
65
158
  var EKU_CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";
159
+ var EKU_SERVER_AUTH_OID = "1.3.6.1.5.5.7.3.1";
66
160
 
67
161
  function _pemBlock(label, der) {
68
162
  var b64 = Buffer.from(der).toString("base64");
@@ -106,6 +200,8 @@ async function generateCa(opts) {
106
200
  ? Math.floor(opts.generation) : 1;
107
201
  var caName = opts.name || DEFAULT_CA_NAME;
108
202
 
203
+ var alg = await _selectAlgorithm();
204
+ CA_KEY_ALG = alg.keyAlg; CA_SIG_ALG = alg.sigAlg;
109
205
  var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
110
206
  var now = new Date();
111
207
  var ca = await x509.X509CertificateGenerator.createSelfSigned({
@@ -137,12 +233,58 @@ async function signClientCert(opts) {
137
233
  ? Math.floor(opts.validityDays) : LEAF_DEFAULT_DAYS;
138
234
  var cn = _normaliseCn(opts.cn);
139
235
 
236
+ // Extended Key Usage: defaults to clientAuth (the historical behaviour).
237
+ // Operators issuing server certs for inbound mTLS reverse-proxy fronts
238
+ // pass `usage: "server"` (sets serverAuth EKU), or `usage: "both"`
239
+ // (clientAuth + serverAuth — dual-purpose certs for service-to-service
240
+ // mTLS where the same workload is both initiator and acceptor).
241
+ var usage = opts.usage || "client";
242
+ var ekuOids = [];
243
+ if (usage === "client" || usage === "both") ekuOids.push(EKU_CLIENT_AUTH_OID);
244
+ if (usage === "server" || usage === "both") ekuOids.push(EKU_SERVER_AUTH_OID);
245
+ if (ekuOids.length === 0) {
246
+ throw new MtlsEngineError("mtls-engine/bad-usage",
247
+ "signClientCert: opts.usage must be 'client' | 'server' | 'both', got " +
248
+ JSON.stringify(opts.usage));
249
+ }
250
+
251
+ // Subject Alternative Names — required for serverAuth (modern TLS
252
+ // clients only honor SANs, not CN). Accept opts.sans as an array of
253
+ // strings (DNS names by default; "DNS:foo" / "IP:1.2.3.4" forms also).
254
+ var sanExt = null;
255
+ if (Array.isArray(opts.sans) && opts.sans.length > 0) {
256
+ var sanEntries = opts.sans.map(function (s) {
257
+ var str = String(s);
258
+ if (/^DNS:/i.test(str)) return { type: "dns", value: str.slice(4) };
259
+ if (/^IP:/i.test(str)) return { type: "ip", value: str.slice(3) };
260
+ // Bare entries default to DNS — matches operator expectation
261
+ return { type: "dns", value: str };
262
+ });
263
+ sanExt = new x509.SubjectAlternativeNameExtension(sanEntries);
264
+ } else if (usage === "server" || usage === "both") {
265
+ // serverAuth without a SAN is unverifiable by modern TLS clients.
266
+ // Auto-add the CN as a DNS SAN so the most common case "just works".
267
+ sanExt = new x509.SubjectAlternativeNameExtension([{ type: "dns", value: cn }]);
268
+ }
269
+
270
+ var alg = await _selectAlgorithm();
271
+ CA_KEY_ALG = alg.keyAlg; CA_SIG_ALG = alg.sigAlg;
140
272
  var caKey = await _importPemPrivateKey(opts.caKeyPem, CA_KEY_ALG, ["sign"]);
141
273
  var caCert = _parseCertPem(opts.caCertPem);
142
274
  var clientKeys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
143
275
 
144
276
  var now = new Date();
145
277
  var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
278
+ var extensions = [
279
+ new x509.BasicConstraintsExtension(false, undefined, true),
280
+ new x509.KeyUsagesExtension(
281
+ x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
282
+ true
283
+ ),
284
+ new x509.ExtendedKeyUsageExtension(ekuOids, true),
285
+ ];
286
+ if (sanExt) extensions.push(sanExt);
287
+
146
288
  var clientCert = await x509.X509CertificateGenerator.create({
147
289
  serialNumber: crypto.generateToken(16),
148
290
  subject: "CN=" + cn,
@@ -152,14 +294,7 @@ async function signClientCert(opts) {
152
294
  signingAlgorithm: CA_SIG_ALG,
153
295
  publicKey: clientKeys.publicKey,
154
296
  signingKey: caKey,
155
- extensions: [
156
- new x509.BasicConstraintsExtension(false, undefined, true),
157
- new x509.KeyUsagesExtension(
158
- x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment,
159
- true
160
- ),
161
- new x509.ExtendedKeyUsageExtension([EKU_CLIENT_AUTH_OID], true),
162
- ],
297
+ extensions: extensions,
163
298
  });
164
299
  var pem = await _exportKeyPairToPem(clientKeys);
165
300
  return {
@@ -168,6 +303,7 @@ async function signClientCert(opts) {
168
303
  ca: opts.caCertPem,
169
304
  issuedAt: now.toISOString(),
170
305
  expiresAt: notAfter.toISOString(),
306
+ usage: usage,
171
307
  };
172
308
  }
173
309
 
@@ -265,7 +401,18 @@ async function packageP12(opts) {
265
401
 
266
402
  function algorithmEnvelope() {
267
403
  return {
268
- cert: { keyAlg: CA_KEY_ALG, sigAlg: CA_SIG_ALG },
404
+ cert: {
405
+ keyAlg: CA_KEY_ALG,
406
+ sigAlg: CA_SIG_ALG,
407
+ label: _selectedAlg && _selectedAlg.label,
408
+ posture: _selectedAlg && _selectedAlg.posture,
409
+ // Operators querying status() before any cert has been issued
410
+ // get the candidate priority list — the engine probes lazily so
411
+ // the chosen algorithm isn't known until first use.
412
+ priority: ALG_CANDIDATES.map(function (c) {
413
+ return { label: c.label, posture: c.posture };
414
+ }),
415
+ },
269
416
  p12: {
270
417
  contentEncryption: P12_CONTENT_ENC,
271
418
  kdfHash: P12_KDF_HASH,
@@ -136,7 +136,7 @@ function setCacheTtlMs(ms, negativeMs) {
136
136
 
137
137
  function useDnsOverHttps(opts) {
138
138
  opts = opts || {};
139
- validateOpts(opts, ["provider", "url", "method"], "dns.useDnsOverHttps");
139
+ validateOpts(opts, ["provider", "url", "method", "ca"], "dns.useDnsOverHttps");
140
140
  var url = opts.url;
141
141
  if (!url && opts.provider) {
142
142
  var p = String(opts.provider).toLowerCase();
@@ -155,22 +155,34 @@ function useDnsOverHttps(opts) {
155
155
  "dns.useDnsOverHttps: method must be 'GET' | 'POST' | undefined (auto), got " +
156
156
  JSON.stringify(method));
157
157
  }
158
- STATE.doh = { url: url, method: method };
158
+ if (opts.ca !== undefined && opts.ca !== null &&
159
+ !Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
160
+ throw new DnsError("dns/bad-doh-ca",
161
+ "dns.useDnsOverHttps: ca must be a PEM string, Buffer, or array of either");
162
+ }
163
+ STATE.doh = { url: url, method: method, ca: opts.ca || null };
159
164
  _clearCache();
160
165
  _emitObs("network.dns.doh.set", { url: url, method: method || "auto" });
161
166
  }
162
167
 
163
168
  function useDnsOverTls(opts) {
164
169
  opts = opts || {};
165
- validateOpts(opts, ["host", "port", "servername"], "dns.useDnsOverTls");
170
+ validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
166
171
  if (typeof opts.host !== "string" || opts.host.length === 0) {
167
172
  throw new DnsError("dns/bad-dot-host", "dns.useDnsOverTls: host required");
168
173
  }
174
+ if (opts.ca !== undefined && opts.ca !== null &&
175
+ !Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
176
+ throw new DnsError("dns/bad-dot-ca",
177
+ "dns.useDnsOverTls: ca must be a PEM string, Buffer, or array of either");
178
+ }
169
179
  STATE.dot = {
170
180
  host: opts.host,
171
181
  port: opts.port || 853,
172
182
  servername: opts.servername || opts.host,
183
+ ca: opts.ca || null,
173
184
  };
185
+ _resetDotPool();
174
186
  _clearCache();
175
187
  _emitObs("network.dns.dot.set", { host: STATE.dot.host, port: STATE.dot.port });
176
188
  }
@@ -278,6 +290,7 @@ async function _dohLookup(host, family) {
278
290
  minVersion: "TLSv1.3",
279
291
  ecdhCurve: C.TLS_GROUP_CURVE_STR,
280
292
  };
293
+ if (STATE.doh.ca) reqOpts.ca = STATE.doh.ca;
281
294
  if (usePost) {
282
295
  reqOpts.headers["content-type"] = "application/dns-message";
283
296
  reqOpts.headers["content-length"] = enc.buf.length;
@@ -317,14 +330,19 @@ function _dotPoolKey() {
317
330
  }
318
331
 
319
332
  function _dotConnect() {
320
- var sock = tls.connect({
333
+ var connectOpts = {
321
334
  host: STATE.dot.host,
322
335
  port: STATE.dot.port,
323
336
  servername: STATE.dot.servername,
324
337
  minVersion: "TLSv1.3",
325
338
  ecdhCurve: C.TLS_GROUP_CURVE_STR,
326
- });
327
- sock.unref && sock.unref();
339
+ };
340
+ if (STATE.dot.ca) connectOpts.ca = STATE.dot.ca;
341
+ var sock = tls.connect(connectOpts);
342
+ // The pool entry is ref()'d while a query is in flight and unref()'d
343
+ // when idle — _dotLookup toggles this around its query. Calling
344
+ // unref() unconditionally here let node exit during a normal lookup
345
+ // when no other I/O kept the event loop alive.
328
346
  return sock;
329
347
  }
330
348
 
@@ -352,9 +370,18 @@ async function _dotLookup(host, family) {
352
370
  idle: true,
353
371
  ready: new Promise(function (res, rej) {
354
372
  sock.once("secureConnect", function () { res(); });
355
- sock.once("error", function (e) { rej(e); });
373
+ sock.once("error", function (e) {
374
+ rej(new DnsError("dns/dot-handshake",
375
+ "DoT TLS handshake to " + STATE.dot.host + ":" + STATE.dot.port +
376
+ " failed: " + ((e && e.message) || String(e))));
377
+ });
356
378
  }),
357
379
  };
380
+ // Pre-attach a no-op observer so a handshake failure isn't reported
381
+ // as an unhandledRejection in the window between this assignment
382
+ // and the first lookup awaiting entry.ready. The real reject path
383
+ // routes through _dotLookup's then(_, onErr) handler below.
384
+ entry.ready.catch(function () { /* observed; routed via per-lookup handler */ });
358
385
  _dotPool.set(key, entry);
359
386
  sock.on("error", function () { _dotEvict(key); });
360
387
  sock.on("close", function () { if (_dotPool.get(key) === entry) _dotPool.delete(key); });
@@ -365,6 +392,8 @@ async function _dotLookup(host, family) {
365
392
  entry._tail = waitTicket.then(function () {
366
393
  return new Promise(function (resolve, reject) {
367
394
  entry.idle = false;
395
+ // Hold the event loop open while a query is in flight.
396
+ try { entry.sock.ref(); } catch (_e) {}
368
397
  Promise.resolve(entry.ready).then(function () {
369
398
  var lenBuf = Buffer.alloc(2);
370
399
  lenBuf.writeUInt16BE(enc.buf.length, 0);
@@ -378,6 +407,9 @@ async function _dotLookup(host, family) {
378
407
  entry.sock.removeListener("error", onErr);
379
408
  entry.idle = true;
380
409
  entry.lastUsedAt = Date.now();
410
+ // Release the event-loop hold so an idle pool socket doesn't
411
+ // keep node alive between queries.
412
+ try { entry.sock.unref(); } catch (_e) {}
381
413
  if (err) reject(err); else resolve(val);
382
414
  }
383
415
  function onData(chunk) {
@@ -398,6 +430,13 @@ async function _dotLookup(host, family) {
398
430
  entry.sock.on("error", onErr);
399
431
  entry.sock.write(lenBuf);
400
432
  entry.sock.write(enc.buf);
433
+ }, function (handshakeErr) {
434
+ // entry.ready rejected (TLS handshake / cert verification failure).
435
+ // Route as the lookup's reject so callers see a real DnsError
436
+ // instead of an unhandledRejection from the TLSSocket event.
437
+ entry.idle = true;
438
+ try { entry.sock.unref(); } catch (_e) {}
439
+ reject(handshakeErr);
401
440
  });
402
441
  });
403
442
  });
package/lib/ntp-check.js CHANGED
@@ -86,7 +86,11 @@ function querySingle(server, opts) {
86
86
  var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
87
87
 
88
88
  return new Promise(function (resolve, reject) {
89
- var socket = dgram.createSocket("udp4");
89
+ // udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
90
+ // this branch a query to an IPv6 NTP host fails with EINVAL because
91
+ // you can't send IPv6 packets through a udp4 socket.
92
+ var family = server.indexOf(":") !== -1 ? "udp6" : "udp4";
93
+ var socket = dgram.createSocket(family);
90
94
  var settled = false;
91
95
 
92
96
  function done(err, result) {
@@ -162,6 +162,17 @@ function create(opts) {
162
162
  var commandTimeoutMs = Number(opts.commandTimeoutMs) || 10000;
163
163
  var maxReconnectAttempts = opts.maxReconnectAttempts === undefined ? 10
164
164
  : Number(opts.maxReconnectAttempts);
165
+ // TLS verification controls. Operators using rediss:// against private
166
+ // CAs (managed Redis services, on-prem clusters with internal PKI)
167
+ // pin the trust roots via opts.ca; rejectUnauthorized stays on by
168
+ // default — never weaken verification to make a connection succeed.
169
+ var caBundle = opts.ca || null;
170
+ // SNI is only legal for hostnames; IP literals must omit servername.
171
+ var servername = opts.servername;
172
+ if (servername === undefined) {
173
+ servername = (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.indexOf(":") !== -1)
174
+ ? undefined : host;
175
+ }
165
176
 
166
177
  var socket = null;
167
178
  var connected = false;
@@ -260,7 +271,10 @@ function create(opts) {
260
271
  reject(_err("CONNECT", "redis connect failed: " + ((e && e.message) || String(e))));
261
272
  }
262
273
  if (useTls) {
263
- sock = tls.connect({ host: host, port: port, servername: host }, onOk);
274
+ var tlsConnectOpts = { host: host, port: port };
275
+ if (servername) tlsConnectOpts.servername = servername;
276
+ if (caBundle) tlsConnectOpts.ca = caBundle;
277
+ sock = tls.connect(tlsConnectOpts, onOk);
264
278
  } else {
265
279
  sock = net.connect({ host: host, port: port }, onOk);
266
280
  }
package/lib/ssrf-guard.js CHANGED
@@ -339,6 +339,25 @@ async function checkUrl(url, opts) {
339
339
  var category = classify(addr);
340
340
  if (!category) continue;
341
341
 
342
+ // Cloud-metadata IPs are NEVER allowed — they leak instance
343
+ // credentials (AWS IMDS, GCP metadata, Azure IMDS) and a blanket
344
+ // allowInternal bypass would let any compromised request exfiltrate
345
+ // them. Operators with a legitimate need to talk to the metadata
346
+ // service do it through the cloud SDK with explicit IAM, never
347
+ // through the framework's outbound HTTP. Same hard-deny applies
348
+ // to allowInternal=[cidr-list]: the list grants exception for
349
+ // private ranges, not for the metadata IP that happens to fall
350
+ // inside link-local.
351
+ if (category === "cloud-metadata") {
352
+ throw new ErrorClass(
353
+ "URL '" + parsed.toString() + "' resolves to " + addr +
354
+ " (cloud-metadata) — blocked unconditionally; allowInternal does NOT override " +
355
+ "this class because metadata IPs leak instance credentials",
356
+ "ssrf-guard/blocked-cloud-metadata",
357
+ { url: parsed.toString(), ip: addr, category: category }
358
+ );
359
+ }
360
+
342
361
  if (allowInternal === true) continue;
343
362
  if (Array.isArray(allowInternal) && allowInternal.some(function (cidr) {
344
363
  return cidrContains(cidr, addr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.27",
3
+ "version": "0.6.29",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:2f9c38f8-03fa-4e5b-aa89-79d42c1df348",
5
+ "serialNumber": "urn:uuid:438bf95a-273c-4fba-b130-bb7dddcdb3de",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-02T15:55:03.075Z",
8
+ "timestamp": "2026-05-02T18:04:26.378Z",
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.6.27",
22
+ "bom-ref": "@blamejs/core@0.6.29",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.27",
25
+ "version": "0.6.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.6.27",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.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.6.27",
57
+ "ref": "@blamejs/core@0.6.29",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]