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.

4,470 lines • 197 kB
/**
 * `deploy` namespace — the canonical unified deploy primitive.
 *
 * Three layers exposed:
 *   - `apply(spec, opts?)`  — one-shot, awaits to completion or terminal failure.
 *   - `start(spec, opts?)`  — returns a `DeployOperation` with `events()` + `result()`.
 *   - `plan` / `upload` / `commit` — low-level steps for CLI and tests.
 *
 * All bytes ride through the CAS content service via presigned PUTs to S3.
 * The wire body to `POST /apply/v1/plans` carries `ContentRef` objects only —
 * never inline file bytes. When the normalized spec exceeds 5 MB JSON, the
 * SDK uploads the manifest itself as a CAS object and references it.
 *
 * Idempotency is keyed on the gateway-computed manifest digest, not the
 * SDK's local digest. The SDK does not canonicalize for correctness — the
 * gateway is authoritative.
 *
 * See `unified-deploy` and `cas-content` capability specs for normative
 * behavior; this file is the implementation.
 */
import { normalizeEdgeEvidence } from "./edge-evidence.js";
import { isCiSessionCredentials } from "../ci-credentials.js";
import { isGrantKeyCredentials } from "../grant-key-credentials.js";
import { isKnownBinaryContent, isUtf8StringSource } from "../validation.js";
import { assertCiDeployableSpec } from "./ci.js";
import { ROUTE_PRICING_NETWORKS, ROUTE_HTTP_METHODS, isModernPlanMigrationDiff, normalizeDeployResolveRequest, } from "./deploy.types.js";
import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isTransferFreezeError, } from "../errors.js";
import { assertAssetMetadata, assertExifPolicy, } from "./assets-validation.js";
// ─── Constants ───────────────────────────────────────────────────────────────
const PLAN_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
const COMMIT_POLL_INITIAL_MS = 1_000;
const COMMIT_POLL_MAX_MS = 30_000;
const COMMIT_POLL_BACKOFF_AFTER_MS = 30_000;
const COMMIT_POLL_TIMEOUT_MS = 10 * 60 * 1000;
const APPLY_RETRY_DEFAULT_MAX_RETRIES = 2;
const APPLY_RETRY_BASE_DELAY_MS = 250;
const APPLY_RETRY_MAX_DELAY_MS = 2_000;
const APPLY_RETRY_JITTER_MS = 100;
const URL_REFRESH_AT_MS = 50 * 60 * 1000;
const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
const APPLY_SAFE_RETRY_CODES = new Set([
    "BASE_RELEASE_CONFLICT",
]);
const EMAIL_TRIGGER_EVENTS = new Set(["reply_received", "delivery", "bounced", "complained", "mailbox_suspended"]);
const STATIC_ACTIVATION_FAILURE_CODES = new Set([
    "BAD_FIELD",
    "INVALID_SPEC",
    "FUNCTION_ACTIVATE_FAILED",
    "FUNCTION_CONFIG_INVALID",
    "FUNCTION_TIMEOUT_EXCEEDS_TIER",
    "FUNCTION_MEMORY_EXCEEDS_TIER",
    "FUNCTION_SCHEDULE_EXCEEDS_TIER",
    "FUNCTION_SCHEDULE_INTERVAL_TOO_SHORT",
    "FUNCTION_SCHEDULE_LIMIT_EXCEEDED",
    "TIER_LIMIT_EXCEEDED",
]);
const STATIC_FUNCTION_LIMITS_BY_TIER = {
    prototype: {
        maxTimeoutSeconds: 10,
        maxMemoryMb: 128,
        maxScheduledFunctions: 1,
        minCronIntervalMinutes: 15,
    },
    hobby: {
        maxTimeoutSeconds: 30,
        maxMemoryMb: 256,
        maxScheduledFunctions: 3,
        minCronIntervalMinutes: 5,
    },
    team: {
        maxTimeoutSeconds: 60,
        maxMemoryMb: 512,
        maxScheduledFunctions: 10,
        minCronIntervalMinutes: 1,
    },
};
const MANIFEST_CONTENT_TYPE = "application/vnd.run402.deploy-manifest+json";
const TERMINAL_STATUSES = [
    "ready",
    "failed",
    "rolled_back",
    "needs_repair",
];
const SUCCESS_STATUS = "ready";
// ─── Public class ────────────────────────────────────────────────────────────
export class Deploy {
    client;
    constructor(client) {
        this.client = client;
    }
    /**
     * One-shot deploy. Normalizes byte sources, plans, uploads missing
     * content, commits, and polls until terminal. Throws
     * {@link Run402DeployError} on any state-machine failure.
     */
    async apply(spec, opts = {}) {
        // A vault-bound apply never auto-retries: each attempt plans a NEW
        // operation, and an activation token is minted for exactly one. Retrying
        // would either present a token bound elsewhere (the platform refuses, as
        // it should) or demand a fresh capture, which is the caller's deliberate
        // re-run — not something the client does behind their back.
        const maxRetries = opts.vault ? 0 : normalizeApplyMaxRetries(opts.maxRetries);
        const maxAttempts = maxRetries + 1;
        const emit = makeEmitter(opts.onEvent);
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return await applyOnce(this.client, spec, opts, emit);
            }
            catch (err) {
                if (!(err instanceof Run402DeployError))
                    throw err;
                const safeToRetry = isSafeDeployApplyRetry(err, spec);
                if (!safeToRetry)
                    throw err;
                if (attempt === maxAttempts) {
                    if (maxRetries > 0) {
                        throw withDeployRetryMetadata(err, attempt, maxRetries, err.code);
                    }
                    throw err;
                }
                const delayMs = deployApplyRetryDelayMs(attempt);
                emit({
                    type: "deploy.retry",
                    attempt,
                    nextAttempt: attempt + 1,
                    maxAttempts,
                    delayMs,
                    code: err.code,
                    phase: err.phase,
                    resource: err.resource,
                    operationId: err.operationId,
                    planId: err.planId,
                    message: err.message,
                });
                await sleep(delayMs);
            }
        }
        throw new Run402DeployError("Deploy retry loop ended without a result", {
            code: "INTERNAL_ERROR",
            phase: "apply",
            retryable: false,
            context: "applying deploy",
        });
    }
    /**
     * Start a resumable deploy operation. Returns an object exposing the
     * operation id, an event async-iterable, and a result promise.
     */
    start(spec, opts = {}) {
        return startInternal(this.client, spec, opts);
    }
    /**
     * Low-level plan: normalize the spec, upload the manifest as CAS if over
     * the inline limit, and call `POST /apply/v1/plans`. Returns the plan
     * response and a byte-reader map keyed by sha256 (used by `upload`).
     */
    async plan(spec, opts = {}) {
        return planInternal(this.client, spec, opts.idempotencyKey, {
            dryRun: opts.dryRun ?? opts.mode === "legacyDryRun",
            reviewedPlan: opts.mode === "reviewedPlan",
            requiredPlan: opts.requiredPlan,
            ...(opts.vault ? { vault: opts.vault } : {}),
        });
    }
    /**
     * Low-level upload: ensure every ref the gateway reported as missing for
     * this project has bytes in CAS. Issues a content-plan, PUTs bytes to
     * the returned presigned URLs, and finalizes the content plan. Caller
     * passes the project id so the apikey-gated CAS routes can authenticate.
     */
    async upload(plan, opts) {
        const emit = makeEmitter(opts.onEvent);
        await uploadMissing(this.client, opts.project, plan.missing_content, opts.byteReaders, emit);
    }
    /**
     * Low-level commit: `POST /apply/v1/plans/:plan_id/commit`, then poll
     * `/operations/:id` until terminal. Pass the project id whose anon_key
     * should authenticate the polling — the operations endpoint requires
     * apikey auth even though the plan/commit endpoints accept SIWX.
     */
    async commit(planId, opts = {}) {
        const emit = makeEmitter(opts.onEvent);
        const commit = requireCloudCommitResponse(await commitInternal(this.client, planId, opts.idempotencyKey, opts.project, opts.requiredPlan, opts.vault), "committing deploy");
        return await pollUntilReady(this.client, commit, {}, [], emit, opts.project);
    }
    async rehearse(planId, opts = {}) {
        if (!planId || typeof planId !== "string") {
            throw new Run402DeployError(`Invalid plan id: "${String(planId)}"`, {
                code: "BAD_REQUEST",
                phase: "rehearsal",
                retryable: false,
                context: "rehearsing deploy plan",
            });
        }
        const body = {};
        if (opts.teardown !== undefined)
            body.teardown = opts.teardown;
        try {
            return await this.client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/rehearse`, {
                method: "POST",
                body,
                ...(opts.project
                    ? {
                        authMeta: {
                            method: "deploy.rehearse",
                            capability: "project.deploy",
                            target: { project_id: opts.project },
                        },
                    }
                    : {}),
                context: "rehearsing deploy plan",
            });
        }
        catch (err) {
            throw translateDeployError(err, "rehearsal", planId, null);
        }
    }
    /**
     * Resume an operation in `schema_settling` or `activation_pending`. The
     * gateway re-runs only the failed phase forward — never replays SQL.
     * Returns the resulting snapshot, polling until terminal. The resume
     * endpoint accepts wallet (SIWX) auth; the polling that follows requires
     * the project's apikey, so pass `project` to enable polling. (Without
     * `project`, this method returns once the gateway accepts the resume
     * request — successful resumes typically reach `ready` synchronously
     * via the auto-resume worker.)
     */
    async resume(operationId, opts = {}) {
        if (!operationId || !operationId.startsWith("op_")) {
            throw new Run402DeployError(`Invalid operation id: "${operationId}"`, {
                code: "OPERATION_NOT_FOUND",
                retryable: false,
                context: "resuming deploy operation",
            });
        }
        const emit = makeEmitter(opts.onEvent);
        let snapshot;
        try {
            snapshot = await this.client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}/resume`, { method: "POST", context: "resuming deploy operation" });
        }
        catch (err) {
            throw translateDeployError(err, "resume", null, operationId);
        }
        return await pollSnapshotUntilReady(this.client, snapshot, {}, [], emit, opts.project);
    }
    /**
     * Promote an existing release to be the project's current live release —
     * a pointer swap on `internal.projects.live_release_id` without re-running
     * the apply pipeline. Designed for recovery from a destructive
     * apply ("oops on a real project ID"). The prior release's bytes,
     * functions, and migrations remain persisted; this just routes traffic
     * back to them.
     *
     * Surfaces structured warnings via the result envelope:
     *
     *   - `MIGRATIONS_NOT_REVERSIBLE` (requires_confirmation: true) when the
     *     target release predates migrations applied since. The migrations
     *     remain applied; the new live release runs against the current
     *     schema. Ack via `opts.allowWarningCodes`.
     *
     *   - `FUNCTION_VERSION_MISMATCH` (informational) when overlapping
     *     function names have different code_hashes. The Lambda code is
     *     whatever's currently $LATEST.
     *
     * Rejected cases:
     *   - `PROMOTE_TARGET_NOT_FOUND` — releaseId doesn't exist
     *   - `PROMOTE_PROJECT_MISMATCH` — releaseId belongs to a different project
     *   - `PROMOTE_RELEASE_NOT_READY` — release status isn't promotable
     *   - `PROMOTE_NO_OP` — releaseId IS already the project's current live
     *   - `PROMOTE_WARNING_REQUIRES_ACK` — at least one blocking warning unacked
     *
     * Capability: unified-deploy (v1.58+, release-promote).
     */
    async promote(project, releaseId, opts = {}) {
        if (!project || typeof project !== "string") {
            throw new Run402DeployError(`Invalid project id: "${String(project)}"`, {
                code: "BAD_REQUEST",
                retryable: false,
                context: "promoting release",
            });
        }
        if (!releaseId || !releaseId.startsWith("rel_")) {
            throw new Run402DeployError(`Invalid release id: "${releaseId}"`, {
                code: "BAD_REQUEST",
                retryable: false,
                context: "promoting release",
            });
        }
        // Note: `allowWarnings: true` is implemented client-side by enumerating
        // every known blocking warning code, since the gateway expects a precise
        // list per warning code (no wildcard accept). v1.58 has exactly one
        // blocking promote warning (MIGRATIONS_NOT_REVERSIBLE); if more land,
        // expand this list.
        const ALL_BLOCKING_PROMOTE_WARNINGS = ["MIGRATIONS_NOT_REVERSIBLE"];
        const allowCodes = opts.allowWarnings === true
            ? ALL_BLOCKING_PROMOTE_WARNINGS
            : (opts.allowWarningCodes ?? []);
        try {
            return await this.client.request(`/apply/v1/releases/${encodeURIComponent(releaseId)}/promote`, {
                method: "POST",
                context: "promoting release",
                headers: { "content-type": "application/json" },
                body: {
                    project_id: project,
                    allow_warning_codes: allowCodes,
                },
            });
        }
        catch (err) {
            throw translateDeployError(err, "promote", null, releaseId);
        }
    }
    /**
     * Snapshot a deploy operation. The endpoint requires `apikey` auth, so
     * pass the project that owns the operation. (When omitted, the request
     * is sent without an apikey header and the gateway will return 401.)
     */
    async status(operationId, opts = {}) {
        if (!operationId || !operationId.startsWith("op_")) {
            throw new Run402DeployError(`Invalid operation id: "${operationId}"`, {
                code: "OPERATION_NOT_FOUND",
                retryable: false,
                context: "fetching deploy operation",
            });
        }
        const headers = opts.project ? await apikeyHeaders(this.client, opts.project) : {};
        try {
            return await this.client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}`, { headers, context: "fetching deploy operation" });
        }
        catch (err) {
            throw translateDeployError(err, "status", null, operationId);
        }
    }
    /**
     * List recent deploy operations for a project. The endpoint requires
     * `apikey` auth, so a project id is required — accepted as a bare string
     * (matches `r.functions.list(projectId)` and friends) or as `{ project,
     * limit?, cursor? }`. `limit` and `cursor` are forwarded to the gateway
     * as query strings when set; the gateway picks a default page otherwise.
     */
    async list(opts) {
        const project = typeof opts === "string" ? opts : opts?.project;
        const limit = typeof opts === "string" ? undefined : opts?.limit;
        const cursor = typeof opts === "string" ? undefined : opts?.cursor;
        if (!project) {
            throw new LocalError("apply.list requires a project id (as a string or { project: 'prj_...' })", "listing deploy operations");
        }
        const normalizedLimit = normalizePositiveSafeIntegerQueryOption(limit, "apply.list limit", "listing deploy operations");
        const headers = await apikeyHeaders(this.client, project);
        const qs = new URLSearchParams();
        if (normalizedLimit !== undefined)
            qs.set("limit", String(normalizedLimit));
        if (cursor !== undefined)
            qs.set("cursor", cursor);
        const path = qs.toString().length > 0
            ? `/apply/v1/operations?${qs.toString()}`
            : `/apply/v1/operations`;
        return this.client.request(path, {
            headers,
            context: "listing deploy operations",
        });
    }
    /**
     * Fetch the synthesized phase event stream for an operation. Returns the
     * events the gateway has recorded so far — useful for inspecting a deploy
     * after the fact, or resuming an event subscription from a different
     * process. For live subscription during an in-flight deploy, use
     * {@link Deploy.start} and iterate `op.events()`.
     *
     * The endpoint requires `apikey` auth, so `project` is required.
     */
    async events(operationId, opts) {
        if (!operationId || !operationId.startsWith("op_")) {
            throw new Run402DeployError(`Invalid operation id: "${operationId}"`, {
                code: "OPERATION_NOT_FOUND",
                retryable: false,
                context: "fetching deploy events",
            });
        }
        const headers = await apikeyHeaders(this.client, opts.project);
        try {
            return await this.client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}/events`, { headers, context: "fetching deploy events" });
        }
        catch (err) {
            throw translateDeployError(err, "events", null, operationId);
        }
    }
    /**
     * Probe whether the gateway and edge pointers agree on the active release
     * for an operation. The endpoint requires `apikey` auth, so `project` is
     * required. Use `waitEdgeCoherent` when you want bounded polling.
     */
    async edgeCoherence(operationId, opts) {
        if (!operationId || !operationId.startsWith("op_")) {
            throw new Run402DeployError(`Invalid operation id: "${operationId}"`, {
                code: "OPERATION_NOT_FOUND",
                retryable: false,
                context: "verifying deploy edge coherence",
            });
        }
        if (!opts?.project) {
            throw new LocalError("apply.edgeCoherence requires a project id ({ project: 'prj_...' })", "verifying deploy edge coherence");
        }
        const headers = await apikeyHeaders(this.client, opts.project);
        try {
            return normalizeEdgeEvidence(await this.client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}/edge-coherence`, { headers, context: "verifying deploy edge coherence" }));
        }
        catch (err) {
            throw translateDeployError(err, "edge-coherence", null, operationId);
        }
    }
    /**
     * Poll `edgeCoherence` until the report is coherent or the timeout elapses.
     * Returns the last report either way so automation can explain stale paths
     * without issuing a second request.
     */
    async waitEdgeCoherent(operationId, opts) {
        const timeoutMs = normalizePositiveSafeIntegerQueryOption(opts?.timeoutMs ?? 60_000, "apply.waitEdgeCoherent timeoutMs", "waiting for deploy edge coherence") ?? 60_000;
        const intervalMs = normalizePositiveSafeIntegerQueryOption(opts?.intervalMs ?? 1_000, "apply.waitEdgeCoherent intervalMs", "waiting for deploy edge coherence") ?? 1_000;
        if (!opts?.project) {
            throw new LocalError("apply.waitEdgeCoherent requires a project id ({ project: 'prj_...' })", "waiting for deploy edge coherence");
        }
        const start = Date.now();
        let attempts = 0;
        while (true) {
            attempts += 1;
            const report = await this.edgeCoherence(operationId, { project: opts.project });
            const elapsedMs = Date.now() - start;
            opts.onPoll?.({ attempts, elapsedMs, report });
            if (report.coherent) {
                return { coherent: true, attempts, elapsedMs, report };
            }
            if (elapsedMs >= timeoutMs) {
                return { coherent: false, attempts, elapsedMs, report };
            }
            await sleep(Math.min(intervalMs, Math.max(timeoutMs - elapsedMs, 0)));
        }
    }
    /**
     * Fetch a release inventory by id. The endpoint requires `apikey` auth, so
     * pass the owning project id. `siteLimit` controls how many site paths the
     * gateway includes before reporting `site.totals.paths`.
     */
    async getRelease(opts) {
        if (!opts?.project) {
            throw new LocalError("apply.getRelease requires a project id ({ project: 'prj_...', releaseId: 'rel_...' })", "fetching release inventory");
        }
        if (!opts.releaseId) {
            throw new LocalError("apply.getRelease requires a release id", "fetching release inventory");
        }
        const siteLimit = normalizePositiveSafeIntegerQueryOption(opts.siteLimit, "apply.getRelease siteLimit", "fetching release inventory");
        const headers = await apikeyHeaders(this.client, opts.project);
        return this.client.request(appendQuery(`/apply/v1/releases/${encodeURIComponent(opts.releaseId)}`, {
            site_limit: siteLimit,
            static_continuity_plan_id: opts.staticContinuityPlanId,
            static_continuity_cursor: opts.staticContinuityCursor,
        }), { headers, context: "fetching release inventory" });
    }
    /**
     * Fetch the currently active release inventory for a project. If the project
     * has not activated a release yet, the gateway returns an empty current-live
     * inventory with `release_id: null`.
     */
    async getActiveRelease(opts) {
        if (!opts?.project) {
            throw new LocalError("apply.getActiveRelease requires a project id ({ project: 'prj_...' })", "fetching active release inventory");
        }
        const siteLimit = normalizePositiveSafeIntegerQueryOption(opts.siteLimit, "apply.getActiveRelease siteLimit", "fetching active release inventory");
        const headers = await apikeyHeaders(this.client, opts.project);
        return this.client.request(appendQuery("/apply/v1/releases/active", {
            site_limit: siteLimit,
        }), { headers, context: "fetching active release inventory" });
    }
    /**
     * Diff two materialized release targets for a project. `from` may be
     * `"empty"`, `"active"`, or a release id. `to` may be `"active"` or a
     * release id; the gateway treats `"active"` as the current-live target.
     */
    async diff(opts) {
        if (!opts?.project) {
            throw new LocalError("apply.diff requires a project id ({ project: 'prj_...', from, to })", "diffing releases");
        }
        const from = requireNonEmptyStringQueryOption(opts.from, "apply.diff from", "diffing releases");
        const to = requireNonEmptyStringQueryOption(opts.to, "apply.diff to", "diffing releases");
        const limit = normalizePositiveSafeIntegerQueryOption(opts.limit, "apply.diff limit", "diffing releases");
        const headers = await apikeyHeaders(this.client, opts.project);
        const qs = new URLSearchParams({ from, to });
        if (limit !== undefined)
            qs.set("limit", String(limit));
        return this.client.request(`/apply/v1/releases/diff?${qs.toString()}`, { headers, context: "diffing releases" });
    }
    /**
     * Diagnose how a stable public URL or host/path would resolve against the
     * current live release. This is an authenticated read: `project` is used
     * only for local apikey lookup and is not sent to the gateway.
     */
    async resolve(opts) {
        const request = normalizeDeployResolveRequest(opts);
        const headers = await apikeyHeaders(this.client, request.project);
        const qs = new URLSearchParams({ host: request.host });
        if ("url" in opts || opts.path !== undefined)
            qs.set("path", request.path);
        if (request.method)
            qs.set("method", request.method);
        return this.client.request(`/apply/v1/resolve?${qs.toString()}`, { headers, context: "resolving deploy public URL" });
    }
}
function appendQuery(path, params) {
    const qs = new URLSearchParams();
    for (const [key, value] of Object.entries(params)) {
        if (value !== undefined)
            qs.set(key, String(value));
    }
    const query = qs.toString();
    return query.length > 0 ? `${path}?${query}` : path;
}
function normalizePositiveSafeIntegerQueryOption(value, label, context) {
    if (value === undefined)
        return undefined;
    if (!Number.isSafeInteger(value) || value <= 0) {
        throw new LocalError(`${label} must be a positive safe integer`, context);
    }
    return value;
}
function requireNonEmptyStringQueryOption(value, label, context) {
    if (typeof value !== "string" || value.length === 0) {
        throw new LocalError(`${label} must be a non-empty string`, context);
    }
    return value;
}
// ─── Internal pipeline ───────────────────────────────────────────────────────
/**
 * Compute the sorted set of slice kinds the spec carried. Surfaces on
 * `commit.phase` and `ready` events so agents can group per-phase
 * telemetry by slice category. `assets` slice → `"asset"`; any of
 * `database` / `functions` / `site` → `"release"`. Order is stable
 * (release before asset).
 */
function deriveSliceKinds(spec) {
    // Guard against non-object spec — the validate phase throws below
    // (INVALID_SPEC), but this is called before validation in applyOnce so
    // we must not blow up first.
    if (!spec || typeof spec !== "object")
        return [];
    const set = new Set();
    if (spec.database || spec.functions || spec.site)
        set.add("release");
    if (spec.assets)
        set.add("asset");
    return [...set].sort((a, b) => (a === "release" ? -1 : 1));
}
/**
 * Decide and (when warranted) run the rehearsal for a cloud apply. Returns
 * the `DeployResult.rehearsal` block plus, after a passed rehearsal, the
 * bound `requiredPlan` the commit must carry. Throws `REHEARSAL_FAILED`
 * (with the report attached) when the rehearsal fails — nothing has been
 * committed at that point.
 */
async function rehearseBeforeCommit(client, plan, planId, project, opts, emit) {
    const skip = (reason) => {
        emit({ type: "rehearsal.skipped", reason });
        return { block: { status: "skipped", reason } };
    };
    if (opts.noRehearse)
        return skip("disabled");
    if (opts.requiredPlan)
        return skip("reviewed_plan");
    const envelope = plan.rehearsal;
    if (!envelope || !envelope.available) {
        const reason = envelope?.reason ?? "no_migrations";
        return skip(reason === "no_live_release"
            ? "no_live_release"
            : reason === "migrations_unchanged"
                ? "migrations_unchanged"
                : "no_migrations");
    }
    // Named migrations that are all checksum-identical noops (already applied
    // to the live project) would run NOTHING on the branch, so a rehearsal
    // proves nothing and only costs time. Newer gateways answer
    // `reason: "migrations_unchanged"` themselves (handled above); derive the
    // same conclusion from the plan's migration buckets for older ones. A
    // same-id/different-checksum migration is a hard 422 before any 2xx plan,
    // so `new.length === 0` means exactly "every declared migration is
    // already applied". A plan without modern buckets is rehearsed as before.
    const unchanged = migrationsUnchangedReason(plan);
    if (unchanged)
        return skip(unchanged);
    emit({ type: "rehearsal.started", planId });
    let rehearsed;
    try {
        rehearsed = await client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/rehearse`, {
            method: "POST",
            body: {},
            authMeta: { method: "deploy.rehearse", capability: "project.deploy", target: { project_id: project } },
            context: "rehearsing deploy plan",
        });
    }
    catch (err) {
        throw translateDeployError(err, "rehearsal", planId, null);
    }
    const report = rehearsed.report;
    emit({
        type: "rehearsal.finished",
        planId,
        status: report.status,
        operationId: report.operation_id,
        branchProjectId: report.branch_project_id,
        durationMs: report.duration_ms,
    });
    if (report.status !== "passed") {
        throw new Run402DeployError(`Rehearsal failed on a contained branch; nothing was committed. ${describeRehearsalFailure(report)}`, {
            code: "REHEARSAL_FAILED",
            phase: "rehearsal",
            retryable: false,
            context: "rehearsing deploy plan",
            planId,
            operationId: report.operation_id,
            body: { rehearsal: report, next_actions: report.next_actions },
        });
    }
    const bound = report.next_actions.find((a) => a.type === "commit_plan")?.body?.required_plan;
    return {
        block: {
            status: "passed",
            report,
            operation_id: report.operation_id,
            branch_project_id: report.branch_project_id,
        },
        ...(bound ? { requiredPlan: { planId: bound.plan_id, planFingerprint: bound.plan_fingerprint } } : {}),
    };
}
/**
 * `"migrations_unchanged"` when the plan carries modern migration buckets and
 * none are new (all noop); `"no_migrations"` when the buckets are both empty;
 * `null` when there is something to rehearse or the buckets are absent.
 */
