@axiom-lattice/opensandbox-gateway 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +22 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/chunk-FTPJIUJT.mjs +1081 -0
- package/dist/chunk-FTPJIUJT.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +11 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +373 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -0
- package/src/app.ts +67 -0
- package/src/cli.ts +8 -0
- package/src/controllers/images.ts +38 -0
- package/src/controllers/sandbox.ts +176 -0
- package/src/controllers/volume-fs.ts +163 -0
- package/src/index.ts +3 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/http.ts +11 -0
- package/src/lib/server-cli.ts +65 -0
- package/src/routes/health.ts +6 -0
- package/src/routes/images.ts +12 -0
- package/src/routes/sandbox.ts +31 -0
- package/src/routes/volume-fs.ts +12 -0
- package/src/schemas/images.ts +12 -0
- package/src/schemas/sandbox.ts +98 -0
- package/src/schemas/volume-fs.ts +23 -0
- package/src/server.ts +50 -0
- package/src/services/ImageService.ts +32 -0
- package/src/services/OpenSandboxRuntimeService.ts +576 -0
- package/src/swagger.ts +40 -0
- package/src/types/runtime-service.ts +79 -0
- package/tsconfig.json +22 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import cors from "@fastify/cors";
|
|
3
|
+
import multipart from "@fastify/multipart";
|
|
4
|
+
import sensible from "@fastify/sensible";
|
|
5
|
+
import fastify from "fastify";
|
|
6
|
+
import { toErrorResponse } from "./lib/errors";
|
|
7
|
+
import { registerHealthRoutes } from "./routes/health";
|
|
8
|
+
import { registerImageRoutes } from "./routes/images";
|
|
9
|
+
import { registerSandboxRoutes } from "./routes/sandbox";
|
|
10
|
+
import { registerVolumeFsRoutes } from "./routes/volume-fs";
|
|
11
|
+
import { ImageService } from "./services/ImageService";
|
|
12
|
+
import { OpenSandboxRuntimeService } from "./services/OpenSandboxRuntimeService";
|
|
13
|
+
import { configureSwagger } from "./swagger";
|
|
14
|
+
|
|
15
|
+
export async function buildApp({
|
|
16
|
+
apiKey,
|
|
17
|
+
}: {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
} = {}): Promise<FastifyInstance> {
|
|
20
|
+
const runtimeService = new OpenSandboxRuntimeService();
|
|
21
|
+
const imageService = new ImageService();
|
|
22
|
+
|
|
23
|
+
const app = fastify({
|
|
24
|
+
logger: false,
|
|
25
|
+
bodyLimit: Number(process.env["BODY_LIMIT"]) || 100 * 1024 * 1024,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await app.register(cors, {
|
|
29
|
+
delegator: (request, callback) => {
|
|
30
|
+
callback(null, {
|
|
31
|
+
origin: true,
|
|
32
|
+
methods: request.headers["access-control-request-method"] ?? "*",
|
|
33
|
+
allowedHeaders: request.headers["access-control-request-headers"],
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
await app.register(sensible);
|
|
38
|
+
await app.register(multipart);
|
|
39
|
+
|
|
40
|
+
if (apiKey) {
|
|
41
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
42
|
+
if (request.method === "OPTIONS" || !request.url.startsWith("/api/")) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (request.headers.authorization !== `Bearer ${apiKey}`) {
|
|
46
|
+
await reply.code(401).send({
|
|
47
|
+
success: false,
|
|
48
|
+
error: { code: "UNAUTHORIZED", message: "Unauthorized" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerHealthRoutes(app);
|
|
55
|
+
registerSandboxRoutes(app, runtimeService);
|
|
56
|
+
registerImageRoutes(app, imageService);
|
|
57
|
+
registerVolumeFsRoutes(app);
|
|
58
|
+
|
|
59
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
60
|
+
const { statusCode, body } = toErrorResponse(error);
|
|
61
|
+
reply.status(statusCode).send(body);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await configureSwagger(app);
|
|
65
|
+
|
|
66
|
+
return app;
|
|
67
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import { HttpError } from "../lib/errors";
|
|
4
|
+
import { ok } from "../lib/http";
|
|
5
|
+
import { ImageService } from "../services/ImageService";
|
|
6
|
+
import { imageRefQuerySchema, pullImageSchema } from "../schemas/images";
|
|
7
|
+
|
|
8
|
+
function parseOrThrow<T>(parse: () => T): T {
|
|
9
|
+
try {
|
|
10
|
+
return parse();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof ZodError) {
|
|
13
|
+
throw new HttpError(400, "INVALID_REQUEST", error.issues[0]?.message ?? "Invalid request");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createImageController(imageService: ImageService) {
|
|
21
|
+
return {
|
|
22
|
+
listImages: async (_request: FastifyRequest, reply: FastifyReply) => {
|
|
23
|
+
return reply.send(ok(await imageService.listImages()));
|
|
24
|
+
},
|
|
25
|
+
pullImage: async (request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply) => {
|
|
26
|
+
const body = parseOrThrow(() => pullImageSchema.parse(request.body ?? {}));
|
|
27
|
+
return reply.send(ok(await imageService.pullImage(body)));
|
|
28
|
+
},
|
|
29
|
+
getImage: async (request: FastifyRequest<{ Querystring: unknown }>, reply: FastifyReply) => {
|
|
30
|
+
const query = parseOrThrow(() => imageRefQuerySchema.parse(request.query ?? {}));
|
|
31
|
+
return reply.send(ok(await imageService.getImage(query.ref)));
|
|
32
|
+
},
|
|
33
|
+
deleteImage: async (request: FastifyRequest<{ Querystring: unknown }>, reply: FastifyReply) => {
|
|
34
|
+
const query = parseOrThrow(() => imageRefQuerySchema.parse(request.query ?? {}));
|
|
35
|
+
return reply.send(ok(await imageService.deleteImage(query.ref)));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import {
|
|
3
|
+
downloadFileSchema,
|
|
4
|
+
ensureSandboxSchema,
|
|
5
|
+
findFilesSchema,
|
|
6
|
+
listSandboxesQuerySchema,
|
|
7
|
+
listPathSchema,
|
|
8
|
+
readFileSchema,
|
|
9
|
+
replaceInFileSchema,
|
|
10
|
+
sandboxLogsSchema,
|
|
11
|
+
sandboxNameParamsSchema,
|
|
12
|
+
searchInFileSchema,
|
|
13
|
+
shellExecSchema,
|
|
14
|
+
uploadFileSchema,
|
|
15
|
+
writeFileSchema,
|
|
16
|
+
} from "../schemas/sandbox";
|
|
17
|
+
import { HttpError } from "../lib/errors";
|
|
18
|
+
import { ok } from "../lib/http";
|
|
19
|
+
import type { RuntimeService } from "../types/runtime-service";
|
|
20
|
+
|
|
21
|
+
export function createSandboxController(runtimeService: RuntimeService): {
|
|
22
|
+
ensureSandbox(
|
|
23
|
+
request: FastifyRequest<{ Params: { name: string }; Body: unknown }>,
|
|
24
|
+
reply: FastifyReply
|
|
25
|
+
): Promise<FastifyReply>;
|
|
26
|
+
startSandbox(
|
|
27
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
28
|
+
reply: FastifyReply
|
|
29
|
+
): Promise<FastifyReply>;
|
|
30
|
+
stopSandbox(
|
|
31
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
32
|
+
reply: FastifyReply
|
|
33
|
+
): Promise<FastifyReply>;
|
|
34
|
+
killSandbox(
|
|
35
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
36
|
+
reply: FastifyReply
|
|
37
|
+
): Promise<FastifyReply>;
|
|
38
|
+
deleteSandbox(
|
|
39
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
40
|
+
reply: FastifyReply
|
|
41
|
+
): Promise<FastifyReply>;
|
|
42
|
+
listSandboxes(
|
|
43
|
+
request: FastifyRequest<{ Querystring: unknown }>,
|
|
44
|
+
reply: FastifyReply
|
|
45
|
+
): Promise<FastifyReply>;
|
|
46
|
+
getSandbox(
|
|
47
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
48
|
+
reply: FastifyReply
|
|
49
|
+
): Promise<FastifyReply>;
|
|
50
|
+
getStatus(
|
|
51
|
+
request: FastifyRequest<{ Params: { name: string } }>,
|
|
52
|
+
reply: FastifyReply
|
|
53
|
+
): Promise<FastifyReply>;
|
|
54
|
+
getSandboxLogs(
|
|
55
|
+
request: FastifyRequest<{ Params: { name: string }; Body: unknown }>,
|
|
56
|
+
reply: FastifyReply
|
|
57
|
+
): Promise<FastifyReply>;
|
|
58
|
+
readFile(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
59
|
+
writeFile(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
60
|
+
listPath(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
61
|
+
findFiles(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
62
|
+
searchInFile(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
63
|
+
replaceInFile(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
64
|
+
uploadFile(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
65
|
+
downloadFile(
|
|
66
|
+
request: FastifyRequest<{ Querystring: unknown }>,
|
|
67
|
+
reply: FastifyReply
|
|
68
|
+
): Promise<FastifyReply>;
|
|
69
|
+
execCommand(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply): Promise<FastifyReply>;
|
|
70
|
+
} {
|
|
71
|
+
return {
|
|
72
|
+
ensureSandbox: async (
|
|
73
|
+
request: FastifyRequest<{ Params: { name: string }; Body: unknown }>,
|
|
74
|
+
reply: FastifyReply
|
|
75
|
+
) => {
|
|
76
|
+
const body = ensureSandboxSchema.parse(request.body ?? {});
|
|
77
|
+
const result = await runtimeService.ensureSandbox(request.params.name, body);
|
|
78
|
+
return reply.send(ok(result));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
startSandbox: async (request, reply) => {
|
|
82
|
+
return reply.send(ok(await runtimeService.startSandbox(request.params.name)));
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
stopSandbox: async (request, reply) => {
|
|
86
|
+
return reply.send(ok(await runtimeService.stopSandbox(request.params.name)));
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
killSandbox: async (request, reply) => {
|
|
90
|
+
return reply.send(ok(await runtimeService.killSandbox(request.params.name)));
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
deleteSandbox: async (request, reply) => {
|
|
94
|
+
return reply.send(ok(await runtimeService.deleteSandbox(request.params.name)));
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
listSandboxes: async (request, reply) => {
|
|
98
|
+
const query = listSandboxesQuerySchema.parse(request.query ?? {});
|
|
99
|
+
return reply.send(ok(await runtimeService.listSandboxes(query)));
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
getSandbox: async (request, reply) => {
|
|
103
|
+
const params = sandboxNameParamsSchema.parse(request.params ?? {});
|
|
104
|
+
const sandbox = await runtimeService.getSandbox(params.name);
|
|
105
|
+
|
|
106
|
+
if (!sandbox) {
|
|
107
|
+
throw new HttpError(404, "SANDBOX_NOT_FOUND", `Sandbox '${params.name}' not found`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return reply.send(ok(sandbox));
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
getStatus: async (request, reply) => {
|
|
114
|
+
return reply.send(ok(await runtimeService.getStatus(request.params.name)));
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
getSandboxLogs: async (request, reply) => {
|
|
118
|
+
const body = sandboxLogsSchema.parse(request.body ?? {});
|
|
119
|
+
return reply.send(ok(await runtimeService.getSandboxLogs(request.params.name, body)));
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
readFile: async (request, reply) => {
|
|
123
|
+
const body = readFileSchema.parse(request.body ?? {});
|
|
124
|
+
return reply.send(ok(await runtimeService.readFile(body.sandboxName, body.path)));
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
writeFile: async (request, reply) => {
|
|
128
|
+
const body = writeFileSchema.parse(request.body ?? {});
|
|
129
|
+
return reply.send(ok(await runtimeService.writeFile(body.sandboxName, body.path, body.content)));
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
listPath: async (request, reply) => {
|
|
133
|
+
const body = listPathSchema.parse(request.body ?? {});
|
|
134
|
+
return reply.send(ok(await runtimeService.listPath(body.sandboxName, body.path, body.recursive)));
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
findFiles: async (request, reply) => {
|
|
138
|
+
const body = findFilesSchema.parse(request.body ?? {});
|
|
139
|
+
return reply.send(ok(await runtimeService.findFiles(body.sandboxName, body.path, body.pattern)));
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
searchInFile: async (request, reply) => {
|
|
143
|
+
const body = searchInFileSchema.parse(request.body ?? {});
|
|
144
|
+
return reply.send(ok(await runtimeService.searchInFile(body.sandboxName, body.path, body.query)));
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
replaceInFile: async (request, reply) => {
|
|
148
|
+
const body = replaceInFileSchema.parse(request.body ?? {});
|
|
149
|
+
return reply.send(
|
|
150
|
+
ok(
|
|
151
|
+
await runtimeService.replaceInFile(body.sandboxName, {
|
|
152
|
+
path: body.path,
|
|
153
|
+
search: body.search,
|
|
154
|
+
replace: body.replace,
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
uploadFile: async (request, reply) => {
|
|
161
|
+
const body = uploadFileSchema.parse(request.body ?? {});
|
|
162
|
+
const contentBase64 = "contentBase64" in body ? body.contentBase64 : Buffer.from(body.content).toString("base64");
|
|
163
|
+
return reply.send(ok(await runtimeService.uploadFile(body.sandboxName, body.path, contentBase64)));
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
downloadFile: async (request, reply) => {
|
|
167
|
+
const query = downloadFileSchema.parse(request.query ?? {});
|
|
168
|
+
return reply.send(ok(await runtimeService.downloadFile(query.sandboxName, query.path)));
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
execCommand: async (request, reply) => {
|
|
172
|
+
const body = shellExecSchema.parse(request.body ?? {});
|
|
173
|
+
return reply.send(ok(await runtimeService.execCommand(body)));
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import { HttpError } from "../lib/errors";
|
|
7
|
+
import { ok } from "../lib/http";
|
|
8
|
+
import {
|
|
9
|
+
volumeFsReadSchema,
|
|
10
|
+
volumeFsWriteSchema,
|
|
11
|
+
volumeFsListSchema,
|
|
12
|
+
volumeFsUploadSchema,
|
|
13
|
+
volumeFsDownloadSchema,
|
|
14
|
+
} from "../schemas/volume-fs";
|
|
15
|
+
|
|
16
|
+
interface VolumeFsParams {
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVolumeBasePath(): string {
|
|
21
|
+
return process.env.VOLUME_BASE_PATH ?? path.join(os.homedir(), ".opensandbox", "volumes");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveVolumeHostPath(name: string): string {
|
|
25
|
+
return path.join(getVolumeBasePath(), name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveGuestPath(hostRoot: string, guestPath: string): string {
|
|
29
|
+
const normalized = guestPath === "~" ? "" : guestPath.replace(/^~\//, "");
|
|
30
|
+
const resolved = path.join(
|
|
31
|
+
hostRoot,
|
|
32
|
+
path.normalize(normalized).replace(/^(\.\.(\/|\\|$))+/, "")
|
|
33
|
+
);
|
|
34
|
+
if (!resolved.startsWith(hostRoot + path.sep) && resolved !== hostRoot) {
|
|
35
|
+
throw new HttpError(403, "PATH_TRAVERSAL", "Path traversal detected");
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createVolumeFsController(): {
|
|
41
|
+
readFile(
|
|
42
|
+
request: FastifyRequest<{ Params: VolumeFsParams; Body: { path: string } }>,
|
|
43
|
+
reply: FastifyReply
|
|
44
|
+
): Promise<FastifyReply>;
|
|
45
|
+
writeFile(
|
|
46
|
+
request: FastifyRequest<{ Params: VolumeFsParams; Body: { path: string; content: string } }>,
|
|
47
|
+
reply: FastifyReply
|
|
48
|
+
): Promise<FastifyReply>;
|
|
49
|
+
listPath(
|
|
50
|
+
request: FastifyRequest<{ Params: VolumeFsParams; Body: { path: string } }>,
|
|
51
|
+
reply: FastifyReply
|
|
52
|
+
): Promise<FastifyReply>;
|
|
53
|
+
downloadFile(
|
|
54
|
+
request: FastifyRequest<{ Params: VolumeFsParams; Querystring: { path: string } }>,
|
|
55
|
+
reply: FastifyReply
|
|
56
|
+
): Promise<FastifyReply>;
|
|
57
|
+
uploadFile(
|
|
58
|
+
request: FastifyRequest<{ Params: VolumeFsParams; Body: { path: string; contentBase64: string } }>,
|
|
59
|
+
reply: FastifyReply
|
|
60
|
+
): Promise<FastifyReply>;
|
|
61
|
+
} {
|
|
62
|
+
return {
|
|
63
|
+
readFile: async (request, reply) => {
|
|
64
|
+
const { name } = request.params;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const { path: guestPath } = volumeFsReadSchema.parse(request.body ?? {});
|
|
68
|
+
const hostRoot = resolveVolumeHostPath(name);
|
|
69
|
+
const fullPath = resolveGuestPath(hostRoot, guestPath);
|
|
70
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
71
|
+
return reply.send(ok({ path: guestPath, content }));
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
if (err instanceof z.ZodError) {
|
|
74
|
+
throw new HttpError(400, "VALIDATION_ERROR", err.message);
|
|
75
|
+
}
|
|
76
|
+
if (err instanceof HttpError) throw err;
|
|
77
|
+
throw new HttpError(404, "VOLUME_READ_ERROR", `Failed to read from volume '${name}': ${String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
writeFile: async (request, reply) => {
|
|
82
|
+
const { name } = request.params;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { path: guestPath, content } = volumeFsWriteSchema.parse(request.body ?? {});
|
|
86
|
+
const hostRoot = resolveVolumeHostPath(name);
|
|
87
|
+
const fullPath = resolveGuestPath(hostRoot, guestPath);
|
|
88
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
89
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
90
|
+
return reply.send(ok({ path: guestPath }));
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
if (err instanceof z.ZodError) {
|
|
93
|
+
throw new HttpError(400, "VALIDATION_ERROR", err.message);
|
|
94
|
+
}
|
|
95
|
+
if (err instanceof HttpError) throw err;
|
|
96
|
+
throw new HttpError(500, "VOLUME_WRITE_ERROR", `Failed to write to volume '${name}': ${String(err)}`);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
listPath: async (request, reply) => {
|
|
101
|
+
const { name } = request.params;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const { path: guestPath } = volumeFsListSchema.parse(request.body ?? {});
|
|
105
|
+
const hostRoot = resolveVolumeHostPath(name);
|
|
106
|
+
const fullPath = resolveGuestPath(hostRoot, guestPath);
|
|
107
|
+
const dirents = await fs.readdir(fullPath, { withFileTypes: true });
|
|
108
|
+
const entries = dirents.map((d) => ({
|
|
109
|
+
path: guestPath ? `${guestPath}/${d.name}` : d.name,
|
|
110
|
+
kind: d.isDirectory() ? "directory" : d.isSymbolicLink() ? "symlink" : "file",
|
|
111
|
+
size: 0,
|
|
112
|
+
mode: 0,
|
|
113
|
+
}));
|
|
114
|
+
return reply.send(ok({ entries }));
|
|
115
|
+
} catch (err: unknown) {
|
|
116
|
+
if (err instanceof z.ZodError) {
|
|
117
|
+
throw new HttpError(400, "VALIDATION_ERROR", err.message);
|
|
118
|
+
}
|
|
119
|
+
if (err instanceof HttpError) throw err;
|
|
120
|
+
throw new HttpError(404, "VOLUME_LIST_ERROR", `Failed to list volume '${name}': ${String(err)}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
downloadFile: async (request, reply) => {
|
|
125
|
+
const { name } = request.params;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { path: guestPath } = volumeFsDownloadSchema.parse(request.query ?? {});
|
|
129
|
+
const hostRoot = resolveVolumeHostPath(name);
|
|
130
|
+
const fullPath = resolveGuestPath(hostRoot, guestPath);
|
|
131
|
+
const buf = await fs.readFile(fullPath);
|
|
132
|
+
const contentBase64 = buf.toString("base64");
|
|
133
|
+
return reply.send(ok({ path: guestPath, contentBase64 }));
|
|
134
|
+
} catch (err: unknown) {
|
|
135
|
+
if (err instanceof z.ZodError) {
|
|
136
|
+
throw new HttpError(400, "VALIDATION_ERROR", err.message);
|
|
137
|
+
}
|
|
138
|
+
if (err instanceof HttpError) throw err;
|
|
139
|
+
throw new HttpError(404, "VOLUME_DOWNLOAD_ERROR", `Failed to download from volume '${name}': ${String(err)}`);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
uploadFile: async (request, reply) => {
|
|
144
|
+
const { name } = request.params;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const { path: guestPath, contentBase64 } = volumeFsUploadSchema.parse(request.body ?? {});
|
|
148
|
+
const hostRoot = resolveVolumeHostPath(name);
|
|
149
|
+
const fullPath = resolveGuestPath(hostRoot, guestPath);
|
|
150
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
151
|
+
const data = Buffer.from(contentBase64, "base64");
|
|
152
|
+
await fs.writeFile(fullPath, data);
|
|
153
|
+
return reply.send(ok({ path: guestPath }));
|
|
154
|
+
} catch (err: unknown) {
|
|
155
|
+
if (err instanceof z.ZodError) {
|
|
156
|
+
throw new HttpError(400, "VALIDATION_ERROR", err.message);
|
|
157
|
+
}
|
|
158
|
+
if (err instanceof HttpError) throw err;
|
|
159
|
+
throw new HttpError(500, "VOLUME_UPLOAD_ERROR", `Failed to upload to volume '${name}': ${String(err)}`);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public statusCode: number,
|
|
4
|
+
public code: string,
|
|
5
|
+
message: string
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toErrorResponse(error: unknown): {
|
|
12
|
+
statusCode: number;
|
|
13
|
+
body: {
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
} {
|
|
21
|
+
if (error instanceof HttpError) {
|
|
22
|
+
return {
|
|
23
|
+
statusCode: error.statusCode,
|
|
24
|
+
body: {
|
|
25
|
+
success: false,
|
|
26
|
+
error: {
|
|
27
|
+
code: error.code,
|
|
28
|
+
message: error.message,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
statusCode: 500,
|
|
36
|
+
body: {
|
|
37
|
+
success: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: "INTERNAL_ERROR",
|
|
40
|
+
message: error instanceof Error ? error.message : String(error),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/lib/http.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface ParsedServerArgs {
|
|
2
|
+
host?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ResolvedServerConfig {
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parsePort(raw: string, source: string): number {
|
|
13
|
+
const value = Number(raw);
|
|
14
|
+
if (!Number.isInteger(value) || value <= 0 || value > 65535) {
|
|
15
|
+
throw new Error(`Invalid value for ${source}: ${raw}`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isMissingOptionValue(raw: string | undefined): raw is undefined {
|
|
21
|
+
return !raw || raw.startsWith("--");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseServerArgs(argv: string[]): ParsedServerArgs {
|
|
25
|
+
const parsed: ParsedServerArgs = {};
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const token = argv[index];
|
|
29
|
+
|
|
30
|
+
if (token === "--port") {
|
|
31
|
+
const raw = argv[index + 1];
|
|
32
|
+
if (isMissingOptionValue(raw)) {
|
|
33
|
+
throw new Error("Missing value for --port");
|
|
34
|
+
}
|
|
35
|
+
parsed.port = parsePort(raw, "--port");
|
|
36
|
+
index += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (token === "--host") {
|
|
41
|
+
const raw = argv[index + 1];
|
|
42
|
+
if (isMissingOptionValue(raw)) {
|
|
43
|
+
throw new Error("Missing value for --host");
|
|
44
|
+
}
|
|
45
|
+
parsed.host = raw;
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveServerConfig({
|
|
54
|
+
args,
|
|
55
|
+
env,
|
|
56
|
+
}: {
|
|
57
|
+
args: ParsedServerArgs;
|
|
58
|
+
env: NodeJS.ProcessEnv;
|
|
59
|
+
}): ResolvedServerConfig {
|
|
60
|
+
return {
|
|
61
|
+
host: args.host ?? env.HOST ?? "0.0.0.0",
|
|
62
|
+
port: args.port ?? (env.PORT ? parsePort(env.PORT, "PORT") : 4002),
|
|
63
|
+
apiKey: env.GATEWAY_API_KEY ?? env.MICROSANDBOX_API_KEY,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { createImageController } from "../controllers/images";
|
|
3
|
+
import { ImageService } from "../services/ImageService";
|
|
4
|
+
|
|
5
|
+
export function registerImageRoutes(app: FastifyInstance, imageService: ImageService): void {
|
|
6
|
+
const controller = createImageController(imageService);
|
|
7
|
+
|
|
8
|
+
app.get("/api/images", controller.listImages);
|
|
9
|
+
app.post("/api/images/pull", controller.pullImage);
|
|
10
|
+
app.get("/api/images/detail", controller.getImage);
|
|
11
|
+
app.delete("/api/images/detail", controller.deleteImage);
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { createSandboxController } from "../controllers/sandbox";
|
|
3
|
+
import type { RuntimeService } from "../types/runtime-service";
|
|
4
|
+
|
|
5
|
+
export function registerSandboxRoutes(
|
|
6
|
+
app: FastifyInstance,
|
|
7
|
+
runtimeService: RuntimeService
|
|
8
|
+
): void {
|
|
9
|
+
const controller = createSandboxController(runtimeService);
|
|
10
|
+
|
|
11
|
+
app.get("/api/sandboxes", controller.listSandboxes);
|
|
12
|
+
app.get("/api/sandboxes/:name", controller.getSandbox);
|
|
13
|
+
app.put("/api/sandboxes/:name", controller.ensureSandbox);
|
|
14
|
+
app.post("/api/sandboxes/:name/start", controller.startSandbox);
|
|
15
|
+
app.post("/api/sandboxes/:name/stop", controller.stopSandbox);
|
|
16
|
+
app.post("/api/sandboxes/:name/kill", controller.killSandbox);
|
|
17
|
+
app.delete("/api/sandboxes/:name", controller.deleteSandbox);
|
|
18
|
+
app.get("/api/sandboxes/:name/status", controller.getStatus);
|
|
19
|
+
app.post("/api/sandboxes/:name/logs", controller.getSandboxLogs);
|
|
20
|
+
|
|
21
|
+
app.post("/api/files/read", controller.readFile);
|
|
22
|
+
app.post("/api/files/write", controller.writeFile);
|
|
23
|
+
app.post("/api/files/list", controller.listPath);
|
|
24
|
+
app.post("/api/files/find", controller.findFiles);
|
|
25
|
+
app.post("/api/files/search", controller.searchInFile);
|
|
26
|
+
app.post("/api/files/replace", controller.replaceInFile);
|
|
27
|
+
app.post("/api/files/upload", controller.uploadFile);
|
|
28
|
+
app.get("/api/files/download", controller.downloadFile);
|
|
29
|
+
|
|
30
|
+
app.post("/api/shell/exec", controller.execCommand);
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { createVolumeFsController } from "../controllers/volume-fs";
|
|
3
|
+
|
|
4
|
+
export function registerVolumeFsRoutes(app: FastifyInstance): void {
|
|
5
|
+
const controller = createVolumeFsController();
|
|
6
|
+
|
|
7
|
+
app.post("/api/volumes/:name/fs/read", controller.readFile);
|
|
8
|
+
app.post("/api/volumes/:name/fs/write", controller.writeFile);
|
|
9
|
+
app.post("/api/volumes/:name/fs/list", controller.listPath);
|
|
10
|
+
app.get("/api/volumes/:name/fs/download", controller.downloadFile);
|
|
11
|
+
app.post("/api/volumes/:name/fs/upload", controller.uploadFile);
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
export const imageRefQuerySchema = z.object({
|
|
4
|
+
ref: z.string().min(1),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const pullImageSchema = z.object({
|
|
8
|
+
ref: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type ImageRefQuery = z.infer<typeof imageRefQuerySchema>;
|
|
12
|
+
export type PullImageInput = z.infer<typeof pullImageSchema>;
|