@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Chain-writer primitive — race-safe append to a hash-chained log table.
|
|
4
|
+
*
|
|
5
|
+
* The framework's audit_log AND consent_log have the same shape:
|
|
6
|
+
*
|
|
7
|
+
* 1. Take the next monotonic counter
|
|
8
|
+
* 2. Compute prevHash from the previous row's rowHash
|
|
9
|
+
* 3. Seal the logical row via field-crypto (sealedFields → vault.seal,
|
|
10
|
+
* derivedHashes computed)
|
|
11
|
+
* 4. Materialize null entries for every hashable column (so canonicalize
|
|
12
|
+
* at write-time and verify-time agree on the key set)
|
|
13
|
+
* 5. Compute rowHash over the sealed content (excluding chain bookkeeping)
|
|
14
|
+
* 6. INSERT with prevHash / rowHash / nonce / fencingToken
|
|
15
|
+
*
|
|
16
|
+
* audit.js and consent.js previously each carried their own copy of
|
|
17
|
+
* the chain-write pattern. The duplication produced bugs at the same
|
|
18
|
+
* architectural points: a chain-fork race had to be fixed in audit
|
|
19
|
+
* via Mutex, and consent had the same race because the fix hadn't
|
|
20
|
+
* propagated. Per the framework's "if a task repeats more than once
|
|
21
|
+
* it should be a primitive" rule, the pattern lives here and every
|
|
22
|
+
* chain-writer consumer gets the same safety guarantees automatically.
|
|
23
|
+
*
|
|
24
|
+
* Each chain-writer instance owns:
|
|
25
|
+
* - The table name (validated via sql-safe.assertOneOf at construction)
|
|
26
|
+
* - The full column list (for INSERT)
|
|
27
|
+
* - The hashable column list (for canonicalization)
|
|
28
|
+
* - A Mutex serializing the chain (read-prev → compute-hash → insert)
|
|
29
|
+
* - A Once initializing the in-process counter from MAX(monotonicCounter)
|
|
30
|
+
*
|
|
31
|
+
* Writes go through the cluster-storage dispatcher so the same chain
|
|
32
|
+
* definition works in single-node SQLite and cluster-mode external-db.
|
|
33
|
+
*
|
|
34
|
+
* Public API:
|
|
35
|
+
*
|
|
36
|
+
* chainWriter.create({
|
|
37
|
+
* table: "audit_log" | "consent_log" | …,
|
|
38
|
+
* columnsForInsert: [string], order matters; INSERT uses this
|
|
39
|
+
* hashableColumns: [string], for canonicalize null-fill
|
|
40
|
+
* validateAction: function (event) optional; throws on invalid input
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* writer.append(logical) async; returns { rowHash, prevHash, …logical }
|
|
44
|
+
* writer._resetForTest() re-initializes counter + mutex
|
|
45
|
+
*
|
|
46
|
+
* Operators usually don't construct chain-writers directly; audit and
|
|
47
|
+
* consent each construct one at module load.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var { generateToken, generateBytes } = require("./crypto");
|
|
51
|
+
var auditChain = require("./audit-chain");
|
|
52
|
+
var cryptoField = require("./crypto-field");
|
|
53
|
+
var cluster = require("./cluster");
|
|
54
|
+
var clusterStorage = require("./cluster-storage");
|
|
55
|
+
var safeAsync = require("./safe-async");
|
|
56
|
+
var safeSql = require("./safe-sql");
|
|
57
|
+
var C = require("./constants");
|
|
58
|
+
var { FrameworkError } = require("./framework-error");
|
|
59
|
+
|
|
60
|
+
// Allowlist of chain table names. Adding a new chain-backed table
|
|
61
|
+
// (e.g. some future _blamejs_security_log) requires registering it here
|
|
62
|
+
// so an operator can't accidentally point a chain-writer at a non-chain
|
|
63
|
+
// table and corrupt the chain semantics.
|
|
64
|
+
var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
|
|
65
|
+
|
|
66
|
+
var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
|
|
67
|
+
|
|
68
|
+
class ChainWriterError extends FrameworkError {
|
|
69
|
+
constructor(message, code) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ChainWriterError";
|
|
72
|
+
this.code = code || "chain-writer/invalid";
|
|
73
|
+
this.isChainWriterError = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function create(opts) {
|
|
78
|
+
if (!opts || !opts.table || !Array.isArray(opts.columnsForInsert) ||
|
|
79
|
+
!Array.isArray(opts.hashableColumns)) {
|
|
80
|
+
throw new ChainWriterError(
|
|
81
|
+
"create requires { table, columnsForInsert, hashableColumns }",
|
|
82
|
+
"chain-writer/invalid-config"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
// Validate table name shape AND require it's in the chain-table allowlist.
|
|
86
|
+
safeSql.validateIdentifier(opts.table);
|
|
87
|
+
safeSql.assertOneOf(opts.table, ALLOWED_CHAIN_TABLES);
|
|
88
|
+
|
|
89
|
+
// Validate every column name against the SQL identifier rules — we
|
|
90
|
+
// interpolate them into the INSERT SQL.
|
|
91
|
+
for (var i = 0; i < opts.columnsForInsert.length; i++) {
|
|
92
|
+
safeSql.validateIdentifier(opts.columnsForInsert[i]);
|
|
93
|
+
}
|
|
94
|
+
for (var j = 0; j < opts.hashableColumns.length; j++) {
|
|
95
|
+
safeSql.validateIdentifier(opts.hashableColumns[j]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var table = opts.table;
|
|
99
|
+
var columnsForInsert = opts.columnsForInsert.slice();
|
|
100
|
+
var hashableColumns = opts.hashableColumns.slice();
|
|
101
|
+
var validateInput = opts.validateInput || null;
|
|
102
|
+
|
|
103
|
+
// Per-chain Mutex serializes the read-prev-tip + compute-hash + insert
|
|
104
|
+
// sequence. Without serialization, two concurrent awaiting append() calls
|
|
105
|
+
// would hash against the same prev-tip and produce sibling rows with the
|
|
106
|
+
// same prevHash — forking the chain.
|
|
107
|
+
var _chainMutex = new safeAsync.Mutex();
|
|
108
|
+
|
|
109
|
+
// Lazy counter primer — first append reads MAX(monotonicCounter) and
|
|
110
|
+
// increments from there. Once ensures concurrent first-callers share
|
|
111
|
+
// one in-flight init Promise.
|
|
112
|
+
var _nextCounter = 1;
|
|
113
|
+
var _counterInit = null;
|
|
114
|
+
|
|
115
|
+
function _ensureCounterInit() {
|
|
116
|
+
if (!_counterInit) {
|
|
117
|
+
_counterInit = new safeAsync.Once(async function () {
|
|
118
|
+
var row = await safeAsync.withTimeout(
|
|
119
|
+
safeAsync.asyncRetry(function () {
|
|
120
|
+
return clusterStorage.executeOne(
|
|
121
|
+
"SELECT MAX(monotonicCounter) AS m FROM " + safeSql.quoteIdentifier(table)
|
|
122
|
+
);
|
|
123
|
+
}),
|
|
124
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
125
|
+
{ name: table + ".readMaxCounter" }
|
|
126
|
+
);
|
|
127
|
+
_nextCounter = (row && row.m ? Number(row.m) : 0) + 1;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return _counterInit.invoke();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function _readChainTipRow() {
|
|
134
|
+
return await safeAsync.withTimeout(
|
|
135
|
+
safeAsync.asyncRetry(function () {
|
|
136
|
+
return clusterStorage.executeOne(
|
|
137
|
+
"SELECT rowHash FROM " + safeSql.quoteIdentifier(table) +
|
|
138
|
+
" ORDER BY monotonicCounter DESC LIMIT 1"
|
|
139
|
+
);
|
|
140
|
+
}),
|
|
141
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
142
|
+
{ name: table + ".readChainTip" }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function _insertRow(values) {
|
|
147
|
+
// Build INSERT with quoted identifiers + ? placeholders. cluster-
|
|
148
|
+
// storage handles dialect-specific placeholder translation.
|
|
149
|
+
var quoted = columnsForInsert.map(function (c) { return safeSql.quoteIdentifier(c); }).join(", ");
|
|
150
|
+
var placeholders = columnsForInsert.map(function () { return "?"; }).join(", ");
|
|
151
|
+
return await safeAsync.withTimeout(
|
|
152
|
+
clusterStorage.execute(
|
|
153
|
+
"INSERT INTO " + safeSql.quoteIdentifier(table) +
|
|
154
|
+
" (" + quoted + ") VALUES (" + placeholders + ")",
|
|
155
|
+
values
|
|
156
|
+
),
|
|
157
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
158
|
+
{ name: table + ".insertRow" }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function append(logical) {
|
|
163
|
+
if (validateInput) validateInput(logical);
|
|
164
|
+
cluster.requireLeader();
|
|
165
|
+
await _ensureCounterInit();
|
|
166
|
+
|
|
167
|
+
return await _chainMutex.runExclusive(async function () {
|
|
168
|
+
return await _appendInsideMutex(logical);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function _appendInsideMutex(logical) {
|
|
173
|
+
var counter = _nextCounter++;
|
|
174
|
+
var nowMs = Date.now();
|
|
175
|
+
var nonce = generateBytes(16);
|
|
176
|
+
|
|
177
|
+
// Caller-supplied logical row: spread + add framework-managed fields.
|
|
178
|
+
var fullLogical = Object.assign({}, logical, {
|
|
179
|
+
_id: (logical && logical._id) || generateToken(16),
|
|
180
|
+
recordedAt: nowMs,
|
|
181
|
+
monotonicCounter: counter,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Seal sealed-fields, compute derived hashes
|
|
185
|
+
var sealed = cryptoField.sealRow(table, fullLogical);
|
|
186
|
+
|
|
187
|
+
// Materialize null entries for every hashable column the schema
|
|
188
|
+
// expects, so canonicalize sees the same key set at write-time and
|
|
189
|
+
// verify-time. JSON canonicalization distinguishes missing-key
|
|
190
|
+
// from key:null — must not.
|
|
191
|
+
for (var hci = 0; hci < hashableColumns.length; hci++) {
|
|
192
|
+
if (!(hashableColumns[hci] in sealed)) sealed[hashableColumns[hci]] = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Compute rowHash over the sealed content fields
|
|
196
|
+
var tipRow = await _readChainTipRow();
|
|
197
|
+
var prevHash = tipRow ? tipRow.rowHash : auditChain.ZERO_HASH;
|
|
198
|
+
var rowHash = auditChain.computeRowHash(prevHash, sealed, nonce);
|
|
199
|
+
|
|
200
|
+
sealed.prevHash = prevHash;
|
|
201
|
+
sealed.rowHash = rowHash;
|
|
202
|
+
sealed.nonce = nonce;
|
|
203
|
+
|
|
204
|
+
var fencingToken = cluster.fencingToken();
|
|
205
|
+
var values = columnsForInsert.map(function (c) {
|
|
206
|
+
if (c === "fencingToken") return fencingToken;
|
|
207
|
+
return c in sealed ? sealed[c] : null;
|
|
208
|
+
});
|
|
209
|
+
await _insertRow(values);
|
|
210
|
+
|
|
211
|
+
return Object.assign({ rowHash: rowHash, prevHash: prevHash }, fullLogical);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _resetForTest() {
|
|
215
|
+
_chainMutex = new safeAsync.Mutex();
|
|
216
|
+
_counterInit = null;
|
|
217
|
+
_nextCounter = 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
table: table,
|
|
222
|
+
append: append,
|
|
223
|
+
_resetForTest: _resetForTest,
|
|
224
|
+
// Expose for diagnostic introspection
|
|
225
|
+
_getMutexForTest: function () { return _chainMutex; },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
create: create,
|
|
231
|
+
ChainWriterError: ChainWriterError,
|
|
232
|
+
ALLOWED_CHAIN_TABLES: ALLOWED_CHAIN_TABLES,
|
|
233
|
+
FRAMEWORK_SQL_TIMEOUT_MS: FRAMEWORK_SQL_TIMEOUT_MS,
|
|
234
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cli-helpers — shared shape for blamejs CLI subcommands AND for
|
|
4
|
+
* operators writing their own one-shot CLI scripts on top of the
|
|
5
|
+
* framework.
|
|
6
|
+
*
|
|
7
|
+
* Three patterns recur across every CLI command (`migrate`, `seed`,
|
|
8
|
+
* `audit`, `vault`, `backup`, `api-key`):
|
|
9
|
+
*
|
|
10
|
+
* 1. Bootstrap a headless `b.createApp` instance from `--data-dir`,
|
|
11
|
+
* operate against vault + DB + audit chain, shut down cleanly.
|
|
12
|
+
* 2. Report success / error / usage with a consistent
|
|
13
|
+
* `blamejs <verb> <sub>: <message>` prefix on stderr + canonical
|
|
14
|
+
* exit codes (0 ok, 1 runtime failure, 2 arg error).
|
|
15
|
+
* 3. Resolve a passphrase from `--<flag>` or an env var, encode to
|
|
16
|
+
* the Buffer the underlying crypto primitive needs.
|
|
17
|
+
*
|
|
18
|
+
* var cli = b.cliHelpers;
|
|
19
|
+
*
|
|
20
|
+
* var report = cli.makeReporter(ctx, "blamejs my-tool issue");
|
|
21
|
+
* if (!args.flags["data-dir"]) return report.usage(MY_USAGE);
|
|
22
|
+
*
|
|
23
|
+
* var pp = cli.resolvePassphrase(args, ctx, {
|
|
24
|
+
* flag: "passphrase",
|
|
25
|
+
* envVar: "MY_TOOL_PASSPHRASE",
|
|
26
|
+
* });
|
|
27
|
+
* if (!pp) return report.error("--passphrase or MY_TOOL_PASSPHRASE is required", 2);
|
|
28
|
+
*
|
|
29
|
+
* var booted;
|
|
30
|
+
* try {
|
|
31
|
+
* booted = await cli.bootApp({
|
|
32
|
+
* dataDir: args.flags["data-dir"],
|
|
33
|
+
* vaultMode: "wrapped",
|
|
34
|
+
* env: ctx.env,
|
|
35
|
+
* });
|
|
36
|
+
* // do the op against booted.b.X / booted.app.db / etc.
|
|
37
|
+
* return report.ok("done");
|
|
38
|
+
* } catch (e) {
|
|
39
|
+
* return report.error(e.message);
|
|
40
|
+
* } finally {
|
|
41
|
+
* if (booted) await booted.app.shutdown();
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* The reporter writes through whichever stream `ctx.stdout` / `ctx.stderr`
|
|
45
|
+
* point at — `process.stdout` / `process.stderr` in production, captured
|
|
46
|
+
* stream stubs in tests — so the same handler is testable without
|
|
47
|
+
* spawning a child process.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var validateOpts = require("./validate-opts");
|
|
51
|
+
|
|
52
|
+
// ---- Streams + exit-code-shaped reporting --------------------------------
|
|
53
|
+
|
|
54
|
+
function _writeLine(stream, line) {
|
|
55
|
+
if (!stream || typeof stream.write !== "function") return;
|
|
56
|
+
if (line == null) return;
|
|
57
|
+
stream.write(String(line) + "\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Make a reporter bound to a CLI context + a verb prefix. Every
|
|
62
|
+
* stderr message produced by .error / .usage gets the prefix.
|
|
63
|
+
*
|
|
64
|
+
* var report = cliHelpers.makeReporter(ctx, "blamejs vault seal");
|
|
65
|
+
* return report.ok("sealed: " + path); // stdout, returns 0
|
|
66
|
+
* return report.error("decrypt failed"); // stderr "blamejs vault seal: decrypt failed", returns 1
|
|
67
|
+
* return report.error("missing arg", 2); // returns 2 (arg error vs runtime)
|
|
68
|
+
* return report.usage(VAULT_USAGE); // stderr USAGE, returns 2
|
|
69
|
+
* return report.helpStdout(VAULT_USAGE); // stdout USAGE, returns 0 (for `help <verb>`)
|
|
70
|
+
*/
|
|
71
|
+
function makeReporter(ctx, prefix) {
|
|
72
|
+
if (!ctx || typeof ctx !== "object") {
|
|
73
|
+
throw new Error("cliHelpers.makeReporter: ctx is required");
|
|
74
|
+
}
|
|
75
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
76
|
+
throw new Error("cliHelpers.makeReporter: prefix is required (non-empty string)");
|
|
77
|
+
}
|
|
78
|
+
var stdout = ctx.stdout || (typeof process !== "undefined" ? process.stdout : null);
|
|
79
|
+
var stderr = ctx.stderr || (typeof process !== "undefined" ? process.stderr : null);
|
|
80
|
+
return {
|
|
81
|
+
ok: function (message) {
|
|
82
|
+
if (message != null) _writeLine(stdout, message);
|
|
83
|
+
return 0;
|
|
84
|
+
},
|
|
85
|
+
error: function (message, exitCode) {
|
|
86
|
+
_writeLine(stderr, prefix + ": " + message);
|
|
87
|
+
return typeof exitCode === "number" ? exitCode : 1;
|
|
88
|
+
},
|
|
89
|
+
usage: function (usageText) {
|
|
90
|
+
_writeLine(stderr, usageText);
|
|
91
|
+
return 2;
|
|
92
|
+
},
|
|
93
|
+
helpStdout: function (usageText) {
|
|
94
|
+
_writeLine(stdout, usageText);
|
|
95
|
+
return 0;
|
|
96
|
+
},
|
|
97
|
+
// Direct access for handlers that have multi-line output.
|
|
98
|
+
write: function (line) { _writeLine(stdout, line); },
|
|
99
|
+
writeErr: function (line) { _writeLine(stderr, line); },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- Passphrase resolution -----------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a passphrase from a flag or env var into a UTF-8 Buffer
|
|
107
|
+
* (the shape the underlying crypto primitives accept).
|
|
108
|
+
*
|
|
109
|
+
* cli.resolvePassphrase(args, ctx, { flag: "passphrase", envVar: "BLAMEJS_VAULT_PASSPHRASE" })
|
|
110
|
+
* → Buffer | null
|
|
111
|
+
*
|
|
112
|
+
* Returns `null` when neither source produced a non-empty string. The
|
|
113
|
+
* caller decides whether absence is a hard error (vault seal) or a
|
|
114
|
+
* soft default (operator chose plaintext-mode).
|
|
115
|
+
*/
|
|
116
|
+
function resolvePassphrase(args, ctx, opts) {
|
|
117
|
+
opts = opts || {};
|
|
118
|
+
validateOpts(opts, ["flag", "envVar"], "cliHelpers.resolvePassphrase");
|
|
119
|
+
if (typeof opts.flag !== "string" || opts.flag.length === 0) {
|
|
120
|
+
throw new Error("cliHelpers.resolvePassphrase: opts.flag is required");
|
|
121
|
+
}
|
|
122
|
+
var raw = null;
|
|
123
|
+
if (args && args.flags && typeof args.flags[opts.flag] === "string" &&
|
|
124
|
+
args.flags[opts.flag].length > 0) {
|
|
125
|
+
raw = args.flags[opts.flag];
|
|
126
|
+
} else if (opts.envVar && ctx && ctx.env &&
|
|
127
|
+
typeof ctx.env[opts.envVar] === "string" &&
|
|
128
|
+
ctx.env[opts.envVar].length > 0) {
|
|
129
|
+
raw = ctx.env[opts.envVar];
|
|
130
|
+
}
|
|
131
|
+
if (raw == null) return null;
|
|
132
|
+
return Buffer.from(raw, "utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- Headless app bootstrap ----------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Boot a serverless `b.createApp` instance from a data dir so a CLI
|
|
139
|
+
* script (the framework's own subcommands or operator-written tools)
|
|
140
|
+
* can operate against the same vault + DB + audit chain the live app
|
|
141
|
+
* uses, without standing up an HTTP listener.
|
|
142
|
+
*
|
|
143
|
+
* var booted = await cli.bootApp({
|
|
144
|
+
* dataDir: "./data",
|
|
145
|
+
* vaultMode: "wrapped", // or "plaintext"; default "wrapped"
|
|
146
|
+
* env: process.env, // BLAMEJS_VAULT_PASSPHRASE read from here
|
|
147
|
+
* });
|
|
148
|
+
* // booted.b — the framework module
|
|
149
|
+
* // booted.app — the headless app instance (call .shutdown() to clean up)
|
|
150
|
+
*
|
|
151
|
+
* Caller MUST call `await booted.app.shutdown()` in a `finally` so the
|
|
152
|
+
* SQLite file handles + cluster lease release. The default DB at-rest
|
|
153
|
+
* mode is `plain` because CLI runs are short-lived ops that never
|
|
154
|
+
* serve requests; the encrypted-at-rest mode needs a tmpfs handle that
|
|
155
|
+
* wouldn't survive the CLI exit anyway. Operators running against a
|
|
156
|
+
* production data dir whose DB is encrypted-at-rest set
|
|
157
|
+
* `dbAtRest: "encrypted"` and ensure `BLAMEJS_TMPDIR` is set.
|
|
158
|
+
*/
|
|
159
|
+
async function bootApp(opts) {
|
|
160
|
+
opts = opts || {};
|
|
161
|
+
validateOpts(opts, ["dataDir", "vaultMode", "dbAtRest", "env"], "cliHelpers.bootApp");
|
|
162
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
163
|
+
throw new Error("cliHelpers.bootApp: opts.dataDir is required");
|
|
164
|
+
}
|
|
165
|
+
var vaultMode = opts.vaultMode || "wrapped";
|
|
166
|
+
if (vaultMode !== "wrapped" && vaultMode !== "plaintext") {
|
|
167
|
+
throw new Error("cliHelpers.bootApp: opts.vaultMode must be 'wrapped' or 'plaintext'");
|
|
168
|
+
}
|
|
169
|
+
var env = opts.env || (typeof process !== "undefined" ? process.env : {});
|
|
170
|
+
|
|
171
|
+
var vaultPassphrase = null;
|
|
172
|
+
if (vaultMode === "wrapped") {
|
|
173
|
+
var raw = env && env.BLAMEJS_VAULT_PASSPHRASE;
|
|
174
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
175
|
+
throw new Error("cliHelpers.bootApp: BLAMEJS_VAULT_PASSPHRASE is required " +
|
|
176
|
+
"for vault mode 'wrapped' (pass vaultMode: 'plaintext' for a dev data dir)");
|
|
177
|
+
}
|
|
178
|
+
vaultPassphrase = Buffer.from(raw, "utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Late require so this module stays loadable in contexts that aren't
|
|
182
|
+
// the framework root (tests, operator-side scripts importing only
|
|
183
|
+
// cli-helpers).
|
|
184
|
+
var b = require("../");
|
|
185
|
+
var app = await b.createApp({
|
|
186
|
+
dataDir: opts.dataDir,
|
|
187
|
+
routes: function () {},
|
|
188
|
+
vault: { mode: vaultMode, passphrase: vaultPassphrase },
|
|
189
|
+
db: {
|
|
190
|
+
atRest: opts.dbAtRest || "plain",
|
|
191
|
+
auditSigning: { mode: "plaintext" },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
return { b: b, app: app };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
makeReporter: makeReporter,
|
|
199
|
+
resolvePassphrase: resolvePassphrase,
|
|
200
|
+
bootApp: bootApp,
|
|
201
|
+
};
|