b46dbbe889
- Astro 5 SSR (node standalone) + React, OKLCH tokens (no tailwind) - migrations/001_init.sql: full schema per 03-datenmodell-api - lib: db+migrations, crypto (AES-256-GCM), auth (argon2+signed session, ratelimit), storage (S3/MinIO, presigned URLs), openrouter (POST /v1/images, cost) - middleware: init-once + session guard + admin gate; /api/health (db+storage) - login + studio placeholder; seeds (till/lea, default recipes); Dockerfile - verified: astro build passes
38 lines
1.4 KiB
TypeScript
38 lines
1.4 KiB
TypeScript
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
|
|
// AES-256-GCM. ENCRYPTION_KEY = 64 Hex-Zeichen (32 Byte).
|
|
function key(): Buffer {
|
|
const hex = process.env.ENCRYPTION_KEY || '';
|
|
if (hex.length !== 64) {
|
|
throw new Error('ENCRYPTION_KEY muss 64 Hex-Zeichen (32 Byte) lang sein');
|
|
}
|
|
return Buffer.from(hex, 'hex');
|
|
}
|
|
|
|
/** Verschlüsselt Klartext → "iv.tag.ciphertext" (base64url-Teile). */
|
|
export function encrypt(plain: string): string {
|
|
const iv = randomBytes(12);
|
|
const cipher = createCipheriv('aes-256-gcm', key(), iv);
|
|
const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
return [iv, tag, enc].map((b) => b.toString('base64url')).join('.');
|
|
}
|
|
|
|
export function decrypt(payload: string): string {
|
|
const [ivB, tagB, dataB] = payload.split('.');
|
|
const decipher = createDecipheriv('aes-256-gcm', key(), Buffer.from(ivB, 'base64url'));
|
|
decipher.setAuthTag(Buffer.from(tagB, 'base64url'));
|
|
return Buffer.concat([
|
|
decipher.update(Buffer.from(dataB, 'base64url')),
|
|
decipher.final(),
|
|
]).toString('utf8');
|
|
}
|
|
|
|
/** Maskiert einen Schlüssel für die Anzeige: sk-or-…4f2a */
|
|
export function maskSecret(secret: string): string {
|
|
if (!secret) return '';
|
|
const tail = secret.slice(-4);
|
|
const head = secret.slice(0, Math.min(5, secret.length));
|
|
return `${head}…${tail}`;
|
|
}
|