@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. 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
+ };