Files
ProxLedger/src/lib/tokens.ts
T
florian.wildeandClaude Opus 5 bef51eb863 feat(api): Agent-API mit Enrollment, Report und Ping (#004)
Gehashte Tokens mit Praefix, Rate-Limit je IP und Token, zod-Validierung mit
durchgaengig optionalen Feldern, idempotente Reports ueber reportId, Key- und
Server-ID-Historie sowie automatischer Laufzeit-Vorschlag. 32 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:39:33 +02:00

49 lines
1.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
/**
* API-Tokens für Agents. In der Datenbank liegt ausschließlich der SHA-256-Hash:
* Die Tokens haben volle Entropie (32 Byte Zufall), deshalb ist kein langsames
* Passwort-Hashing nötig – SHA-256 erlaubt zudem das direkte Nachschlagen per Index.
*/
export const PRAEFIX_ENROLLMENT = "plx_e_";
export const PRAEFIX_HOST = "plx_h_";
export interface ErzeugterToken {
/** Vollständiger Token im Klartext – wird nur einmal ausgegeben. */
klartext: string;
hash: string;
/** Erkennbarer Anfang für Anzeige und Protokoll, z. B. `plx_h_9f3a2b`. */
praefix: string;
}
export function erzeugeToken(typ: "enrollment" | "host"): ErzeugterToken {
const kennung = typ === "enrollment" ? PRAEFIX_ENROLLMENT : PRAEFIX_HOST;
const zufall = randomBytes(32).toString("base64url");
const klartext = `${kennung}${zufall}`;
return {
klartext,
hash: hashToken(klartext),
praefix: klartext.slice(0, kennung.length + 6),
};
}
export function hashToken(klartext: string): string {
return createHash("sha256").update(klartext.trim()).digest("hex");
}
/** Vergleicht zwei Hashes ohne zeitliche Seitenkanäle. */
export function hashGleich(a: string, b: string): boolean {
const links = Buffer.from(a, "hex");
const rechts = Buffer.from(b, "hex");
if (links.length !== rechts.length || links.length === 0) return false;
return timingSafeEqual(links, rechts);
}
/** Liest den Bearer-Token aus dem Authorization-Header. */
export function tokenAusHeader(header: string | null): string | null {
if (!header) return null;
const treffer = /^Bearer\s+(.+)$/i.exec(header.trim());
return treffer ? treffer[1].trim() : null;
}