@copilotkit/aimock 1.35.1 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +22 -0
  3. package/dist/agui-types.d.cts.map +1 -1
  4. package/dist/agui-types.d.ts.map +1 -1
  5. package/dist/cli.cjs +2 -0
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +2 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config-loader.cjs +9 -0
  10. package/dist/config-loader.cjs.map +1 -1
  11. package/dist/config-loader.d.cts +4 -0
  12. package/dist/config-loader.d.cts.map +1 -1
  13. package/dist/config-loader.d.ts +4 -0
  14. package/dist/config-loader.d.ts.map +1 -1
  15. package/dist/config-loader.js +9 -0
  16. package/dist/config-loader.js.map +1 -1
  17. package/dist/fal-audio.cjs +1 -0
  18. package/dist/fal-audio.cjs.map +1 -1
  19. package/dist/fal-audio.js +1 -0
  20. package/dist/fal-audio.js.map +1 -1
  21. package/dist/fal.cjs +4 -1
  22. package/dist/fal.cjs.map +1 -1
  23. package/dist/fal.d.cts.map +1 -1
  24. package/dist/fal.d.ts.map +1 -1
  25. package/dist/fal.js +4 -1
  26. package/dist/fal.js.map +1 -1
  27. package/dist/provider-auth.cjs +170 -0
  28. package/dist/provider-auth.cjs.map +1 -0
  29. package/dist/provider-auth.js +168 -0
  30. package/dist/provider-auth.js.map +1 -0
  31. package/dist/recorder.cjs +5 -1
  32. package/dist/recorder.cjs.map +1 -1
  33. package/dist/recorder.d.cts.map +1 -1
  34. package/dist/recorder.d.ts.map +1 -1
  35. package/dist/recorder.js +5 -1
  36. package/dist/recorder.js.map +1 -1
  37. package/dist/types.d.cts +13 -0
  38. package/dist/types.d.cts.map +1 -1
  39. package/dist/types.d.ts +13 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/vector-types.d.cts.map +1 -1
  42. package/dist/vector-types.d.ts.map +1 -1
  43. package/package.json +1 -1
