feat(rotation): gefuehrte Rotation mit Test vor dem Widerruf (#023)

Einen Schlüssel zu tauschen soll keinen Ausfall riskieren: Der Nachfolger
übernimmt alle aktiven Zuweisungen, wird ausgerollt und per
Verbindungstest geprüft – erst wenn alle Tests seit dem letzten Ausrollen
grün sind, entfernt der Widerruf aus #024 den alten Schlüssel, der dann
als ROTIERT endet.

Private Keys bekommen einen neuen Pfad (-rJJJJMMTT) bei gleichem Alias.
Damit ssh den neuen nimmt, schreibt privat.ts je Alias nur noch einen
config-Eintrag; die jüngste Zuweisung gewinnt. Der Worker setzt
vorgaengerId schon beim Erzeugen, so ist die Kette auch nach einem
Abbruch vollständig und die Schrittfolge wiederaufnehmbar.

Personen-Keys ohne Private Key in sshkv bekommen nur einen Hinweis:
dafür braucht es einen neuen Public Key von der Person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:30:39 +02:00
co-authored by Claude Opus 5.5
parent a2d4f8428a
commit f6ed66fc7e
17 changed files with 1368 additions and 11 deletions
+37
View File
@@ -0,0 +1,37 @@
/**
* Namens- und Pfadregeln der gefuehrten Rotation (#023). Rein, damit testbar.
*/
const NAME_SUFFIX = /\s*\(rotiert \d{2}\.\d{2}\.\d{4}\)$/;
const PFAD_SUFFIX = /-r\d{8}(?:-\d+)?$/;
/** TT.MM.JJJJ und JJJJMMTT nach dem Kalender in Berlin. */
export function rotationsDatum(jetzt: Date = new Date()): { anzeige: string; kompakt: string } {
const [jahr, monat, tag] = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Berlin" })
.format(jetzt)
.split("-");
return { anzeige: `${tag}.${monat}.${jahr}`, kompakt: `${jahr}${monat}${tag}` };
}
/**
* „Backup A → B (rotiert 12.10.2026)“ – ein frueheres Rotations-Suffix wird ersetzt, damit
* die Namen bei jeder Rotation nicht laenger werden.
*/
export function rotationsName(name: string, datum: string, maxLaenge = 200): string {
const suffix = ` (rotiert ${datum})`;
const basis = name.replace(NAME_SUFFIX, "").trim();
return `${basis.slice(0, Math.max(1, maxLaenge - suffix.length))}${suffix}`;
}
/**
* Neuer Pfad fuer den Private Key des Nachfolgers: gleicher Name mit `-rJJJJMMTT`. Ein alter
* Suffix wird ersetzt; ist der Pfad (auch durch eine fruehere, entfernte Zuweisung) belegt,
* kommt eine laufende Nummer dazu. Zwei Dateien nebeneinander sind gewollt: bis der Nachfolger
* getestet ist, bleibt der alte Schluessel benutzbar.
*/
export function rotationsPfad(pfad: string, datumKompakt: string, belegt: ReadonlySet<string>): string {
const basis = pfad.replace(PFAD_SUFFIX, "");
let kandidat = `${basis}-r${datumKompakt}`;
for (let nummer = 2; belegt.has(kandidat); nummer += 1) kandidat = `${basis}-r${datumKompakt}-${nummer}`;
return kandidat;
}