Server-Bibliothek
vor 17 Std. 98bce1b0da675715b2e5482c0ad61ba21135f62f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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);
}