From 1b9a92a790868c24b6622e917f339fe3c52ba00d Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 1 Jun 2026 09:00:04 +0330 Subject: [PATCH] chore: drop legacy nexrender render-worker; remove @supabase, stripe, @nexrender/core, tsx packages V2 render orchestrator (render-svc) + future node-agent Go binary replace the entire server/ directory. All dead dependencies uninstalled. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 138 ------------------ package.json | 8 +- server/nexrender-job-builder.ts | 173 ---------------------- server/nexrender.d.ts | 13 -- server/render-job-processor.ts | 247 -------------------------------- server/render-worker.ts | 71 --------- 6 files changed, 1 insertion(+), 649 deletions(-) delete mode 100644 server/nexrender-job-builder.ts delete mode 100644 server/nexrender.d.ts delete mode 100644 server/render-job-processor.ts delete mode 100644 server/render-worker.ts diff --git a/package-lock.json b/package-lock.json index 9bdd65e..5feae5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,6 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", - "@supabase/ssr": "^0.10.3", - "@supabase/supabase-js": "^2.106.1", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -43,7 +41,6 @@ "react-hook-form": "^7.76.0", "react-konva": "^18.2.16", "react-rnd": "^10.5.3", - "stripe": "^22.1.1", "tailwind-merge": "^3.6.0", "use-image": "^1.1.4", "use-undoable": "^5.0.0", @@ -3455,102 +3452,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@supabase/auth-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.1.tgz", - "integrity": "sha512-7eyheXfAGwkB9bZewJPs+N3UYt6kra2JG6mIxNEgbkvcO15PLD1e75PTIUEYYl3zrifm3GrpShVl7QZxKrXO/w==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/functions-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.1.tgz", - "integrity": "sha512-XbOPnR2mW7jp/EcW447xmGwCa+/Wc00Hkw8t4tUIJjRsHQ4xAESsLKcyLRhRJjJoUnJVXUlC+w0wUxUCM7CG2A==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/phoenix": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz", - "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==", - "license": "MIT" - }, - "node_modules/@supabase/postgrest-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.1.tgz", - "integrity": "sha512-Qbn6d2lqiqeaBX1Uko0e/hL90dtQGRN6CG2wMVQtJpRFstlVW45qmUTyTOsiB8dYUWu1fWYo4YzJuDbokGv3tQ==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/realtime-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.1.tgz", - "integrity": "sha512-eQCYri5E8KsjpDgC7g28cOOS2britjUWdNSJluFMainqrMRepzjOnaxqXc3RoAz7H0dxmBrfLUNF6NGP8C+YaA==", - "license": "MIT", - "dependencies": { - "@supabase/phoenix": "^0.4.2", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/ssr": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.3.tgz", - "integrity": "sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.2" - }, - "peerDependencies": { - "@supabase/supabase-js": "^2.105.3" - } - }, - "node_modules/@supabase/storage-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.1.tgz", - "integrity": "sha512-HWcLIhqinhWKpOQ3WzglR2unjW0eh9J7yOu3IZrZNIEkraK4La/HDvTqndljGsNw0itPtyHhuKBxRoPG1VUARw==", - "license": "MIT", - "dependencies": { - "iceberg-js": "^0.8.1", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/supabase-js": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", - "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", - "license": "MIT", - "dependencies": { - "@supabase/auth-js": "2.106.1", - "@supabase/functions-js": "2.106.1", - "@supabase/postgrest-js": "2.106.1", - "@supabase/realtime-js": "2.106.1", - "@supabase/storage-js": "2.106.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@swc/core-darwin-arm64": { "version": "1.15.40", "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", @@ -5336,19 +5237,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -7176,15 +7064,6 @@ "ms": "^2.0.0" } }, - "node_modules/iceberg-js": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/icu-minify": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.12.0.tgz", @@ -10527,23 +10406,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stripe": { - "version": "22.1.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.1.tgz", - "integrity": "sha512-cmodIYP27tBkJ8G7DuGgWw0PFuemlFZbuF3Wwr1TrjFjUa3T7NIgCe6TVwX8BO2ynu+xtTuDGfHafNDCPt9lXA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", diff --git a/package.json b/package.json index 406a8a1..ab00956 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "render-worker": "tsx server/render-worker.ts" + "lint": "next lint" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -19,7 +18,6 @@ "@fontsource-variable/plus-jakarta-sans": "^5.2.8", "@fontsource/vazirmatn": "^5.2.8", "@hookform/resolvers": "^5.2.2", - "@nexrender/core": "^1.46.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -30,8 +28,6 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", - "@supabase/ssr": "^0.10.3", - "@supabase/supabase-js": "^2.106.1", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -45,7 +41,6 @@ "react-hook-form": "^7.76.0", "react-konva": "^18.2.16", "react-rnd": "^10.5.3", - "stripe": "^22.1.1", "tailwind-merge": "^3.6.0", "use-image": "^1.1.4", "use-undoable": "^5.0.0", @@ -61,7 +56,6 @@ "postcss": "^8", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", - "tsx": "^4.19.4", "typescript": "^5" } } diff --git a/server/nexrender-job-builder.ts b/server/nexrender-job-builder.ts deleted file mode 100644 index 1e9ee3d..0000000 --- a/server/nexrender-job-builder.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { RenderScene, RenderSettings } from "../src/lib/render-schemas"; -import { RESOLUTION_DIMENSIONS } from "../src/lib/render-schemas"; - -export interface NexrenderAsset { - type: string; - layerName?: string; - composition?: string; - property?: string; - value?: string | number; - src?: string; - width?: number; - height?: number; - time?: number; - [key: string]: unknown; -} - -export interface NexrenderJob { - template: { - src: string; - composition: string; - frameStart?: number; - frameEnd?: number; - }; - assets: NexrenderAsset[]; - actions?: { - postrender?: Array>; - }; - onRenderProgress?: string; - metadata?: Record; -} - -function layerToAsset( - layer: RenderScene["layers"][number], - scene: RenderScene, - sceneIndex: number -): NexrenderAsset | null { - const time = sceneIndex * scene.duration; - - switch (layer.type) { - case "text": { - const text = - typeof layer.props.text === "string" ? layer.props.text : "Text"; - return { - type: "data", - layerName: `Scene${sceneIndex + 1}_Text`, - composition: scene.name, - property: "Source Text", - value: text, - time, - }; - } - case "image": { - const src = typeof layer.props.src === "string" ? layer.props.src : ""; - if (!src) return null; - return { - type: "image", - layerName: `Scene${sceneIndex + 1}_Image`, - composition: scene.name, - src, - width: layer.width, - height: layer.height, - time, - }; - } - case "video": { - const src = typeof layer.props.src === "string" ? layer.props.src : ""; - if (!src) return null; - return { - type: "video", - layerName: `Scene${sceneIndex + 1}_Video`, - composition: scene.name, - src, - time, - }; - } - case "shape": - return { - type: "data", - layerName: `Scene${sceneIndex + 1}_Shape`, - composition: scene.name, - property: "Opacity", - value: Math.round(layer.opacity * 100), - time, - }; - default: - return null; - } -} - -export function buildNexrenderJob( - scenes: RenderScene[], - settings: RenderSettings, - jobId: string, - outputPath: string -): NexrenderJob { - const templateSrc = - process.env.NEXRENDER_TEMPLATE_SRC ?? - "file:///templates/creatorstudio-base.aep"; - const composition = - process.env.NEXRENDER_COMPOSITION ?? "CreatorStudio_Main"; - - const { width, height } = RESOLUTION_DIMENSIONS[settings.resolution]; - const totalDuration = scenes.reduce((sum, scene) => sum + scene.duration, 0); - const frameEnd = Math.ceil(totalDuration * settings.fps); - - const assets: NexrenderAsset[] = [ - { - type: "data", - layerName: "Settings", - composition, - property: "Width", - value: width, - }, - { - type: "data", - layerName: "Settings", - composition, - property: "Height", - value: height, - }, - { - type: "data", - layerName: "Settings", - composition, - property: "Frame Rate", - value: settings.fps, - }, - ]; - - scenes.forEach((scene, sceneIndex) => { - assets.push({ - type: "data", - layerName: `Scene${sceneIndex + 1}`, - composition, - property: "Duration", - value: scene.duration, - time: sceneIndex * scene.duration, - }); - - const sortedLayers = [...scene.layers].sort( - (a, b) => a.zIndex - b.zIndex - ); - - sortedLayers.forEach((layer) => { - const asset = layerToAsset(layer, scene, sceneIndex); - if (asset) assets.push(asset); - }); - }); - - return { - template: { - src: templateSrc, - composition, - frameStart: 0, - frameEnd, - }, - assets, - actions: { - postrender: [ - { - module: "@nexrender/action-copy", - output: outputPath, - }, - ], - }, - metadata: { - jobId, - resolution: settings.resolution, - fps: settings.fps, - format: settings.format, - }, - }; -} diff --git a/server/nexrender.d.ts b/server/nexrender.d.ts deleted file mode 100644 index 35dfc51..0000000 --- a/server/nexrender.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module "@nexrender/core" { - export interface NexrenderRenderOptions { - workPath?: string; - binary?: string; - skipCleanup?: boolean; - onProgress?: (job: { metadata?: Record }, percent: number) => void; - } - - export function render( - job: unknown, - options?: NexrenderRenderOptions - ): Promise; -} diff --git a/server/render-job-processor.ts b/server/render-job-processor.ts deleted file mode 100644 index 77ea1dd..0000000 --- a/server/render-job-processor.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { readFile } from "node:fs/promises"; -import path from "node:path"; - -import { createClient } from "@supabase/supabase-js"; - -import { buildNexrenderJob } from "./nexrender-job-builder"; -import type { RenderScene, RenderSettings } from "../src/lib/render-schemas"; - -function getSupabase() { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL; - const key = process.env.SUPABASE_SERVICE_ROLE_KEY; - if (!url || !key) { - throw new Error("Supabase env vars required for render worker"); - } - return createClient(url, key, { - auth: { autoRefreshToken: false, persistSession: false }, - }); -} - -async function updateJob( - jobId: string, - updates: Record -): Promise { - const supabase = getSupabase(); - await supabase.from("render_jobs").update(updates).eq("id", jobId); -} - -async function uploadToStorage( - jobId: string, - filePath: string -): Promise { - const supabase = getSupabase(); - const buffer = await readFile(filePath); - const storagePath = `${jobId}/output.mp4`; - - const { error } = await supabase.storage - .from("renders") - .upload(storagePath, buffer, { - contentType: "video/mp4", - upsert: true, - }); - - if (error) throw error; - - const { data } = supabase.storage.from("renders").getPublicUrl(storagePath); - return data.publicUrl; -} - -async function submitToNexrenderServer( - job: ReturnType -): Promise { - const serverUrl = process.env.NEXRENDER_SERVER_URL; - if (!serverUrl) { - throw new Error("NEXRENDER_SERVER_URL not configured"); - } - - const response = await fetch( - `${serverUrl.replace(/\/$/, "")}/api/v1/jobs`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(job), - } - ); - - if (!response.ok) { - throw new Error(`Nexrender server error: ${response.status}`); - } - - const payload = (await response.json()) as { uid?: string; id?: string }; - return payload.uid ?? payload.id ?? "unknown"; -} - -async function renderWithCore( - job: ReturnType, - onProgress: (percent: number, message: string) => Promise -): Promise { - const { render } = await import("@nexrender/core"); - const workPath = - process.env.NEXRENDER_WORKPATH ?? path.join(process.cwd(), ".nexrender"); - - await onProgress(10, "Starting After Effects render…"); - - const result = await render(job, { - workPath, - binary: process.env.NEXRENDER_BINARY, - skipCleanup: false, - onProgress: ( - nexJob: { metadata?: Record }, - percent: number - ) => { - const label = nexJob.metadata?.progressMessage as string | undefined; - void onProgress( - Math.min(95, Math.round(percent * 100)), - label ?? "Rendering composition…" - ); - }, - }); - - const outputPath = - typeof result === "string" - ? result - : ((result as { output?: string })?.output ?? - job.actions?.postrender?.[0]?.output); - - if (!outputPath || typeof outputPath !== "string") { - throw new Error("Nexrender did not return output path"); - } - - return outputPath; -} - -async function mockRender( - scenes: RenderScene[], - onProgress: (percent: number, message: string) => Promise -): Promise { - const total = scenes.length; - for (let i = 0; i < total; i += 1) { - const percent = Math.round(((i + 1) / total) * 90); - await onProgress( - percent, - `Rendering scene ${i + 1} of ${total}…` - ); - await new Promise((resolve) => setTimeout(resolve, 800)); - } - await onProgress(95, "Encoding MP4…"); - await new Promise((resolve) => setTimeout(resolve, 500)); - return ""; -} - -export async function processRenderJob(jobId: string): Promise { - const supabase = getSupabase(); - const { data: row, error } = await supabase - .from("render_jobs") - .select("*") - .eq("id", jobId) - .single(); - - if (error || !row) { - throw new Error(`Job ${jobId} not found`); - } - - const scenes = row.scenes as RenderScene[]; - const settings = row.settings as RenderSettings; - const totalScenes = scenes.length; - - const onProgress = async (percent: number, message: string) => { - await updateJob(jobId, { - status: "processing", - progress: percent, - progress_message: message, - }); - }; - - await updateJob(jobId, { - status: "processing", - progress: 2, - progress_message: "Preparing render…", - }); - - const workDir = process.env.NEXRENDER_WORKPATH ?? path.join(process.cwd(), ".nexrender"); - const outputPath = path.join(workDir, "output", `${jobId}.mp4`); - - try { - const nexrenderJob = buildNexrenderJob( - scenes, - settings, - jobId, - outputPath - ); - - let renderedPath = ""; - - const useMock = - process.env.RENDER_MOCK === "true" || - (!process.env.NEXRENDER_SERVER_URL && - !process.env.NEXRENDER_BINARY && - !process.env.NEXRENDER_TEMPLATE_SRC); - - if (useMock) { - await mockRender(scenes, onProgress); - await onProgress(96, "Uploading to storage…"); - const placeholder = Buffer.from( - "Mock render — configure NEXRENDER_BINARY or RENDER_MOCK=false" - ); - const storagePath = `${jobId}/output.mp4`; - await supabase.storage.from("renders").upload(storagePath, placeholder, { - contentType: "text/plain", - upsert: true, - }); - const { data: urlData } = supabase.storage - .from("renders") - .getPublicUrl(storagePath); - await updateJob(jobId, { - status: "completed", - progress: 100, - progress_message: "Render complete (mock)", - output_url: urlData.publicUrl, - }); - return; - } - - if (process.env.NEXRENDER_SERVER_URL) { - await onProgress(15, "Submitting to nexrender server…"); - const uid = await submitToNexrenderServer(nexrenderJob); - await onProgress(25, `Nexrender job ${uid} started…`); - - for (let i = 0; i < totalScenes; i += 1) { - await onProgress( - 25 + Math.round(((i + 1) / totalScenes) * 60), - `Rendering scene ${i + 1} of ${totalScenes}…` - ); - await new Promise((resolve) => setTimeout(resolve, 1200)); - } - - renderedPath = outputPath; - } else { - for (let i = 0; i < totalScenes; i += 1) { - await onProgress( - 10 + Math.round((i / totalScenes) * 20), - `Rendering scene ${i + 1} of ${totalScenes}…` - ); - } - renderedPath = await renderWithCore(nexrenderJob, onProgress); - } - - await onProgress(96, "Uploading to storage…"); - const publicUrl = await uploadToStorage(jobId, renderedPath); - - await updateJob(jobId, { - status: "completed", - progress: 100, - progress_message: "Render complete", - output_url: publicUrl, - }); - } catch (err) { - const message = - err instanceof Error ? err.message : "Unknown render error"; - await updateJob(jobId, { - status: "failed", - progress: 0, - progress_message: "Render failed", - error_message: message, - }); - throw err; - } -} diff --git a/server/render-worker.ts b/server/render-worker.ts deleted file mode 100644 index fabdbd3..0000000 --- a/server/render-worker.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Standalone render worker — run: npm run render-worker - * POST /process { jobId } — requires RENDER_WORKER_SECRET if set - */ -import http from "node:http"; - -import { processRenderJob } from "./render-job-processor"; - -const PORT = Number(process.env.RENDER_WORKER_PORT ?? 3355); -const SECRET = process.env.RENDER_WORKER_SECRET; - -function isAuthorized(request: http.IncomingMessage): boolean { - if (!SECRET) return true; - const header = request.headers.authorization; - return header === `Bearer ${SECRET}`; -} - -function readBody(request: http.IncomingMessage): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - request.on("data", (chunk) => chunks.push(Buffer.from(chunk))); - request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); - request.on("error", reject); - }); -} - -const server = http.createServer(async (request, response) => { - const url = request.url ?? "/"; - - if (request.method === "GET" && url === "/health") { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ ok: true })); - return; - } - - if (request.method === "POST" && url === "/process") { - if (!isAuthorized(request)) { - response.writeHead(401, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ error: "Unauthorized" })); - return; - } - - try { - const body = JSON.parse(await readBody(request)) as { jobId?: string }; - if (!body.jobId) { - response.writeHead(400, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ error: "jobId required" })); - return; - } - - response.writeHead(202, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ accepted: true, jobId: body.jobId })); - - void processRenderJob(body.jobId).catch((err) => { - console.error(`Render job ${body.jobId} failed:`, err); - }); - } catch { - response.writeHead(400, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ error: "Invalid JSON" })); - } - return; - } - - response.writeHead(404, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ error: "Not found" })); -}); - -server.listen(PORT, () => { - console.log(`Render worker listening on http://localhost:${PORT}`); - console.log("Endpoints: GET /health, POST /process"); -});