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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 09:41:47 +02:00
1 parent 2a4dc868d9
commit 9795f98006
27 files changed
+2212 -31

No files matched your search

@@ -0,0 +1,91 @@
import { Logger } from '@nestjs/common';
import { anthropicApiKey, assertAnthropicKey } from './anthropic-config';
const KEY = 'sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789';
function fakeLogger() {
return { warn: jest.fn(), error: jest.fn() } satisfies Pick<
Logger,
'warn' | 'error'
>;
}
describe('anthropicApiKey', () => {
it('returns a well-formed key, trimmed', () => {
expect(anthropicApiKey({ ANTHROPIC_API_KEY: ` ${KEY} ` })).toBe(KEY);
});
it.each([
['unset', {}],
['empty', { ANTHROPIC_API_KEY: ' ' }],
['a placeholder', { ANTHROPIC_API_KEY: 'change-me-anthropic-key' }],
['the wrong format', { ANTHROPIC_API_KEY: 'sk-ant-short' }],
['dummy', { ANTHROPIC_API_KEY: 'dummy' }],
])('returns null for %s', (_label, env) => {
expect(anthropicApiKey(env)).toBeNull();
});
});
describe('assertAnthropicKey', () => {
it('assertAnthropicKey never throws and disables on a placeholder', () => {
for (const value of ['change-me-anthropic-key', 'dummy', '', 'sk-ant-x']) {
const logger = fakeLogger();
expect(() =>
assertAnthropicKey(
'worker',
{ NODE_ENV: 'production', ANTHROPIC_API_KEY: value },
logger,
),
).not.toThrow();
}
const logger = fakeLogger();
assertAnthropicKey(
'worker',
{ NODE_ENV: 'production', ANTHROPIC_API_KEY: 'change-me-x' },
logger,
);
expect(logger.error).toHaveBeenCalledTimes(1);
expect(anthropicApiKey({ ANTHROPIC_API_KEY: 'change-me-x' })).toBeNull();
});
it('logs the variable name once, never the value', () => {
const logger = fakeLogger();
assertAnthropicKey(
'worker',
{ ANTHROPIC_API_KEY: 'sk-ant-short-secret' },
logger,
);
const output = JSON.stringify([
logger.warn.mock.calls,
logger.error.mock.calls,
]);
expect(output).toContain('ANTHROPIC_API_KEY');
expect(output).not.toContain('short-secret');
expect(logger.error).toHaveBeenCalledTimes(1);
});
it('api and job roles skip the Anthropic check', () => {
for (const role of ['api', 'job'] as const) {
const logger = fakeLogger();
assertAnthropicKey(role, { ANTHROPIC_API_KEY: 'garbage' }, logger);
expect(logger.error).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
}
});
it('accepts a good key silently in the worker and in all-in-one mode', () => {
for (const role of ['worker', 'all'] as const) {
const logger = fakeLogger();
assertAnthropicKey(role, { ANTHROPIC_API_KEY: KEY }, logger);
expect(logger.error).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
}
});
it('warns that extraction stays off when the worker has no key', () => {
const logger = fakeLogger();
assertAnthropicKey('worker', {}, logger);
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.error).not.toHaveBeenCalled();
});
});