From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally

---
 packages/bibliothek-server/src/geheimnisse.ts |  127 ++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 127 insertions(+), 0 deletions(-)

diff --git a/packages/bibliothek-server/src/geheimnisse.ts b/packages/bibliothek-server/src/geheimnisse.ts
new file mode 100644
index 0000000..05ccd8b
--- /dev/null
+++ b/packages/bibliothek-server/src/geheimnisse.ts
@@ -0,0 +1,127 @@
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import { GEHEIMNIS_ORDNER } from './datenbank';
+
+/**
+ * Bau-Geheimnisse (API-Schlüssel, Tokens …) nach einheitlichem Muster.
+ *
+ * Standard — bewusst wie bei gängigen Bau-Systemen, damit die Bibliothek nichts
+ * über einzelne Tools wissen muss:
+ *
+ *  1. Das Repository deklariert in `software-bibliothek.json`, welche Geheimnisse
+ *     es braucht:  "secrets": [{ "name": "ANTHROPIC_API_KEY", "description": "…" }]
+ *  2. Der Wert wird in der Bibliothek hinterlegt (nur lokal am Server pflegbar).
+ *  3. Beim Bau wird er als **Umgebungsvariable** gesetzt. Was das Tool daraus macht
+ *     — Datei erzeugen, direkt einkompilieren — entscheidet sein eigenes Bau-Skript.
+ *
+ * Ablage getrennt vom Katalog und NIE im Software-Datensatz: `/api/software` ist aus
+ * dem Netzwerk lesbar. Auch der Ordner `secrets/` wird nirgends ausgeliefert —
+ * die Download-Route bedient ausschließlich `files/<id>/<datei>`.
+ */
+export interface Geheimniswert {
+  /** Name der Umgebungsvariablen, z. B. `ANTHROPIC_API_KEY`. */
+  name: string;
+  wert: string;
+}
+
+/** Was die Oberfläche zu sehen bekommt — nie der Wert selbst. */
+export interface GeheimnisUebersicht {
+  name: string;
+  hinterlegt: boolean;
+  zeichen: number;
+}
+
+function ablagePfad(softwareId: number): string {
+  return path.join(GEHEIMNIS_ORDNER, `${softwareId}.json`);
+}
+
+/**
+ * Schränkt die Zugriffsrechte auf den Geheimnis-Ordner ein.
+ *
+ * Nötig, weil Unterordner von %ProgramData% die Leseberechtigung für die
+ * Gruppe „Benutzer" erben — sonst könnte jeder lokal angemeldete Benutzer die
+ * hinterlegten Schlüssel lesen. Node's `mode` greift unter Windows nicht auf
+ * ACLs, deshalb über `icacls`. Bezeichnung über die bekannten SIDs, damit es
+ * unabhängig von der Systemsprache funktioniert.
+ */
+export function sichereGeheimnisOrdner(): void {
+  if (process.platform !== 'win32') {
+    return;
+  }
+  const dienstbenutzer = `${process.env.USERDOMAIN ?? ''}\\${process.env.USERNAME ?? ''}`;
+  const rechte = [
+    '*S-1-5-18:(OI)(CI)F', // SYSTEM
+    '*S-1-5-32-544:(OI)(CI)F', // Administratoren
+  ];
+  if (process.env.USERNAME) {
+    rechte.push(`${dienstbenutzer}:(OI)(CI)F`); // der Benutzer, unter dem der Dienst läuft
+  }
+  try {
+    execFileSync('icacls', [GEHEIMNIS_ORDNER, '/inheritance:r', '/grant:r', ...rechte], { stdio: 'pipe', windowsHide: true });
+  } catch (fehler) {
+    console.warn(`Zugriffsrechte des Schlüssel-Ordners konnten nicht eingeschränkt werden: ${String(fehler).slice(0, 200)}`);
+  }
+}
+
+export function ladeGeheimnisse(softwareId: number): Geheimniswert[] {
+  const pfad = ablagePfad(softwareId);
+  if (!fs.existsSync(pfad)) {
+    return [];
+  }
+  try {
+    const gelesen = JSON.parse(fs.readFileSync(pfad, 'utf8')) as Geheimniswert[];
+    return Array.isArray(gelesen) ? gelesen.filter((eintrag) => istGueltigerName(eintrag?.name)) : [];
+  } catch {
+    return [];
+  }
+}
+
+/** Nur Namen und Länge — der Wert verlässt den Server nicht. */
+export function uebersichtGeheimnisse(softwareId: number): GeheimnisUebersicht[] {
+  return ladeGeheimnisse(softwareId).map((eintrag) => ({
+    name: eintrag.name,
+    hinterlegt: eintrag.wert.length > 0,
+    zeichen: eintrag.wert.length,
+  }));
+}
+
+/**
+ * Speichert die Werte. Ein leerer Wert bedeutet „unverändert lassen" — so kann die
+ * Oberfläche die Liste zurückschicken, ohne die Geheimnisse je gesehen zu haben.
+ */
+export function speichereGeheimnisse(softwareId: number, eingehende: Geheimniswert[]): GeheimnisUebersicht[] {
+  const bisherige = new Map(ladeGeheimnisse(softwareId).map((eintrag) => [eintrag.name, eintrag.wert]));
+  const zusammengefuehrt: Geheimniswert[] = [];
+  for (const eintrag of eingehende) {
+    const name = (eintrag?.name ?? '').trim();
+    if (!istGueltigerName(name)) {
+      continue;
+    }
+    const neuerWert = String(eintrag.wert ?? '');
+    zusammengefuehrt.push({ name, wert: neuerWert || bisherige.get(name) || '' });
+  }
+  fs.mkdirSync(GEHEIMNIS_ORDNER, { recursive: true });
+  fs.writeFileSync(ablagePfad(softwareId), JSON.stringify(zusammengefuehrt, null, 2), { mode: 0o600 });
+  return zusammengefuehrt.map((eintrag) => ({ name: eintrag.name, hinterlegt: eintrag.wert.length > 0, zeichen: eintrag.wert.length }));
+}
+
+export function loescheGeheimnisse(softwareId: number): void {
+  fs.rmSync(ablagePfad(softwareId), { force: true });
+}
+
+/** Umgebungsvariablen für den Bau — nur Einträge mit hinterlegtem Wert. */
+export function alsUmgebung(geheimnisse: Geheimniswert[]): Record<string, string> {
+  const umgebung: Record<string, string> = {};
+  for (const eintrag of geheimnisse) {
+    if (eintrag.wert) {
+      umgebung[eintrag.name] = eintrag.wert;
+    }
+  }
+  return umgebung;
+}
+
+/** Nur was als Umgebungsvariable taugt — schützt zugleich vor Einschleusen. */
+function istGueltigerName(name: string | undefined): boolean {
+  return !!name && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
+}

--
Gitblit v1.9.3