Files
Cibello-app/apps/worker/src/context.ts
T
Sven (AAMOS AI) 6e2d794130 feat(storage,deploy): delad S3/mock-lagring, SSM .env-sync, Gemini readyz
- @app/storage med MockStorage + AwsStorage delas mellan API och worker

- Workerns readUrl() ger presignerad S3-URL i aws-läge (fixar mock-s3 404)

- sync-env-from-ssm.py hämtar /cibello/prod/* och skriver .env; first-deploy.sh kör det i prod

- /readyz returnerar 503 om app.aamos.healthCheck() misslyckas

- docs/SECRETS.md med rotations- och SSM-regler
2026-08-12 17:28:58 +07:00

110 lines
3.6 KiB
TypeScript

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 { createDatabase, type Database } from "@app/database";
import { createAamosClient, type AamosClient, type BudgetStore } from "@app/ai-contracts";
import { createStorageService, type StorageService } from "@app/storage";
import type { Redis } from "ioredis";
export interface WorkerContext {
db: Database;
aamos: AamosClient;
redis?: Redis;
/** Delad dagsbudget-store så att memory-jobbet kan spegla skanningens budgetkoll. */
budgetStore?: BudgetStore;
/** Lagringstjänst för att läsa/skriva bilder (S3 eller mock). */
storage: StorageService;
/** Bygger läs-URL för lagrade bilder (presignerad S3 i aws-läge, mock-URL lokalt). */
readUrl: (key: string) => Promise<string>;
apiBaseUrl: string;
close: () => Promise<void>;
}
class RedisBudgetStore implements BudgetStore {
constructor(
private readonly redis: Redis,
private readonly key: string,
) {}
private alarmKey(): string {
return this.key.replace(/^gemini:daily:budget:/, "gemini:daily:budget:alarm:");
}
async getDailySpendUsd(): Promise<number> {
const val = await this.redis.get(this.key);
return val ? Number(val) : 0;
}
async incrementDailySpendUsd(amountUsd: number): Promise<number> {
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);
}
async raiseBudgetAlarm(): Promise<void> {
const ttl = this.secondsUntilMidnightUtc();
await this.redis.set(this.alarmKey(), "1", "EX", ttl);
}
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));
}
}
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}. Worker must be configured explicitly.`);
return value;
}
export function createContext(redis?: Redis): WorkerContext {
const databaseUrl = requireEnv("DATABASE_URL");
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 signingSecret = requireEnv("ENTITLEMENT_SIGNING_SECRET");
const s3Mode = process.env.S3_MODE === "aws" ? "aws" : "mock";
const storage = createStorageService({
mode: s3Mode,
baseUrl: apiBaseUrl,
signingSecret,
s3Bucket: process.env.S3_BUCKET ?? "",
s3Region: process.env.S3_REGION ?? "",
s3Endpoint: process.env.S3_ENDPOINT || undefined,
s3AccessKeyId: process.env.S3_ACCESS_KEY_ID || undefined,
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY || undefined,
});
return {
db,
aamos,
redis,
budgetStore,
storage,
apiBaseUrl,
readUrl: (key: string) => storage.getReadUrl(key),
close: async () => {
await pool.end();
},
};
}