refactor: replace master.key file with ENCRYPTION_KEY env var and fix k8s deployment

- Replace file-based master key (data/master.key) with ENCRYPTION_KEY env var (hex-encoded)
- App now requires ENCRYPTION_KEY to start, removing MASTER_KEY_PATH entirely
- Fix k8s: add missing gitea-assistant-data volume, replace PVC with hostPath for single-node
- Fix k8s: change qdrant from StatefulSet+PVC to Deployment+hostPath
- Add K8s Secret for ENCRYPTION_KEY injection
- Update all tests, .env.example, and documentation
This commit is contained in:
jeffusion
2026-03-05 15:24:08 +08:00
committed by 路遥知码力
parent 9b063afba0
commit 0bc147cbc5
12 changed files with 129 additions and 167 deletions

View File

@@ -1,7 +1,7 @@
# 应用配置
PORT=3000
# DATABASE_PATH=./data/assistant.db # 可选,默认为 ./data/assistant.db
# MASTER_KEY_PATH=./data/master.key # 可选,默认为 ./data/master.key
ENCRYPTION_KEY= # 必填,运行 openssl rand -hex 32 生成
# 所有其他配置Gitea连接、飞书通知、Webhook密钥、管理员密码、审查引擎、记忆系统等
# 均通过 Web 管理后台进行配置。

View File

@@ -58,12 +58,14 @@ bun install
Create a `.env` file with only infrastructure-level settings:
```bash
# Server port (the only required setting)
# Server port
PORT=3000
# Optional: custom data paths (defaults shown)
# REQUIRED: encryption key for API key storage (generate with: openssl rand -hex 32)
ENCRYPTION_KEY=
# Optional: custom database path (default shown)
# DATABASE_PATH=./data/assistant.db
# MASTER_KEY_PATH=./data/master.key
```
> **All other configuration** (Gitea connection, webhook secret, admin password, review engine, Feishu, memory settings, etc.) is managed through the **Admin Dashboard Web UI** at `http://your-server:3000`. On first boot, all settings are seeded with secure defaults automatically.
@@ -102,7 +104,7 @@ Only infrastructure-level settings that must be known before the database is ini
|----------|-------------|---------|
| `PORT` | Server port | `5174` |
| `DATABASE_PATH` | SQLite database file path | `./data/assistant.db` |
| `MASTER_KEY_PATH` | Encryption master key file path | `./data/master.key` |
| `ENCRYPTION_KEY` | **Required.** AES-256-GCM encryption key for API key storage (64 hex chars). Generate with `openssl rand -hex 32` | — |
### Web UI Configuration (Admin Dashboard)

View File

@@ -58,12 +58,14 @@ bun install
创建 `.env` 文件,仅填写基础设施级别的配置:
```bash
# 服务端口(唯一必需的配置项)
# 服务端口
PORT=3000
# 可选:自定义数据路径(以下为默认值
# 必填API Key 加密存储密钥(运行 openssl rand -hex 32 生成
ENCRYPTION_KEY=
# 可选:自定义数据库路径(以下为默认值)
# DATABASE_PATH=./data/assistant.db
# MASTER_KEY_PATH=./data/master.key
```
> **所有其他配置**Gitea 连接、Webhook 密钥、管理员密码、审查引擎、飞书、记忆系统等)均通过 **Web 管理后台** 在 `http://your-server:3000` 进行配置。首次启动时,所有设置会自动以安全的默认值进行初始化。
@@ -102,7 +104,7 @@ bun run start # 生产模式
|------|------|--------|
| `PORT` | 服务端口 | `5174` |
| `DATABASE_PATH` | SQLite 数据库文件路径 | `./data/assistant.db` |
| `MASTER_KEY_PATH` | 加密密钥文件路径 | `./data/master.key` |
| `ENCRYPTION_KEY` | **必填。** AES-256-GCM 加密密钥,用于加密存储 API Key64 位十六进制字符串)。运行 `openssl rand -hex 32` 生成 | — |
### Web 界面配置(管理后台)

View File

