Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub. Warum so: - Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil nur er den Master-Key hat. Passphrase und importierter Private Key reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach. - Ein reiner Public Key ist kein Geheimnis und wird direkt im Web angelegt, mit verstaendlicher Meldung bei einem Duplikat. - Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist, verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf Schluessel ausserhalb des Workers ein festes select. - Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das Loeschen des Private Keys; CHECKs fuer den Slug (landet in Dateinamen) und "geloescht nur ohne Chiffrat". - Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||
import path from "node:path";
|
||
import { describe, expect, it } from "vitest";
|
||
import { SCHLUESSEL_DETAIL_SELECT, SCHLUESSEL_LISTE_SELECT } from "@/server/queries/schluessel";
|
||
|
||
/**
|
||
* Strukturell (#012): Jede Prisma-Abfrage auf `Schluessel` ausserhalb des Workers hat ein
|
||
* festes `select`. Ohne `select` liefert Prisma die ganze Zeile – samt Chiffrat –, ohne dass
|
||
* der Feldname im Code steht; tests/geheimnisse-grenze.test.ts saehe das nicht.
|
||
*/
|
||
function dateien(verzeichnis: string): string[] {
|
||
return readdirSync(verzeichnis).flatMap((name) => {
|
||
const pfad = path.join(verzeichnis, name);
|
||
if (statSync(pfad).isDirectory()) return name === "generated" ? [] : dateien(pfad);
|
||
return /\.(ts|tsx)$/.test(name) ? [pfad.split(path.sep).join("/")] : [];
|
||
});
|
||
}
|
||
|
||
/** Der Worker darf (und muss) die ganze Zeile sehen; die Grenze dort zieht geheimnisse.ts. */
|
||
const WEB = dateien("src").filter((d) => !d.startsWith("src/worker/"));
|
||
|
||
/** Methoden, die Zeilen zurueckgeben – nur die brauchen ein `select`. */
|
||
const ZEILEN =
|
||
/\.schluessel\.(findMany|findFirst|findFirstOrThrow|findUnique|findUniqueOrThrow|create|update|upsert|delete)\(/g;
|
||
|
||
/** Argumenttext eines Aufrufs ab der oeffnenden Klammer, per Klammerzaehlung. */
|
||
function argumente(text: string, start: number): string {
|
||
let tiefe = 0;
|
||
for (let i = start; i < text.length; i++) {
|
||
if (text[i] === "(") tiefe++;
|
||
else if (text[i] === ")" && --tiefe === 0) return text.slice(start, i + 1);
|
||
}
|
||
return text.slice(start);
|
||
}
|
||
|
||
describe("Abfragen auf Schluessel im Web", () => {
|
||
it("findet Aufrufe", () => {
|
||
const anzahl = WEB.reduce((summe, d) => summe + [...readFileSync(d, "utf8").matchAll(ZEILEN)].length, 0);
|
||
expect(anzahl).toBeGreaterThan(5);
|
||
});
|
||
|
||
it("haben alle ein select", () => {
|
||
const verstoesse: string[] = [];
|
||
for (const datei of WEB) {
|
||
const text = readFileSync(datei, "utf8");
|
||
for (const treffer of text.matchAll(ZEILEN)) {
|
||
const start = treffer.index + treffer[0].length - 1;
|
||
if (!/\bselect\s*:/.test(argumente(text, start))) {
|
||
verstoesse.push(`${datei}: ${treffer[0]}`);
|
||
}
|
||
}
|
||
}
|
||
expect(verstoesse).toEqual([]);
|
||
});
|
||
|
||
it("holen Schluessel nie als ganze Relation (include/select: { schluessel: true })", () => {
|
||
// Zeilen mit `_count` zaehlen nur und sind erlaubt.
|
||
const verstoesse = WEB.filter((d) =>
|
||
readFileSync(d, "utf8")
|
||
.split("\n")
|
||
.some(
|
||
(zeile) =>
|
||
!zeile.includes("_count") &&
|
||
/\b(schluessel|vorgaenger|nachfolger|deploymentSchluessel|erstellteSchluessel)\s*:\s*true/.test(
|
||
zeile,
|
||
),
|
||
),
|
||
);
|
||
expect(verstoesse).toEqual([]);
|
||
});
|
||
|
||
it("die festen select-Listen nennen kein Chiffrat", () => {
|
||
for (const auswahl of [SCHLUESSEL_LISTE_SELECT, SCHLUESSEL_DETAIL_SELECT]) {
|
||
expect(Object.keys(auswahl)).not.toContain("privatVerschluesselt");
|
||
expect(Object.keys(auswahl)).not.toContain("dekVerschluesselt");
|
||
}
|
||
});
|
||
});
|