mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 21:52:21 +03:00
chore(duplication): share service install helpers (#5495)
Share service install helpers; re-add SERVICE_VERSION_PATTERN regex to the shared schema (dropped in extraction, #5474) + tests rejecting malformed versions. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -1,41 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { install, InstallResult } from "@/lib/services/installers/ninerouter";
|
||||
import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const BodySchema = z.object({
|
||||
version: z
|
||||
.string()
|
||||
.regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed")
|
||||
.optional()
|
||||
.default("latest"),
|
||||
});
|
||||
import { install } from "@/lib/services/installers/ninerouter";
|
||||
import { handleServiceInstall } from "@/app/api/services/_shared/installRoute";
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = request.body === null ? {} : await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const parsed = BodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return createErrorResponse({ status: 400, message: parsed.error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
const result: InstallResult = await install(parsed.data.version);
|
||||
return Response.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
if (err instanceof InstallError) {
|
||||
return createErrorResponse({
|
||||
status: err.httpStatus,
|
||||
message: err.friendly,
|
||||
});
|
||||
}
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
return createErrorResponse({ status: 500, message: msg });
|
||||
}
|
||||
return handleServiceInstall(request, install);
|
||||
}
|
||||
|
||||
79
src/app/api/services/_shared/installRoute.ts
Normal file
79
src/app/api/services/_shared/installRoute.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
export type ServiceInstallResult = {
|
||||
installedVersion: string;
|
||||
installPath: string;
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
export type ServiceInstaller = (version: string) => Promise<ServiceInstallResult>;
|
||||
|
||||
const installBodySchema = z.object({
|
||||
// Keep the version constrained by SERVICE_VERSION_PATTERN — the per-route schemas
|
||||
// enforced this before the extraction (#5474); dropping it here would let strings
|
||||
// like "../../malicious" reach the installer (#5495).
|
||||
version: z
|
||||
.string()
|
||||
.regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed")
|
||||
.optional()
|
||||
.default("latest"),
|
||||
});
|
||||
|
||||
export async function readServiceInstallVersion(request: Request): Promise<
|
||||
| {
|
||||
ok: true;
|
||||
version: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
response: Response;
|
||||
}
|
||||
> {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = request.body === null ? {} : await request.json();
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
response: createErrorResponse({ status: 400, message: "Invalid JSON body" }),
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = installBodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
ok: false,
|
||||
response: createErrorResponse({ status: 400, message: parsed.error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, version: parsed.data.version };
|
||||
}
|
||||
|
||||
export async function handleServiceInstall(
|
||||
request: Request,
|
||||
install: ServiceInstaller
|
||||
): Promise<Response> {
|
||||
const parsed = await readServiceInstallVersion(request);
|
||||
if (!parsed.ok) {
|
||||
return parsed.response;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await install(parsed.version);
|
||||
return Response.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
if (err instanceof InstallError) {
|
||||
return createErrorResponse({
|
||||
status: err.httpStatus,
|
||||
message: err.friendly,
|
||||
});
|
||||
}
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
return createErrorResponse({ status: 500, message: msg });
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { install, InstallResult } from "@/lib/services/installers/cliproxy";
|
||||
import { InstallError, SERVICE_VERSION_PATTERN } from "@/lib/services/installers/utils";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const BodySchema = z.object({
|
||||
version: z
|
||||
.string()
|
||||
.regex(SERVICE_VERSION_PATTERN, "Invalid version: only letters, digits and . _ + - are allowed")
|
||||
.optional()
|
||||
.default("latest"),
|
||||
});
|
||||
import { install } from "@/lib/services/installers/cliproxy";
|
||||
import { handleServiceInstall } from "@/app/api/services/_shared/installRoute";
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = request.body === null ? {} : await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const parsed = BodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return createErrorResponse({ status: 400, message: parsed.error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
const result: InstallResult = await install(parsed.data.version);
|
||||
return Response.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
if (err instanceof InstallError) {
|
||||
return createErrorResponse({ status: err.httpStatus, message: err.friendly });
|
||||
}
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
return createErrorResponse({ status: 500, message: msg });
|
||||
}
|
||||
return handleServiceInstall(request, install);
|
||||
}
|
||||
|
||||
121
tests/unit/services/install-route-helper.test.ts
Normal file
121
tests/unit/services/install-route-helper.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
handleServiceInstall,
|
||||
readServiceInstallVersion,
|
||||
} from "../../../src/app/api/services/_shared/installRoute.ts";
|
||||
import { InstallError } from "../../../src/lib/services/installers/utils.ts";
|
||||
|
||||
async function readJson(response: Response) {
|
||||
return (await response.json()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
test("readServiceInstallVersion defaults empty requests to latest", async () => {
|
||||
const result = await readServiceInstallVersion(
|
||||
new Request("http://localhost/api/services/example/install", { method: "POST" })
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { ok: true, version: "latest" });
|
||||
});
|
||||
|
||||
test("readServiceInstallVersion returns the requested version", async () => {
|
||||
const result = await readServiceInstallVersion(
|
||||
new Request("http://localhost/api/services/example/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ version: "1.2.3" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { ok: true, version: "1.2.3" });
|
||||
});
|
||||
|
||||
test("readServiceInstallVersion rejects malformed versions (#5495 SERVICE_VERSION_PATTERN guard)", async () => {
|
||||
const result = await readServiceInstallVersion(
|
||||
new Request("http://localhost/api/services/example/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ version: "../../malicious" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) throw new Error("expected version validation failure");
|
||||
assert.equal(result.response.status, 400);
|
||||
});
|
||||
|
||||
test("handleServiceInstall never reaches the installer for a malformed version (#5495)", async () => {
|
||||
const calls: string[] = [];
|
||||
const response = await handleServiceInstall(
|
||||
new Request("http://localhost/api/services/example/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ version: "v1; rm -rf /" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
async (version) => {
|
||||
calls.push(version);
|
||||
return { installedVersion: version, installPath: "/tmp/service", durationMs: 1 };
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(calls, []);
|
||||
assert.equal(response.status, 400);
|
||||
});
|
||||
|
||||
test("readServiceInstallVersion preserves invalid JSON error shape", async () => {
|
||||
const result = await readServiceInstallVersion(
|
||||
new Request("http://localhost/api/services/example/install", {
|
||||
method: "POST",
|
||||
body: "not-json",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) throw new Error("expected parse failure");
|
||||
assert.equal(result.response.status, 400);
|
||||
const body = await readJson(result.response);
|
||||
assert.equal((body.error as Record<string, unknown>).message, "Invalid JSON body");
|
||||
});
|
||||
|
||||
test("handleServiceInstall wraps successful installer results", async () => {
|
||||
const calls: string[] = [];
|
||||
const response = await handleServiceInstall(
|
||||
new Request("http://localhost/api/services/example/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ version: "2.0.0" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
async (version) => {
|
||||
calls.push(version);
|
||||
return {
|
||||
installedVersion: version,
|
||||
installPath: "/tmp/service",
|
||||
durationMs: 42,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(calls, ["2.0.0"]);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await readJson(response), {
|
||||
ok: true,
|
||||
installedVersion: "2.0.0",
|
||||
installPath: "/tmp/service",
|
||||
durationMs: 42,
|
||||
});
|
||||
});
|
||||
|
||||
test("handleServiceInstall maps InstallError to its friendly message and status", async () => {
|
||||
const response = await handleServiceInstall(
|
||||
new Request("http://localhost/api/services/example/install", { method: "POST" }),
|
||||
async () => {
|
||||
throw new InstallError("raw command failed", "Friendly install failure", 503);
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
const body = await readJson(response);
|
||||
assert.equal((body.error as Record<string, unknown>).message, "Friendly install failure");
|
||||
});
|
||||
Reference in New Issue
Block a user