import type { IncomingMessage, ServerResponse } from 'http'; import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; import { basename, extname, isAbsolute, join, normalize, resolve } from 'node:path'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; import { proxyAwareFetch } from '../../utils/proxy-fetch'; import { WORKS_SQUARE_CONFIG } from '../works-config'; import { readWorksDeployCheck } from '../../opencode/works-square-deploy-check'; import { readWorksPublishFile } from '../../opencode/works-publish-file'; type CreateProjectInput = { accessToken?: unknown; project?: unknown; }; type UploadProjectVersionInput = { accessToken?: unknown; projectId?: unknown; versionName?: unknown; changeLog?: unknown; zipFilePath?: unknown; }; type DownloadAssetInput = { projectId?: unknown; }; type SpeechTranscriptionInput = { accessToken?: unknown; audioBase64?: unknown; fileName?: unknown; mimeType?: unknown; language?: unknown; prompt?: unknown; model?: unknown; }; type ImageGenerationInput = Record; type AgentProfileUpdateInput = Record; function readRequiredString(value: unknown, field: string): string { if (typeof value !== 'string' || !value.trim()) { throw new Error(`Missing ${field}`); } return value.trim(); } function readOptionalString(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined; } function resolveProjectFilePath(projectPath: string, value: string): string { return isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) ? value : resolve(projectPath, value); } function readRequiredHeader(req: IncomingMessage, name: string): string { const value = req.headers[name.toLowerCase()]; const firstValue = Array.isArray(value) ? value[0] : value; return readRequiredString(firstValue, name); } function normalizeWorksBase(value = WORKS_SQUARE_CONFIG.apiBaseUrl): string { const apiBase = value.replace(/\/+$/, ''); if (!/^https?:\/\//i.test(apiBase)) { throw new Error('Works Square API base URL must start with http:// or https://'); } return apiBase; } function createWorksUrl(pathname: string): URL { return new URL(`${normalizeWorksBase()}${pathname}`); } async function readResponsePayload(response: Response): Promise { const text = await response.text(); if (!text.trim()) return null; try { return JSON.parse(text) as unknown; } catch { return text; } } function getErrorMessage(payload: unknown, fallback: string): string { if (payload && typeof payload === 'object') { const record = payload as Record; for (const field of ['msg', 'message', 'error_description', 'error', 'detail']) { const value = record[field]; if (typeof value === 'string' && value.trim()) { return value; } } } if (typeof payload === 'string' && payload.trim()) { return payload; } return fallback; } async function sendUpstreamError( res: ServerResponse, response: Response, fallback: string, ): Promise { const payload = await readResponsePayload(response); sendJson(res, response.status >= 400 && response.status < 500 ? response.status : 502, { success: false, status: response.status, error: getErrorMessage(payload, fallback), }); } function appendOptionalSearchParam(target: URL, source: URLSearchParams, name: string): void { const value = source.get(name); if (value !== null && value.trim()) { target.searchParams.set(name, value.trim()); } } function unwrapPayload(payload: unknown, field: string): unknown { if (payload && typeof payload === 'object' && !Array.isArray(payload)) { const record = payload as Record; if (field in record) return record[field]; } return payload; } async function handleListProjects(res: ServerResponse, url: URL): Promise { const upstreamUrl = createWorksUrl('/api/projects'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'q'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'category'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'limit'); const response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET' }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square list failed (${response.status})`); return; } sendJson(res, 200, { success: true, page: await readResponsePayload(response) }); } async function handleGetProject(res: ServerResponse, appId: string): Promise { const upstreamUrl = createWorksUrl(`/api/projects/${encodeURIComponent(appId)}`); const response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET' }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square detail failed (${response.status})`); return; } sendJson(res, 200, { success: true, project: await readResponsePayload(response) }); } async function handleListAssets(res: ServerResponse, url: URL): Promise { const upstreamUrl = createWorksUrl('/api/assets'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'q'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'category'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'tag'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'source'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'limit'); const response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET' }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square asset list failed (${response.status})`); return; } sendJson(res, 200, { success: true, assets: await readResponsePayload(response) }); } async function handleGetAsset(res: ServerResponse, slug: string): Promise { const upstreamUrl = createWorksUrl(`/api/assets/${encodeURIComponent(slug)}`); const response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET' }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square asset detail failed (${response.status})`); return; } sendJson(res, 200, { success: true, asset: await readResponsePayload(response) }); } function assetZipFileName(slug: string): string { if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) { throw new Error('Invalid asset slug'); } return `${slug}.zip`; } function isRedirectStatus(status: number): boolean { return status === 301 || status === 302 || status === 303 || status === 307 || status === 308; } async function readArchiveBytesFromDownloadUrl(downloadUrl: string): Promise { let currentUrl = downloadUrl; for (let redirects = 0; redirects <= 5; redirects += 1) { const response = await proxyAwareFetch(currentUrl, { method: 'GET', redirect: 'manual' }); if (isRedirectStatus(response.status)) { const location = response.headers.get('location'); if (!location) { throw new Error(`Works Square asset download redirect missing Location (${response.status})`); } currentUrl = new URL(location, currentUrl).toString(); continue; } if (!response.ok) { throw new Error(`Works Square asset download failed (${response.status})`); } return Buffer.from(await response.arrayBuffer()); } throw new Error('Works Square asset download redirected too many times'); } async function handleDownloadAssetToProject( req: IncomingMessage, res: ServerResponse, slug: string, ctx: HostApiContext, ): Promise { const body = await parseJsonBody(req); const projectId = readRequiredString(body.projectId, 'projectId'); const fileName = assetZipFileName(slug); const projects = await ctx.opencodeProjectStore.listProjects(); const project = projects.find((candidate) => candidate.id === projectId); if (!project) { throw new Error('Project not found'); } const projectStat = await stat(project.path); if (!projectStat.isDirectory()) { throw new Error('Project path must point to a directory'); } const archiveBytes = await readArchiveBytesFromDownloadUrl( createWorksUrl(`/api/assets/${encodeURIComponent(slug)}/download`).toString(), ); const relativePath = `assets/resource-square/${fileName}`; const targetDir = join(project.path, 'assets', 'resource-square'); await mkdir(targetDir, { recursive: true }); const filePath = join(targetDir, fileName); await writeFile(filePath, archiveBytes); sendJson(res, 200, { success: true, download: { slug, filePath, relativePath, bytesWritten: archiveBytes.byteLength, }, }); } async function handleCreateProject(req: IncomingMessage, res: ServerResponse): Promise { const body = await parseJsonBody(req); const accessToken = readRequiredString(body.accessToken, 'accessToken'); if (!body.project || typeof body.project !== 'object' || Array.isArray(body.project)) { throw new Error('Missing project'); } const response = await proxyAwareFetch(createWorksUrl('/api/projects').toString(), { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body.project), }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square project create failed (${response.status})`); return; } sendJson(res, response.status, { success: true, project: await readResponsePayload(response) }); } async function handleListMyProjects( req: IncomingMessage, res: ServerResponse, url: URL, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const upstreamUrl = createWorksUrl('/api/projects/mine'); appendOptionalSearchParam(upstreamUrl, url.searchParams, 'limit'); const response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square my projects failed (${response.status})`); return; } sendJson(res, response.status, { success: true, page: await readResponsePayload(response) }); } async function handleGetBillingTokenUsage( req: IncomingMessage, res: ServerResponse, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const response = await proxyAwareFetch(createWorksUrl('/api/billing/token-usage').toString(), { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square token usage failed (${response.status})`); return; } sendJson(res, response.status, { success: true, usage: await readResponsePayload(response) }); } async function handleAgentProfile( req: IncomingMessage, res: ServerResponse, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const body = req.method === 'PUT' ? await parseJsonBody(req) : undefined; const response = await proxyAwareFetch(createWorksUrl('/api/user/agent-profile').toString(), { method: req.method, headers: { Authorization: `Bearer ${accessToken}`, ...(body ? { 'Content-Type': 'application/json' } : {}), }, ...(body ? { body: JSON.stringify(body) } : {}), }); const payload = await readResponsePayload(response); if (!response.ok) { const detail = payload && typeof payload === 'object' && !Array.isArray(payload) ? (payload as Record).detail : undefined; // Keep the local Host API response successful so the renderer can inspect // the upstream status and conflict detail instead of losing it in the // generic Host API error parser. sendJson(res, 200, { success: false, status: response.status, error: getErrorMessage(payload, `Agent Profile request failed (${response.status})`), ...(detail === undefined ? {} : { detail }), }); return; } sendJson(res, response.status, { success: true, profile: payload }); } async function handleSubmitImageGeneration( req: IncomingMessage, res: ServerResponse, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const body = await parseJsonBody(req); const response = await proxyAwareFetch(createWorksUrl('/api/ai-gateway/images/generations').toString(), { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square image generation submit failed (${response.status})`); return; } const payload = await readResponsePayload(response); sendJson(res, response.status, { success: true, job: unwrapPayload(payload, 'job'), }); } async function handleGetImageGenerationTask( req: IncomingMessage, res: ServerResponse, taskId: string, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const response = await proxyAwareFetch( createWorksUrl(`/api/ai-gateway/images/tasks/${encodeURIComponent(taskId)}`).toString(), { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }, ); if (!response.ok) { await sendUpstreamError(res, response, `Works Square image generation task failed (${response.status})`); return; } const payload = await readResponsePayload(response); sendJson(res, response.status, { success: true, job: unwrapPayload(payload, 'job'), }); } async function handleGetMyProjectStatus( req: IncomingMessage, res: ServerResponse, appId: string, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const response = await proxyAwareFetch( createWorksUrl(`/api/projects/mine/${encodeURIComponent(appId)}/status`).toString(), { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }, ); if (!response.ok) { await sendUpstreamError(res, response, `Works Square project status failed (${response.status})`); return; } sendJson(res, response.status, { success: true, status: await readResponsePayload(response) }); } async function createArchiveFormData( body: UploadProjectVersionInput, ctx: HostApiContext, appId: string, ): Promise { const versionName = readRequiredString(body.versionName, 'versionName'); const changeLog = readRequiredString(body.changeLog, 'changeLog'); const zipFilePath = readRequiredString(body.zipFilePath, 'zipFilePath'); const projectId = readRequiredString(body.projectId, 'projectId'); const project = (await ctx.opencodeProjectStore.listProjects()).find((item) => item.id === projectId); if (!project) throw new Error('Project not found'); const publish = await readWorksPublishFile(project.path); if (publish.status !== 'ready') throw new Error('BLOCKED: works-publish.json is missing or invalid'); if (publish.publish.app_id !== appId) throw new Error('BLOCKED: upload app id does not match project publish data'); const resolvedZipPath = resolveProjectFilePath(project.path, zipFilePath); const publishZipPath = resolveProjectFilePath(project.path, publish.publish.zip_file_path); if (normalize(publishZipPath).toLowerCase() !== normalize(resolvedZipPath).toLowerCase()) { throw new Error('BLOCKED: upload zip path does not match project publish data'); } const deployCheck = await readWorksDeployCheck(project.path, publish.publish); if (deployCheck.status !== 'pass' && deployCheck.status !== 'warning') { throw new Error(`BLOCKED: ${deployCheck.error || 'deployment checks did not pass'}`); } if (extname(resolvedZipPath).toLowerCase() !== '.zip') { throw new Error('zipFilePath must point to a .zip file'); } const archiveStat = await stat(resolvedZipPath); if (!archiveStat.isFile()) { throw new Error('zipFilePath must point to a file'); } const archiveBytes = await readFile(resolvedZipPath); const archiveBlob = new Blob([new Uint8Array(archiveBytes)], { type: 'application/zip' }); const form = new FormData(); form.set('version_name', versionName); form.set('change_log', changeLog); form.set('archive', archiveBlob, basename(resolvedZipPath)); return form; } async function handleUploadProjectVersion( req: IncomingMessage, res: ServerResponse, appId: string, ctx: HostApiContext, ): Promise { const body = await parseJsonBody(req); const accessToken = readRequiredString(body.accessToken, 'accessToken'); const form = await createArchiveFormData(body, ctx, appId); const response = await proxyAwareFetch( createWorksUrl(`/api/projects/${encodeURIComponent(appId)}/versions/upload`).toString(), { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, }, body: form, }, ); if (!response.ok) { await sendUpstreamError(res, response, `Works Square upload failed (${response.status})`); return; } sendJson(res, response.status, { success: true, upload: await readResponsePayload(response) }); } function createSpeechTranscriptionFormData(body: SpeechTranscriptionInput): FormData { const audioBase64 = readRequiredString(body.audioBase64, 'audioBase64'); const fileName = readOptionalString(body.fileName) ?? 'voice.wav'; const mimeType = readOptionalString(body.mimeType) ?? 'audio/wav'; const audioBytes = Buffer.from(audioBase64, 'base64'); if (audioBytes.length === 0) { throw new Error('audioBase64 must contain audio data'); } const form = new FormData(); const audioBlob = new Blob([new Uint8Array(audioBytes)], { type: mimeType }); form.set('audio', audioBlob, fileName); const language = readOptionalString(body.language); const prompt = readOptionalString(body.prompt); const model = readOptionalString(body.model); if (language) form.set('language', language); if (prompt) form.set('prompt', prompt); if (model) form.set('model', model); return form; } async function handleSpeechTranscription( req: IncomingMessage, res: ServerResponse, ): Promise { const body = await parseJsonBody(req); const accessToken = readRequiredString(body.accessToken, 'accessToken'); const form = createSpeechTranscriptionFormData(body); const response = await proxyAwareFetch(createWorksUrl('/api/speech/transcriptions').toString(), { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, }, body: form, }); if (!response.ok) { await sendUpstreamError(res, response, `Works Square speech transcription failed (${response.status})`); return; } sendJson(res, response.status, { success: true, transcription: await readResponsePayload(response) }); } async function handleListProjectVersions( req: IncomingMessage, res: ServerResponse, appId: string, ): Promise { const accessToken = readRequiredHeader(req, 'x-niancode-access-token'); const response = await proxyAwareFetch( createWorksUrl(`/api/projects/${encodeURIComponent(appId)}/versions`).toString(), { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }, ); if (!response.ok) { await sendUpstreamError(res, response, `Works Square versions failed (${response.status})`); return; } sendJson(res, response.status, { success: true, versions: await readResponsePayload(response) }); } export async function handleWorksRoutes( req: IncomingMessage, res: ServerResponse, url: URL, ctx: HostApiContext, ): Promise { if (!url.pathname.startsWith('/api/works')) { return false; } try { if (url.pathname === '/api/works/user/agent-profile' && (req.method === 'GET' || req.method === 'PUT')) { await handleAgentProfile(req, res); return true; } if (url.pathname === '/api/works/speech/transcriptions' && req.method === 'POST') { await handleSpeechTranscription(req, res); return true; } if (url.pathname === '/api/works/billing/token-usage' && req.method === 'GET') { await handleGetBillingTokenUsage(req, res); return true; } if (url.pathname === '/api/works/ai-gateway/images/generations' && req.method === 'POST') { await handleSubmitImageGeneration(req, res); return true; } const imageTaskPrefix = '/api/works/ai-gateway/images/tasks/'; if (url.pathname.startsWith(imageTaskPrefix) && req.method === 'GET') { const taskId = decodeURIComponent(url.pathname.slice(imageTaskPrefix.length)); if (!taskId) { sendJson(res, 400, { success: false, error: 'Missing task_id' }); return true; } await handleGetImageGenerationTask(req, res, taskId); return true; } if (url.pathname === '/api/works/assets' && req.method === 'GET') { await handleListAssets(res, url); return true; } const assetDownloadMatch = url.pathname.match(/^\/api\/works\/assets\/([^/]+)\/download$/); if (assetDownloadMatch && req.method === 'POST') { await handleDownloadAssetToProject(req, res, decodeURIComponent(assetDownloadMatch[1]), ctx); return true; } const assetDetailMatch = url.pathname.match(/^\/api\/works\/assets\/([^/]+)$/); if (assetDetailMatch && req.method === 'GET') { await handleGetAsset(res, decodeURIComponent(assetDetailMatch[1])); return true; } if (url.pathname === '/api/works/projects' && req.method === 'GET') { await handleListProjects(res, url); return true; } if (url.pathname === '/api/works/projects' && req.method === 'POST') { await handleCreateProject(req, res); return true; } if (url.pathname === '/api/works/projects/mine' && req.method === 'GET') { await handleListMyProjects(req, res, url); return true; } const myProjectStatusMatch = url.pathname.match(/^\/api\/works\/projects\/mine\/([^/]+)\/status$/); if (myProjectStatusMatch && req.method === 'GET') { await handleGetMyProjectStatus(req, res, decodeURIComponent(myProjectStatusMatch[1])); return true; } const detailMatch = url.pathname.match(/^\/api\/works\/projects\/([^/]+)$/); if (detailMatch && req.method === 'GET') { await handleGetProject(res, decodeURIComponent(detailMatch[1])); return true; } const versionsMatch = url.pathname.match(/^\/api\/works\/projects\/([^/]+)\/versions$/); if (versionsMatch && req.method === 'GET') { await handleListProjectVersions(req, res, decodeURIComponent(versionsMatch[1])); return true; } const uploadMatch = url.pathname.match(/^\/api\/works\/projects\/([^/]+)\/versions\/upload$/); if (uploadMatch && req.method === 'POST') { await handleUploadProjectVersion(req, res, decodeURIComponent(uploadMatch[1]), ctx); return true; } sendJson(res, 404, { success: false, error: `No route for ${req.method} ${url.pathname}` }); return true; } catch (error) { sendJson(res, 400, { success: false, error: error instanceof Error ? error.message : String(error), }); return true; } }