fix(db): fallback load STORAGE_ENCRYPTION_KEY from env files (#11614)

Merged via /merge-batch (lote 2026-08-26 batch 2, v3.8.51). Encontrei um bug real na validação: o guard `NODE_ENV==="test"||VITEST` não cobre o test runner nativo do projeto (`node --import tsx/esm --test`, que não seta NODE_ENV), então o fallback estava lendo `~/.omniroute/.env` — o .env de PRODUÇÃO real do operador — durante qualquer execução de teste sem esse env var. Corrigi usando o `isTestContext()` já existente e mais robusto em `dataPaths.ts` (cobre `NODE_TEST_CONTEXT`/`--test` argv), e troquei o path hardcoded `os.homedir()/.omniroute` por `resolveDataDir()` para respeitar `DATA_DIR` quando configurado. Enviei a correção para o seu branch antes do merge. Validado: 7/7 testes de `db-encryption.test.ts` passando. Obrigado pela contribuição — a ideia central (fallback do STORAGE_ENCRYPTION_KEY) é boa e necessária.
This commit is contained in:
Benson K B
2026-08-26 17:53:57 +05:30
committed by GitHub
parent ae8ab27294
commit 7b9b36836c
3 changed files with 56 additions and 4 deletions

View File

@@ -91,7 +91,7 @@ export function resolveDataDir({ isCloud = false }: { isCloud?: boolean } = {}):
* plus the AGENTS.md single-file command, which does NOT load
* `tests/_setup/isolateDataDir.ts`.
*/
function isTestContext(): boolean {
export function isTestContext(): boolean {
return (
process.env.NODE_ENV === "test" ||
!!process.env.VITEST ||

View File

@@ -76,6 +76,43 @@ function decryptFailureSignature(
const RECOVERY_HINT =
"Re-authenticate this account, or verify STORAGE_ENCRYPTION_KEY matches the key used to store it.";
import fs from "fs";
import path from "path";
import os from "os";
import { isTestContext, resolveDataDir } from "../dataPaths.ts";
function ensureSecretLoaded(): string | undefined {
if (isTestContext()) {
return process.env.STORAGE_ENCRYPTION_KEY;
}
if (process.env.STORAGE_ENCRYPTION_KEY) {
return process.env.STORAGE_ENCRYPTION_KEY;
}
const candidates = [
path.join(resolveDataDir(), ".env"),
path.join(process.cwd(), ".env"),
path.join(os.homedir(), ".hermes", ".env"),
];
for (const envPath of candidates) {
try {
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, "utf8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("STORAGE_ENCRYPTION_KEY=")) {
const val = trimmed.split("=", 2)[1]?.trim().replace(/^["'](.*)["']$/, "$1");
if (val) {
process.env.STORAGE_ENCRYPTION_KEY = val;
return val;
}
}
}
}
} catch {}
}
return undefined;
}
/**
* Derive the PRIMARY encryption key using the static salt.
* This is the canonical key derivation that all new encryptions use.
@@ -84,7 +121,7 @@ const RECOVERY_HINT =
function getStaticKey(): Buffer | null {
if (_staticKey !== null) return _staticKey;
const secret = process.env.STORAGE_ENCRYPTION_KEY;
const secret = ensureSecretLoaded();
if (!secret || typeof secret !== "string" || secret.trim().length === 0) return null;
try {
@@ -110,7 +147,7 @@ function getStaticKey(): Buffer | null {
function getLegacyDynamicKey(): Buffer | null {
if (_legacyDynamicKey !== null) return _legacyDynamicKey;
const secret = process.env.STORAGE_ENCRYPTION_KEY;
const secret = ensureSecretLoaded();
if (!secret || typeof secret !== "string" || secret.trim().length === 0) return null;
const dynamicSalt = createHash("sha256").update(secret).digest().slice(0, 16);
@@ -124,7 +161,7 @@ function getLegacyDynamicKey(): Buffer | null {
/** Check if encryption is enabled. */
export function isEncryptionEnabled(): boolean {
return !!process.env.STORAGE_ENCRYPTION_KEY;
return !!ensureSecretLoaded();
}
/**

View File

@@ -123,3 +123,18 @@ test("legacy encryption migration parses ciphertext in canonical payload order",
assert.match(migrated.value, /^enc:v1:/);
assert.equal(encryption.decrypt(migrated.value), "legacy-provider-token");
});
test("ensureSecretLoaded loads key from .env file when process.env.STORAGE_ENCRYPTION_KEY is unset", async () => {
delete process.env.STORAGE_ENCRYPTION_KEY;
const originalNodeEnv = process.env.NODE_ENV;
try {
delete process.env.NODE_ENV;
const encryption = await importFresh("src/lib/db/encryption.ts");
// Under non-test NODE_ENV, if a local .env exists with a key, it discovers it
const enabled = encryption.isEncryptionEnabled();
assert.equal(typeof enabled, "boolean");
} finally {
process.env.NODE_ENV = originalNodeEnv;
}
});