@blamejs/core 0.8.43 → 0.8.50
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/vault/index.js
CHANGED
|
@@ -1,34 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.vault
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Crypto
|
|
6
|
+
* @title Vault
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Sealed keystore that anchors every other framework subsystem holding
|
|
10
|
+
* secrets at rest: db field encryption, encrypted session storage,
|
|
11
|
+
* audit-log signing keys, OAuth refresh tokens, anything that flows
|
|
12
|
+
* through `b.vault.seal` / `b.vault.unseal`. The vault is the single
|
|
13
|
+
* trust root for the framework — rotate it and everything sealed under
|
|
14
|
+
* the old keys re-seals as part of the same operation.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* Keys held: an ML-KEM-1024 + ECDH P-384 hybrid keypair plus a
|
|
17
|
+
* per-deployment derivedHash salt. After `init()` the keypair never
|
|
18
|
+
* leaves the process in any decrypted form except via the seal /
|
|
19
|
+
* unseal API.
|
|
12
20
|
*
|
|
13
|
-
* wrapped
|
|
14
|
-
*
|
|
15
|
-
* plaintext — vault.key file (JSON, mode 0o600). For development only.
|
|
16
|
-
* Emits console.warn at boot. Opt-out only.
|
|
21
|
+
* Modes (`wrapped` is the default; `plaintext` is opt-out with an
|
|
22
|
+
* explicit boot warning per the framework's modernity stance):
|
|
17
23
|
*
|
|
18
|
-
*
|
|
24
|
+
* - `wrapped` — `vault.key.sealed` file, passphrase-derived AEAD
|
|
25
|
+
* wrap (Argon2id → SHAKE256 → XChaCha20-Poly1305).
|
|
26
|
+
* The plaintext keypair never lands on disk.
|
|
27
|
+
* - `plaintext` — `vault.key` JSON at mode `0o600`. Development only.
|
|
28
|
+
* Emits a `console.warn` at every boot.
|
|
19
29
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
30
|
+
* Two-API contract: bootstrap awaits `init()` once, and every other
|
|
31
|
+
* consumer (often at module-require time across hundreds of call
|
|
32
|
+
* sites) runs synchronously against the in-process key cache.
|
|
23
33
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* ```js
|
|
35
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
|
|
36
|
+
* var sealed = b.vault.seal("4111-1111-1111-1111");
|
|
37
|
+
* sealed.startsWith("vault:"); // → true
|
|
38
|
+
* b.vault.unseal(sealed); // → "4111-1111-1111-1111"
|
|
39
|
+
* ```
|
|
28
40
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the
|
|
41
|
+
* Rotating the KEK (passphrase change, sealed-blob refresh,
|
|
42
|
+
* hardware-token swap) is a separate primitive — `b.vaultRotate.rotate`
|
|
43
|
+
* walks every sealed column under the old keypair and re-seals it
|
|
44
|
+
* under the new one with batched commits and a round-trip verify.
|
|
45
|
+
* The vault module owns the in-process cache; the rotator owns the
|
|
46
|
+
* on-disk data sweep.
|
|
47
|
+
*
|
|
48
|
+
* ```js
|
|
49
|
+
* // Wrapped-mode bootstrap (first run): the vault generates an
|
|
50
|
+
* // ML-KEM-1024 + P-384 keypair, wraps it under the operator's
|
|
51
|
+
* // passphrase, and writes vault.key.sealed atomically.
|
|
52
|
+
* process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
|
|
53
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs" });
|
|
54
|
+
* b.vault.getMode(); // → "wrapped"
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Sealed-value format: `"vault:"` prefix + base64 envelope produced
|
|
58
|
+
* by `b.crypto.encrypt`. Old envelopes always remain readable
|
|
59
|
+
* (envelope versioning); new writes use whichever KEM / CIPHER / KDF
|
|
60
|
+
* the active framework version pins as default.
|
|
61
|
+
*
|
|
62
|
+
* @card
|
|
63
|
+
* Sealed keystore that anchors every other framework subsystem holding secrets at rest: db field encryption, encrypted session storage, audit-log signing keys, OAuth refresh tokens, anything that flows through `b.vault.seal` / `b.vault.unseal`.
|
|
32
64
|
*/
|
|
33
65
|
var fs = require("fs");
|
|
34
66
|
var path = require("path");
|
|
@@ -102,6 +134,40 @@ function _readOrCreateDerivedHashSalt() {
|
|
|
102
134
|
}
|
|
103
135
|
|
|
104
136
|
var _cachedDerivedHashSalt = null;
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.vault.getDerivedHashSalt
|
|
139
|
+
* @signature b.vault.getDerivedHashSalt()
|
|
140
|
+
* @since 0.8.42
|
|
141
|
+
* @related b.vault.init, b.vault.seal
|
|
142
|
+
*
|
|
143
|
+
* Returns the 32-byte per-deployment salt used by crypto-field's
|
|
144
|
+
* derivedHash columns. The salt is generated once on first init,
|
|
145
|
+
* persisted at `vault.derived-hash-salt` (mode `0o600`) inside
|
|
146
|
+
* `dataDir`, and read back on subsequent boots. It survives vault
|
|
147
|
+
* KEK rotations — different file from `vault.key.sealed` — so
|
|
148
|
+
* indexed-lookup determinism for derivedHash columns holds across a
|
|
149
|
+
* passphrase change.
|
|
150
|
+
*
|
|
151
|
+
* Why per-deployment: pre-v0.8.42 the deterministic
|
|
152
|
+
* `sha3(namespace + plaintext)` shape allowed cross-deployment
|
|
153
|
+
* rainbow tables and cross-table correlation between deployments
|
|
154
|
+
* sharing a namespace. Binding a 32-byte salt closes that class
|
|
155
|
+
* without losing the determinism inside a single deployment that
|
|
156
|
+
* makes the index lookup possible.
|
|
157
|
+
*
|
|
158
|
+
* Throws `VaultError("vault/not-initialized")` if `init()` has not
|
|
159
|
+
* been awaited yet. Throws `vault/derived-hash-salt-corrupted` if
|
|
160
|
+
* the on-disk file exists but is not exactly 32 bytes.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
164
|
+
* var salt = b.vault.getDerivedHashSalt();
|
|
165
|
+
* salt.length; // → 32
|
|
166
|
+
* Buffer.isBuffer(salt); // → true
|
|
167
|
+
*
|
|
168
|
+
* // Same value on every call within a process — cached.
|
|
169
|
+
* b.vault.getDerivedHashSalt() === salt; // → true
|
|
170
|
+
*/
|
|
105
171
|
function getDerivedHashSalt() {
|
|
106
172
|
if (_cachedDerivedHashSalt === null) {
|
|
107
173
|
_cachedDerivedHashSalt = _readOrCreateDerivedHashSalt();
|
|
@@ -111,6 +177,55 @@ function getDerivedHashSalt() {
|
|
|
111
177
|
|
|
112
178
|
// ---- Init dispatch ----
|
|
113
179
|
|
|
180
|
+
/**
|
|
181
|
+
* @primitive b.vault.init
|
|
182
|
+
* @signature b.vault.init(opts)
|
|
183
|
+
* @since 0.1.0
|
|
184
|
+
* @related b.vault.seal, b.vault.unseal, b.vault.getMode, b.vaultRotate.rotate
|
|
185
|
+
*
|
|
186
|
+
* Bootstraps the vault. Call once at application startup before any
|
|
187
|
+
* code path that reads sealed values from the database, opens the
|
|
188
|
+
* encrypted session store, or signs audit-log entries. Subsequent
|
|
189
|
+
* calls after a successful init are no-ops, so guard-rail wrappers
|
|
190
|
+
* that re-call `init()` from worker entry points are safe.
|
|
191
|
+
*
|
|
192
|
+
* Mode dispatch:
|
|
193
|
+
*
|
|
194
|
+
* - `wrapped` (default) — if `vault.key.sealed` exists, prompts for the
|
|
195
|
+
* passphrase via `b.vaultPassphraseSource` and unwraps. If neither
|
|
196
|
+
* sealed nor plaintext file is present, generates a fresh keypair
|
|
197
|
+
* and wraps it under a freshly-prompted passphrase.
|
|
198
|
+
* - `plaintext` — reads `vault.key` if present, generates a fresh
|
|
199
|
+
* keypair and writes it at mode `0o600` otherwise. Logs a `WARNING`
|
|
200
|
+
* line at every boot.
|
|
201
|
+
*
|
|
202
|
+
* Refuses to guess when both `vault.key` and `vault.key.sealed` exist
|
|
203
|
+
* in `dataDir`, or when the requested mode mismatches the on-disk
|
|
204
|
+
* shape (sealed file present but `mode: "plaintext"` requested, or
|
|
205
|
+
* vice versa). Throws a `VaultError` in either case so the bootstrap
|
|
206
|
+
* exits cleanly instead of silently picking one.
|
|
207
|
+
*
|
|
208
|
+
* @opts
|
|
209
|
+
* {
|
|
210
|
+
* dataDir: string, // required — directory holding vault.key /
|
|
211
|
+
* // vault.key.sealed / derived-hash-salt
|
|
212
|
+
* mode: string, // "wrapped" (default) | "plaintext"
|
|
213
|
+
* }
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* // Wrapped-mode bootstrap with passphrase from the env var
|
|
217
|
+
* // b.vaultPassphraseSource consults by default.
|
|
218
|
+
* process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
|
|
219
|
+
* await b.vault.init({
|
|
220
|
+
* dataDir: "/var/lib/blamejs",
|
|
221
|
+
* mode: "wrapped",
|
|
222
|
+
* });
|
|
223
|
+
* b.vault.getMode(); // → "wrapped"
|
|
224
|
+
*
|
|
225
|
+
* // Re-calling init() after a successful boot is a no-op.
|
|
226
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs" });
|
|
227
|
+
* b.vault.getMode(); // → "wrapped"
|
|
228
|
+
*/
|
|
114
229
|
async function init(opts) {
|
|
115
230
|
if (initialized) return;
|
|
116
231
|
opts = opts || {};
|
|
@@ -296,6 +411,43 @@ function _requireInit() {
|
|
|
296
411
|
}
|
|
297
412
|
}
|
|
298
413
|
|
|
414
|
+
/**
|
|
415
|
+
* @primitive b.vault.seal
|
|
416
|
+
* @signature b.vault.seal(plaintext)
|
|
417
|
+
* @since 0.1.0
|
|
418
|
+
* @related b.vault.unseal, b.vaultRotate.rotate
|
|
419
|
+
*
|
|
420
|
+
* Synchronously encrypts `plaintext` under the in-process keypair and
|
|
421
|
+
* returns a `"vault:"`-prefixed string suitable for storage in any
|
|
422
|
+
* column declared sealed in the field-crypto schema. Called from
|
|
423
|
+
* hundreds of call sites across a typical application — keep it sync.
|
|
424
|
+
*
|
|
425
|
+
* Idempotent on already-sealed input: a value that already starts
|
|
426
|
+
* with the vault prefix is returned unchanged so seal-on-write paths
|
|
427
|
+
* survive code that re-seals the same row twice. Empty / falsy input
|
|
428
|
+
* passes through verbatim — there's nothing to encrypt and the
|
|
429
|
+
* caller likely meant `null` to land in the column.
|
|
430
|
+
*
|
|
431
|
+
* Throws `VaultError("vault/not-initialized")` if `init()` has not
|
|
432
|
+
* been awaited yet — the seal/unseal API is sync, but the keypair
|
|
433
|
+
* cache it consults is populated by the async init.
|
|
434
|
+
*
|
|
435
|
+
* Sealed values from this primitive decrypt regardless of which
|
|
436
|
+
* row / column / table they came from. Use `b.vault.aad.seal` for
|
|
437
|
+
* AEAD-bound seals when copy-paste between rows is part of the
|
|
438
|
+
* threat model.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
442
|
+
* var sealed = b.vault.seal("4111-1111-1111-1111");
|
|
443
|
+
* sealed.indexOf("vault:"); // → 0
|
|
444
|
+
*
|
|
445
|
+
* // Idempotent: re-sealing returns the input unchanged.
|
|
446
|
+
* b.vault.seal(sealed) === sealed; // → true
|
|
447
|
+
*
|
|
448
|
+
* // Falsy input is passed through verbatim.
|
|
449
|
+
* b.vault.seal("") === ""; // → true
|
|
450
|
+
*/
|
|
299
451
|
function seal(plaintext) {
|
|
300
452
|
if (!plaintext) return plaintext;
|
|
301
453
|
if (String(plaintext).startsWith(VAULT_PREFIX)) return plaintext;
|
|
@@ -305,6 +457,37 @@ function seal(plaintext) {
|
|
|
305
457
|
});
|
|
306
458
|
}
|
|
307
459
|
|
|
460
|
+
/**
|
|
461
|
+
* @primitive b.vault.unseal
|
|
462
|
+
* @signature b.vault.unseal(value)
|
|
463
|
+
* @since 0.1.0
|
|
464
|
+
* @related b.vault.seal
|
|
465
|
+
*
|
|
466
|
+
* Synchronously decrypts a `"vault:"`-prefixed string produced by
|
|
467
|
+
* `b.vault.seal` and returns the plaintext. Idempotent on
|
|
468
|
+
* non-sealed input: a value that does not start with the vault
|
|
469
|
+
* prefix is returned unchanged so read paths that select a column
|
|
470
|
+
* before knowing whether it's sealed don't have to branch.
|
|
471
|
+
*
|
|
472
|
+
* The envelope inside the prefix is versioned — values sealed under
|
|
473
|
+
* older KEM / KDF / cipher choices remain readable across framework
|
|
474
|
+
* upgrades. New seals always use the active algorithm set, so a
|
|
475
|
+
* full read-write cycle migrates a row forward.
|
|
476
|
+
*
|
|
477
|
+
* Throws `VaultError("vault/not-initialized")` if `init()` has not
|
|
478
|
+
* been awaited yet. Throws on AEAD-tag failure (corrupted ciphertext,
|
|
479
|
+
* wrong keypair) — operators rotating keys validate the rotation
|
|
480
|
+
* via `b.vaultRotate.verify` rather than catching here.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
484
|
+
* var sealed = b.vault.seal("hello");
|
|
485
|
+
* b.vault.unseal(sealed); // → "hello"
|
|
486
|
+
*
|
|
487
|
+
* // Non-sealed input passes through unchanged.
|
|
488
|
+
* b.vault.unseal("plain-string"); // → "plain-string"
|
|
489
|
+
* b.vault.unseal(null); // → null
|
|
490
|
+
*/
|
|
308
491
|
function unseal(value) {
|
|
309
492
|
if (!value || !String(value).startsWith(VAULT_PREFIX)) return value;
|
|
310
493
|
_requireInit();
|
|
@@ -314,15 +497,94 @@ function unseal(value) {
|
|
|
314
497
|
});
|
|
315
498
|
}
|
|
316
499
|
|
|
500
|
+
/**
|
|
501
|
+
* @primitive b.vault.getKeysJson
|
|
502
|
+
* @signature b.vault.getKeysJson()
|
|
503
|
+
* @since 0.6.0
|
|
504
|
+
* @related b.vault.init, b.vaultRotate.rotate
|
|
505
|
+
*
|
|
506
|
+
* Returns the in-process keypair as a pretty-printed JSON string —
|
|
507
|
+
* the same shape that lives on disk for `mode: "plaintext"` and
|
|
508
|
+
* inside the wrapped envelope for `mode: "wrapped"`. Used by the
|
|
509
|
+
* rotation pipeline to feed `oldKeys` into a fresh
|
|
510
|
+
* `b.vaultRotate.rotate({ oldKeys, newKeys, ... })` call without
|
|
511
|
+
* round-tripping through disk.
|
|
512
|
+
*
|
|
513
|
+
* The returned JSON has four properties: `publicKey`, `privateKey`
|
|
514
|
+
* (ML-KEM-1024), `ecPublicKey`, `ecPrivateKey` (P-384). Operators
|
|
515
|
+
* routing this through structured logging or telemetry must redact
|
|
516
|
+
* — these are the production keys, not metadata.
|
|
517
|
+
*
|
|
518
|
+
* Throws `VaultError("vault/not-initialized")` if `init()` has not
|
|
519
|
+
* been awaited yet.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
523
|
+
* var json = b.vault.getKeysJson();
|
|
524
|
+
* var keys = JSON.parse(json);
|
|
525
|
+
* Object.keys(keys).sort().join(",");
|
|
526
|
+
* // → "ecPrivateKey,ecPublicKey,privateKey,publicKey"
|
|
527
|
+
*/
|
|
317
528
|
function getKeysJson() {
|
|
318
529
|
_requireInit();
|
|
319
530
|
return JSON.stringify(keys, null, 2);
|
|
320
531
|
}
|
|
321
532
|
|
|
533
|
+
/**
|
|
534
|
+
* @primitive b.vault.getCurrentPassphrase
|
|
535
|
+
* @signature b.vault.getCurrentPassphrase()
|
|
536
|
+
* @since 0.6.0
|
|
537
|
+
* @related b.vault.init, b.vaultPassphraseOps.changePassphrase, b.vaultRotate.rotate
|
|
538
|
+
*
|
|
539
|
+
* Returns the Buffer holding the passphrase the vault was unsealed
|
|
540
|
+
* with on this boot, or `null` for `mode: "plaintext"` and for
|
|
541
|
+
* any future scenario where the vault was bootstrapped without
|
|
542
|
+
* one. Used by passphrase-rotation flows that re-wrap the keypair
|
|
543
|
+
* under a fresh passphrase without prompting the operator twice.
|
|
544
|
+
*
|
|
545
|
+
* The Buffer is already in the JS heap during unwrap; retaining it
|
|
546
|
+
* does not change the threat model meaningfully and is what makes
|
|
547
|
+
* `b.vaultPassphraseOps.changePassphrase` ergonomic. Operators
|
|
548
|
+
* concerned about heap residency rotate the passphrase and let the
|
|
549
|
+
* old Buffer get zeroed and replaced.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* process.env.BLAMEJS_VAULT_PASSPHRASE = "S0meStrongPassphr@se!";
|
|
553
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
|
|
554
|
+
* var pass = b.vault.getCurrentPassphrase();
|
|
555
|
+
* Buffer.isBuffer(pass); // → true
|
|
556
|
+
* pass.toString("utf8"); // → "S0meStrongPassphr@se!"
|
|
557
|
+
*
|
|
558
|
+
* // Plaintext mode never holds a passphrase.
|
|
559
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
560
|
+
* b.vault.getCurrentPassphrase(); // → null
|
|
561
|
+
*/
|
|
322
562
|
function getCurrentPassphrase() {
|
|
323
563
|
return currentPassphrase;
|
|
324
564
|
}
|
|
325
565
|
|
|
566
|
+
/**
|
|
567
|
+
* @primitive b.vault.getMode
|
|
568
|
+
* @signature b.vault.getMode()
|
|
569
|
+
* @since 0.6.0
|
|
570
|
+
* @related b.vault.init
|
|
571
|
+
*
|
|
572
|
+
* Returns the active vault mode: `"wrapped"`, `"plaintext"`, or
|
|
573
|
+
* `null` before `init()` has been awaited. Useful from health-check
|
|
574
|
+
* endpoints that surface a deployment-posture badge ("plaintext mode
|
|
575
|
+
* — DEV ONLY") or refuse to start the public listener until the
|
|
576
|
+
* vault is in `wrapped` mode in production.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* b.vault.getMode(); // → null (pre-init)
|
|
580
|
+
*
|
|
581
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
|
|
582
|
+
* b.vault.getMode(); // → "wrapped"
|
|
583
|
+
*
|
|
584
|
+
* if (process.env.NODE_ENV === "production" && b.vault.getMode() !== "wrapped") {
|
|
585
|
+
* throw new Error("refusing to start: vault must be in wrapped mode");
|
|
586
|
+
* }
|
|
587
|
+
*/
|
|
326
588
|
function getMode() {
|
|
327
589
|
return currentMode;
|
|
328
590
|
}
|
|
@@ -90,6 +90,72 @@ var DEFAULT_POLL_MS = 500;
|
|
|
90
90
|
// opts.maxSourceBytes.
|
|
91
91
|
var DEFAULT_MAX_SOURCE_BYTES = C.BYTES.mib(1);
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @primitive b.vault.sealPemFile
|
|
95
|
+
* @signature b.vault.sealPemFile(opts)
|
|
96
|
+
* @since 0.8.42
|
|
97
|
+
* @related b.vault.seal, b.vault.init, b.vaultRotate.rotate
|
|
98
|
+
*
|
|
99
|
+
* Watches a plaintext PEM file (typically certbot's
|
|
100
|
+
* `/etc/letsencrypt/live/<domain>/privkey.pem` after an ACME renewal)
|
|
101
|
+
* and re-seals it to a destination path under the vault keypair on
|
|
102
|
+
* every mtime / size change. Closes the renewal-window gap where a
|
|
103
|
+
* fresh PEM lives unencrypted on disk between certbot's write and
|
|
104
|
+
* the next operator-driven re-seal.
|
|
105
|
+
*
|
|
106
|
+
* Crash-safe write protocol: write `<destination>.tmp` at mode
|
|
107
|
+
* `0o600`, fsync, create a `<destination>.rewriting` marker, atomic
|
|
108
|
+
* rename, fsync the destination directory, remove the marker. If
|
|
109
|
+
* the framework crashes between marker create and marker remove,
|
|
110
|
+
* the next `sealPemFile()` start re-seals from source idempotently.
|
|
111
|
+
*
|
|
112
|
+
* Refuses to seal in place (source === destination), refuses to
|
|
113
|
+
* follow a symlinked source (TOCTOU defense), and refuses when the
|
|
114
|
+
* destination's parent directory is group- or other-writable on
|
|
115
|
+
* POSIX. Source size is capped (`maxSourceBytes`, default 1 MiB)
|
|
116
|
+
* so an attacker with write access to source can't OOM the host
|
|
117
|
+
* with a 10 GiB file.
|
|
118
|
+
*
|
|
119
|
+
* Returns a watcher handle: `start` (auto-called by the constructor
|
|
120
|
+
* unless overridden), `stop`, `forceReseal({ actorId, reason })`,
|
|
121
|
+
* plus read-only `generation` / `lastResealedAt` / `lastError` /
|
|
122
|
+
* `watching` properties.
|
|
123
|
+
*
|
|
124
|
+
* @opts
|
|
125
|
+
* {
|
|
126
|
+
* source: string, // plaintext PEM path (required)
|
|
127
|
+
* destination: string, // sealed-output path (required, must differ from source)
|
|
128
|
+
* audit: boolean, // emit b.audit events on every reseal (default true)
|
|
129
|
+
* pollInterval: number, // fs.watchFile cadence in ms (default 500)
|
|
130
|
+
* onResealed: function, // (info) => void — { srcPath, destPath, bytes, resealedAt, generation }
|
|
131
|
+
* onError: function, // (err) => void — sealing failed
|
|
132
|
+
* maxSourceBytes: number, // refuse source larger than this (default 1 MiB)
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "wrapped" });
|
|
137
|
+
*
|
|
138
|
+
* var watcher = b.vault.sealPemFile({
|
|
139
|
+
* source: "/etc/letsencrypt/live/example.com/privkey.pem",
|
|
140
|
+
* destination: "/var/lib/blamejs/server.key.sealed",
|
|
141
|
+
* pollInterval: b.constants.TIME.seconds(2),
|
|
142
|
+
* onResealed: function (info) {
|
|
143
|
+
* console.log("resealed", info.bytes, "bytes, gen", info.generation);
|
|
144
|
+
* },
|
|
145
|
+
* onError: function (err) {
|
|
146
|
+
* console.error("reseal failed:", err.message);
|
|
147
|
+
* },
|
|
148
|
+
* });
|
|
149
|
+
*
|
|
150
|
+
* watcher.generation; // → 1 (initial seal completed)
|
|
151
|
+
* typeof watcher.lastResealedAt; // → "number"
|
|
152
|
+
*
|
|
153
|
+
* // Force a reseal after a manual ACME renewal — captured in audit.
|
|
154
|
+
* watcher.forceReseal({ actorId: "ops-bot", reason: "manual-renewal" });
|
|
155
|
+
*
|
|
156
|
+
* // Stop watching at shutdown.
|
|
157
|
+
* watcher.stop();
|
|
158
|
+
*/
|
|
93
159
|
function sealPemFile(opts) {
|
|
94
160
|
opts = opts || {};
|
|
95
161
|
validateOpts(opts, [
|