@@ -30,7 +30,7 @@
| **UI-Only 配置** | 所有业务配置仅通过 Web 管理后台设置,不再有环境变量覆盖层(仅保留极少数启动参数如 `PORT``WEBHOOK_SECRET``DATABASE_PATH` |
| **4 Provider 并存** | `openai_compatible`(现有兼容格式)、`openai_responses`Responses API`anthropic`Messages API`gemini`generateContent API |
| **SQLite 持久化** | 使用 `bun:sqlite` 零依赖,单文件 `data/assistant.db` |
| **密钥应用层加密** | API Key 使用 AES-256-GCM 加密后存 DB主密钥来自本地文件 `data/master.key`(首次启动自动生成,权限 600 |
| **密钥应用层加密** | API Key 使用 AES-256-GCM 加密后存 DB主密钥通过环境变量 `ENCRYPTION_KEY` 传入hex 编码64 字符 = 32 字节),未设置则拒绝启动 |
| **不做向前兼容** | 旧 JSON 配置文件方案直接废弃,新版本仅支持数据库配置 |
### 开源参考
@@ -170,7 +170,7 @@ CREATE INDEX idx_providers_enabled ON llm_providers(is_enabled);
| `llm_providers.type` | 决定使用哪个 adapter 实现 |
| `llm_providers.base_url` | `openai_compatible` 类型必填(用户自建代理地址);其他类型可选覆盖官方默认 endpoint |
| `llm_providers.extra_config` | JSON 字段,存放 provider 特有参数。例如 Gemini 的 `projectId`、OpenAI 的 `organization`、Anthropic 的 `anthropic-version` header 等 |
| `llm_secrets.key_version` | 用于密钥轮换:当 `master.key` 更新后,启动时批量重加密所有 `key_version < current` 的记录 |
| `llm_secrets.key_version` | 用于密钥轮换:当 `ENCRYPTION_KEY` 更新后,启动时批量重加密所有 `key_version < current` 的记录 |
| `model_role_assignments.role` | 业务角色枚举,对应代码中不同调用场景 |
| `system_settings.is_sensitive` | 为 1 时 value 字段存密文(复用 `crypto/secrets.ts`GET API 返回 masked |
@@ -612,11 +612,10 @@ export function toGeminiTools(tools: LLMToolDefinition[]): object[];
```
启动流程:
1. 检查 data/master.key 是否存在
├── 不存在 → crypto.randomBytes(32) 生成
│ 写入文件chmod 600仅 owner 读写)
日志输出: "Generated new master key at data/master.key"
└── 存在 → 读取 32 bytes
1. 读取环境变量 ENCRYPTION_KEYhex 编码64 字符)
├── 未设置或为空 → 抛出错误,拒绝启动
├── 长度不正确 → 抛出错误,提示需要 64 个十六进制字符
└── 正确 → 解码为 32 字节 Buffer
2. 主密钥常驻内存(进程生命周期)
3. 绝对不写入日志、不暴露给 API
```
@@ -647,11 +646,11 @@ export function toGeminiTools(tools: LLMToolDefinition[]): object[];
### 6.4 密钥轮换
```
场景: 管理员data/master.key
1. 启动时读取新 master key
场景: 管理员ENCRYPTION_KEY
1. 启动时读取新的 ENCRYPTION_KEY 环境变量
2. 查询所有 llm_secrets WHERE key_version < current_version
3. 逐条: 用旧 key 解密 → 用新 key 重加密 → 更新 key_version
4. 如果旧 key 不可用(文件丢失)→ 启动报错,要求重新设置所有 API Key
4. 如果旧 key 不可用(环境变量缺失)→ 启动报错,要求重新设置所有 API Key
```
---
@@ -798,7 +797,7 @@ Day 6.5: 旧代码清理完毕文档更新Ready for review
| **Anthropic 无原生 JSON mode** | `response_format: json_object` 不可用JSON 解析可能失败 | Adapter 内 prompt 注入 JSON 指令 + `JSON.parse()` 容错(正则提取 \`\`\`json\`\`\` 块 → 重试 parse |
| **Gemini function calling 格式差异大** | `functionDeclarations` 包装层级不同;`functionResponse` 嵌套在 `parts` 中 | `tool-converter.ts` 单独处理finish reason 映射表全覆盖测试 |
| **Embedding 维度变化导致 Qdrant 不兼容** | `src/review/memory/vector-store.ts` 硬编码 1536 维 | `model_role_assignments.role='embedding'` 变更时UI 提示用户需重建 collection或自动检测维度创建新 collection |
| **master.key 丢失** | 所有加密的 API Key 不可恢复 | 启动时检测密钥版本不匹配 → 报错并要求重新设置所有 API Keytrade-off安全性 > 便利性) |
| **ENCRYPTION_KEY 丢失** | 所有加密的 API Key 不可恢复 | 启动时检测密钥版本不匹配 → 报错并要求重新设置所有 API Keytrade-off安全性 > 便利性) |
| **SQLite 并发写** | 多请求同时写入可能 SQLITE_BUSY | `bun:sqlite` 开启 WAL mode写操作走单连接序列化读可并行 |
| **Provider SDK 版本冲突** | `openai``@anthropic-ai/sdk``@google/generative-ai` 三个 SDK 共存 | 各 adapter 独立 import无交叉依赖`package.json` 锁定主版本 |
| **配置热更新** | UI 修改 provider 配置后,正在进行的审查仍用旧配置 | Gateway 缓存按 provider_id 粒度 invalidate正在执行的请求不受影响用的是已创建的实例下次请求用新实例 |

View File

@@ -1,3 +1,18 @@
---
# Secret: sensitive configuration (create before deploying)
# Generate a 64-char hex key: openssl rand -hex 32
apiVersion: v1
kind: Secret
metadata:
name: gitea-assistant-secret
namespace: gitea-assistant
labels:
app.kubernetes.io/name: gitea-assistant
app.kubernetes.io/part-of: gitea-assistant
type: Opaque
stringData:
ENCRYPTION_KEY: "" # REQUIRED: run `openssl rand -hex 32` and paste here
---
# ConfigMap: only infrastructure-level env vars that must be known before DB init
apiVersion: v1
@@ -43,6 +58,8 @@ spec:
envFrom:
- configMapRef:
name: gitea-assistant-config
- secretRef:
name: gitea-assistant-secret
resources:
limits:
memory: "512Mi"
@@ -70,8 +87,10 @@ spec:
failureThreshold: 3
volumes:
- name: data
persistentVolumeClaim:
claimName: gitea-assistant-data
hostPath:
# Customize this path to match your node's storage layout
path: /opt/gitea-assistant/data
type: DirectoryOrCreate
---
apiVersion: v1

View File

@@ -1,22 +1,6 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qdrant-data
namespace: gitea-assistant
labels:
app.kubernetes.io/name: qdrant
app.kubernetes.io/part-of: gitea-assistant
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: StatefulSet
kind: Deployment
metadata:
name: qdrant
namespace: gitea-assistant
@@ -24,7 +8,6 @@ metadata:
app.kubernetes.io/name: qdrant
app.kubernetes.io/part-of: gitea-assistant
spec:
serviceName: qdrant
replicas: 1
selector:
matchLabels:
@@ -72,8 +55,10 @@ spec:
failureThreshold: 3
volumes:
- name: qdrant-storage
persistentVolumeClaim:
claimName: qdrant-data
hostPath:
# Customize this path to match your node's storage layout
path: /opt/gitea-assistant/qdrant
type: DirectoryOrCreate
---
apiVersion: v1

View File

@@ -34,10 +34,12 @@ function makeTmpDb(): string {
describe('ConfigManager (DB backend)', () => {
let dbPath: string;
const savedDbPath = process.env.DATABASE_PATH;
const savedEncryptionKey = process.env.ENCRYPTION_KEY;
beforeEach(() => {
dbPath = makeTmpDb();
process.env.DATABASE_PATH = dbPath;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
initMasterKey();
initDatabase();
});
@@ -49,6 +51,11 @@ describe('ConfigManager (DB backend)', () => {
} else {
process.env.DATABASE_PATH = savedDbPath;
}
if (savedEncryptionKey === undefined) {
delete process.env.ENCRYPTION_KEY;
} else {
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
try { if (existsSync(dbPath)) unlinkSync(dbPath); } catch { /* ok */ }
try { if (existsSync(`${dbPath}-wal`)) unlinkSync(`${dbPath}-wal`); } catch { /* ok */ }
try { if (existsSync(`${dbPath}-shm`)) unlinkSync(`${dbPath}-shm`); } catch { /* ok */ }

View File

@@ -59,18 +59,16 @@ async function jsonRequest(
describe('llm-config controller', () => {
let dbPath: string;
let keyPath: string;
let app: Hono;
const savedDbPath = process.env.DATABASE_PATH;
const savedKeyPath = process.env.MASTER_KEY_PATH;
const savedEncryptionKey = process.env.ENCRYPTION_KEY;
beforeEach(() => {
const tmpDir = join(tmpdir(), `ctrl-test-${randomUUID()}`);
mkdirSync(tmpDir, { recursive: true });
dbPath = join(tmpDir, 'test.db');
keyPath = join(tmpDir, 'master.key');
process.env.DATABASE_PATH = dbPath;
process.env.MASTER_KEY_PATH = keyPath;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
initMasterKey();
initDatabase();
@@ -84,10 +82,10 @@ describe('llm-config controller', () => {
} else {
process.env.DATABASE_PATH = savedDbPath;
}
if (savedKeyPath === undefined) {
delete process.env.MASTER_KEY_PATH;
if (savedEncryptionKey === undefined) {
delete process.env.ENCRYPTION_KEY;
} else {
process.env.MASTER_KEY_PATH = savedKeyPath;
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
try {
if (existsSync(dbPath)) unlinkSync(dbPath);
@@ -104,11 +102,6 @@ describe('llm-config controller', () => {
} catch {
/* ok */
}
try {
if (existsSync(keyPath)) unlinkSync(keyPath);
} catch {
/* ok */
}
});
// ─── Provider CRUD ────────────────────────────────────────────────

View File

@@ -10,10 +10,6 @@ declare module 'bun:test' {
// @ts-expect-error bun:test is provided by Bun at runtime
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { randomUUID } from 'node:crypto';
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
/**
* Import a fresh secrets module to bypass module cache (same pattern as config-manager.test.ts).
@@ -23,76 +19,58 @@ async function importFresh() {
return await import(`../../crypto/secrets.ts?t=${Date.now()}-${randomUUID()}`);
}
/** Generate a valid 64-char hex key for tests */
function generateHexKey(): string {
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
}
describe('secrets — AES-256-GCM encryption', () => {
let tmpDir: string;
let keyPath: string;
const savedMasterKeyPath = process.env.MASTER_KEY_PATH;
let savedEncryptionKey: string | undefined;
beforeEach(() => {
tmpDir = join(tmpdir(), `secrets-test-${randomUUID()}`);
mkdirSync(tmpDir, { recursive: true });
keyPath = join(tmpDir, 'master.key');
process.env.MASTER_KEY_PATH = keyPath;
savedEncryptionKey = process.env.ENCRYPTION_KEY;
delete process.env.ENCRYPTION_KEY;
});
afterEach(() => {
if (savedMasterKeyPath === undefined) {
delete process.env.MASTER_KEY_PATH;
if (savedEncryptionKey === undefined) {
delete process.env.ENCRYPTION_KEY;
} else {
process.env.MASTER_KEY_PATH = savedMasterKeyPath;
}
// Cleanup temp files
try {
if (existsSync(keyPath)) unlinkSync(keyPath);
} catch {
/* ok */
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
});
// ─── Master Key Init ─────────────────────────────────────────────────
describe('initMasterKey()', () => {
test('generates a new 32-byte key file when none exists', async () => {
const secrets = await importFresh();
expect(existsSync(keyPath)).toBe(false);
test('loads key from ENCRYPTION_KEY env var (hex)', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
expect(existsSync(keyPath)).toBe(true);
const raw = readFileSync(keyPath);
expect(raw.length).toBe(32);
expect(secrets.isMasterKeyReady()).toBe(true);
});
test('loads an existing key file without overwriting', async () => {
// Pre-create a 32-byte key
const existingKey = Buffer.from(crypto.getRandomValues(new Uint8Array(32)));
writeFileSync(keyPath, existingKey, { mode: 0o600 });
test('throws if ENCRYPTION_KEY is not set', async () => {
delete process.env.ENCRYPTION_KEY;
const secrets = await importFresh();
secrets.initMasterKey();
const loaded = readFileSync(keyPath);
expect(Buffer.compare(loaded, existingKey)).toBe(0);
expect(secrets.isMasterKeyReady()).toBe(true);
expect(() => secrets.initMasterKey()).toThrow('ENCRYPTION_KEY env var is required');
});
test('throws if key file is wrong length', async () => {
writeFileSync(keyPath, Buffer.alloc(16)); // Wrong length
test('throws if ENCRYPTION_KEY is wrong length', async () => {
process.env.ENCRYPTION_KEY = 'abcd'; // Only 2 bytes
const secrets = await importFresh();
expect(() => secrets.initMasterKey()).toThrow('16 bytes, expected 32');
expect(() => secrets.initMasterKey()).toThrow('2 bytes');
});
test('creates parent directories if needed', async () => {
const nestedPath = join(tmpDir, 'nested', 'deep', 'master.key');
process.env.MASTER_KEY_PATH = nestedPath;
test('throws if ENCRYPTION_KEY is empty string', async () => {
process.env.ENCRYPTION_KEY = '';
const secrets = await importFresh();
secrets.initMasterKey();
expect(existsSync(nestedPath)).toBe(true);
expect(readFileSync(nestedPath).length).toBe(32);
expect(() => secrets.initMasterKey()).toThrow('ENCRYPTION_KEY env var is required');
});
});
@@ -105,6 +83,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('returns true after initMasterKey', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
expect(secrets.isMasterKeyReady()).toBe(true);
@@ -120,6 +99,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('roundtrip: encrypt then decrypt recovers plaintext', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -131,6 +111,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('encrypts empty string', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -139,6 +120,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('encrypts unicode content', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -148,6 +130,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('encrypts long content', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -157,6 +140,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('each encryption produces unique IV (different ciphertext)', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -172,6 +156,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('encrypted payload has expected structure', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -188,6 +173,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('tampered ciphertext fails decryption', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -199,6 +185,7 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('tampered auth tag fails decryption', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets = await importFresh();
secrets.initMasterKey();
@@ -209,14 +196,15 @@ describe('secrets — AES-256-GCM encryption', () => {
});
test('wrong master key fails decryption', async () => {
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets1 = await importFresh();
secrets1.initMasterKey();
const encrypted = secrets1.encrypt('secret');
// Create a different key
unlinkSync(keyPath);
// Use a different key
process.env.ENCRYPTION_KEY = generateHexKey();
const secrets2 = await importFresh();
secrets2.initMasterKey(); // Generates a new key
secrets2.initMasterKey();
expect(() => secrets2.decrypt(encrypted)).toThrow();
});

View File

@@ -2,14 +2,13 @@
* AES-256-GCM encryption module for API key storage.
*
* Master key lifecycle:
* 1. On first startup, generate 32 random bytes → write to DATA_DIR/master.key (chmod 600)
* 2. On subsequent startups, read existing master.key
* 1. Read `ENCRYPTION_KEY` env var (hex-encoded, 64 hex chars = 32 bytes)
* 2. If not set, throw — the app refuses to start without an explicit key
* 3. Key stays in memory for process lifetime; never logged or exposed via API
*
* Generate a key: `openssl rand -hex 32`
*/
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -17,7 +16,6 @@ import { dirname, resolve } from 'node:path';
const KEY_LENGTH = 32; // AES-256
const IV_LENGTH = 12; // GCM recommended nonce size
// ---------------------------------------------------------------------------
// Master Key Management
// ---------------------------------------------------------------------------
@@ -25,45 +23,28 @@ const IV_LENGTH = 12; // GCM recommended nonce size
let masterKey: Buffer | null = null;
/**
* Resolve the master key file path.
* Defaults to `data/master.key` relative to CWD, overridable via `MASTER_KEY_PATH` env.
*/
function getMasterKeyPath(): string {
return resolve(process.env.MASTER_KEY_PATH || './data/master.key');
}
/**
* Initialize (load or generate) the master encryption key.
* Initialize the master encryption key from `ENCRYPTION_KEY` env var.
* MUST be called once at application startup before any encrypt/decrypt operations.
*
* @throws If ENCRYPTION_KEY is not set or has wrong length
*/
export function initMasterKey(): void {
const keyPath = getMasterKeyPath();
if (existsSync(keyPath)) {
const raw = readFileSync(keyPath);
if (raw.length !== KEY_LENGTH) {
throw new Error(
`Master key at ${keyPath} is ${raw.length} bytes, expected ${KEY_LENGTH}. Delete the file and restart to generate a new key (all encrypted API keys will need to be re-entered).`
);
}
masterKey = Buffer.from(raw);
console.log(`🔑 Master key loaded from ${keyPath}`);
} else {
const dir = dirname(keyPath);
mkdirSync(dir, { recursive: true });
const key = Buffer.from(crypto.getRandomValues(new Uint8Array(KEY_LENGTH)));
writeFileSync(keyPath, key, { mode: 0o600 });
try {
chmodSync(keyPath, 0o600);
} catch {
// chmod may fail on some filesystems (e.g. Windows); non-fatal
}
masterKey = key;
console.log(`🔑 Generated new master key at ${keyPath}`);
const envKey = process.env.ENCRYPTION_KEY;
if (!envKey) {
throw new Error(
'ENCRYPTION_KEY env var is required but not set. Generate one with: openssl rand -hex 32'
);
}
const buf = Buffer.from(envKey, 'hex');
if (buf.length !== KEY_LENGTH) {
throw new Error(
`ENCRYPTION_KEY env var is ${buf.length} bytes (decoded from hex), expected ${KEY_LENGTH}. Provide exactly 64 hex characters.`
);
}
masterKey = buf;
console.log('🔑 Master key loaded from ENCRYPTION_KEY env var');
}
/**

View File

@@ -21,10 +21,9 @@ import { secretRepo } from '../repositories/secret-repo';
describe('secret-repo', () => {
let dbPath: string;
let keyPath: string;
let providerId: string;
const savedDbPath = process.env.DATABASE_PATH;
const savedKeyPath = process.env.MASTER_KEY_PATH;
const savedEncryptionKey = process.env.ENCRYPTION_KEY;
const providerInput: CreateProviderInput = {
name: 'Test Provider',
@@ -37,9 +36,8 @@ describe('secret-repo', () => {
const tmpDir = join(tmpdir(), `db-test-${randomUUID()}`);
mkdirSync(tmpDir, { recursive: true });
dbPath = join(tmpDir, 'test.db');
keyPath = join(tmpDir, 'master.key');
process.env.DATABASE_PATH = dbPath;
process.env.MASTER_KEY_PATH = keyPath;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
initMasterKey();
initDatabase();
@@ -55,10 +53,10 @@ describe('secret-repo', () => {
} else {
process.env.DATABASE_PATH = savedDbPath;
}
if (savedKeyPath === undefined) {
delete process.env.MASTER_KEY_PATH;
if (savedEncryptionKey === undefined) {
delete process.env.ENCRYPTION_KEY;
} else {
process.env.MASTER_KEY_PATH = savedKeyPath;
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
try {
if (existsSync(dbPath)) unlinkSync(dbPath);
@@ -75,11 +73,6 @@ describe('secret-repo', () => {
} catch {
/* ok */
}
try {
if (existsSync(keyPath)) unlinkSync(keyPath);
} catch {
/* ok */
}
});
// ─── Set ──────────────────────────────────────────────────────────

View File

@@ -23,11 +23,10 @@ import { LLMGateway } from '../gateway';
describe('LLMGateway', () => {
let dbPath: string;
let keyPath: string;
let gateway: LLMGateway;
let providerId: string;
const savedDbPath = process.env.DATABASE_PATH;
const savedKeyPath = process.env.MASTER_KEY_PATH;
const savedEncryptionKey = process.env.ENCRYPTION_KEY;
const providerInput: CreateProviderInput = {
name: 'Test OpenAI',
@@ -40,9 +39,8 @@ describe('LLMGateway', () => {
const tmpDir = join(tmpdir(), `gw-test-${randomUUID()}`);
mkdirSync(tmpDir, { recursive: true });
dbPath = join(tmpDir, 'test.db');
keyPath = join(tmpDir, 'master.key');
process.env.DATABASE_PATH = dbPath;
process.env.MASTER_KEY_PATH = keyPath;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
initMasterKey();
initDatabase();
@@ -62,10 +60,10 @@ describe('LLMGateway', () => {
} else {
process.env.DATABASE_PATH = savedDbPath;
}
if (savedKeyPath === undefined) {
delete process.env.MASTER_KEY_PATH;
if (savedEncryptionKey === undefined) {
delete process.env.ENCRYPTION_KEY;
} else {
process.env.MASTER_KEY_PATH = savedKeyPath;
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
try {
if (existsSync(dbPath)) unlinkSync(dbPath);
@@ -82,11 +80,6 @@ describe('LLMGateway', () => {
} catch {
/* ok */
}
try {
if (existsSync(keyPath)) unlinkSync(keyPath);
} catch {
/* ok */
}
});
// ─── chatForRole: Error Cases ──────────────────────────────────────