@blamejs/core 0.6.25 → 0.6.26
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 +1 -0
- package/README.md +2 -1
- package/lib/cli.js +343 -2
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.6.x
|
|
10
10
|
|
|
11
|
+
- **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.
|
|
11
12
|
- **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).
|
|
12
13
|
- **0.6.24** (2026-05-02) — `b.logStream` gains an OTLP/HTTP-JSON sink. `protocol: "otlp"` now forwards log records to any OpenTelemetry collector via the OTel Logs Data Model — `resourceLogs` → `scopeLogs` → `logRecords` envelope with `severityNumber` (debug=5/info=9/warn=13/error=17), `severityText`, `timeUnixNano` (string-encoded for JSON-safe 64-bit), `body.stringValue`, and OTel-typed attributes. Operator config: `{ url, serviceName, serviceVersion, resourceAttributes, auth, headers, batchSize, retry, onDrop, ... }` — same back-pressure semantics as the webhook sink (per-sink ring buffer, batched flush on size or maxBatchAgeMs, exponential-backoff retry, drop-on-overflow with operator-supplied onDrop callback). URL convention: `/v1/logs` is auto-appended when the operator passes the collector root. JSON not protobuf — operators benchmarking >100K logs/s ship the OTel Collector locally so the framework hands JSON to a sidecar that forwards via gRPC. Removed `otlp` from `DEFERRED_PROTOCOLS`. Wiki observability page documents the new sink with a `b.logStream.init` example pointing at an OTel collector. Tests cover URL resolution, attribute encoding (string / int / float / bool / array / nested object), severity mapping, round-trip via mock collector, auth header pass-through, retry on 5xx + drop on retry-exhaustion, buffer overflow, dispatcher integration. Verified 0 leaks across 291 commits via `gitleaks`.
|
|
13
14
|
- **0.6.23** (2026-05-02) — v0.6.22 follow-up cleanup. The cron-repeat call site in queue-local.js was passing both `availableAt` AND `delaySeconds` (the former was the precise next-fire ms; the latter was a redundant `Math.floor((nextMs - nowMs) / 1000)` computation that only existed to work around the bug v0.6.22 fixed). Dropped — the cron repeat now passes `availableAt` alone, matching the queue's documented precedence rule. The enqueue() docstring gains a 20-line "SCHEDULING PRECEDENCE" header documenting that `opts.availableAt` wins over `opts.delaySeconds` when both are passed, why the framework chose that direction, and which callers should use which form. New round-trip preservation regression test (`testEnqueueRoundTripsAvailableAt`) covers three precise targets, the delaySeconds-only path, and the both-opts-set case — gates against any future "I'll just rederive it from the floored seconds" mistake. Audited the rest of the framework for the same `(absolute-time, relative-time)` opt-overlap shape (cache.set, session.rotate, apiKey, dualControl): queue is the only primitive carrying both forms, so a generalized `b.time.resolveTimePoint` primitive would be premature with one call site.
|
package/README.md
CHANGED
|
@@ -76,8 +76,9 @@ blamejs seed run | status --db <path> --
|
|
|
76
76
|
blamejs dev --command <cmd> [--watch <dir>...]
|
|
77
77
|
blamejs api-snapshot capture | compare --file <path>
|
|
78
78
|
blamejs api-key issue | revoke | list | rotate | verify --data-dir <path> --namespace <ns>
|
|
79
|
-
blamejs audit archive | export | verify | purge
|
|
79
|
+
blamejs audit archive | export | verify-bundle | verify-chain | purge --data-dir <path>
|
|
80
80
|
blamejs backup inspect | verify | extract --bundle <path>
|
|
81
|
+
blamejs restore list | inspect | apply | rollback | list-rollbacks --data-dir <path> --bundle <dir>
|
|
81
82
|
blamejs mtls status | show-cert | init | issue | issue-p12 --data-dir <path>
|
|
82
83
|
blamejs vault status | seal | unseal | rotate --data-dir <path>
|
|
83
84
|
blamejs security assert --data-dir <path>
|
package/lib/cli.js
CHANGED
|
@@ -38,14 +38,18 @@ var fs = require("node:fs");
|
|
|
38
38
|
var os = require("node:os");
|
|
39
39
|
var path = require("path");
|
|
40
40
|
var apiSnapshot = require("./api-snapshot");
|
|
41
|
+
var auditChain = require("./audit-chain");
|
|
41
42
|
var auditTools = require("./audit-tools");
|
|
43
|
+
var backup = require("./backup");
|
|
42
44
|
var cliHelpers = require("./cli-helpers");
|
|
43
45
|
var constants = require("./constants");
|
|
44
46
|
var crypto = require("./crypto");
|
|
45
47
|
var dev = require("./dev");
|
|
46
48
|
var migrations = require("./migrations");
|
|
47
49
|
var requestHelpers = require("./request-helpers");
|
|
50
|
+
var restore = require("./restore");
|
|
48
51
|
var restoreBundle = require("./restore-bundle");
|
|
52
|
+
var restoreRollback = require("./restore-rollback");
|
|
49
53
|
var seeders = require("./seeders");
|
|
50
54
|
var vaultPassphraseOps = require("./vault/passphrase-ops");
|
|
51
55
|
|
|
@@ -512,6 +516,7 @@ var AUDIT_USAGE = [
|
|
|
512
516
|
" archive Bundle audit rows older than --before into a verified archive",
|
|
513
517
|
" export Auditor evidence bundle for a date range",
|
|
514
518
|
" verify-bundle Round-trip integrity check on an archive or export bundle",
|
|
519
|
+
" verify-chain Walk the live audit chain end-to-end; reports tampering",
|
|
515
520
|
" purge Delete live rows already captured in a verified archive",
|
|
516
521
|
"",
|
|
517
522
|
"Common flags:",
|
|
@@ -527,12 +532,17 @@ var AUDIT_USAGE = [
|
|
|
527
532
|
" --to <date> Latest recordedAt (inclusive)",
|
|
528
533
|
" --action <name> Restrict to a single audit action",
|
|
529
534
|
"",
|
|
535
|
+
"verify-chain flags:",
|
|
536
|
+
" --db <path> SQLite database path (required)",
|
|
537
|
+
" --table <name> Audit table name (default audit_log)",
|
|
538
|
+
" --max-rows <N> Stop after walking N rows (default: walk all)",
|
|
539
|
+
"",
|
|
530
540
|
"purge flags:",
|
|
531
541
|
" --confirm REQUIRED — operator acknowledgement of destructive op",
|
|
532
542
|
"",
|
|
533
543
|
"Exit codes:",
|
|
534
|
-
" 0 success",
|
|
535
|
-
" 1 operation failed",
|
|
544
|
+
" 0 success (or chain verified ok)",
|
|
545
|
+
" 1 operation failed (or chain tampered)",
|
|
536
546
|
" 2 bad invocation",
|
|
537
547
|
].join("\n");
|
|
538
548
|
|
|
@@ -561,6 +571,7 @@ async function _runAudit(args, ctx) {
|
|
|
561
571
|
}
|
|
562
572
|
var sub = args.pos[0];
|
|
563
573
|
var passphrase = _resolvePassphrase(args, ctx);
|
|
574
|
+
// verify-chain reads the live DB, no bundle passphrase needed.
|
|
564
575
|
var passRequired = sub === "archive" || sub === "export" ||
|
|
565
576
|
sub === "verify-bundle" || sub === "purge";
|
|
566
577
|
if (passRequired && !passphrase) {
|
|
@@ -633,6 +644,56 @@ async function _runAudit(args, ctx) {
|
|
|
633
644
|
}
|
|
634
645
|
}
|
|
635
646
|
|
|
647
|
+
if (sub === "verify-chain") {
|
|
648
|
+
var dbPathV = args.flags.db;
|
|
649
|
+
if (!dbPathV || dbPathV === true) {
|
|
650
|
+
_writeLine(ctx.stderr, "blamejs audit verify-chain: --db <path> is required");
|
|
651
|
+
return 2;
|
|
652
|
+
}
|
|
653
|
+
dbPathV = _resolvePath(String(dbPathV), ctx.cwd);
|
|
654
|
+
var tableV = args.flags.table ? String(args.flags.table) : "audit_log";
|
|
655
|
+
var maxRows = args.flags["max-rows"];
|
|
656
|
+
var maxRowsN = maxRows === undefined ? undefined : Number(maxRows);
|
|
657
|
+
if (maxRowsN !== undefined && (!Number.isFinite(maxRowsN) || maxRowsN < 1)) {
|
|
658
|
+
_writeLine(ctx.stderr, "blamejs audit verify-chain: --max-rows must be a positive integer");
|
|
659
|
+
return 2;
|
|
660
|
+
}
|
|
661
|
+
var dbV;
|
|
662
|
+
try { dbV = _openSqlite(dbPathV); }
|
|
663
|
+
catch (e) {
|
|
664
|
+
_writeLine(ctx.stderr, "blamejs audit verify-chain: cannot open db at " + dbPathV +
|
|
665
|
+
": " + ((e && e.message) || String(e)));
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
var queryAllAsync = async function (sql, params) {
|
|
670
|
+
var stmt = dbV.prepare(sql);
|
|
671
|
+
return Array.isArray(params) ? stmt.all.apply(stmt, params) : stmt.all();
|
|
672
|
+
};
|
|
673
|
+
var vc = await auditChain.verifyChain(queryAllAsync, tableV,
|
|
674
|
+
maxRowsN ? { maxRows: maxRowsN } : {});
|
|
675
|
+
if (vc.ok) {
|
|
676
|
+
_writeLine(ctx.stdout, "OK — chain verified" +
|
|
677
|
+
" (table=" + vc.table +
|
|
678
|
+
", rowsVerified=" + vc.rowsVerified + ")");
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
_writeLine(ctx.stderr, "FAIL — " + vc.reason +
|
|
682
|
+
" (table=" + vc.table +
|
|
683
|
+
", rowsVerified=" + vc.rowsVerified +
|
|
684
|
+
", breakAt=" + vc.breakAt +
|
|
685
|
+
", breakRowId=" + vc.breakRowId + ")");
|
|
686
|
+
_writeLine(ctx.stderr, " expected prevHash: " + vc.expected);
|
|
687
|
+
_writeLine(ctx.stderr, " actual: " + vc.actual);
|
|
688
|
+
return 1;
|
|
689
|
+
} catch (e) {
|
|
690
|
+
_writeLine(ctx.stderr, "blamejs audit verify-chain: " + ((e && e.message) || String(e)));
|
|
691
|
+
return 1;
|
|
692
|
+
} finally {
|
|
693
|
+
try { dbV.close(); } catch (_e) { /* close best-effort */ }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
636
697
|
if (sub === "purge") {
|
|
637
698
|
var inP = _resolveOutPath(args.flags.archive || args.flags.in, ctx);
|
|
638
699
|
if (!inP) {
|
|
@@ -661,6 +722,282 @@ async function _runAudit(args, ctx) {
|
|
|
661
722
|
return 2;
|
|
662
723
|
}
|
|
663
724
|
|
|
725
|
+
// ---- Subcommand: restore ----
|
|
726
|
+
//
|
|
727
|
+
// Operator workflow on top of b.restore: list bundles in storage,
|
|
728
|
+
// inspect a specific one, do a live in-place restore (with rollback
|
|
729
|
+
// preservation), and roll back to a previous restore point. Wraps the
|
|
730
|
+
// restore primitive's run / inspect / list / rollback / list-rollbacks
|
|
731
|
+
// surface; uses b.backup.localStorage as the storage adapter (the same
|
|
732
|
+
// adapter that wrote the bundles).
|
|
733
|
+
//
|
|
734
|
+
// Two ways to identify a bundle for inspect / apply:
|
|
735
|
+
// --bundle <dir> point at an extracted bundle directory
|
|
736
|
+
// (parent dir is treated as storage root,
|
|
737
|
+
// dir basename as bundle id — matches the
|
|
738
|
+
// shape `blamejs backup extract` produces)
|
|
739
|
+
// --storage-root <root> --bundle-id <id> use a multi-bundle root
|
|
740
|
+
|
|
741
|
+
var RESTORE_USAGE = [
|
|
742
|
+
"Usage: blamejs restore <subcommand> [flags]",
|
|
743
|
+
"",
|
|
744
|
+
"Subcommands:",
|
|
745
|
+
" list List bundles available in storage",
|
|
746
|
+
" inspect Read a bundle manifest summary (no live changes)",
|
|
747
|
+
" apply Live in-place restore with rollback preserved",
|
|
748
|
+
" rollback Revert the most-recent (or named) restore",
|
|
749
|
+
" list-rollbacks List preserved rollback points",
|
|
750
|
+
"",
|
|
751
|
+
"Common flags:",
|
|
752
|
+
" --data-dir <path> Live data directory (apply / rollback / list-rollbacks)",
|
|
753
|
+
" --storage-root <path> Directory containing bundle subdirs (list)",
|
|
754
|
+
" --bundle <dir> Extracted bundle directory (inspect / apply)",
|
|
755
|
+
" --bundle-id <id> Alternative: pass id with --storage-root",
|
|
756
|
+
" --rollback-root <path> Override rollback dir (default <data-dir>.rollbacks)",
|
|
757
|
+
" --passphrase <string> Bundle passphrase (or env BLAMEJS_BACKUP_PASSPHRASE)",
|
|
758
|
+
"",
|
|
759
|
+
"apply flags:",
|
|
760
|
+
" --max-pulled-bytes <N> Refuse a bundle whose pulled bytes exceed N (default 4 GiB)",
|
|
761
|
+
" --max-pulled-files <N> Refuse a bundle whose pulled file count exceeds N (default 100K)",
|
|
762
|
+
" --no-audit Suppress audit emission (default ON)",
|
|
763
|
+
"",
|
|
764
|
+
"rollback flags:",
|
|
765
|
+
" --rollback <pathOrId> Specific rollback point to restore (default: most recent)",
|
|
766
|
+
"",
|
|
767
|
+
"Exit codes:",
|
|
768
|
+
" 0 success",
|
|
769
|
+
" 1 operation failed",
|
|
770
|
+
" 2 bad invocation",
|
|
771
|
+
].join("\n");
|
|
772
|
+
|
|
773
|
+
// Resolve {storageRoot, bundleId} from either --bundle <dir> OR
|
|
774
|
+
// --storage-root <root> --bundle-id <id>. Returns null + writes an
|
|
775
|
+
// error on the report when neither shape works.
|
|
776
|
+
function _resolveRestoreBundleSelector(args, ctx, report, requireBundle) {
|
|
777
|
+
var bundleFlag = args.flags.bundle;
|
|
778
|
+
var storageRootFlag = args.flags["storage-root"];
|
|
779
|
+
var bundleIdFlag = args.flags["bundle-id"];
|
|
780
|
+
if (bundleFlag && bundleFlag !== true) {
|
|
781
|
+
var bundlePath = _resolvePath(String(bundleFlag), ctx.cwd);
|
|
782
|
+
return {
|
|
783
|
+
storageRoot: path.dirname(bundlePath),
|
|
784
|
+
bundleId: path.basename(bundlePath),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (storageRootFlag && storageRootFlag !== true) {
|
|
788
|
+
var sr = _resolvePath(String(storageRootFlag), ctx.cwd);
|
|
789
|
+
if (requireBundle) {
|
|
790
|
+
if (!bundleIdFlag || bundleIdFlag === true) {
|
|
791
|
+
report.error("--bundle-id is required when using --storage-root", 2);
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
return { storageRoot: sr, bundleId: String(bundleIdFlag) };
|
|
795
|
+
}
|
|
796
|
+
return { storageRoot: sr, bundleId: null };
|
|
797
|
+
}
|
|
798
|
+
if (requireBundle) {
|
|
799
|
+
report.error("--bundle <dir> OR --storage-root <root> --bundle-id <id> is required", 2);
|
|
800
|
+
} else {
|
|
801
|
+
report.error("--storage-root <root> is required", 2);
|
|
802
|
+
}
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function _runRestore(args, ctx) {
|
|
807
|
+
if (args.pos.length === 0) {
|
|
808
|
+
return cliHelpers.makeReporter(ctx, "blamejs restore").usage(RESTORE_USAGE);
|
|
809
|
+
}
|
|
810
|
+
var sub = args.pos[0];
|
|
811
|
+
var report = cliHelpers.makeReporter(ctx, "blamejs restore " + sub);
|
|
812
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
813
|
+
return report.helpStdout(RESTORE_USAGE);
|
|
814
|
+
}
|
|
815
|
+
if (["list", "inspect", "apply", "rollback", "list-rollbacks"].indexOf(sub) === -1) {
|
|
816
|
+
cliHelpers.makeReporter(ctx, "blamejs restore").error("unknown subcommand '" + sub + "'", 2);
|
|
817
|
+
return cliHelpers.makeReporter(ctx, "blamejs restore").usage(RESTORE_USAGE);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// list / inspect / apply all need a storage root + (for inspect/apply) a bundle id.
|
|
821
|
+
// rollback / list-rollbacks only need data-dir.
|
|
822
|
+
var dataDirFlag = args.flags["data-dir"];
|
|
823
|
+
function _requireDataDir() {
|
|
824
|
+
if (!dataDirFlag || dataDirFlag === true) {
|
|
825
|
+
report.error("--data-dir <path> is required", 2);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
return _resolvePath(String(dataDirFlag), ctx.cwd);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (sub === "list") {
|
|
832
|
+
var sel = _resolveRestoreBundleSelector(args, ctx, report, false);
|
|
833
|
+
if (!sel) return 2;
|
|
834
|
+
try {
|
|
835
|
+
var storage = backup.localStorage({ root: sel.storageRoot });
|
|
836
|
+
var bundles = await storage.listBundles();
|
|
837
|
+
if (bundles.length === 0) {
|
|
838
|
+
report.write("no bundles in " + sel.storageRoot);
|
|
839
|
+
return report.ok();
|
|
840
|
+
}
|
|
841
|
+
report.write("bundles in " + sel.storageRoot + ": " + bundles.length);
|
|
842
|
+
for (var i = 0; i < bundles.length; i++) {
|
|
843
|
+
var b = bundles[i];
|
|
844
|
+
report.write(" " + b.bundleId +
|
|
845
|
+
" size=" + (b.size != null ? b.size + "B" : "?") +
|
|
846
|
+
" createdAt=" + (b.createdAt || "?"));
|
|
847
|
+
}
|
|
848
|
+
return report.ok();
|
|
849
|
+
} catch (e) {
|
|
850
|
+
return report.error((e && e.message) || String(e));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (sub === "inspect") {
|
|
855
|
+
var selI = _resolveRestoreBundleSelector(args, ctx, report, true);
|
|
856
|
+
if (!selI) return 2;
|
|
857
|
+
try {
|
|
858
|
+
var storageI = backup.localStorage({ root: selI.storageRoot });
|
|
859
|
+
// restore.create needs a passphrase + dataDir even for inspect because
|
|
860
|
+
// its closure captures them; pass placeholders since inspect doesn't
|
|
861
|
+
// touch them.
|
|
862
|
+
var rI = restore.create({
|
|
863
|
+
dataDir: path.join(os.tmpdir(), "blamejs-restore-inspect-noop"),
|
|
864
|
+
storage: storageI,
|
|
865
|
+
passphrase: "inspect-only-not-used",
|
|
866
|
+
audit: false,
|
|
867
|
+
});
|
|
868
|
+
var manifest = await rI.inspect(selI.bundleId);
|
|
869
|
+
var totalBytesI = 0;
|
|
870
|
+
for (var ix = 0; ix < manifest.files.length; ix++) {
|
|
871
|
+
totalBytesI += manifest.files[ix].encryptedSize || 0;
|
|
872
|
+
}
|
|
873
|
+
report.write("bundle: " + selI.bundleId);
|
|
874
|
+
report.write("storage root: " + selI.storageRoot);
|
|
875
|
+
report.write("manifest: v" + (manifest.manifestVersion || manifest.version || "unknown"));
|
|
876
|
+
report.write("created: " + (manifest.createdAt || "unknown"));
|
|
877
|
+
report.write("files: " + manifest.files.length);
|
|
878
|
+
report.write("encrypted size: " + totalBytesI + " bytes");
|
|
879
|
+
return report.ok();
|
|
880
|
+
} catch (e) {
|
|
881
|
+
return report.error((e && e.message) || String(e));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (sub === "apply") {
|
|
886
|
+
var dd = _requireDataDir();
|
|
887
|
+
if (!dd) return 2;
|
|
888
|
+
var selA = _resolveRestoreBundleSelector(args, ctx, report, true);
|
|
889
|
+
if (!selA) return 2;
|
|
890
|
+
var pp = cliHelpers.resolvePassphrase(args, ctx, {
|
|
891
|
+
flag: "passphrase", envVar: "BLAMEJS_BACKUP_PASSPHRASE",
|
|
892
|
+
});
|
|
893
|
+
if (!pp) {
|
|
894
|
+
return report.error("--passphrase or BLAMEJS_BACKUP_PASSPHRASE is required", 2);
|
|
895
|
+
}
|
|
896
|
+
var rollbackRootA = args.flags["rollback-root"]
|
|
897
|
+
? _resolvePath(String(args.flags["rollback-root"]), ctx.cwd) : undefined;
|
|
898
|
+
var maxBytes = args.flags["max-pulled-bytes"];
|
|
899
|
+
var maxFiles = args.flags["max-pulled-files"];
|
|
900
|
+
if (maxBytes !== undefined && (!Number.isFinite(Number(maxBytes)) || Number(maxBytes) <= 0)) {
|
|
901
|
+
return report.error("--max-pulled-bytes must be a positive number", 2);
|
|
902
|
+
}
|
|
903
|
+
if (maxFiles !== undefined && (!Number.isFinite(Number(maxFiles)) || Number(maxFiles) <= 0)) {
|
|
904
|
+
return report.error("--max-pulled-files must be a positive number", 2);
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
var storageA = backup.localStorage({ root: selA.storageRoot });
|
|
908
|
+
var rA = restore.create({
|
|
909
|
+
dataDir: dd,
|
|
910
|
+
storage: storageA,
|
|
911
|
+
passphrase: pp,
|
|
912
|
+
rollbackRoot: rollbackRootA,
|
|
913
|
+
audit: args.flags["no-audit"] !== true,
|
|
914
|
+
maxPulledBytes: maxBytes !== undefined ? Number(maxBytes) : undefined,
|
|
915
|
+
maxPulledFiles: maxFiles !== undefined ? Number(maxFiles) : undefined,
|
|
916
|
+
});
|
|
917
|
+
var summary = await rA.run({ bundleId: selA.bundleId });
|
|
918
|
+
report.write("OK — restored");
|
|
919
|
+
report.write(" bundle: " + summary.bundleId);
|
|
920
|
+
report.write(" files: " + summary.fileCount);
|
|
921
|
+
report.write(" bytes: " + summary.totalBytes);
|
|
922
|
+
report.write(" rollback at: " + summary.rollbackPath);
|
|
923
|
+
report.write(" duration ms: " + summary.durationMs);
|
|
924
|
+
report.write("");
|
|
925
|
+
report.write("Stop and start the app fresh against " + dd + " to pick up the restored state.");
|
|
926
|
+
report.write("Roll back with: blamejs restore rollback --data-dir " + dd);
|
|
927
|
+
return report.ok();
|
|
928
|
+
} catch (e) {
|
|
929
|
+
return report.error((e && e.message) || String(e));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (sub === "rollback") {
|
|
934
|
+
var ddR = _requireDataDir();
|
|
935
|
+
if (!ddR) return 2;
|
|
936
|
+
var rollbackRootR = args.flags["rollback-root"]
|
|
937
|
+
? _resolvePath(String(args.flags["rollback-root"]), ctx.cwd)
|
|
938
|
+
: (ddR + ".rollbacks");
|
|
939
|
+
var rollbackTarget = args.flags.rollback;
|
|
940
|
+
var targetPath = null;
|
|
941
|
+
if (rollbackTarget && rollbackTarget !== true) {
|
|
942
|
+
// operator can pass either a full path or just the basename inside rollback-root
|
|
943
|
+
var rt = String(rollbackTarget);
|
|
944
|
+
targetPath = path.isAbsolute(rt) ? rt : path.resolve(rollbackRootR, rt);
|
|
945
|
+
} else {
|
|
946
|
+
// Default to most-recent rollback point (mirrors restore.create().rollback()).
|
|
947
|
+
var ptsR;
|
|
948
|
+
try { ptsR = restoreRollback.list({ rollbackRoot: rollbackRootR }); }
|
|
949
|
+
catch (e) {
|
|
950
|
+
return report.error("listing rollbacks at " + rollbackRootR + ": " + ((e && e.message) || String(e)));
|
|
951
|
+
}
|
|
952
|
+
if (!ptsR || ptsR.length === 0) {
|
|
953
|
+
return report.error("no rollback points at " + rollbackRootR + " — pass --rollback <pathOrId> explicitly", 2);
|
|
954
|
+
}
|
|
955
|
+
targetPath = ptsR[0].rollbackPath;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
var rR = await restoreRollback.rollback({
|
|
959
|
+
dataDir: ddR,
|
|
960
|
+
rollbackPath: targetPath,
|
|
961
|
+
rollbackRoot: rollbackRootR,
|
|
962
|
+
});
|
|
963
|
+
report.write("OK — rolled back");
|
|
964
|
+
report.write(" data dir: " + ddR);
|
|
965
|
+
report.write(" used: " + (targetPath || "most-recent rollback point"));
|
|
966
|
+
report.write(" discarded at: " + (rR.discardedAt || "(unknown)"));
|
|
967
|
+
return report.ok();
|
|
968
|
+
} catch (e) {
|
|
969
|
+
return report.error((e && e.message) || String(e));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (sub === "list-rollbacks") {
|
|
974
|
+
var ddL = _requireDataDir();
|
|
975
|
+
if (!ddL) return 2;
|
|
976
|
+
var rollbackRootL = args.flags["rollback-root"]
|
|
977
|
+
? _resolvePath(String(args.flags["rollback-root"]), ctx.cwd)
|
|
978
|
+
: (ddL + ".rollbacks");
|
|
979
|
+
try {
|
|
980
|
+
var pts = restoreRollback.list({ rollbackRoot: rollbackRootL });
|
|
981
|
+
if (pts.length === 0) {
|
|
982
|
+
report.write("no rollback points at " + rollbackRootL);
|
|
983
|
+
return report.ok();
|
|
984
|
+
}
|
|
985
|
+
report.write("rollback points at " + rollbackRootL + ": " + pts.length);
|
|
986
|
+
for (var p = 0; p < pts.length; p++) {
|
|
987
|
+
var pt = pts[p];
|
|
988
|
+
report.write(" " + (pt.rollbackPath || pt) +
|
|
989
|
+
(pt.recordedAt ? " recordedAt=" + pt.recordedAt : "") +
|
|
990
|
+
(pt.bundleId ? " bundleId=" + pt.bundleId : ""));
|
|
991
|
+
}
|
|
992
|
+
return report.ok();
|
|
993
|
+
} catch (e) {
|
|
994
|
+
return report.error((e && e.message) || String(e));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return 2;
|
|
999
|
+
}
|
|
1000
|
+
|
|
664
1001
|
// ---- Top-level help ----
|
|
665
1002
|
|
|
666
1003
|
// ---- Subcommand: api-key ----
|
|
@@ -1860,6 +2197,7 @@ var TOP_USAGE = [
|
|
|
1860
2197
|
" api-key Issue / revoke / list / rotate / verify API keys for a namespace",
|
|
1861
2198
|
" audit Operator tooling on top of the audit chain (archive / export / verify / purge)",
|
|
1862
2199
|
" backup Inspect / verify / extract a backup bundle from disk",
|
|
2200
|
+
" restore Live in-place restore from a bundle (list / inspect / apply / rollback / list-rollbacks)",
|
|
1863
2201
|
" mtls Inspect or generate the in-box mTLS CA + leaf certs (status / show-cert / init / issue / issue-p12)",
|
|
1864
2202
|
" vault Seal / unseal / rotate the on-disk vault keypair (plaintext ↔ wrapped)",
|
|
1865
2203
|
" security Run b.security.assertProduction against the live framework",
|
|
@@ -1919,6 +2257,7 @@ async function main(argv, opts) {
|
|
|
1919
2257
|
if (subTopic === "api-key") { _writeLine(ctx.stdout, API_KEY_USAGE); return 0; }
|
|
1920
2258
|
if (subTopic === "audit") { _writeLine(ctx.stdout, AUDIT_USAGE); return 0; }
|
|
1921
2259
|
if (subTopic === "backup") { _writeLine(ctx.stdout, BACKUP_USAGE); return 0; }
|
|
2260
|
+
if (subTopic === "restore") { _writeLine(ctx.stdout, RESTORE_USAGE); return 0; }
|
|
1922
2261
|
if (subTopic === "mtls") { _writeLine(ctx.stdout, MTLS_USAGE); return 0; }
|
|
1923
2262
|
if (subTopic === "vault") { _writeLine(ctx.stdout, VAULT_USAGE); return 0; }
|
|
1924
2263
|
if (subTopic === "security") { _writeLine(ctx.stdout, SECURITY_USAGE); return 0; }
|
|
@@ -1940,6 +2279,7 @@ async function main(argv, opts) {
|
|
|
1940
2279
|
if (cmd === "api-key") return await _runApiKey(rest, ctx);
|
|
1941
2280
|
if (cmd === "audit") return await _runAudit(rest, ctx);
|
|
1942
2281
|
if (cmd === "backup") return await _runBackup(rest, ctx);
|
|
2282
|
+
if (cmd === "restore") return await _runRestore(rest, ctx);
|
|
1943
2283
|
if (cmd === "mtls") return await _runMtls(rest, ctx);
|
|
1944
2284
|
if (cmd === "vault") return await _runVault(rest, ctx);
|
|
1945
2285
|
if (cmd === "security") return await _runSecurity(rest, ctx);
|
|
@@ -1966,6 +2306,7 @@ module.exports = {
|
|
|
1966
2306
|
API_KEY_USAGE: API_KEY_USAGE,
|
|
1967
2307
|
AUDIT_USAGE: AUDIT_USAGE,
|
|
1968
2308
|
BACKUP_USAGE: BACKUP_USAGE,
|
|
2309
|
+
RESTORE_USAGE: RESTORE_USAGE,
|
|
1969
2310
|
MTLS_USAGE: MTLS_USAGE,
|
|
1970
2311
|
VAULT_USAGE: VAULT_USAGE,
|
|
1971
2312
|
};
|
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:e07786d1-53d1-4554-b3d1-ff784476a034",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-02T15:17:41.618Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.6.26",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.26",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.6.26",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.6.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|