package/dist/fal.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fal.js","names":[],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getContext,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport type { Logger } from \"./logger.js\";\nimport {\n buildFixtureMatch,\n buildForwardHeaders,\n clampTimeout,\n persistFixture,\n proxyAndRecord,\n sanitizeHeaderValue,\n} from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /**\n * Billed quantity emitted as the `x-fal-billable-units` header on the\n * completed `queue-result` response. Sourced from the fixture's\n * `response.billableUnits`; `null` when the fixture omitted it (no header).\n */\n billableUnits: number | null;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nexport function resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n // Thresholds must resolve to non-negative integers: a NaN would make\n // advanceJob's `pollCount >= threshold` comparison permanently false (a\n // polling client never reaches terminal), and a negative would break the\n // documented \"explicit 0 enables progression\" contract. Non-finite values\n // are treated as unset; anything else is floored and clamped to >= 0.\n const sanitize = (value: number | undefined): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value)\n ? Math.max(0, Math.floor(value))\n : undefined;\n const explicitInProgress = sanitize(config?.pollsBeforeInProgress);\n const explicitCompleted = sanitize(config?.pollsBeforeCompleted);\n const pollsBeforeInProgress = explicitInProgress ?? 0;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted !== undefined) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (explicitInProgress !== undefined) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\n// Headers real fal sets on its queue responses. `@tanstack/ai-fal` (and\n// likely other adapters) read both to correlate a billed quantity with the\n// originating request: the request-id keys the lookup, the billable-units\n// value is the quantity. aimock emits the request-id on the `status` and\n// `result` queue responses (where adapters perform the billing lookup); the\n// submit envelope already carries `request_id` in its JSON body, and the\n// cancel / not-found responses omit it. The billable-units header rides only\n// the completed `result`, and only when a fixture opts in via\n// `response.billableUnits`.\nconst FAL_REQUEST_ID_HEADER = \"x-fal-request-id\";\nconst FAL_BILLABLE_UNITS_HEADER = \"x-fal-billable-units\";\n\n/**\n * Read an optional `billableUnits` off a resolved fixture response. Returns the\n * finite numeric value or `null` (absent, non-numeric, or non-finite) — the\n * `x-fal-billable-units` header is emitted only for a non-null result.\n */\nfunction extractBillableUnits(response: unknown): number | null {\n if (response && typeof response === \"object\" && \"billableUnits\" in response) {\n // The `in` narrowing types `response.billableUnits` as `unknown` — no cast.\n const value: unknown = response.billableUnits;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n }\n return null;\n}\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal, {\n [FAL_REQUEST_ID_HEADER]: job.requestId,\n });\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n // Emit x-fal-request-id on both the in-progress (202) and completed (200)\n // result; adapters key their billing lookup off it.\n const resultHeaders: Record<string, string> = { [FAL_REQUEST_ID_HEADER]: job.requestId };\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal, resultHeaders);\n return \"handled\";\n }\n // x-fal-billable-units rides only the completed result, and only when the\n // fixture opted into a billed quantity.\n if (job.billableUnits != null) {\n resultHeaders[FAL_BILLABLE_UNITS_HEADER] = String(job.billableUnits);\n }\n writeJson(req, res, 200, job.result, pathname, journal, resultHeaders);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n _context: getContext(req),\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n matchCounts,\n defaults.requestTransform,\n );\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(\n strictNoMatchLogLine(req.method ?? \"POST\", pathname, skippedBySequenceOrTurn),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to 404 (strict was\n // already handled above)\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n billableUnits: extractBillableUnits(response),\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n// Per-fetch upstream timeout default for the walk's submit/status/result\n// fetches — same value and clamp conventions as the rest of the recording\n// surfaces (recorder.ts / openrouter-video.ts).\nconst DEFAULT_FAL_FETCH_TIMEOUT_MS = 30_000;\n\n// Upstream header forwarding uses buildForwardHeaders (recorder.ts) — the\n// shared strip list (hop-by-hop, client-set, and the mock-internal x-test-id\n// / x-aimock-strict / x-aimock-context / x-aimock-chaos-* family), so the\n// fal queue walk never leaks control headers onto a real provider's wire\n// (one shared list, not a per-surface copy, so the surfaces cannot drift).\n\n/**\n * The result of a queue walk: the parsed final body plus the billed quantity\n * captured from the result fetch's `x-fal-billable-units` header (`null` when\n * upstream didn't set it). The caller persists `body` as the fixture and seeds\n * local queue state; `billableUnits` round-trips real fal billing so a recorded\n * fixture replays `x-fal-billable-units` without hand-editing.\n */\nexport interface FalQueueWalkResult {\n body: unknown;\n billableUnits: number | null;\n}\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result. Returns the parsed final body and the result\n * response's billed quantity so the caller can persist the fixture and seed\n * local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Per-fetch upstream timeout (`record.upstreamTimeoutMs` clamp conventions,\n * 30s default) applied to each of the walk's submit/status/result fetches\n * via AbortSignal — a hung upstream socket must not pin the walk past its\n * budget. Each fetch's signal is additionally clamped to the walk's\n * remaining deadline; an abort surfaces through the caller's existing\n * failure handling (502, no fixture persisted).\n */\n upstreamTimeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n /** Warn sink for the same-origin envelope-URL gate (omitting it only mutes the warns). */\n logger?: Logger;\n}): Promise<FalQueueWalkResult> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n upstreamTimeoutMs,\n fallbackStatusPath,\n fallbackResultPath,\n logger,\n } = args;\n\n const deadline = Date.now() + timeoutMs;\n const perFetchTimeoutMs = clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);\n // Bound every upstream fetch: the per-fetch timeout, additionally clamped\n // to the walk's remaining budget so a fetch can never outlive the deadline\n // (floored at 1ms — AbortSignal.timeout rejects non-positive values; the\n // deadline check in the poll loop is the authoritative expiry).\n const fetchSignal = (): AbortSignal =>\n AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, {\n method: \"POST\",\n headers,\n body,\n signal: fetchSignal(),\n });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n // JSON.parse admits null/arrays/scalars — reject non-object envelopes with\n // a clean error instead of TypeError-ing on the field reads below (the\n // OpenRouter video proxies apply the same guard).\n if (submitJson === null || typeof submitJson !== \"object\" || Array.isArray(submitJson)) {\n throw new Error(\"Submit response is not a JSON object\");\n }\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — but ONLY same-origin with the\n // configured upstream (mirroring the OpenRouter video proxy's\n // polling_url gate): every status/result fetch below forwards the client's\n // headers — including Authorization — so an envelope nominating a foreign\n // host must never receive them. Same-origin still covers the documented\n // proxy-in-front case (the configured upstream IS that proxy); off-origin,\n // unparseable, or absent envelope URLs fall back to the constructed\n // canonical paths on the upstream origin, with a warn.\n const upstreamOrigin = submitUrl.origin;\n const adoptSameOrigin = (value: unknown, label: string, fallbackPath: string): URL => {\n if (typeof value === \"string\" && value) {\n try {\n const parsed = new URL(value);\n if (parsed.origin === upstreamOrigin) return parsed;\n logger?.warn(\n `Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`,\n );\n } catch {\n logger?.warn(\n `Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`,\n );\n }\n }\n return resolveUpstreamUrl(upstreamBase, fallbackPath);\n };\n const statusUrl = adoptSameOrigin(\n env.status_url,\n \"status_url\",\n fallbackStatusPath(upstreamRequestId),\n );\n const resultUrl = adoptSameOrigin(\n env.response_url,\n \"response_url\",\n fallbackResultPath(upstreamRequestId),\n );\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers, signal: fetchSignal() });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusParsed = parseJsonOrThrow(statusText, \"Status\");\n // Same non-object guard as the submit envelope above.\n if (statusParsed === null || typeof statusParsed !== \"object\" || Array.isArray(statusParsed)) {\n throw new Error(\"Status response is not a JSON object\");\n }\n const statusJson = statusParsed as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n // Time out only when the budget is actually exhausted — a zero\n // pollIntervalMs is a valid \"poll as fast as possible\" configuration,\n // not an instant expiry (setTimeout(0) still yields the event loop).\n if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const sleep = Math.min(pollIntervalMs, remaining);\n await new Promise<void>((r) => setTimeout(r, Math.max(0, sleep)));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers, signal: fetchSignal() });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return {\n body: parseJsonOrThrow(resultText, \"Result\"),\n billableUnits: parseBillableUnitsHeader(resultRes.headers.get(FAL_BILLABLE_UNITS_HEADER)),\n };\n}\n\n/**\n * Parse the upstream `x-fal-billable-units` header into a finite number, or\n * `null` when absent/unparseable — mirrors the consumer-side parse so a\n * recorded fixture's captured units match what the live adapter would have read.\n */\nfunction parseBillableUnitsHeader(raw: string | null): number | null {\n if (raw == null) return null;\n const value = Number(raw.trim());\n return Number.isFinite(value) ? value : null;\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n let billableUnits: number | null;\n try {\n const walk = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildForwardHeaders(req),\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n upstreamTimeoutMs: record.upstreamTimeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n logger: defaults.logger,\n });\n finalBody = walk.body;\n billableUnits = walk.billableUnits;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n // Persist the captured billed quantity so the recorded fixture replays\n // x-fal-billable-units verbatim; omit the field entirely when upstream\n // didn't bill (keeps recorded fixtures clean).\n response: {\n json: finalBody,\n status: 200,\n ...(billableUnits != null ? { billableUnits } : {}),\n },\n };\n const persistResult = persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n // Surface a persist failure on the envelope (parity with the generic\n // recorder relay and the OpenRouter failed branch) — the synthesized\n // envelope below has not been written yet, so the header can still ride it.\n if (persistResult.kind === \"failed\" && !res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n }\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n // Captured from the upstream result's x-fal-billable-units header so the\n // same-session replay (before the persisted fixture is reloaded) already\n // surfaces the billed quantity.\n billableUnits,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry. The persisted fixture and seeded state above stay — the\n // captured upstream response is valuable regardless.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n extraHeaders?: Record<string, string>,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\", ...extraHeaders });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fal.js","names":[],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getContext,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport type { Logger } from \"./logger.js\";\nimport {\n buildFixtureMatch,\n buildForwardHeaders,\n clampTimeout,\n persistFixture,\n proxyAndRecord,\n sanitizeHeaderValue,\n} from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport { applyProviderAuth } from \"./provider-auth.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /**\n * Billed quantity emitted as the `x-fal-billable-units` header on the\n * completed `queue-result` response. Sourced from the fixture's\n * `response.billableUnits`; `null` when the fixture omitted it (no header).\n */\n billableUnits: number | null;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nexport function resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n // Thresholds must resolve to non-negative integers: a NaN would make\n // advanceJob's `pollCount >= threshold` comparison permanently false (a\n // polling client never reaches terminal), and a negative would break the\n // documented \"explicit 0 enables progression\" contract. Non-finite values\n // are treated as unset; anything else is floored and clamped to >= 0.\n const sanitize = (value: number | undefined): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value)\n ? Math.max(0, Math.floor(value))\n : undefined;\n const explicitInProgress = sanitize(config?.pollsBeforeInProgress);\n const explicitCompleted = sanitize(config?.pollsBeforeCompleted);\n const pollsBeforeInProgress = explicitInProgress ?? 0;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted !== undefined) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (explicitInProgress !== undefined) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\n// Headers real fal sets on its queue responses. `@tanstack/ai-fal` (and\n// likely other adapters) read both to correlate a billed quantity with the\n// originating request: the request-id keys the lookup, the billable-units\n// value is the quantity. aimock emits the request-id on the `status` and\n// `result` queue responses (where adapters perform the billing lookup); the\n// submit envelope already carries `request_id` in its JSON body, and the\n// cancel / not-found responses omit it. The billable-units header rides only\n// the completed `result`, and only when a fixture opts in via\n// `response.billableUnits`.\nconst FAL_REQUEST_ID_HEADER = \"x-fal-request-id\";\nconst FAL_BILLABLE_UNITS_HEADER = \"x-fal-billable-units\";\n\n/**\n * Read an optional `billableUnits` off a resolved fixture response. Returns the\n * finite numeric value or `null` (absent, non-numeric, or non-finite) — the\n * `x-fal-billable-units` header is emitted only for a non-null result.\n */\nfunction extractBillableUnits(response: unknown): number | null {\n if (response && typeof response === \"object\" && \"billableUnits\" in response) {\n // The `in` narrowing types `response.billableUnits` as `unknown` — no cast.\n const value: unknown = response.billableUnits;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n }\n return null;\n}\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal, {\n [FAL_REQUEST_ID_HEADER]: job.requestId,\n });\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n // Emit x-fal-request-id on both the in-progress (202) and completed (200)\n // result; adapters key their billing lookup off it.\n const resultHeaders: Record<string, string> = { [FAL_REQUEST_ID_HEADER]: job.requestId };\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal, resultHeaders);\n return \"handled\";\n }\n // x-fal-billable-units rides only the completed result, and only when the\n // fixture opted into a billed quantity.\n if (job.billableUnits != null) {\n resultHeaders[FAL_BILLABLE_UNITS_HEADER] = String(job.billableUnits);\n }\n writeJson(req, res, 200, job.result, pathname, journal, resultHeaders);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n _context: getContext(req),\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n matchCounts,\n defaults.requestTransform,\n );\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(\n strictNoMatchLogLine(req.method ?? \"POST\", pathname, skippedBySequenceOrTurn),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to 404 (strict was\n // already handled above)\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n billableUnits: extractBillableUnits(response),\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n// Per-fetch upstream timeout default for the walk's submit/status/result\n// fetches — same value and clamp conventions as the rest of the recording\n// surfaces (recorder.ts / openrouter-video.ts).\nconst DEFAULT_FAL_FETCH_TIMEOUT_MS = 30_000;\n\n// Upstream header forwarding uses buildForwardHeaders (recorder.ts) — the\n// shared strip list (hop-by-hop, client-set, and the mock-internal x-test-id\n// / x-aimock-strict / x-aimock-context / x-aimock-chaos-* family), so the\n// fal queue walk never leaks control headers onto a real provider's wire\n// (one shared list, not a per-surface copy, so the surfaces cannot drift).\n\n/**\n * The result of a queue walk: the parsed final body plus the billed quantity\n * captured from the result fetch's `x-fal-billable-units` header (`null` when\n * upstream didn't set it). The caller persists `body` as the fixture and seeds\n * local queue state; `billableUnits` round-trips real fal billing so a recorded\n * fixture replays `x-fal-billable-units` without hand-editing.\n */\nexport interface FalQueueWalkResult {\n body: unknown;\n billableUnits: number | null;\n}\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result. Returns the parsed final body and the result\n * response's billed quantity so the caller can persist the fixture and seed\n * local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Per-fetch upstream timeout (`record.upstreamTimeoutMs` clamp conventions,\n * 30s default) applied to each of the walk's submit/status/result fetches\n * via AbortSignal — a hung upstream socket must not pin the walk past its\n * budget. Each fetch's signal is additionally clamped to the walk's\n * remaining deadline; an abort surfaces through the caller's existing\n * failure handling (502, no fixture persisted).\n */\n upstreamTimeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n /** Warn sink for the same-origin envelope-URL gate (omitting it only mutes the warns). */\n logger?: Logger;\n /**\n * aimock's built-in fal key, when configured. Injected as `Authorization: Key\n * <key>` on every walk fetch (submit/status/result) if the caller sent no\n * credential or a dummy placeholder; a real caller credential overrides. This\n * is the single chokepoint for all fal queue walks — the queue path doesn't\n * route through `proxyAndRecord`, so own-key injection lives here.\n */\n builtinKey?: string;\n}): Promise<FalQueueWalkResult> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n upstreamTimeoutMs,\n fallbackStatusPath,\n fallbackResultPath,\n logger,\n builtinKey,\n } = args;\n\n // Inject aimock's own fal credential onto the walk headers up front so every\n // fetch below (submit + polls) carries it. fal's scheme ignores the target\n // URL, so any resolvable URL suffices.\n applyProviderAuth(headers, resolveUpstreamUrl(upstreamBase, submitPath), \"fal\", builtinKey);\n\n const deadline = Date.now() + timeoutMs;\n const perFetchTimeoutMs = clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);\n // Bound every upstream fetch: the per-fetch timeout, additionally clamped\n // to the walk's remaining budget so a fetch can never outlive the deadline\n // (floored at 1ms — AbortSignal.timeout rejects non-positive values; the\n // deadline check in the poll loop is the authoritative expiry).\n const fetchSignal = (): AbortSignal =>\n AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, {\n method: \"POST\",\n headers,\n body,\n signal: fetchSignal(),\n });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n // JSON.parse admits null/arrays/scalars — reject non-object envelopes with\n // a clean error instead of TypeError-ing on the field reads below (the\n // OpenRouter video proxies apply the same guard).\n if (submitJson === null || typeof submitJson !== \"object\" || Array.isArray(submitJson)) {\n throw new Error(\"Submit response is not a JSON object\");\n }\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — but ONLY same-origin with the\n // configured upstream (mirroring the OpenRouter video proxy's\n // polling_url gate): every status/result fetch below forwards the client's\n // headers — including Authorization — so an envelope nominating a foreign\n // host must never receive them. Same-origin still covers the documented\n // proxy-in-front case (the configured upstream IS that proxy); off-origin,\n // unparseable, or absent envelope URLs fall back to the constructed\n // canonical paths on the upstream origin, with a warn.\n const upstreamOrigin = submitUrl.origin;\n const adoptSameOrigin = (value: unknown, label: string, fallbackPath: string): URL => {\n if (typeof value === \"string\" && value) {\n try {\n const parsed = new URL(value);\n if (parsed.origin === upstreamOrigin) return parsed;\n logger?.warn(\n `Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`,\n );\n } catch {\n logger?.warn(\n `Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`,\n );\n }\n }\n return resolveUpstreamUrl(upstreamBase, fallbackPath);\n };\n const statusUrl = adoptSameOrigin(\n env.status_url,\n \"status_url\",\n fallbackStatusPath(upstreamRequestId),\n );\n const resultUrl = adoptSameOrigin(\n env.response_url,\n \"response_url\",\n fallbackResultPath(upstreamRequestId),\n );\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers, signal: fetchSignal() });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusParsed = parseJsonOrThrow(statusText, \"Status\");\n // Same non-object guard as the submit envelope above.\n if (statusParsed === null || typeof statusParsed !== \"object\" || Array.isArray(statusParsed)) {\n throw new Error(\"Status response is not a JSON object\");\n }\n const statusJson = statusParsed as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n // Time out only when the budget is actually exhausted — a zero\n // pollIntervalMs is a valid \"poll as fast as possible\" configuration,\n // not an instant expiry (setTimeout(0) still yields the event loop).\n if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const sleep = Math.min(pollIntervalMs, remaining);\n await new Promise<void>((r) => setTimeout(r, Math.max(0, sleep)));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers, signal: fetchSignal() });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return {\n body: parseJsonOrThrow(resultText, \"Result\"),\n billableUnits: parseBillableUnitsHeader(resultRes.headers.get(FAL_BILLABLE_UNITS_HEADER)),\n };\n}\n\n/**\n * Parse the upstream `x-fal-billable-units` header into a finite number, or\n * `null` when absent/unparseable — mirrors the consumer-side parse so a\n * recorded fixture's captured units match what the live adapter would have read.\n */\nfunction parseBillableUnitsHeader(raw: string | null): number | null {\n if (raw == null) return null;\n const value = Number(raw.trim());\n return Number.isFinite(value) ? value : null;\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n let billableUnits: number | null;\n try {\n const walk = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildForwardHeaders(req),\n // aimock's built-in fal key (Authorization: Key), injected by the walk on\n // a no/dummy caller credential; a real caller key overrides.\n builtinKey: record.providerKeys?.fal,\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n upstreamTimeoutMs: record.upstreamTimeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n logger: defaults.logger,\n });\n finalBody = walk.body;\n billableUnits = walk.billableUnits;\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n // Persist the captured billed quantity so the recorded fixture replays\n // x-fal-billable-units verbatim; omit the field entirely when upstream\n // didn't bill (keeps recorded fixtures clean).\n response: {\n json: finalBody,\n status: 200,\n ...(billableUnits != null ? { billableUnits } : {}),\n },\n };\n const persistResult = persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n // Surface a persist failure on the envelope (parity with the generic\n // recorder relay and the OpenRouter failed branch) — the synthesized\n // envelope below has not been written yet, so the header can still ride it.\n if (persistResult.kind === \"failed\" && !res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n }\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n // Captured from the upstream result's x-fal-billable-units header so the\n // same-session replay (before the persisted fixture is reloaded) already\n // surfaces the billed quantity.\n billableUnits,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry. The persisted fixture and seeded state above stay — the\n // captured upstream response is valuable regardless.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n extraHeaders?: Record<string, string>,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\", ...extraHeaders });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+
2
+ //#region src/provider-auth.ts
3
+ /**
4
+ * Default marker prefix identifying a "dummy" credential that a caller sends
5
+ * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).
6
+ * A caller credential that is absent OR begins with this prefix is treated as a
7
+ * placeholder that aimock is free to override with its own built-in upstream key.
8
+ * A caller credential that does NOT begin with this prefix is treated as a real
9
+ * key and forwarded verbatim (the caller overrides aimock).
10
+ *
11
+ * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint
12
+ * placeholder keys under a different prefix.
13
+ */
14
+ const DEFAULT_DUMMY_KEY_MARKER = "sk-aimock-";
15
+ /** Resolve the active dummy-key marker, honoring the env override. */
16
+ function getDummyKeyMarker() {
17
+ const override = process.env.AIMOCK_DUMMY_KEY_MARKER;
18
+ return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;
19
+ }
20
+ const PROVIDER_AUTH_SCHEMES = {
21
+ openai: { kind: "bearer" },
22
+ openrouter: { kind: "bearer" },
23
+ cohere: { kind: "bearer" },
24
+ grok: { kind: "bearer" },
25
+ ollama: { kind: "bearer" },
26
+ anthropic: { kind: "x-api-key" },
27
+ gemini: { kind: "x-goog-api-key" },
28
+ "gemini-interactions": { kind: "x-goog-api-key" },
29
+ veo: { kind: "x-goog-api-key" },
30
+ azure: { kind: "api-key" },
31
+ elevenlabs: { kind: "xi-api-key" },
32
+ fal: { kind: "fal-key" }
33
+ };
34
+ /**
35
+ * Case-insensitively delete a header from a forward-header map, returning the
36
+ * value that was present (if any). Header maps preserve the original casing of
37
+ * incoming headers, so we can't assume a canonical key.
38
+ */
39
+ function takeHeader(headers, name) {
40
+ const lowerName = name.toLowerCase();
41
+ let found;
42
+ for (const key of Object.keys(headers)) if (key.toLowerCase() === lowerName) {
43
+ found = headers[key];
44
+ delete headers[key];
45
+ }
46
+ return found;
47
+ }
48
+ /**
49
+ * Extract the caller's credential value for a given auth scheme from the
50
+ * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.
51
+ */
52
+ function readCallerCredential(headers, scheme) {
53
+ if (scheme.kind === "bearer" || scheme.kind === "fal-key") {
54
+ const raw = scanHeader(headers, "authorization");
55
+ if (raw === void 0) return void 0;
56
+ const prefix = scheme.kind === "fal-key" ? "Key" : "Bearer";
57
+ const match = new RegExp(`^\\s*${prefix}\\s+(.+)$`, "i").exec(raw);
58
+ return match ? match[1].trim() : raw.trim();
59
+ }
60
+ return scanHeader(headers, authHeaderName(scheme));
61
+ }
62
+ /**
63
+ * The wire header name a non-Authorization scheme carries its credential in.
64
+ * (bearer/fal-key both use `Authorization` and are handled separately.)
65
+ */
66
+ function authHeaderName(scheme) {
67
+ switch (scheme.kind) {
68
+ case "x-api-key": return "x-api-key";
69
+ case "x-goog-api-key": return "x-goog-api-key";
70
+ case "api-key": return "api-key";
71
+ case "xi-api-key": return "xi-api-key";
72
+ case "bearer":
73
+ case "fal-key": return "authorization";
74
+ }
75
+ }
76
+ /** Case-insensitive header lookup that does not mutate the map. */
77
+ function scanHeader(headers, name) {
78
+ const lowerName = name.toLowerCase();
79
+ for (const key of Object.keys(headers)) if (key.toLowerCase() === lowerName) return headers[key];
80
+ }
81
+ /**
82
+ * Decide whether aimock should override the caller's credential with its own
83
+ * built-in key. Override when the caller sent no credential OR sent a dummy
84
+ * placeholder (prefixed with the active marker). A real caller key is left
85
+ * untouched so the caller can always override aimock.
86
+ */
87
+ function shouldOverride(callerCredential, marker) {
88
+ if (callerCredential === void 0 || callerCredential.length === 0) return true;
89
+ return callerCredential.startsWith(marker);
90
+ }
91
+ /**
92
+ * Inject aimock's built-in upstream credential into the forwarded headers when
93
+ * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place
94
+ * and may mutate `target` (query-param schemes; none currently).
95
+ *
96
+ * Precedence:
97
+ * - No built-in key configured for this provider → no-op (forward verbatim).
98
+ * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).
99
+ * - Caller credential absent or dummy-prefixed → inject built-in key.
100
+ * - Caller credential is a real key → forward it unchanged.
101
+ *
102
+ * @param forwardHeaders headers already built by buildForwardHeaders(req)
103
+ * @param target upstream URL (reserved for query-param auth schemes)
104
+ * @param providerKey which provider this request is routed to
105
+ * @param builtinKey aimock's configured key for this provider (if any)
106
+ */
107
+ function applyProviderAuth(forwardHeaders, target, providerKey, builtinKey) {
108
+ if (!builtinKey) return;
109
+ const scheme = PROVIDER_AUTH_SCHEMES[providerKey];
110
+ if (!scheme) return;
111
+ const marker = getDummyKeyMarker();
112
+ if (!shouldOverride(readCallerCredential(forwardHeaders, scheme), marker)) return;
113
+ switch (scheme.kind) {
114
+ case "bearer":
115
+ takeHeader(forwardHeaders, "authorization");
116
+ forwardHeaders["Authorization"] = `Bearer ${builtinKey}`;
117
+ break;
118
+ case "fal-key":
119
+ takeHeader(forwardHeaders, "authorization");
120
+ forwardHeaders["Authorization"] = `Key ${builtinKey}`;
121
+ break;
122
+ case "x-api-key":
123
+ takeHeader(forwardHeaders, "x-api-key");
124
+ forwardHeaders["x-api-key"] = builtinKey;
125
+ break;
126
+ case "x-goog-api-key":
127
+ takeHeader(forwardHeaders, "x-goog-api-key");
128
+ forwardHeaders["x-goog-api-key"] = builtinKey;
129
+ break;
130
+ case "api-key":
131
+ takeHeader(forwardHeaders, "api-key");
132
+ forwardHeaders["api-key"] = builtinKey;
133
+ break;
134
+ case "xi-api-key":
135
+ takeHeader(forwardHeaders, "xi-api-key");
136
+ forwardHeaders["xi-api-key"] = builtinKey;
137
+ break;
138
+ }
139
+ }
140
+ /**
141
+ * Read per-provider built-in keys from the environment. Returns undefined when
142
+ * no provider key is configured so the feature stays fully inert by default.
143
+ *
144
+ * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An
145
+ * empty-string value is treated as unset (existing truthiness pattern), keeping
146
+ * the feature inert. `gemini-interactions` is not read here: it reuses the
147
+ * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth
148
+ * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites
149
+ * their auth.
150
+ */
151
+ function readProviderKeysFromEnv(env = process.env) {
152
+ const keys = {};
153
+ if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;
154
+ if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;
155
+ if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;
156
+ if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;
157
+ if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;
158
+ if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;
159
+ if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;
160
+ if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;
161
+ if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;
162
+ if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;
163
+ if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;
164
+ return Object.keys(keys).length > 0 ? keys : void 0;
165
+ }
166
+
167
+ //#endregion
168
+ exports.applyProviderAuth = applyProviderAuth;
169
+ exports.readProviderKeysFromEnv = readProviderKeysFromEnv;
170
+ //# sourceMappingURL=provider-auth.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-auth.cjs","names":[],"sources":["../src/provider-auth.ts"],"sourcesContent":["import type { RecordProviderKey } from \"./types.js\";\n\n/**\n * Default marker prefix identifying a \"dummy\" credential that a caller sends\n * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).\n * A caller credential that is absent OR begins with this prefix is treated as a\n * placeholder that aimock is free to override with its own built-in upstream key.\n * A caller credential that does NOT begin with this prefix is treated as a real\n * key and forwarded verbatim (the caller overrides aimock).\n *\n * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint\n * placeholder keys under a different prefix.\n */\nexport const DEFAULT_DUMMY_KEY_MARKER = \"sk-aimock-\";\n\n/** Resolve the active dummy-key marker, honoring the env override. */\nexport function getDummyKeyMarker(): string {\n const override = process.env.AIMOCK_DUMMY_KEY_MARKER;\n return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;\n}\n\n/**\n * How a given provider carries its API credential on the wire. aimock injects\n * its built-in key using the provider's native scheme so the upstream accepts\n * it. Providers whose auth is a signed/exchanged credential (Bedrock SigV4,\n * Vertex AI OAuth) are deliberately absent — those are NOT simple static-key\n * schemes, so aimock never rewrites their auth and always forwards the caller's\n * credential unchanged.\n *\n * Scheme kinds (all static long-lived keys):\n * - bearer `Authorization: Bearer <key>` (OpenAI, OpenRouter, Cohere, Grok/xAI, Ollama)\n * - fal-key `Authorization: Key <key>` (fal.ai — note the `Key ` prefix, NOT `Bearer`)\n * - x-api-key `x-api-key: <key>` (Anthropic)\n * - x-goog-api-key `x-goog-api-key: <key>` (Gemini, Gemini Interactions, Veo)\n * - api-key `api-key: <key>` (Azure OpenAI static-key auth)\n * - xi-api-key `xi-api-key: <key>` (ElevenLabs)\n *\n * Note on Azure: Azure OpenAI also supports Microsoft Entra ID `Authorization:\n * Bearer <token>` OAuth. aimock only ever injects the static `api-key` header,\n * and a real Entra bearer token from the caller is never dummy-prefixed, so it\n * is always forwarded verbatim (the caller overrides aimock).\n */\ntype AuthScheme =\n | { kind: \"bearer\" } // Authorization: Bearer <key>\n | { kind: \"fal-key\" } // Authorization: Key <key>\n | { kind: \"x-api-key\" } // x-api-key: <key>\n | { kind: \"x-goog-api-key\" } // x-goog-api-key: <key>\n | { kind: \"api-key\" } // api-key: <key>\n | { kind: \"xi-api-key\" }; // xi-api-key: <key>\n\nconst PROVIDER_AUTH_SCHEMES: Partial<Record<RecordProviderKey, AuthScheme>> = {\n // Bearer-token providers.\n openai: { kind: \"bearer\" },\n openrouter: { kind: \"bearer\" },\n cohere: { kind: \"bearer\" },\n grok: { kind: \"bearer\" }, // xAI\n ollama: { kind: \"bearer\" }, // Ollama Cloud / bearer-gated Ollama servers\n // Anthropic.\n anthropic: { kind: \"x-api-key\" },\n // Google (Gemini + Veo) use the x-goog-api-key header scheme.\n gemini: { kind: \"x-goog-api-key\" },\n \"gemini-interactions\": { kind: \"x-goog-api-key\" },\n veo: { kind: \"x-goog-api-key\" },\n // Azure OpenAI static-key auth.\n azure: { kind: \"api-key\" },\n // ElevenLabs.\n elevenlabs: { kind: \"xi-api-key\" },\n // fal.ai — uses the `Key ` prefix on Authorization, NOT `Bearer `.\n fal: { kind: \"fal-key\" },\n};\n\n/**\n * Case-insensitively delete a header from a forward-header map, returning the\n * value that was present (if any). Header maps preserve the original casing of\n * incoming headers, so we can't assume a canonical key.\n */\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n let found: string | undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) {\n found = headers[key];\n delete headers[key];\n }\n }\n return found;\n}\n\n/**\n * Extract the caller's credential value for a given auth scheme from the\n * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.\n */\nfunction readCallerCredential(\n headers: Record<string, string>,\n scheme: AuthScheme,\n): string | undefined {\n if (scheme.kind === \"bearer\" || scheme.kind === \"fal-key\") {\n const raw = scanHeader(headers, \"authorization\");\n if (raw === undefined) return undefined;\n // Strip the scheme prefix (`Bearer ` for bearer, `Key ` for fal) if present,\n // otherwise treat the whole value as the credential.\n const prefix = scheme.kind === \"fal-key\" ? \"Key\" : \"Bearer\";\n const match = new RegExp(`^\\\\s*${prefix}\\\\s+(.+)$`, \"i\").exec(raw);\n return match ? match[1].trim() : raw.trim();\n }\n return scanHeader(headers, authHeaderName(scheme));\n}\n\n/**\n * The wire header name a non-Authorization scheme carries its credential in.\n * (bearer/fal-key both use `Authorization` and are handled separately.)\n */\nfunction authHeaderName(scheme: AuthScheme): string {\n switch (scheme.kind) {\n case \"x-api-key\":\n return \"x-api-key\";\n case \"x-goog-api-key\":\n return \"x-goog-api-key\";\n case \"api-key\":\n return \"api-key\";\n case \"xi-api-key\":\n return \"xi-api-key\";\n case \"bearer\":\n case \"fal-key\":\n return \"authorization\";\n }\n}\n\n/** Case-insensitive header lookup that does not mutate the map. */\nfunction scanHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Decide whether aimock should override the caller's credential with its own\n * built-in key. Override when the caller sent no credential OR sent a dummy\n * placeholder (prefixed with the active marker). A real caller key is left\n * untouched so the caller can always override aimock.\n */\nfunction shouldOverride(callerCredential: string | undefined, marker: string): boolean {\n if (callerCredential === undefined || callerCredential.length === 0) return true;\n return callerCredential.startsWith(marker);\n}\n\n/**\n * Inject aimock's built-in upstream credential into the forwarded headers when\n * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place\n * and may mutate `target` (query-param schemes; none currently).\n *\n * Precedence:\n * - No built-in key configured for this provider → no-op (forward verbatim).\n * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).\n * - Caller credential absent or dummy-prefixed → inject built-in key.\n * - Caller credential is a real key → forward it unchanged.\n *\n * @param forwardHeaders headers already built by buildForwardHeaders(req)\n * @param target upstream URL (reserved for query-param auth schemes)\n * @param providerKey which provider this request is routed to\n * @param builtinKey aimock's configured key for this provider (if any)\n */\nexport function applyProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n builtinKey: string | undefined,\n): void {\n void target; // reserved for future query-param schemes (e.g. Gemini ?key=)\n if (!builtinKey) return; // feature inert for this provider\n\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return; // signed/OAuth provider — never rewrite auth\n\n const marker = getDummyKeyMarker();\n const callerCredential = readCallerCredential(forwardHeaders, scheme);\n if (!shouldOverride(callerCredential, marker)) return; // caller sent a real key\n\n // Strip any existing auth headers for this scheme (across casings) before\n // setting aimock's key, so a dummy caller key can't linger alongside ours.\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Bearer ${builtinKey}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Key ${builtinKey}`;\n break;\n case \"x-api-key\":\n takeHeader(forwardHeaders, \"x-api-key\");\n forwardHeaders[\"x-api-key\"] = builtinKey;\n break;\n case \"x-goog-api-key\":\n takeHeader(forwardHeaders, \"x-goog-api-key\");\n forwardHeaders[\"x-goog-api-key\"] = builtinKey;\n break;\n case \"api-key\":\n takeHeader(forwardHeaders, \"api-key\");\n forwardHeaders[\"api-key\"] = builtinKey;\n break;\n case \"xi-api-key\":\n takeHeader(forwardHeaders, \"xi-api-key\");\n forwardHeaders[\"xi-api-key\"] = builtinKey;\n break;\n }\n}\n\n/**\n * Read per-provider built-in keys from the environment. Returns undefined when\n * no provider key is configured so the feature stays fully inert by default.\n *\n * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An\n * empty-string value is treated as unset (existing truthiness pattern), keeping\n * the feature inert. `gemini-interactions` is not read here: it reuses the\n * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth\n * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites\n * their auth.\n */\nexport function readProviderKeysFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Partial<Record<RecordProviderKey, string>> | undefined {\n const keys: Partial<Record<RecordProviderKey, string>> = {};\n if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;\n if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;\n if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;\n if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;\n if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;\n if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;\n if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;\n if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;\n if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;\n if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;\n if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;\n return Object.keys(keys).length > 0 ? keys : undefined;\n}\n"],"mappings":"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"}