function migrationsUnchangedReason(plan) {
    const diff = isModernPlanMigrationDiff(plan.migrations)
        ? plan.migrations
        : isModernPlanMigrationDiff(plan.diff?.migrations)
            ? plan.diff.migrations
            : null;
    if (!diff)
        return null;
    if (diff.new.length > 0)
        return null;
    return diff.noop.length > 0 ? "migrations_unchanged" : "no_migrations";
}
function describeRehearsalFailure(report) {
    const failedMigration = report.migrations.find((m) => m.status === "failed");
    if (failedMigration)
        return `Migration ${failedMigration.id} failed${failedMigration.error ? `: ${failedMigration.error}` : ""}.`;
    const failedCheck = report.checks.find((c) => c.status === "failed");
    if (failedCheck)
        return `Check ${failedCheck.name} failed${failedCheck.error ? `: ${failedCheck.error}` : ""}.`;
    return report.error?.message ? `${report.error.message}.` : "";
}
async function applyOnce(client, spec, opts, emit) {
    const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
    const target = opts.target === "core" ? "core" : "cloud";
    if (opts.vault && target === "core") {
        // Refuse rather than drop the hooks: silently committing without the
        // activation block would activate a `required` project unvaulted.
        throw new Run402DeployError("vault-bound deploys are not supported against a self-hosted Core gateway; the vault gate is a Cloud control-plane capability.", { code: "VAULT_UNSUPPORTED_TARGET", phase: "validate", retryable: false, context: "applying deploy" });
    }
    const sliceKinds = deriveSliceKinds(spec);
    emit({ type: "plan.started" });
    const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey, {
        target,
        requiredPlan: opts.requiredPlan,
        ...(opts.vault ? { vault: opts.vault.declaration } : {}),
    });
    emit({ type: "plan.diff", diff: plan.diff });
    emitPlanWarnings(plan, emit);
    if (!opts.requiredPlan)
        abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
    if (plan.payment_required) {
        emit({
            type: "payment.required",
            amount: plan.payment_required.amount,
            asset: plan.payment_required.asset,
            payTo: plan.payment_required.payTo,
            reason: plan.payment_required.reason,
        });
        // The kernel's x402-wrapped fetch (Node) handles 402 transparently
        // when the commit happens; we don't block here. Agents using a
        // sandbox provider without payment auto-handling can intercept the
        // event and resolve before we hit upload.
    }
    if (target === "core") {
        await uploadCoreContent(client, spec.project_id, byteReaders, emit);
        emit({
            type: "commit.phase",
            phase: "validate",
            status: "started",
            ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
        });
        const planId = requirePlanId(plan, "applying deploy to Core");
        const commit = await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan);
        if (!isCoreCommitResponse(commit)) {
            return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
        }
        return await coreDeployResult(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
    }
    await uploadMissing(client, spec.project_id, plan.missing_content, byteReaders, emit);
    emit({
        type: "commit.phase",
        phase: "validate",
        status: "started",
        ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
    });
    const { planId, operationId } = requirePersistedPlan(plan, "applying deploy");
    // first-deploy-agent-dx: rehearsal is apply's decision, from the plan
    // envelope. A migration-bearing plan against a project with a live release
    // is rehearsed on a contained branch and committed only on a passing
    // report; a first deploy (`reason: "no_live_release"`) has nothing to
    // protect and commits directly. A passed rehearsal binds the commit to the
    // report's `required_plan` — unbound commits of rehearsed plans are
    // refused by the gateway.
    const rehearsal = await rehearseBeforeCommit(client, plan, planId, spec.project_id, opts, emit);
    const requiredPlan = rehearsal.requiredPlan ?? opts.requiredPlan;
    // vault §6.5 — the handshake. Content is uploaded, so the artifacts this
    // release ships are fixed; `authorize` verifies snapshot correspondence,
    // mints the activation token, and hands back the commit block. It throws
    // rather than returning when the deploy must not proceed, and nothing has
    // been committed at that point.
    const vaultCommit = opts.vault
        ? await opts.vault.authorize({
            plan_id: planId,
            operation_id: operationId,
            apply_plan_sha256: plan.vault?.apply_plan_sha256 ?? null,
        })
        : undefined;
    const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, requiredPlan, vaultCommit), "applying deploy");
    const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
    result.rehearsal = rehearsal.block;
    // v1.48 unified-apply: thread the plan response's `asset_entries[]` back
    // into DeployResult.assets so callers reading `result.assets.byKey[key]`
    // get the gateway-authoritative `AssetRef` envelope (URLs, SRI, etag).
    // Release-only applies leave `result.assets` undefined.
    if (plan.asset_entries && plan.asset_entries.length > 0) {
        result.assets = buildAssetManifestFromPlanEntries(plan.asset_entries);
        // v1.49 image-variant follow-up: variants are generated AT COMMIT TIME
        // (parent gateway change Section 5 — `prepareStagedAssetVariants` runs
        // before the activation txn opens). The plan that funded `result.assets`
        // was built BEFORE commit, so its `asset_entries[].asset_ref` doesn't
        // include the variant fields (the gateway only threads `image_data`
        // through `buildAssetRefForPlan` for SHAs that ALREADY have variants
        // in `internal.blob_image_variants`).
        //
        // For image puts, do a dry-run re-plan with the same spec. Bytes are
        // now in CAS (the commit just landed) AND variant rows exist in the
        // DB. The new plan response surfaces the variants. Dry-run keeps it
        // cheap: no new plan_id or operation_id rows are created.
        //
        // Best-effort: a re-plan failure shouldn't fail the apply. The bytes
        // are committed, the release is live, and the variant fields are
        // strictly additive — leaving them empty when the recheck errors is
        // worse than failing the apply.
        const hasImagePut = spec.assets?.put?.some((entry) => {
            const ct = ("content_type" in entry && typeof entry.content_type === "string")
                ? entry.content_type
                : "";
            return ct.startsWith("image/");
        });
        if (hasImagePut) {
            try {
                const { plan: recheck } = await planInternal(client, spec, undefined, { dryRun: true });
                if (recheck.asset_entries && recheck.asset_entries.length > 0) {
                    for (const recheckEntry of recheck.asset_entries) {
                        const existing = result.assets.byKey[recheckEntry.key];
                        if (!existing)
                            continue;
                        const ref = recheckEntry.asset_ref;
                        // v1.49 — variants + intrinsics surfaced now that the encoder ran.
                        if (ref.width_px !== undefined)
                            existing.width_px = ref.width_px;
                        if (ref.height_px !== undefined)
                            existing.height_px = ref.height_px;
                        if (ref.blurhash !== undefined)
                            existing.blurhash = ref.blurhash;
                        if (ref.variant_spec_version !== undefined) {
                            existing.variant_spec_version = ref.variant_spec_version;
                        }
                        if (ref.display_url !== undefined)
                            existing.display_url = ref.display_url;
                        if (ref.display_immutable_url !== undefined) {
                            existing.display_immutable_url = ref.display_immutable_url;
                        }
                        if (ref.variants !== undefined)
                            existing.variants = ref.variants;
                        // v1.50 — image-intrinsic + caller-metadata columns surfaced by
                        // the gateway plan-path enrichment.
                        // The first plan ran BEFORE the encoder; the re-plan picks them up.
                        // Without this merge, the SDK's buildAssetRef widens them back
                        // to null in the final result.
                        if (ref.image_format !== undefined)
                            existing.image_format = ref.image_format;
                        if (ref.image_info !== undefined)
                            existing.image_info = ref.image_info;
                        if (ref.image_exif !== undefined)
                            existing.image_exif = ref.image_exif;
                        if (ref.image_exif_policy !== undefined) {
                            existing.image_exif_policy = ref.image_exif_policy;
                        }
                        if (ref.metadata !== undefined)
                            existing.metadata = ref.metadata;
                        // v1.54 — shape-contract fields. `<Run402Image>` placeholder
                        // rendering + strict-mode schema filtering both key on these.
                        if (ref.blurhash_data_url !== undefined) {
                            existing.blurhash_data_url = ref.blurhash_data_url;
                        }
                        if (ref.asset_schema !== undefined)
                            existing.asset_schema = ref.asset_schema;
                    }
                }
            }
            catch {
                // Best-effort: leave variant fields unpopulated rather than
                // failing a successful apply. Consumers can re-call
                // `r.assets.put` with the same bytes (dedup) to trigger the
                // plan-time surfacing if variants are missing.
            }
        }
    }
    return result;
}
function normalizeApplyMaxRetries(value) {
    if (value === undefined)
        return APPLY_RETRY_DEFAULT_MAX_RETRIES;
    if (!Number.isInteger(value) || value < 0 || !Number.isFinite(value)) {
        throw new Run402DeployError("ApplyOptions.maxRetries must be a non-negative integer", {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: "maxRetries",
            retryable: false,
            context: "validating deploy retry options",
        });
    }
    return value;
}
function isSafeDeployApplyRetry(err, spec) {
    return (err.safeToRetry === true &&
        APPLY_SAFE_RETRY_CODES.has(err.code) &&
        isAutoRebasableSpec(spec));
}
function isAutoRebasableSpec(spec) {
    if (spec.base === undefined)
        return true;
    return "release" in spec.base && spec.base.release === "current";
}
function deployApplyRetryDelayMs(attempt) {
    const exponential = APPLY_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
    const capped = Math.min(exponential, APPLY_RETRY_MAX_DELAY_MS);
    return capped + Math.floor(Math.random() * (APPLY_RETRY_JITTER_MS + 1));
}
function withDeployRetryMetadata(err, attempts, maxRetries, lastRetryCode) {
    return new Run402DeployError(err.message, {
        code: err.code,
        phase: err.phase,
        resource: err.resource,
        retryable: err.retryable,
        operationId: err.operationId,
        planId: err.planId,
        fix: err.fix,
        logs: err.logs,
        rolledBack: err.rolledBack,
        attempts,
        maxRetries,
        lastRetryCode,
        status: err.status,
        body: enrichDeployRetryBody(err.body, attempts, maxRetries, lastRetryCode),
        context: err.context,
    });
}
function enrichDeployRetryBody(body, attempts, maxRetries, lastRetryCode) {
    const retryFields = {
        attempts,
        max_retries: maxRetries,
        last_retry_code: lastRetryCode,
    };
    if (body && typeof body === "object" && !Array.isArray(body)) {
        return { ...body, ...retryFields };
    }
    return retryFields;
}
function contentRefToWire(ref) {
    const maybeWire = ref;
    return {
        sha256: ref.sha256,
        size: ref.size,
        ...(ref.contentType ?? maybeWire.content_type
            ? { content_type: ref.contentType ?? maybeWire.content_type }
            : {}),
        ...(ref.integrity ? { integrity: ref.integrity } : {}),
    };
}
function contentRefToCoreSpec(ref) {
    const maybeWire = ref;
    const contentType = ref.contentType ?? maybeWire.content_type;
    return {
        sha256: ref.sha256,
        size: ref.size,
        ...(contentType ? { contentType } : {}),
        ...(ref.integrity ? { integrity: ref.integrity } : {}),
    };
}
function requireContentRef(ref, resource) {
    if (ref)
        return ref;
    throw new Run402DeployError(`Missing content ref for ${resource}`, {
        code: "INTERNAL_ERROR",
        phase: "validate",
        resource,
        retryable: false,
        context: "serializing deploy spec",
    });
}
function fileSetToWire(map) {
    const out = {};
    for (const [path, ref] of Object.entries(map))
        out[path] = contentRefToWire(ref);
    return out;
}
function fileSetToCoreSpec(map) {
    const out = {};
    for (const [path, ref] of Object.entries(map))
        out[path] = contentRefToCoreSpec(ref);
    return out;
}
function requireRoleToWire(gate) {
    return {
        table: gate.table,
        id_column: gate.idColumn,
        role_column: gate.roleColumn,
        allowed: gate.allowed,
        ...(gate.cacheTtl !== undefined ? { cache_ttl: gate.cacheTtl } : {}),
        ...(gate.onDeny !== undefined ? { on_deny: gate.onDeny } : {}),
        ...(gate.signInPath !== undefined ? { sign_in_path: gate.signInPath } : {}),
    };
}
function functionToCoreSpec(fn) {
    return {
        ...(fn.runtime !== undefined ? { runtime: fn.runtime } : {}),
        ...(fn.source !== undefined ? { source: contentRefToCoreSpec(fn.source) } : {}),
        ...(fn.files !== undefined ? { files: fileSetToCoreSpec(fn.files) } : {}),
        ...(fn.entrypoint !== undefined ? { entrypoint: fn.entrypoint } : {}),
        ...(fn.config !== undefined ? { config: fn.config } : {}),
        ...(fn.deps !== undefined ? { deps: fn.deps } : {}),
        ...(fn.triggers !== undefined ? { triggers: fn.triggers } : {}),
        ...(fn.schedule !== undefined ? { schedule: fn.schedule } : {}),
        ...(fn.requireAuth !== undefined ? { requireAuth: fn.requireAuth } : {}),
        ...(fn.requireRole !== undefined ? { requireRole: fn.requireRole } : {}),
        ...(fn.class !== undefined ? { class: fn.class } : {}),
        ...(fn.capabilities !== undefined ? { capabilities: fn.capabilities } : {}),
    };
}
function functionToWire(fn) {
    return {
        ...(fn.runtime !== undefined ? { runtime: fn.runtime } : {}),
        ...(fn.source !== undefined ? { source: contentRefToWire(fn.source) } : {}),
        ...(fn.files !== undefined ? { files: fileSetToWire(fn.files) } : {}),
        ...(fn.entrypoint !== undefined ? { entrypoint: fn.entrypoint } : {}),
        ...(fn.config !== undefined
            ? {
                config: {
                    ...(fn.config.timeoutSeconds !== undefined ? { timeout_seconds: fn.config.timeoutSeconds } : {}),
                    ...(fn.config.memoryMb !== undefined ? { memory_mb: fn.config.memoryMb } : {}),
                },
            }
            : {}),
        ...(fn.deps !== undefined ? { deps: fn.deps } : {}),
        ...(fn.triggers !== undefined ? { triggers: fn.triggers } : {}),
        ...(fn.schedule !== undefined ? { schedule: fn.schedule } : {}),
        ...(fn.requireAuth !== undefined ? { require_auth: fn.requireAuth } : {}),
        ...(fn.requireRole !== undefined
            ? { require_role: fn.requireRole === null ? null : requireRoleToWire(fn.requireRole) }
            : {}),
        ...(fn.class !== undefined ? { class: fn.class } : {}),
        ...(fn.capabilities !== undefined ? { capabilities: fn.capabilities } : {}),
    };
}
function functionMapToWire(map) {
    const out = {};
    for (const [name, fn] of Object.entries(map))
        out[name] = functionToWire(fn);
    return out;
}
function functionMapToCoreSpec(map) {
    const out = {};
    for (const [name, fn] of Object.entries(map))
        out[name] = functionToCoreSpec(fn);
    return out;
}
function functionsToCoreSpec(functions) {
    return {
        ...(functions.replace !== undefined ? { replace: functionMapToCoreSpec(functions.replace) } : {}),
        ...(functions.patch !== undefined
            ? {
                patch: {
                    ...(functions.patch.set !== undefined ? { set: functionMapToCoreSpec(functions.patch.set) } : {}),
                    ...(functions.patch.delete !== undefined ? { delete: functions.patch.delete } : {}),
                },
            }
            : {}),
    };
}
function databaseToWire(database) {
    return {
        ...(database.expose !== undefined ? { expose: database.expose } : {}),
        ...(database.zero_downtime !== undefined ? { zero_downtime: database.zero_downtime } : {}),
        ...(database.migrations !== undefined
            ? {
                migrations: database.migrations.map((m) => ({
                    id: m.id,
                    checksum: m.checksum,
                    ...(m.sql !== undefined
                        ? { sql: m.sql }
                        : { sql_ref: contentRefToWire(requireContentRef(m.sql_ref, `database.migrations.${m.id}.sql_ref`)) }),
                    ...(m.transaction !== undefined ? { transaction: m.transaction } : {}),
                })),
            }
            : {}),
    };
}
function databaseToCoreSpec(database) {
    return {
        ...(database.expose !== undefined ? { expose: database.expose } : {}),
        ...(database.zero_downtime !== undefined ? { zero_downtime: database.zero_downtime } : {}),
        ...(database.migrations !== undefined
            ? {
                migrations: database.migrations.map((m) => ({
                    id: m.id,
                    checksum: m.checksum,
                    ...(m.sql !== undefined
                        ? { sql: m.sql }
                        : { sql_ref: contentRefToCoreSpec(requireContentRef(m.sql_ref, `database.migrations.${m.id}.sql_ref`)) }),
                    ...(m.transaction !== undefined ? { transaction: m.transaction } : {}),
                })),
            }
            : {}),
    };
}
function functionsToWire(functions) {
    return {
        ...(functions.replace !== undefined ? { replace: functionMapToWire(functions.replace) } : {}),
        ...(functions.patch !== undefined
            ? {
                patch: {
                    ...(functions.patch.set !== undefined ? { set: functionMapToWire(functions.patch.set) } : {}),
                    ...(functions.patch.delete !== undefined ? { delete: functions.patch.delete } : {}),
                },
            }
            : {}),
    };
}
function siteToCoreSpec(site) {
    if ("replace" in site && site.replace) {
        return {
            replace: fileSetToCoreSpec(site.replace),
            ...(site.public_paths ? { public_paths: site.public_paths } : {}),
        };
    }
    if ("patch" in site && site.patch) {
        return {
            patch: {
                ...(site.patch.put ? { put: fileSetToCoreSpec(site.patch.put) } : {}),
                ...(site.patch.delete ? { delete: site.patch.delete } : {}),
            },
            ...(site.public_paths ? { public_paths: site.public_paths } : {}),
        };
    }
    // tenant-site-embedding never reaches core: the catalog is gateway-owned and
    // core validates site keys against its own list.
    return site.public_paths ? { public_paths: site.public_paths } : {};
}
function siteToWire(site) {
    // tenant-site-embedding: `null` is a meaningful value on the wire (clear), so
    // it is carried whenever the key is present, unlike public_paths.
    const embedding = "embedding" in site && site.embedding !== undefined ? { embedding: site.embedding } : {};
    if ("replace" in site && site.replace) {
        return {
            replace: fileSetToWire(site.replace),
            ...(site.public_paths ? { public_paths: site.public_paths } : {}),
            ...embedding,
        };
    }
    if ("patch" in site && site.patch) {
        return {
            patch: {
                ...(site.patch.put ? { put: fileSetToWire(site.patch.put) } : {}),
                ...(site.patch.delete ? { delete: site.patch.delete } : {}),
            },
            ...(site.public_paths ? { public_paths: site.public_paths } : {}),
            ...embedding,
        };
    }
    return {
        ...(site.public_paths ? { public_paths: site.public_paths } : {}),
        ...embedding,
    };
}
function i18nToWire(i18n) {
    return {
        default_locale: i18n.defaultLocale,
        locales: i18n.locales,
        ...(i18n.detect !== undefined ? { detect: i18n.detect } : {}),
        ...(i18n.unknownLocalePolicy !== undefined ? { unknown_locale_policy: i18n.unknownLocalePolicy } : {}),
    };
}
function releaseSpecToCoreSpec(spec) {
    return {
        project: spec.project_id,
        ...(spec.base !== undefined ? { base: spec.base } : {}),
        ...(spec.database !== undefined ? { database: databaseToCoreSpec(spec.database) } : {}),
        ...(spec.secrets !== undefined ? { secrets: spec.secrets } : {}),
        ...(spec.functions !== undefined ? { functions: functionsToCoreSpec(spec.functions) } : {}),
        ...(spec.site !== undefined ? { site: siteToCoreSpec(spec.site) } : {}),
        ...(spec.subdomains !== undefined ? { subdomains: spec.subdomains } : {}),
        ...(spec.routes !== undefined ? { routes: spec.routes } : {}),
        ...(spec.checks !== undefined ? { checks: spec.checks } : {}),
        ...(spec.assets !== undefined ? { assets: spec.assets } : {}),
        ...(spec.i18n !== undefined ? { i18n: spec.i18n } : {}),
    };
}
function releaseSpecToWire(spec) {
    return {
        project_id: spec.project_id,
        ...(spec.base !== undefined ? { base: spec.base } : {}),
        ...(spec.database !== undefined ? { database: databaseToWire(spec.database) } : {}),
        ...(spec.secrets !== undefined ? { secrets: spec.secrets } : {}),
        ...(spec.functions !== undefined ? { functions: functionsToWire(spec.functions) } : {}),
        ...(spec.site !== undefined ? { site: siteToWire(spec.site) } : {}),
        ...(spec.subdomains !== undefined ? { subdomains: spec.subdomains } : {}),
        ...(spec.routes !== undefined ? { routes: spec.routes } : {}),
        ...(spec.checks !== undefined ? { checks: spec.checks } : {}),
        ...(spec.assets !== undefined ? { assets: spec.assets } : {}),
        ...(spec.i18n !== undefined ? { i18n: spec.i18n === null ? null : i18nToWire(spec.i18n) } : {}),
    };
}
async function planInternal(client, spec, idempotencyKey, opts = {}) {
    const dryRun = opts.dryRun === true;
    const reviewedPlan = opts.reviewedPlan === true;
    const target = opts.target ?? "cloud";
    const ciCredentials = isCiClient(client);
    validateSpec(spec);
    if (ciCredentials)
        assertCiDeployableSpec(spec);
    const isCore = target === "core";
    const { normalized, byteReaders } = await normalizeReleaseSpec(client, spec, {
        inlineMigrationSql: isCore,
    });
    if (!isCore)
        await preflightTierFunctionLimits(client, normalized, ciCredentials);
    const wireSpec = isCore ? releaseSpecToCoreSpec(normalized) : releaseSpecToWire(normalized);
    // The gateway expects { spec, manifest_ref?, idempotency_key? }. Cloud
    // receives the legacy snake_case apply wire shape; Core receives the
    // open ReleaseSpec shape from @run402/release. For oversized Cloud specs
    // the SDK uploads the manifest JSON to CAS first and references it; the
    // gateway still needs `spec` in the body (with at least the project), so
    // we keep a minimal stub there.
    const inlineBody = { spec: wireSpec };
    if (idempotencyKey && !dryRun && !reviewedPlan)
        inlineBody.idempotency_key = idempotencyKey;
    if (reviewedPlan)
        inlineBody.mode = "reviewed_plan";
    if (opts.requiredPlan)
        inlineBody.required_plan = requiredPlanToWire(opts.requiredPlan);
    // vault §6.5: the capture declaration must ride the SAME plan the gateway
    // digests, so it is set before the inline/manifest_ref size decision below
    // and re-attached to the manifest_ref body too.
    if (opts.vault)
        inlineBody.vault = opts.vault;
    const inlineBytes = new TextEncoder().encode(JSON.stringify(inlineBody)).byteLength;
    let body;
    if (inlineBytes <= PLAN_BODY_LIMIT_BYTES) {
        body = inlineBody;
    }
    else if (isCore) {
        throw new Run402DeployError("Core deploy planning requires an inline spec under the gateway body cap; manifest_ref is not supported by Run402 Core yet.", {
            code: "DRY_RUN_REQUIRES_INLINE_SPEC",
            phase: "validate",
            resource: "manifest_ref",
            retryable: false,
            context: "planning Core deploy",
        });
    }
    else {
        if (dryRun || reviewedPlan || opts.requiredPlan) {
            throw new Run402DeployError("Check/plan/require-plan deploy planning requires an inline spec under the gateway body cap; the normalized deploy plan would require manifest_ref.", {
                code: "PLAN_REQUIRES_INLINE_SPEC",
                phase: "validate",
                resource: "manifest_ref",
                retryable: false,
                context: "planning deploy",
            });
        }
        if (ciCredentials) {
            throw new Run402DeployError("CI deploys must use inline specs under the gateway body cap; the normalized deploy plan would require manifest_ref.", {
                code: "forbidden_spec_field",
                phase: "validate",
                resource: "manifest_ref",
                retryable: false,
                context: "validating CI deploy spec",
            });
        }
        // Upload the normalized manifest itself as a CAS object so the gateway
        // can pick it up via `manifest_ref`. The body still carries a minimal
        // `spec` so the gateway has the project for auth + plan persistence.
        const manifestBytes = new TextEncoder().encode(JSON.stringify(wireSpec));
        const ref = await uploadInlineCas(client, spec.project_id, manifestBytes, MANIFEST_CONTENT_TYPE);
        body = { spec: { project_id: spec.project_id }, manifest_ref: contentRefToWire(ref) };
        if (idempotencyKey)
            body.idempotency_key = idempotencyKey;
        if (opts.vault)
            body.vault = opts.vault;
    }
    let plan;
    try {
        plan = withClientPlanWarnings(normalized, normalizePlanResponse(await client.request(dryRun ? "/apply/v1/plans?dry_run=true" : "/apply/v1/plans", {
            method: "POST",
            body,
            // Write-approval scope: deploying a release is `project.deploy` on this project.
            authMeta: { method: "deploy.plan", capability: "project.deploy", target: { project_id: spec.project_id } },
            context: "planning deploy",
        })));
    }
    catch (err) {
        throw translateDeployError(err, "plan", null, null);
    }
    return { plan, byteReaders };
}
function requiredPlanToWire(plan) {
    const wire = { plan_id: plan.planId };
    if (plan.planFingerprint !== undefined)
        wire.plan_fingerprint = plan.planFingerprint;
    return wire;
}
async function preflightTierFunctionLimits(client, spec, ciCredentials) {
    if (ciCredentials)
        return;
    if (!hasFunctionTierPreflightInputs(spec.functions))
        return;
    const limits = await readTierFunctionLimits(client);
    if (!limits)
        return;
    const entries = collectFunctionPreflightEntries(spec.functions);
    for (const entry of entries) {
        const timeout = entry.fn.config?.timeoutSeconds;
        if (timeout !== undefined &&
            limits.maxTimeoutSeconds &&
            timeout > limits.maxTimeoutSeconds.value) {
            throw tierLimitError(`Function ${entry.name} timeoutSeconds ${timeout} exceeds the ${limits.tier} tier maximum of ${limits.maxTimeoutSeconds.value}.`, `${entry.fieldPrefix}.config.timeoutSeconds`, timeout, limits, limits.maxTimeoutSeconds, {
                tier_max: limits.maxTimeoutSeconds.value,
                max_function_timeout_seconds: limits.maxTimeoutSeconds.value,
            });
        }
        const memory = entry.fn.config?.memoryMb;
        if (memory !== undefined &&
            limits.maxMemoryMb &&
            memory > limits.maxMemoryMb.value) {
            throw tierLimitError(`Function ${entry.name} memoryMb ${memory} exceeds the ${limits.tier} tier maximum of ${limits.maxMemoryMb.value}.`, `${entry.fieldPrefix}.config.memoryMb`, memory, limits, limits.maxMemoryMb, {
                tier_max: limits.maxMemoryMb.value,
                max_function_memory_mb: limits.maxMemoryMb.value,
            });
        }
        for (const trigger of scheduleTriggersForFunction(entry.fn)) {
            if (!limits.minCronIntervalMinutes)
                continue;
            const intervalMinutes = estimateCronMinimumIntervalMinutes(trigger.cron);
            if (intervalMinutes !== null &&
                intervalMinutes < limits.minCronIntervalMinutes.value) {
                const triggerLabel = trigger.id === "__legacy__" ? "schedule" : `trigger ${trigger.id}`;
                throw tierLimitError(`Function ${entry.name} ${triggerLabel} runs every ${intervalMinutes} minute(s), below the ${limits.tier} tier minimum interval of ${limits.minCronIntervalMinutes.value} minutes.`, `${entry.fieldPrefix}.${trigger.fieldSuffix}`, trigger.cron, limits, limits.minCronIntervalMinutes, {
                    interval_minutes: intervalMinutes,
                    min_interval_minutes: limits.minCronIntervalMinutes.value,
                    min_cron_interval_minutes: limits.minCronIntervalMinutes.value,
                });
            }
        }
    }
    if (limits.maxScheduledFunctions) {
        const lowerBound = countScheduledFunctionsInSetEntries(spec.functions);
        if (lowerBound > limits.maxScheduledFunctions.value) {
            throw scheduledCountTierLimitError(lowerBound, limits, limits.maxScheduledFunctions, "manifest");
        }
        const desired = await computeDesiredScheduledFunctionCount(client, spec);
        if (desired && desired.count > limits.maxScheduledFunctions.value) {
            throw scheduledCountTierLimitError(desired.count, limits, limits.maxScheduledFunctions, desired.source);
        }
    }
}
function hasFunctionTierPreflightInputs(functions) {
    if (!functions)
        return false;
    return collectFunctionPreflightEntries(functions).some((entry) => (entry.fn.config?.timeoutSeconds !== undefined ||
        entry.fn.config?.memoryMb !== undefined ||
        scheduleTriggersForFunction(entry.fn).length > 0));
}
function collectFunctionPreflightEntries(functions) {
    if (!functions)
        return [];
    const entries = [];
    for (const [name, fn] of Object.entries(functions.replace ?? {})) {
        entries.push({ name, fn, fieldPrefix: `functions.${name}` });
    }
    for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
        entries.push({ name, fn, fieldPrefix: `functions.${name}` });
    }
    return entries;
}
async function readTierFunctionLimits(client) {
    let status;
    try {
        status = await client.request("/tiers/v1/status", {
            context: "checking tier status for deploy preflight",
        });
    }
    catch {
        return null;
    }
    if (typeof status.tier !== "string" || status.tier.length === 0) {
        return null;
    }
    const tierKey = status.tier.toLowerCase();
    const fallback = STATIC_FUNCTION_LIMITS_BY_TIER[tierKey];
    const limits = { tier: status.tier };
    limits.maxTimeoutSeconds = tierStatusLimitOrFallback(status, [
        "max_function_timeout_seconds",
        "max_timeout_seconds",
        "timeout_seconds_max",
        "function_timeout_seconds_max",
    ], fallback?.maxTimeoutSeconds);
    limits.maxMemoryMb = tierStatusLimitOrFallback(status, [
        "max_function_memory_mb",
        "max_memory_mb",
        "memory_mb_max",
        "function_memory_mb_max",
    ], fallback?.maxMemoryMb);
    limits.maxScheduledFunctions = tierStatusLimitOrFallback(status, [
        "max_scheduled_functions",
        "scheduled_functions_limit",
        "scheduled_function_limit",
        "max_function_schedules",
    ], fallback?.maxScheduledFunctions);
    limits.minCronIntervalMinutes = tierStatusLimitOrFallback(status, [
        "min_cron_interval_minutes",
        "minimum_cron_interval_minutes",
        "min_schedule_interval_minutes",
        "min_scheduled_function_interval_minutes",
    ], fallback?.minCronIntervalMinutes);
    limits.currentScheduledFunctions = tierStatusLimit(status, [
        "current_scheduled_functions",
        "current_scheduled_function_count",
        "scheduled_function_count",
        "scheduled_functions",
    ]);
    return hasAnyTierFunctionLimit(limits) ? limits : null;
}
function tierStatusLimitOrFallback(status, keys, fallback) {
    return tierStatusLimit(status, keys) ?? (fallback === undefined
        ? undefined
        : { value: fallback, source: "local_static_fallback" });
}
function tierStatusLimit(status, keys) {
    const limits = objectField(status, "limits");
    const containers = [
        objectField(status, "function_limits"),
        objectField(limits, "functions"),
        objectField(limits, "function_limits"),
        objectField(status, "pool_usage"),
        status,
    ];
    for (const container of containers) {
        if (!container || typeof container !== "object" || Array.isArray(container))
            continue;
        const obj = container;
        for (const key of keys) {
            const value = obj[key];
            if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
                return { value, source: "tier_status" };
            }
        }
    }
    return undefined;
}
function hasAnyTierFunctionLimit(limits) {
    return Boolean(limits.maxTimeoutSeconds ||
        limits.maxMemoryMb ||
        limits.maxScheduledFunctions ||
        limits.minCronIntervalMinutes ||
        limits.currentScheduledFunctions);
}
async function computeDesiredScheduledFunctionCount(client, spec) {
    const functions = spec.functions;
    if (!functions)
        return null;
    const replaceScheduled = scheduledFunctionNames(functions.replace);
    if (replaceScheduled) {
        applyScheduledFunctionPatch(replaceScheduled, functions.patch);
        return { count: replaceScheduled.size, source: "manifest" };
    }
    if (!functions.patch)
        return null;
    const activeScheduled = await readActiveScheduledFunctionNames(client, spec.project_id);
    if (!activeScheduled)
        return null;
    applyScheduledFunctionPatch(activeScheduled, functions.patch);
    return { count: activeScheduled.size, source: "active_release_inventory" };
}
function countScheduledFunctionsInSetEntries(functions) {
    if (!functions)
        return 0;
    let count = 0;
    for (const fn of Object.values(functions.replace ?? {})) {
        count += scheduleTriggersForFunction(fn).length;
    }
    for (const fn of Object.values(functions.patch?.set ?? {})) {
        count += scheduleTriggersForFunction(fn).length;
    }
    return count;
}
function scheduledFunctionNames(functions) {
    if (!functions)
        return null;
    const scheduled = new Set();
    for (const [name, fn] of Object.entries(functions)) {
        if (scheduleTriggersForFunction(fn).length > 0)
            scheduled.add(name);
    }
    return scheduled;
}
function applyScheduledFunctionPatch(scheduled, patch) {
    for (const name of patch?.delete ?? []) {
        scheduled.delete(name);
    }
    for (const [name, fn] of Object.entries(patch?.set ?? {})) {
        if (fn.schedule === null)
            scheduled.delete(name);
        else if (scheduleTriggersForFunction(fn).length > 0)
            scheduled.add(name);
    }
}
async function readActiveScheduledFunctionNames(client, projectId) {
    let inventory;
    try {
        inventory = await client.request(appendQuery("/apply/v1/releases/active", { site_limit: 1 }), {
            headers: await apikeyHeaders(client, projectId),
            context: "fetching active release inventory for deploy preflight",
        });
    }
    catch {
        return null;
    }
    const scheduled = new Set();
    for (const fn of inventory.functions ?? []) {
        if (isScheduledCron(fn.schedule) || scheduleTriggersForFunction(fn).length > 0)
            scheduled.add(fn.name);
    }
    return scheduled;
}
function scheduleTriggersForFunction(fn) {
    if (fn.triggers && fn.triggers.length > 0) {
        return fn.triggers
            .filter((trigger) => trigger.type === "schedule")
            .map((trigger) => ({
            ...trigger,
            fieldSuffix: `triggers.${trigger.id}.cron`,
        }));
    }
    return isScheduledCron(fn.schedule)
        ? [{
                id: "__legacy__",
                type: "schedule",
                cron: fn.schedule,
                timezone: "UTC",
                misfire_policy: "skip",
                overlap_policy: "allow",
                run: { event_type: "legacy.schedule", payload: {} },
                fieldSuffix: "schedule",
            }]
        : [];
}
function scheduledCountTierLimitError(count, limits, limit, countSource) {
    return tierLimitError(`Deploy would have ${count} scheduled function(s), exceeding the ${limits.tier} tier maximum of ${limit.value}.`, "functions.scheduled_count", count, limits, limit, {
        tier_max: limit.value,
        max_scheduled_functions: limit.value,
        count_source: countSource,
    });
}
function tierLimitError(message, field, value, limits, limit, extraDetails) {
    const hint = limit.source === "local_static_fallback"
        ? "Tier limits came from the SDK's static fallback because /tiers/v1/status did not expose function caps. Run `run402 tier status` to refresh, lower the function setting, upgrade the tier, or retry and let gateway validation decide if this seems stale."
        : "Lower the function setting or upgrade the tier before deploying.";
    const details = {
        field,
        value,
        tier: limits.tier,
        limit_source: limit.source,
        ...extraDetails,
    };
    const body = {
        code: "BAD_FIELD",
        category: "deploy",
        message,
        retryable: false,
        details,
        hint,
    };
    return new Run402DeployError(message, {
        code: "BAD_FIELD",
        phase: "validate",
        resource: field,
        retryable: false,
        body,
        context: "validating deploy tier limits",
    });
}
function isScheduledCron(value) {
    return typeof value === "string" && value.trim().length > 0;
}
function estimateCronMinimumIntervalMinutes(expression) {
    const parts = expression.trim().split(/\s+/);
    if (parts.length !== 5)
        return null;
    const minutes = expandCronNumberField(parts[0], 0, 59);
    const hours = expandCronNumberField(parts[1], 0, 23);
    if (!minutes || !hours || minutes.length === 0 || hours.length === 0)
        return null;
    const occurrences = [];
    for (const hour of hours) {
        for (const minute of minutes) {
            occurrences.push(hour * 60 + minute);
        }
    }
    occurrences.sort((a, b) => a - b);
    if (occurrences.length <= 1)
        return 24 * 60;
    let minGap = Number.POSITIVE_INFINITY;
    for (let i = 1; i < occurrences.length; i += 1) {
        minGap = Math.min(minGap, occurrences[i] - occurrences[i - 1]);
    }
    minGap = Math.min(minGap, 24 * 60 - occurrences[occurrences.length - 1] + occurrences[0]);
    return Number.isFinite(minGap) ? minGap : null;
}
function expandCronNumberField(field, min, max) {
    const values = new Set();
    for (const part of field.split(",")) {
        const expanded = expandCronNumberPart(part.trim(), min, max);
        if (!expanded)
            return null;
        for (const value of expanded)
            values.add(value);
    }
    return [...values].sort((a, b) => a - b);
}
function expandCronNumberPart(part, min, max) {
    if (!part)
        return null;
    const [rangePart, stepPart] = part.split("/");
    if (part.split("/").length > 2)
        return null;
    const step = stepPart === undefined ? 1 : Number(stepPart);
    if (!Number.isSafeInteger(step) || step < 1)
        return null;
    let start;
    let end;
    if (rangePart === "*") {
        start = min;
        end = max;
    }
    else if (rangePart?.includes("-")) {
        const [rawStart, rawEnd] = rangePart.split("-");
        start = Number(rawStart);
        end = Number(rawEnd);
    }
    else {
        start = Number(rangePart);
        end = stepPart === undefined ? start : max;
    }
    if (!Number.isSafeInteger(start) ||
        !Number.isSafeInteger(end) ||
        start < min ||
        end > max ||
        start > end) {
        return null;
    }
    const values = [];
    for (let value = start; value <= end; value += step) {
        values.push(value);
    }
    return values;
}
async function commitInternal(client, planId, idempotencyKey, project, requiredPlan, vault) {
    try {
        const body = {};
        if (idempotencyKey)
            body.idempotency_key = idempotencyKey;
        if (requiredPlan)
            body.required_plan = requiredPlanToWire(requiredPlan);
        if (vault)
            body.vault = vault;
        return await client.request(`/apply/v1/plans/${encodeURIComponent(planId)}/commit`, {
            method: "POST",
            body,
            // Write-approval scope: committing a deploy is `project.deploy` on this project.
            ...(project
                ? {
                    authMeta: {
                        method: "deploy.commit",
                        capability: "project.deploy",
                        target: { project_id: project },
                    },
                }
                : {}),
            context: "committing deploy",
        });
    }
    catch (err) {
        throw translateDeployError(err, "commit", planId, null);
    }
}
async function uploadMissing(client, projectId, presence, byteReaders, emit) {
    // Surface CAS dedup hits so agents can distinguish "N files were already
    // present" from "nothing happened". The gateway reports both present and
    // missing refs in `missing_content`; emit a skipped event for each present
    // one before short-circuiting on a fully-deduped plan.
    const skipped = presence.filter((p) => p.present);
    for (const p of skipped) {
        const reader = byteReaders.get(p.sha256);
        emit({
            type: "content.upload.skipped",
            label: reader?.label ?? p.sha256,
            sha256: p.sha256,
            reason: "present",
            ...(reader?.slice ? { slice_kind: reader.slice } : {}),
        });
    }
    // Filter to refs the gateway reported as missing for this project.
    const needsUpload = presence.filter((p) => !p.present);
    if (needsUpload.length === 0)
        return;
    // Hand off to the CAS content service: hand it the list of missing
    // refs, it issues an upload session per ref with presigned PUT URLs,
    // then we PUT the bytes and commit the content plan.
    const headers = await apikeyHeaders(client, projectId);
    const ciCredentials = isCiClient(client);
    const contentRequest = needsUpload.map((p) => {
        const reader = byteReaders.get(p.sha256);
        return {
            sha256: p.sha256,
            size: p.size,
            content_type: reader?.contentType,
        };
    });
    const planRes = await client.request("/content/v1/plans", {
        method: "POST",
        headers,
        body: ciCredentials
            ? { project_id: projectId, content: contentRequest }
            : { content: contentRequest },
        context: "planning content upload",
    });
    const total = planRes.missing.length;
    let done = 0;
    for (const session of planRes.missing) {
        const reader = byteReaders.get(session.sha256);
        if (!reader) {
            throw new Run402DeployError(`internal: no local byte reader for sha ${session.sha256.slice(0, 12)}…`, {
                code: "CONTENT_UPLOAD_FAILED",
                phase: "upload",
                retryable: false,
                context: "uploading deploy bytes",
            });
        }
        const bytes = await reader();
        await uploadOneWithRetry(client.fetch, session, bytes);
        // v1.48 unified-apply: per-session completion via /storage/v1/uploads/:id/
        // complete was removed (the route is now 404). All sessions are CAS-style
        // staged-then-promote; the plan-level POST /content/v1/plans/:id/commit
        // call below promotes every session for the plan in one shot.
        done += 1;
        emit({
            type: "content.upload.progress",
            label: reader.label ?? session.sha256,
            sha256: session.sha256,
            done,
            total,
            ...(reader.slice ? { slice_kind: reader.slice } : {}),
        });
    }
    // Plan-level finalize — marks the plan committed in the deploy_plans
    // table. Per-session promotion to CAS already happened in the loop
    // above; this call is the plan-level idempotency anchor.
    await client.request(`/content/v1/plans/${encodeURIComponent(planRes.plan_id)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
}
async function uploadCoreContent(client, projectId, byteReaders, emit) {
    const entries = [...byteReaders.entries()].sort(([a], [b]) => a.localeCompare(b));
    const total = entries.length;
    let done = 0;
    for (const [sha256, reader] of entries) {
        const bytes = await reader();
        await client.request(`/projects/v1/${encodeURIComponent(projectId)}/content`, {
            method: "POST",
            body: {
                sha256,
                size: bytes.byteLength,
                content_type: reader.contentType ?? "application/octet-stream",
                bytes_base64: base64FromBytes(bytes),
            },
            context: "staging Core deploy content",
        });
        done += 1;
        emit({
            type: "content.upload.progress",
            label: reader.label ?? sha256,
            sha256,
            done,
            total,
            ...(reader.slice ? { slice_kind: reader.slice } : {}),
        });
    }
}
async function uploadOneWithRetry(fetchFn, session, bytes) {
    const MAX_ATTEMPTS = 3;
    for (let attempt = 1;; attempt++) {
        try {
            return await uploadOne(fetchFn, session, bytes);
        }
        catch (err) {
            const retryable = err instanceof Run402DeployError && err.retryable;
            if (!retryable || attempt >= MAX_ATTEMPTS)
                throw err;
            await sleep(1000 * Math.pow(2, attempt - 1)); // 1s, 2s
        }
    }
}
async function uploadOne(fetchFn, entry, bytes) {
    if (entry.mode === "single") {
        if (entry.parts.length !== 1) {
            throw new Run402DeployError(`internal: single-mode upload for ${entry.sha256.slice(0, 12)}… returned ${entry.parts.length} parts`, {
                code: "CONTENT_UPLOAD_FAILED",
                phase: "upload",
                retryable: false,
                context: "uploading deploy bytes",
            });
        }
        const part = entry.parts[0];
        const slice = bytes.subarray(part.byte_start, part.byte_end + 1);
        const checksum = base64FromHex(entry.sha256);
        await putToS3(fetchFn, part.url, slice, checksum, part.part_number);
        return [];
    }
    const uploadedParts = [];
    for (const part of entry.parts) {
        const slice = bytes.subarray(part.byte_start, part.byte_end + 1);
        const checksum = await sha256Base64(slice);
        const etag = await putToS3(fetchFn, part.url, slice, checksum, part.part_number);
        if (!etag) {
            throw new Run402DeployError(`S3 PUT succeeded for multipart part ${part.part_number} but did not return an ETag`, {
                code: "CONTENT_UPLOAD_FAILED",
                phase: "upload",
                retryable: false,
                context: "uploading deploy bytes",
            });
        }
        uploadedParts.push({ part_number: part.part_number, etag });
    }
    return uploadedParts;
}
function uploadCompleteBody(session, uploadedParts) {
    if (session.mode === "multipart") {
        return { parts: uploadedParts };
    }
    return {};
}
async function putToS3(fetchFn, url, body, checksumBase64, partNumber) {
    // The gateway issues SigV4 presigned URLs with `ChecksumSHA256` set on
    // PutObjectCommand / UploadPartCommand. The AWS SDK (v3) encodes that
    // value as the `x-amz-checksum-sha256` query parameter and only signs
    // `host` + `content-length`. If we ALSO send it as a request header, S3
    // returns 403 "There were headers present in the request which were not
    // signed: x-amz-checksum-sha256" because the header isn't in the
    // SigV4-signed list.
    //
    // So: only send the header when the URL doesn't already encode the
    // checksum as a query param. This keeps us compatible with both
    // signing styles (query-param-encoded, the default for AWS SDK v3, and
    // header-signed, which an older signer might still produce).
    const headers = {};
    const urlHasChecksum = (() => {
        try {
            return new URL(url).searchParams.has("x-amz-checksum-sha256");
        }
        catch {
            return false;
        }
    })();
    if (!urlHasChecksum) {
        headers["x-amz-checksum-sha256"] = checksumBase64;
    }
    void checksumBase64; // silence unused-var if both branches skip the header
    let res;
    try {
        res = await fetchFn(url, {
            method: "PUT",
            headers,
            body: body,
        });
    }
    catch (err) {
        throw new Run402DeployError(`S3 PUT failed for part ${partNumber}: ${err.message}`, {
            code: "CONTENT_UPLOAD_FAILED",
            phase: "upload",
            retryable: true,
            context: "uploading deploy bytes",
        });
    }
    if (!res.ok) {
        const text = await res.text().catch(() => "");
        throw new Run402DeployError(`S3 PUT failed for part ${partNumber} (HTTP ${res.status})${text ? ": " + text.slice(0, 200) : ""}`, {
            code: "CONTENT_UPLOAD_FAILED",
            phase: "upload",
            retryable: res.status >= 500 || res.status === 403,
            status: res.status,
            body: text,
            context: "uploading deploy bytes",
        });
    }
    return res.headers.get("etag");
}
async function pollUntilReady(client, commit, diff, warnings, emit, projectId, sliceKinds = []) {
    if (commit.status === "failed") {
        throw translateGatewayError(commit.error, "commit", null, commit.operation_id);
    }
    if (commit.status === "ready") {
        if (!commit.release_id || !commit.urls) {
            throw new Run402DeployError("Commit returned ready but no release_id/urls", {
                code: "INTERNAL_ERROR",
                phase: "ready",
                retryable: false,
                operationId: commit.operation_id,
                context: "committing deploy",
            });
        }
        if (projectId) {
            const headers = await apikeyHeaders(client, projectId);
            await drainRecordedOperationEvents(client, commit.operation_id, headers, null, emit);
        }
        emit({
            type: "ready",
            releaseId: commit.release_id,
            urls: commit.urls,
            ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
        });
        return {
            release_id: commit.release_id,
            operation_id: commit.operation_id,
            urls: commit.urls,
            diff,
            warnings: [...warnings.filter(w => !commit.warnings?.some(updated => updated.code === w.code)), ...(commit.warnings ?? [])],
            ...(commit.subdomain_bindings ? { subdomain_bindings: commit.subdomain_bindings } : {}),
            ...(commit.static_continuity ? { static_continuity: commit.static_continuity } : {}),
            ...(commit.edge ? { edge: commit.edge } : {}),
            // The gateway's riders (poll / watch_errors / hand_to_member) ride the
            // synchronous ready response and nowhere else — dropping them here is
            // how an agent never learns the offer existed (builder-promotion).
            ...(Array.isArray(commit.next_actions) && commit.next_actions.length > 0
                ? { next_actions: commit.next_actions }
                : {}),
        };
    }
    const opHeaders = projectId ? await apikeyHeaders(client, projectId) : {};
    const initialSnapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(commit.operation_id)}`, { headers: opHeaders, context: "fetching deploy operation" });
    return await pollSnapshotUntilReady(client, initialSnapshot, diff, warnings, emit, projectId, sliceKinds);
}
async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, projectId, sliceKinds = []) {
    // Helper to spread slice_kinds onto every commit.phase / ready emit so
    // agents grouping per-slice telemetry don't need to track the apply's
    // spec separately. The low-level commit/upload helpers that pass no
    // sliceKinds get an empty array → field is omitted from events.
    const withSliceKinds = (ev) => sliceKinds.length > 0
        ? { ...ev, slice_kinds: sliceKinds }
        : ev;
    let snapshot = initial;
    const opHeaders = projectId ? await apikeyHeaders(client, projectId) : {};
    let lastPhaseEmitted = null;
    let recordedEventCursor = null;
    let recordedEventsSupported = Boolean(projectId);
    const start = Date.now();
    let interval = COMMIT_POLL_INITIAL_MS;
    const phaseFor = (status) => {
        const map = {
            staging: { type: "commit.phase", phase: "stage", status: "started" },
            gating: { type: "commit.phase", phase: "migrate-gate", status: "started" },
            migrating: { type: "commit.phase", phase: "migrate", status: "started" },
            schema_settling: {
                type: "commit.phase",
                phase: "schema-settle",
                status: "started",
            },
            activating: { type: "commit.phase", phase: "activate", status: "started" },
            activation_pending: {
                type: "commit.phase",
                phase: "activate",
                status: "failed",
            },
        };
        return map[status] ?? null;
    };
    // Close out the previously-emitted phase as `done` (or `failed`) before
    // emitting the next phase's `started` event. Skips when there's no prior
    // phase, when the prior emission wasn't a `started` event (e.g. the
    // `activation_pending` path which already emits `failed`), or when the
    // prior phase string equals the next phase.
    const closePreviousPhase = (nextPhase, closeStatus = "done") => {
        if (lastPhaseEmitted === null)
            return;
        const prev = phaseFor(lastPhaseEmitted);
        if (!prev || prev.type !== "commit.phase")
            return;
        if (prev.status !== "started")
            return;
        if (nextPhase !== undefined && prev.phase === nextPhase)
            return;
        emit(withSliceKinds({ type: "commit.phase", phase: prev.phase, status: closeStatus }));
    };
    while (true) {
        if (recordedEventsSupported && projectId) {
            const recorded = await emitRecordedOperationEvents(client, snapshot.operation_id, opHeaders, recordedEventCursor, emit);
            recordedEventsSupported = recorded.supported;
            recordedEventCursor = recorded.cursor;
        }
        if (lastPhaseEmitted !== snapshot.status) {
            const ev = phaseFor(snapshot.status);
            if (ev) {
                if (ev.type === "commit.phase")
                    closePreviousPhase(ev.phase);
                emit(withSliceKinds(ev));
                lastPhaseEmitted = snapshot.status;
            }
            // If `ev` is null (status not in the phase map, e.g. "ready"), leave
            // lastPhaseEmitted pointing at the prior in-flight phase so the
            // terminal-success closePreviousPhase() below can emit its `done`.
        }
        if (snapshot.status === SUCCESS_STATUS) {
            if (!snapshot.release_id || !snapshot.urls) {
                throw new Run402DeployError("Operation reached ready but no release_id/urls available", {
                    code: "INTERNAL_ERROR",
                    phase: "ready",
                    retryable: false,
                    operationId: snapshot.operation_id,
                    context: "polling deploy",
                });
            }
            if (recordedEventsSupported && projectId) {
                const recorded = await drainRecordedOperationEvents(client, snapshot.operation_id, opHeaders, recordedEventCursor, emit);
                recordedEventsSupported = recorded.supported;
                recordedEventCursor = recorded.cursor;
            }
            closePreviousPhase();
            emit(withSliceKinds({ type: "ready", releaseId: snapshot.release_id, urls: snapshot.urls }));
            return {
                release_id: snapshot.release_id,
                operation_id: snapshot.operation_id,
                urls: snapshot.urls,
                diff,
                warnings: [...warnings.filter(w => !snapshot.warnings?.some(updated => updated.code === w.code)), ...(snapshot.warnings ?? [])],
                ...(snapshot.subdomain_bindings ? { subdomain_bindings: snapshot.subdomain_bindings } : {}),
                ...(snapshot.static_continuity ? { static_continuity: snapshot.static_continuity } : {}),
                ...(snapshot.edge ? { edge: snapshot.edge } : {}),
                // The gateway now attaches the same riders to a ready operation
                // snapshot as to a synchronous ready commit (poll / watch_errors /
                // hand_to_member) — pass them through exactly as the fast path does.
                ...(Array.isArray(snapshot.next_actions) && snapshot.next_actions.length > 0
                    ? { next_actions: snapshot.next_actions }
                    : {}),
            };
        }
        if (TERMINAL_STATUSES.includes(snapshot.status)) {
            closePreviousPhase(undefined, "failed");
            throw translateGatewayError(snapshot.error, snapshot.status, snapshot.plan_id, snapshot.operation_id);
        }
        if (snapshot.status === "activation_pending" &&
            isTerminalStaticActivationError(snapshot.error)) {
            closePreviousPhase(undefined, "failed");
            throw translateGatewayError(snapshot.error, "activate", snapshot.plan_id, snapshot.operation_id);
        }
        if (Date.now() - start > COMMIT_POLL_TIMEOUT_MS) {
            throw new Run402DeployError(`Timed out waiting for operation ${snapshot.operation_id} to reach ready`, {
                code: "INTERNAL_ERROR",
                phase: snapshot.status,
                retryable: true,
                operationId: snapshot.operation_id,
                status: 504,
                context: "polling deploy",
            });
        }
        await sleep(interval);
        if (Date.now() - start > COMMIT_POLL_BACKOFF_AFTER_MS) {
            interval = Math.min(Math.floor(interval * 1.5), COMMIT_POLL_MAX_MS);
        }
        snapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(snapshot.operation_id)}`, { headers: opHeaders, context: "polling deploy operation" });
    }
}
async function drainRecordedOperationEvents(client, operationId, headers, cursor, emit) {
    let nextCursor = cursor;
    for (let i = 0; i < 10; i += 1) {
        const recorded = await emitRecordedOperationEvents(client, operationId, headers, nextCursor, emit);
        nextCursor = recorded.cursor;
        if (!recorded.supported || !recorded.hasMore) {
            return { cursor: nextCursor, supported: recorded.supported };
        }
    }
    return { cursor: nextCursor, supported: true };
}
async function emitRecordedOperationEvents(client, operationId, headers, cursor, emit) {
    const query = cursor
        ? `?limit=100&cursor=${encodeURIComponent(cursor)}`
        : "?limit=100";
    let page;
    try {
        page = await client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}/events${query}`, { headers, context: "fetching deploy operation events" });
    }
    catch {
        return { cursor, supported: false, hasMore: false };
    }
    if (!page || !Array.isArray(page.events)) {
        return { cursor, supported: false, hasMore: false };
    }
    let nextCursor = cursor;
    for (const event of page.events) {
        const maybeRecorded = event;
        if (maybeRecorded.id === "synthetic")
            continue;
        emit(event);
        const eventId = maybeRecorded.id;
        if (typeof eventId === "string" && /^\d+$/.test(eventId)) {
            nextCursor = eventId;
        }
    }
    return { cursor: nextCursor, supported: true, hasMore: typeof page.cursor === "string" && page.cursor.length > 0 };
}
function isTerminalStaticActivationError(error) {
    if (!error?.code)
        return false;
    if (error.retryable === false)
        return true;
    if (error.safe_to_retry === false)
        return true;
    return STATIC_ACTIVATION_FAILURE_CODES.has(error.code.toUpperCase());
}
// ─── start() implementation ──────────────────────────────────────────────────
async function startInternal(client, spec, opts) {
    const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
    const buffered = [];
    const subscribers = [];
    const emit = (event) => {
        buffered.push(event);
        if (opts.onEvent) {
            try {
                opts.onEvent(event);
            }
            catch {
                /* swallow */
            }
        }
        for (const fn of subscribers) {
            try {
                fn(event);
            }
            catch {
                /* swallow */
            }
        }
    };
    emit({ type: "plan.started" });
    const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey, {
        requiredPlan: opts.requiredPlan,
    });
    emit({ type: "plan.diff", diff: plan.diff });
    emitPlanWarnings(plan, emit);
    if (!opts.requiredPlan)
        abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
    if (plan.payment_required) {
        emit({
            type: "payment.required",
            amount: plan.payment_required.amount,
            asset: plan.payment_required.asset,
            payTo: plan.payment_required.payTo,
            reason: plan.payment_required.reason,
        });
    }
    const sliceKinds = deriveSliceKinds(spec);
    const resultPromise = (async () => {
        await uploadMissing(client, spec.project_id, plan.missing_content, byteReaders, emit);
        emit({
            type: "commit.phase",
            phase: "validate",
            status: "started",
            ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
        });
        const { planId } = requirePersistedPlan(plan, "starting deploy");
        const commit = requireCloudCommitResponse(await commitInternal(client, planId, opts.idempotencyKey, spec.project_id, opts.requiredPlan), "starting deploy");
        return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project_id, sliceKinds);
    })();
    // Avoid an unhandled-rejection at construction time. Consumers must call
    // .result() to actually observe the error.
    resultPromise.catch(() => { });
    let snapshot = null;
    const { operationId } = requirePersistedPlan(plan, "starting deploy");
    const startHeaders = await apikeyHeaders(client, spec.project_id);
    const fetchSnapshot = async () => {
        if (snapshot && TERMINAL_STATUSES.includes(snapshot.status))
            return snapshot;
        snapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(operationId)}`, { headers: startHeaders, context: "fetching deploy operation" });
        return snapshot;
    };
    return {
        id: operationId,
        async snapshot() {
            return fetchSnapshot();
        },
        async result() {
            return resultPromise;
        },
        events() {
            return {
                [Symbol.asyncIterator]() {
                    const queue = [...buffered];
                    let resolveNext = null;
                    let done = false;
                    // If a terminal event was already buffered before this iterator
                    // attached (e.g. iteration starts after `await op.result()`
                    // resolved), we'll go done after the queue drains. Without this,
                    // late iteration would hang forever waiting for an emit that
                    // will never come.
                    const terminalAlreadyBuffered = buffered.some((ev) => ev.type === "ready");
                    const subscriber = (ev) => {
                        const waiter = resolveNext;
                        if (waiter) {
                            resolveNext = null;
                            waiter({ value: ev, done: false });
                        }
                        else {
                            queue.push(ev);
                        }
                        if (ev.type === "ready") {
                            done = true;
                            // The next() loop checks `done` after queue drain, so a
                            // pending waiter that was just satisfied above will see
                            // `done` on its next call. No second wake-up needed here.
                        }
                    };
                    subscribers.push(subscriber);
                    // Wake up on either success or failure of the result promise
                    // so an iterator attached after termination always exits.
                    const finalize = () => {
                        done = true;
                        if (resolveNext) {
                            const r = resolveNext;
                            resolveNext = null;
                            r({ value: undefined, done: true });
                        }
                    };
                    resultPromise.then(finalize, finalize);
                    return {
                        next() {
                            if (queue.length > 0) {
                                return Promise.resolve({ value: queue.shift(), done: false });
                            }
                            // After queue drain, if we already saw the terminal event
                            // in the initial buffer (or the result promise has
                            // resolved/rejected), we're done.
                            if (done || terminalAlreadyBuffered) {
                                return Promise.resolve({
                                    value: undefined,
                                    done: true,
                                });
                            }
                            return new Promise((resolve) => {
                                resolveNext = resolve;
                            });
                        },
                        return() {
                            done = true;
                            const idx = subscribers.indexOf(subscriber);
                            if (idx >= 0)
                                subscribers.splice(idx, 1);
                            return Promise.resolve({
                                value: undefined,
                                done: true,
                            });
                        },
                    };
                },
            };
        },
    };
}
const RELEASE_SPEC_FIELDS = new Set([
    "$schema",
    "project_id",
    "base",
    "database",
    "secrets",
    "functions",
    "site",
    "subdomains",
    "routes",
    "checks",
    "assets", // v1.48 unified-apply
    "i18n", // v2.5 routed-locale-context
]);
const DEPLOYABLE_SPEC_FIELDS = [
    "assets", // v1.48 unified-apply
    "database",
    "site",
    "functions",
    "secrets",
    "subdomains",
    "routes",
    "checks",
    "i18n", // v2.5 routed-locale-context
];
const BASE_SPEC_FIELDS = new Set(["release", "release_id"]);
const DATABASE_SPEC_FIELDS = new Set(["migrations", "expose", "zero_downtime"]);
const MIGRATION_SPEC_FIELDS = new Set(["id", "name", "checksum", "sql", "sql_ref", "transaction"]);
const FUNCTIONS_SPEC_FIELDS = new Set(["replace", "patch"]);
const FUNCTIONS_PATCH_FIELDS = new Set(["set", "delete"]);
const FUNCTION_SPEC_FIELDS = new Set([
    "runtime",
    "source",
    "files",
    "entrypoint",
    "config",
    "deps",
    "triggers",
    "schedule",
    "requireAuth",
    "requireRole",
    "class",
    "capabilities",
]);
const FUNCTION_CONFIG_FIELDS = new Set(["timeoutSeconds", "memoryMb"]);
const FUNCTION_TRIGGER_FIELDS = new Set(["id", "type", "cron", "timezone", "misfire_policy", "overlap_policy", "mailbox", "events", "run"]);
const FUNCTION_TRIGGER_RUN_FIELDS = new Set(["event_type", "payload", "retry", "expires_after_seconds"]);
const SITE_SPEC_FIELDS = new Set(["replace", "patch", "public_paths", "embedding"]);
const SITE_PATCH_FIELDS = new Set(["put", "delete"]);
const SITE_PUBLIC_PATHS_FIELDS = new Set(["mode", "replace"]);
const PUBLIC_STATIC_PATH_FIELDS = new Set(["asset", "cache_class"]);
const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
const ROUTES_SPEC_FIELDS = new Set(["replace"]);
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target", "pricing", "acknowledge_readonly"]);
const FUNCTION_ROUTE_TARGET_FIELDS = new Set(["type", "name"]);
const STATIC_ROUTE_TARGET_FIELDS = new Set(["type", "file"]);
const ROUTE_METHOD_SET = new Set(ROUTE_HTTP_METHODS);
const ROUTE_PRICING_FIELDS = new Set([
    "mode",
    "amount_usd_micros",
    "pay_to",
    "networks",
    "receipt",
]);
const ROUTE_PRICING_NETWORK_SET = new Set(ROUTE_PRICING_NETWORKS);
const I18N_SPEC_FIELDS = new Set(["defaultLocale", "locales", "detect", "unknownLocalePolicy"]);
const I18N_LOCALE_TAG_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
const I18N_COOKIE_NAME_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
const I18N_MAX_LOCALES = 50;
const I18N_MAX_DETECT_SOURCES = 10;
const MIGRATION_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
function validateSpec(spec) {
    if (!spec || typeof spec !== "object") {
        throw new Run402DeployError("ReleaseSpec must be an object", {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: "spec",
            retryable: false,
            fix: { action: "set_field", path: "" },
            context: "validating spec",
        });
    }
    const raw = spec;
    validateKnownFields(raw, "spec", RELEASE_SPEC_FIELDS, {
        subdomain: "Use `subdomains: { set: [name] }`.",
    });
    if (!spec.project_id || typeof spec.project_id !== "string") {
        throw new Run402DeployError("ReleaseSpec.project_id is required", {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: "spec.project_id",
            retryable: false,
            fix: { action: "set_field", path: "project_id" },
            context: "validating spec",
        });
    }
    validateBaseSpec(raw.base);
    validateDatabaseSpec(raw.database);
    validateFunctionsSpec(raw.functions);
    validateSiteSpec(raw.site);
    validateSubdomainsSpec(raw.subdomains);
    validateRoutesSpec(raw.routes);
    validateChecksSpec(raw.checks);
    validateSecretsSpec(raw.secrets);
    validateI18nSpec(raw.i18n);
    const subdomains = raw.subdomains;
    const set = subdomains?.set;
    if (set && set.length > 1) {
        throw new Run402DeployError("subdomains.set accepts at most one subdomain per project; multi-subdomain support is not yet available", {
            code: "SUBDOMAIN_MULTI_NOT_SUPPORTED",
            phase: "validate",
            resource: "subdomains.set",
            retryable: false,
            fix: { action: "set_field", path: "subdomains.set" },
            context: "validating spec",
        });
    }
    if (!hasDeployableContent(raw)) {
        throw new Run402DeployError(`ReleaseSpec contains no deployable sections. Expected at least one non-empty section: ${DEPLOYABLE_SPEC_FIELDS.join(", ")}`, {
            code: "MANIFEST_EMPTY",
            phase: "validate",
            resource: "spec",
            retryable: false,
            fix: { action: "set_field", path: "site.replace" },
            body: { deployable_fields: DEPLOYABLE_SPEC_FIELDS },
            context: "validating spec",
        });
    }
}
function validateBaseSpec(base) {
    if (base === undefined)
        return;
    const obj = requireObject(base, "base");
    validateKnownFields(obj, "base", BASE_SPEC_FIELDS);
    if (hasOwn(obj, "release") && hasOwn(obj, "release_id")) {
        throw invalidSpec("ReleaseSpec.base must use either release or release_id, not both", "base");
    }
}
function validateDatabaseSpec(database) {
    if (database === undefined)
        return;
    const obj = requireObject(database, "database");
    validateKnownFields(obj, "database", DATABASE_SPEC_FIELDS);
    if (obj.migrations !== undefined) {
        if (!Array.isArray(obj.migrations)) {
            throw invalidSpec("ReleaseSpec.database.migrations must be an array", "database.migrations");
        }
        const seenNames = new Map();
        for (const [index, migration] of obj.migrations.entries()) {
            const m = requireObject(migration, `database.migrations.${index}`);
            validateKnownFields(m, `database.migrations.${index}`, MIGRATION_SPEC_FIELDS);
            validateMigrationIdentity(m, index, seenNames);
        }
    }
    if (obj.expose !== undefined) {
        requireObject(obj.expose, "database.expose");
    }
}
function validateMigrationIdentity(migration, index, seenNames) {
    const resource = `database.migrations.${index}`;
    const hasId = hasOwn(migration, "id") && migration.id !== undefined;
    const hasName = hasOwn(migration, "name") && migration.name !== undefined;
    if (hasId && hasName) {
        throw invalidSpec(`ReleaseSpec.${resource} declares both id and name; use exactly one`, resource);
    }
    if (!hasId && !hasName) {
        throw invalidSpec(`ReleaseSpec.${resource} must declare exactly one of id or name`, resource);
    }
    if (hasName) {
        if (typeof migration.name !== "string" || !MIGRATION_NAME_RE.test(migration.name)) {
            throw invalidSpec(`ReleaseSpec.${resource}.name must match /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/`, `${resource}.name`);
        }
        const firstIndex = seenNames.get(migration.name);
        if (firstIndex !== undefined) {
            throw invalidSpec(`ReleaseSpec.${resource}.name duplicates database.migrations.${firstIndex}.name ${JSON.stringify(migration.name)}`, `${resource}.name`);
        }
        seenNames.set(migration.name, index);
    }
}
function validateFunctionsSpec(functions) {
    if (functions === undefined)
        return;
    const obj = requireObject(functions, "functions");
    validateKnownFields(obj, "functions", FUNCTIONS_SPEC_FIELDS);
    if (obj.replace !== undefined) {
        validateFunctionMap(obj.replace, "functions.replace");
    }
    if (obj.patch !== undefined) {
        const patch = requireObject(obj.patch, "functions.patch");
        validateKnownFields(patch, "functions.patch", FUNCTIONS_PATCH_FIELDS);
        if (patch.set !== undefined)
            validateFunctionMap(patch.set, "functions.patch.set");
        if (patch.delete !== undefined)
            validateStringArray(patch.delete, "functions.patch.delete");
    }
}
function validateFunctionMap(value, resource) {
    const map = requireObject(value, resource);
    for (const [name, fn] of Object.entries(map)) {
        const entry = requireObject(fn, `${resource}.${name}`);
        validateKnownFields(entry, `${resource}.${name}`, FUNCTION_SPEC_FIELDS);
        if (entry.config !== undefined) {
            const config = requireObject(entry.config, `${resource}.${name}.config`);
            validateKnownFields(config, `${resource}.${name}.config`, FUNCTION_CONFIG_FIELDS);
            validateFunctionConfigInteger(config.timeoutSeconds, `${resource}.${name}.config.timeoutSeconds`);
            validateFunctionConfigInteger(config.memoryMb, `${resource}.${name}.config.memoryMb`);
        }
        if (entry.files !== undefined) {
            requireObject(entry.files, `${resource}.${name}.files`);
        }
        validateFunctionTriggers(entry.triggers, `${resource}.${name}.triggers`);
    }
}
function validateFunctionTriggers(value, resource) {
    if (value === undefined)
        return;
    if (!Array.isArray(value))
        throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
    const seen = new Set();
    for (const [index, trigger] of value.entries()) {
        const path = `${resource}.${index}`;
        const obj = requireObject(trigger, path);
        validateKnownFields(obj, path, FUNCTION_TRIGGER_FIELDS, {
            delivery: "Remove delivery; schedule triggers always start durable function runs.",
        });
        if (typeof obj.id !== "string" || obj.id.trim() === "") {
            throw invalidSpec(`ReleaseSpec.${path}.id is required for function triggers`, `${path}.id`);
        }
        if (seen.has(obj.id))
            throw invalidSpec(`ReleaseSpec.${path}.id duplicates ${JSON.stringify(obj.id)}`, `${path}.id`);
        seen.add(obj.id);
        if (obj.type === "schedule") {
            if (obj.mailbox !== undefined || obj.events !== undefined) {
                throw invalidSpec(`ReleaseSpec.${path} schedule triggers cannot include mailbox or events`, path);
            }
            if (typeof obj.cron !== "string" || obj.cron.trim().split(/\s+/).length !== 5) {
                throw invalidSpec(`ReleaseSpec.${path}.cron must be a 5-field cron expression`, `${path}.cron`);
            }
            if (obj.timezone !== undefined && typeof obj.timezone !== "string") {
                throw invalidSpec(`ReleaseSpec.${path}.timezone must be a string`, `${path}.timezone`);
            }
            if (obj.misfire_policy !== undefined && obj.misfire_policy !== "skip") {
                throw invalidSpec(`ReleaseSpec.${path}.misfire_policy must be "skip"`, `${path}.misfire_policy`);
            }
            if (obj.overlap_policy !== undefined && obj.overlap_policy !== "allow") {
                throw invalidSpec(`ReleaseSpec.${path}.overlap_policy must be "allow"`, `${path}.overlap_policy`);
            }
        }
        else if (obj.type === "email") {
            if (obj.cron !== undefined ||
                obj.timezone !== undefined ||
                obj.misfire_policy !== undefined ||
                obj.overlap_policy !== undefined) {
                throw invalidSpec(`ReleaseSpec.${path} email triggers cannot include schedule fields`, path);
            }
            if (typeof obj.mailbox !== "string" || obj.mailbox.trim() === "") {
                throw invalidSpec(`ReleaseSpec.${path}.mailbox is required`, `${path}.mailbox`);
            }
            if (!Array.isArray(obj.events) || obj.events.length === 0) {
                throw invalidSpec(`ReleaseSpec.${path}.events must be a non-empty array`, `${path}.events`);
            }
            for (const [eventIndex, event] of obj.events.entries()) {
                if (typeof event !== "string" || !EMAIL_TRIGGER_EVENTS.has(event)) {
                    throw invalidSpec(`ReleaseSpec.${path}.events.${eventIndex} must be one of reply_received, delivery, bounced, complained`, `${path}.events.${eventIndex}`);
                }
            }
        }
        else {
            throw invalidSpec(`ReleaseSpec.${path}.type must be "schedule" or "email"`, `${path}.type`);
        }
        const run = requireObject(obj.run, `${path}.run`);
        validateKnownFields(run, `${path}.run`, FUNCTION_TRIGGER_RUN_FIELDS);
        if (typeof run.event_type !== "string" || run.event_type.trim() === "") {
            throw invalidSpec(`ReleaseSpec.${path}.run.event_type is required`, `${path}.run.event_type`);
        }
        if (run.payload !== undefined)
            requireObject(run.payload, `${path}.run.payload`);
        if (run.retry !== undefined)
            requireObject(run.retry, `${path}.run.retry`);
        if (run.expires_after_seconds !== undefined &&
            (typeof run.expires_after_seconds !== "number" ||
                !Number.isSafeInteger(run.expires_after_seconds) ||
                run.expires_after_seconds <= 0)) {
            throw invalidSpec(`ReleaseSpec.${path}.run.expires_after_seconds must be a positive integer`, `${path}.run.expires_after_seconds`);
        }
    }
}
/**
 * Detect a site path key that belongs to the `@run402/astro` SSR adapter's
 * build tree rather than to deployable static content. The adapter writes
 * `dist/run402/{adapter.json, server/**, client/**}`; only `client/**` is
 * servable. `adapter.json` and `server/**` are build internals — their
 * presence in a site spec means the caller rooted their file source at the
 * build root (`dist/`) instead of `dist/run402/client/`.
 */
function isAstroAdapterTreeSitePath(path) {
    return path === "run402/adapter.json" || path.startsWith("run402/server/");
}
/**
 * Return the synchronously-knowable keys of a site file container. Plain
 * path-keyed `FileSet`s expose their keys directly; a `LocalDirRef`
 * (`dir(path)`) or any future source sentinel carries an `__source` marker
 * and is only knowable after expansion — those return `[]` here and are
 * re-checked post-normalization.
 */
function siteFileSetKeysForGuard(container) {
    if (!container ||
        typeof container !== "object" ||
        Array.isArray(container) ||
        container.__source !== undefined) {
        return [];
    }
    return Object.keys(container);
}
/**
 * Reject a site slice that ships the `@run402/astro` adapter build tree as
 * static content. The mis-rooting: a deploy points
 * `fileSetFromDir`/`dir()` at `dist/` (not `dist/run402/client/`),
 * so every page landed under a `run402/client/` path prefix while
 * `run402/adapter.json` + `run402/server/**` leaked in as assets — producing a
 * release that 404'd every URL and exposed the SSR bundle. Fail fast, locally,
 * with the fix, before any CAS upload or plan.
 */
function assertNoAstroAdapterTreeInSite(paths, resource) {
    const offenders = [];
    for (const p of paths) {
        if (isAstroAdapterTreeSitePath(p))
            offenders.push(p);
        if (offenders.length >= 3)
            break;
    }
    if (offenders.length === 0)
        return;
    throw new Run402DeployError(`${resource} ships the @run402/astro adapter build tree (e.g. ${offenders
        .map((p) => `\`${p}\``)
        .join(", ")}) as static content. Only \`dist/run402/client/\` is deployable; ` +
        `\`run402/adapter.json\` and \`run402/server/**\` are build internals. You likely ` +
        `rooted your file source at the build root (\`dist/\`) instead of \`dist/run402/client/\`. ` +
        `Use \`buildAstroReleaseSlice("dist")\` from @run402/astro (it roots the site and bundles ` +
        `the SSR function correctly), or point your dir at \`dist/run402/client\`.`, {
        code: "ASTRO_ADAPTER_TREE_IN_SITE",
        phase: "validate",
        resource,
        retryable: false,
        fix: {
            action: "reroot_site_to_astro_client_dir",
            path: resource,
            expected_dir: "dist/run402/client",
        },
        context: "validating spec",
    });
}
function validateSiteSpec(site) {
    if (site === undefined)
        return;
    const obj = requireObject(site, "site");
    validateKnownFields(obj, "site", SITE_SPEC_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.",
    });
    if (hasOwn(obj, "replace") && hasOwn(obj, "patch")) {
        throw invalidSpec("ReleaseSpec.site must use either replace or patch, not both", "site");
    }
    if (obj.replace !== undefined) {
        requireObject(obj.replace, "site.replace");
        assertNoAstroAdapterTreeInSite(siteFileSetKeysForGuard(obj.replace), "site.replace");
    }
    if (obj.patch !== undefined) {
        const patch = requireObject(obj.patch, "site.patch");
        validateKnownFields(patch, "site.patch", SITE_PATCH_FIELDS);
        if (patch.put !== undefined) {
            requireObject(patch.put, "site.patch.put");
            assertNoAstroAdapterTreeInSite(siteFileSetKeysForGuard(patch.put), "site.patch.put");
        }
        if (patch.delete !== undefined)
            validateStringArray(patch.delete, "site.patch.delete");
    }
    if (obj.public_paths !== undefined) {
        validateSitePublicPathsSpec(obj.public_paths, "site.public_paths");
    }
    if (obj.embedding !== undefined) {
        validateSiteEmbeddingSpec(obj.embedding, "site.embedding");
    }
}
/** tenant-site-embedding. Structural check only: the catalog is the gateway's
 *  (it names the valid keys in INVALID_SPEC), so a new key never needs an SDK
 *  release. `null` clears; omitted carries forward. */
