import { config as loadDotenv } from "dotenv"; import { existsSync } from "node:fs"; import path from "node:path"; // Ladda .env från paketet ELLER monorepo-roten (pnpm --filter sätter cwd till paketet). for (const candidate of [".env", "../.env", "../../.env"]) { const p = path.resolve(process.cwd(), candidate); if (existsSync(p)) { loadDotenv({ path: p }); break; } } import { createHmac } from "node:crypto"; import { createDatabase, type Database } from "@app/database"; import { createAamosClient, type AamosClient, type BudgetStore } from "@app/ai-contracts"; import type { Redis } from "ioredis"; export interface WorkerContext { db: Database; aamos: AamosClient; /** Bygger läs-URL för lagrade bilder (samma signaturlogik som API:ts mock-S3). */ readUrl: (key: string) => string; apiBaseUrl: string; close: () => Promise; } class RedisBudgetStore implements BudgetStore { constructor( private readonly redis: Redis, private readonly key: string, ) {} async getDailySpendUsd(): Promise { const val = await this.redis.get(this.key); return val ? Number(val) : 0; } async incrementDailySpendUsd(amountUsd: number): Promise { const newVal = await this.redis.incrbyfloat(this.key, amountUsd); // Expire at next midnight UTC to keep daily window. const ttl = this.secondsUntilMidnightUtc(); await this.redis.expire(this.key, ttl); return Number(newVal); } private secondsUntilMidnightUtc(): number { const now = new Date(); const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)); return Math.max(1, Math.floor((midnight.getTime() - now.getTime()) / 1000)); } } export function createContext(redis?: Redis): WorkerContext { const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { throw new Error("Missing DATABASE_URL. Worker must connect explicitly to the app database."); } const { db, pool } = createDatabase(databaseUrl); const budgetStore = redis ? new RedisBudgetStore(redis, `gemini:daily:budget:${new Date().toISOString().slice(0, 10)}`) : undefined; const aamos = createAamosClient(undefined, { budgetStore }); const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:4000"; const secret = process.env.ENTITLEMENT_SIGNING_SECRET ?? "dev-only-change-me-three"; return { db, aamos, apiBaseUrl, readUrl: (key: string) => { const sig = createHmac("sha256", secret).update(key).digest("hex").slice(0, 32); return `${apiBaseUrl}/v1/mock-s3/${encodeURIComponent(key)}?sig=${sig}`; }, close: async () => { await pool.end(); }, }; }