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.
This commit is contained in:
ashim-hq
2026-04-18 10:23:14 +08:00
parent d7b6037b3d
commit 2a94cdc024
10 changed files with 140 additions and 1 deletions
+14
View File
@@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { blurFaces } from "@ashim/ai"; import { blurFaces } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
import { updateSingleFileProgress } from "../progress.js"; import { updateSingleFileProgress } from "../progress.js";
@@ -13,6 +15,18 @@ import { registerToolProcessFn } from "../tool-factory.js";
/** Face detection and blurring route. */ /** Face detection and blurring route. */
export function registerBlurFaces(app: FastifyInstance) { export function registerBlurFaces(app: FastifyInstance) {
app.post("/api/v1/tools/blur-faces", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
+14
View File
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { colorize } from "@ashim/ai"; import { colorize } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic } from "../../lib/heic-converter.js";
import { resolveOutputFormat } from "../../lib/output-format.js"; import { resolveOutputFormat } from "../../lib/output-format.js";
@@ -20,6 +22,18 @@ import { registerToolProcessFn } from "../tool-factory.js";
*/ */
export function registerColorize(app: FastifyInstance) { export function registerColorize(app: FastifyInstance) {
app.post("/api/v1/tools/colorize", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { enhanceFaces } from "@ashim/ai"; import { enhanceFaces } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
@@ -15,6 +17,18 @@ import { registerToolProcessFn } from "../tool-factory.js";
/** Face enhancement route using GFPGAN/CodeFormer. */ /** Face enhancement route using GFPGAN/CodeFormer. */
export function registerEnhanceFaces(app: FastifyInstance) { export function registerEnhanceFaces(app: FastifyInstance) {
app.post("/api/v1/tools/enhance-faces", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
+14
View File
@@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { inpaint } from "@ashim/ai"; import { inpaint } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp"; import sharp from "sharp";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace } from "../../lib/workspace.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) { export function registerEraseObject(app: FastifyInstance) {
app.post("/api/v1/tools/erase-object", async (request: FastifyRequest, reply: FastifyReply) => { 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 imageBuffer: Buffer | null = null;
let maskBuffer: Buffer | null = null; let maskBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
@@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { noiseRemoval } from "@ashim/ai"; import { noiseRemoval } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
@@ -26,6 +28,18 @@ const settingsSchema = z.object({
*/ */
export function registerNoiseRemoval(app: FastifyInstance) { export function registerNoiseRemoval(app: FastifyInstance) {
app.post("/api/v1/tools/noise-removal", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
+14
View File
@@ -1,9 +1,11 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { basename } from "node:path"; import { basename } from "node:path";
import { extractText } from "@ashim/ai"; import { extractText } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { formatZodErrors } from "../../lib/errors.js"; import { formatZodErrors } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
import { updateSingleFileProgress } from "../progress.js"; import { updateSingleFileProgress } from "../progress.js";
@@ -22,6 +24,18 @@ const settingsSchema = z.object({
*/ */
export function registerOcr(app: FastifyInstance) { export function registerOcr(app: FastifyInstance) {
app.post("/api/v1/tools/ocr", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
+14 -1
View File
@@ -2,12 +2,13 @@ import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises"; import { readFile, writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { detectFaceLandmarks, removeBackground } from "@ashim/ai"; 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 type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors } from "../../lib/errors.js"; import { formatZodErrors } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js"; import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js";
@@ -124,6 +125,18 @@ export function registerPassportPhoto(app: FastifyInstance) {
app.post( app.post(
"/api/v1/tools/passport-photo/analyze", "/api/v1/tools/passport-photo/analyze",
async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let clientJobId: string | null = null; let clientJobId: string | null = null;
@@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { removeRedEye } from "@ashim/ai"; import { removeRedEye } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
import { updateSingleFileProgress } from "../progress.js"; import { updateSingleFileProgress } from "../progress.js";
@@ -15,6 +17,18 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
app.post( app.post(
"/api/v1/tools/red-eye-removal", "/api/v1/tools/red-eye-removal",
async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises"; import { readFile, writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { removeBackground } from "@ashim/ai"; import { removeBackground } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { applyEffects } from "../../lib/bg-effects.js"; import { applyEffects } from "../../lib/bg-effects.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js"; import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js";
@@ -41,6 +43,18 @@ export function registerRemoveBackground(app: FastifyInstance) {
app.post( app.post(
"/api/v1/tools/remove-background", "/api/v1/tools/remove-background",
async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;
+14
View File
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { upscale } from "@ashim/ai"; import { upscale } from "@ashim/ai";
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js"; import { autoOrient } from "../../lib/auto-orient.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js"; import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
@@ -18,6 +20,18 @@ import { registerToolProcessFn } from "../tool-factory.js";
*/ */
export function registerUpscale(app: FastifyInstance) { export function registerUpscale(app: FastifyInstance) {
app.post("/api/v1/tools/upscale", async (request: FastifyRequest, reply: FastifyReply) => { 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 fileBuffer: Buffer | null = null;
let filename = "image"; let filename = "image";
let settingsRaw: string | null = null; let settingsRaw: string | null = null;