function validateSiteEmbeddingSpec(value, resource) {
    if (value === null)
        return;
    const obj = requireObject(value, resource);
    validateKnownFields(obj, resource, new Set(["frame_ancestors"]));
    if (!Array.isArray(obj.frame_ancestors) || obj.frame_ancestors.length === 0) {
        throw invalidSpec(`ReleaseSpec.${resource}.frame_ancestors must be a non-empty array of embedding catalog keys (e.g. ["localhost"]); omit ${resource} to carry the previous declaration forward or send null to deny`, `${resource}.frame_ancestors`);
    }
    validateStringArray(obj.frame_ancestors, `${resource}.frame_ancestors`);
}
function validateSitePublicPathsSpec(value, resource) {
    const obj = requireObject(value, resource);
    validateKnownFields(obj, resource, SITE_PUBLIC_PATHS_FIELDS);
    if (obj.mode !== "implicit" && obj.mode !== "explicit") {
        throw invalidSpec(`ReleaseSpec.${resource}.mode must be "implicit" or "explicit"`, `${resource}.mode`);
    }
    if (obj.mode === "implicit") {
        if (hasOwn(obj, "replace")) {
            throw invalidSpec("ReleaseSpec.site.public_paths.replace is not allowed when mode is implicit", `${resource}.replace`);
        }
        return;
    }
    if (!hasOwn(obj, "replace")) {
        throw invalidSpec("ReleaseSpec.site.public_paths with mode explicit requires a complete public_paths.replace map", `${resource}.replace`);
    }
    const replace = requireObject(obj.replace, `${resource}.replace`);
    for (const [publicPath, entry] of Object.entries(replace)) {
        const entryResource = `${resource}.replace.${publicPath}`;
        const pathSpec = requireObject(entry, entryResource);
        validateKnownFields(pathSpec, entryResource, PUBLIC_STATIC_PATH_FIELDS);
        if (typeof pathSpec.asset !== "string" || pathSpec.asset.length === 0) {
            throw invalidSpec(`ReleaseSpec.${entryResource}.asset must be a non-empty release static asset path`, `${entryResource}.asset`);
        }
        if (pathSpec.cache_class !== undefined && typeof pathSpec.cache_class !== "string") {
            throw invalidSpec(`ReleaseSpec.${entryResource}.cache_class must be a string`, `${entryResource}.cache_class`);
        }
    }
}
function validateSubdomainsSpec(subdomains) {
    if (subdomains === undefined)
        return;
    const obj = requireObject(subdomains, "subdomains");
    validateKnownFields(obj, "subdomains", SUBDOMAINS_SPEC_FIELDS);
    if (obj.set !== undefined)
        validateStringArray(obj.set, "subdomains.set");
    if (obj.add !== undefined)
        validateStringArray(obj.add, "subdomains.add");
    if (obj.remove !== undefined)
        validateStringArray(obj.remove, "subdomains.remove");
}
function validateRoutesSpec(routes) {
    if (routes === undefined)
        return;
    if (routes === null)
        return;
    const obj = requireObject(routes, "routes");
    for (const key of Object.keys(obj)) {
        if (key.startsWith("/")) {
            throw invalidRouteSpec(`Unknown ReleaseSpec field: routes.${key}. ${routeShapeHints(obj)[key]}`, `routes.${key}`);
        }
    }
    validateKnownFields(obj, "routes", ROUTES_SPEC_FIELDS, routeShapeHints(obj));
    if (!hasOwn(obj, "replace")) {
        throw invalidRouteSpec("ReleaseSpec.routes must be null or { replace: [{ pattern, target: { type: \"function\", name } | { type: \"static\", file } }] }. Path-keyed route maps are not supported.", "routes");
    }
    if (!Array.isArray(obj.replace)) {
        throw invalidRouteSpec("ReleaseSpec.routes.replace must be an array of route entries", "routes.replace");
    }
    for (const [index, route] of obj.replace.entries()) {
        validateRouteEntry(route, `routes.replace.${index}`);
    }
}
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 validateRouteEntry(route, resource) {
    const entry = requireObject(route, resource);
    validateKnownFields(entry, resource, ROUTE_ENTRY_FIELDS);
    if (typeof entry.pattern !== "string" || entry.pattern.length === 0) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pattern must be a non-empty string`, `${resource}.pattern`);
    }
    if (entry.methods !== undefined) {
        if (!Array.isArray(entry.methods)) {
            throw invalidRouteSpec(`ReleaseSpec.${resource}.methods must be an array of HTTP methods`, `${resource}.methods`);
        }
        if (entry.methods.length === 0) {
            throw invalidRouteSpec(`ReleaseSpec.${resource}.methods must not be empty; omit methods to allow all supported methods`, `${resource}.methods`);
        }
        for (const method of entry.methods) {
            if (typeof method !== "string" || !ROUTE_METHOD_SET.has(method)) {
                throw invalidRouteSpec(`Unsupported route method ${JSON.stringify(method)} at ReleaseSpec.${resource}.methods. Supported methods: ${ROUTE_HTTP_METHODS.join(", ")}`, `${resource}.methods`);
            }
        }
        const seen = new Set();
        for (const method of entry.methods) {
            const methodString = method;
            if (seen.has(methodString)) {
                throw invalidRouteSpec(`ReleaseSpec.${resource}.methods contains duplicate method ${JSON.stringify(method)}`, `${resource}.methods`);
            }
            seen.add(methodString);
        }
    }
    const targetType = validateRouteTarget(entry.target, `${resource}.target`);
    validateRoutePricing(entry.pricing, resource, targetType);
    validateRouteReadOnlyAcknowledgement(entry, targetType, resource);
    if (targetType === "static") {
        validateStaticRouteEntry(entry, resource);
    }
}
function validateRoutePricing(pricing, resource, targetType) {
    if (pricing === undefined)
        return;
    if (targetType !== "function") {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing is only supported on function route targets`, `${resource}.pricing`);
    }
    const obj = requireObject(pricing, `${resource}.pricing`);
    validateKnownFields(obj, `${resource}.pricing`, ROUTE_PRICING_FIELDS);
    if (obj.mode !== "always") {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.mode must be "always"`, `${resource}.pricing.mode`);
    }
    if (typeof obj.amount_usd_micros !== "number" || !Number.isSafeInteger(obj.amount_usd_micros) || obj.amount_usd_micros <= 0) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.amount_usd_micros must be a positive safe JSON integer`, `${resource}.pricing.amount_usd_micros`);
    }
    if (obj.pay_to !== "org_default_payout") {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.pay_to must be "org_default_payout"`, `${resource}.pricing.pay_to`);
    }
    if (obj.receipt !== undefined &&
        obj.receipt !== "on_fulfillment") {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.receipt must be "on_fulfillment"`, `${resource}.pricing.receipt`);
    }
    if (obj.networks === undefined)
        return;
    if (!Array.isArray(obj.networks)) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.networks must be an array`, `${resource}.pricing.networks`);
    }
    if (obj.networks.length === 0) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.networks must not be empty; omit networks for production mainnet only`, `${resource}.pricing.networks`);
    }
    const seen = new Set();
    for (const network of obj.networks) {
        if (typeof network !== "string" || !ROUTE_PRICING_NETWORK_SET.has(network)) {
            throw invalidRouteSpec(`Unsupported route pricing network ${JSON.stringify(network)} at ReleaseSpec.${resource}.pricing.networks. Supported networks: ${ROUTE_PRICING_NETWORKS.join(", ")}`, `${resource}.pricing.networks`);
        }
        if (seen.has(network)) {
            throw invalidRouteSpec(`ReleaseSpec.${resource}.pricing.networks contains duplicate network ${JSON.stringify(network)}`, `${resource}.pricing.networks`);
        }
        seen.add(network);
    }
}
function validateRouteReadOnlyAcknowledgement(entry, targetType, resource) {
    if (entry.acknowledge_readonly === undefined)
        return;
    if (entry.acknowledge_readonly !== true) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly must be true when present`, `${resource}.acknowledge_readonly`);
    }
    if (targetType !== "function" ||
        typeof entry.pattern !== "string" ||
        !isFinalWildcardRoutePattern(entry.pattern) ||
        !isReadOnlyRouteMethods(entry.methods)) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly applies only to GET/HEAD final-wildcard function routes`, `${resource}.acknowledge_readonly`);
    }
}
function validateRouteTarget(target, resource) {
    const obj = requireObject(target, resource);
    if ((hasOwn(obj, "function") || hasOwn(obj, "static")) && !hasOwn(obj, "type")) {
        throw invalidRouteSpec(`ReleaseSpec.${resource} uses an unsupported target shorthand. Use { type: "function", name: "api" } or { type: "static", file: "events.html" }.`, resource);
    }
    if (obj.type === undefined) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.type is required; use "function" or "static"`, `${resource}.type`);
    }
    if (obj.type === "function") {
        validateKnownFields(obj, resource, FUNCTION_ROUTE_TARGET_FIELDS);
        if (typeof obj.name !== "string" || obj.name.length === 0) {
            throw invalidRouteSpec(`ReleaseSpec.${resource}.name is required for function route targets`, `${resource}.name`);
        }
        return "function";
    }
    if (obj.type === "static") {
        validateKnownFields(obj, resource, STATIC_ROUTE_TARGET_FIELDS);
        if (typeof obj.file !== "string" || obj.file.length === 0) {
            throw invalidRouteSpec(`ReleaseSpec.${resource}.file is required for static route targets`, `${resource}.file`);
        }
        validateStaticTargetFile(obj.file, `${resource}.file`);
        return "static";
    }
    throw invalidRouteSpec(`Unsupported route target type ${JSON.stringify(obj.type)} at ReleaseSpec.${resource}.type; route targets support "function" and "static"`, `${resource}.type`);
}
function validateStaticRouteEntry(entry, resource) {
    const pattern = entry.pattern;
    if (pattern.includes("*")) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.pattern uses a static route target, so it must be an exact path pattern (wildcard patterns such as /docs/* are not supported for static targets)`, `${resource}.pattern`);
    }
    if (entry.methods === undefined) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.methods is required for static route targets; use ["GET"] or ["GET", "HEAD"]`, `${resource}.methods`);
    }
    const methods = entry.methods;
    const methodSet = new Set(methods);
    const valid = methodSet.has("GET") &&
        (methodSet.size === 1 || (methodSet.size === 2 && methodSet.has("HEAD")));
    if (!valid) {
        throw invalidRouteSpec(`ReleaseSpec.${resource}.methods for static route targets must be ["GET"] or ["GET", "HEAD"]; either form materializes effective GET plus HEAD`, `${resource}.methods`);
    }
}
function validateStaticTargetFile(file, resource) {
    const invalid = file.startsWith("/") ||
        file.includes("?") ||
        file.includes("#") ||
        file.includes("\\") ||
        file.endsWith("/") ||
        file.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
    if (invalid) {
        throw invalidRouteSpec(`ReleaseSpec.${resource} must be a relative materialized static-site file path without leading slash, query, fragment, traversal, empty segments, backslashes, or directory shorthand`, resource);
    }
}
function validateChecksSpec(checks) {
    if (checks === undefined)
        return;
    if (!Array.isArray(checks)) {
        throw invalidSpec("ReleaseSpec.checks must be an array", "checks");
    }
}
function validateI18nSpec(i18n) {
    if (i18n === undefined)
        return;
    if (i18n === null)
        return;
    const obj = requireObject(i18n, "i18n");
    validateKnownFields(obj, "i18n", I18N_SPEC_FIELDS, {
        default_locale: "Use `defaultLocale` (camelCase) in i18n.",
        default: "Use `defaultLocale` in i18n.",
        locale: "Use `locales` (plural array) in i18n.",
    });
    if (typeof obj.defaultLocale !== "string" || obj.defaultLocale.length === 0) {
        throw invalidSpec("ReleaseSpec.i18n.defaultLocale is required and must be a non-empty string", "i18n.defaultLocale");
    }
    if (!I18N_LOCALE_TAG_REGEX.test(obj.defaultLocale)) {
        throw invalidSpec(`ReleaseSpec.i18n.defaultLocale ${JSON.stringify(obj.defaultLocale)} must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`, "i18n.defaultLocale");
    }
    if (!Array.isArray(obj.locales)) {
        throw invalidSpec("ReleaseSpec.i18n.locales is required and must be a non-empty array of locale tags", "i18n.locales");
    }
    if (obj.locales.length === 0) {
        throw invalidSpec("ReleaseSpec.i18n.locales must contain at least one entry", "i18n.locales");
    }
    if (obj.locales.length > I18N_MAX_LOCALES) {
        throw invalidSpec(`ReleaseSpec.i18n.locales accepts at most ${I18N_MAX_LOCALES} entries (got ${obj.locales.length})`, "i18n.locales");
    }
    const seenLocales = new Set();
    for (let i = 0; i < obj.locales.length; i++) {
        const tag = obj.locales[i];
        const path = `i18n.locales.${i}`;
        if (typeof tag !== "string" || tag.length === 0) {
            throw invalidSpec(`ReleaseSpec.${path} must be a non-empty string`, path);
        }
        if (!I18N_LOCALE_TAG_REGEX.test(tag)) {
            throw invalidSpec(`ReleaseSpec.${path} (${JSON.stringify(tag)}) must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`, path);
        }
        if (seenLocales.has(tag)) {
            throw invalidSpec(`ReleaseSpec.i18n.locales contains duplicate entry ${JSON.stringify(tag)}`, path);
        }
        seenLocales.add(tag);
    }
    if (!seenLocales.has(obj.defaultLocale)) {
        throw invalidSpec(`ReleaseSpec.i18n.defaultLocale (${JSON.stringify(obj.defaultLocale)}) must be byte-identical to one entry in i18n.locales`, "i18n.defaultLocale");
    }
    if (obj.detect !== undefined) {
        if (!Array.isArray(obj.detect)) {
            throw invalidSpec("ReleaseSpec.i18n.detect must be an array of detect sources", "i18n.detect");
        }
        if (obj.detect.length > I18N_MAX_DETECT_SOURCES) {
            throw invalidSpec(`ReleaseSpec.i18n.detect accepts at most ${I18N_MAX_DETECT_SOURCES} entries (got ${obj.detect.length})`, "i18n.detect");
        }
        for (let i = 0; i < obj.detect.length; i++) {
            const source = obj.detect[i];
            const path = `i18n.detect.${i}`;
            if (typeof source !== "string" || source.length === 0) {
                throw invalidSpec(`ReleaseSpec.${path} must be a non-empty string ("accept-language" or "cookie:<name>")`, path);
            }
            if (source === "accept-language")
                continue;
            if (source.startsWith("cookie:")) {
                const cookieName = source.slice("cookie:".length);
                if (cookieName.length === 0) {
                    throw invalidSpec(`ReleaseSpec.${path} cookie source is missing the cookie name (use "cookie:<name>")`, path);
                }
                if (!I18N_COOKIE_NAME_REGEX.test(cookieName)) {
                    throw invalidSpec(`ReleaseSpec.${path} cookie name ${JSON.stringify(cookieName)} must match the RFC 6265 cookie-name grammar /^[!#$%&'*+\\-.^_\`|~0-9A-Za-z]+$/`, path);
                }
                continue;
            }
            throw invalidSpec(`ReleaseSpec.${path} must be "accept-language" or "cookie:<name>" (got ${JSON.stringify(source)})`, path);
        }
    }
}
function validateKnownFields(obj, resource, allowed, hints = {}) {
    for (const key of Object.keys(obj)) {
        if (allowed.has(key))
            continue;
        const field = resource === "spec" ? `spec.${key}` : `${resource}.${key}`;
        const hint = hints[key] ? ` ${hints[key]}` : "";
        throw invalidSpec(`Unknown ReleaseSpec field: ${field}.${hint}`, field);
    }
}
function requireObject(value, resource) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        throw invalidSpec(`ReleaseSpec.${resource} must be an object`, resource);
    }
    return value;
}
function validateStringArray(value, resource) {
    if (!Array.isArray(value)) {
        throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
    }
    if (value.some((entry) => typeof entry !== "string")) {
        throw invalidSpec(`ReleaseSpec.${resource} entries must be strings`, resource);
    }
}
function validateFunctionConfigInteger(value, resource) {
    if (value === undefined)
        return;
    if (!Number.isSafeInteger(value) || value < 1) {
        throw invalidSpec(`ReleaseSpec.${resource} must be a positive safe JSON integer`, resource);
    }
}
function hasDeployableContent(spec) {
    return (hasDatabaseContent(spec.database) ||
        hasSiteContent(spec.site) ||
        hasFunctionsContent(spec.functions) ||
        hasSecretsContent(spec.secrets) ||
        hasSubdomainsContent(spec.subdomains) ||
        hasRecordEntries(spec.routes) ||
        hasArrayEntries(spec.checks) ||
        hasAssetsContent(spec.assets) ||
        hasI18nContent(spec.i18n));
}
function hasI18nContent(i18n) {
    if (i18n === null)
        return true;
    return isRecord(i18n);
}
function hasAssetsContent(assets) {
    if (!isRecord(assets))
        return false;
    if (hasArrayEntries(assets.put))
        return true;
    if (hasArrayEntries(assets.delete))
        return true;
    return isRecord(assets.sync);
}
function hasDatabaseContent(database) {
    if (!isRecord(database))
        return false;
    return hasArrayEntries(database.migrations) || hasRecordEntries(database.expose);
}
function hasSiteContent(site) {
    // tenant-site-embedding: a declaration (or an explicit null that clears one)
    // is deploy content on its own, like a public_paths-only site.
    if (site && typeof site === "object" && "embedding" in site && site.embedding !== undefined) {
        return true;
    }
    if (!isRecord(site))
        return false;
    if (hasRecordEntries(site.replace))
        return true;
    if (isRecord(site.patch)) {
        return hasRecordEntries(site.patch.put) || hasArrayEntries(site.patch.delete);
    }
    return isRecord(site.public_paths);
}
function hasFunctionsContent(functions) {
    if (!isRecord(functions))
        return false;
    if (hasRecordEntries(functions.replace))
        return true;
    if (!isRecord(functions.patch))
        return false;
    return hasRecordEntries(functions.patch.set) || hasArrayEntries(functions.patch.delete);
}
function hasSecretsContent(secrets) {
    if (!isRecord(secrets))
        return false;
    return hasArrayEntries(secrets.require) || hasArrayEntries(secrets.delete);
}
function hasSubdomainsContent(subdomains) {
    if (!isRecord(subdomains))
        return false;
    return (hasArrayEntries(subdomains.set) ||
        hasArrayEntries(subdomains.add) ||
        hasArrayEntries(subdomains.remove));
}
function hasRecordEntries(value) {
    return isRecord(value) && Object.keys(value).length > 0;
}
function hasArrayEntries(value) {
    return Array.isArray(value) && value.length > 0;
}
function hasOwn(obj, key) {
    return Object.prototype.hasOwnProperty.call(obj, key);
}
function isRecord(value) {
    return !!value && typeof value === "object" && !Array.isArray(value);
}
function invalidSpec(message, resource) {
    return new Run402DeployError(message, {
        code: "INVALID_SPEC",
        phase: "validate",
        resource,
        retryable: false,
        fix: { action: "set_field", path: resource.replace(/^spec\./, "") },
        context: "validating spec",
    });
}
function invalidRouteSpec(message, resource) {
    return new Run402DeployError(message, {
        code: "INVALID_SPEC",
        phase: "validate",
        resource,
        retryable: false,
        fix: {
            action: "set_field",
            path: "routes.replace",
            example: {
                routes: {
                    replace: [
                        {
                            pattern: "/api/*",
                            target: { type: "function", name: "api" },
                        },
                        {
                            pattern: "/events",
                            methods: ["GET", "HEAD"],
                            target: { type: "static", file: "events.html" },
                        },
                    ],
                },
            },
        },
        context: "validating spec",
    });
}
function normalizePlanResponse(plan) {
    const raw = plan;
    const warnings = Array.isArray(raw.warnings) ? raw.warnings : [];
    const missingContent = Array.isArray(raw.missing_content) ? raw.missing_content : [];
    if (raw.kind === "plan_response") {
        const diff = {
            is_noop: raw.is_noop,
            summary: raw.summary,
            warnings,
            migrations: raw.migrations,
            site: raw.site,
            functions: raw.functions,
            secrets: raw.secrets,
            subdomains: raw.subdomains,
            routes: raw.routes,
            static_assets: raw.static_assets,
        };
        return {
            ...plan,
            warnings,
            missing_content: missingContent,
            diff,
            payment_required: raw.payment_required ?? null,
        };
    }
    return {
        ...plan,
        warnings,
        missing_content: missingContent,
        diff: plan.diff ?? {},
    };
}
function requirePersistedPlan(plan, context) {
    if (plan.plan_id && plan.operation_id) {
        return { planId: plan.plan_id, operationId: plan.operation_id };
    }
    throw new Run402DeployError("Dry-run plan responses cannot be uploaded or committed because they do not create plan or operation rows.", {
        code: "DRY_RUN_PLAN_NOT_COMMITTABLE",
        phase: "plan",
        resource: "plan_id",
        retryable: false,
        context,
    });
}
function requirePlanId(plan, context) {
    if (plan.plan_id)
        return plan.plan_id;
    throw new Run402DeployError("Plan response cannot be committed because it does not include plan_id.", {
        code: "DRY_RUN_PLAN_NOT_COMMITTABLE",
        phase: "plan",
        resource: "plan_id",
        retryable: false,
        context,
    });
}
function isCoreCommitResponse(commit) {
    return (typeof commit.plan_id === "string" &&
        typeof commit.project_id === "string" &&
        typeof commit.release_digest === "string" &&
        (commit.status === "committed" ||
            commit.status === "noop" ||
            commit.status === "deferred"));
}
function requireCloudCommitResponse(commit, context) {
    if (!isCoreCommitResponse(commit))
        return commit;
    throw new Run402DeployError("Core commit response reached a Cloud operation-polling path.", {
        code: "INTERNAL_ERROR",
        phase: "commit",
        retryable: false,
        context,
    });
}
async function coreDeployResult(client, commit, diff, warnings, emit, projectId, sliceKinds = []) {
    if (commit.status === "deferred") {
        throw new Run402DeployError(commit.deferred_reason ?? "Core deploy commit deferred.", {
            code: "INTERNAL_ERROR",
            phase: commit.deferred_phase ?? "commit",
            retryable: true,
            context: "committing Core deploy",
        });
    }
    const urls = await coreProjectUrls(client, projectId);
    emit({
        type: "ready",
        releaseId: commit.release_id,
        urls,
        ...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
    });
    return {
        release_id: commit.release_id,
        operation_id: `core:${commit.plan_id}`,
        urls,
        diff,
        warnings,
    };
}
async function coreProjectUrls(client, projectId) {
    try {
        const project = await client.request(`/projects/v1/${encodeURIComponent(projectId)}`, {
            method: "GET",
            context: "fetching Core project endpoints",
        });
        const staticUrl = project.endpoints?.static_base_url;
        return {
            ...(staticUrl ? { site: staticUrl, static: staticUrl } : {}),
            ...(project.endpoints?.rest_url ? { rest: project.endpoints.rest_url } : {}),
            ...(project.endpoints?.storage_base_url ? { storage: project.endpoints.storage_base_url } : {}),
        };
    }
    catch {
        const staticUrl = `${client.apiBase.replace(/\/+$/, "")}/projects/v1/${encodeURIComponent(projectId)}/static`;
        return { site: staticUrl, static: staticUrl };
    }
}
function emitPlanWarnings(plan, emit) {
    if (plan.warnings.length > 0) {
        emit({ type: "plan.warnings", warnings: plan.warnings });
    }
}
function withClientPlanWarnings(spec, plan) {
    const warnings = clientRoutePlanWarnings(spec);
    if (warnings.length === 0)
        return plan;
    const seen = new Set(plan.warnings.map((warning) => warningKey(warning)));
    const nextWarnings = [...plan.warnings];
    for (const warning of warnings) {
        const key = warningKey(warning);
        if (seen.has(key))
            continue;
        seen.add(key);
        nextWarnings.push(warning);
    }
    return { ...plan, warnings: nextWarnings };
}
function warningKey(warning) {
    return `${warning.code}:${(warning.affected ?? []).join(",")}`;
}
function clientRoutePlanWarnings(spec) {
    const routes = spec.routes;
    if (!routes || !("replace" in routes))
        return [];
    const affected = routes.replace
        .filter((route) => {
        if (route.target.type !== "function")
            return false;
        if (!isFinalWildcardRoutePattern(route.pattern))
            return false;
        if (!route.methods)
            return false;
        if (route.acknowledge_readonly === true)
            return false;
        return isReadOnlyRouteMethods(route.methods);
    })
        .map((route) => route.pattern)
        .sort();
    if (affected.length === 0)
        return [];
    return [
        {
            code: "WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS",
            severity: "warn",
            requires_confirmation: true,
            message: "A wildcard function route only allows GET/HEAD. Mutation endpoints under that prefix will be rejected by the gateway before the function runs.",
            affected,
            confidence: "heuristic",
            details: {
                missing_common_methods: ["POST", "PUT", "PATCH", "DELETE"],
                fix: "Add the mutation methods your routed function supports, or omit methods to allow every supported method.",
            },
        },
    ];
}
function isFinalWildcardRoutePattern(pattern) {
    return pattern.endsWith("/*");
}
function isReadOnlyRouteMethods(methods) {
    if (!Array.isArray(methods) || methods.length === 0)
        return false;
    return methods.every((method) => method === "GET" || method === "HEAD");
}
function normalizeAllowWarningCodes(value) {
    if (value === undefined)
        return new Set();
    if (!Array.isArray(value)) {
        throw new Run402DeployError("ApplyOptions.allowWarningCodes must be an array of warning-code strings", {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: "allowWarningCodes",
            retryable: false,
            context: "validating deploy warning options",
        });
    }
    const codes = new Set();
    for (const code of value) {
        if (typeof code !== "string" || code.length === 0) {
            throw new Run402DeployError("ApplyOptions.allowWarningCodes entries must be non-empty strings", {
                code: "INVALID_SPEC",
                phase: "validate",
                resource: "allowWarningCodes",
                retryable: false,
                context: "validating deploy warning options",
            });
        }
        codes.add(code);
    }
    return codes;
}
function abortOnConfirmationWarnings(plan, opts, allowWarningCodes) {
    if (opts.allowWarnings)
        return;
    const blocking = plan.warnings.filter((w) => w.requires_confirmation || w.code === "MISSING_REQUIRED_SECRET");
    if (blocking.length === 0)
        return;
    const unacknowledged = blocking.filter((w) => !allowWarningCodes.has(w.code));
    if (unacknowledged.length === 0)
        return;
    const missing = unacknowledged.find((w) => w.code === "MISSING_REQUIRED_SECRET");
    const first = missing ?? unacknowledged[0];
    const unacknowledgedCodes = Array.from(new Set(unacknowledged.map((w) => w.code))).sort();
    throw new Run402DeployError(`Deploy plan requires review of ${first.code}. Review the affected access or resources, then acknowledge only the reviewed warning codes.`, {
        code: first.code || "DEPLOY_WARNING_REQUIRES_CONFIRMATION",
        phase: "plan",
        resource: "warnings",
        retryable: false,
        fix: { action: "review_warnings", path: "warnings" },
        body: {
            warnings: blocking,
            unacknowledged_warnings: unacknowledged,
            unacknowledged_warning_codes: unacknowledgedCodes,
            next_actions: [{ type: "review_warnings", warning_codes: unacknowledgedCodes, why: "Review these warnings, then pass the reviewed codes as allowWarningCodes." }],
            allowed_warning_codes: Array.from(allowWarningCodes).sort(),
        },
        context: "planning deploy",
    });
}
function validateSecretsSpec(secrets) {
    if (secrets === undefined)
        return;
    if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) {
        throw invalidSecretSpec("ReleaseSpec.secrets must be an object", "secrets");
    }
    const obj = secrets;
    const allowed = new Set(["require", "delete"]);
    for (const key of Object.keys(obj)) {
        if (!allowed.has(key)) {
            throw invalidSecretSpec(key === "set" || key === "replace_all"
                ? `ReleaseSpec.secrets.${key} is no longer supported; set values with the secrets API, then use secrets.require[] in the deploy spec.`
                : `Unknown ReleaseSpec.secrets field: ${key}`, `secrets.${key}`);
        }
    }
    const required = validateSecretKeyArray(obj.require, "secrets.require");
    const deleted = validateSecretKeyArray(obj.delete, "secrets.delete");
    const deleteSet = new Set(deleted);
    const conflict = required.find((key) => deleteSet.has(key));
    if (conflict) {
        throw invalidSecretSpec(`Secret key ${conflict} cannot appear in both secrets.require and secrets.delete`, "secrets");
    }
}
function validateSecretKeyArray(value, resource) {
    if (value === undefined)
        return [];
    if (!Array.isArray(value)) {
        throw invalidSecretSpec(`${resource} must be an array of secret keys`, resource);
    }
    const seen = new Set();
    const out = [];
    for (const key of value) {
        if (typeof key !== "string" || !SECRET_KEY_RE.test(key)) {
            throw invalidSecretSpec(`${resource} entries must match ${SECRET_KEY_RE.source}`, resource);
        }
        if (seen.has(key)) {
            throw invalidSecretSpec(`${resource} contains duplicate key ${key}`, resource);
        }
        seen.add(key);
        out.push(key);
    }
    return out;
}
function invalidSecretSpec(message, resource) {
    return new Run402DeployError(message, {
        code: "INVALID_SPEC",
        phase: "validate",
        resource,
        retryable: false,
        fix: { action: "set_field", path: resource },
        context: "validating spec",
    });
}
async function normalizeReleaseSpec(client, spec, opts = {}) {
    const byteReaders = new Map();
    // Slice-tagged `remember`. Each slice category creates its own remember
    // closure so the registered reader carries `reader.slice = "release" |
    // "asset"`. On cross-kind dedup (same SHA from both a release-bound
    // slice and the asset slice) the value escalates to `"mixed"`. This
    // value surfaces on `content.upload.*` events so agents can group
    // upload telemetry by slice kind.
    const makeRemember = (slice) => (resolved) => {
        // Propagate the final content-type onto the deferred reader so the CAS
        // upload session can declare it correctly. Callers may set
        // ref.contentType *after* resolveContent returns (e.g. normalizeFileSet
        // sets it from the path extension), so do this at remember time.
        if (resolved.ref.contentType && !resolved.reader.contentType) {
            resolved.reader.contentType = resolved.ref.contentType;
        }
        if (!byteReaders.has(resolved.ref.sha256)) {
            resolved.reader.slice = slice;
            byteReaders.set(resolved.ref.sha256, resolved.reader);
        }
        else {
            // Already remembered — but if the existing reader has no contentType
            // and we just learned it, fill it in. Also escalate slice tag when
            // the second registration comes from a different kind.
            const existing = byteReaders.get(resolved.ref.sha256);
            if (resolved.ref.contentType && !existing.contentType) {
                existing.contentType = resolved.ref.contentType;
            }
            if (existing.slice && existing.slice !== slice && existing.slice !== "mixed") {
                existing.slice = "mixed";
            }
        }
        return resolved.ref;
    };
    const rememberRelease = makeRemember("release");
    const rememberAsset = makeRemember("asset");
    const normalized = { project_id: spec.project_id };
    if (spec.base)
        normalized.base = spec.base;
    if (spec.subdomains)
        normalized.subdomains = spec.subdomains;
    if (hasOwn(spec, "routes")) {
        normalized.routes = spec.routes;
    }
    if (hasOwn(spec, "i18n")) {
        normalized.i18n = spec.i18n;
    }
    if (spec.checks)
        normalized.checks = spec.checks;
    if (spec.secrets)
        normalized.secrets = spec.secrets;
    if (spec.database) {
        const db = {};
        if (spec.database.expose)
            db.expose = spec.database.expose;
        if (typeof spec.database.zero_downtime === "boolean") {
            db.zero_downtime = spec.database.zero_downtime;
        }
        if (spec.database.migrations && spec.database.migrations.length > 0) {
            db.migrations = await Promise.all(spec.database.migrations.map(async (m) => normalizeMigration(client, spec.project_id, m, rememberRelease, opts)));
            assertUniqueMigrationIds(db.migrations);
        }
        normalized.database = db;
    }
    if (spec.functions) {
        const fns = {};
        if (spec.functions.replace) {
            fns.replace = await normalizeFunctionMap(spec.functions.replace, rememberRelease);
        }
        if (spec.functions.patch) {
            fns.patch = {};
            if (spec.functions.patch.set) {
                fns.patch.set = await normalizeFunctionMap(spec.functions.patch.set, rememberRelease);
            }
            if (spec.functions.patch.delete)
                fns.patch.delete = spec.functions.patch.delete;
        }
        normalized.functions = fns;
    }
    if (spec.site) {
        const publicPaths = "public_paths" in spec.site ? spec.site.public_paths : undefined;
        // tenant-site-embedding: present (object or null) travels as-is.
        const embedding = "embedding" in spec.site && spec.site.embedding !== undefined
            ? { embedding: spec.site.embedding }
            : {};
        if ("replace" in spec.site && spec.site.replace) {
            const map = await normalizeFileSet(spec.site.replace, rememberRelease);
            // Re-check post-expansion so `dir("dist")` (a LocalDirRef whose keys are
            // unknown at validateSpec time) is caught too, not just literal FileSets.
            assertNoAstroAdapterTreeInSite(Object.keys(map), "site.replace");
            normalized.site = {
                replace: map,
                ...(publicPaths ? { public_paths: publicPaths } : {}),
                ...embedding,
            };
        }
        else if ("patch" in spec.site && spec.site.patch) {
            const patch = {};
            if (spec.site.patch.put) {
                patch.put = await normalizeFileSet(spec.site.patch.put, rememberRelease);
                assertNoAstroAdapterTreeInSite(Object.keys(patch.put), "site.patch.put");
            }
            if (spec.site.patch.delete)
                patch.delete = spec.site.patch.delete;
            normalized.site = {
                patch,
                ...(publicPaths ? { public_paths: publicPaths } : {}),
                ...embedding,
            };
        }
        else if (publicPaths || "embedding" in embedding) {
            normalized.site = {
                ...(publicPaths ? { public_paths: publicPaths } : {}),
                ...embedding,
            };
        }
    }
    // v1.48 unified-apply: asset slice normalization. Mirrors the site
    // branch — for each `put` entry with a `source`, hash the bytes and
    // register a byte-reader; emit the wire-shaped `AssetPutEntry[]`.
    // Cross-kind SHA dedup is automatic via the shared `byteReaders` map.
    if (spec.assets) {
        normalized.assets = await normalizeAssetSlice(spec.assets, rememberAsset);
    }
    return { normalized, byteReaders };
}
async function normalizeFunctionMap(map, remember) {
    const out = {};
    for (const [name, fn] of Object.entries(map)) {
        out[name] = await normalizeFunction(fn, remember);
    }
    return out;
}
async function normalizeFunction(fn, remember) {
    const out = {
        runtime: fn.runtime ?? "node22",
    };
    if (fn.config)
        out.config = fn.config;
    if (fn.deps !== undefined)
        out.deps = fn.deps;
    if (fn.triggers !== undefined)
        out.triggers = normalizeFunctionTriggers(fn.triggers);
    if (fn.schedule !== undefined)
        out.schedule = fn.schedule;
    if (fn.entrypoint)
        out.entrypoint = fn.entrypoint;
    if (fn.requireAuth !== undefined)
        out.requireAuth = fn.requireAuth;
    if (fn.requireRole !== undefined)
        out.requireRole = fn.requireRole;
    if (fn.class !== undefined)
        out.class = fn.class;
    if (fn.capabilities !== undefined)
        out.capabilities = [...fn.capabilities];
    if (fn.source !== undefined) {
        const resolved = await resolveContent(fn.source, "function source");
        out.source = remember(resolved);
    }
    if (fn.files) {
        out.files = await normalizeFileSet(fn.files, remember);
    }
    return out;
}
function normalizeFunctionTriggers(triggers) {
    return triggers
        .map((trigger) => {
        const run = {
            event_type: trigger.run.event_type,
            payload: trigger.run.payload ?? {},
            ...(trigger.run.retry !== undefined ? { retry: { ...trigger.run.retry } } : {}),
            ...(trigger.run.expires_after_seconds !== undefined
                ? { expires_after_seconds: trigger.run.expires_after_seconds }
                : {}),
        };
        if (trigger.type === "email") {
            return {
                id: trigger.id,
                type: "email",
                mailbox: trigger.mailbox,
                events: [...trigger.events].sort(),
                run,
            };
        }
        return {
            id: trigger.id,
            type: "schedule",
            cron: trigger.cron,
            timezone: trigger.timezone ?? "UTC",
            misfire_policy: trigger.misfire_policy ?? "skip",
            overlap_policy: trigger.overlap_policy ?? "allow",
            run,
        };
    })
        .sort((a, b) => a.id.localeCompare(b.id));
}
async function normalizeFileSet(set, remember) {
    // `dir(path)` produces a LocalDirRef sentinel that is documented as a
    // valid site.replace / site.patch.put input. Expand it to a plain FileSet
    // before iterating — otherwise Object.entries would walk the sentinel's
    // own keys (__source, path, prefix, ignore, …) and feed each value to
    // resolveContent, throwing "Unsupported byte source for prefix" on the
    // first `undefined` option.
    const fileSet = isLocalDirRef(set) ? await expandLocalDirRef(set) : set;
    const out = {};
    for (const [path, source] of Object.entries(fileSet)) {
        const resolved = await resolveContent(source, path);
        if (!resolved.ref.contentType) {
            resolved.ref.contentType = guessContentType(path);
        }
        if (isUtf8StringSource(source) && isKnownBinaryContent(path, resolved.ref.contentType)) {
            throw binaryContentRequiresBytes(path, resolved.ref.contentType);
        }
        out[path] = remember(resolved);
    }
    return out;
}
function isLocalDirRef(source) {
    return (typeof source === "object" &&
        source !== null &&
        source.__source === "local-dir" &&
        typeof source.path === "string");
}
async function expandLocalDirRef(ref) {
    // Lazy import — keeps the root SDK V8-isolate-safe. The site slice's
    // LocalDirRef branch is Node-only by construction (the walker reads
    // from disk).
    let walker;
    try {
        walker = (await import("../node/files.js"));
    }
    catch {
        throw new Run402DeployError("site dir() is only supported in Node runtimes (received a LocalDirRef in a non-Node environment)", {
            code: "INVALID_SPEC",
            resource: "site.replace",
            retryable: false,
            context: "normalizing byte sources",
        });
    }
    const fileSet = await walker.fileSetFromDir(ref.path, {
        ignore: ref.ignore,
        includeSensitive: ref.includeSensitive,
    });
    if (!ref.prefix)
        return fileSet;
    const prefixed = {};
    const sep = ref.prefix.endsWith("/") ? "" : "/";
    for (const [relPath, source] of Object.entries(fileSet)) {
        prefixed[`${ref.prefix}${sep}${relPath}`] = source;
    }
    return prefixed;
}
// ─── Asset manifest assembly from plan response (v1.48 unified-apply) ───────
/**
 * Build a {@link AssetManifest} from the plan response's `asset_entries[]`
 * array. Each entry's `asset_ref` carries gateway-authoritative URLs that
 * mirror the AssetRef envelope `Assets.put` returns for the single-entry
 * case. Keys are stored in null-prototype objects (design D9) so
 * attacker-controlled or filesystem-derived keys (`__proto__`,
 * `constructor`, `toString`) don't collide with prototype properties.
 *
 * Totals are derived from the plan response's per-entry `status`:
 *   - `"upload_pending"` → bytes_uploaded (the SDK is about to PUT these to S3)
 *   - `"present"` or `"satisfied_by_plan"` → bytes_reused (already in CAS,
 *     dedup hit either project-locally or via a same-spec sibling)
 * `duration_ms` is filled in by `manifestFromResult` at the NodeAssets layer.
 */
