UNPKG

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
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