From 2a94cdc024075cb5e8837bfa6283e49d5026d015 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Sat, 18 Apr 2026 10:23:14 +0800 Subject: [PATCH] fix: add FEATURE_NOT_INSTALLED guard to all 10 remaining AI tool routes The guard was only present on restore-photo.ts and the createToolRoute factory. All other AI tools used custom route handlers that bypassed the check entirely, allowing requests to reach the Python sidecar even when the feature bundle was not installed. This adds an early 501 response before any multipart parsing or file processing. --- apps/api/src/routes/tools/blur-faces.ts | 14 ++++++++++++++ apps/api/src/routes/tools/colorize.ts | 14 ++++++++++++++ apps/api/src/routes/tools/enhance-faces.ts | 14 ++++++++++++++ apps/api/src/routes/tools/erase-object.ts | 14 ++++++++++++++ apps/api/src/routes/tools/noise-removal.ts | 14 ++++++++++++++ apps/api/src/routes/tools/ocr.ts | 14 ++++++++++++++ apps/api/src/routes/tools/passport-photo.ts | 15 ++++++++++++++- apps/api/src/routes/tools/red-eye-removal.ts | 14 ++++++++++++++ apps/api/src/routes/tools/remove-background.ts | 14 ++++++++++++++ apps/api/src/routes/tools/upscale.ts | 14 ++++++++++++++ 10 files changed, 140 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/tools/blur-faces.ts b/apps/api/src/routes/tools/blur-faces.ts index b4a71e1a..839d815f 100644 --- a/apps/api/src/routes/tools/blur-faces.ts +++ b/apps/api/src/routes/tools/blur-faces.ts @@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { blurFaces } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { createWorkspace } from "../../lib/workspace.js"; import { updateSingleFileProgress } from "../progress.js"; @@ -13,6 +15,18 @@ import { registerToolProcessFn } from "../tool-factory.js"; /** Face detection and blurring route. */ export function registerBlurFaces(app: FastifyInstance) { app.post("/api/v1/tools/blur-faces", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "blur-faces"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/colorize.ts b/apps/api/src/routes/tools/colorize.ts index 05ca8abe..308da930 100644 --- a/apps/api/src/routes/tools/colorize.ts +++ b/apps/api/src/routes/tools/colorize.ts @@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { colorize } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; @@ -20,6 +22,18 @@ import { registerToolProcessFn } from "../tool-factory.js"; */ export function registerColorize(app: FastifyInstance) { app.post("/api/v1/tools/colorize", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "colorize"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/enhance-faces.ts b/apps/api/src/routes/tools/enhance-faces.ts index 397bb7f7..40374160 100644 --- a/apps/api/src/routes/tools/enhance-faces.ts +++ b/apps/api/src/routes/tools/enhance-faces.ts @@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { enhanceFaces } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace } from "../../lib/workspace.js"; @@ -15,6 +17,18 @@ import { registerToolProcessFn } from "../tool-factory.js"; /** Face enhancement route using GFPGAN/CodeFormer. */ export function registerEnhanceFaces(app: FastifyInstance) { app.post("/api/v1/tools/enhance-faces", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "enhance-faces"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/erase-object.ts b/apps/api/src/routes/tools/erase-object.ts index fac5ed3d..914571e6 100644 --- a/apps/api/src/routes/tools/erase-object.ts +++ b/apps/api/src/routes/tools/erase-object.ts @@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { inpaint } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace } from "../../lib/workspace.js"; @@ -30,6 +32,18 @@ const BROWSER_PREVIEWABLE = new Set(["png", "jpg", "jpeg", "webp", "gif", "avif" */ export function registerEraseObject(app: FastifyInstance) { app.post("/api/v1/tools/erase-object", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "erase-object"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let imageBuffer: Buffer | null = null; let maskBuffer: Buffer | null = null; let filename = "image"; diff --git a/apps/api/src/routes/tools/noise-removal.ts b/apps/api/src/routes/tools/noise-removal.ts index dba9d1f3..24fb90c6 100644 --- a/apps/api/src/routes/tools/noise-removal.ts +++ b/apps/api/src/routes/tools/noise-removal.ts @@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { noiseRemoval } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace } from "../../lib/workspace.js"; @@ -26,6 +28,18 @@ const settingsSchema = z.object({ */ export function registerNoiseRemoval(app: FastifyInstance) { app.post("/api/v1/tools/noise-removal", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "noise-removal"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/ocr.ts b/apps/api/src/routes/tools/ocr.ts index 8ce6e057..2b06e139 100644 --- a/apps/api/src/routes/tools/ocr.ts +++ b/apps/api/src/routes/tools/ocr.ts @@ -1,9 +1,11 @@ import { randomUUID } from "node:crypto"; import { basename } from "node:path"; import { extractText } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { formatZodErrors } from "../../lib/errors.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { createWorkspace } from "../../lib/workspace.js"; import { updateSingleFileProgress } from "../progress.js"; @@ -22,6 +24,18 @@ const settingsSchema = z.object({ */ export function registerOcr(app: FastifyInstance) { app.post("/api/v1/tools/ocr", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "ocr"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/passport-photo.ts b/apps/api/src/routes/tools/passport-photo.ts index d8e9c648..efca7257 100644 --- a/apps/api/src/routes/tools/passport-photo.ts +++ b/apps/api/src/routes/tools/passport-photo.ts @@ -2,12 +2,13 @@ import { randomUUID } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { detectFaceLandmarks, removeBackground } from "@ashim/ai"; -import { PASSPORT_SPECS, PRINT_LAYOUTS } from "@ashim/shared"; +import { getBundleForTool, PASSPORT_SPECS, PRINT_LAYOUTS, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; import { formatZodErrors } from "../../lib/errors.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js"; @@ -124,6 +125,18 @@ export function registerPassportPhoto(app: FastifyInstance) { app.post( "/api/v1/tools/passport-photo/analyze", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "passport-photo"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let clientJobId: string | null = null; diff --git a/apps/api/src/routes/tools/red-eye-removal.ts b/apps/api/src/routes/tools/red-eye-removal.ts index df71676c..c4d1744f 100644 --- a/apps/api/src/routes/tools/red-eye-removal.ts +++ b/apps/api/src/routes/tools/red-eye-removal.ts @@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { removeRedEye } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { createWorkspace } from "../../lib/workspace.js"; import { updateSingleFileProgress } from "../progress.js"; @@ -15,6 +17,18 @@ export function registerRedEyeRemoval(app: FastifyInstance) { app.post( "/api/v1/tools/red-eye-removal", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "red-eye-removal"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/remove-background.ts b/apps/api/src/routes/tools/remove-background.ts index fa4ed243..90b8bc55 100644 --- a/apps/api/src/routes/tools/remove-background.ts +++ b/apps/api/src/routes/tools/remove-background.ts @@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { removeBackground } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; import { applyEffects } from "../../lib/bg-effects.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js"; @@ -41,6 +43,18 @@ export function registerRemoveBackground(app: FastifyInstance) { app.post( "/api/v1/tools/remove-background", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "remove-background"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/tools/upscale.ts b/apps/api/src/routes/tools/upscale.ts index 4b559e15..8057c5f2 100644 --- a/apps/api/src/routes/tools/upscale.ts +++ b/apps/api/src/routes/tools/upscale.ts @@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { upscale } from "@ashim/ai"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js"; import { createWorkspace } from "../../lib/workspace.js"; @@ -18,6 +20,18 @@ import { registerToolProcessFn } from "../tool-factory.js"; */ export function registerUpscale(app: FastifyInstance) { app.post("/api/v1/tools/upscale", async (request: FastifyRequest, reply: FastifyReply) => { + const toolId = "upscale"; + if (!isToolInstalled(toolId)) { + const bundle = getBundleForTool(toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: TOOL_BUNDLE_MAP[toolId], + featureName: bundle?.name ?? toolId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + let fileBuffer: Buffer | null = null; let filename = "image"; let settingsRaw: string | null = null;