function buildAssetManifestFromPlanEntries(entries) {
    const list = [];
    const byKey = Object.create(null);
    const manifest = Object.create(null);
    let bytesUploaded = 0;
    let bytesReused = 0;
    for (const entry of entries) {
        const ref = entry.asset_ref;
        const e = {
            key: entry.key,
            sha256: entry.sha256,
            size_bytes: entry.size_bytes,
            content_type: entry.content_type,
            visibility: entry.visibility,
            url: ref.url,
            immutable_url: ref.immutable_url,
            cdn_url: ref.cdn_url,
            cdn_immutable_url: ref.cdn_immutable_url,
            sri: ref.sri,
            etag: ref.etag,
            content_digest: ref.content_digest,
        };
        // v1.49+ image-variant pass-through. Only emitted when the gateway
        // returned them (image MIMEs ≥320×320; HEIC/HEIF sources also include
        // `display_jpeg`). Pre-v1.49 plan responses omit these fields entirely
        // and the manifest entry stays bytewise-identical to before.
        if (ref.width_px !== undefined)
            e.width_px = ref.width_px;
        if (ref.height_px !== undefined)
            e.height_px = ref.height_px;
        if (ref.blurhash !== undefined)
            e.blurhash = ref.blurhash;
        if (ref.variant_spec_version !== undefined) {
            e.variant_spec_version = ref.variant_spec_version;
        }
        if (ref.display_url !== undefined)
            e.display_url = ref.display_url;
        if (ref.display_immutable_url !== undefined) {
            e.display_immutable_url = ref.display_immutable_url;
        }
        if (ref.variants !== undefined)
            e.variants = ref.variants;
        // v1.50 pass-through: metadata + EXIF policy + image intrinsics.
        // Pre-v1.50 plan responses omit them; the manifest entry stays
        // bytewise-identical to before for older gateways.
        if (ref.metadata !== undefined)
            e.metadata = ref.metadata;
        if (ref.image_format !== undefined)
            e.image_format = ref.image_format;
        if (ref.image_info !== undefined)
            e.image_info = ref.image_info;
        if (ref.image_exif !== undefined)
            e.image_exif = ref.image_exif;
        if (ref.image_exif_policy !== undefined)
            e.image_exif_policy = ref.image_exif_policy;
        // v1.54 pass-through: shape-contract fields. Pre-v1.54 plan responses
        // omit them; the manifest entry stays bytewise-identical to before.
        if (ref.blurhash_data_url !== undefined)
            e.blurhash_data_url = ref.blurhash_data_url;
        if (ref.asset_schema !== undefined)
            e.asset_schema = ref.asset_schema;
        list.push(e);
        byKey[entry.key] = e;
        manifest[entry.key] = e;
        if (entry.status === "upload_pending") {
            bytesUploaded += entry.size_bytes;
        }
        else {
            // "present" or "satisfied_by_plan" — already in CAS or covered by a sibling.
            bytesReused += entry.size_bytes;
        }
    }
    return {
        list,
        byKey,
        manifest,
        totals: {
            files: entries.length,
            bytes_uploaded: bytesUploaded,
            bytes_reused: bytesReused,
            duration_ms: 0,
        },
    };
}
// ─── Asset slice normalization (v1.48 unified-apply) ─────────────────────────
/**
 * Type guard: distinguish the SDK-input shape (`AssetPutEntryInput` with
 * `source: ContentSource`) from the wire shape (`AssetPutEntry` with
 * `sha256` already computed). The two forms can be mixed in the same
 * `assets.put` array — the normalizer handles both branches.
 */
