feat: Schlüssel und gemeinsamer Kern für Sprachnotizen (T7 L2 Tasks 5+6)
Task 5: Schlüsselbund-Zweck voice (VOICE_ENCRYPTION_KEY), AES-256-GCM- Hilfen für Text und Bytes mit AAD capture:<id>:<art>, voice-config (Modell, Tageslimit, Aufnahme, Whisper-Grösse) und anthropic-config (nie geloggt, nur worker/all), Startprüfung verlangt den Schlüssel in Produktion in jeder Rolle, rekey versiegelt offene Sprachnotizen neu (Audio einzeln gelesen, CAS mit Status), Secret-Vorlagen und ensure-secrets.sh ergänzt. Task 6: Text-Normalisierung und Bereinigung (Steuer- und Formatzeichen, Codepoint-Kürzung), Zitatprüfung mit Dezimalzahlen als ein Wort (Ruling 7), Felderliste passend zu CreateInspectionDto, versiegeltes Transkript mit Grenzen, Fachwörter und Skalen mit Paritätstests. Rollout: Ein Image mit diesem Stand startet in Produktion nur mit VOICE_ENCRYPTION_KEY in apicore-backend-secret (ARCHITECTURE Annahme 22). Gates: Backend tsc, eslint, 922 Unit-Tests, e2e 100 Suites / 1004 Tests. Refs #27 #28 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
2a4dc868d9
commit
9795f98006
27 files changed
+2212
-31
No files matched your search
@@ -6,6 +6,7 @@ const KEYS = {
|
||||
MFA_ENCRYPTION_KEY: 'm'.repeat(32),
|
||||
WEBHOOK_ENCRYPTION_KEY: 'w'.repeat(32),
|
||||
BACKUP_ENCRYPTION_KEY: 'b'.repeat(32),
|
||||
VOICE_ENCRYPTION_KEY: 'v'.repeat(32),
|
||||
};
|
||||
const PROD_ENV = {
|
||||
...BASE_ENV,
|
||||
@@ -81,16 +82,17 @@ describe('assertStartupEnvironment', () => {
|
||||
});
|
||||
|
||||
describe('encryption keys', () => {
|
||||
it('production refuses to start without MFA_/WEBHOOK_/BACKUP_ENCRYPTION_KEY', () => {
|
||||
it('production refuses to start without MFA_/WEBHOOK_/BACKUP_/VOICE_ENCRYPTION_KEY', () => {
|
||||
expect(() =>
|
||||
assertStartupEnvironment('worker', {
|
||||
...PROD_ENV,
|
||||
MFA_ENCRYPTION_KEY: undefined,
|
||||
WEBHOOK_ENCRYPTION_KEY: undefined,
|
||||
BACKUP_ENCRYPTION_KEY: undefined,
|
||||
VOICE_ENCRYPTION_KEY: undefined,
|
||||
}),
|
||||
).toThrow(
|
||||
'Missing required environment variable(s): MFA_ENCRYPTION_KEY, WEBHOOK_ENCRYPTION_KEY, BACKUP_ENCRYPTION_KEY',
|
||||
'Missing required environment variable(s): MFA_ENCRYPTION_KEY, WEBHOOK_ENCRYPTION_KEY, BACKUP_ENCRYPTION_KEY, VOICE_ENCRYPTION_KEY',
|
||||
);
|
||||
expect(() =>
|
||||
assertStartupEnvironment('job', {
|
||||
@@ -126,7 +128,7 @@ describe('assertStartupEnvironment', () => {
|
||||
WEBHOOK_ENCRYPTION_KEY: KEYS.MFA_ENCRYPTION_KEY,
|
||||
}),
|
||||
).toThrow(
|
||||
'MFA_ENCRYPTION_KEY, WEBHOOK_ENCRYPTION_KEY and BACKUP_ENCRYPTION_KEY must be pairwise different',
|
||||
'MFA_ENCRYPTION_KEY, WEBHOOK_ENCRYPTION_KEY, BACKUP_ENCRYPTION_KEY and VOICE_ENCRYPTION_KEY must be pairwise different',
|
||||
);
|
||||
expect(() =>
|
||||
assertStartupEnvironment('api', {
|
||||
@@ -198,9 +200,11 @@ describe('assertStartupEnvironment', () => {
|
||||
'MFA_ENCRYPTION_KEY',
|
||||
'WEBHOOK_ENCRYPTION_KEY',
|
||||
'BACKUP_ENCRYPTION_KEY',
|
||||
'VOICE_ENCRYPTION_KEY',
|
||||
'MFA_ENCRYPTION_KEY_PREVIOUS',
|
||||
'WEBHOOK_ENCRYPTION_KEY_PREVIOUS',
|
||||
'BACKUP_ENCRYPTION_KEY_PREVIOUS',
|
||||
'VOICE_ENCRYPTION_KEY_PREVIOUS',
|
||||
])('production refuses a change-me placeholder in %s', (name) => {
|
||||
expect(() =>
|
||||
assertStartupEnvironment('api', { ...PROD_ENV, [name]: PLACEHOLDER }),
|
||||
@@ -360,4 +364,70 @@ describe('assertStartupEnvironment', () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('voice', () => {
|
||||
it('production requires VOICE_ENCRYPTION_KEY distinct from the others', () => {
|
||||
expect(() =>
|
||||
assertStartupEnvironment('api', {
|
||||
...PROD_ENV,
|
||||
VOICE_ENCRYPTION_KEY: undefined,
|
||||
}),
|
||||
).toThrow(
|
||||
'Missing required environment variable(s): VOICE_ENCRYPTION_KEY',
|
||||
);
|
||||
for (const other of [
|
||||
'MFA_ENCRYPTION_KEY',
|
||||
'WEBHOOK_ENCRYPTION_KEY',
|
||||
'BACKUP_ENCRYPTION_KEY',
|
||||
] as const) {
|
||||
expect(() =>
|
||||
assertStartupEnvironment('job', {
|
||||
...PROD_ENV,
|
||||
VOICE_ENCRYPTION_KEY: KEYS[other],
|
||||
}),
|
||||
).toThrow('must be pairwise different');
|
||||
}
|
||||
expect(() =>
|
||||
assertStartupEnvironment('worker', {
|
||||
...PROD_ENV,
|
||||
VOICE_ENCRYPTION_KEY: PROD_ENV.JWT_REFRESH_SECRET,
|
||||
}),
|
||||
).toThrow(
|
||||
'VOICE_ENCRYPTION_KEY must differ from JWT_ACCESS_SECRET and JWT_REFRESH_SECRET',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a change-me VOICE_ENCRYPTION_KEY in production', () => {
|
||||
expect(() =>
|
||||
assertStartupEnvironment('api', {
|
||||
...PROD_ENV,
|
||||
VOICE_ENCRYPTION_KEY: 'change-me-to-a-fifth-random-value-32-chars',
|
||||
}),
|
||||
).toThrow(
|
||||
'Secret(s) still set to an example placeholder: VOICE_ENCRYPTION_KEY',
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses an unknown VOICE_MODEL in every role', () => {
|
||||
for (const role of ['api', 'worker', 'job', 'all'] as const) {
|
||||
expect(() =>
|
||||
assertStartupEnvironment(role, {
|
||||
...PROD_ENV,
|
||||
VOICE_MODEL: 'claude-unknown',
|
||||
}),
|
||||
).toThrow('VOICE_MODEL must be one of');
|
||||
}
|
||||
});
|
||||
|
||||
it('a bad ANTHROPIC_API_KEY never stops a pod', () => {
|
||||
for (const role of ['api', 'worker', 'job', 'all'] as const) {
|
||||
expect(() =>
|
||||
assertStartupEnvironment(role, {
|
||||
...PROD_ENV,
|
||||
ANTHROPIC_API_KEY: 'change-me-anthropic',
|
||||
}),
|
||||
).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
readRingSecrets,
|
||||
} from '../common/crypto/key-ring';
|
||||
import { assertTelegramToken } from '../telegram/telegram-config';
|
||||
import { assertAnthropicKey } from '../voice/anthropic-config';
|
||||
import { voiceConfig } from '../voice/voice-config';
|
||||
import type { ApicoreRole } from './role';
|
||||
|
||||
const REQUIRED_ENV_VARS = ['DATABASE_URL', 'JWT_ACCESS_SECRET'] as const;
|
||||
@@ -132,8 +134,9 @@ function assertEncryptionKeys(env: NodeJS.ProcessEnv): void {
|
||||
/**
|
||||
* Startup checks shared by every entry point (API, worker, CLI job). All
|
||||
* roles load the same modules and secrets today, so the checks hardly differ
|
||||
* per role; the Telegram bot token is the first role-specific one (only the
|
||||
* worker holds it).
|
||||
* per role; the Telegram bot token and the Anthropic key are role-specific
|
||||
* (only the worker holds them, both optional). The voice settings are
|
||||
* validated in every role: a mistyped model id stops the pod, not a night job.
|
||||
*/
|
||||
export function assertStartupEnvironment(
|
||||
role: ApicoreRole,
|
||||
@@ -144,4 +147,6 @@ export function assertStartupEnvironment(
|
||||
assertNoPlaceholderSecrets(env);
|
||||
assertEncryptionKeys(env);
|
||||
assertTelegramToken(role, env);
|
||||
voiceConfig(env);
|
||||
assertAnthropicKey(role, env);
|
||||
}
|
||||
Reference in new issue
Block a user