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