run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
1,521 lines • 66.1 kB
JavaScript
import { copyFile, readFile, rm, stat } from "node:fs/promises";
import { dirname, extname, isAbsolute, resolve as resolvePath, } from "node:path";
import { pathToFileURL } from "node:url";
import { LocalError } from "../errors.js";
import { ROUTE_HTTP_METHODS } from "../namespaces/deploy.types.js";
function isLocalDirRef(value) {
return (typeof value === "object" &&
value !== null &&
value.__source === "local-dir" &&
typeof value.path === "string");
}
function isFsFileSource(value) {
return (typeof value === "object" &&
value !== null &&
value.__source === "fs-file" &&
typeof value.path === "string");
}
const CONTEXT = "normalizing deploy manifest";
/** Collect every local filesystem reference a normalized ReleaseSpec carries. */
export function collectLocalFileReferences(spec) {
const refs = [];
const pushFile = (field_path, path, kind) => {
refs.push({ field_path, path, kind, expects: "file" });
};
const visitFileSet = (set, fieldPrefix, kind) => {
if (!set || typeof set !== "object")
return;
for (const [path, entry] of Object.entries(set)) {
if (isFsFileSource(entry))
pushFile(`${fieldPrefix}[${JSON.stringify(path)}]`, entry.path, kind);
}
};
const visitFunctions = (map, fieldPrefix) => {
if (!map || typeof map !== "object")
return;
for (const [name, fn] of Object.entries(map)) {
if (!fn || typeof fn !== "object")
continue;
if (isFsFileSource(fn.source))
pushFile(`${fieldPrefix}.${name}.source`, fn.source.path, "function_source");
visitFileSet(fn.files, `${fieldPrefix}.${name}.files`, "function_source");
}
};
visitFunctions(spec.functions?.replace, "functions.replace");
visitFunctions(spec.functions?.patch?.set, "functions.patch.set");
const site = spec.site;
const visitSiteTarget = (target, fieldPath) => {
if (!target)
return;
if (isLocalDirRef(target)) {
refs.push({ field_path: fieldPath, path: target.path, kind: "site_dir", expects: "directory" });
return;
}
visitFileSet(target, fieldPath, "site_file");
};
if (site && typeof site === "object") {
visitSiteTarget(site.replace, "site.replace");
visitSiteTarget(site.patch?.put, "site.patch.put");
}
const puts = spec.assets?.put;
if (Array.isArray(puts)) {
puts.forEach((entry, idx) => {
const source = entry?.source;
if (isFsFileSource(source))
pushFile(`assets.put[${idx}].source`, source.path, "asset");
});
}
return refs;
}
/** `stat` every local reference; returns the ones that are absent or of the wrong kind. */
export async function findMissingLocalFileReferences(spec) {
const refs = collectLocalFileReferences(spec);
const missing = [];
const seen = new Set();
for (const ref of refs) {
const key = `${ref.field_path}\u0000${ref.path}`;
if (seen.has(key))
continue;
seen.add(key);
let ok = false;
try {
const info = await stat(ref.path);
ok = ref.expects === "directory" ? info.isDirectory() : info.isFile();
}
catch {
ok = false;
}
if (!ok)
missing.push({ field_path: ref.field_path, path: ref.path, kind: ref.kind });
}
return missing;
}
const LOCAL_FILE_KIND_LABEL = {
migration_sql: "migration SQL file",
function_source: "function source file",
site_file: "site file",
site_dir: "site directory",
asset: "asset file",
};
/**
* Build the `MANIFEST_FILE_MISSING` envelope: the message names the first
* missing file, `details.missing[]` lists all of them, and there is one
* `create_file` next action per missing reference.
*/
export function manifestFileMissingError(missing, opts = {}) {
const first = missing[0];
const rest = missing.length - 1;
const message = first
? `Manifest references a ${LOCAL_FILE_KIND_LABEL[first.kind]} that does not exist: ${first.path} (${first.field_path})` +
(rest > 0 ? ` and ${rest} more` : "") +
"."
: "Manifest references files that do not exist.";
return new LocalError(message, "checking manifest file references", {
code: "MANIFEST_FILE_MISSING",
details: {
...(opts.manifestPath ? { manifest_path: opts.manifestPath } : {}),
missing,
},
next_actions: [
...missing.map((entry) => ({
type: "create_file",
path: entry.path,
field_path: entry.field_path,
kind: entry.kind,
why: `${entry.field_path} points at a ${LOCAL_FILE_KIND_LABEL[entry.kind]} that does not exist. Create it at that path, or fix the reference in the manifest, then re-run.`,
})),
checkManifestNextAction(opts.manifestPath),
],
});
}
/**
* The `check_manifest` next action: `run402 up --check` validates the
* manifest and prints the preflight (file references, the site inventory by
* content type, function and migration counts) with no gateway call, so an
* agent can confirm a fix before spending an upload.
*/
export function checkManifestNextAction(manifestPath) {
return {
type: "check_manifest",
argv: ["run402", "up", ...(manifestPath ? ["--manifest", manifestPath] : []), "--check"],
why: "After the fix, validate locally: prints the preflight with the site inventory by content type and makes no gateway call.",
};
}
/**
* Throw `MANIFEST_FILE_MISSING` when any local filesystem reference in the
* normalized spec is absent. Cheap (one `stat` per reference) — run it in
* `--check` / `--print-spec` and before any plan so a typo in `source.path`
* or a site `{ path }` never reaches the gateway.
*/
export async function assertLocalFileReferencesExist(spec, opts = {}) {
const missing = await findMissingLocalFileReferences(spec);
if (missing.length > 0)
throw manifestFileMissingError(missing, opts);
}
/**
* Typed ENOENT for an explicitly named manifest path: `MANIFEST_NOT_FOUND`
* with `details.path` and a `create_manifest` next action pointing at it.
*/
export function manifestNotFoundError(path, context) {
const starter = {
site: {
replace: {
"index.html": { data: "<!doctype html><h1>Hello from Run402</h1>" },
},
},
};
return new LocalError(`Manifest not found: ${path}. Check the --manifest path (it is resolved against the current directory) or create the file.`, context, {
code: "MANIFEST_NOT_FOUND",
details: { path },
next_actions: [
{
type: "create_manifest",
path,
content: JSON.stringify(starter, null, 2),
why: "No file exists at the path given to --manifest. Create it (this minimal site manifest deploys as-is) or point --manifest at the file you meant.",
},
checkManifestNextAction(path),
],
});
}
function isMissingFileError(err) {
return typeof err === "object" && err !== null && err.code === "ENOENT";
}
const MANIFEST_FIELDS = new Set([
"$schema",
"x-run402-omitted_features",
"project_id",
"idempotency_key",
"base",
"database",
"secrets",
"functions",
"site",
"assets",
"subdomains",
"routes",
"checks",
"i18n",
"verify",
]);
const MANIFEST_VERIFY_FIELDS = new Set(["http"]);
const MANIFEST_VERIFY_HTTP_CHECK_FIELDS = new Set([
"id",
"path",
"url",
"expect",
"expected_status",
"retries",
]);
const MANIFEST_VERIFY_EXPECT_FIELDS = new Set(["status"]);
const MANIFEST_I18N_FIELDS = new Set([
"default_locale",
"defaultLocale",
"locales",
"detect",
"unknown_locale_policy",
"unknownLocalePolicy",
]);
const MANIFEST_DATABASE_FIELDS = new Set(["migrations", "expose", "zero_downtime"]);
const MANIFEST_MIGRATION_FIELDS = new Set([
"id",
"name",
"checksum",
"sql",
"sql_ref",
"sql_path",
"sql_file",
"transaction",
]);
const MANIFEST_FUNCTIONS_FIELDS = new Set(["replace", "patch"]);
const MANIFEST_FUNCTIONS_PATCH_FIELDS = new Set(["set", "delete"]);
const MANIFEST_FUNCTION_FIELDS = new Set([
"runtime",
"source",
"files",
"entrypoint",
"config",
"deps",
"triggers",
"schedule",
"requireAuth",
"requireRole",
"require_auth",
"require_role",
"class",
"capabilities",
]);
const MANIFEST_FUNCTION_CONFIG_FIELDS = new Set(["timeout_seconds", "memory_mb", "timeoutSeconds", "memoryMb"]);
const MANIFEST_REQUIRE_ROLE_FIELDS = new Set([
"table",
"idColumn",
"id_column",
"roleColumn",
"role_column",
"allowed",
"cacheTtl",
"cache_ttl",
"onDeny",
"on_deny",
"signInPath",
"sign_in_path",
]);
const MANIFEST_SITE_FIELDS = new Set(["replace", "patch", "public_paths", "embedding", "sign_in_path", "noindex"]);
const MANIFEST_SITE_PATCH_FIELDS = new Set(["put", "delete"]);
const MANIFEST_SITE_PUBLIC_PATHS_FIELDS = new Set(["mode", "replace"]);
const MANIFEST_PUBLIC_STATIC_PATH_FIELDS = new Set(["asset", "cache_class"]);
const MANIFEST_ASSETS_FIELDS = new Set(["put", "delete", "sync"]);
const MANIFEST_ASSETS_PUT_ENTRY_FIELDS = new Set([
"key",
"source",
"sha256",
"size_bytes",
"content_type",
"visibility",
"immutable",
]);
const MANIFEST_ASSETS_SYNC_FIELDS = new Set(["prefix", "prune", "confirm"]);
const MANIFEST_ASSETS_SYNC_CONFIRM_FIELDS = new Set([
"base_revision",
"delete_set_digest",
"expected_delete_count",
]);
const MANIFEST_ROUTES_FIELDS = new Set(["replace"]);
const MANIFEST_ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target", "pricing"]);
const MANIFEST_FUNCTION_ROUTE_TARGET_FIELDS = new Set(["type", "name"]);
const MANIFEST_STATIC_ROUTE_TARGET_FIELDS = new Set(["type", "file"]);
const ROUTE_METHOD_SET = new Set(ROUTE_HTTP_METHODS);
const DATA_MANIFEST_EXTENSIONS = new Set([".json"]);
const EXECUTABLE_MANIFEST_EXTENSIONS = new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]);
const TYPESCRIPT_MANIFEST_EXTENSIONS = new Set([".ts", ".mts", ".cts"]);
const TYPESCRIPT_FUNCTION_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]);
const EXECUTABLE_CONFIG_TIMEOUT_MS = 5_000;
const EXECUTABLE_CONFIG_METADATA = Symbol("run402.executableConfigMetadata");
const MIGRATION_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
export async function loadDeployManifest(path, opts = {}) {
const manifestPath = isAbsolute(path) ? path : resolvePath(process.cwd(), path);
try {
await stat(manifestPath);
}
catch (err) {
if (isMissingFileError(err))
throw manifestNotFoundError(manifestPath, "loading deploy manifest");
}
const extension = extname(manifestPath).toLowerCase();
if (EXECUTABLE_MANIFEST_EXTENSIONS.has(extension)) {
const loaded = await loadExecutableDeployConfig(manifestPath, {
...(opts.executableTimeoutMs !== undefined ? { timeoutMs: opts.executableTimeoutMs } : {}),
...(opts.env !== undefined ? { env: opts.env } : {}),
});
const metadata = getExecutableDeployConfigMetadata(loaded);
const normalized = await normalizeDeployManifest(loaded, {
...opts,
baseDir: dirname(manifestPath),
manifestPath,
});
return metadata
? { ...normalized, manifestPath, config: metadata }
: { ...normalized, manifestPath };
}
if (!DATA_MANIFEST_EXTENSIONS.has(extension)) {
throw new LocalError(`Unsupported deploy manifest extension '${extension || "(none)"}'. Use .json for data manifests or pass an explicit .ts/.js executable config.`, "loading deploy manifest", {
code: "UNSUPPORTED_DEPLOY_MANIFEST_EXTENSION",
details: { path, extension },
});
}
let raw;
try {
raw = await readFile(manifestPath, "utf-8");
}
catch (err) {
if (isMissingFileError(err))
throw manifestNotFoundError(manifestPath, "loading deploy manifest");
throw new LocalError(`Failed to read deploy manifest '${path}': ${err.message}`, "loading deploy manifest", err);
}
let parsed;
try {
parsed = JSON.parse(raw);
}
catch (err) {
throw new LocalError(`Deploy manifest is not valid JSON: ${err.message}`, "parsing deploy manifest", err);
}
const normalized = await normalizeDeployManifest(parsed, {
...opts,
baseDir: dirname(manifestPath),
manifestPath,
});
return { ...normalized, manifestPath };
}
export async function loadExecutableDeployConfig(path, opts = {}) {
const manifestPath = isAbsolute(path) ? path : resolvePath(process.cwd(), path);
const extension = extname(manifestPath).toLowerCase();
if (!EXECUTABLE_MANIFEST_EXTENSIONS.has(extension)) {
throw new LocalError(`Executable deploy config must use .ts, .mts, .cts, .js, .mjs, or .cjs; got '${extension || "(none)"}'.`, "loading executable deploy config", {
code: "UNSUPPORTED_EXECUTABLE_CONFIG_EXTENSION",
details: { path, extension },
});
}
let mod;
try {
mod = await importConfigModule(manifestPath, extension);
}
catch (err) {
let message = `Failed to load executable deploy config '${path}': ${err.message}`;
// a .ts manifest in a project whose nearest package.json lacks
// `"type": "module"` is compiled as CommonJS by the tsx loader, so its
// imports resolve with require conditions. Against an @run402/sdk older
// than 4.11.1 (import-only exports map) that fails with the misleading
// "Package subpath ... is not defined by \"exports\"" even though the
// subpath exists — no condition matches. Name the real fix in the error.
if (/is not defined by "exports".*@run402[\\/]sdk/s.test(err.message ?? "")) {
message +=
`\nLikely cause: this project's package.json has no "type": "module", so the ` +
`TypeScript manifest is loaded as CommonJS and the installed @run402/sdk predates ` +
`4.11.1 (its exports map had no require/default conditions). Fix either side: ` +
`upgrade @run402/sdk to >=4.11.1, or add "type": "module" to package.json.`;
}
throw new LocalError(message, "loading executable deploy config", {
cause: err,
code: "EXECUTABLE_CONFIG_LOAD_FAILED",
details: { path, extension },
});
}
const exported = resolveExecutableConfigExport(mod);
if (exported === undefined) {
throw new LocalError("Executable deploy config must export a default config object or a named `config` export.", "loading executable deploy config", {
code: "EXECUTABLE_CONFIG_INVALID_EXPORT",
details: { path, exports: Object.keys(mod).sort() },
});
}
const env = createConfigEnv(opts.env ?? process.env);
const context = {
manifestPath,
rootDir: dirname(manifestPath),
env,
};
let value;
try {
value = typeof exported === "function"
? await withConfigTimeout(Promise.resolve(exported(context)), path, opts.timeoutMs ?? EXECUTABLE_CONFIG_TIMEOUT_MS)
: exported;
}
catch (err) {
if (err instanceof LocalError)
throw err;
throw new LocalError(`Executable deploy config '${path}' failed while evaluating: ${err.message}`, "loading executable deploy config", {
cause: err,
code: "EXECUTABLE_CONFIG_EVALUATION_FAILED",
details: { path },
});
}
if (!isRecord(value) || Array.isArray(value)) {
throw new LocalError("Executable deploy config export must resolve to an object.", "loading executable deploy config", {
code: "EXECUTABLE_CONFIG_INVALID_EXPORT",
details: { path, received: typeof value },
});
}
assertNoUnsupportedTypeScriptFunctionSources(value, "config");
attachExecutableDeployConfigMetadata(value, { env_accessed: [...env.accessed] });
return value;
}
function resolveExecutableConfigExport(mod) {
let exported = (mod.default ?? mod.config);
for (let i = 0; i < 2; i++) {
if (!isRecord(exported) || looksLikeDeployConfig(exported))
break;
const wrapper = exported;
const nested = wrapper.default ?? wrapper.config;
if (nested === undefined)
break;
exported = nested;
}
return exported;
}
function looksLikeDeployConfig(value) {
return [...MANIFEST_FIELDS].some((field) => Object.prototype.hasOwnProperty.call(value, field));
}
function createConfigEnv(source) {
const accessed = new Set();
const read = (name) => {
accessed.add(name);
return source[name];
};
return new Proxy(Object.create(null), {
get(_target, prop) {
if (prop === "accessed")
return [...accessed].sort();
if (prop === "get")
return read;
if (prop === "required") {
return (name) => {
const value = read(name);
if (value === undefined || value === "") {
throw new LocalError(`Required environment variable ${name} is not set.`, "evaluating executable deploy config", {
code: "CONFIG_ENV_REQUIRED",
details: { name },
});
}
return value;
};
}
if (prop === Symbol.toStringTag)
return "Run402ConfigEnv";
if (typeof prop !== "string" || prop === "then" || prop === "toJSON")
return undefined;
return read(prop);
},
});
}
function attachExecutableDeployConfigMetadata(value, metadata) {
Object.defineProperty(value, EXECUTABLE_CONFIG_METADATA, {
value: { env_accessed: [...metadata.env_accessed].sort() },
enumerable: false,
configurable: true,
});
}
function getExecutableDeployConfigMetadata(value) {
return value[EXECUTABLE_CONFIG_METADATA];
}
export async function normalizeDeployManifest(input, opts = {}) {
assertPlainRecord(input, "Deploy manifest");
const manifest = input;
assertKnownFields(manifest, "Deploy manifest", MANIFEST_FIELDS, {
subdomain: "Use `subdomains: { set: [name] }`.",
});
if (opts.validateFiles)
await assertAuthoringFileReferences(manifest, opts.baseDir ?? process.cwd(), opts.manifestPath);
const project = resolveProject(manifest, opts);
const spec = { project_id: project };
if (manifest.base !== undefined)
spec.base = manifest.base;
if (manifest.subdomains !== undefined)
spec.subdomains = manifest.subdomains;
if (manifest.secrets !== undefined)
spec.secrets = manifest.secrets;
if (manifest.routes !== undefined)
spec.routes = mapRoutes(manifest.routes);
if (manifest.checks !== undefined)
spec.checks = manifest.checks;
if (manifest.i18n !== undefined)
spec.i18n = mapI18n(manifest.i18n);
if (manifest.database !== undefined) {
spec.database = await mapDatabase(manifest.database, opts);
}
if (manifest.functions !== undefined) {
spec.functions = mapFunctions(manifest.functions, opts);
}
if (manifest.site !== undefined) {
spec.site = mapSite(manifest.site, opts);
}
if (manifest.assets !== undefined) {
spec.assets = mapAssets(manifest.assets, opts);
}
// Authoring-only: `verify` never reaches the wire ReleaseSpec (like
// `$schema`); it is validated here and returned as sidecar metadata.
const verify = manifest.verify !== undefined ? mapVerify(manifest.verify) : undefined;
const idempotencyKey = resolveIdempotencyKey(manifest);
return {
spec,
manifest,
...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
...(verify !== undefined ? { verify } : {}),
};
}
const VERIFY_HINT = 'Minimal valid shape: "verify": { "http": [{ "id": "home", "path": "/", "expect": { "status": 200 } }] }';
function verifyError(message, field) {
return new LocalError(message, CONTEXT, {
code: "INVALID_VERIFY_SPEC",
details: { field, hint: VERIFY_HINT },
});
}
function mapVerify(verify) {
if (!isRecord(verify) || Array.isArray(verify)) {
throw verifyError("Deploy manifest verify must be a JSON object", "verify");
}
assertKnownFields(verify, "Deploy manifest verify", MANIFEST_VERIFY_FIELDS, {
checks: "Use `verify.http` (an array of HTTP checks).",
});
const raw = verify;
const http = [];
if (raw.http !== undefined) {
if (!Array.isArray(raw.http)) {
throw verifyError("Deploy manifest verify.http must be an array of checks", "verify.http");
}
const seenIds = new Map();
for (const [index, check] of raw.http.entries()) {
http.push(mapVerifyHttpCheck(check, index, seenIds));
}
}
return { http };
}
function mapVerifyHttpCheck(check, index, seenIds) {
const field = `verify.http[${index}]`;
if (!isRecord(check) || Array.isArray(check)) {
throw verifyError(`Deploy manifest ${field} must be a JSON object`, field);
}
assertKnownFields(check, `Deploy manifest ${field}`, MANIFEST_VERIFY_HTTP_CHECK_FIELDS, {
method: "verify.http checks are always fetched with GET; there is no method field.",
status: "Use `expect: { status }` (or the `expected_status` alias).",
});
const raw = check;
if (typeof raw.id !== "string" || raw.id.length === 0) {
throw verifyError(`Deploy manifest ${field}.id must be a non-empty string`, `${field}.id`);
}
const firstIndex = seenIds.get(raw.id);
if (firstIndex !== undefined) {
throw verifyError(`Deploy manifest ${field}.id duplicates verify.http[${firstIndex}].id ${JSON.stringify(raw.id)}`, `${field}.id`);
}
seenIds.set(raw.id, index);
if (raw.path !== undefined && (typeof raw.path !== "string" || raw.path.length === 0)) {
throw verifyError(`Deploy manifest ${field}.path must be a non-empty string`, `${field}.path`);
}
if (raw.url !== undefined && (typeof raw.url !== "string" || raw.url.length === 0)) {
throw verifyError(`Deploy manifest ${field}.url must be a non-empty string`, `${field}.url`);
}
if (raw.path === undefined && raw.url === undefined) {
throw verifyError(`Deploy manifest ${field} must declare \`path\` (resolved against the project public origin) or \`url\``, field);
}
let expectStatus;
if (raw.expect !== undefined) {
if (!isRecord(raw.expect) || Array.isArray(raw.expect)) {
throw verifyError(`Deploy manifest ${field}.expect must be a JSON object`, `${field}.expect`);
}
assertKnownFields(raw.expect, `Deploy manifest ${field}.expect`, MANIFEST_VERIFY_EXPECT_FIELDS);
const status = raw.expect.status;
if (!isHttpStatus(status)) {
throw verifyError(`Deploy manifest ${field}.expect.status must be an integer HTTP status (100-599)`, `${field}.expect.status`);
}
expectStatus = status;
}
if (raw.expected_status !== undefined) {
if (!isHttpStatus(raw.expected_status)) {
throw verifyError(`Deploy manifest ${field}.expected_status must be an integer HTTP status (100-599)`, `${field}.expected_status`);
}
if (expectStatus !== undefined && expectStatus !== raw.expected_status) {
throw verifyError(`Deploy manifest ${field} must not set both expect.status and expected_status with different values`, `${field}.expected_status`);
}
expectStatus = raw.expected_status;
}
if (expectStatus === undefined) {
throw verifyError(`Deploy manifest ${field} must declare the expected HTTP status via \`expect: { status }\` (or \`expected_status\`)`, `${field}.expect.status`);
}
if (raw.retries !== undefined && (!Number.isSafeInteger(raw.retries) || raw.retries < 1)) {
throw verifyError(`Deploy manifest ${field}.retries must be a positive integer`, `${field}.retries`);
}
return {
id: raw.id,
...(raw.path !== undefined ? { path: raw.path } : {}),
...(raw.url !== undefined ? { url: raw.url } : {}),
expect: { status: expectStatus },
...(raw.retries !== undefined ? { retries: raw.retries } : {}),
};
}
function isHttpStatus(value) {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 100 && value <= 599;
}
function resolveProject(manifest, opts) {
const manifestProject = resolveManifestProject(manifest);
if (opts.project !== undefined &&
manifestProject !== undefined &&
opts.project !== manifestProject) {
throw new LocalError(`project conflict: manifest project_id=${manifestProject} but override project=${opts.project}`, CONTEXT);
}
const project = opts.project ?? manifestProject ?? opts.defaultProject;
if (!project) {
throw new LocalError("Deploy manifest requires project_id. Pass a project override or set a default project before normalizing.", CONTEXT);
}
return project;
}
function resolveIdempotencyKey(manifest) {
return manifest.idempotency_key;
}
function resolveManifestProject(manifest) {
return manifest.project_id;
}
async function mapDatabase(database, opts) {
assertPlainRecord(database, "Deploy manifest database");
assertKnownFields(database, "Deploy manifest database", MANIFEST_DATABASE_FIELDS);
const raw = database;
const out = {};
if (raw.expose !== undefined)
out.expose = raw.expose;
if (raw.zero_downtime !== undefined) {
out.zero_downtime = raw.zero_downtime;
}
if (raw.migrations !== undefined) {
if (!Array.isArray(raw.migrations)) {
throw new LocalError("Deploy manifest database.migrations must be an array", CONTEXT);
}
out.migrations = [];
const seenNames = new Map();
for (const [index, migration] of raw.migrations.entries()) {
out.migrations.push(await mapMigration(migration, opts, index, seenNames));
}
}
return out;
}
async function mapMigration(migration, opts, index, seenNames) {
assertPlainRecord(migration, "Deploy manifest database.migrations[]");
assertKnownFields(migration, "Deploy manifest database.migrations[]", MANIFEST_MIGRATION_FIELDS);
const hasId = Object.prototype.hasOwnProperty.call(migration, "id") && migration.id !== undefined;
const hasName = Object.prototype.hasOwnProperty.call(migration, "name") && migration.name !== undefined;
const label = migrationLabel(migration, index);
if (hasId && hasName) {
throw new LocalError(`${label} declares both id and name; use exactly one`, CONTEXT);
}
if (!hasId && !hasName) {
throw new LocalError(`${label} must declare exactly one of id or name`, CONTEXT);
}
let out;
if (hasName) {
if (typeof migration.name !== "string" || !MIGRATION_NAME_RE.test(migration.name)) {
throw new LocalError(`${label}.name must match /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/`, CONTEXT);
}
const firstIndex = seenNames.get(migration.name);
if (firstIndex !== undefined) {
throw new LocalError(`${label}.name duplicates database.migrations[${firstIndex}].name ${JSON.stringify(migration.name)}`, CONTEXT);
}
seenNames.set(migration.name, index);
out = { name: migration.name };
}
else {
if (typeof migration.id !== "string" || migration.id.trim() === "") {
throw new LocalError(`${label}.id must be a non-empty string`, CONTEXT);
}
out = { id: migration.id };
}
if (migration.sql !== undefined)
out.sql = migration.sql;
if (migration.sql_ref !== undefined)
out.sql_ref = migration.sql_ref;
if (migration.checksum !== undefined)
out.checksum = migration.checksum;
if (migration.transaction !== undefined)
out.transaction = migration.transaction;
const sqlPath = migration.sql_path ?? migration.sql_file;
if (migration.sql_path !== undefined &&
migration.sql_file !== undefined &&
migration.sql_path !== migration.sql_file) {
throw new LocalError(`${label} has both sql_path and sql_file with different values`, CONTEXT);
}
if (out.sql === undefined && sqlPath !== undefined) {
const field = migration.sql_path !== undefined ? "sql_path" : "sql_file";
const abs = resolveLocalPath(sqlPath, opts.baseDir);
try {
out.sql = await readFile(abs, "utf-8");
}
catch (err) {
if (isMissingFileError(err)) {
const typed = manifestFileMissingError([
{ field_path: `database.migrations[${index}].${field}`, path: abs, kind: "migration_sql" },
]);
throw new LocalError(`Failed to read migration ${field} '${sqlPath}' for ${label}: ${typed.message}`, CONTEXT, {
cause: err,
code: "MANIFEST_FILE_MISSING",
details: typed.details,
next_actions: typed.nextActions,
});
}
throw new LocalError(`Failed to read migration ${field} '${sqlPath}' for ${label}: ${err.message}`, CONTEXT, err);
}
}
return out;
}
function migrationLabel(migration, index) {
const identity = typeof migration.id === "string"
? migration.id
: typeof migration.name === "string"
? migration.name
: null;
return identity
? `Deploy manifest database.migrations[${index}] (${identity})`
: `Deploy manifest database.migrations[${index}]`;
}
function mapFunctions(functions, opts) {
assertPlainRecord(functions, "Deploy manifest functions");
assertKnownFields(functions, "Deploy manifest functions", MANIFEST_FUNCTIONS_FIELDS);
const out = {};
const replace = functions.replace;
if (replace !== undefined) {
assertPlainRecord(replace, "Deploy manifest functions.replace");
out.replace = mapFunctionMap(replace, opts);
}
const patch = functions.patch;
if (patch !== undefined) {
assertPlainRecord(patch, "Deploy manifest functions.patch");
assertKnownFields(patch, "Deploy manifest functions.patch", MANIFEST_FUNCTIONS_PATCH_FIELDS);
out.patch = {};
if (patch.set !== undefined) {
assertPlainRecord(patch.set, "Deploy manifest functions.patch.set");
out.patch.set = mapFunctionMap(patch.set, opts);
}
if (patch.delete !== undefined) {
if (!Array.isArray(patch.delete)) {
throw new LocalError("Deploy manifest functions.patch.delete must be an array", CONTEXT);
}
out.patch.delete = patch.delete;
}
}
return out;
}
function mapFunctionMap(map, opts) {
const out = {};
for (const [name, fn] of Object.entries(map)) {
out[name] = mapFunction(fn, opts);
}
return out;
}
function mapFunctionConfig(config) {
assertPlainRecord(config, "Deploy manifest function config");
assertKnownFields(config, "Deploy manifest function config", MANIFEST_FUNCTION_CONFIG_FIELDS);
const timeoutSeconds = config.timeoutSeconds ?? config.timeout_seconds;
const memoryMb = config.memoryMb ?? config.memory_mb;
return {
...(timeoutSeconds !== undefined ? { timeoutSeconds: timeoutSeconds } : {}),
...(memoryMb !== undefined ? { memoryMb: memoryMb } : {}),
};
}
function mapRequireRole(value) {
assertPlainRecord(value, "Deploy manifest function require_role");
assertKnownFields(value, "Deploy manifest function require_role", MANIFEST_REQUIRE_ROLE_FIELDS);
return {
table: value.table,
idColumn: (value.idColumn ?? value.id_column),
roleColumn: (value.roleColumn ?? value.role_column),
allowed: value.allowed,
...(value.cacheTtl !== undefined || value.cache_ttl !== undefined
? { cacheTtl: (value.cacheTtl ?? value.cache_ttl) }
: {}),
...(value.onDeny !== undefined || value.on_deny !== undefined
? { onDeny: (value.onDeny ?? value.on_deny) }
: {}),
...(value.signInPath !== undefined || value.sign_in_path !== undefined
? { signInPath: (value.signInPath ?? value.sign_in_path) }
: {}),
};
}
function mapFunction(fn, opts) {
assertPlainRecord(fn, "Deploy manifest function entry");
assertKnownFields(fn, "Deploy manifest function entry", MANIFEST_FUNCTION_FIELDS);
const raw = fn;
const out = {};
if (raw.runtime !== undefined)
out.runtime = raw.runtime;
if (raw.source !== undefined) {
out.source = fileEntryToContentSource(raw.source, opts, "functions.source");
}
if (raw.files !== undefined)
out.files = mapFileSet(raw.files, opts);
if (raw.entrypoint !== undefined)
out.entrypoint = raw.entrypoint;
if (raw.config !== undefined)
out.config = mapFunctionConfig(raw.config);
if (raw.deps !== undefined)
out.deps = [...raw.deps];
if (raw.triggers !== undefined)
out.triggers = raw.triggers;
if (raw.schedule !== undefined)
out.schedule = raw.schedule;
const rawRecord = raw;
const requireAuth = rawRecord.requireAuth ?? rawRecord.require_auth;
const requireRole = rawRecord.requireRole ?? rawRecord.require_role;
if (requireAuth !== undefined)
out.requireAuth = requireAuth;
if (requireRole !== undefined) {
out.requireRole = requireRole === null ? null : mapRequireRole(requireRole);
}
if (raw.class !== undefined)
out.class = raw.class;
if (rawRecord.capabilities !== undefined) {
if (!Array.isArray(rawRecord.capabilities) || rawRecord.capabilities.some((v) => typeof v !== "string")) {
throw new LocalError("Deploy manifest function capabilities must be an array of strings", CONTEXT);
}
out.capabilities = [...rawRecord.capabilities];
}
return out;
}
async function importConfigModule(manifestPath, extension) {
const url = pathToFileURL(manifestPath).href;
if (TYPESCRIPT_MANIFEST_EXTENSIONS.has(extension)) {
let tsx;
try {
tsx = (await import("tsx/esm/api"));
}
catch (err) {
throw new LocalError("TypeScript deploy configs require the bundled tsx loader.", "loading executable deploy config", {
cause: err,
code: "EXECUTABLE_CONFIG_TS_LOADER_UNAVAILABLE",
details: { manifest_path: manifestPath },
});
}
try {
return await tsx.tsImport(url, { parentURL: import.meta.url });
}
catch (err) {
// tsx picks the module format from the manifest's
// NEAREST package.json. In a project without `"type": "module"` (the
// `npm init` default) a .ts manifest compiles to CommonJS, so its
// imports resolve with require conditions — and an import-only exports
// map (any @run402/sdk before 4.11.1, and plenty of other packages)
// fails with "Package subpath ... is not defined by exports" even
// though the subpath exists. Retry once through a sibling .mts file:
// the .mts extension forces ESM regardless of package.json, and living
// in the SAME directory keeps relative imports and node_modules
// resolution identical. Only this exact failure is retried.
if (!isExportsConditionFailure(err))
throw err;
const retried = await importAsEsmSibling(manifestPath);
if (retried)
return retried;
throw err;
}
}
return await import(`${url}?run402_config=${Date.now()}`);
}
/** Node's signature for "no exports condition matched" (ERR_PACKAGE_PATH_NOT_EXPORTED). */
function isExportsConditionFailure(err) {
const code = err?.code;
if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED")
return true;
const message = err?.message ?? "";
return /is not defined by "exports"/.test(message);
}
/**
* Copy the manifest to a sibling `.mts` file and import that, so the module
* format is ESM no matter what the project's package.json says. Returns null
* (rather than throwing) when the sibling cannot be written — e.g. a
* read-only checkout — so the caller can surface the ORIGINAL error, which
* names the real fix.
*/
async function importAsEsmSibling(manifestPath) {
const suffix = Math.random().toString(36).slice(2, 10);
const sibling = `${manifestPath}.run402-esm-${suffix}.mts`;
try {
await copyFile(manifestPath, sibling);
}
catch {
return null;
}
try {
const tsx = (await import("tsx/esm/api"));
return await tsx.tsImport(pathToFileURL(sibling).href, {
parentURL: import.meta.url,
});
}
finally {
await rm(sibling, { force: true }).catch(() => { });
}
}
async function withConfigTimeout(promise, path, timeoutMs) {
let timer;
try {
return await Promise.race([
promise,
new Promise((_resolve, reject) => {
timer = setTimeout(() => {
reject(new LocalError(`Executable deploy config '${path}' did not finish within ${timeoutMs}ms.`, "loading executable deploy config", {
code: "EXECUTABLE_CONFIG_TIMEOUT",
details: { path, timeout_ms: timeoutMs },
}));
}, timeoutMs);
}),
]);
}
finally {
if (timer)
clearTimeout(timer);
}
}
function assertNoUnsupportedTypeScriptFunctionSources(value, resource) {
if (!isRecord(value))
return;
const functions = value.functions;
if (!isRecord(functions))
return;
const maps = [];
if (isRecord(functions.replace))
maps.push([`${resource}.functions.replace`, functions.replace]);
if (isRecord(functions.patch) && isRecord(functions.patch.set)) {
maps.push([`${resource}.functions.patch.set`, functions.patch.set]);
}
for (const [mapResource, map] of maps) {
for (const [name, fn] of Object.entries(map)) {
if (!isRecord(fn))
continue;
const source = fn.source;
if (!isRecord(source) || typeof source.path !== "string")
continue;
const extension = extname(source.path).toLowerCase();
if (TYPESCRIPT_FUNCTION_EXTENSIONS.has(extension)) {
throw new LocalError(`nodeFunction('${source.path}') points at TypeScript source. Pre-bundle it to JavaScript for now; Run402 will not upload unbundled function TypeScript.`, "loading executable deploy config", {
code: "TYPESCRIPT_FUNCTION_REQUIRES_BUNDLE",
details: { resource: `${mapResource}.${name}.source`, path: source.path },
});
}
}
}
}
function mapSite(site, opts) {
assertPlainRecord(site, "Deploy manifest site");
assertKnownFields(site, "Deploy manifest site", MANIFEST_SITE_FIELDS, {
file: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
files: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
});
const raw = site;
if (Object.prototype.hasOwnProperty.call(raw, "replace") &&
Object.prototype.hasOwnProperty.call(raw, "patch")) {
throw new LocalError("Deploy manifest site must use either replace or patch, not both", CONTEXT);
}
const publicPaths = Object.prototype.hasOwnProperty.call(raw, "public_paths")
? mapSitePublicPaths(raw.public_paths)
: undefined;
// tenant-site-embedding: `null` clears, so presence (not truthiness) decides.
const embedding = Object.prototype.hasOwnProperty.call(raw, "embedding")
? { embedding: mapSiteEmbedding(raw.embedding) }
: {};
// site.sign_in_path: same presence rule (`null` clears to the hosted page).
const signInPath = Object.prototype.hasOwnProperty.call(raw, "sign_in_path")
? { sign_in_path: mapSiteSignInPath(raw.sign_in_path) }
: {};
// site.noindex: same presence rule (`false` or `null` clears to indexable).
const noindex = Object.prototype.hasOwnProperty.call(raw, "noindex")
? { noindex: mapSiteNoindex(raw.noindex) }
: {};
if (Object.prototype.hasOwnProperty.call(raw, "replace")) {
if (raw.replace === undefined) {
throw new LocalError("Deploy manifest site.replace is undefined", CONTEXT);
}
return {
replace: isLocalDirRef(raw.replace)
? resolveLocalDirRef(raw.replace, opts)
: mapFileSet(raw.replace, opts),
...(publicPaths ? { public_paths: publicPaths } : {}),
...embedding,
...signInPath,
...noindex,
};
}
if (Object.prototype.hasOwnProperty.call(raw, "patch")) {
const patch = {};
assertPlainRecord(raw.patch, "Deploy manifest site.patch");
const rawPatch = raw.patch;
assertKnownFields(rawPatch, "Deploy manifest site.patch", MANIFEST_SITE_PATCH_FIELDS);
if (rawPatch.put !== undefined) {
patch.put = isLocalDirRef(rawPatch.put)
? resolveLocalDirRef(rawPatch.put, opts)
: mapFileSet(rawPatch.put, opts);
}
if (rawPatch.delete !== undefined) {
if (!Array.isArray(rawPatch.delete)) {
throw new LocalError("Deploy manifest site.patch.delete must be an array", CONTEXT);
}
patch.delete = rawPatch.delete;
}
return {
patch,
...(publicPaths ? { public_paths: publicPaths } : {}),
...embedding,
...signInPath,
...noindex,
};
}
if (publicPaths || "embedding" in embedding || "sign_in_path" in signInPath || "noindex" in noindex) {
return {
...(publicPaths ? { public_paths: publicPaths } : {}),
...embedding,
...signInPath,
...noindex,
};
}
throw new LocalError("Deploy manifest site must include replace, patch, public_paths, embedding, sign_in_path, or noindex", CONTEXT);
}
function resolveLocalDirRef(ref, opts) {
return {
...ref,
path: resolveLocalPath(ref.path, opts.baseDir),
};
}
function mapSitePublicPaths(value) {
assertPlainRecord(value, "Deploy manifest site.public_paths");
assertKnownFields(value, "Deploy manifest site.public_paths", MANIFEST_SITE_PUBLIC_PATHS_FIELDS);
if (value.mode !== "implicit" && value.mode !== "explicit") {
throw new LocalError('Deploy manifest site.public_paths.mode must be "implicit" or "explicit"', CONTEXT);
}
if (value.mode === "implicit") {
if (Object.prototype.hasOwnProperty.call(value, "replace")) {
throw new LocalError("Deploy manifest site.public_paths.replace is invalid for implicit mode", CONTEXT);
}
return { mode: "implicit" };
}
if (!Object.prototype.hasOwnProperty.call(value, "replace")) {
throw new LocalError("Deploy manifest site.public_paths with mode explicit requires a complete replace map", CONTEXT);
}
assertPlainRecord(value.replace, "Deploy manifest site.public_paths.replace");
const replace = {};
for (const [publicPath, entry] of Object.entries(value.replace)) {
const label = `Deploy manifest site.public_paths.replace[${JSON.stringify(publicPath)}]`;
assertPlainRecord(entry, label);
assertKnownFields(entry, label, MANIFEST_PUBLIC_STATIC_PATH_FIELDS);
if (typeof entry.asset !== "string" || entry.asset.length === 0) {
throw new LocalError(`${label}.asset must be a non-empty release static asset path`, CONTEXT);
}
if (entry.cache_class !== undefined && typeof entry.cache_class !== "string") {
throw new LocalError(`${label}.cache_class must be a string`, CONTEXT);
}
replace[publicPath] =
entry.cache_class === undefined
? { asset: entry.asset }
: { asset: entry.asset, cache_class: entry.cache_class };
}
return { mode: "explicit", replace };
}
function mapAssets(assets, opts) {
assertPlainRecord(assets, "Deploy manifest assets");
assertKnownFields(assets, "Deploy manifest assets", MANIFEST_ASSETS_FIELDS);
const out = {};
if (assets.put !== undefined) {
if (!Array.isArray(assets.put)) {
throw new LocalError("Deploy manifest assets.put must be an array of put entries", CONTEXT);
}
const put = [];
for (let idx = 0; idx < assets.put.length; idx++) {
const rawEntry = assets.put[idx];
const label = `Deploy manifest assets.put[${idx}]`;
assertPlainRecord(rawEntry, label);
assertKnownFields(rawEntry, label, MANIFEST_ASSETS_PUT_ENTRY_FIELDS);
const entry = rawEntry;
if (typeof entry.key !== "string" || entry.key.length === 0) {
throw new LocalError(`${label}.key is required and must be a non-empty string`, CONTEXT);
}
const hasSource = entry.source !== undefined;
const hasSha = entry.sha256 !== undefined;
if (hasSource === hasSha) {
throw new LocalError(`${label} must include exactly one of \`source\` or \`sha256\``, CONTEXT);
}
if (hasSource) {
const source = fileEntryToContentSource(entry.source, opts, `${label}.source`);
const input = {
key: entry.key,
source,
};
if (entry.content_type !== undefined)
input.content_type = entry.content_type;
if (entry.visibility !== undefined)
input.visibility = entry.visibility;
if (entry.immutable !== undefined)
input.immutable = entry.immutable;
put.push(input);
}
else {
if (typeof entry.size_bytes !== "number") {
throw new LocalError(`${label}.size_bytes is required when using the wire form (sha256 set)`, CONTEXT);
}
// Wire shape — typed as AssetPutEntryInput but the SDK's
// normalizeAssetSlice accepts the wire form too (it discriminates
// on the presence of `source`).
const wire = {
key: entry.key,
sha256: entry.sha256,
size_bytes: entry.size_bytes,
content_type: entry.content_type,
visibility: entry.visibility,
immutable: entry.immutable,
};
put.push(wire);
}
}
out.put = put;
}
if (assets.delete !== undefined) {
if (!Array.isArray(assets.delete)) {
throw new LocalError("Deploy manifest assets.delete must be an array of keys", CONTEXT);
}
out.delete = [...assets.delete];
}
if (assets.sync !== undefined) {
assertPlainRecord(assets.sync, "Deploy manifest assets.sync");
assertKnownFields(assets.sync, "Deploy manifest assets.sync", MANIFEST_ASSETS_SYNC_FIELDS);
const rawSync = assets.sync;
if (typeof rawSync.prefix !== "string" || rawSync.prefix.length === 0) {
throw new LocalError("Deploy manifest assets.sync.prefix is required", CONTEXT);
}
if (rawSync.prune !== true) {
throw new LocalError("Deploy manifest assets.sync.prune must be `true`", CONTEXT);
}
const sync = {
prefix: rawSync.prefix,
prune: true,
};
if (rawSync.confirm !== undefined) {
assertPlainRecord(rawSync.confirm, "Deploy manifest assets.sync.confirm");
assertKnownFields(rawSync.confirm, "Deploy manifest assets.sync.confirm", MANIFEST_ASSETS_SYNC_CONFIRM_FIELDS);
const confirm = rawSync.confirm;
if (typeof confirm.base_revision !== "string" ||
typeof confirm.delete_set_digest !== "string" ||
typeof confirm.expected_delete_count !== "number") {
throw new LocalError("Deploy manifest assets.sync.confirm requires base_revision (string), delete_set_digest (string), expected_delete_count (number)", CONTEXT);
}
sync.confirm = {
base_revision: confirm.base_revision,
delete_set_digest: confirm.delete_set_digest,
expected_delete_count: confirm.expected_delete_count,
};
}
out.sync = sync;
}
return out;
}
function mapI18n(i18n) {
if (i18n === null)
return null;
assertPlainRecord(i18n, "Deploy manifest i18n");
assertKnownFields(i18n, "Deploy manifest i18n", MANIFEST_I18N_FIELDS, {
defaultLocale: "Use `default_locale` in CLI/manifest JSON.",
unknownLocalePolicy: "Use `unknown_locale_policy` in CLI/manifest JSON.",
default: "Use `default_locale` in i18n.",
locale: "Use `locales` (plural array) in i18n.",
});
const raw = i18n;
if (typeof raw.defaultLocale === "string") {
return {
defaultLocale: raw.defaultLocale,
locales: Array.isArray(raw.locales)
? [...raw.locales]
: raw.locales,
...(raw.detect !== undefined ? {
detect: Array.isArray(raw.detect)
? [...raw.detect]
: raw.detect,
} : {}),
...(raw.unknownLocalePolicy !== undefined ? {
unknownLocalePolicy: raw.unknownLocalePolicy,
} : {}),
};
}
const out = {
defaultLocale: raw.default_locale,
locales: Array.isArray(raw.locales)
? [...raw.locales]
: raw.locales,
};
if (raw.detect !== undefined) {
out.detect = Array.isArray(raw.detect)
? [...raw.detect]
: raw.detect;
}
if (raw.unknown_locale_policy !== undefined) {
out.unknownLocalePolicy = raw.unknown_locale_policy;
}
return out;
}
function mapRoutes(routes) {
if (routes === null)
return null;
assertPlainRecord(routes, "Deploy manifest routes");
assertKnownFields(routes, "Deploy manifest routes", MANIFEST_ROUTES_FIELDS, routeShapeHints(routes));
if (!Object.prototype.hasOwnProperty.call(routes, "replace")) {
throw new LocalError("Deploy manifest routes must be null or { \"replace\": [{ \"pattern\": \"/api/*\", \"target\": { \"type\": \"function\", \"name\": \"api\" } }, { \"pattern\": \"/events\", \"methods\": [\"GET\"], \"target\": { \"type\": \"static\", \"file\": \"events.html\" } }] }. Path-keyed route maps are not supported.", CONTEXT);
}
const replace = routes.replace;
if (!Array.isArray(replace)) {
throw new LocalError("Deploy manifest routes.replace must be an array", CONTEXT);
}
replace.forEach((route, index) => validateManifestRouteEntry(route, index));
return routes;
}
function routeShapeHints(obj) {
const hints = {};
for (const key of Object.keys(obj)) {
if (key.startsWith("/")) {
hints[key] = "Use routes.replace[] entries like { pattern, target: { type: \"function\", name } } or { pattern, methods: [\"GET\"], target: { type: \"static\", file } } instead of a path-keyed route map.";
}
}
return hints;
}
function validateManifestRouteEntry(route, index) {
const label = `Deploy manifest routes.replace[${index}]`;
assertPlainRecord(route, label);
assertKnownFields(route, label, MANIFEST_ROUTE_ENTRY_FIELDS);
if (typeof route.pattern !== "string" || route.pattern.length === 0) {
throw new LocalError(`${label}.pattern must be a non-empty string`, CONTEXT);
}
if (route.methods !== undefined) {
if (!Array.isArray(route.methods)) {
throw new LocalError(`${label}.methods must be an array of HTTP methods`, CONTEXT);
}
if (route.methods.length === 0) {
throw new LocalError(`${label}.methods must not be empty; omit methods to allow all supported methods`, CONTEXT);
}
for (const method of route.methods) {
if (typeof method !== "string" || !ROUTE_METHOD_SET.has(method)) {
throw new LocalError(`${label}.methods contains unsupported method ${JSON.stringify(method)}. Supported methods: ${ROUTE_HTTP_METHODS.join(", ")}`, CONTEXT);
}
}
const seen = new Set();
for (const method of route.methods) {
if (seen.has(method)) {
throw new LocalError(`${label}.methods contains duplicate method ${JSON.stringify(method)}`, CONTEXT);
}
seen.add(method);
}
}
const targetType = validateManifestRouteTarget(route.target, `${label}.target`);
if (targetType === "static")
validateManifestStaticRouteEntry(route, label);
}
function validateManifestRouteTarget(target, label) {
assertPlainRecord(target, label);
if ((Object.prototype.hasOwnProperty.call(target, "function") ||
Object.prototype.hasOwnProperty.call(target, "static")) &&
!Object.prototype.hasOwnProperty.call(target, "type")) {
throw new LocalError(`${label} uses unsupported target shorthand. Use { "type": "function", "name": "api" } or { "type": "static", "file": "events.html" }.`, CONTEXT);
}
if (target.type === undefined) {
throw new LocalError(`${label}.type is required; use "function" or "static"`, CONTEXT);
}
if (target.type === "function") {
assertKnownFields(target, label, MANIFEST_FUNCTION_ROUTE_TARGET_FIELDS);
if (typeof target.name !== "string" || target.name.length === 0) {
throw new LocalError(`${label}.name is required for function route targets`, CONTEXT);
}
return "function";
}
if (target.type === "static") {
assertKnownFields(target, label, MANIFEST_STATIC_ROUTE_TARGET_FIELDS);
if (typeof target.file !== "string" || target.file.length === 0) {
throw new LocalError(`${label}.file is required for static route targets`, CONTEXT);
}
validateManifestStaticTargetFile(target.file, `${label}.file`);
return "static";
}
throw new LocalError(`${label}.type must be "function" or "static"; got ${JSON.stringify(target.type)}`, CONTEXT);
}
function validateManifestStaticRouteEntry(route, label) {
const pattern = route.pattern;
if (pattern.includes("*")) {
throw new LocalError(`${label}.pattern uses a static route target, so it must be an exact path pattern; wildcard static route targets such as /docs/* are not supported`, CONTEXT);
}
if (route.methods === undefined) {
throw new LocalError(`${label}.methods is required for static route targets; use ["GET"] or ["GET", "HEAD"]`, CONTEXT);
}
const methods = route.methods;
const methodSet = new Set(methods);
const valid = methodSet.has("GET") &&
(methodSet.size === 1 || (methodSet.size === 2 && methodSet.has("HEAD")));
if (!valid) {
throw new LocalError(`${label}.methods for static route targets must be ["GET"] or ["GET", "HEAD"]; either form materializes effective GET plus HEAD`, CONTEXT);
}
}
function validateManifestStaticTargetFile(file, label) {
const invalid = file.startsWith("/") ||
file.includes("?") ||
file.includes("#") ||
file.includes("\\") ||
file.endsWith("/") ||
file.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
if (invalid) {
throw new LocalError(`${label} must be a relative materialized static-site file path without leading slash, query, fragment, traversal, empty segments, backslashes, or directory shorthand`, CONTEXT);
}
}
function mapFileSet(map, opts) {
assertPlainRecord(map, "Deploy manifest file map");
const out = {};
for (const [path, entry] of Object.entries(map)) {
out[path] = fileEntryToContentSource(entry, opts, path);
}
return out;
}
function fileEntryToContentSource(entry, opts, label) {
if (typeof entry === "string")
return entry;
if (entry instanceof Uint8Array)
return entry;
if (entry instanceof ArrayBuffer)
return entry;
if (typeof Blob !== "undefined" && entry instanceof Blob)
return entry;
if (isReadableStream(entry))
return entry;
if (isContentRef(entry))
return contentRefFromManifest(entry);
if (!isRecord(entry) || Array.isArray(entry)) {
return entry;
}
const rec = entry;
const encoding = rec.encoding;
if (encoding !== undefined &&
encoding !== "utf-8" &&
encoding !== "base64") {
throw new LocalError(`Unsupported encoding for ${label}: ${String(encoding)}`, CONTEXT);
}
const contentType = contentTypeFromManifestRecord(rec, label);
if (typeof rec.path === "string" &&
!Object.prototype.hasOwnProperty.call(rec, "data")) {
const source = {
__source: "fs-file",
path: resolveLocalPath(rec.path, opts.baseDir),
};
return contentType
? { ...source, contentType }
: source;
}
if (encoding === "base64") {
if (typeof rec.data !== "string") {
throw new LocalError(`Base64 file entry for ${label} must use a string data field`, CONTEXT);
}
const bytes = base64ToBytes(rec.data);
return contentType
? { data: bytes, contentType }
: bytes;
}
if (Object.prototype.hasOwnProperty.call(rec, "data")) {
if (rec.data === undefined) {
throw new LocalError(`File entry for ${label} is missing data`, CONTEXT);
}
const data = rec.data;
return contentType
? { data, contentType }
: data;
}
return entry;
}
function contentTypeFromManifestRecord(rec, label) {
// A manifest is a wire document: one spelling, `content_type`. `contentType`
// is the in-function JS option name only (first-deploy-agent-dx).
if (rec.contentType !== undefined) {
throw new LocalError(`${label}.contentType is not a manifest field — spell it content_type (wire fields are snake_case; contentType is only the in-function JS option)`, CONTEXT);
}
const snake = rec.content_type;
if (snake !== undefined && typeof snake !== "string") {
throw new LocalError(`${label}.content_type must be a string`, CONTEXT);
}
return typeof snake === "string" ? snake : undefined;
}
function base64ToBytes(value) {
const buf = Buffer.from(value, "base64");
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
}
function resolveLocalPath(path, baseDir = process.cwd()) {
return isAbsolute(path) ? path : resolvePath(baseDir, path);
}
function isContentRef(value) {
return (isRecord(value) &&
typeof value.sha256 === "string" &&
typeof value.size === "number");
}
function contentRefFromManifest(value) {
const wire = value;
return {
sha256: value.sha256,
size: value.size,
...(value.contentType ?? wire.content_type ? { contentType: value.contentType ?? wire.content_type } : {}),
...(value.integrity ? { integrity: value.integrity } : {}),
};
}
function isReadableStream(value) {
return (typeof ReadableStream !== "undefined" &&
value instanceof ReadableStream);
}
function isRecord(value) {
return value !== null && typeof value === "object";
}
function assertPlainRecord(value, label) {
if (!isRecord(value) || Array.isArray(value)) {
throw new LocalError(`${label} must be a JSON object`, CONTEXT);
}
}
function assertKnownFields(value, label, allowed, hints = {}) {
for (const key of Object.keys(value)) {
if (allowed.has(key))
continue;
const hint = hints[key] ? ` ${hints[key]}` : "";
throw new LocalError(`Unknown ${label} field: ${key}.${hint}`, CONTEXT);
}
}
/** Collect authoring paths before eager SQL reads so every missing input is reported together. */
export function collectAuthoringFileReferences(input, baseDir) {
const refs = [];
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
const add = (value, field, kind) => {
const entry = record(value);
if (typeof entry.path === "string" && entry.data === undefined)
refs.push({ field_path: field, path: resolvePath(baseDir, entry.path), kind, expects: kind === "site_dir" ? "directory" : "file" });
};
const files = (value, field, kind) => {
if (isLocalDirRef(value)) {
add(value, field, "site_dir");
return;
}
for (const [name, entry] of Object.entries(record(value)))
add(entry, `${field}[${JSON.stringify(name)}]`, kind);
};
const raw = record(input);
const fns = record(raw.functions);
for (const [field, bucket] of [["functions.replace", fns.replace], ["functions.patch.set", record(fns.patch).set]]) {
for (const [name, fn] of Object.entries(record(bucket))) {
add(record(fn).source, `${field}.${name}.source`, "function_source");
files(record(fn).files, `${field}.${name}.files`, "function_source");
}
}
files(record(raw.site).replace, "site.replace", "site_file");
files(record(record(raw.site).patch).put, "site.patch.put", "site_file");
const puts = record(raw.assets).put;
if (Array.isArray(puts))
puts.forEach((entry, index) => add(record(entry).source, `assets.put[${index}].source`, "asset"));
const migrations = record(raw.database).migrations;
if (Array.isArray(migrations))
migrations.forEach((entry, index) => {
const migration = record(entry);
if (migration.sql !== undefined)
return;
const field = migration.sql_path !== undefined ? "sql_path" : "sql_file";
const path = migration[field];
if (typeof path === "string")
refs.push({ field_path: `database.migrations[${index}].${field}`, path: resolvePath(baseDir, path), kind: "migration_sql", expects: "file" });
});
return refs;
}
async function assertAuthoringFileReferences(input, baseDir, manifestPath) {
const missing = [];
for (const { expects, ...ref } of collectAuthoringFileReferences(input, baseDir)) {
try {
const info = await stat(ref.path);
if (expects === "file" ? info.isFile() : info.isDirectory())
continue;
}
catch { /* The complete missing list is the recovery contract. */ }
missing.push(ref);
}
if (missing.length)
throw manifestFileMissingError(missing.sort((a, b) => Number(b.kind === "migration_sql") - Number(a.kind === "migration_sql")), { manifestPath });
}
/** tenant-site-embedding. Shape only; the gateway owns the catalog and names
* the valid keys in its INVALID_SPEC, so a new key never needs a client release. */
function mapSiteEmbedding(value) {
if (value === null)
return null;
assertPlainRecord(value, "Deploy manifest site.embedding");
assertKnownFields(value, "Deploy manifest site.embedding", new Set(["frame_ancestors"]));
const keys = value.frame_ancestors;
if (!Array.isArray(keys) || keys.length === 0 || keys.some((k) => typeof k !== "string")) {
throw new LocalError('Deploy manifest site.embedding.frame_ancestors must be a non-empty array of embedding catalog keys (e.g. ["localhost"]); omit site.embedding to carry the previous declaration forward or set it to null to deny framing', CONTEXT);
}
return { frame_ancestors: keys };
}
/** site.sign_in_path. Shape only (string or null); the SDK's spec validation
* and the gateway check the path itself. */
function mapSiteSignInPath(value) {
if (value === null)
return null;
if (typeof value !== "string") {
throw new LocalError('Deploy manifest site.sign_in_path must be a same-origin path string (e.g. "/join") or null; omit it to carry the previous value forward or set it to null to use the hosted sign-in page', CONTEXT);
}
return value;
}
/** site.noindex. true, false, or null; anything else is refused here. */
function mapSiteNoindex(value) {
if (value === null || typeof value === "boolean")
return value;
throw new LocalError("Deploy manifest site.noindex must be true, false, or null; set true to add X-Robots-Tag: noindex, nofollow, nosnippet to every response of the host, false or null to make it indexable again, or omit it to carry the previous value forward", CONTEXT);
}
//# sourceMappingURL=deploy-manifest.js.map