feat: Sprachnotizen-API (T7 L2 Task 7)

Upload (Audio nur per Magic Bytes, versiegelt, 2 MiB), Text-Weg,
Liste, Abruf, Wiederholen, Verwerfen, alle verwerfen, Konfiguration und
Zähler unter /voice-captures. Rechte voice-captures:use plus
inspections:create (eigener Guard, API-Schlüssel ausgeschlossen),
Zähler nur mit system:manage; jede Abfrage auf den Besitzer begrenzt.
Grenzen je Benutzer unter Advisory-Lock (10 offen, 30 je Tag, 10 je
60 s), Wiederholen ebenfalls begrenzt und gedrosselt, Audio für QUEUED
nur unter 24 h. Verwerfen löscht Inhalt und Claim sofort. Keine Inhalte
in Log oder Audit. Wächter gegen Sprachinhalte in Suche, Export und
Berichten erweitert.

Refs #27

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 10:10:54 +02:00
1 parent 19550d1394
commit 0b0bc3c6dc
21 files changed
+2843 -12

No files matched your search

@@ -1,4 +1,4 @@
import { matchesFileSignature } from './file-signature';
import { detectAudioType, matchesFileSignature } from './file-signature';
describe('matchesFileSignature', () => {
it('accepts a buffer starting with the PNG signature', () => {
@@ -47,3 +47,63 @@ describe('matchesFileSignature', () => {
);
});
});
describe('detectAudioType', () => {
it('knows EBML, OggS and ftyp and nothing else', () => {
const webm = Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x9f, 0x42]);
const ogg = Buffer.concat([Buffer.from('OggS', 'ascii'), Buffer.alloc(8)]);
const mp4 = Buffer.concat([
Buffer.from([0x00, 0x00, 0x00, 0x20]),
Buffer.from('ftypM4A ', 'ascii'),
]);
expect(detectAudioType(webm)).toBe('audio/webm');
expect(detectAudioType(ogg)).toBe('audio/ogg');
expect(detectAudioType(mp4)).toBe('audio/mp4');
expect(
detectAudioType(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a])),
).toBeNull();
expect(detectAudioType(Buffer.from('RIFF....WAVE', 'ascii'))).toBeNull();
expect(detectAudioType(Buffer.from('%PDF-1.7', 'ascii'))).toBeNull();
});
it.each(['heic', 'avif', 'mif1', 'qt ', '3gp4'])(
'rejects an ftyp file with the brand %s',
(brand) => {
const file = Buffer.concat([
Buffer.from([0, 0, 0, 0x20]),
Buffer.from(`ftyp${brand}`, 'latin1'),
]);
expect(detectAudioType(file)).toBeNull();
},
);
it.each(['M4A ', 'isom', 'iso2', 'iso6', 'mp41', 'mp42', 'dash'])(
'accepts the audio brand %s',
(brand) => {
const file = Buffer.concat([
Buffer.from([0, 0, 0, 0x20]),
Buffer.from(`ftyp${brand}`, 'latin1'),
]);
expect(detectAudioType(file)).toBe('audio/mp4');
},
);
it('rejects buffers that are too short for any signature', () => {
expect(detectAudioType(Buffer.alloc(0))).toBeNull();
expect(detectAudioType(Buffer.from([0x1a, 0x45, 0xdf]))).toBeNull();
expect(
detectAudioType(Buffer.from('OggS', 'ascii').subarray(0, 3)),
).toBeNull();
expect(
detectAudioType(Buffer.from([0, 0, 0, 0x20, 0x66, 0x74])),
).toBeNull();
});
it('does not take a signature from the middle of the buffer', () => {
const late = Buffer.concat([
Buffer.alloc(16),
Buffer.from('OggS', 'ascii'),
]);
expect(detectAudioType(late)).toBeNull();
});
});