import fs from "node:fs/promises"; import path from "node:path"; import { execFileSync } from "node:child_process"; export async function buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile, }) { const relative = compiler.artifactPath; if (config.restricted) { if (config.service.entry || pkg.exports.some((e) => e.execution.kind === "remote")) throw Error( "Restricted services require portable snapshot/transaction exports; remote/custom services keep their VM path", ); const modules = implementations.filter((i) => i.target === "server"); if (modules.length !== 1 || pkg.exports.some((e) => !modules[0].exports.includes(e.id))) throw Error("Restricted service requires exactly one checked portable registry covering all exports"); const limits = config.restricted.limits; if (!limits || typeof limits !== "object" || Array.isArray(limits)) throw Error("Restricted limits must be an object"); if (Object.keys(limits).some((key) => !["deadlineMs", "maxBytes", "maxPortCalls", "maxQueued"].includes(key))) throw Error("Unknown restricted service limit"); for (const [key, min, max] of [ ["deadlineMs", 1, 60000], ["maxBytes", 1024, 4194304], ["maxPortCalls", 1, 10000], ["maxQueued", 0, 64], ]) if (limits[key] !== undefined && (!Number.isSafeInteger(limits[key]) || limits[key] < min || limits[key] > max)) throw Error("Invalid restricted service limit " + key); await fs.mkdir(path.join(output, "bin"), { recursive: true }); await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); const configFile = "share/quixos/service/restricted.json"; const module = modules[0]; await fs.writeFile( path.join(output, configFile), JSON.stringify({ module: path.relative("share/quixos/service", module.path), registryExport: module.registryExport, exports: module.exports, sdk: [{ name: config.restricted.sdk, path: config.restricted.sdk }], ...limits, }), ); await fs.writeFile( path.join(output, "bin/service"), `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.restricted.runtime} "$service_root/${configFile}"\n`, { mode: 0o755 }, ); const body = { id: "service", target: "server", compatibility: "quixos-service-v1", path: "bin/service", ports: "quixos-transaction-ports-v1", exports: module.exports, files: [ await recordFile("bin/service", "text/x-shellscript"), await recordFile(configFile, "application/json"), ...module.files, ], }; implementations.unshift({ ...body, contentDigest: compiler.implementationDigest(body) }); return; } if (pkg.exports.length || config.service.entry) { await fs.mkdir(path.join(output, "bin"), { recursive: true }); await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); let serviceEntry; if (config.service.entry) { if (!/\.m?js$/.test(config.service.entry)) throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging"); serviceEntry = path.join(config.source, relative(config.service.entry)); } else { const modules = implementations.filter((implementation) => implementation.target === "server"); if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id)))) throw Error("Generated service does not cover every package export; provide a custom service"); if (!modules.length) throw Error("Generated service requires a checked registry"); serviceEntry = path.resolve("service-entry.mjs"); const imports = modules .map( (module, index) => `import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`, ) .join("\n"); await fs.writeFile( serviceEntry, imports + `\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)}; const registries = [${modules.map((_, index) => "registry" + index).join(",")}]; const owners = new Map(); for (const registry of registries) for (const id of registry.exports) { if (owners.has(id)) throw Error("Duplicate registry export " + id); owners.set(id, registry); } servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) { const registry = owners.get(request.exportId); if (!registry) throw Error("Unknown export"); return registry.invoke(request, channel, context); }});\n`, ); } const serviceModule = "share/quixos/service/main.mjs"; execFileSync( config.esbuild, [ serviceEntry, "--bundle", "--platform=node", "--format=esm", "--minify-whitespace", "--target=es2024", `--external:${coreServer}`, `--outfile=${path.join(output, serviceModule)}`, ], { stdio: "inherit" }, ); await fs.writeFile( path.join(output, "bin/service"), `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`, { mode: 0o755 }, ); const serviceBody = { id: "service", target: "server", compatibility: "quixos-service-v1", path: "bin/service", ports: "quixos-transaction-ports-v1", exports: pkg.exports.map((e) => e.id), files: [ await recordFile("bin/service", "text/x-shellscript"), await recordFile(serviceModule, "text/javascript"), ...sharedFiles, ], }; implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) }); } }