mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-15 10:43:36 -06:00
Competitor pressure (Mandoe 'AI Magic Create'): prompt -> signage. We answer it in a way that's actually BETTER for signage and costs the operator nothing. Key idea: don't generate raw images (AI garbles text - fatal for menus/promos). The LLM returns a STRUCTURED design spec (headline, supporting text, accent shapes, palette) that the existing Designer renders with real fonts - crisp and fully editable. Reuses the whole Designer. BYOK, fully under the customer's control: each workspace configures its own OpenAI-COMPATIBLE endpoint + key - OpenAI cloud OR self-hosted (Ollama / LM Studio / llama.cpp). Operator bears zero AI cost/liability. - server/lib/secretbox.js: AES-256-GCM for the key at rest (never returned). - routes/ai.js: GET/PUT /api/ai/settings (admin; key write-only) + POST /generate-design (editor+). Output is strictly validated/normalized (cap count, clamp ranges, px->%, strip HTML, validate colors) - never trust the model. SSRF guard: hosted instances block private/internal targets; self-hosted (the whole point of local AI) may point at localhost/LAN. - Designer: an 'AI generate' panel (prompt + Generate) + a settings modal. Verified end-to-end against local Ollama (llama3.1:8b): prompt -> editable design on the canvas. Unit tests cover normalization + the SSRF guard. Suite 61/61. Phase 2 (next): AI background images (OpenAI images / AUTOMATIC1111).
32 lines
1.2 KiB
JavaScript
32 lines
1.2 KiB
JavaScript
'use strict';
|
|
|
|
// AES-256-GCM encrypt/decrypt for secrets at rest (e.g. BYOK AI provider keys,
|
|
// #41). The key is derived from the instance's JWT secret so there's no extra
|
|
// env to manage; rotating JWT_SECRET invalidates stored secrets (they're
|
|
// re-enterable). Format: base64(iv[12] | tag[16] | ciphertext).
|
|
const crypto = require('crypto');
|
|
const config = require('../config');
|
|
|
|
const KEY = crypto.createHash('sha256').update(String(config.jwtSecret) + ':secretbox-v1').digest();
|
|
|
|
function encrypt(plain) {
|
|
if (plain == null || plain === '') return null;
|
|
const iv = crypto.randomBytes(12);
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv);
|
|
const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
|
return Buffer.concat([iv, cipher.getAuthTag(), enc]).toString('base64');
|
|
}
|
|
|
|
function decrypt(b64) {
|
|
if (!b64) return null;
|
|
try {
|
|
const buf = Buffer.from(b64, 'base64');
|
|
const iv = buf.subarray(0, 12), tag = buf.subarray(12, 28), enc = buf.subarray(28);
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv);
|
|
decipher.setAuthTag(tag);
|
|
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
|
|
} catch { return null; }
|
|
}
|
|
|
|
module.exports = { encrypt, decrypt };
|