@blamejs/core 0.9.12 → 0.9.15
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 +3 -0
- package/lib/a2a.js +11 -11
- package/lib/acme.js +5 -5
- package/lib/ai-input.js +2 -2
- package/lib/api-key.js +4 -4
- package/lib/api-snapshot.js +10 -7
- package/lib/app-shutdown.js +2 -2
- package/lib/app.js +5 -5
- package/lib/archive.js +8 -8
- package/lib/argon2-builtin.js +2 -2
- package/lib/atomic-file.js +53 -53
- package/lib/audit-sign.js +8 -8
- package/lib/audit-tools.js +22 -22
- package/lib/audit.js +29 -17
- package/lib/auth/dpop.js +3 -3
- package/lib/auth/sd-jwt-vc.js +2 -2
- package/lib/backup/bundle.js +17 -17
- package/lib/backup/index.js +36 -36
- package/lib/budr.js +3 -3
- package/lib/bundler.js +20 -20
- package/lib/circuit-breaker.js +24 -9
- package/lib/cli.js +25 -26
- package/lib/cluster.js +2 -2
- package/lib/compliance-sanctions.js +2 -2
- package/lib/config-drift.js +15 -15
- package/lib/content-credentials.js +4 -4
- package/lib/credential-hash.js +3 -3
- package/lib/crypto.js +145 -0
- package/lib/daemon.js +19 -19
- package/lib/db-file-lifecycle.js +24 -24
- package/lib/db-schema.js +2 -2
- package/lib/db.js +35 -35
- package/lib/dev.js +10 -10
- package/lib/dr-runbook.js +5 -5
- package/lib/dsr.js +22 -15
- package/lib/dual-control.js +2 -2
- package/lib/external-db-migrate.js +2 -2
- package/lib/external-db.js +2 -2
- package/lib/fdx.js +2 -2
- package/lib/file-upload.js +30 -30
- package/lib/flag-providers.js +4 -4
- package/lib/gate-contract.js +5 -5
- package/lib/graphql-federation.js +4 -7
- package/lib/honeytoken.js +6 -6
- package/lib/http-client-cookie-jar.js +6 -6
- package/lib/http-client.js +18 -18
- package/lib/i18n.js +5 -5
- package/lib/inbox.js +21 -15
- package/lib/keychain.js +9 -9
- package/lib/legal-hold.js +2 -2
- package/lib/local-db-thin.js +9 -9
- package/lib/log-stream-local.js +17 -17
- package/lib/log-stream-syslog.js +2 -2
- package/lib/log-stream.js +3 -3
- package/lib/mail-bounce.js +2 -2
- package/lib/mail-mdn.js +2 -2
- package/lib/mail-srs.js +2 -2
- package/lib/mail.js +4 -4
- package/lib/mcp.js +2 -2
- package/lib/metrics.js +249 -2
- package/lib/middleware/api-encrypt.js +16 -16
- package/lib/middleware/body-parser.js +16 -16
- package/lib/middleware/compression.js +3 -3
- package/lib/middleware/csp-nonce.js +4 -4
- package/lib/middleware/health.js +7 -7
- package/lib/middleware/idempotency-key.js +250 -0
- package/lib/migrations.js +3 -3
- package/lib/mtls-ca.js +26 -26
- package/lib/mtls-engine-default.js +5 -5
- package/lib/network-dns.js +2 -2
- package/lib/network-nts.js +2 -2
- package/lib/network-proxy.js +3 -3
- package/lib/network-smtp-policy.js +2 -2
- package/lib/network-tls.js +17 -17
- package/lib/network.js +13 -13
- package/lib/notify.js +3 -3
- package/lib/object-store/gcs-bucket-ops.js +2 -2
- package/lib/object-store/gcs.js +5 -5
- package/lib/object-store/index.js +6 -6
- package/lib/object-store/local.js +19 -19
- package/lib/object-store/sigv4.js +3 -3
- package/lib/observability-tracer.js +4 -4
- package/lib/otel-export.js +3 -3
- package/lib/pagination.js +5 -5
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/pqc-agent.js +116 -26
- package/lib/pqc-gate.js +5 -5
- package/lib/pubsub-redis.js +2 -2
- package/lib/queue-local.js +3 -3
- package/lib/queue.js +2 -2
- package/lib/redis-client.js +4 -4
- package/lib/restore-bundle.js +18 -18
- package/lib/restore-rollback.js +34 -34
- package/lib/restore.js +16 -16
- package/lib/retry.js +50 -0
- package/lib/router.js +13 -13
- package/lib/sandbox.js +8 -8
- package/lib/sec-cyber.js +3 -3
- package/lib/security-assert.js +2 -2
- package/lib/seeders.js +4 -4
- package/lib/self-update-standalone-verifier.js +280 -0
- package/lib/self-update.js +32 -26
- package/lib/session-device-binding.js +2 -2
- package/lib/static.js +22 -22
- package/lib/template.js +19 -19
- package/lib/testing.js +7 -7
- package/lib/tls-exporter.js +5 -5
- package/lib/tracing.js +3 -3
- package/lib/vault/index.js +11 -11
- package/lib/vault/passphrase-ops.js +37 -37
- package/lib/vault/passphrase-source.js +2 -2
- package/lib/vault/rotate.js +70 -66
- package/lib/vault/seal-pem-file.js +26 -26
- package/lib/watcher.js +23 -23
- package/lib/webhook.js +10 -10
- package/lib/worker-pool.js +6 -6
- package/lib/ws-client.js +4 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -44,6 +44,11 @@ var nodeCrypto = require("node:crypto");
|
|
|
44
44
|
var lazyRequire = require("../lazy-require");
|
|
45
45
|
var numericBounds = require("../numeric-bounds");
|
|
46
46
|
var safeBuffer = require("../safe-buffer");
|
|
47
|
+
var safeJson = require("../safe-json");
|
|
48
|
+
var safeSql = require("../safe-sql");
|
|
49
|
+
var bCrypto = require("../crypto");
|
|
50
|
+
var cryptoField = require("../crypto-field");
|
|
51
|
+
var vault = require("../vault");
|
|
47
52
|
var { defineClass } = require("../framework-error");
|
|
48
53
|
|
|
49
54
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -124,6 +129,250 @@ function memoryStore(opts) {
|
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
// Operator-supplied table name is validated via b.safeSql.validateIdentifier
|
|
133
|
+
// — single source of truth for the framework's SQL-identifier shape
|
|
134
|
+
// (ASCII identifier chars only, 63-char cap, no reserved words). Direct
|
|
135
|
+
// interpolation is safe once the validator throws on bad input.
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.middleware.idempotencyKey.dbStore
|
|
139
|
+
* @signature b.middleware.idempotencyKey.dbStore(opts)
|
|
140
|
+
* @since 0.9.14
|
|
141
|
+
* @status stable
|
|
142
|
+
* @related b.middleware.idempotencyKey, b.middleware.idempotencyKey.memoryStore, b.db, b.cryptoField
|
|
143
|
+
*
|
|
144
|
+
* Persistent-backed store for `idempotencyKey` middleware. Implements
|
|
145
|
+
* the same three-method interface as `memoryStore` (`get` / `set` /
|
|
146
|
+
* `delete`) but stores records in a SQLite-shaped database — the
|
|
147
|
+
* framework's internal `b.db`, an operator-supplied better-sqlite3
|
|
148
|
+
* instance, or any object exposing `prepare(sql) → { run, get, all }`.
|
|
149
|
+
*
|
|
150
|
+
* Use `dbStore` instead of `memoryStore` when:
|
|
151
|
+
*
|
|
152
|
+
* - multiple processes share the request-handling fleet (forks
|
|
153
|
+
* behind a load balancer, multi-instance K8s deployment) and a
|
|
154
|
+
* retry can land on a different process than the original;
|
|
155
|
+
* - the daemon may restart between the original request and the
|
|
156
|
+
* retry (graceful rolling deploy, OOM kill, planned reboot) —
|
|
157
|
+
* `memoryStore` is volatile, `dbStore` survives the restart;
|
|
158
|
+
* - audit / compliance review needs to walk historic
|
|
159
|
+
* idempotency cache decisions queryable with
|
|
160
|
+
* `SELECT k, status_code, expires_at FROM <tableName>` —
|
|
161
|
+
* non-sealed columns are forensic-queryable without unsealing.
|
|
162
|
+
*
|
|
163
|
+
* **Defense-in-depth defaults (since 0.9.15) — both can be opted out:**
|
|
164
|
+
*
|
|
165
|
+
* - `hashKeys: true` — operator-supplied keys are sha3-512
|
|
166
|
+
* namespace-hashed via `b.crypto.namespaceHash("idempotency-key",
|
|
167
|
+
* key)` before insert/lookup. The `k` column carries the hash, not
|
|
168
|
+
* the raw key. Operator keys often carry PII (order numbers,
|
|
169
|
+
* emails, vendor prefixes); the DB never sees them.
|
|
170
|
+
* - `seal: true` — `headers` and `body` columns are sealed via
|
|
171
|
+
* `b.cryptoField.sealRow` (vault-managed key, AEAD envelope) so a
|
|
172
|
+
* DB dump leaks neither cached response bodies nor headers.
|
|
173
|
+
* Requires `b.vault.init(...)` to have run; falls back to plain-
|
|
174
|
+
* text with a one-shot audit warning when vault isn't ready, so
|
|
175
|
+
* test-fixture / boot-script callers still work.
|
|
176
|
+
*
|
|
177
|
+
* Lazily-expired: `get(key)` returns `null` for any row whose
|
|
178
|
+
* `expires_at` has passed. The cleanup is scoped by the observed
|
|
179
|
+
* `expires_at` so a concurrent upsert from a sibling process isn't
|
|
180
|
+
* clobbered.
|
|
181
|
+
*
|
|
182
|
+
* **Schema (v0.9.15, split columns):**
|
|
183
|
+
*
|
|
184
|
+
* ```
|
|
185
|
+
* k TEXT PRIMARY KEY -- hashed key when hashKeys=true
|
|
186
|
+
* fingerprint TEXT NOT NULL -- request method+path+body digest
|
|
187
|
+
* status_code INTEGER NOT NULL -- forensic-queryable
|
|
188
|
+
* headers TEXT NOT NULL -- JSON, sealed when seal=true
|
|
189
|
+
* body TEXT NOT NULL -- base64, sealed when seal=true
|
|
190
|
+
* expires_at INTEGER NOT NULL
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* **Migration note**: v0.9.14 used a single `v` JSON envelope column.
|
|
194
|
+
* Operators with a v0.9.14 table must `DROP TABLE <tableName>;` (or
|
|
195
|
+
* pick a fresh `tableName`) before upgrading — `CREATE TABLE IF NOT
|
|
196
|
+
* EXISTS` won't migrate column layout. Pre-v1 the framework breaks
|
|
197
|
+
* across patch versions for security correctness.
|
|
198
|
+
*
|
|
199
|
+
* @opts
|
|
200
|
+
* db: object, // required — sqlite-shaped: { prepare(sql) → { run, get, all } }
|
|
201
|
+
* tableName?: string, // default "blamejs_idempotency_keys"; validated via b.safeSql.validateIdentifier
|
|
202
|
+
* init?: boolean, // default true — run CREATE TABLE IF NOT EXISTS at construction
|
|
203
|
+
* hashKeys?: boolean, // default true — store sha3-512 namespace-hash of the key, not the raw key
|
|
204
|
+
* seal?: boolean, // default true — seal headers + body via b.cryptoField when vault is ready
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // single-process daemon, framework's internal sqlite, both defaults on:
|
|
208
|
+
* var b = require("blamejs");
|
|
209
|
+
* await b.vault.init({ dataDir: "/var/lib/myapp" });
|
|
210
|
+
* await b.db.init({ dataDir: "/var/lib/myapp", schema: [] });
|
|
211
|
+
* var store = b.middleware.idempotencyKey.dbStore({ db: b.db });
|
|
212
|
+
* var mw = b.middleware.idempotencyKey({
|
|
213
|
+
* store: store,
|
|
214
|
+
* ttlMs: b.constants.TIME.hours(24),
|
|
215
|
+
* });
|
|
216
|
+
* app.use(mw);
|
|
217
|
+
*/
|
|
218
|
+
function dbStore(opts) {
|
|
219
|
+
opts = opts || {};
|
|
220
|
+
if (!opts.db || typeof opts.db !== "object" || typeof opts.db.prepare !== "function") {
|
|
221
|
+
throw new IdempotencyError("idempotency/bad-db",
|
|
222
|
+
"dbStore: opts.db must be a sqlite-shaped database with a `prepare(sql)` method", true);
|
|
223
|
+
}
|
|
224
|
+
var tableNameRaw = opts.tableName !== undefined ? opts.tableName : "blamejs_idempotency_keys";
|
|
225
|
+
// Quote-and-validate via safeSql.quoteIdentifier — runs
|
|
226
|
+
// validateIdentifier internally + emits the dialect-correct quoted
|
|
227
|
+
// form. Identifier always reaches SQL through the quoted form.
|
|
228
|
+
var qTable;
|
|
229
|
+
try { qTable = safeSql.quoteIdentifier(tableNameRaw, "sqlite"); }
|
|
230
|
+
catch (sqlErr) {
|
|
231
|
+
throw new IdempotencyError("idempotency/bad-table-name",
|
|
232
|
+
"dbStore: opts.tableName is not a valid SQL identifier: " +
|
|
233
|
+
(sqlErr && sqlErr.message ? sqlErr.message : String(sqlErr)), true);
|
|
234
|
+
}
|
|
235
|
+
var qIndex = safeSql.quoteIdentifier(tableNameRaw + "_expires_idx", "sqlite");
|
|
236
|
+
var doInit = opts.init !== false;
|
|
237
|
+
var hashKeys = opts.hashKeys !== false;
|
|
238
|
+
var sealReq = opts.seal !== false;
|
|
239
|
+
var db = opts.db;
|
|
240
|
+
|
|
241
|
+
// Probe vault readiness with a sentinel seal. If vault.init() hasn't
|
|
242
|
+
// run (test fixture / boot-script / operator simply hasn't wired the
|
|
243
|
+
// posture yet) sealing falls back to plaintext for the lifetime of
|
|
244
|
+
// this dbStore instance and a single audit warning emits so the
|
|
245
|
+
// posture gap is visible in the chain.
|
|
246
|
+
var sealEnabled = false;
|
|
247
|
+
if (sealReq) {
|
|
248
|
+
try {
|
|
249
|
+
vault.seal("__idempotency_seal_probe__");
|
|
250
|
+
sealEnabled = true;
|
|
251
|
+
} catch (_vaultErr) {
|
|
252
|
+
_emitAudit("idempotency.seal_skipped_no_vault",
|
|
253
|
+
{ tableName: tableNameRaw,
|
|
254
|
+
reason: "vault.init() has not run; sealing falls back to plaintext" },
|
|
255
|
+
"warning");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Register the table with cryptoField. registerTable is idempotent
|
|
260
|
+
// — subsequent dbStore() calls with the same tableName re-declare
|
|
261
|
+
// the same sealedFields and no-op.
|
|
262
|
+
if (sealEnabled) {
|
|
263
|
+
cryptoField.registerTable(tableNameRaw, {
|
|
264
|
+
sealedFields: ["headers", "body"],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (doInit) {
|
|
269
|
+
db.prepare("CREATE TABLE IF NOT EXISTS " + qTable + " (" +
|
|
270
|
+
"k TEXT PRIMARY KEY, " +
|
|
271
|
+
"fingerprint TEXT NOT NULL, " +
|
|
272
|
+
"status_code INTEGER NOT NULL, " +
|
|
273
|
+
"headers TEXT NOT NULL, " +
|
|
274
|
+
"body TEXT NOT NULL, " +
|
|
275
|
+
"expires_at INTEGER NOT NULL)").run();
|
|
276
|
+
db.prepare("CREATE INDEX IF NOT EXISTS " + qIndex + " ON " +
|
|
277
|
+
qTable + "(expires_at)").run();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Prepared statements. status_code + expires_at stay non-sealed
|
|
281
|
+
// so audit/forensic SELECTs don't have to unseal-everything.
|
|
282
|
+
var stmtGet = db.prepare(
|
|
283
|
+
"SELECT fingerprint, status_code, headers, body, expires_at FROM " +
|
|
284
|
+
qTable + " WHERE k = ?");
|
|
285
|
+
var stmtUpsert = db.prepare(
|
|
286
|
+
"INSERT INTO " + qTable +
|
|
287
|
+
"(k, fingerprint, status_code, headers, body, expires_at) " +
|
|
288
|
+
"VALUES (?, ?, ?, ?, ?, ?) " +
|
|
289
|
+
"ON CONFLICT(k) DO UPDATE SET " +
|
|
290
|
+
" fingerprint = excluded.fingerprint, " +
|
|
291
|
+
" status_code = excluded.status_code, " +
|
|
292
|
+
" headers = excluded.headers, " +
|
|
293
|
+
" body = excluded.body, " +
|
|
294
|
+
" expires_at = excluded.expires_at");
|
|
295
|
+
var stmtDeleteStale = db.prepare("DELETE FROM " + qTable +
|
|
296
|
+
" WHERE k = ? AND expires_at <= ?");
|
|
297
|
+
var stmtDelete = db.prepare("DELETE FROM " + qTable + " WHERE k = ?");
|
|
298
|
+
|
|
299
|
+
function _k(rawKey) {
|
|
300
|
+
if (!hashKeys) return rawKey;
|
|
301
|
+
return bCrypto.namespaceHash("idempotency-key", rawKey);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
get: function (rawKey) {
|
|
306
|
+
var row = stmtGet.get(_k(rawKey));
|
|
307
|
+
if (!row) return null;
|
|
308
|
+
if (row.expires_at < Date.now()) {
|
|
309
|
+
stmtDeleteStale.run(_k(rawKey), row.expires_at);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
var liveRow = row;
|
|
313
|
+
if (sealEnabled) {
|
|
314
|
+
try { liveRow = cryptoField.unsealRow(tableNameRaw, row); }
|
|
315
|
+
catch (_unsealErr) {
|
|
316
|
+
// Decryption failed (key rotation gap / corrupt envelope).
|
|
317
|
+
// Treat as miss + drop the row so the handler runs fresh
|
|
318
|
+
// and we capture a re-sealable replacement.
|
|
319
|
+
stmtDeleteStale.run(_k(rawKey), row.expires_at);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var headersObj;
|
|
324
|
+
try {
|
|
325
|
+
headersObj = safeJson.parse(liveRow.headers, { maxBytes: 4 * 1024 * 1024 }); // allow:raw-byte-literal — 4 MiB headers ceiling
|
|
326
|
+
} catch (_jsonErr) {
|
|
327
|
+
// Parse failure has two distinct causes:
|
|
328
|
+
// 1. Genuine corruption (truncated row, encoding mishap) — drop.
|
|
329
|
+
// 2. The row was sealed by a sibling process (vault: prefix
|
|
330
|
+
// present) but THIS process has sealEnabled=false (vault
|
|
331
|
+
// not initialized OR opts.seal=false). The row is valid
|
|
332
|
+
// cross-process state we just can't read locally;
|
|
333
|
+
// DELETING it would clobber another process's cache and
|
|
334
|
+
// turn a hit into a miss with potential side-effect re-
|
|
335
|
+
// execution. Treat as miss + LEAVE the row in place.
|
|
336
|
+
// Per Codex P1 on PR #45.
|
|
337
|
+
var lookedSealed = typeof liveRow.headers === "string" &&
|
|
338
|
+
liveRow.headers.indexOf("vault:") === 0;
|
|
339
|
+
if (!lookedSealed) {
|
|
340
|
+
stmtDeleteStale.run(_k(rawKey), row.expires_at);
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
fingerprint: liveRow.fingerprint,
|
|
346
|
+
statusCode: liveRow.status_code,
|
|
347
|
+
headers: headersObj,
|
|
348
|
+
body: liveRow.body,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
set: function (rawKey, value, ttlMs) {
|
|
352
|
+
var rowOut = {
|
|
353
|
+
k: _k(rawKey),
|
|
354
|
+
fingerprint: value.fingerprint,
|
|
355
|
+
status_code: value.statusCode,
|
|
356
|
+
headers: JSON.stringify(value.headers || {}),
|
|
357
|
+
body: value.body || "",
|
|
358
|
+
expires_at: Date.now() + ttlMs,
|
|
359
|
+
};
|
|
360
|
+
if (sealEnabled) {
|
|
361
|
+
rowOut = cryptoField.sealRow(tableNameRaw, rowOut);
|
|
362
|
+
}
|
|
363
|
+
stmtUpsert.run(
|
|
364
|
+
rowOut.k, rowOut.fingerprint, rowOut.status_code,
|
|
365
|
+
rowOut.headers, rowOut.body, rowOut.expires_at);
|
|
366
|
+
},
|
|
367
|
+
delete: function (rawKey) {
|
|
368
|
+
stmtDelete.run(_k(rawKey));
|
|
369
|
+
},
|
|
370
|
+
_tableName: tableNameRaw,
|
|
371
|
+
_hashKeys: hashKeys,
|
|
372
|
+
_sealEnabled: sealEnabled,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
127
376
|
function _validateStore(store, where) {
|
|
128
377
|
if (!store || typeof store !== "object") {
|
|
129
378
|
throw new IdempotencyError("idempotency/bad-store",
|
|
@@ -420,5 +669,6 @@ function _redactKey(key) {
|
|
|
420
669
|
module.exports = create;
|
|
421
670
|
module.exports.create = create;
|
|
422
671
|
module.exports.memoryStore = memoryStore;
|
|
672
|
+
module.exports.dbStore = dbStore;
|
|
423
673
|
module.exports.DEFAULT_METHODS = DEFAULT_METHODS;
|
|
424
674
|
module.exports.IdempotencyError = IdempotencyError;
|
package/lib/migrations.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* down() succeeds.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
var
|
|
41
|
+
var nodePath = require("path");
|
|
42
42
|
var atomicFile = require("./atomic-file");
|
|
43
43
|
var dbSchema = require("./db-schema");
|
|
44
44
|
var lazyRequire = require("./lazy-require");
|
|
@@ -200,7 +200,7 @@ function _acquireLock(db, opts) {
|
|
|
200
200
|
function _releaseLock(db, holder) {
|
|
201
201
|
// Only release our own lock — a process whose deploy was killed
|
|
202
202
|
// shouldn't have its lock cleared by an unrelated next deploy unless
|
|
203
|
-
// the operator explicitly used the staleAfterMs
|
|
203
|
+
// the operator explicitly used the staleAfterMs nodePath.
|
|
204
204
|
try {
|
|
205
205
|
db.prepare(
|
|
206
206
|
"DELETE FROM " + Q_LOCK_TABLE + " WHERE scope = 'lock' AND lockedBy = ?"
|
|
@@ -234,7 +234,7 @@ function _resolveDb(opts) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
function _loadMigration(file, dir) {
|
|
237
|
-
var fullPath =
|
|
237
|
+
var fullPath = nodePath.join(dir, file);
|
|
238
238
|
// Drop the require cache for this path before loading so a test that
|
|
239
239
|
// changes a migration file between calls picks up the new content.
|
|
240
240
|
// Production deployments would always restart the process, but this
|
package/lib/mtls-ca.js
CHANGED
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
* Mutual TLS Certificate Authority — internal CA cert issuance, mTLS gate setup, fingerprint pinning.
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
var
|
|
56
|
-
var
|
|
55
|
+
var nodeFs = require("fs");
|
|
56
|
+
var nodePath = require("path");
|
|
57
57
|
var nodeCrypto = require("node:crypto");
|
|
58
58
|
var atomicFile = require("./atomic-file");
|
|
59
59
|
var C = require("./constants");
|
|
@@ -101,10 +101,10 @@ var VALID_SEAL_MODES = { required: 1, disabled: 1 };
|
|
|
101
101
|
// through unchanged. The pre-v0.8.58 shape always joined under
|
|
102
102
|
// dataDir, which silently overrode an operator-supplied absolute
|
|
103
103
|
// path (e.g. `MTLS_CA_KEY=/etc/ssl/ca.key` → `<dataDir>/etc/ssl/ca.key`).
|
|
104
|
-
// Standard Node `
|
|
104
|
+
// Standard Node `nodePath.join` semantics already preserve absolute
|
|
105
105
|
// arguments — the always-join was an oversight, not by design.
|
|
106
106
|
function _absoluteOrUnderDataDir(dataDir, p) {
|
|
107
|
-
return
|
|
107
|
+
return nodePath.isAbsolute(p) ? p : nodePath.join(dataDir, p);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
function _resolvePaths(dataDir, paths) {
|
|
@@ -202,8 +202,8 @@ function create(opts) {
|
|
|
202
202
|
// the first initCA() / generateClientCert() call fails with ENOENT
|
|
203
203
|
// on `ca.key.tmp` because the atomic-file write expects the parent
|
|
204
204
|
// dir to exist.
|
|
205
|
-
if (!
|
|
206
|
-
|
|
205
|
+
if (!nodeFs.existsSync(opts.dataDir)) {
|
|
206
|
+
nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
|
|
207
207
|
}
|
|
208
208
|
var paths = _resolvePaths(opts.dataDir, opts.paths);
|
|
209
209
|
var vault = opts.vault || null;
|
|
@@ -228,10 +228,10 @@ function create(opts) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
function keyExists() {
|
|
231
|
-
return
|
|
231
|
+
return nodeFs.existsSync(paths.caKey) || nodeFs.existsSync(paths.caKeySealed);
|
|
232
232
|
}
|
|
233
233
|
function exists() {
|
|
234
|
-
return keyExists() &&
|
|
234
|
+
return keyExists() && nodeFs.existsSync(paths.caCert);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function status() {
|
|
@@ -243,7 +243,7 @@ function create(opts) {
|
|
|
243
243
|
current: generation,
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
var pem =
|
|
246
|
+
var pem = nodeFs.readFileSync(paths.caCert);
|
|
247
247
|
var gen = parseGeneration(pem);
|
|
248
248
|
return {
|
|
249
249
|
exists: true,
|
|
@@ -257,8 +257,8 @@ function create(opts) {
|
|
|
257
257
|
// caKeySealedMode dispatch. Returns Buffer of PEM bytes, or throws
|
|
258
258
|
// with a precise reason when the mode rejects the on-disk form.
|
|
259
259
|
function loadKey() {
|
|
260
|
-
var hasPlain =
|
|
261
|
-
var hasSealed =
|
|
260
|
+
var hasPlain = nodeFs.existsSync(paths.caKey);
|
|
261
|
+
var hasSealed = nodeFs.existsSync(paths.caKeySealed);
|
|
262
262
|
if (!hasPlain && !hasSealed) {
|
|
263
263
|
throw new MtlsCaError("mtls-ca/missing-key",
|
|
264
264
|
"no CA key on disk at " + paths.caKey + " or " + paths.caKeySealed);
|
|
@@ -269,7 +269,7 @@ function create(opts) {
|
|
|
269
269
|
"CA_KEY_SEALED='required' but " + paths.caKeySealed + " does not exist");
|
|
270
270
|
}
|
|
271
271
|
_requireVault("sealed CA key load");
|
|
272
|
-
var sealedBytes =
|
|
272
|
+
var sealedBytes = nodeFs.readFileSync(paths.caKeySealed, "utf8").trim();
|
|
273
273
|
var pem = vault.unseal(sealedBytes);
|
|
274
274
|
if (!pem) {
|
|
275
275
|
throw new MtlsCaError("mtls-ca/unseal-failed",
|
|
@@ -282,15 +282,15 @@ function create(opts) {
|
|
|
282
282
|
throw new MtlsCaError("mtls-ca/plain-required",
|
|
283
283
|
"caKeySealedMode='disabled' but " + paths.caKey + " does not exist");
|
|
284
284
|
}
|
|
285
|
-
return
|
|
285
|
+
return nodeFs.readFileSync(paths.caKey);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function loadCert() {
|
|
289
|
-
if (!
|
|
289
|
+
if (!nodeFs.existsSync(paths.caCert)) {
|
|
290
290
|
throw new MtlsCaError("mtls-ca/missing-cert",
|
|
291
291
|
"no CA cert on disk at " + paths.caCert);
|
|
292
292
|
}
|
|
293
|
-
return
|
|
293
|
+
return nodeFs.readFileSync(paths.caCert);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// Atomic commit: write .tmp + atomic rename for both key and cert.
|
|
@@ -311,22 +311,22 @@ function create(opts) {
|
|
|
311
311
|
try {
|
|
312
312
|
if (sealed) {
|
|
313
313
|
_requireVault("sealed CA key commit");
|
|
314
|
-
|
|
314
|
+
nodeFs.writeFileSync(keyTmp, vault.seal(opts2.caKeyPem), { mode: 0o600 });
|
|
315
315
|
} else {
|
|
316
|
-
|
|
316
|
+
nodeFs.writeFileSync(keyTmp, opts2.caKeyPem, { mode: 0o600 });
|
|
317
317
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
nodeFs.writeFileSync(certTmp, opts2.caCertPem, { mode: 0o644 });
|
|
319
|
+
nodeFs.renameSync(keyTmp, keyDest);
|
|
320
|
+
nodeFs.renameSync(certTmp, paths.caCert);
|
|
321
321
|
} catch (e) {
|
|
322
322
|
// Best-effort cleanup of half-written tmp files; the original
|
|
323
323
|
// commit error is what we re-raise. Log cleanup failures at debug
|
|
324
324
|
// so a genuinely-broken filesystem state surfaces in operator logs
|
|
325
325
|
// rather than getting silently swallowed.
|
|
326
|
-
try { if (
|
|
327
|
-
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "
|
|
328
|
-
try { if (
|
|
329
|
-
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "
|
|
326
|
+
try { if (nodeFs.existsSync(keyTmp)) nodeFs.unlinkSync(keyTmp); }
|
|
327
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: keyTmp, error: cleanupErr.message }); }
|
|
328
|
+
try { if (nodeFs.existsSync(certTmp)) nodeFs.unlinkSync(certTmp); }
|
|
329
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "nodeFs.unlinkSync", path: certTmp, error: cleanupErr.message }); }
|
|
330
330
|
throw new MtlsCaError("mtls-ca/commit-failed",
|
|
331
331
|
"atomic CA commit failed: " + ((e && e.message) || String(e)));
|
|
332
332
|
}
|
|
@@ -381,13 +381,13 @@ function create(opts) {
|
|
|
381
381
|
// ---- Revocation registry + CRL ----
|
|
382
382
|
|
|
383
383
|
function _loadRevocations() {
|
|
384
|
-
if (!
|
|
384
|
+
if (!nodeFs.existsSync(paths.revocations)) return { revocations: [] };
|
|
385
385
|
try {
|
|
386
386
|
// safeJson.parse caps depth + size + protects against
|
|
387
387
|
// proto-pollution; the revocation file is under the operator's
|
|
388
388
|
// dataDir but a tampered or truncated file shouldn't be able to
|
|
389
389
|
// corrupt the rotator process.
|
|
390
|
-
var json = safeJson.parse(
|
|
390
|
+
var json = safeJson.parse(nodeFs.readFileSync(paths.revocations, "utf8"),
|
|
391
391
|
{ maxBytes: C.BYTES.mib(16) });
|
|
392
392
|
if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
|
|
393
393
|
return json;
|
|
@@ -32,8 +32,8 @@ var nodeCrypto = require("node:crypto");
|
|
|
32
32
|
var pki = require("./vendor/pki.cjs");
|
|
33
33
|
|
|
34
34
|
var C = require("./constants");
|
|
35
|
-
var
|
|
36
|
-
var
|
|
35
|
+
var bCrypto = require("./crypto");
|
|
36
|
+
var numericBounds = require("./numeric-bounds");
|
|
37
37
|
var { FrameworkError } = require("./framework-error");
|
|
38
38
|
|
|
39
39
|
var x509 = pki.x509;
|
|
@@ -231,7 +231,7 @@ async function generateCa(opts) {
|
|
|
231
231
|
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
232
232
|
var now = new Date();
|
|
233
233
|
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
234
|
-
serialNumber:
|
|
234
|
+
serialNumber: bCrypto.generateToken(C.BYTES.bytes(16)),
|
|
235
235
|
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
236
236
|
notBefore: now,
|
|
237
237
|
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
@@ -255,7 +255,7 @@ async function signClientCert(opts) {
|
|
|
255
255
|
throw new MtlsEngineError("mtls-engine/missing-arg",
|
|
256
256
|
"signClientCert requires { cn, caCertPem, caKeyPem }");
|
|
257
257
|
}
|
|
258
|
-
|
|
258
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.validityDays,
|
|
259
259
|
"signClientCert: validityDays", MtlsEngineError, "mtls-engine/bad-validity-days");
|
|
260
260
|
var validityDays = opts.validityDays !== undefined
|
|
261
261
|
? opts.validityDays : LEAF_DEFAULT_DAYS;
|
|
@@ -314,7 +314,7 @@ async function signClientCert(opts) {
|
|
|
314
314
|
if (sanExt) extensions.push(sanExt);
|
|
315
315
|
|
|
316
316
|
var clientCert = await x509.X509CertificateGenerator.create({
|
|
317
|
-
serialNumber:
|
|
317
|
+
serialNumber: bCrypto.generateToken(C.BYTES.bytes(16)),
|
|
318
318
|
subject: "CN=" + cn,
|
|
319
319
|
issuer: caCert.subject,
|
|
320
320
|
notBefore: now,
|
package/lib/network-dns.js
CHANGED
|
@@ -4,7 +4,7 @@ var dns = require("node:dns");
|
|
|
4
4
|
var net = require("node:net");
|
|
5
5
|
var nodeCrypto = require("node:crypto");
|
|
6
6
|
var https = require("node:https");
|
|
7
|
-
var
|
|
7
|
+
var nodeTls = require("node:tls");
|
|
8
8
|
var dnsPromises = dns.promises;
|
|
9
9
|
|
|
10
10
|
var C = require("./constants");
|
|
@@ -561,7 +561,7 @@ function _dotConnect() {
|
|
|
561
561
|
ecdhCurve: C.TLS_GROUP_CURVE_STR,
|
|
562
562
|
};
|
|
563
563
|
if (STATE.dot.ca) connectOpts.ca = STATE.dot.ca;
|
|
564
|
-
var sock =
|
|
564
|
+
var sock = nodeTls.connect(connectOpts);
|
|
565
565
|
// The pool entry is ref()'d while a query is in flight and unref()'d
|
|
566
566
|
// when idle — _dotLookup toggles this around its query. Calling
|
|
567
567
|
// unref() unconditionally here let node exit during a normal lookup
|
package/lib/network-nts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var nodeTls = require("node:tls");
|
|
4
4
|
var dgram = require("node:dgram");
|
|
5
5
|
var nodeCrypto = require("node:crypto");
|
|
6
6
|
|
|
@@ -258,7 +258,7 @@ function performKeHandshake(opts) {
|
|
|
258
258
|
ecdhCurve: C.TLS_GROUP_CURVE_STR,
|
|
259
259
|
};
|
|
260
260
|
if (opts.ca) connectOpts.ca = opts.ca;
|
|
261
|
-
var sock =
|
|
261
|
+
var sock = nodeTls.connect(connectOpts);
|
|
262
262
|
var timer = setTimeout(function () {
|
|
263
263
|
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
264
264
|
done(new NtsError("nts/ke-timeout", "NTS-KE handshake timed out after " + timeoutMs + "ms"));
|
package/lib/network-proxy.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var http = require("node:http");
|
|
4
4
|
var https = require("node:https");
|
|
5
5
|
var net = require("node:net");
|
|
6
|
-
var
|
|
6
|
+
var nodeTls = require("node:tls");
|
|
7
7
|
|
|
8
8
|
var C = require("./constants");
|
|
9
9
|
var lazyRequire = require("./lazy-require");
|
|
@@ -155,7 +155,7 @@ function _proxyAuthHeader(proxyUrl) {
|
|
|
155
155
|
function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
|
|
156
156
|
var proxyPort = proxyUrl.port || (proxyUrl.protocol === "https:" ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
|
|
157
157
|
var proxySocket = proxyUrl.protocol === "https:"
|
|
158
|
-
?
|
|
158
|
+
? nodeTls.connect({
|
|
159
159
|
host: proxyUrl.hostname,
|
|
160
160
|
port: proxyPort,
|
|
161
161
|
servername: proxyUrl.hostname,
|
|
@@ -212,7 +212,7 @@ function agentFor(targetUrl) {
|
|
|
212
212
|
agent.createConnection = function (options, cb) {
|
|
213
213
|
_connectThroughTunnel(proxy, options.host, options.port, function (err, tunnel) {
|
|
214
214
|
if (err) return cb(err);
|
|
215
|
-
var secure =
|
|
215
|
+
var secure = nodeTls.connect({
|
|
216
216
|
socket: tunnel,
|
|
217
217
|
servername: options.servername || options.host,
|
|
218
218
|
minVersion: "TLSv1.3",
|
|
@@ -58,7 +58,7 @@ var zlib = require("node:zlib");
|
|
|
58
58
|
var asn1 = require("./asn1-der");
|
|
59
59
|
var lazyRequire = require("./lazy-require");
|
|
60
60
|
var validateOpts = require("./validate-opts");
|
|
61
|
-
var
|
|
61
|
+
var bCrypto = require("./crypto");
|
|
62
62
|
var safeUrl = require("./safe-url");
|
|
63
63
|
var safeJson = require("./safe-json");
|
|
64
64
|
var C = require("./constants");
|
|
@@ -561,7 +561,7 @@ function tlsRptRecordShape(opts) {
|
|
|
561
561
|
|
|
562
562
|
function _genReportId() {
|
|
563
563
|
// RFC 8460 §4.4 requires uniqueness — use timestamp + random token.
|
|
564
|
-
return Date.now() + "-" +
|
|
564
|
+
return Date.now() + "-" + bCrypto.generateToken(C.BYTES.bytes(8));
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
// ---- TLS-RPT policy fetch (RFC 8460 §3) ----
|
package/lib/network-tls.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
3
|
+
var nodeTls = require("node:tls");
|
|
4
|
+
var nodeFs = require("node:fs");
|
|
5
|
+
var nodePath = require("node:path");
|
|
6
6
|
var net = require("node:net");
|
|
7
7
|
var nodeCrypto = require("node:crypto");
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var bCrypto = require("./crypto");
|
|
10
10
|
var C = require("./constants");
|
|
11
11
|
var safeBuffer = require("./safe-buffer");
|
|
12
12
|
var validateOpts = require("./validate-opts");
|
|
@@ -85,14 +85,14 @@ function _isPathLike(s) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function _readPath(p) {
|
|
88
|
-
var stat =
|
|
88
|
+
var stat = nodeFs.statSync(p);
|
|
89
89
|
if (stat.isDirectory()) {
|
|
90
|
-
var files =
|
|
90
|
+
var files = nodeFs.readdirSync(p)
|
|
91
91
|
.filter(function (f) { return /\.(pem|crt|cer)$/i.test(f); })
|
|
92
92
|
.sort();
|
|
93
|
-
return files.map(function (f) { return
|
|
93
|
+
return files.map(function (f) { return nodeFs.readFileSync(nodePath.join(p, f), "utf8"); }).join("\n");
|
|
94
94
|
}
|
|
95
|
-
return
|
|
95
|
+
return nodeFs.readFileSync(p, "utf8");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function addCa(pemOrPath, opts) {
|
|
@@ -101,7 +101,7 @@ function addCa(pemOrPath, opts) {
|
|
|
101
101
|
var raw = pemOrPath;
|
|
102
102
|
if (typeof pemOrPath === "string" && _isPathLike(pemOrPath)) {
|
|
103
103
|
var stat;
|
|
104
|
-
try { stat =
|
|
104
|
+
try { stat = nodeFs.statSync(pemOrPath); } catch (_e) {
|
|
105
105
|
throw new TlsTrustError("tls/empty-pem", "tls.addCa: input has no PEM marker and is not a readable path: " +
|
|
106
106
|
pemOrPath);
|
|
107
107
|
}
|
|
@@ -408,7 +408,7 @@ function applyToContext(opts) {
|
|
|
408
408
|
var base = Object.assign({}, opts.base || {});
|
|
409
409
|
var caStrings = STATE.cas.map(function (e) { return e.pem; });
|
|
410
410
|
if (STATE.systemTrust) {
|
|
411
|
-
var rootCAs =
|
|
411
|
+
var rootCAs = nodeTls.rootCertificates;
|
|
412
412
|
if (Array.isArray(rootCAs)) {
|
|
413
413
|
caStrings = caStrings.concat(rootCAs);
|
|
414
414
|
}
|
|
@@ -719,7 +719,7 @@ function _connectAndCheckOcsp(opts, requireStapled) {
|
|
|
719
719
|
var connectOpts = Object.assign({}, opts, { requestOCSP: true });
|
|
720
720
|
var sock;
|
|
721
721
|
try {
|
|
722
|
-
sock =
|
|
722
|
+
sock = nodeTls.connect(connectOpts);
|
|
723
723
|
} catch (e) {
|
|
724
724
|
reject(new TlsTrustError("tls/connect-failed",
|
|
725
725
|
"tls.connect threw: " + ((e && e.message) || String(e))));
|
|
@@ -1110,7 +1110,7 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
1110
1110
|
// critical here (the OCSP response is CA-signed and signature
|
|
1111
1111
|
// already verified) but matches the project discipline.
|
|
1112
1112
|
// (Audit 2026-05-11.)
|
|
1113
|
-
if (!
|
|
1113
|
+
if (!bCrypto.timingSafeEqual(parsed.basic.nonce, opts.expectedNonce)) {
|
|
1114
1114
|
return { ok: false, status: parsed.status, signatureValid: true,
|
|
1115
1115
|
errors: ["OCSP nonce mismatch — possible replay or wrong responder"] };
|
|
1116
1116
|
}
|
|
@@ -2182,7 +2182,7 @@ var ct = Object.freeze({
|
|
|
2182
2182
|
if (!Buffer.isBuffer(sthRoot) || sthRoot.length !== 32) { // allow:raw-byte-literal — RFC 9162 SHA-256 digest length
|
|
2183
2183
|
return { valid: false, reason: "bad-sth-root" };
|
|
2184
2184
|
}
|
|
2185
|
-
if (!
|
|
2185
|
+
if (!bCrypto.timingSafeEqual(computedRoot, sthRoot)) {
|
|
2186
2186
|
return { valid: false, reason: "root-mismatch",
|
|
2187
2187
|
computedRoot: computedRoot.toString("hex") };
|
|
2188
2188
|
}
|
|
@@ -2201,7 +2201,7 @@ var ct = Object.freeze({
|
|
|
2201
2201
|
var computedSecond = _ctVerifyConsistencyPath(
|
|
2202
2202
|
opts.consistency.firstSize, opts.sthFromLog.treeSize,
|
|
2203
2203
|
opts.consistency.proof || [], firstRoot);
|
|
2204
|
-
var ok =
|
|
2204
|
+
var ok = bCrypto.timingSafeEqual(computedSecond, sthRoot);
|
|
2205
2205
|
consistencyResult = {
|
|
2206
2206
|
ok: ok,
|
|
2207
2207
|
computedSecondRoot: computedSecond.toString("hex"),
|
|
@@ -2256,7 +2256,7 @@ var ct = Object.freeze({
|
|
|
2256
2256
|
return { valid: false, reason: "consistency-walk-failed",
|
|
2257
2257
|
error: (e && e.message) || String(e) };
|
|
2258
2258
|
}
|
|
2259
|
-
if (!
|
|
2259
|
+
if (!bCrypto.timingSafeEqual(computed, secondRoot)) {
|
|
2260
2260
|
return { valid: false, reason: "root-mismatch",
|
|
2261
2261
|
computedRoot: computed.toString("hex") };
|
|
2262
2262
|
}
|
|
@@ -2500,7 +2500,7 @@ function _isEchSupported() {
|
|
|
2500
2500
|
// immediately-destroyed socket. Any non-throwing path = supported.
|
|
2501
2501
|
var supported = false;
|
|
2502
2502
|
try {
|
|
2503
|
-
var probe =
|
|
2503
|
+
var probe = nodeTls.connect({
|
|
2504
2504
|
host: "127.0.0.1",
|
|
2505
2505
|
port: 1,
|
|
2506
2506
|
ech: Buffer.alloc(0),
|
|
@@ -2651,7 +2651,7 @@ function connectWithEch(opts) {
|
|
|
2651
2651
|
}
|
|
2652
2652
|
|
|
2653
2653
|
var sock;
|
|
2654
|
-
try { sock =
|
|
2654
|
+
try { sock = nodeTls.connect(connectOpts); }
|
|
2655
2655
|
catch (e) {
|
|
2656
2656
|
reject(new NetworkTlsError("tls/ech-connect-failed",
|
|
2657
2657
|
"connectWithEch: tls.connect threw: " + ((e && e.message) || String(e))));
|