From 3c3aa74e985e9e7516c201ff759f95f9f3ce1fa2 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Sat, 18 Apr 2026 02:40:16 +0800 Subject: [PATCH] feat: add FEATURE_NOT_INSTALLED guards to API tool routes Return 501 with structured error when an AI tool's feature bundle is not installed, preventing Python ImportError crashes. Guards added to tool-factory, batch, pipeline (both validation loops), and restore-photo custom route. --- apps/api/src/routes/batch.ts | 14 +++++++++++++ apps/api/src/routes/pipeline.ts | 24 ++++++++++++++++++++++ apps/api/src/routes/tool-factory.ts | 15 ++++++++++++++ apps/api/src/routes/tools/restore-photo.ts | 14 +++++++++++++ 4 files changed, 67 insertions(+) diff --git a/apps/api/src/routes/batch.ts b/apps/api/src/routes/batch.ts index 9d30feab..edb22a8b 100644 --- a/apps/api/src/routes/batch.ts +++ b/apps/api/src/routes/batch.ts @@ -8,12 +8,14 @@ * Returns a ZIP file containing all processed images. */ import { randomUUID } from "node:crypto"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import archiver from "archiver"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import PQueue from "p-queue"; import { env } from "../config.js"; 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 { sanitizeFilename } from "../lib/filename.js"; import { decodeHeic } from "../lib/heic-converter.js"; @@ -37,6 +39,18 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise { return reply.status(404).send({ error: `Tool "${toolId}" not found` }); } + // Guard: check if the tool's AI feature bundle is installed + 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", + }); + } + // Parse multipart: collect all files and the settings field const files: ParsedFile[] = []; let settingsRaw: string | null = null; diff --git a/apps/api/src/routes/pipeline.ts b/apps/api/src/routes/pipeline.ts index f2901cf3..a7650ada 100644 --- a/apps/api/src/routes/pipeline.ts +++ b/apps/api/src/routes/pipeline.ts @@ -9,6 +9,7 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared"; import archiver from "archiver"; import { eq } from "drizzle-orm"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; @@ -18,6 +19,7 @@ import { env } from "../config.js"; import { db, schema } from "../db/index.js"; 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 { sanitizeFilename } from "../lib/filename.js"; import { decodeHeic } from "../lib/heic-converter.js"; @@ -156,6 +158,17 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise(app: FastifyInstance, config: ToolRouteConfig return reply.status(400).send({ error: "Settings must be valid JSON" }); } + // Guard: check if the tool's AI feature bundle is installed + const bundleId = TOOL_BUNDLE_MAP[config.toolId]; + if (bundleId && !isToolInstalled(config.toolId)) { + const bundle = getBundleForTool(config.toolId); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: bundleId, + featureName: bundle?.name ?? bundleId, + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + // Process the image (worker thread or main thread) try { let result: { buffer: Buffer; filename: string; contentType: string }; diff --git a/apps/api/src/routes/tools/restore-photo.ts b/apps/api/src/routes/tools/restore-photo.ts index 2a40df24..b917dd70 100644 --- a/apps/api/src/routes/tools/restore-photo.ts +++ b/apps/api/src/routes/tools/restore-photo.ts @@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { restorePhoto } from "@ashim/ai"; +import { getBundleForTool } 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"; @@ -67,6 +69,18 @@ export function registerRestorePhoto(app: FastifyInstance) { return reply.status(400).send({ error: `Invalid image: ${validation.reason}` }); } + // Guard: check if the photo restoration feature bundle is installed + if (!isToolInstalled("restore-photo")) { + const bundle = getBundleForTool("restore-photo"); + return reply.status(501).send({ + error: "Feature not installed", + code: "FEATURE_NOT_INSTALLED", + feature: "photo-restoration", + featureName: bundle?.name ?? "Photo Restoration", + estimatedSize: bundle?.estimatedSize ?? "unknown", + }); + } + try { const settings = settingsSchema.parse(settingsRaw ? JSON.parse(settingsRaw) : {});