@blamejs/core 0.4.1
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 +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cli — the engine behind `blamejs` on the command line.
|
|
4
|
+
*
|
|
5
|
+
* bin/blamejs.js is a 4-line shim that calls cli.main(process.argv.slice(2)).
|
|
6
|
+
* Putting the dispatch logic in lib/ means tests drive it without
|
|
7
|
+
* spawning a child process: pass argv + a captured-output writer +
|
|
8
|
+
* env, get an exit code back.
|
|
9
|
+
*
|
|
10
|
+
* var cli = b.cli;
|
|
11
|
+
* var captured = { out: "", err: "" };
|
|
12
|
+
* var rc = await cli.main(["migrate", "status", "--db", "./test.db"], {
|
|
13
|
+
* stdout: { write: function (s) { captured.out += s; } },
|
|
14
|
+
* stderr: { write: function (s) { captured.err += s; } },
|
|
15
|
+
* env: {},
|
|
16
|
+
* cwd: "/repo",
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* Subcommands ship one at a time as the framework grows. The
|
|
20
|
+
* dispatch table is the registry — adding `vault rotate`, `audit verify`,
|
|
21
|
+
* `subject export` etc. lands as new entries here, not new bin scripts.
|
|
22
|
+
*
|
|
23
|
+
* Currently shipped:
|
|
24
|
+
* blamejs migrate up --db <path> [--dir <path>]
|
|
25
|
+
* blamejs migrate down --db <path> [--dir <path>] [--steps N]
|
|
26
|
+
* blamejs migrate status --db <path> [--dir <path>]
|
|
27
|
+
* blamejs dev --command <cmd> [--watch <dir>...] [--grace-ms N]
|
|
28
|
+
* blamejs version
|
|
29
|
+
* blamejs help [<command>]
|
|
30
|
+
*
|
|
31
|
+
* The migrate command operates against a node:sqlite file directly —
|
|
32
|
+
* no vault / framework bootstrap. Encrypted-at-rest dbs are out of
|
|
33
|
+
* scope for this slice; operators with that mode either run migrations
|
|
34
|
+
* before encryption or temporarily switch the file to plaintext.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var path = require("path");
|
|
38
|
+
var apiSnapshot = require("./api-snapshot");
|
|
39
|
+
var auditTools = require("./audit-tools");
|
|
40
|
+
var cliHelpers = require("./cli-helpers");
|
|
41
|
+
var constants = require("./constants");
|
|
42
|
+
var dev = require("./dev");
|
|
43
|
+
var migrations = require("./migrations");
|
|
44
|
+
var restoreBundle = require("./restore-bundle");
|
|
45
|
+
var seeders = require("./seeders");
|
|
46
|
+
var vaultPassphraseOps = require("./vault/passphrase-ops");
|
|
47
|
+
|
|
48
|
+
var DEFAULT_MIG_DIR = "./migrations";
|
|
49
|
+
var DEFAULT_SEED_DIR = "./seeders";
|
|
50
|
+
|
|
51
|
+
function _writeLine(stream, line) {
|
|
52
|
+
if (!stream || typeof stream.write !== "function") return;
|
|
53
|
+
stream.write(line + "\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Minimal argv parser: positional args + flag map. Supports both
|
|
57
|
+
// `--flag value` and `--flag=value`. Single-dash forms (-v) treated
|
|
58
|
+
// as long aliases to keep the surface predictable.
|
|
59
|
+
function _parseArgs(argv) {
|
|
60
|
+
var pos = [];
|
|
61
|
+
var flags = {};
|
|
62
|
+
for (var i = 0; i < argv.length; i++) {
|
|
63
|
+
var tok = argv[i];
|
|
64
|
+
if (tok === "--") {
|
|
65
|
+
for (var j = i + 1; j < argv.length; j++) pos.push(argv[j]);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (tok.indexOf("--") === 0) {
|
|
69
|
+
var name = tok.slice(2);
|
|
70
|
+
var eq = name.indexOf("=");
|
|
71
|
+
var val;
|
|
72
|
+
if (eq !== -1) {
|
|
73
|
+
val = name.slice(eq + 1);
|
|
74
|
+
name = name.slice(0, eq);
|
|
75
|
+
} else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0) {
|
|
76
|
+
val = argv[++i];
|
|
77
|
+
} else {
|
|
78
|
+
val = true; // boolean flag
|
|
79
|
+
}
|
|
80
|
+
flags[name] = val;
|
|
81
|
+
} else if (tok.indexOf("-") === 0 && tok.length === 2) {
|
|
82
|
+
// -v, -h
|
|
83
|
+
flags[tok.slice(1)] = true;
|
|
84
|
+
} else {
|
|
85
|
+
pos.push(tok);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { pos: pos, flags: flags };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _resolvePath(p, cwd) {
|
|
92
|
+
if (!p) return p;
|
|
93
|
+
if (path.isAbsolute(p)) return p;
|
|
94
|
+
return path.resolve(cwd || process.cwd(), p);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _openSqlite(dbPath) {
|
|
98
|
+
// Lazy-required so the CLI doesn't crash on `blamejs version` or
|
|
99
|
+
// `blamejs help` if node:sqlite isn't usable for some reason.
|
|
100
|
+
var { DatabaseSync } = require("node:sqlite");
|
|
101
|
+
return new DatabaseSync(dbPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- Subcommand: migrate ----
|
|
105
|
+
|
|
106
|
+
var MIGRATE_USAGE = [
|
|
107
|
+
"Usage: blamejs migrate <subcommand> [flags]",
|
|
108
|
+
"",
|
|
109
|
+
"Subcommands:",
|
|
110
|
+
" up Apply all pending migrations",
|
|
111
|
+
" down Roll back the most-recent applied migration",
|
|
112
|
+
" status Print applied + pending migrations",
|
|
113
|
+
"",
|
|
114
|
+
"Flags:",
|
|
115
|
+
" --db <path> Path to the SQLite database file (required)",
|
|
116
|
+
" --dir <path> Path to migrations directory (default ./migrations)",
|
|
117
|
+
" --steps <N> For down: number of migrations to revert (default 1)",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
async function _runMigrate(args, ctx) {
|
|
121
|
+
if (args.pos.length === 0) {
|
|
122
|
+
_writeLine(ctx.stderr, MIGRATE_USAGE);
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
var sub = args.pos[0];
|
|
126
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
127
|
+
_writeLine(ctx.stdout, MIGRATE_USAGE);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (sub !== "up" && sub !== "down" && sub !== "status") {
|
|
131
|
+
_writeLine(ctx.stderr, "blamejs migrate: unknown subcommand '" + sub + "'");
|
|
132
|
+
_writeLine(ctx.stderr, MIGRATE_USAGE);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
var dbPath = args.flags.db;
|
|
137
|
+
if (!dbPath || dbPath === true) {
|
|
138
|
+
_writeLine(ctx.stderr, "blamejs migrate " + sub + ": --db <path> is required");
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
dbPath = _resolvePath(String(dbPath), ctx.cwd);
|
|
142
|
+
|
|
143
|
+
var dir = _resolvePath(String(args.flags.dir || DEFAULT_MIG_DIR), ctx.cwd);
|
|
144
|
+
|
|
145
|
+
var db;
|
|
146
|
+
try { db = _openSqlite(dbPath); }
|
|
147
|
+
catch (e) {
|
|
148
|
+
_writeLine(ctx.stderr, "blamejs migrate: cannot open db at " + dbPath +
|
|
149
|
+
": " + ((e && e.message) || String(e)));
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
var runner = migrations.create({ db: db, dir: dir });
|
|
155
|
+
|
|
156
|
+
if (sub === "status") {
|
|
157
|
+
var s = runner.status();
|
|
158
|
+
_writeLine(ctx.stdout, "applied: " + s.applied.length + " / " + s.total);
|
|
159
|
+
for (var i = 0; i < s.applied.length; i++) {
|
|
160
|
+
_writeLine(ctx.stdout, " ✓ " + s.applied[i].name +
|
|
161
|
+
" (applied " + s.applied[i].appliedAt + ")");
|
|
162
|
+
}
|
|
163
|
+
_writeLine(ctx.stdout, "pending: " + s.pending.length);
|
|
164
|
+
for (var j = 0; j < s.pending.length; j++) {
|
|
165
|
+
_writeLine(ctx.stdout, " · " + s.pending[j]);
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (sub === "up") {
|
|
171
|
+
var r = runner.up();
|
|
172
|
+
if (r.applied.length === 0) {
|
|
173
|
+
_writeLine(ctx.stdout, "no pending migrations (" + r.skipped.length + " already applied)");
|
|
174
|
+
} else {
|
|
175
|
+
_writeLine(ctx.stdout, "applied " + r.applied.length + " migration(s):");
|
|
176
|
+
for (var k = 0; k < r.applied.length; k++) {
|
|
177
|
+
_writeLine(ctx.stdout, " ✓ " + r.applied[k]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (sub === "down") {
|
|
184
|
+
var steps = args.flags.steps === undefined ? 1 : Number(args.flags.steps);
|
|
185
|
+
if (!Number.isFinite(steps) || steps < 1 || Math.floor(steps) !== steps) {
|
|
186
|
+
_writeLine(ctx.stderr, "blamejs migrate down: --steps must be a positive integer");
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
var rd = runner.down({ steps: steps });
|
|
190
|
+
if (rd.reverted.length === 0) {
|
|
191
|
+
_writeLine(ctx.stdout, "nothing to revert");
|
|
192
|
+
} else {
|
|
193
|
+
_writeLine(ctx.stdout, "reverted " + rd.reverted.length + " migration(s):");
|
|
194
|
+
for (var m = 0; m < rd.reverted.length; m++) {
|
|
195
|
+
_writeLine(ctx.stdout, " ↶ " + rd.reverted[m]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
var msg = (e && e.message) || String(e);
|
|
202
|
+
var code = (e && e.code) || "ERROR";
|
|
203
|
+
_writeLine(ctx.stderr, "blamejs migrate " + sub + ": " + code + ": " + msg);
|
|
204
|
+
return 1;
|
|
205
|
+
} finally {
|
|
206
|
+
try { db.close(); } catch (_e) { /* close best-effort */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- Subcommand: seed ----
|
|
213
|
+
|
|
214
|
+
var SEED_USAGE = [
|
|
215
|
+
"Usage: blamejs seed <subcommand> [flags]",
|
|
216
|
+
"",
|
|
217
|
+
"Subcommands:",
|
|
218
|
+
" run Apply pending seeds for the given env",
|
|
219
|
+
" status Print applied + pending seeds for the given env",
|
|
220
|
+
"",
|
|
221
|
+
"Flags:",
|
|
222
|
+
" --db <path> Path to the SQLite database file [required]",
|
|
223
|
+
" --env <name> Environment to seed (dev / test / prod / ...) [required]",
|
|
224
|
+
" --dir <path> Path to seeders directory (default ./seeders)",
|
|
225
|
+
" --only <name> Apply just one seed by filename (run subcommand only)",
|
|
226
|
+
" --force Re-apply already-applied seeds (operator-explicit)",
|
|
227
|
+
].join("\n");
|
|
228
|
+
|
|
229
|
+
async function _runSeed(args, ctx) {
|
|
230
|
+
if (args.pos.length === 0) {
|
|
231
|
+
_writeLine(ctx.stderr, SEED_USAGE);
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
var sub = args.pos[0];
|
|
235
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
236
|
+
_writeLine(ctx.stdout, SEED_USAGE);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
if (sub !== "run" && sub !== "status") {
|
|
240
|
+
_writeLine(ctx.stderr, "blamejs seed: unknown subcommand '" + sub + "'");
|
|
241
|
+
_writeLine(ctx.stderr, SEED_USAGE);
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
var dbPath = args.flags.db;
|
|
246
|
+
if (!dbPath || dbPath === true) {
|
|
247
|
+
_writeLine(ctx.stderr, "blamejs seed " + sub + ": --db <path> is required");
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
dbPath = _resolvePath(String(dbPath), ctx.cwd);
|
|
251
|
+
|
|
252
|
+
var env = args.flags.env;
|
|
253
|
+
if (!env || env === true) {
|
|
254
|
+
_writeLine(ctx.stderr, "blamejs seed " + sub + ": --env <name> is required");
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
var dir = _resolvePath(String(args.flags.dir || DEFAULT_SEED_DIR), ctx.cwd);
|
|
259
|
+
|
|
260
|
+
var db;
|
|
261
|
+
try { db = _openSqlite(dbPath); }
|
|
262
|
+
catch (e) {
|
|
263
|
+
_writeLine(ctx.stderr, "blamejs seed: cannot open db at " + dbPath +
|
|
264
|
+
": " + ((e && e.message) || String(e)));
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
var runner = seeders.create({ db: db, dir: dir });
|
|
270
|
+
|
|
271
|
+
if (sub === "status") {
|
|
272
|
+
var s = await runner.status({ env: String(env) });
|
|
273
|
+
_writeLine(ctx.stdout, "env: " + s.env);
|
|
274
|
+
_writeLine(ctx.stdout, "applied: " + s.applied.length + " / " + s.total);
|
|
275
|
+
for (var i = 0; i < s.applied.length; i++) {
|
|
276
|
+
_writeLine(ctx.stdout, " ✓ " + s.applied[i].name +
|
|
277
|
+
" (applied " + s.applied[i].appliedAt + ")");
|
|
278
|
+
}
|
|
279
|
+
_writeLine(ctx.stdout, "pending: " + s.pending.length);
|
|
280
|
+
for (var j = 0; j < s.pending.length; j++) {
|
|
281
|
+
_writeLine(ctx.stdout, " · " + s.pending[j]);
|
|
282
|
+
}
|
|
283
|
+
if (s.rerunnable.length > 0) {
|
|
284
|
+
_writeLine(ctx.stdout, "rerunnable: " + s.rerunnable.length);
|
|
285
|
+
for (var k = 0; k < s.rerunnable.length; k++) {
|
|
286
|
+
_writeLine(ctx.stdout, " ↻ " + s.rerunnable[k]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (sub === "run") {
|
|
293
|
+
var only = args.flags.only ? String(args.flags.only) : undefined;
|
|
294
|
+
var force = !!args.flags.force;
|
|
295
|
+
var r = await runner.run({ env: String(env), only: only, force: force });
|
|
296
|
+
if (r.applied.length === 0) {
|
|
297
|
+
_writeLine(ctx.stdout, "no seeds applied (" + r.skipped.length + " skipped)");
|
|
298
|
+
} else {
|
|
299
|
+
_writeLine(ctx.stdout, "applied " + r.applied.length + " seed(s):");
|
|
300
|
+
for (var m = 0; m < r.applied.length; m++) {
|
|
301
|
+
_writeLine(ctx.stdout, " ✓ " + r.applied[m]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (r.skipped.length > 0) {
|
|
305
|
+
_writeLine(ctx.stdout, "skipped " + r.skipped.length + " (already applied)");
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
} catch (e) {
|
|
310
|
+
var msg = (e && e.message) || String(e);
|
|
311
|
+
var code = (e && e.code) || "ERROR";
|
|
312
|
+
_writeLine(ctx.stderr, "blamejs seed " + sub + ": " + code + ": " + msg);
|
|
313
|
+
return 1;
|
|
314
|
+
} finally {
|
|
315
|
+
try { db.close(); } catch (_e) { /* close best-effort */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- Subcommand: dev ----
|
|
322
|
+
|
|
323
|
+
var DEV_USAGE = [
|
|
324
|
+
"Usage: blamejs dev --command <cmd> [args] [flags]",
|
|
325
|
+
"",
|
|
326
|
+
"Spawn a child process and restart it on file changes.",
|
|
327
|
+
"",
|
|
328
|
+
"Flags:",
|
|
329
|
+
" --command <cmd> Program to spawn (e.g. node) [required]",
|
|
330
|
+
" --arg <value> Argument for the spawned program (repeatable)",
|
|
331
|
+
" --watch <dir> Directory to watch (repeatable; default '.')",
|
|
332
|
+
" --ignore <pattern> Glob/regex fragment to ignore (repeatable)",
|
|
333
|
+
" --grace-ms <N> Debounce window in ms (default 250)",
|
|
334
|
+
" --kill-signal <S> Signal to send on restart (default SIGTERM)",
|
|
335
|
+
"",
|
|
336
|
+
"Example:",
|
|
337
|
+
" blamejs dev --command node --arg ./server.js --watch ./routes --watch ./views",
|
|
338
|
+
].join("\n");
|
|
339
|
+
|
|
340
|
+
function _coerceList(val) {
|
|
341
|
+
if (val === undefined || val === null) return [];
|
|
342
|
+
return Array.isArray(val) ? val.slice() : [val];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function _runDev(args, ctx) {
|
|
346
|
+
if (args.flags.help || args.flags.h) {
|
|
347
|
+
_writeLine(ctx.stdout, DEV_USAGE);
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
var command = args.flags.command;
|
|
351
|
+
if (!command || command === true) {
|
|
352
|
+
_writeLine(ctx.stderr, "blamejs dev: --command <cmd> is required");
|
|
353
|
+
_writeLine(ctx.stderr, DEV_USAGE);
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
var argList = _coerceList(args.flags.arg).map(String);
|
|
357
|
+
var watchList = _coerceList(args.flags.watch).map(String);
|
|
358
|
+
var ignoreList = _coerceList(args.flags.ignore).map(function (s) { return new RegExp(String(s)); });
|
|
359
|
+
var graceMs = args.flags["grace-ms"] !== undefined ? Number(args.flags["grace-ms"]) : undefined;
|
|
360
|
+
if (graceMs !== undefined && (!Number.isFinite(graceMs) || graceMs < 0)) {
|
|
361
|
+
_writeLine(ctx.stderr, "blamejs dev: --grace-ms must be a non-negative number");
|
|
362
|
+
return 2;
|
|
363
|
+
}
|
|
364
|
+
var killSignal = args.flags["kill-signal"];
|
|
365
|
+
|
|
366
|
+
var d = dev.create({
|
|
367
|
+
command: String(command),
|
|
368
|
+
args: argList,
|
|
369
|
+
watch: watchList.length ? watchList : undefined,
|
|
370
|
+
ignore: ignoreList.length ? ignoreList : undefined,
|
|
371
|
+
graceMs: graceMs,
|
|
372
|
+
killSignal: typeof killSignal === "string" ? killSignal : undefined,
|
|
373
|
+
cwd: ctx.cwd,
|
|
374
|
+
env: ctx.env,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Forward parent SIGINT/SIGTERM to the child via stop()
|
|
378
|
+
var stopped = false;
|
|
379
|
+
function shutdown() {
|
|
380
|
+
if (stopped) return;
|
|
381
|
+
stopped = true;
|
|
382
|
+
d.stop().then(function () { /* exit naturally */ });
|
|
383
|
+
}
|
|
384
|
+
process.once("SIGINT", shutdown);
|
|
385
|
+
process.once("SIGTERM", shutdown);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
await d.start();
|
|
389
|
+
} catch (e) {
|
|
390
|
+
_writeLine(ctx.stderr, "blamejs dev: " + ((e && e.message) || String(e)));
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
// The dev loop runs until the operator interrupts. Resolve a
|
|
394
|
+
// never-settling promise so main() awaits forever; the SIGINT handler
|
|
395
|
+
// above flips stopped+resolves on Ctrl-C.
|
|
396
|
+
await new Promise(function (resolve) {
|
|
397
|
+
var iv = setInterval(function () {
|
|
398
|
+
if (stopped) { clearInterval(iv); resolve(); }
|
|
399
|
+
}, 250);
|
|
400
|
+
if (typeof iv.unref === "function") iv.unref();
|
|
401
|
+
});
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---- Subcommand: api-snapshot ----
|
|
406
|
+
|
|
407
|
+
var API_SNAPSHOT_USAGE = [
|
|
408
|
+
"Usage: blamejs api-snapshot <subcommand> [flags]",
|
|
409
|
+
"",
|
|
410
|
+
"Subcommands:",
|
|
411
|
+
" capture Walk the framework's public surface and write a snapshot",
|
|
412
|
+
" compare Diff the current surface against a saved snapshot",
|
|
413
|
+
"",
|
|
414
|
+
"Flags:",
|
|
415
|
+
" --file <path> Snapshot file path (default ./api-snapshot.json)",
|
|
416
|
+
" --module <path> Module to inspect (default require('@blamejs/core'))",
|
|
417
|
+
"",
|
|
418
|
+
"Exit codes:",
|
|
419
|
+
" 0 no changes (compare) or write succeeded (capture)",
|
|
420
|
+
" 1 breaking changes detected (compare)",
|
|
421
|
+
" 2 bad invocation",
|
|
422
|
+
].join("\n");
|
|
423
|
+
|
|
424
|
+
function _resolveTargetModule(modulePath, ctx) {
|
|
425
|
+
// Default: load index.js from the framework root (one level up from lib/cli.js)
|
|
426
|
+
if (!modulePath) {
|
|
427
|
+
var root = path.resolve(__dirname, "..");
|
|
428
|
+
return require(path.join(root, "index.js"));
|
|
429
|
+
}
|
|
430
|
+
var abs = path.isAbsolute(modulePath) ? modulePath : path.resolve(ctx.cwd, modulePath);
|
|
431
|
+
delete require.cache[require.resolve(abs)];
|
|
432
|
+
return require(abs);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function _runApiSnapshot(args, ctx) {
|
|
436
|
+
if (args.flags.help || args.flags.h) {
|
|
437
|
+
_writeLine(ctx.stdout, API_SNAPSHOT_USAGE);
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
if (args.pos.length === 0) {
|
|
441
|
+
_writeLine(ctx.stderr, API_SNAPSHOT_USAGE);
|
|
442
|
+
return 2;
|
|
443
|
+
}
|
|
444
|
+
var sub = args.pos[0];
|
|
445
|
+
var file = String(args.flags.file || "./api-snapshot.json");
|
|
446
|
+
var filePath = path.isAbsolute(file) ? file : path.resolve(ctx.cwd, file);
|
|
447
|
+
var modulePathOpt = typeof args.flags.module === "string" ? args.flags.module : null;
|
|
448
|
+
|
|
449
|
+
if (sub === "capture") {
|
|
450
|
+
var target;
|
|
451
|
+
try { target = _resolveTargetModule(modulePathOpt, ctx); }
|
|
452
|
+
catch (e) {
|
|
453
|
+
_writeLine(ctx.stderr, "blamejs api-snapshot capture: cannot load module: " +
|
|
454
|
+
((e && e.message) || String(e)));
|
|
455
|
+
return 1;
|
|
456
|
+
}
|
|
457
|
+
var snap;
|
|
458
|
+
try { snap = apiSnapshot.capture(target); }
|
|
459
|
+
catch (e) {
|
|
460
|
+
_writeLine(ctx.stderr, "blamejs api-snapshot capture: " +
|
|
461
|
+
((e && e.message) || String(e)));
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
apiSnapshot.write(snap, filePath);
|
|
465
|
+
_writeLine(ctx.stdout, "wrote snapshot to " + filePath +
|
|
466
|
+
" (frameworkVersion " + snap.frameworkVersion + ")");
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (sub === "compare") {
|
|
471
|
+
var loaded;
|
|
472
|
+
try { loaded = apiSnapshot.read(filePath); }
|
|
473
|
+
catch (e) {
|
|
474
|
+
_writeLine(ctx.stderr, "blamejs api-snapshot compare: " +
|
|
475
|
+
((e && e.message) || String(e)));
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
var current;
|
|
479
|
+
try {
|
|
480
|
+
var t = _resolveTargetModule(modulePathOpt, ctx);
|
|
481
|
+
current = apiSnapshot.capture(t);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
_writeLine(ctx.stderr, "blamejs api-snapshot compare: cannot capture current surface: " +
|
|
484
|
+
((e && e.message) || String(e)));
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
var diff = apiSnapshot.compare(loaded, current);
|
|
488
|
+
_writeLine(ctx.stdout, apiSnapshot.formatDiff(diff));
|
|
489
|
+
if (diff.breaking.length > 0) return 1;
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
_writeLine(ctx.stderr, "blamejs api-snapshot: unknown subcommand '" + sub + "'");
|
|
494
|
+
_writeLine(ctx.stderr, API_SNAPSHOT_USAGE);
|
|
495
|
+
return 2;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---- Subcommand: audit ----
|
|
499
|
+
//
|
|
500
|
+
// Operator tooling on top of the audit chain. Programmatic API is at
|
|
501
|
+
// b.auditTools — the CLI is a thin wrapper that's easier to script
|
|
502
|
+
// against from operator runbooks (cron, retention pipelines, etc.).
|
|
503
|
+
|
|
504
|
+
var AUDIT_USAGE = [
|
|
505
|
+
"Usage: blamejs audit <subcommand> [flags]",
|
|
506
|
+
"",
|
|
507
|
+
"Subcommands:",
|
|
508
|
+
" archive Bundle audit rows older than --before into a verified archive",
|
|
509
|
+
" export Auditor evidence bundle for a date range",
|
|
510
|
+
" verify-bundle Round-trip integrity check on an archive or export bundle",
|
|
511
|
+
" purge Delete live rows already captured in a verified archive",
|
|
512
|
+
"",
|
|
513
|
+
"Common flags:",
|
|
514
|
+
" --out <path> Output bundle directory (must NOT exist)",
|
|
515
|
+
" --in <path> Input bundle directory (verify-bundle, purge)",
|
|
516
|
+
" --passphrase <string> Bundle passphrase (or env BLAMEJS_AUDIT_PASSPHRASE)",
|
|
517
|
+
"",
|
|
518
|
+
"archive flags:",
|
|
519
|
+
" --before <date> Archive rows with recordedAt < this date (ISO-8601 or epoch ms)",
|
|
520
|
+
"",
|
|
521
|
+
"export flags:",
|
|
522
|
+
" --from <date> Earliest recordedAt (inclusive)",
|
|
523
|
+
" --to <date> Latest recordedAt (inclusive)",
|
|
524
|
+
" --action <name> Restrict to a single audit action",
|
|
525
|
+
"",
|
|
526
|
+
"purge flags:",
|
|
527
|
+
" --confirm REQUIRED — operator acknowledgement of destructive op",
|
|
528
|
+
"",
|
|
529
|
+
"Exit codes:",
|
|
530
|
+
" 0 success",
|
|
531
|
+
" 1 operation failed",
|
|
532
|
+
" 2 bad invocation",
|
|
533
|
+
].join("\n");
|
|
534
|
+
|
|
535
|
+
function _resolvePassphrase(args, ctx) {
|
|
536
|
+
if (typeof args.flags.passphrase === "string" && args.flags.passphrase.length > 0) {
|
|
537
|
+
return args.flags.passphrase;
|
|
538
|
+
}
|
|
539
|
+
var env = ctx.env && ctx.env.BLAMEJS_AUDIT_PASSPHRASE;
|
|
540
|
+
if (typeof env === "string" && env.length > 0) return env;
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function _resolveOutPath(p, ctx) {
|
|
545
|
+
if (!p) return null;
|
|
546
|
+
return path.isAbsolute(p) ? p : path.resolve(ctx.cwd, p);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function _runAudit(args, ctx) {
|
|
550
|
+
if (args.flags.help || args.flags.h) {
|
|
551
|
+
_writeLine(ctx.stdout, AUDIT_USAGE);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
if (args.pos.length === 0) {
|
|
555
|
+
_writeLine(ctx.stderr, AUDIT_USAGE);
|
|
556
|
+
return 2;
|
|
557
|
+
}
|
|
558
|
+
var sub = args.pos[0];
|
|
559
|
+
var passphrase = _resolvePassphrase(args, ctx);
|
|
560
|
+
var passRequired = sub === "archive" || sub === "export" ||
|
|
561
|
+
sub === "verify-bundle" || sub === "purge";
|
|
562
|
+
if (passRequired && !passphrase) {
|
|
563
|
+
_writeLine(ctx.stderr, "blamejs audit " + sub +
|
|
564
|
+
": --passphrase or BLAMEJS_AUDIT_PASSPHRASE is required");
|
|
565
|
+
return 2;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (sub === "archive") {
|
|
569
|
+
var out = _resolveOutPath(args.flags.out, ctx);
|
|
570
|
+
var before = args.flags.before;
|
|
571
|
+
if (!out) { _writeLine(ctx.stderr, "blamejs audit archive: --out is required"); return 2; }
|
|
572
|
+
if (!before) { _writeLine(ctx.stderr, "blamejs audit archive: --before is required"); return 2; }
|
|
573
|
+
try {
|
|
574
|
+
var r = await auditTools.archive({
|
|
575
|
+
before: before, out: out, passphrase: passphrase,
|
|
576
|
+
});
|
|
577
|
+
_writeLine(ctx.stdout, "wrote archive bundle to " + r.outDir +
|
|
578
|
+
" (rowCount=" + r.rowCount +
|
|
579
|
+
", counters=" + r.range.firstCounter + ".." + r.range.lastCounter + ")");
|
|
580
|
+
return 0;
|
|
581
|
+
} catch (e) {
|
|
582
|
+
_writeLine(ctx.stderr, "blamejs audit archive: " + ((e && e.message) || String(e)));
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (sub === "export") {
|
|
588
|
+
var outE = _resolveOutPath(args.flags.out, ctx);
|
|
589
|
+
var from = args.flags.from;
|
|
590
|
+
var to = args.flags.to;
|
|
591
|
+
var action = args.flags.action;
|
|
592
|
+
if (!outE) { _writeLine(ctx.stderr, "blamejs audit export: --out is required"); return 2; }
|
|
593
|
+
if (!from && !to && !action) {
|
|
594
|
+
_writeLine(ctx.stderr, "blamejs audit export: at least one of --from / --to / --action is required");
|
|
595
|
+
return 2;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
var r2 = await auditTools.exportSlice({
|
|
599
|
+
from: from, to: to, action: action,
|
|
600
|
+
out: outE, passphrase: passphrase,
|
|
601
|
+
});
|
|
602
|
+
_writeLine(ctx.stdout, "wrote export bundle to " + r2.outDir +
|
|
603
|
+
" (rowCount=" + r2.rowCount +
|
|
604
|
+
", counters=" + r2.range.firstCounter + ".." + r2.range.lastCounter + ")");
|
|
605
|
+
return 0;
|
|
606
|
+
} catch (e) {
|
|
607
|
+
_writeLine(ctx.stderr, "blamejs audit export: " + ((e && e.message) || String(e)));
|
|
608
|
+
return 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (sub === "verify-bundle") {
|
|
613
|
+
var inV = _resolveOutPath(args.flags.in, ctx);
|
|
614
|
+
if (!inV) { _writeLine(ctx.stderr, "blamejs audit verify-bundle: --in is required"); return 2; }
|
|
615
|
+
try {
|
|
616
|
+
var v = await auditTools.verifyBundle({ in: inV, passphrase: passphrase });
|
|
617
|
+
if (v.ok) {
|
|
618
|
+
_writeLine(ctx.stdout, "OK — bundle verified" +
|
|
619
|
+
" (kind=" + v.kind +
|
|
620
|
+
", rowsVerified=" + v.rowsVerified +
|
|
621
|
+
", counters=" + v.range.firstCounter + ".." + v.range.lastCounter + ")");
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
_writeLine(ctx.stderr, "FAIL — " + v.reason);
|
|
625
|
+
return 1;
|
|
626
|
+
} catch (e) {
|
|
627
|
+
_writeLine(ctx.stderr, "blamejs audit verify-bundle: " + ((e && e.message) || String(e)));
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (sub === "purge") {
|
|
633
|
+
var inP = _resolveOutPath(args.flags.archive || args.flags.in, ctx);
|
|
634
|
+
if (!inP) {
|
|
635
|
+
_writeLine(ctx.stderr, "blamejs audit purge: --archive (path to verified archive bundle) is required");
|
|
636
|
+
return 2;
|
|
637
|
+
}
|
|
638
|
+
if (args.flags.confirm !== true && args.flags.confirm !== "true") {
|
|
639
|
+
_writeLine(ctx.stderr, "blamejs audit purge: --confirm is REQUIRED — destructive operation");
|
|
640
|
+
return 2;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
var p = await auditTools.purge({
|
|
644
|
+
archive: inP, passphrase: passphrase, confirm: true,
|
|
645
|
+
});
|
|
646
|
+
_writeLine(ctx.stdout, "OK — purged " + p.rowsDeleted + " rows" +
|
|
647
|
+
" (counters ≤ " + p.lastPurgedCounter + ")");
|
|
648
|
+
return 0;
|
|
649
|
+
} catch (e) {
|
|
650
|
+
_writeLine(ctx.stderr, "blamejs audit purge: " + ((e && e.message) || String(e)));
|
|
651
|
+
return 1;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
_writeLine(ctx.stderr, "blamejs audit: unknown subcommand '" + sub + "'");
|
|
656
|
+
_writeLine(ctx.stderr, AUDIT_USAGE);
|
|
657
|
+
return 2;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ---- Top-level help ----
|
|
661
|
+
|
|
662
|
+
// ---- Subcommand: api-key ----
|
|
663
|
+
|
|
664
|
+
var API_KEY_USAGE = [
|
|
665
|
+
"Usage: blamejs api-key <subcommand> [flags]",
|
|
666
|
+
"",
|
|
667
|
+
"Subcommands:",
|
|
668
|
+
" issue Issue a new API key under a namespace. Prints the",
|
|
669
|
+
" composite key + id. The plaintext secret is shown",
|
|
670
|
+
" ONCE and cannot be recovered after the command exits.",
|
|
671
|
+
" revoke Revoke an issued key by its composite id (namespace:idHex).",
|
|
672
|
+
" list List active keys for a given owner under a namespace.",
|
|
673
|
+
" rotate Issue a new secret for an existing id while leaving the",
|
|
674
|
+
" old secret valid for opts.gracePeriodMs (default 0 — immediate).",
|
|
675
|
+
" verify Verify a token string and print the resolved metadata.",
|
|
676
|
+
" Useful for debugging an integration that's seeing 401s.",
|
|
677
|
+
"",
|
|
678
|
+
"Flags (all subcommands):",
|
|
679
|
+
" --data-dir <path> Path to the app's data dir (required)",
|
|
680
|
+
" --namespace <name> API-key namespace (required) — typically matches the",
|
|
681
|
+
" operator's b.apiKey.create({ namespace }) at boot",
|
|
682
|
+
" --vault-mode <mode> plaintext | wrapped (default wrapped). When wrapped,",
|
|
683
|
+
" BLAMEJS_VAULT_PASSPHRASE must be set.",
|
|
684
|
+
"",
|
|
685
|
+
"Subcommand flags:",
|
|
686
|
+
" issue: --owner-id <id> --scopes <comma-separated> [--label <text>] [--expires-ms <ms>]",
|
|
687
|
+
" revoke: --id <idHex>",
|
|
688
|
+
" list: --owner-id <id>",
|
|
689
|
+
" rotate: --id <idHex> [--grace-ms <ms>]",
|
|
690
|
+
" verify: --token <key>",
|
|
691
|
+
].join("\n");
|
|
692
|
+
|
|
693
|
+
async function _runApiKey(args, ctx) {
|
|
694
|
+
if (args.pos.length === 0) {
|
|
695
|
+
return cliHelpers.makeReporter(ctx, "blamejs api-key").usage(API_KEY_USAGE);
|
|
696
|
+
}
|
|
697
|
+
var sub = args.pos[0];
|
|
698
|
+
var report = cliHelpers.makeReporter(ctx, "blamejs api-key " + sub);
|
|
699
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
700
|
+
return report.helpStdout(API_KEY_USAGE);
|
|
701
|
+
}
|
|
702
|
+
if (["issue", "revoke", "list", "rotate", "verify"].indexOf(sub) === -1) {
|
|
703
|
+
cliHelpers.makeReporter(ctx, "blamejs api-key").error("unknown subcommand '" + sub + "'", 2);
|
|
704
|
+
return cliHelpers.makeReporter(ctx, "blamejs api-key").usage(API_KEY_USAGE);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
var dataDirFlag = args.flags["data-dir"];
|
|
708
|
+
if (!dataDirFlag || dataDirFlag === true) {
|
|
709
|
+
return report.error("--data-dir <path> is required", 2);
|
|
710
|
+
}
|
|
711
|
+
var dataDir = _resolvePath(String(dataDirFlag), ctx.cwd);
|
|
712
|
+
|
|
713
|
+
var namespace = args.flags.namespace;
|
|
714
|
+
if (!namespace || namespace === true) {
|
|
715
|
+
return report.error("--namespace <name> is required", 2);
|
|
716
|
+
}
|
|
717
|
+
namespace = String(namespace);
|
|
718
|
+
|
|
719
|
+
var vaultMode = args.flags["vault-mode"] || "wrapped";
|
|
720
|
+
if (vaultMode !== "wrapped" && vaultMode !== "plaintext") {
|
|
721
|
+
return report.error("--vault-mode must be 'wrapped' or 'plaintext'", 2);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
var booted;
|
|
725
|
+
try {
|
|
726
|
+
booted = await cliHelpers.bootApp({
|
|
727
|
+
dataDir: dataDir,
|
|
728
|
+
vaultMode: vaultMode,
|
|
729
|
+
env: ctx.env,
|
|
730
|
+
});
|
|
731
|
+
} catch (e) {
|
|
732
|
+
return report.error("boot failed: " + ((e && e.message) || String(e)));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
var registry = booted.b.apiKey.create({
|
|
737
|
+
namespace: namespace,
|
|
738
|
+
audit: booted.b.audit,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
if (sub === "issue") {
|
|
742
|
+
var ownerId = args.flags["owner-id"];
|
|
743
|
+
var scopes = args.flags.scopes;
|
|
744
|
+
if (!ownerId || ownerId === true) return report.error("--owner-id <id> is required", 2);
|
|
745
|
+
if (!scopes || scopes === true) return report.error("--scopes <comma-separated> is required", 2);
|
|
746
|
+
var scopeList = String(scopes).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
747
|
+
if (scopeList.length === 0) {
|
|
748
|
+
return report.error("--scopes must contain at least one non-empty scope", 2);
|
|
749
|
+
}
|
|
750
|
+
var label = typeof args.flags.label === "string" ? args.flags.label : null;
|
|
751
|
+
var expiresMs = args.flags["expires-ms"];
|
|
752
|
+
var issued = await registry.issue({
|
|
753
|
+
ownerId: String(ownerId),
|
|
754
|
+
scopes: scopeList,
|
|
755
|
+
metadata: label ? { label: label } : null,
|
|
756
|
+
expiresMs: expiresMs && expiresMs !== true ? Number(expiresMs) : undefined,
|
|
757
|
+
});
|
|
758
|
+
report.write("id: " + issued.id);
|
|
759
|
+
report.write("key: " + issued.key);
|
|
760
|
+
report.write("scopes: " + issued.scopes.join(", "));
|
|
761
|
+
if (issued.expiresAt) report.write("expires: " + new Date(issued.expiresAt).toISOString());
|
|
762
|
+
return report.ok("\nThe plaintext secret is shown ONCE — copy it now.");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (sub === "revoke") {
|
|
766
|
+
var revokeId = args.flags.id;
|
|
767
|
+
if (!revokeId || revokeId === true) return report.error("--id <idHex> is required", 2);
|
|
768
|
+
var revoked = await registry.revoke(String(revokeId));
|
|
769
|
+
return revoked
|
|
770
|
+
? report.ok("revoked: " + revokeId)
|
|
771
|
+
: report.error("no-op: " + revokeId + " not found or already revoked");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (sub === "list") {
|
|
775
|
+
var listOwnerId = args.flags["owner-id"];
|
|
776
|
+
if (!listOwnerId || listOwnerId === true) return report.error("--owner-id <id> is required", 2);
|
|
777
|
+
var rows = await registry.listForOwner(String(listOwnerId));
|
|
778
|
+
report.write("owner: " + listOwnerId + " (" + rows.length + " active keys)");
|
|
779
|
+
for (var i = 0; i < rows.length; i++) {
|
|
780
|
+
var r = rows[i];
|
|
781
|
+
var scope = Array.isArray(r.scopes) ? r.scopes.join(",") : "";
|
|
782
|
+
report.write(" " + r.id + " scopes=[" + scope + "]" +
|
|
783
|
+
(r.expiresAt ? " expires=" + new Date(r.expiresAt).toISOString() : ""));
|
|
784
|
+
}
|
|
785
|
+
return report.ok();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (sub === "rotate") {
|
|
789
|
+
var rotateId = args.flags.id;
|
|
790
|
+
if (!rotateId || rotateId === true) return report.error("--id <idHex> is required", 2);
|
|
791
|
+
var graceMs = args.flags["grace-ms"];
|
|
792
|
+
var rotated = await registry.rotate(String(rotateId), {
|
|
793
|
+
gracePeriodMs: graceMs && graceMs !== true ? Number(graceMs) : 0,
|
|
794
|
+
});
|
|
795
|
+
report.write("id: " + rotated.id);
|
|
796
|
+
report.write("key (new): " + rotated.key);
|
|
797
|
+
return report.ok("\nUpdate your integration to the new key, then revoke the old secret " +
|
|
798
|
+
"(or wait gracePeriodMs for it to expire).");
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (sub === "verify") {
|
|
802
|
+
var token = args.flags.token;
|
|
803
|
+
if (!token || token === true) return report.error("--token <key> is required", 2);
|
|
804
|
+
var v = await registry.verify(String(token));
|
|
805
|
+
if (!v) return report.error("rejected: token does not verify (bad format, unknown id, revoked, or expired)");
|
|
806
|
+
report.write("id: " + v.id);
|
|
807
|
+
report.write("ownerId: " + v.ownerId);
|
|
808
|
+
report.write("scopes: " + (v.scopes || []).join(", "));
|
|
809
|
+
if (v.lastUsedAt) report.write("last-used: " + new Date(v.lastUsedAt).toISOString());
|
|
810
|
+
if (v.expiresAt) report.write("expires: " + new Date(v.expiresAt).toISOString());
|
|
811
|
+
return report.ok();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return 2;
|
|
815
|
+
} catch (e) {
|
|
816
|
+
return report.error((e && e.message) || String(e));
|
|
817
|
+
} finally {
|
|
818
|
+
try { await booted.app.shutdown(); } catch (_e) { /* best-effort */ }
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ---- Subcommand: backup ----
|
|
823
|
+
|
|
824
|
+
var BACKUP_USAGE = [
|
|
825
|
+
"Usage: blamejs backup <subcommand> [flags]",
|
|
826
|
+
"",
|
|
827
|
+
"Subcommands:",
|
|
828
|
+
" inspect Read a bundle's manifest without decrypting and",
|
|
829
|
+
" print a summary (file count, total bytes, kinds,",
|
|
830
|
+
" timestamp). No passphrase required — useful for",
|
|
831
|
+
" pre-flight before a restore.",
|
|
832
|
+
" verify Decrypt + verify the bundle in a temp directory,",
|
|
833
|
+
" discard the output. Confirms passphrase is correct",
|
|
834
|
+
" and every encrypted blob's HMAC validates against",
|
|
835
|
+
" the manifest, without committing a restore.",
|
|
836
|
+
" extract Decrypt + verify into the target staging directory.",
|
|
837
|
+
" The staging directory is the operator's responsibility",
|
|
838
|
+
" to inspect and then move into place; this command",
|
|
839
|
+
" never touches the live data dir.",
|
|
840
|
+
"",
|
|
841
|
+
"Flags:",
|
|
842
|
+
" --bundle <dir> Path to a bundle directory (must contain manifest.json)",
|
|
843
|
+
" --to <stagingDir> For extract — fresh directory to decrypt into (must not exist)",
|
|
844
|
+
" --passphrase <string> Backup passphrase (or env BLAMEJS_BACKUP_PASSPHRASE)",
|
|
845
|
+
].join("\n");
|
|
846
|
+
|
|
847
|
+
async function _runBackup(args, ctx) {
|
|
848
|
+
if (args.pos.length === 0) {
|
|
849
|
+
return cliHelpers.makeReporter(ctx, "blamejs backup").usage(BACKUP_USAGE);
|
|
850
|
+
}
|
|
851
|
+
var sub = args.pos[0];
|
|
852
|
+
var report = cliHelpers.makeReporter(ctx, "blamejs backup " + sub);
|
|
853
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
854
|
+
return report.helpStdout(BACKUP_USAGE);
|
|
855
|
+
}
|
|
856
|
+
if (["inspect", "verify", "extract"].indexOf(sub) === -1) {
|
|
857
|
+
cliHelpers.makeReporter(ctx, "blamejs backup").error("unknown subcommand '" + sub + "'", 2);
|
|
858
|
+
return cliHelpers.makeReporter(ctx, "blamejs backup").usage(BACKUP_USAGE);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
var bundleFlag = args.flags.bundle;
|
|
862
|
+
if (!bundleFlag || bundleFlag === true) {
|
|
863
|
+
return report.error("--bundle <dir> is required", 2);
|
|
864
|
+
}
|
|
865
|
+
var bundleDir = _resolvePath(String(bundleFlag), ctx.cwd);
|
|
866
|
+
|
|
867
|
+
if (sub === "inspect") {
|
|
868
|
+
try {
|
|
869
|
+
var m = restoreBundle.inspect({ bundleDir: bundleDir });
|
|
870
|
+
var totalBytes = 0;
|
|
871
|
+
for (var i = 0; i < m.files.length; i++) totalBytes += m.files[i].encryptedSize || 0;
|
|
872
|
+
report.write("bundle: " + bundleDir);
|
|
873
|
+
report.write("manifest: v" + (m.manifestVersion || m.version || "unknown"));
|
|
874
|
+
report.write("created: " + (m.createdAt || "unknown"));
|
|
875
|
+
report.write("files: " + m.files.length);
|
|
876
|
+
report.write("encrypted size: " + totalBytes + " bytes");
|
|
877
|
+
var kinds = {};
|
|
878
|
+
for (var k = 0; k < m.files.length; k++) {
|
|
879
|
+
var kind = m.files[k].kind || "unknown";
|
|
880
|
+
kinds[kind] = (kinds[kind] || 0) + 1;
|
|
881
|
+
}
|
|
882
|
+
var ks = Object.keys(kinds).sort();
|
|
883
|
+
for (var ki = 0; ki < ks.length; ki++) {
|
|
884
|
+
report.write(" " + ks[ki] + ": " + kinds[ks[ki]]);
|
|
885
|
+
}
|
|
886
|
+
return report.ok();
|
|
887
|
+
} catch (e) {
|
|
888
|
+
return report.error((e && e.message) || String(e));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// verify + extract both decrypt — both need a passphrase.
|
|
893
|
+
var pp = cliHelpers.resolvePassphrase(args, ctx, {
|
|
894
|
+
flag: "passphrase", envVar: "BLAMEJS_BACKUP_PASSPHRASE",
|
|
895
|
+
});
|
|
896
|
+
if (!pp) {
|
|
897
|
+
return report.error("--passphrase or BLAMEJS_BACKUP_PASSPHRASE is required", 2);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (sub === "verify") {
|
|
901
|
+
var fs2 = require("node:fs");
|
|
902
|
+
var os2 = require("node:os");
|
|
903
|
+
var nodePath = require("node:path");
|
|
904
|
+
var nodeCrypto = require("node:crypto");
|
|
905
|
+
var stagingDir = nodePath.join(os2.tmpdir(),
|
|
906
|
+
"blamejs-backup-verify-" + nodeCrypto.randomBytes(8).toString("hex"));
|
|
907
|
+
try {
|
|
908
|
+
var r = await restoreBundle.extract({
|
|
909
|
+
bundleDir: bundleDir,
|
|
910
|
+
stagingDir: stagingDir,
|
|
911
|
+
passphrase: pp,
|
|
912
|
+
});
|
|
913
|
+
report.write("verified: " + (r && r.fileCount != null ? r.fileCount : "n/a") + " files");
|
|
914
|
+
report.write("passphrase decrypts the vault-key wrap");
|
|
915
|
+
report.write("every blob's HMAC validates against the manifest");
|
|
916
|
+
return report.ok();
|
|
917
|
+
} catch (e) {
|
|
918
|
+
return report.error((e && e.message) || String(e));
|
|
919
|
+
} finally {
|
|
920
|
+
try { fs2.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (sub === "extract") {
|
|
925
|
+
var toFlag = args.flags.to;
|
|
926
|
+
if (!toFlag || toFlag === true) {
|
|
927
|
+
return report.error("--to <stagingDir> is required", 2);
|
|
928
|
+
}
|
|
929
|
+
var stagingDir2 = _resolvePath(String(toFlag), ctx.cwd);
|
|
930
|
+
try {
|
|
931
|
+
var rr = await restoreBundle.extract({
|
|
932
|
+
bundleDir: bundleDir,
|
|
933
|
+
stagingDir: stagingDir2,
|
|
934
|
+
passphrase: pp,
|
|
935
|
+
});
|
|
936
|
+
report.write("extracted: " + (rr && rr.fileCount != null ? rr.fileCount : "n/a") +
|
|
937
|
+
" files → " + stagingDir2);
|
|
938
|
+
report.write("");
|
|
939
|
+
report.write("Inspect the staging directory before moving any files into your live data dir.");
|
|
940
|
+
report.write("blamejs does NOT auto-promote — that's an operator decision.");
|
|
941
|
+
return report.ok();
|
|
942
|
+
} catch (e) {
|
|
943
|
+
return report.error((e && e.message) || String(e));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return 2;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ---- Subcommand: mtls ----
|
|
951
|
+
|
|
952
|
+
var MTLS_USAGE = [
|
|
953
|
+
"Usage: blamejs mtls <subcommand> [flags]",
|
|
954
|
+
"",
|
|
955
|
+
"Subcommands:",
|
|
956
|
+
" status Print CA state — exists / generation / sealed-mode.",
|
|
957
|
+
" No engine required.",
|
|
958
|
+
" show-cert Print the CA certificate PEM to stdout. Operators",
|
|
959
|
+
" paste this into client truststores. No engine required.",
|
|
960
|
+
" init Generate a fresh CA keypair + self-signed cert and",
|
|
961
|
+
" write to data-dir. Requires opts.engine — see note below.",
|
|
962
|
+
" issue Issue a leaf client certificate signed by the CA.",
|
|
963
|
+
" Requires opts.engine + --subject. Prints cert + key PEM.",
|
|
964
|
+
" issue-p12 Issue + package as PKCS#12 with --password. Useful for",
|
|
965
|
+
" importing into browsers / OS keychains. Requires engine.",
|
|
966
|
+
"",
|
|
967
|
+
"Flags:",
|
|
968
|
+
" --data-dir <path> Path to the app's data dir (required)",
|
|
969
|
+
" --vault-mode <mode> plaintext | wrapped (default wrapped). When wrapped,",
|
|
970
|
+
" BLAMEJS_VAULT_PASSPHRASE must be set.",
|
|
971
|
+
" --sealed-mode <mode> auto | required | disabled (default auto). 'required'",
|
|
972
|
+
" seals the CA key under the vault before writing it",
|
|
973
|
+
" to disk; 'disabled' keeps it plaintext on disk;",
|
|
974
|
+
" 'auto' loads whichever form exists.",
|
|
975
|
+
"",
|
|
976
|
+
"Subcommand flags:",
|
|
977
|
+
" issue: --subject <CN> [--days <N>]",
|
|
978
|
+
" issue-p12: --subject <CN> --password <pkcs12-passphrase> [--days <N>] [--out <path>]",
|
|
979
|
+
"",
|
|
980
|
+
"Cert issuance ('init', 'issue', 'issue-p12') uses the framework's",
|
|
981
|
+
"bundled pure-JS engine (lib/mtls-engine-default.js, ECDSA P-384",
|
|
982
|
+
"signatures, AES-256-CBC + HMAC-SHA-512 PBKDF2 PKCS#12 with 2,000,000",
|
|
983
|
+
"iterations). Operators with custom requirements pass a different",
|
|
984
|
+
"engine via b.mtlsCa.create({ engine: ... }) when wiring their app;",
|
|
985
|
+
"the CLI always uses the default.",
|
|
986
|
+
].join("\n");
|
|
987
|
+
|
|
988
|
+
async function _runMtls(args, ctx) {
|
|
989
|
+
if (args.pos.length === 0) {
|
|
990
|
+
return cliHelpers.makeReporter(ctx, "blamejs mtls").usage(MTLS_USAGE);
|
|
991
|
+
}
|
|
992
|
+
var sub = args.pos[0];
|
|
993
|
+
var report = cliHelpers.makeReporter(ctx, "blamejs mtls " + sub);
|
|
994
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
995
|
+
return report.helpStdout(MTLS_USAGE);
|
|
996
|
+
}
|
|
997
|
+
if (["status", "show-cert", "init", "issue", "issue-p12"].indexOf(sub) === -1) {
|
|
998
|
+
cliHelpers.makeReporter(ctx, "blamejs mtls").error("unknown subcommand '" + sub + "'", 2);
|
|
999
|
+
return cliHelpers.makeReporter(ctx, "blamejs mtls").usage(MTLS_USAGE);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
var dataDirFlag = args.flags["data-dir"];
|
|
1003
|
+
if (!dataDirFlag || dataDirFlag === true) {
|
|
1004
|
+
return report.error("--data-dir <path> is required", 2);
|
|
1005
|
+
}
|
|
1006
|
+
var dataDir = _resolvePath(String(dataDirFlag), ctx.cwd);
|
|
1007
|
+
|
|
1008
|
+
var vaultMode = args.flags["vault-mode"] || "wrapped";
|
|
1009
|
+
if (vaultMode !== "wrapped" && vaultMode !== "plaintext") {
|
|
1010
|
+
return report.error("--vault-mode must be 'wrapped' or 'plaintext'", 2);
|
|
1011
|
+
}
|
|
1012
|
+
var sealedMode = args.flags["sealed-mode"] || "auto";
|
|
1013
|
+
if (["auto", "required", "disabled"].indexOf(sealedMode) === -1) {
|
|
1014
|
+
return report.error("--sealed-mode must be 'auto', 'required', or 'disabled'", 2);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
var booted;
|
|
1018
|
+
try {
|
|
1019
|
+
booted = await cliHelpers.bootApp({
|
|
1020
|
+
dataDir: dataDir,
|
|
1021
|
+
vaultMode: vaultMode,
|
|
1022
|
+
env: ctx.env,
|
|
1023
|
+
});
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
return report.error("boot failed: " + ((e && e.message) || String(e)));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
var ca = booted.b.mtlsCa.create({
|
|
1030
|
+
dataDir: dataDir,
|
|
1031
|
+
vault: booted.b.vault,
|
|
1032
|
+
caKeySealedMode: sealedMode,
|
|
1033
|
+
// No engine passed — b.mtlsCa falls back to the bundled default
|
|
1034
|
+
// (lib/mtls-engine-default.js).
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
if (sub === "status") {
|
|
1038
|
+
var s = ca.status();
|
|
1039
|
+
report.write("data-dir: " + dataDir);
|
|
1040
|
+
report.write("CA exists: " + (s.exists ? "yes" : "no"));
|
|
1041
|
+
if (s.exists) {
|
|
1042
|
+
report.write("generation: " + s.generation +
|
|
1043
|
+
(s.isLegacy ? " (LEGACY — current is " + s.current + ", rotate via init)" : ""));
|
|
1044
|
+
report.write("ca-key-sealed-mode: " + ca.caKeySealedMode);
|
|
1045
|
+
report.write("paths:");
|
|
1046
|
+
report.write(" cert: " + ca.paths.caCert);
|
|
1047
|
+
report.write(" key: " + ca.paths.caKey);
|
|
1048
|
+
report.write(" key-sealed: " + ca.paths.caKeySealed);
|
|
1049
|
+
} else {
|
|
1050
|
+
report.write("(run 'blamejs mtls init' to generate a CA)");
|
|
1051
|
+
}
|
|
1052
|
+
return report.ok();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (sub === "show-cert") {
|
|
1056
|
+
if (!ca.exists()) {
|
|
1057
|
+
return report.error("no CA on disk at " + ca.paths.caCert + " — run 'blamejs mtls init' first");
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
var pem = ca.loadCert().toString("utf8");
|
|
1061
|
+
report.write(pem.trim());
|
|
1062
|
+
return report.ok();
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
return report.error("could not load CA cert: " + ((e && e.message) || String(e)));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (sub === "init") {
|
|
1069
|
+
try {
|
|
1070
|
+
await ca.initCA();
|
|
1071
|
+
report.write("ca-cert: " + ca.paths.caCert);
|
|
1072
|
+
report.write("ca-key: " + (ca.caKeySealedMode === "required" ? ca.paths.caKeySealed : ca.paths.caKey));
|
|
1073
|
+
return report.ok("CA generated. Distribute ca-cert to clients via 'blamejs mtls show-cert'.");
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
return report.error((e && e.message) || String(e));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (sub === "issue") {
|
|
1080
|
+
var subject = args.flags.subject;
|
|
1081
|
+
if (!subject || subject === true) return report.error("--subject <CN> is required", 2);
|
|
1082
|
+
var days = args.flags.days && args.flags.days !== true ? Number(args.flags.days) : undefined;
|
|
1083
|
+
try {
|
|
1084
|
+
var leaf = await ca.generateClientCert({ cn: String(subject), validityDays: days });
|
|
1085
|
+
report.write("# certificate");
|
|
1086
|
+
report.write(leaf.cert.trim());
|
|
1087
|
+
report.write("");
|
|
1088
|
+
report.write("# private key");
|
|
1089
|
+
report.write(leaf.key.trim());
|
|
1090
|
+
// Framework-canonical fingerprint via b.crypto.sha3Hash. Computed
|
|
1091
|
+
// here over the leaf cert PEM bytes so the audit trail is
|
|
1092
|
+
// independent of whatever fingerprint format the operator-
|
|
1093
|
+
// supplied engine returns. Operators wanting the X.509-
|
|
1094
|
+
// conventional SHA-256 fingerprint (browsers, openssl) can run
|
|
1095
|
+
// `openssl x509 -fingerprint -sha256 -in cert.pem` separately.
|
|
1096
|
+
report.write("");
|
|
1097
|
+
report.write("# fingerprint (sha3-512): " + booted.b.crypto.sha3Hash(Buffer.from(leaf.cert, "utf8")));
|
|
1098
|
+
if (leaf.expiresAt) report.write("# expires: " + new Date(leaf.expiresAt).toISOString());
|
|
1099
|
+
return report.ok();
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
return report.error((e && e.message) || String(e));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (sub === "issue-p12") {
|
|
1106
|
+
var subjectP = args.flags.subject;
|
|
1107
|
+
var password = args.flags.password;
|
|
1108
|
+
if (!subjectP || subjectP === true) return report.error("--subject <CN> is required", 2);
|
|
1109
|
+
if (!password || password === true) return report.error("--password <pkcs12-passphrase> is required", 2);
|
|
1110
|
+
var daysP = args.flags.days && args.flags.days !== true ? Number(args.flags.days) : undefined;
|
|
1111
|
+
var outPath = args.flags.out && args.flags.out !== true
|
|
1112
|
+
? _resolvePath(String(args.flags.out), ctx.cwd)
|
|
1113
|
+
: null;
|
|
1114
|
+
try {
|
|
1115
|
+
var p12 = await ca.generateClientP12({
|
|
1116
|
+
cn: String(subjectP),
|
|
1117
|
+
password: String(password),
|
|
1118
|
+
validityDays: daysP,
|
|
1119
|
+
});
|
|
1120
|
+
if (outPath) {
|
|
1121
|
+
require("node:fs").writeFileSync(outPath, p12.p12, { mode: 0o600 });
|
|
1122
|
+
report.write("p12 written: " + outPath);
|
|
1123
|
+
} else {
|
|
1124
|
+
// No --out: stream the bytes to stdout for piping. Operators
|
|
1125
|
+
// can `blamejs mtls issue-p12 ... > client.p12`.
|
|
1126
|
+
if (ctx.stdout && typeof ctx.stdout.write === "function") {
|
|
1127
|
+
ctx.stdout.write(p12.p12);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Framework-canonical fingerprint via b.crypto.sha3Hash over
|
|
1131
|
+
// the embedded cert PEM, same posture as the issue path above.
|
|
1132
|
+
// Independent of the engine's fingerprint format.
|
|
1133
|
+
if (p12.certPem) {
|
|
1134
|
+
report.write("# fingerprint (sha3-512): " + booted.b.crypto.sha3Hash(Buffer.from(p12.certPem, "utf8")));
|
|
1135
|
+
}
|
|
1136
|
+
if (p12.expiresAt) report.write("# expires: " + new Date(p12.expiresAt).toISOString());
|
|
1137
|
+
return report.ok();
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
return report.error((e && e.message) || String(e));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return 2;
|
|
1144
|
+
} finally {
|
|
1145
|
+
try { await booted.app.shutdown(); } catch (_e) { /* best-effort */ }
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// ---- Subcommand: vault ----
|
|
1150
|
+
|
|
1151
|
+
var VAULT_USAGE = [
|
|
1152
|
+
"Usage: blamejs vault <subcommand> [flags]",
|
|
1153
|
+
"",
|
|
1154
|
+
"Subcommands:",
|
|
1155
|
+
" status Report whether vault.key (plaintext) and/or",
|
|
1156
|
+
" vault.key.sealed (wrapped) exist under <data-dir>",
|
|
1157
|
+
" seal Wrap a plaintext vault.key into a passphrase-",
|
|
1158
|
+
" sealed vault.key.sealed (Argon2id KDF +",
|
|
1159
|
+
" XChaCha20-Poly1305). Crash-safe: writes to .tmp",
|
|
1160
|
+
" + fsync + atomic rename, leaves the original",
|
|
1161
|
+
" untouched on any failure.",
|
|
1162
|
+
" unseal Reverse — write a plaintext vault.key from a",
|
|
1163
|
+
" sealed file. For audits / migration to a new",
|
|
1164
|
+
" machine; remove the plaintext file as soon as",
|
|
1165
|
+
" you're done.",
|
|
1166
|
+
" rotate Re-wrap a sealed vault.key.sealed under a new",
|
|
1167
|
+
" passphrase. The old passphrase is required to",
|
|
1168
|
+
" unwrap; the new passphrase wraps. The keypair",
|
|
1169
|
+
" itself is unchanged (use `b.vault.rotateKey()`",
|
|
1170
|
+
" at runtime if you want to rotate the keypair).",
|
|
1171
|
+
"",
|
|
1172
|
+
"Flags:",
|
|
1173
|
+
" --data-dir <path> Path to the app's data dir (default ./data)",
|
|
1174
|
+
" --passphrase <string> Passphrase to wrap with (or env",
|
|
1175
|
+
" BLAMEJS_VAULT_PASSPHRASE). For `rotate`,",
|
|
1176
|
+
" this is the OLD passphrase; pair with",
|
|
1177
|
+
" --new-passphrase / BLAMEJS_VAULT_PASSPHRASE_NEW.",
|
|
1178
|
+
" --new-passphrase <s> Rotate-only — the NEW passphrase to re-wrap",
|
|
1179
|
+
" under. Or env BLAMEJS_VAULT_PASSPHRASE_NEW.",
|
|
1180
|
+
" --keep-plaintext For `seal` — retain the plaintext vault.key",
|
|
1181
|
+
" file (default: delete it after sealing).",
|
|
1182
|
+
].join("\n");
|
|
1183
|
+
|
|
1184
|
+
async function _runVault(args, ctx) {
|
|
1185
|
+
if (args.pos.length === 0) {
|
|
1186
|
+
return cliHelpers.makeReporter(ctx, "blamejs vault").usage(VAULT_USAGE);
|
|
1187
|
+
}
|
|
1188
|
+
var sub = args.pos[0];
|
|
1189
|
+
var report = cliHelpers.makeReporter(ctx, "blamejs vault " + sub);
|
|
1190
|
+
if (sub === "help" || args.flags.help || args.flags.h) {
|
|
1191
|
+
return report.helpStdout(VAULT_USAGE);
|
|
1192
|
+
}
|
|
1193
|
+
if (["status", "seal", "unseal", "rotate"].indexOf(sub) === -1) {
|
|
1194
|
+
cliHelpers.makeReporter(ctx, "blamejs vault").error("unknown subcommand '" + sub + "'", 2);
|
|
1195
|
+
return cliHelpers.makeReporter(ctx, "blamejs vault").usage(VAULT_USAGE);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
var dataDir = _resolvePath(String(args.flags["data-dir"] || "./data"), ctx.cwd);
|
|
1199
|
+
|
|
1200
|
+
if (sub === "status") {
|
|
1201
|
+
var pre = vaultPassphraseOps.preflightSealable({ dataDir: dataDir });
|
|
1202
|
+
var unsealable = vaultPassphraseOps.preflightUnsealable
|
|
1203
|
+
? vaultPassphraseOps.preflightUnsealable({ dataDir: dataDir })
|
|
1204
|
+
: null;
|
|
1205
|
+
report.write("data-dir: " + dataDir);
|
|
1206
|
+
report.write("vault.key (plaintext): " +
|
|
1207
|
+
(pre.ok ? "present (sealable)" : "absent — " + (pre.reason || "n/a")));
|
|
1208
|
+
if (unsealable) {
|
|
1209
|
+
report.write("vault.key.sealed (wrapped): " +
|
|
1210
|
+
(unsealable.ok ? "present" : "absent — " + (unsealable.reason || "n/a")));
|
|
1211
|
+
}
|
|
1212
|
+
return report.ok();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (sub === "seal") {
|
|
1216
|
+
var pp = cliHelpers.resolvePassphrase(args, ctx, {
|
|
1217
|
+
flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE",
|
|
1218
|
+
});
|
|
1219
|
+
if (!pp) {
|
|
1220
|
+
return report.error("--passphrase or BLAMEJS_VAULT_PASSPHRASE is required", 2);
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
var r = await vaultPassphraseOps.seal({
|
|
1224
|
+
dataDir: dataDir,
|
|
1225
|
+
passphrase: pp,
|
|
1226
|
+
keepPlaintext: !!args.flags["keep-plaintext"],
|
|
1227
|
+
});
|
|
1228
|
+
report.write("sealed: " + r.sealedPath);
|
|
1229
|
+
report.write(r.plaintextDeleted
|
|
1230
|
+
? "removed plaintext vault.key"
|
|
1231
|
+
: "kept plaintext vault.key (--keep-plaintext set)");
|
|
1232
|
+
report.write("");
|
|
1233
|
+
report.write("Set BLAMEJS_VAULT_PASSPHRASE in the runtime environment and");
|
|
1234
|
+
return report.ok("boot the app with vault: { mode: \"wrapped\" }.");
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
return report.error((e && e.message) || String(e));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (sub === "unseal") {
|
|
1241
|
+
var pp2 = cliHelpers.resolvePassphrase(args, ctx, {
|
|
1242
|
+
flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE",
|
|
1243
|
+
});
|
|
1244
|
+
if (!pp2) {
|
|
1245
|
+
return report.error("--passphrase or BLAMEJS_VAULT_PASSPHRASE is required", 2);
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
var u = await vaultPassphraseOps.unseal({ dataDir: dataDir, passphrase: pp2 });
|
|
1249
|
+
report.write("unsealed: " + u.plaintextPath);
|
|
1250
|
+
report.write("");
|
|
1251
|
+
report.write("WARNING: vault.key is now plaintext on disk. Re-seal as soon");
|
|
1252
|
+
return report.ok("as you're done auditing or migrating.");
|
|
1253
|
+
} catch (e) {
|
|
1254
|
+
return report.error((e && e.message) || String(e));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (sub === "rotate") {
|
|
1259
|
+
var oldPp = cliHelpers.resolvePassphrase(args, ctx, {
|
|
1260
|
+
flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE",
|
|
1261
|
+
});
|
|
1262
|
+
var newPp = cliHelpers.resolvePassphrase(args, ctx, {
|
|
1263
|
+
flag: "new-passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE_NEW",
|
|
1264
|
+
});
|
|
1265
|
+
if (!oldPp || !newPp) {
|
|
1266
|
+
report.error("both --passphrase (old) and --new-passphrase are required", 2);
|
|
1267
|
+
report.writeErr("(or BLAMEJS_VAULT_PASSPHRASE + BLAMEJS_VAULT_PASSPHRASE_NEW)");
|
|
1268
|
+
return 2;
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
var rr = await vaultPassphraseOps.rotate({
|
|
1272
|
+
dataDir: dataDir,
|
|
1273
|
+
oldPassphrase: oldPp,
|
|
1274
|
+
newPassphrase: newPp,
|
|
1275
|
+
});
|
|
1276
|
+
report.write("rotated: " + rr.sealedPath);
|
|
1277
|
+
return report.ok("Update BLAMEJS_VAULT_PASSPHRASE in the runtime environment to the new value.");
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
return report.error((e && e.message) || String(e));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return 2;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
var TOP_USAGE = [
|
|
1287
|
+
"Usage: blamejs <command> [args]",
|
|
1288
|
+
"",
|
|
1289
|
+
"Commands:",
|
|
1290
|
+
" migrate Manage database migrations (up / down / status)",
|
|
1291
|
+
" seed Apply seed-data fixtures by env (run / status)",
|
|
1292
|
+
" dev Run an app with file-watch + auto-restart",
|
|
1293
|
+
" api-snapshot Capture / compare the public API surface (CI gate)",
|
|
1294
|
+
" api-key Issue / revoke / list / rotate / verify API keys for a namespace",
|
|
1295
|
+
" audit Operator tooling on top of the audit chain (archive / export / verify / purge)",
|
|
1296
|
+
" backup Inspect / verify / extract a backup bundle from disk",
|
|
1297
|
+
" mtls Inspect or generate the in-box mTLS CA + leaf certs (status / show-cert / init / issue / issue-p12)",
|
|
1298
|
+
" vault Seal / unseal / rotate the on-disk vault keypair (plaintext ↔ wrapped)",
|
|
1299
|
+
" version Print framework version",
|
|
1300
|
+
" help [<command>] Show this message or details for a command",
|
|
1301
|
+
].join("\n");
|
|
1302
|
+
|
|
1303
|
+
function _printTopHelp(ctx) { _writeLine(ctx.stdout, TOP_USAGE); }
|
|
1304
|
+
|
|
1305
|
+
// ---- Dispatch ----
|
|
1306
|
+
|
|
1307
|
+
async function main(argv, opts) {
|
|
1308
|
+
opts = opts || {};
|
|
1309
|
+
var ctx = {
|
|
1310
|
+
stdout: opts.stdout || process.stdout,
|
|
1311
|
+
stderr: opts.stderr || process.stderr,
|
|
1312
|
+
env: opts.env || process.env,
|
|
1313
|
+
cwd: opts.cwd || process.cwd(),
|
|
1314
|
+
};
|
|
1315
|
+
if (!Array.isArray(argv)) argv = [];
|
|
1316
|
+
var args = _parseArgs(argv);
|
|
1317
|
+
|
|
1318
|
+
// Top-level flags handled before subcommand dispatch
|
|
1319
|
+
if (args.flags.version || args.flags.v) {
|
|
1320
|
+
_writeLine(ctx.stdout, constants.version);
|
|
1321
|
+
return 0;
|
|
1322
|
+
}
|
|
1323
|
+
if (args.flags.help || args.flags.h) {
|
|
1324
|
+
_printTopHelp(ctx);
|
|
1325
|
+
return 0;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
var cmd = args.pos[0];
|
|
1329
|
+
|
|
1330
|
+
if (cmd === undefined) { _printTopHelp(ctx); return 0; }
|
|
1331
|
+
if (cmd === "help") {
|
|
1332
|
+
var subTopic = args.pos[1];
|
|
1333
|
+
if (subTopic === "migrate") { _writeLine(ctx.stdout, MIGRATE_USAGE); return 0; }
|
|
1334
|
+
if (subTopic === "seed") { _writeLine(ctx.stdout, SEED_USAGE); return 0; }
|
|
1335
|
+
if (subTopic === "dev") { _writeLine(ctx.stdout, DEV_USAGE); return 0; }
|
|
1336
|
+
if (subTopic === "api-snapshot") { _writeLine(ctx.stdout, API_SNAPSHOT_USAGE); return 0; }
|
|
1337
|
+
if (subTopic === "api-key") { _writeLine(ctx.stdout, API_KEY_USAGE); return 0; }
|
|
1338
|
+
if (subTopic === "audit") { _writeLine(ctx.stdout, AUDIT_USAGE); return 0; }
|
|
1339
|
+
if (subTopic === "backup") { _writeLine(ctx.stdout, BACKUP_USAGE); return 0; }
|
|
1340
|
+
if (subTopic === "mtls") { _writeLine(ctx.stdout, MTLS_USAGE); return 0; }
|
|
1341
|
+
if (subTopic === "vault") { _writeLine(ctx.stdout, VAULT_USAGE); return 0; }
|
|
1342
|
+
_printTopHelp(ctx);
|
|
1343
|
+
return 0;
|
|
1344
|
+
}
|
|
1345
|
+
if (cmd === "version") { _writeLine(ctx.stdout, constants.version); return 0; }
|
|
1346
|
+
|
|
1347
|
+
var rest = { pos: args.pos.slice(1), flags: args.flags };
|
|
1348
|
+
if (cmd === "migrate") return await _runMigrate(rest, ctx);
|
|
1349
|
+
if (cmd === "seed") return await _runSeed(rest, ctx);
|
|
1350
|
+
if (cmd === "dev") return await _runDev(rest, ctx);
|
|
1351
|
+
if (cmd === "api-snapshot") return _runApiSnapshot(rest, ctx);
|
|
1352
|
+
if (cmd === "api-key") return await _runApiKey(rest, ctx);
|
|
1353
|
+
if (cmd === "audit") return await _runAudit(rest, ctx);
|
|
1354
|
+
if (cmd === "backup") return await _runBackup(rest, ctx);
|
|
1355
|
+
if (cmd === "mtls") return await _runMtls(rest, ctx);
|
|
1356
|
+
if (cmd === "vault") return await _runVault(rest, ctx);
|
|
1357
|
+
|
|
1358
|
+
_writeLine(ctx.stderr, "blamejs: unknown command '" + cmd + "'");
|
|
1359
|
+
_printTopHelp(ctx);
|
|
1360
|
+
return 2;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
module.exports = {
|
|
1364
|
+
main: main,
|
|
1365
|
+
// Internal helpers exposed so tests can drive the parser without
|
|
1366
|
+
// running the full dispatch.
|
|
1367
|
+
_parseArgs: _parseArgs,
|
|
1368
|
+
TOP_USAGE: TOP_USAGE,
|
|
1369
|
+
MIGRATE_USAGE: MIGRATE_USAGE,
|
|
1370
|
+
DEV_USAGE: DEV_USAGE,
|
|
1371
|
+
API_SNAPSHOT_USAGE: API_SNAPSHOT_USAGE,
|
|
1372
|
+
API_KEY_USAGE: API_KEY_USAGE,
|
|
1373
|
+
AUDIT_USAGE: AUDIT_USAGE,
|
|
1374
|
+
BACKUP_USAGE: BACKUP_USAGE,
|
|
1375
|
+
MTLS_USAGE: MTLS_USAGE,
|
|
1376
|
+
VAULT_USAGE: VAULT_USAGE,
|
|
1377
|
+
};
|