Preview: Variablen verschluesselt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:53:41 +00:00
1 parent e358514240
commit 20ca1de694
2 files changed
+152

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
// Preview-Variablen: Verschluesselung (AES-256-GCM), Validierung, Log-Maskierung
import crypto from 'node:crypto'
const NAME_RE = /^[A-Z_][A-Z0-9_]*$/
const MAX_VARS = 30
const MAX_BYTES = 4096
// Schluessel aus PREVIEW_ENV_KEY (base64, genau 32 Byte) oder null
export function envKey() {
const raw = process.env.PREVIEW_ENV_KEY
if (!raw) return null
const buf = Buffer.from(raw, 'base64')
return buf.length === 32 ? buf : null
}
export function encryptEnv(obj, key) {
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const ct = Buffer.concat([cipher.update(JSON.stringify(obj || {}), 'utf8'), cipher.final()])
const tag = cipher.getAuthTag()
return ['v1', iv, tag, ct].map((p) => (typeof p === 'string' ? p : p.toString('base64'))).join(':')
}
export function decryptEnv(str, key) {
if (!str) return {}
const parts = String(str).split(':')
if (parts.length !== 4 || parts[0] !== 'v1') throw new Error('Ungueltiges Variablenformat')
const [, iv, tag, ct] = parts.map((p, i) => (i === 0 ? p : Buffer.from(p, 'base64')))
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(tag)
const plain = Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8')
return JSON.parse(plain)
}
export function envNames(str, key) {
return Object.keys(decryptEnv(str, key)).sort()
}
export function validateEnvChange(current, { set = {}, remove = [] } = {}) {
const env = { ...(current || {}) }
for (const name of remove || []) delete env[name]
for (const [name, value] of Object.entries(set || {})) {
if (!NAME_RE.test(name)) {
return { ok: false, error: `Ungültiger Variablenname „${name}“ (erlaubt: Großbuchstaben, Ziffern, Unterstrich; nicht mit Ziffer beginnen).` }
}
if (typeof value !== 'string') {
return { ok: false, error: `Der Wert von ${name} muss Text sein.` }
}
if (Buffer.byteLength(value, 'utf8') > MAX_BYTES) {
return { ok: false, error: `Der Wert von ${name} ist zu lang (maximal ${MAX_BYTES} Byte).` }
}
if (/[\0\r\n]/.test(value)) {
return { ok: false, error: `Der Wert von ${name} darf keine Zeilenumbrüche oder Nullzeichen enthalten.` }
}
env[name] = value
}
if (Object.keys(env).length > MAX_VARS) {
return { ok: false, error: `Zu viele Variablen (maximal ${MAX_VARS}).` }
}
return { ok: true, env }
}
// Werte (>= 4 Zeichen) im Protokoll durch ••• ersetzen, laengste zuerst
export function maskLog(text, env) {
let out = String(text ?? '')
const values = Object.values(env || {})
.filter((v) => typeof v === 'string' && v.length >= 4)
.sort((a, b) => b.length - a.length)
for (const v of values) out = out.split(v).join('•••')
return out
}