From 2d009a9662ec13d2fe04a4a4908c9ed34d8b1164 Mon Sep 17 00:00:00 2001 From: Bl0ck154 <36800583+Bl0ck154@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:20:39 +0200 Subject: [PATCH] feat(staging): restore scoped Order Forge image artifact retention core --- .github/workflows/api-route-typecheck.yml | 13 + ...DER_FORGE_IMAGE_RETENTION_PORT_20261025.md | 37 ++ src/app/api/v1/images/generations/route.ts | 23 ++ src/lib/usage/orderForgeImageArtifactSink.ts | 349 ++++++++++++++++++ .../order-forge-image-artifact-sink.test.ts | 212 +++++++++++ ...forge-image-generation-route-scope.test.ts | 26 ++ 6 files changed, 660 insertions(+) create mode 100644 docs/maintenance/ORDER_FORGE_IMAGE_RETENTION_PORT_20261025.md create mode 100644 src/lib/usage/orderForgeImageArtifactSink.ts create mode 100644 tests/unit/order-forge-image-artifact-sink.test.ts create mode 100644 tests/unit/order-forge-image-generation-route-scope.test.ts diff --git a/.github/workflows/api-route-typecheck.yml b/.github/workflows/api-route-typecheck.yml index add53a403d1b..ebab23b7e424 100644 --- a/.github/workflows/api-route-typecheck.yml +++ b/.github/workflows/api-route-typecheck.yml @@ -133,3 +133,16 @@ jobs: run: | node --import tsx/esm --test tests/unit/antigravity-resilience-tab-wiring.test.ts npx vitest run tests/unit/antigravity-resilience-settings-card-render.test.tsx + - name: Order Forge scope-gated image artifact retention + env: + API_KEY_SECRET: ci-only-retention-api-key + JWT_SECRET: ci-only-retention-jwt + DISABLE_SQLITE_AUTO_BACKUP: "true" + OMNIROUTE_DISABLE_BACKGROUND_SERVICES: "true" + run: | + node --import tsx/esm \ + --import ./open-sse/utils/setupPolyfill.ts \ + --import ./tests/_setup/isolateDataDir.ts \ + --test --test-force-exit --test-concurrency=1 \ + tests/unit/order-forge-image-artifact-sink.test.ts \ + tests/unit/order-forge-image-generation-route-scope.test.ts diff --git a/docs/maintenance/ORDER_FORGE_IMAGE_RETENTION_PORT_20261025.md b/docs/maintenance/ORDER_FORGE_IMAGE_RETENTION_PORT_20261025.md new file mode 100644 index 000000000000..91f956a8afca --- /dev/null +++ b/docs/maintenance/ORDER_FORGE_IMAGE_RETENTION_PORT_20261025.md @@ -0,0 +1,37 @@ +# Fork-only Order Forge image artifacts — upstream-first port + +**Isolated candidate / non-deployable phase 1.** Restore this explicitly +scoped feature from the currently deployed fork; do not submit customer-specific +application naming / directories to the official upstream repository. + +## Included + +- `src/lib/usage/orderForgeImageArtifactSink.ts`: original bounded + PNG/JPEG/WEBP payload validation (25 MiB), atomic private writes (0600), + per-attempt metadata without prompt/body text, 72-hour expiry, 2 GiB cap, + duplicate-safe IDs and cancellable stream drain. +- Original standalone sink regression suite from fork production. +- Narrow integration in **direct** `/v1/images/generations` (without changing + the response body, status, quota eligibility or original image accounting). + Capture needs the authenticated API key's explicit + `image_artifact_retention` scope **and** a syntactically valid + `X-EtsyTrello-Artifact-Id` header. A bare header is not sufficient. + Retention disk failures never turn a paid image success into a 500. +- Hosted regression gate for scope and basic route placement. + +## Remaining before release (hard requirements) + +- Port into non-streaming and streaming `open-sse/handlers/chatCore.ts` + including safe tee/drain across client disconnect and normalized + `image_generation_call` outputs. +- Port `/v1/images/edits` on direct AND combo/custom-provider branches, + preserving existing host proxy / account fallback and billing. +- Port independent Codex image quota telemetry, account availability and + quota header merging only after comparing modern upstream accounting. +- Run focused image pipeline tests, full hosted build and packaged artifact + smoke, isolated migration rehearsal and review. +- Never copy stored customer image payloads or prompts into GitHub artifacts. + +Existing custom artifact directory and metadata are operational data; preserve +the configured volume/directory during rollout and validate backup/rollback +without uploading customer files. diff --git a/src/app/api/v1/images/generations/route.ts b/src/app/api/v1/images/generations/route.ts index cbd63cbdc188..1345d5d97823 100644 --- a/src/app/api/v1/images/generations/route.ts +++ b/src/app/api/v1/images/generations/route.ts @@ -54,6 +54,10 @@ import { generateRequestId } from "@/shared/utils/requestId"; import { getSpecialtyModelsResponse } from "@/app/api/v1/_shared/specialtyCatalog"; import { enforceClientApiRouteAuth } from "@/shared/utils/clientApiRouteAuth"; import { runImageRequestWithAccounting } from "@/lib/usage/imageRequestAccounting"; +import { + persistOrderForgeImageArtifacts, + resolveOrderForgeImageArtifactCapture, +} from "@/lib/usage/orderForgeImageArtifactSink"; import { executeImageWithCredentialFallback } from "@/sse/services/imageCredentialRetry"; import { AUTHZ_HEADER_PEER_LOCALITY } from "@/server/authz/headers"; import { @@ -172,6 +176,13 @@ async function postHandler(request, context) { if (endpointMismatch) return errorResponse(HTTP_STATUS.BAD_REQUEST, endpointMismatch); const { allowedConnections } = scope; + // Strictly opt-in, per-authenticated-principal retention. A caller-supplied + // artifact ID does nothing without the dedicated API-key scope. + const artifactCapture = resolveOrderForgeImageArtifactCapture({ + apiKeyScopes: policy.apiKeyInfo?.scopes, + headers: request.headers, + }); + const modelPrefix = body.model.includes("/") ? body.model.slice(0, body.model.indexOf("/")) : body.model; @@ -431,6 +442,18 @@ async function postHandler(request, context) { if (result.success) { await clearRecoveredProviderState(credentials); + if (artifactCapture) { + try { + persistOrderForgeImageArtifacts({ + capture: artifactCapture, + responseBody: (result as { data?: unknown }).data, + }); + } catch { + // Retention is optional; its local disk should never turn a successful + // image response into a provider/API failure or expose customer data. + log.warn("IMAGE", "Optional image artifact retention failed"); + } + } // Bill the images actually delivered: a provider that returns fewer than the // requested `n` must not be charged for the missing ones (#16398 follow-up). const n = (result as { data?: { data?: unknown[] } }).data?.data?.length || 1; diff --git a/src/lib/usage/orderForgeImageArtifactSink.ts b/src/lib/usage/orderForgeImageArtifactSink.ts new file mode 100644 index 000000000000..52e1d479a3ce --- /dev/null +++ b/src/lib/usage/orderForgeImageArtifactSink.ts @@ -0,0 +1,349 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +import { resolveDataDir } from "../dataPaths"; + +const ARTIFACT_DIR_ENV = "OMNIROUTE_ETSY_IMAGE_ARTIFACT_DIR"; +export const ORDER_FORGE_IMAGE_ARTIFACT_SCOPE = "image_artifact_retention"; +const ARTIFACT_ID_HEADER = "x-etsytrello-artifact-id"; +const ARTIFACT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const SAFE_ID_PATTERN = /[^A-Za-z0-9._-]+/g; +const MAX_IMAGE_BYTES = 25 * 1024 * 1024; +const MAX_ENCODED_BYTES = Math.ceil(MAX_IMAGE_BYTES / 3) * 4 + 8; +const RETENTION_MS = 72 * 60 * 60 * 1000; +const MAX_STORE_BYTES = 2 * 1024 * 1024 * 1024; +const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; + +let lastCleanupAt = 0; + +type HeaderSource = + | Headers + | Record + | { get?: (name: string) => string | null } + | null + | undefined; + +export type OrderForgeImageArtifactCapture = { + artifactId: string; + correlationId: string; +}; + +export type StoredOrderForgeImageArtifact = { + artifactId: string; + correlationId: string; + imageCallId: string; + responseId: string; + format: "png" | "jpeg" | "webp"; + sizeBytes: number; + sha256: string; + path: string; +}; + +type CaptureEligibilityInput = { + apiKeyScopes?: readonly string[] | null; + headers?: HeaderSource; + correlationId?: string | null; +}; + +type PersistInput = { + capture: OrderForgeImageArtifactCapture; + responseBody: unknown; + baseDir?: string; + now?: number; +}; + +type CleanupOptions = { + baseDir?: string; + now?: number; + retentionMs?: number; + maxStoreBytes?: number; +}; + +function getHeader(headers: HeaderSource, name: string): string | null { + if (!headers) return null; + if (typeof (headers as { get?: unknown }).get === "function") { + return (headers as { get: (headerName: string) => string | null }).get(name); + } + const record = headers as Record; + for (const [key, value] of Object.entries(record)) { + if (key.toLowerCase() === name && typeof value === "string") return value; + } + return null; +} + +function normalizeArtifactId(headers: HeaderSource): string | null { + const value = String(getHeader(headers, ARTIFACT_ID_HEADER) || "").trim(); + return ARTIFACT_ID_PATTERN.test(value) ? value : null; +} + +function safeSegment(value: string, fallback: string): string { + const normalized = value.replace(SAFE_ID_PATTERN, "-").replace(/^-+|-+$/g, "").slice(0, 128); + return normalized || fallback; +} + +function resolveArtifactDir(): string { + const configured = String(process.env[ARTIFACT_DIR_ENV] || "").trim(); + return configured || path.join(resolveDataDir(), "etsytrello-image-artifacts"); +} + +export function resolveOrderForgeImageArtifactCapture( + input: CaptureEligibilityInput +): OrderForgeImageArtifactCapture | null { + // Provider/model/route agnostic by design. Retention is a capability granted + // to an API principal, not a hard-coded model/provider/endpoint or key id. + if (!input.apiKeyScopes?.includes(ORDER_FORGE_IMAGE_ARTIFACT_SCOPE)) return null; + const artifactId = normalizeArtifactId(input.headers); + if (!artifactId) return null; + + return { + artifactId, + correlationId: safeSegment( + String(input.correlationId || crypto.randomUUID()), + crypto.randomUUID() + ), + }; +} + +function decodeValidatedImage(result: unknown): { + data: Buffer; + format: "png" | "jpeg" | "webp"; +} | null { + if (typeof result !== "string" || !result || result.length > MAX_ENCODED_BYTES) return null; + if (result.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(result)) return null; + + let data: Buffer; + try { + data = Buffer.from(result, "base64"); + } catch { + return null; + } + if (!data.length || data.length > MAX_IMAGE_BYTES) return null; + if (data.toString("base64").replace(/=+$/, "") !== result.replace(/=+$/, "")) return null; + + if (data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) { + return { data, format: "png" }; + } + if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) { + return { data, format: "jpeg" }; + } + if ( + data.length >= 12 && + data.subarray(0, 4).toString("ascii") === "RIFF" && + data.subarray(8, 12).toString("ascii") === "WEBP" + ) { + return { data, format: "webp" }; + } + return null; +} + +function writeAtomic(filePath: string, data: Buffer | string): void { + const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`; + let fd: number | null = null; + try { + fd = fs.openSync(tmpPath, "wx", 0o600); + fs.writeFileSync(fd, data); + fs.fsyncSync(fd); + fs.closeSync(fd); + fd = null; + fs.renameSync(tmpPath, filePath); + fs.chmodSync(filePath, 0o600); + } catch (error) { + if (fd !== null) { + try { + fs.closeSync(fd); + } catch {} + } + try { + fs.rmSync(tmpPath, { force: true }); + } catch {} + throw error; + } +} + +function listAttemptDirectories(baseDir: string): Array<{ + path: string; + mtimeMs: number; + sizeBytes: number; +}> { + if (!fs.existsSync(baseDir)) return []; + const attempts: Array<{ path: string; mtimeMs: number; sizeBytes: number }> = []; + for (const dateEntry of fs.readdirSync(baseDir, { withFileTypes: true })) { + if (!dateEntry.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(dateEntry.name)) continue; + const datePath = path.join(baseDir, dateEntry.name); + for (const attemptEntry of fs.readdirSync(datePath, { withFileTypes: true })) { + if (!attemptEntry.isDirectory() || attemptEntry.isSymbolicLink()) continue; + const attemptPath = path.join(datePath, attemptEntry.name); + let sizeBytes = 0; + let mtimeMs = 0; + for (const fileEntry of fs.readdirSync(attemptPath, { withFileTypes: true })) { + if (!fileEntry.isFile() || fileEntry.isSymbolicLink()) continue; + const stat = fs.statSync(path.join(attemptPath, fileEntry.name)); + sizeBytes += stat.size; + mtimeMs = Math.max(mtimeMs, stat.mtimeMs); + } + attempts.push({ path: attemptPath, mtimeMs, sizeBytes }); + } + } + return attempts; +} + +export function cleanupOrderForgeImageArtifacts(options: CleanupOptions = {}): { + deletedAttempts: number; + remainingBytes: number; +} { + const baseDir = options.baseDir || resolveArtifactDir(); + const now = options.now ?? Date.now(); + const retentionMs = options.retentionMs ?? RETENTION_MS; + const maxStoreBytes = options.maxStoreBytes ?? MAX_STORE_BYTES; + let deletedAttempts = 0; + let attempts = listAttemptDirectories(baseDir); + + for (const attempt of attempts) { + if (attempt.mtimeMs > 0 && now - attempt.mtimeMs > retentionMs) { + fs.rmSync(attempt.path, { recursive: true, force: true }); + deletedAttempts += 1; + } + } + + attempts = listAttemptDirectories(baseDir).sort((left, right) => left.mtimeMs - right.mtimeMs); + let remainingBytes = attempts.reduce((total, attempt) => total + attempt.sizeBytes, 0); + for (const attempt of attempts) { + if (remainingBytes <= maxStoreBytes) break; + fs.rmSync(attempt.path, { recursive: true, force: true }); + remainingBytes -= attempt.sizeBytes; + deletedAttempts += 1; + } + + if (fs.existsSync(baseDir)) { + for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(entry.name)) continue; + const datePath = path.join(baseDir, entry.name); + if (fs.readdirSync(datePath).length === 0) fs.rmdirSync(datePath); + } + } + return { deletedAttempts, remainingBytes: Math.max(0, remainingBytes) }; +} + +function maybeCleanup(baseDir: string, now: number): void { + if (now - lastCleanupAt < CLEANUP_INTERVAL_MS) return; + lastCleanupAt = now; + cleanupOrderForgeImageArtifacts({ baseDir, now }); +} + +export function persistOrderForgeImageArtifacts(input: PersistInput): StoredOrderForgeImageArtifact[] { + const response = + input.responseBody && typeof input.responseBody === "object" && !Array.isArray(input.responseBody) + ? (input.responseBody as Record) + : null; + const responseOutput = response?.output; + const imageData = response?.data; + const output = Array.isArray(responseOutput) + ? responseOutput + : Array.isArray(imageData) + ? imageData.map((item, index) => { + const record = + item && typeof item === "object" && !Array.isArray(item) + ? (item as Record) + : {}; + const dataUrl = typeof record.url === "string" ? record.url : ""; + const dataUrlMatch = dataUrl.match(/^data:image\/(?:png|jpeg|webp);base64,(.+)$/i); + return { + id: "image-" + String(index + 1), + type: "image_generation_call", + result: + typeof record.b64_json === "string" + ? record.b64_json + : dataUrlMatch?.[1] || "", + }; + }) + : []; + if (output.length === 0) return []; + + const now = input.now ?? Date.now(); + const baseDir = input.baseDir || resolveArtifactDir(); + maybeCleanup(baseDir, now); + fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 }); + fs.chmodSync(baseDir, 0o700); + + const createdAt = new Date(now).toISOString(); + const dateDir = createdAt.slice(0, 10); + const attemptId = safeSegment(input.capture.correlationId, crypto.randomUUID()); + const attemptDir = path.join(baseDir, dateDir, attemptId); + fs.mkdirSync(attemptDir, { recursive: true, mode: 0o700 }); + fs.chmodSync(path.join(baseDir, dateDir), 0o700); + fs.chmodSync(attemptDir, 0o700); + + const created = Number(response?.created || 0); + const responseId = safeSegment( + String(response?.id || (created > 0 ? "image-response:" + String(created) : "")), + "unknown-response" + ); + const stored: StoredOrderForgeImageArtifact[] = []; + + try { + for (const item of output) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const record = item as Record; + if (record.type !== "image_generation_call") continue; + const decoded = decodeValidatedImage(record.result); + if (!decoded) continue; + + const imageCallId = safeSegment(String(record.id || ""), `image-${stored.length + 1}`); + const sha256 = crypto.createHash("sha256").update(decoded.data).digest("hex"); + const extension = decoded.format === "jpeg" ? "jpg" : decoded.format; + const imagePath = path.join(attemptDir, `${imageCallId}-${sha256.slice(0, 16)}.${extension}`); + writeAtomic(imagePath, decoded.data); + stored.push({ + artifactId: input.capture.artifactId, + correlationId: input.capture.correlationId, + imageCallId, + responseId, + format: decoded.format, + sizeBytes: decoded.data.length, + sha256, + path: imagePath, + }); + } + + if (stored.length === 0) { + fs.rmSync(attemptDir, { recursive: true, force: true }); + return []; + } + + const metadata = { + schemaVersion: 1, + artifactId: input.capture.artifactId, + correlationId: input.capture.correlationId, + responseId, + createdAt, + expiresAt: new Date(now + RETENTION_MS).toISOString(), + images: stored.map((item) => ({ + imageCallId: item.imageCallId, + format: item.format, + sizeBytes: item.sizeBytes, + sha256: item.sha256, + fileName: path.basename(item.path), + })), + }; + writeAtomic(path.join(attemptDir, "metadata.json"), JSON.stringify(metadata, null, 2)); + return stored; + } catch (error) { + fs.rmSync(attemptDir, { recursive: true, force: true }); + throw error; + } +} + +export async function drainOrderForgeImageArtifactStream( + stream: ReadableStream +): Promise { + const reader = stream.getReader(); + try { + for (;;) { + const { done } = await reader.read(); + if (done) return; + } + } finally { + reader.releaseLock(); + } +} diff --git a/tests/unit/order-forge-image-artifact-sink.test.ts b/tests/unit/order-forge-image-artifact-sink.test.ts new file mode 100644 index 000000000000..0e3309534862 --- /dev/null +++ b/tests/unit/order-forge-image-artifact-sink.test.ts @@ -0,0 +1,212 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { afterEach } from "node:test"; + +import { + cleanupOrderForgeImageArtifacts, + drainOrderForgeImageArtifactStream, + persistOrderForgeImageArtifacts, + resolveOrderForgeImageArtifactCapture, +} from "../../src/lib/usage/orderForgeImageArtifactSink"; + +const ENV_KEYS = ["OMNIROUTE_ETSY_IMAGE_ARTIFACT_DIR"] as const; +const previousEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); +const tempDirs: string[] = []; + +afterEach(() => { + for (const key of ENV_KEYS) { + const previous = previousEnv[key]; + if (previous === undefined) delete process.env[key]; + else process.env[key] = previous; + } + while (tempDirs.length > 0) { + fs.rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +function tempDir(): string { + const value = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-image-artifact-")); + tempDirs.push(value); + return value; +} + +function enableSink(baseDir = tempDir()): string { + process.env.OMNIROUTE_ETSY_IMAGE_ARTIFACT_DIR = baseDir; + return baseDir; +} + +function eligible(overrides: Record = {}) { + return resolveOrderForgeImageArtifactCapture({ + apiKeyScopes: ["image_artifact_retention"], + provider: "codex", + model: "gpt-5.6-terra-medium", + endpoint: "/api/v1/responses", + requestBody: { tools: [{ type: "image_generation", action: "edit" }] }, + headers: new Headers({ "X-EtsyTrello-Artifact-Id": "card-123:composition-1:variant-2" }), + correlationId: "corr-123", + ...overrides, + }); +} + +test("artifact capture depends only on the Order Forge principal and stable artifact id", () => { + enableSink(); + assert.deepEqual(eligible(), { + artifactId: "card-123:composition-1:variant-2", + correlationId: "corr-123", + }); + assert.equal(eligible({ apiKeyScopes: [] }), null); + assert.equal(eligible({ apiKeyScopes: ["manage"] }), null); + assert.deepEqual( + eligible({ + provider: "future-provider", + model: "brand-new-model", + endpoint: "/v42/future/image/path", + requestBody: { completely: "different" }, + }), + { + artifactId: "card-123:composition-1:variant-2", + correlationId: "corr-123", + } + ); + assert.equal(eligible({ headers: new Headers({ "X-EtsyTrello-Artifact-Id": "../bad" }) }), null); + assert.equal(eligible({ headers: new Headers() }), null); + +}); + +test("validated image output is written atomically with metadata but no payload or prompt", () => { + const baseDir = enableSink(); + const capture = eligible(); + assert.ok(capture); + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]); + const encoded = png.toString("base64"); + + const stored = persistOrderForgeImageArtifacts({ + capture, + baseDir, + now: Date.UTC(2026, 7, 9, 18, 0, 0), + responseBody: { + id: "resp_123", + output: [ + { type: "reasoning", summary: [] }, + { + id: "ig_123", + type: "image_generation_call", + result: encoded, + revised_prompt: "private customer prompt", + }, + ], + }, + }); + + assert.equal(stored.length, 1); + assert.deepEqual(fs.readFileSync(stored[0].path), png); + assert.equal(fs.statSync(stored[0].path).mode & 0o777, 0o600); + const metadataPath = path.join(path.dirname(stored[0].path), "metadata.json"); + const metadataText = fs.readFileSync(metadataPath, "utf8"); + const metadata = JSON.parse(metadataText); + assert.equal(metadata.artifactId, "card-123:composition-1:variant-2"); + assert.equal(metadata.images[0].sizeBytes, png.length); + assert.equal(metadataText.includes(encoded), false); + assert.equal(metadataText.includes("private customer prompt"), false); + assert.equal(fs.statSync(metadataPath).mode & 0o777, 0o600); + assert.deepEqual( + fs.readdirSync(path.dirname(stored[0].path)).filter((name) => name.endsWith(".tmp")), + [] + ); +}); + +test("OpenAI-style image payloads are retained without provider or route knowledge", () => { + const baseDir = enableSink(); + const capture = eligible({ provider: "future-provider", model: "future-model", endpoint: "/future" }); + assert.ok(capture); + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 8, 7]); + const encoded = png.toString("base64"); + const stored = persistOrderForgeImageArtifacts({ + capture, + baseDir, + responseBody: { created: 123, data: [{ b64_json: encoded }] }, + }); + assert.equal(stored.length, 1); + assert.deepEqual(fs.readFileSync(stored[0].path), png); + assert.equal(stored[0].artifactId, "card-123:composition-1:variant-2"); +}); + +test("data-URL image payloads are retained too", () => { + const baseDir = enableSink(); + const capture = eligible(); + assert.ok(capture); + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 3, 2, 1]); + const stored = persistOrderForgeImageArtifacts({ + capture, + baseDir, + responseBody: { data: [{ url: `data:image/png;base64,${png.toString("base64")}` }] }, + }); + assert.equal(stored.length, 1); + assert.deepEqual(fs.readFileSync(stored[0].path), png); +}); + +test("invalid base64, unsupported bytes, and oversized declarations are not stored", () => { + const baseDir = enableSink(); + const capture = eligible(); + assert.ok(capture); + const result = persistOrderForgeImageArtifacts({ + capture, + baseDir, + responseBody: { + output: [ + { type: "image_generation_call", result: "not-base64" }, + { type: "image_generation_call", result: Buffer.from("plain text").toString("base64") }, + { type: "message", result: Buffer.from([0x89, 0x50]).toString("base64") }, + ], + }, + }); + assert.deepEqual(result, []); +}); + +test("private tee drain finishes after the downstream branch is cancelled", async () => { + let sent = 0; + const source = new ReadableStream({ + async pull(controller) { + if (sent === 4) { + controller.close(); + return; + } + await new Promise((resolve) => setTimeout(resolve, 2)); + controller.enqueue(Uint8Array.of(++sent)); + }, + }); + const [downstream, privateBranch] = source.tee(); + const drained = drainOrderForgeImageArtifactStream(privateBranch); + await downstream.cancel("client timeout"); + await drained; + assert.equal(sent, 4); +}); + +test("cleanup enforces retention and total byte cap by deleting oldest attempts", () => { + const baseDir = enableSink(); + const dateDir = path.join(baseDir, "2026-08-09"); + const oldDir = path.join(dateDir, "old"); + const firstDir = path.join(dateDir, "first"); + const secondDir = path.join(dateDir, "second"); + for (const dir of [oldDir, firstDir, secondDir]) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(oldDir, "x.png"), Buffer.alloc(6)); + fs.writeFileSync(path.join(firstDir, "x.png"), Buffer.alloc(6)); + fs.writeFileSync(path.join(secondDir, "x.png"), Buffer.alloc(6)); + fs.utimesSync(path.join(oldDir, "x.png"), new Date(1_000), new Date(1_000)); + fs.utimesSync(path.join(firstDir, "x.png"), new Date(8_000), new Date(8_000)); + fs.utimesSync(path.join(secondDir, "x.png"), new Date(9_000), new Date(9_000)); + + const result = cleanupOrderForgeImageArtifacts({ + baseDir, + now: 10_000, + retentionMs: 5_000, + maxStoreBytes: 6, + }); + assert.equal(result.deletedAttempts, 2); + assert.equal(result.remainingBytes, 6); + assert.equal(fs.existsSync(oldDir), false); + assert.equal(fs.existsSync(firstDir), false); + assert.equal(fs.existsSync(secondDir), true); +}); diff --git a/tests/unit/order-forge-image-generation-route-scope.test.ts b/tests/unit/order-forge-image-generation-route-scope.test.ts new file mode 100644 index 000000000000..08832facae88 --- /dev/null +++ b/tests/unit/order-forge-image-generation-route-scope.test.ts @@ -0,0 +1,26 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +/** Source guard complements the sink's runtime access-control tests: retention + * must be authorized by the API-key policy, not just a caller-controlled header. */ +const source = fs.readFileSync( + path.resolve(process.cwd(), "src/app/api/v1/images/generations/route.ts"), + "utf8" +); + +test("optional image retention is gated by authenticated principal and success", () => { + const auth = source.indexOf("await enforceClientApiRouteAuth(request)"); + const policy = source.indexOf("await enforceApiKeyPolicy(request, body.model)"); + const keyScope = source.indexOf("await resolveKeyConnectionScope(policy.apiKeyInfo)"); + const capture = source.indexOf("const artifactCapture = resolveOrderForgeImageArtifactCapture"); + const success = source.indexOf("if (result.success) {"); + const persisted = source.indexOf("persistOrderForgeImageArtifacts({"); + assert.ok(auth >= 0 && auth < policy); + assert.ok(policy < keyScope && keyScope < capture); + assert.ok(success > capture && persisted > success); + assert.match(source, /apiKeyScopes:\s*policy\.apiKeyInfo\?\.scopes/); + assert.match(source, /headers:\s*request\.headers/); + assert.match(source, /catch \{[\s\S]*?Optional image artifact retention failed/); +});