Harden runtime config and input validation

This commit is contained in:
Remi
2025-09-18 13:39:16 +02:00
committed by itsrubberduck
parent 88497a3848
commit e17169655d
10 changed files with 295 additions and 36 deletions

View File

@@ -1,17 +1,18 @@
// yarn add openai dotenv
// yarn add openai
import OpenAI from "openai";
import dotenv from "dotenv";
import fs from "node:fs";
import { getServerRuntimeConfig } from "./runtimeConfig";
dotenv.config();
const { openaiKey, openaiProject, llmModel, ttsModel } = getServerRuntimeConfig();
const normalizeClientOptions: ConstructorParameters<typeof OpenAI>[0] = { apiKey: openaiKey };
if (openaiProject) {
normalizeClientOptions.project = openaiProject;
}
export const normalize = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
project: process.env.OPENAI_PROJECT, // optional
});
export const normalize = new OpenAI(normalizeClientOptions);
export const LLM_MODEL = process.env.LLM_MODEL || "gpt-5-nano";
export const TTS_MODEL = process.env.TTS_MODEL || "tts-1";
export const LLM_MODEL = llmModel;
export const TTS_MODEL = ttsModel;
/* =========================
LLM PROMPTS (überarbeitet)

View File

@@ -1,12 +1,43 @@
// server/utils/openai.ts
import OpenAI from 'openai'
import { getServerRuntimeConfig } from './runtimeConfig'
const MODEL = process.env.LLM_MODEL || 'gpt-5-nano'
export const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY!})
let openaiClient: OpenAI | null = null
let cachedModel: string | null = null
function ensureOpenAI(): OpenAI {
if (!openaiClient) {
const { openaiKey, openaiProject, llmModel } = getServerRuntimeConfig()
if (!openaiKey) {
throw new Error('OPENAI_API_KEY fehlt. Bitte den Schlüssel setzen, bevor KI-Funktionen genutzt werden.')
}
const clientOptions: ConstructorParameters<typeof OpenAI>[0] = { apiKey: openaiKey }
if (openaiProject) {
clientOptions.project = openaiProject
}
openaiClient = new OpenAI(clientOptions)
cachedModel = llmModel
}
return openaiClient
}
function getModel(): string {
if (!cachedModel) {
const { llmModel } = getServerRuntimeConfig()
cachedModel = llmModel
}
return cachedModel
}
export function getOpenAIClient(): OpenAI {
return ensureOpenAI()
}
export async function decide(system: string, user: string): Promise<string> {
const r = await openai.chat.completions.create({
model: MODEL,
const client = ensureOpenAI()
const model = getModel()
const r = await client.chat.completions.create({
model,
messages: [
{role: 'system', content: system},
{role: 'user', content: user}
@@ -146,8 +177,10 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
const user = JSON.stringify(optimizedInput)
try {
const r = await openai.chat.completions.create({
model: MODEL,
const client = ensureOpenAI()
const model = getModel()
const r = await client.chat.completions.create({
model,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: system },

View File

@@ -0,0 +1,83 @@
import { useRuntimeConfig } from '#imports'
export interface ServerRuntimeConfig {
openaiKey: string
openaiProject?: string
llmModel: string
ttsModel: string
voiceId: string
usePiper: boolean
piperPort: number
useSpeaches: boolean
speachesBaseUrl?: string
speechModelId: string
}
let cachedConfig: ServerRuntimeConfig | null = null
let warnedMissingOpenAIKey = false
function toBoolean(value: unknown, fallback = false): boolean {
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (!normalized) {
return fallback
}
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true
}
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false
}
}
return fallback
}
function toNumber(value: unknown, fallback: number): number {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number.parseInt(value, 10)
if (!Number.isNaN(parsed)) {
return parsed
}
}
return fallback
}
export function getServerRuntimeConfig(): ServerRuntimeConfig {
if (cachedConfig) {
return cachedConfig
}
const runtimeConfig = useRuntimeConfig()
const openaiKey = String(runtimeConfig.openaiKey || '').trim()
if (!openaiKey && !warnedMissingOpenAIKey) {
console.warn('[OpenSquawk] OPENAI_API_KEY fehlt. Einige KI-Funktionen stehen ohne Schlüssel nicht zur Verfügung.')
warnedMissingOpenAIKey = true
}
const config: ServerRuntimeConfig = {
openaiKey,
openaiProject: String(runtimeConfig.openaiProject || '').trim() || undefined,
llmModel: String(runtimeConfig.llmModel || '').trim() || 'gpt-5-nano',
ttsModel: String(runtimeConfig.ttsModel || '').trim() || 'tts-1',
voiceId: String(runtimeConfig.defaultVoiceId || '').trim() || 'alloy',
usePiper: toBoolean(runtimeConfig.usePiper),
piperPort: toNumber(runtimeConfig.piperPort, 5001),
useSpeaches: toBoolean(runtimeConfig.useSpeaches),
speachesBaseUrl: String(runtimeConfig.speachesBaseUrl || '').trim() || undefined,
speechModelId: String(runtimeConfig.speechModelId || '').trim() || 'speaches-ai/piper-en_US-ryan-low',
}
cachedConfig = config
return config
}
export function resetServerRuntimeConfigCache() {
cachedConfig = null
}

View File

@@ -0,0 +1,27 @@
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i
export function isValidEmail(email: string): boolean {
return EMAIL_REGEX.test(email.trim())
}
export interface PasswordValidationResult {
valid: boolean
message?: string
}
export function validatePasswordStrength(password: string): PasswordValidationResult {
const trimmed = password.trim()
if (trimmed.length < 10) {
return { valid: false, message: 'Passwort muss mindestens 10 Zeichen lang sein.' }
}
if (/\s/.test(trimmed)) {
return { valid: false, message: 'Passwort darf keine Leerzeichen enthalten.' }
}
if (!/[A-Za-zÄÖÜäöüß]/.test(trimmed) || !/[0-9]/.test(trimmed)) {
return { valid: false, message: 'Bitte Buchstaben und Zahlen kombinieren.' }
}
if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmed)) {
return { valid: false, message: 'Mindestens ein Sonderzeichen erhöht die Sicherheit.' }
}
return { valid: true }
}