113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
import { DevicePackageError } from '../../coding-packages/device-package-manager';
|
|
import type { HostApiContext } from '../context';
|
|
import { parseJsonBody, sendJson } from '../route-utils';
|
|
|
|
const ROOT = '/api/coding/device-packages';
|
|
const PACKAGE = /^\/api\/coding\/device-packages\/([^/]+)$/u;
|
|
const PACKAGE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
|
|
|
class DevicePackageRouteError extends Error {
|
|
constructor(readonly status: 400 | 404, message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
function packageId(value: string): string {
|
|
try {
|
|
const decoded = decodeURIComponent(value);
|
|
if (PACKAGE_ID.test(decoded)) return decoded;
|
|
} catch {
|
|
// Project a single bounded request error below.
|
|
}
|
|
throw new DevicePackageRouteError(400, 'Device package id is invalid');
|
|
}
|
|
|
|
function noQuery(url: URL): void {
|
|
if (url.search) throw new DevicePackageRouteError(400, 'Query parameters are not supported');
|
|
}
|
|
|
|
async function exactBody(req: IncomingMessage, keys: readonly string[]): Promise<Record<string, unknown>> {
|
|
const body = await parseJsonBody<unknown>(req);
|
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
throw new DevicePackageRouteError(400, 'Request body is invalid');
|
|
}
|
|
const record = body as Record<string, unknown>;
|
|
const actual = Object.keys(record).sort();
|
|
const expected = [...keys].sort();
|
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
throw new DevicePackageRouteError(400, 'Request body has unexpected fields');
|
|
}
|
|
return record;
|
|
}
|
|
|
|
function sendError(res: ServerResponse, error: unknown): void {
|
|
if (error instanceof DevicePackageRouteError) {
|
|
sendJson(res, error.status, {
|
|
success: false, code: 'local_package_request_invalid', error: error.message,
|
|
});
|
|
return;
|
|
}
|
|
if (error instanceof DevicePackageError) {
|
|
const status = error.code === 'local_package_not_installed' || error.code === 'local_package_not_found'
|
|
? 404
|
|
: error.code === 'local_package_in_use' || error.code === 'local_package_confirmation_required'
|
|
? 409
|
|
: error.code === 'local_package_dependency_failed' || error.code === 'local_package_install_failed'
|
|
? 503
|
|
: 422;
|
|
sendJson(res, status, { success: false, code: error.code, error: error.message });
|
|
return;
|
|
}
|
|
if (error instanceof SyntaxError) {
|
|
sendJson(res, 400, {
|
|
success: false, code: 'local_package_request_invalid', error: 'Request body is invalid',
|
|
});
|
|
return;
|
|
}
|
|
sendJson(res, 503, {
|
|
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
|
});
|
|
}
|
|
|
|
export async function handleDevicePackageRoutes(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
url: URL,
|
|
ctx: HostApiContext,
|
|
): Promise<boolean> {
|
|
const match = PACKAGE.exec(url.pathname);
|
|
const known = (url.pathname === ROOT && req.method === 'GET')
|
|
|| Boolean(match && (req.method === 'PATCH' || req.method === 'DELETE'));
|
|
if (!known) return false;
|
|
const manager = ctx.codingProducts?.devicePackages;
|
|
if (!manager) {
|
|
sendJson(res, 503, {
|
|
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
|
});
|
|
return true;
|
|
}
|
|
try {
|
|
noQuery(url);
|
|
if (url.pathname === ROOT) {
|
|
sendJson(res, 200, await manager.list());
|
|
return true;
|
|
}
|
|
const id = packageId(match?.[1] ?? '');
|
|
if (req.method === 'PATCH') {
|
|
const body = await exactBody(req, ['enabled']);
|
|
if (typeof body.enabled !== 'boolean') {
|
|
throw new DevicePackageRouteError(400, 'enabled is invalid');
|
|
}
|
|
sendJson(res, 200, await manager.setEnabled(id, body.enabled));
|
|
return true;
|
|
}
|
|
await exactBody(req, []);
|
|
sendJson(res, 200, await manager.uninstall(id));
|
|
return true;
|
|
} catch (error) {
|
|
sendError(res, error);
|
|
return true;
|
|
}
|
|
}
|