function isAssetPutEntryInput(entry) {
    return (typeof entry.source !== "undefined" &&
        typeof entry.sha256 === "undefined");
}
/**
 * Normalize the assets slice per design D3 (three-schema fidelity). For
 * each `put` entry:
 *
 * 1. If it's an `AssetPutEntryInput` (has `source`): call `resolveContent`
 *    to hash the bytes, register a byte-reader via `remember()`, and emit
 *    a wire-shaped `AssetPutEntry` (no `source` field).
 * 2. If it's already an `AssetPutEntry` (has `sha256`): pass through.
 *
 * Validates per-spec invariants before any network call: duplicate keys
 * in `put`, key in both `put` and `delete`, empty manifest (only the
 * assets slice was set and it had no `put`/`delete`/`sync` content).
 *
 * `spec.assets.delete` and `spec.assets.sync` pass through unchanged.
 */
async function normalizeAssetSlice(slice, remember) {
    const out = {};
    if (slice.put && slice.put.length > 0) {
        const seenKeys = new Set();
        const put = [];
        for (let idx = 0; idx < slice.put.length; idx++) {
            const entry = slice.put[idx];
            if (!entry.key || typeof entry.key !== "string") {
                throw new Run402DeployError(`assets.put[${idx}] missing required \`key\``, {
                    code: "INVALID_SPEC",
                    phase: "validate",
                    resource: `assets.put[${idx}]`,
                    retryable: false,
                    fix: { action: "set_field", path: `assets.put[${idx}].key` },
                    context: "validating spec",
                });
            }
            if (seenKeys.has(entry.key)) {
                throw new Run402DeployError(`assets.put contains duplicate key \`${entry.key}\``, {
                    code: "ASSET_DUPLICATE_KEY_IN_PUT",
                    phase: "validate",
                    resource: `assets.put[${idx}]`,
                    retryable: false,
                    fix: { action: "set_field", path: `assets.put[${idx}].key` },
                    context: "validating spec",
                });
            }
            seenKeys.add(entry.key);
            // v1.50: validate caller-supplied metadata / EXIF policy on BOTH
            // shapes BEFORE any HTTP traffic. Throws LocalError with the
            // canonical gateway code (INVALID_ASSET_METADATA / INVALID_EXIF_
            // POLICY) so consumers see the same `e.code` whether the rejection
            // was local or remote.
            const inputShape = isAssetPutEntryInput(entry) ? entry : null;
            const wireShape = inputShape ? null : entry;
            const callerMetadata = inputShape?.metadata ?? wireShape?.metadata;
            const callerExifPolicy = inputShape?.exifPolicy ?? wireShape?.exif_policy;
            if (callerMetadata !== undefined) {
                assertAssetMetadata(callerMetadata, "validating asset metadata");
            }
            if (callerExifPolicy !== undefined) {
                assertExifPolicy(callerExifPolicy, "validating asset EXIF policy");
            }
            if (inputShape) {
                const label = `assets.put[${idx}] (${inputShape.key})`;
                const resolved = await resolveContent(inputShape.source, label);
                const contentType = inputShape.content_type ?? resolved.ref.contentType ?? guessContentType(inputShape.key);
                if (isUtf8StringSource(inputShape.source) &&
                    isKnownBinaryContent(inputShape.key, contentType)) {
                    throw binaryContentRequiresBytes(inputShape.key, contentType, `assets.put[${idx}]`);
                }
                if (!resolved.ref.contentType) {
                    resolved.ref.contentType = contentType;
                }
                const ref = remember(resolved);
                put.push({
                    key: inputShape.key,
                    sha256: ref.sha256,
                    size_bytes: ref.size,
                    content_type: inputShape.content_type ?? ref.contentType ?? "application/octet-stream",
                    visibility: inputShape.visibility ?? "public",
                    immutable: inputShape.immutable ?? true,
                    // v1.50: thread metadata + exif_policy onto the wire-shape. The
                    // SDK input field is camelCase (`exifPolicy`); the wire field is
                    // snake_case (`exif_policy`).
                    ...(inputShape.metadata !== undefined ? { metadata: inputShape.metadata } : {}),
                    ...(inputShape.exifPolicy !== undefined ? { exif_policy: inputShape.exifPolicy } : {}),
                });
            }
            else {
                // Wire-shaped entry — pass through verbatim. The caller is
                // responsible for ensuring the bytes are already in CAS (or will
                // be uploaded out-of-band).
                const w = wireShape;
                put.push({
                    key: w.key,
                    sha256: w.sha256,
                    size_bytes: w.size_bytes,
                    content_type: w.content_type ?? "application/octet-stream",
                    visibility: w.visibility ?? "public",
                    immutable: w.immutable ?? true,
                    // v1.50 wire-shape passthrough (no camelCase conversion needed).
                    ...(w.metadata !== undefined ? { metadata: w.metadata } : {}),
                    ...(w.exif_policy !== undefined ? { exif_policy: w.exif_policy } : {}),
                });
            }
        }
        out.put = put;
    }
    if (slice.delete && slice.delete.length > 0) {
        out.delete = [...slice.delete];
    }
    // Cross-slice invariant: a key may not be both `put` and `delete`-d.
    if (out.put && out.delete) {
        const putKeys = new Set(out.put.map((e) => e.key));
        for (const k of out.delete) {
            if (putKeys.has(k)) {
                throw new Run402DeployError(`assets.put and assets.delete both reference key \`${k}\``, {
                    code: "ASSET_KEY_IN_PUT_AND_DELETE",
                    phase: "validate",
                    resource: `assets`,
                    retryable: false,
                    fix: { action: "remove_field", path: `assets.delete[\`${k}\`]` },
                    context: "validating spec",
                });
            }
        }
    }
    if (slice.sync) {
        out.sync = {
            prefix: slice.sync.prefix,
            prune: slice.sync.prune,
            ...(slice.sync.confirm ? { confirm: slice.sync.confirm } : {}),
        };
    }
    return out;
}
function binaryContentRequiresBytes(path, contentType, resource = path) {
    return new Run402DeployError(`${resource} uses a JavaScript string for binary content (${contentType}). ` +
        "Read the file without an encoding and pass Uint8Array, ArrayBuffer, Blob, or a filesystem byte-source helper so the original bytes are hashed.", {
        code: "BINARY_CONTENT_REQUIRES_BYTES",
        phase: "validate",
        resource,
        retryable: false,
        fix: { action: "use_bytes", path: resource },
        body: { details: { path, content_type: contentType } },
        context: "normalizing byte sources",
    });
}
async function normalizeMigration(client, projectId, m, remember, opts = {}) {
    const originalId = "id" in m ? m.id : undefined;
    const name = "name" in m ? m.name : undefined;
    if (!originalId && !name) {
        throw new Run402DeployError("MigrationSpec.id or MigrationSpec.name is required", {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: "database.migrations",
            retryable: false,
            fix: { action: "set_field", path: "database.migrations[].id" },
            context: "validating spec",
        });
    }
    let sql_ref;
    let sql;
    let checksum;
    let contentHash;
    const identityLabel = originalId ?? name ?? "<unknown>";
    if (m.sql_ref) {
        sql_ref = m.sql_ref;
        contentHash = m.sql_ref.sha256;
        checksum = m.checksum ?? m.sql_ref.sha256;
    }
    else if (m.sql !== undefined) {
        const bytes = new TextEncoder().encode(m.sql);
        const sha256 = await sha256Hex(bytes);
        contentHash = sha256;
        if (opts.inlineMigrationSql) {
            sql = m.sql;
        }
        else {
            const ref = { sha256, size: bytes.byteLength, contentType: "application/sql" };
            remember({ ref, reader: makeBytesReader(bytes, `migration:${identityLabel}`) });
            sql_ref = ref;
        }
        checksum = m.checksum ?? sha256;
    }
    else {
        throw new Run402DeployError(`MigrationSpec ${identityLabel} must include sql or sql_ref`, {
            code: "INVALID_SPEC",
            phase: "validate",
            resource: `database.migrations.${identityLabel}`,
            retryable: false,
            fix: {
                action: "set_field",
                path: `database.migrations.${identityLabel}.sql`,
            },
            context: "validating spec",
        });
    }
    const id = originalId ?? `${name}_${contentHash.slice(0, 16)}`;
    const out = { id, checksum };
    if (sql_ref)
        out.sql_ref = sql_ref;
    if (sql !== undefined)
        out.sql = sql;
    if (m.transaction)
        out.transaction = m.transaction;
    return out;
    // projectId / client params reserved for future content-presence preflight.
    void client;
    void projectId;
}
function assertUniqueMigrationIds(migrations) {
    const seen = new Map();
    for (const [index, migration] of migrations.entries()) {
        const firstIndex = seen.get(migration.id);
        if (firstIndex !== undefined) {
            throw new Run402DeployError(`ReleaseSpec.database.migrations.${index}.id duplicates database.migrations.${firstIndex}.id ${JSON.stringify(migration.id)}`, {
                code: "INVALID_SPEC",
                phase: "validate",
                resource: `database.migrations.${index}.id`,
                retryable: false,
                fix: { action: "set_field", path: `database.migrations.${index}.id` },
                context: "validating spec",
            });
        }
        seen.set(migration.id, index);
    }
}
// ─── Content source resolution ───────────────────────────────────────────────
async function resolveContent(source, label) {
    // Pre-resolved ContentRef — pass through, no reader needed (caller is
    // responsible for ensuring the bytes are already in CAS).
    if (isContentRef(source)) {
        return {
            ref: { ...source },
            reader: makeUnreadableReader(source.sha256, label),
        };
    }
    // { data, contentType } wrapper — recurse into data, override contentType.
    if (typeof source === "object" &&
        source !== null &&
        !Array.isArray(source) &&
        !(source instanceof Uint8Array) &&
        !(source instanceof ArrayBuffer) &&
        !(typeof Blob !== "undefined" && source instanceof Blob) &&
        !isReadableStream(source) &&
        !isFsFileSource(source) &&
        "data" in source) {
        const inner = await resolveContent(source.data, label);
        if (source.contentType) {
            inner.ref.contentType = source.contentType;
        }
        return inner;
    }
    if (isFsFileSource(source)) {
        return await resolveFsFile(source, label);
    }
    if (typeof source === "string") {
        const bytes = new TextEncoder().encode(source);
        return makeMemResolved(bytes, undefined, label);
    }
    if (source instanceof Uint8Array) {
        return makeMemResolved(source, undefined, label);
    }
    if (source instanceof ArrayBuffer) {
        return makeMemResolved(new Uint8Array(source), undefined, label);
    }
    if (typeof Blob !== "undefined" && source instanceof Blob) {
        const bytes = new Uint8Array(await source.arrayBuffer());
        const ct = source.type && source.type.length > 0 ? source.type : undefined;
        return makeMemResolved(bytes, ct, label);
    }
    if (isReadableStream(source)) {
        const bytes = await readStreamFully(source);
        return makeMemResolved(bytes, undefined, label);
    }
    throw new Run402DeployError(`Unsupported byte source for ${label}`, {
        code: "INVALID_SPEC",
        resource: label,
        retryable: false,
        context: "normalizing byte sources",
    });
}
async function makeMemResolved(bytes, contentType, label) {
    const sha256 = await sha256Hex(bytes);
    const ref = { sha256, size: bytes.byteLength };
    if (contentType)
        ref.contentType = contentType;
    return { ref, reader: makeBytesReader(bytes, label) };
}
async function resolveFsFile(source, label) {
    // Lazy import — keeps the root SDK V8-isolate-safe. fileSetFromDir lives
    // in `@run402/sdk/node`, so any `FsFileSource` we see here must be in a
    // Node runtime where `node:fs/promises` resolves.
    let fsMod;
    try {
        fsMod = (await import("node:fs/promises"));
    }
    catch {
        throw new Run402DeployError("FsFileSource is only supported in Node runtimes (received in a non-Node environment)", {
            code: "INVALID_SPEC",
            resource: label,
            retryable: false,
            context: "normalizing byte sources",
        });
    }
    const buf = await fsMod.readFile(source.path);
    const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
    const sha256 = await sha256Hex(bytes);
    const ref = { sha256, size: bytes.byteLength };
    if (source.contentType)
        ref.contentType = source.contentType;
    return {
        ref,
        reader: Object.assign(async () => {
            const buf2 = await fsMod.readFile(source.path);
            return new Uint8Array(buf2.buffer, buf2.byteOffset, buf2.byteLength);
        }, { label }),
    };
}
function makeBytesReader(bytes, label, contentType) {
    const reader = async () => bytes;
    reader.label = label;
    if (contentType)
        reader.contentType = contentType;
    return reader;
}
function makeUnreadableReader(sha256, label) {
    const reader = async () => {
        throw new Run402DeployError(`ContentRef ${sha256.slice(0, 12)}… was passed pre-resolved but the gateway reports it missing — provide bytes inline instead`, {
            code: "CONTENT_UPLOAD_FAILED",
            resource: label,
            retryable: false,
            context: "uploading deploy bytes",
        });
    };
    reader.label = label;
    return reader;
}
// ─── Manifest-ref CAS upload (bypasses the upload phase loop) ────────────────
async function uploadInlineCas(client, projectId, bytes, contentType) {
    const sha256 = await sha256Hex(bytes);
    const headers = await apikeyHeaders(client, projectId);
    const planRes = await client.request("/content/v1/plans", {
        method: "POST",
        headers,
        body: {
            content: [{ sha256, size: bytes.byteLength, content_type: contentType }],
        },
        context: "planning content upload",
    });
    if (planRes.missing.length > 0) {
        const session = planRes.missing[0];
        await uploadOne(client.fetch, session, bytes);
        // v1.48 unified-apply: per-session /storage/v1/uploads/:id/complete is
        // gone (404). The plan-level commit below promotes the session to CAS.
        await client.request(`/content/v1/plans/${encodeURIComponent(planRes.plan_id)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
    }
    return { sha256, size: bytes.byteLength, contentType };
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
/**
 * Build the apikey header set for a project. The v1.34 gateway's
 * `/apply/v1/operations/:operation_id*` and `/content/v1/plans*` routes require
 * `apikey: <project.anon_key>` (apikeyAuth middleware). Plan + commit on
 * `/apply/v1/plans*` use SIWX, which the kernel's getAuth provides
 * automatically — only the apikey-gated paths need this helper.
 *
 * COLD-RESTART RECOVERY: when the local credential cache has no entry for the project — the
 * returning-agent case, where the wallet survives but the once-issued project
 * keys did not — this helper mints a SHORT-LIVED anon token via
 * `POST /projects/v1/:project_id/tokens` (the route the platform built as
 * exactly this recovery path; the gateway's own 401 envelope names it as the
 * `mint_token` next_action) and uses it as the apikey, memoized in-process
 * until shortly before it expires. A wallet-holding agent on a fresh machine
 * deploys with zero extra commands; nothing durable is written anywhere.
 *
 * The mint is attempted only on a cache miss and never retried within a
 * process on failure; a client that cannot mint (no signer, no authority)
 * falls through to the pre-existing behavior — the request goes out without
 * an apikey and the gateway rejects with 401, whose envelope names the
 * recovery.
 */
async function apikeyHeaders(client, projectId) {
    // A CI session and a grant key both already authorize these routes via
    // `Authorization: Bearer`, supplied by the kernel's getAuth. Attaching an
    // apikey beside a bearer mixes credential families on one request, which the
    // kernel explicitly forbids — so stand down and let the bearer carry it.
    if (isCiClient(client) || isGrantKeyClient(client))
        return {};
    const project = await client.getProjectCredentials(projectId);
    if (project?.anon_key)
        return { apikey: project.anon_key };
    const minted = await mintedAnonToken(client, projectId);
    return minted ? { apikey: minted } : {};
}
/**
 * Per-client, per-project memo of short-lived minted anon tokens (plus a
 * one-shot "minting failed, don't hammer" marker). Held only in process
 * memory — a short-lived token must never land in a durable cache.
 */
const mintedTokenMemo = new WeakMap();
/** Refresh margin: stop using a minted token this long before it expires. */
const MINTED_TOKEN_REFRESH_MARGIN_MS = 30_000;
async function mintedAnonToken(client, projectId) {
    let perProject = mintedTokenMemo.get(client);
    if (!perProject) {
        perProject = new Map();
        mintedTokenMemo.set(client, perProject);
    }
    const memo = perProject.get(projectId);
    if (memo) {
        if ("failed" in memo)
            return null;
        if (Date.now() < memo.expiresAtMs - MINTED_TOKEN_REFRESH_MARGIN_MS)
            return memo.token;
    }
    try {
        const issued = await client.request(`/projects/v1/${encodeURIComponent(projectId)}/tokens`, {
            method: "POST",
            // Least authority: the apikey-gated deploy legs need exactly what the
            // anon key carries, never the service key's power.
            body: { kind: "anon" },
            context: "minting a short-lived anon token for deploy (local project keys not cached)",
        });
        if (!issued?.secret) {
            perProject.set(projectId, { failed: true });
            return null;
        }
        const ttlMs = (typeof issued.expires_in === "number" && issued.expires_in > 0 ? issued.expires_in : 300) * 1000;
        perProject.set(projectId, { token: issued.secret, expiresAtMs: Date.now() + ttlMs });
        return issued.secret;
    }
    catch {
        // No signer, no authority, or the route is unavailable: fall through to
        // the pre-existing no-apikey behavior. Marked so one deploy's many
        // apikey-gated legs don't each re-attempt a doomed mint.
        perProject.set(projectId, { failed: true });
        return null;
    }
}
function isCiClient(client) {
    return isCiSessionCredentials(client.credentials);
}
function isGrantKeyClient(client) {
    return isGrantKeyCredentials(client.credentials);
}
function makeEmitter(cb) {
    if (!cb)
        return () => { };
    return (event) => {
        try {
            cb(event);
        }
        catch {
            /* swallow */
        }
    };
}
function isContentRef(source) {
    return (typeof source === "object" &&
        source !== null &&
        typeof source.sha256 === "string" &&
        typeof source.size === "number" &&
        !("data" in source) &&
        !("__source" in source));
}
function isFsFileSource(source) {
    return (typeof source === "object" &&
        source !== null &&
        source.__source === "fs-file" &&
        typeof source.path === "string");
}
function isReadableStream(source) {
    return (typeof source === "object" &&
        source !== null &&
        typeof source.getReader === "function" &&
        typeof source.tee === "function");
}
async function readStreamFully(stream) {
    const reader = stream.getReader();
    const chunks = [];
    let total = 0;
    while (true) {
        const { value, done } = await reader.read();
        if (done)
            break;
        if (value) {
            chunks.push(value);
            total += value.byteLength;
        }
    }
    const out = new Uint8Array(total);
    let off = 0;
    for (const c of chunks) {
        out.set(c, off);
        off += c.byteLength;
    }
    return out;
}
async function sha256Hex(bytes) {
    const buf = await crypto.subtle.digest("SHA-256", bytes);
    return Array.from(new Uint8Array(buf))
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");
}
async function sha256Base64(bytes) {
    const buf = await crypto.subtle.digest("SHA-256", bytes);
    return base64FromBytes(new Uint8Array(buf));
}
function base64FromHex(hex) {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < bytes.byteLength; i++) {
        bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
    }
    return base64FromBytes(bytes);
}
function base64FromBytes(bytes) {
    if (typeof Buffer !== "undefined") {
        return Buffer.from(bytes).toString("base64");
    }
    let bin = "";
    for (let i = 0; i < bytes.byteLength; i++)
        bin += String.fromCharCode(bytes[i]);
    return btoa(bin);
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
const CONTENT_TYPE_BY_EXT = {
    html: "text/html; charset=utf-8",
    htm: "text/html; charset=utf-8",
    css: "text/css; charset=utf-8",
    js: "text/javascript; charset=utf-8",
    mjs: "text/javascript; charset=utf-8",
    cjs: "text/javascript; charset=utf-8",
    json: "application/json",
    webmanifest: "application/manifest+json",
    svg: "image/svg+xml",
    png: "image/png",
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    gif: "image/gif",
    webp: "image/webp",
    ico: "image/x-icon",
    woff: "font/woff",
    woff2: "font/woff2",
    ttf: "font/ttf",
    otf: "font/otf",
    txt: "text/plain; charset=utf-8",
    md: "text/markdown; charset=utf-8",
    xml: "application/xml",
    pdf: "application/pdf",
    wasm: "application/wasm",
    sql: "application/sql",
};
export function guessContentType(path) {
    const ix = path.lastIndexOf(".");
    if (ix < 0)
        return "application/octet-stream";
    const ext = path.slice(ix + 1).toLowerCase();
    return CONTENT_TYPE_BY_EXT[ext] ?? "application/octet-stream";
}
// ─── Error translation ──────────────────────────────────────────────────────
function translateDeployError(err, phase, planId, operationId) {
    if (err instanceof Run402DeployError)
        return err;
    if (err instanceof ApiError) {
        const body = err.body && typeof err.body === "object"
            ? err.body
            : null;
        const gw = body && typeof body === "object" ? extractGatewayError(body) : null;
        if (gw) {
            return translateGatewayError(gw, phase, planId, operationId);
        }
        return new Run402DeployError(err.message, {
            code: "INTERNAL_ERROR",
            phase,
            retryable: err.status !== null && err.status >= 500,
            operationId,
            planId,
            status: err.status,
            body: err.body,
            context: err.context,
        });
    }
    // 409 transfer-freeze (PROJECT_HAS_PENDING_TRANSFER) arrives as a dedicated
    // TransferFreezeError, not an ApiError. Route it through the same
    // gateway-envelope path so the structured code, details (transfer_id), and
    // next_actions (cancel/view transfer) survive instead of flattening to
    // INTERNAL_ERROR. Use the structural guard so the check holds across
    // duplicate SDK copies / realm boundaries (V8-isolate code-mode).
    if (isTransferFreezeError(err)) {
        const body = err.body && typeof err.body === "object" && !Array.isArray(err.body)
            ? err.body
            : null;
        const gw = body ? extractGatewayError(body) : null;
        if (gw) {
            return translateGatewayError(gw, phase, planId, operationId);
        }
        return new Run402DeployError(err.message, {
            code: "PROJECT_HAS_PENDING_TRANSFER",
            phase,
            retryable: false,
            operationId,
            planId,
            status: err.status,
            body: err.body,
            context: err.context,
        });
    }
    if (err instanceof NetworkError) {
        return new Run402DeployError(err.message, {
            code: "NETWORK_ERROR",
            phase,
            retryable: true,
            operationId,
            planId,
            context: phase,
        });
    }
    // Re-throw other Run402Error subclasses (PaymentRequired, Unauthorized, etc.)
    // as-is — the consumer handles them at a different layer than
    // deploy-state-machine errors.
    if (err instanceof PaymentRequired || err instanceof Unauthorized) {
        throw err;
    }
    if (err instanceof Error) {
        return new Run402DeployError(err.message, {
            code: "INTERNAL_ERROR",
            phase,
            retryable: false,
            operationId,
            planId,
            context: phase,
        });
    }
    return new Run402DeployError(String(err), {
        code: "INTERNAL_ERROR",
        phase,
        retryable: false,
        operationId,
        planId,
        context: phase,
    });
}
function extractGatewayError(body) {
    // Gateway returns the error in any of:
    //   { error: { code, message?, phase?, ... } }       — nested
    //   { code, message?, phase?, ... }                  — top-level
    //   { error: "<message>", code: "..." }              — older shape, error as string
    // The only required field is `code`; `message` is convenient but
    // optional (some gateway routes return just a code on simple validation
    // failures, e.g. `{code: "invalid_spec"}`).
    if (body.error &&
        typeof body.error === "object" &&
        typeof body.error.code === "string") {
        const nested = body.error;
        return {
            ...nested,
            category: nested.category ?? stringField(body, "category"),
            retryable: nested.retryable ?? booleanField(body, "retryable"),
            safe_to_retry: nested.safe_to_retry ?? booleanField(body, "safe_to_retry"),
            mutation_state: nested.mutation_state ?? stringField(body, "mutation_state"),
            trace_id: nested.trace_id ?? stringField(body, "trace_id"),
            details: nested.details ?? objectField(body, "details"),
            next_actions: nested.next_actions ?? arrayField(body, "next_actions"),
        };
    }
    if (typeof body.code === "string") {
        const details = objectField(body, "details");
        const out = { code: body.code };
        if (typeof body.message === "string") {
            out.message = body.message;
        }
        else if (typeof body.error === "string") {
            out.message = body.error;
        }
        else if (typeof details?.message === "string") {
            out.message = details.message;
        }
        else {
            out.message = `Deploy error: ${body.code}`;
        }
        const phase = stringField(body, "phase") ?? stringField(details, "phase");
        const resource = stringField(body, "resource") ?? stringField(details, "resource");
        const retryable = booleanField(body, "retryable") ?? booleanField(details, "retryable");
        const rolledBack = booleanField(body, "rolled_back") ?? booleanField(details, "rolled_back");
        const operationId = stringField(body, "operation_id") ?? stringField(details, "operation_id");
        const planId = stringField(body, "plan_id") ?? stringField(details, "plan_id");
        const fix = body.fix !== undefined ? body.fix : details?.fix;
        const logs = arrayField(body, "logs") ?? arrayField(details, "logs");
        if (phase !== undefined)
            out.phase = phase;
        if (resource !== undefined)
            out.resource = resource;
        if (retryable !== undefined)
            out.retryable = retryable;
        if (typeof body.category === "string")
            out.category = body.category;
        if (typeof body.safe_to_retry === "boolean")
            out.safe_to_retry = body.safe_to_retry;
        if (typeof body.mutation_state === "string")
            out.mutation_state = body.mutation_state;
        if (typeof body.trace_id === "string")
            out.trace_id = body.trace_id;
        if (details !== null)
            out.details = details;
        if (Array.isArray(body.next_actions))
            out.next_actions = body.next_actions;
        if (fix !== undefined)
            out.fix = fix;
        if (logs !== undefined)
            out.logs = logs;
        if (rolledBack !== undefined)
            out.rolled_back = rolledBack;
        if (operationId !== undefined)
            out.operation_id = operationId;
        if (planId !== undefined)
            out.plan_id = planId;
        out.source_body = body;
        return out;
    }
    return null;
}
function stringField(obj, key) {
    return obj && typeof obj === "object" && typeof obj[key] === "string"
        ? obj[key]
        : undefined;
}
function booleanField(obj, key) {
    return obj && typeof obj === "object" && typeof obj[key] === "boolean"
        ? obj[key]
        : undefined;
}
function objectField(obj, key) {
    const value = obj && typeof obj === "object" ? obj[key] : undefined;
    return value && typeof value === "object" && !Array.isArray(value)
        ? value
        : null;
}
function arrayField(obj, key) {
    const value = obj && typeof obj === "object" ? obj[key] : undefined;
    return Array.isArray(value) ? value : undefined;
}
function translateGatewayError(gw, phase, planId, operationId) {
    if (!gw) {
        return new Run402DeployError("Deploy failed without a structured error", {
            code: "INTERNAL_ERROR",
            phase,
            retryable: false,
            operationId,
            planId,
            context: phase,
        });
    }
    // Normalize the gateway code to the SCREAMING_SNAKE_CASE convention used
    // by `Run402DeployErrorCode`. Some gateway routes return lowercase
    // (`operation_not_found`) while services return uppercase
    // (`OPERATION_NOT_FOUND`); consumers expect the canonical uppercase form.
    const normalizedCode = gw.code.toUpperCase();
    // Prefer body-supplied ids — the gateway is the authoritative source for
    // which operation/plan an error belongs to. The caller-provided arguments
    // are only used as a fallback (e.g., commit failures where the call site
    // already knows the plan id but the body omits it).
    const details = objectField(gw, "details");
    const opId = (gw && gw.operation_id) ??
        stringField(details, "operation_id") ??
        operationId;
    const pId = (gw && gw.plan_id) ??
        stringField(details, "plan_id") ??
        planId;
    const body = gw.source_body ?? gw;
    const fix = (gw.fix ?? details?.fix ?? null);
    const logs = (gw.logs ?? arrayField(details, "logs") ?? null);
    return new Run402DeployError(gw.message ?? `Deploy failed: ${gw.code}`, {
        code: normalizedCode,
        phase: gw.phase ?? stringField(details, "phase") ?? phase,
        resource: gw.resource ?? stringField(details, "resource") ?? null,
        retryable: gw.retryable ?? false,
        operationId: opId,
        planId: pId,
        fix,
        logs,
        rolledBack: gw.rolled_back ?? booleanField(details, "rolled_back") ?? false,
        body,
        context: phase,
    });
}
//# sourceMappingURL=deploy.js.map