@manifest-network/manifest-agent-core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/close-lease.d.ts.map +1 -1
  2. package/dist/close-lease.js.map +1 -1
  3. package/dist/deploy-app.js.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/internals/build-fred-input.d.ts.map +1 -1
  6. package/dist/internals/build-fred-input.js.map +1 -1
  7. package/dist/internals/classify-deploy-error.d.ts +13 -9
  8. package/dist/internals/classify-deploy-error.d.ts.map +1 -1
  9. package/dist/internals/classify-deploy-error.js +15 -11
  10. package/dist/internals/classify-deploy-error.js.map +1 -1
  11. package/dist/internals/classify-deploy-response.d.ts.map +1 -1
  12. package/dist/internals/classify-deploy-response.js.map +1 -1
  13. package/dist/internals/connection.d.ts.map +1 -1
  14. package/dist/internals/connection.js.map +1 -1
  15. package/dist/internals/evaluate-readiness-from-fred.js.map +1 -1
  16. package/dist/internals/evaluate-readiness.d.ts.map +1 -1
  17. package/dist/internals/evaluate-readiness.js.map +1 -1
  18. package/dist/internals/find-sku-uuid.d.ts.map +1 -1
  19. package/dist/internals/find-sku-uuid.js.map +1 -1
  20. package/dist/internals/format-success.d.ts.map +1 -1
  21. package/dist/internals/format-success.js.map +1 -1
  22. package/dist/internals/guarded-fetch.d.ts +1 -1
  23. package/dist/internals/guarded-fetch.js +1 -1
  24. package/dist/internals/humanize-denom.d.ts.map +1 -1
  25. package/dist/internals/humanize-denom.js.map +1 -1
  26. package/dist/internals/inspect-image.d.ts.map +1 -1
  27. package/dist/internals/inspect-image.js.map +1 -1
  28. package/dist/internals/lease-items.d.ts.map +1 -1
  29. package/dist/internals/lease-items.js.map +1 -1
  30. package/dist/internals/lease-state.d.ts.map +1 -1
  31. package/dist/internals/lease-state.js.map +1 -1
  32. package/dist/internals/render-deployment-plan.d.ts.map +1 -1
  33. package/dist/internals/render-deployment-plan.js.map +1 -1
  34. package/dist/internals/render-intent-recap.d.ts.map +1 -1
  35. package/dist/internals/render-intent-recap.js.map +1 -1
  36. package/dist/internals/render-partial-success-prompt.d.ts.map +1 -1
  37. package/dist/internals/render-partial-success-prompt.js.map +1 -1
  38. package/dist/internals/save-manifest.d.ts.map +1 -1
  39. package/dist/internals/save-manifest.js.map +1 -1
  40. package/dist/internals/secret-denylist.d.ts.map +1 -1
  41. package/dist/internals/secret-denylist.js.map +1 -1
  42. package/dist/internals/spec-normalize.d.ts.map +1 -1
  43. package/dist/internals/spec-normalize.js.map +1 -1
  44. package/dist/internals/verify-domain-state.d.ts.map +1 -1
  45. package/dist/internals/verify-domain-state.js.map +1 -1
  46. package/dist/internals/verify-recover.d.ts.map +1 -1
  47. package/dist/internals/verify-recover.js.map +1 -1
  48. package/dist/manage-domain.js.map +1 -1
  49. package/dist/troubleshoot.js.map +1 -1
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"inspect-image.js","names":[],"sources":["../../src/internals/inspect-image.ts"],"sourcesContent":["import { createGuardedFetch } from './guarded-fetch.js';\n\n/**\n * Inspect a public container image via the OCI Distribution API. Returns\n * the manifest digest, exposed ports, image defaults (env / cmd /\n * entrypoint / user / workingDir), healthcheck, labels, volumes, and a\n * heuristic `suggestedTmpfs` list for known-good Fred image families.\n *\n * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`\n * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6\n * normalization — see `guarded-fetch.ts` for the design).\n *\n * **Fail-soft contract:** returns `null` on every non-fatal failure mode:\n * - 401 / 403 (private registry / auth required)\n * - 429 (Docker Hub rate-limit)\n * - OCI grammar violation in the `imageRef`\n * - Manifest body exceeding the 10 MiB cap\n * - Request timeout (10s)\n * - Unparseable manifest / blob JSON\n * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)\n * Callers treat `null` as \"no info, ask the user\".\n * Diagnostics flow through `opts.logger` instead of stderr.\n *\n * ## Security — SSRF (production callers MUST read)\n *\n * `imageRef` is user-controlled (it comes from `DeploySpec.image`).\n * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`\n * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would\n * cause this function to probe internal services on the host. The CJS\n * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to\n * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which\n * blocks at connect time.\n *\n * Opt-out-of-safety semantics (parent's PR-2 directive): the default\n * `opts.fetch = createGuardedFetch()` is safe by construction. Callers\n * pass their own `opts.fetch` ONLY for tests (canned responses) or\n * unusual production cases (e.g. a trusted private registry on an RFC\n * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s\n * JSDoc for the production-guard contract.\n */\n\nconst ACCEPT_MANIFEST = [\n 'application/vnd.oci.image.index.v1+json',\n 'application/vnd.oci.image.manifest.v1+json',\n 'application/vnd.docker.distribution.manifest.list.v2+json',\n 'application/vnd.docker.distribution.manifest.v2+json',\n].join(', ');\n\n// OCI Distribution Spec v1.1 grammar for URL-interpolated fields. Validate\n// BEFORE the URL is constructed; the `imageRef` flag is user-controlled,\n// so a malformed input like `foo/bar:..%2F..%2Fconfig` must be rejected\n// here rather than forwarded to the registry.\nconst OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\\.|_|__|-+)[a-z0-9]+)*$/;\nconst OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;\nconst OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;\n\n// Body-size cap (10 MiB). Real-world configs are <100 KB; even JVM-rich\n// images rarely exceed a few MB. Anything over 10 MiB indicates a hostile\n// or buggy registry; abort rather than risk OOM.\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n// Request timeout — 10s. Registry queries should be fast; longer waits\n// indicate a hung registry.\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n// Heuristic table: image base name or resolved Cmd/Entrypoint contains\n// one of these tokens → suggest the corresponding tmpfs paths. Order:\n// longer/more-specific tokens first when there's ambiguity.\nconst TMPFS_HINTS: ReadonlyArray<{\n readonly match: string;\n readonly paths: readonly string[];\n}> = [\n { match: 'wordpress', paths: ['/run/lock', '/var/run/apache2'] },\n { match: 'mariadb', paths: ['/run/mysqld'] },\n { match: 'postgres', paths: ['/var/run/postgresql'] },\n { match: 'mysql', paths: ['/var/run/mysqld'] },\n { match: 'nginx', paths: ['/var/cache/nginx', '/var/run'] },\n];\n\nexport interface ImageInfo {\n image: string;\n digest: string | null;\n ports: string[];\n env: Record<string, string>;\n cmd: string[] | null;\n entrypoint: string[] | null;\n user: string;\n workingDir: string;\n healthcheck: Record<string, unknown> | null;\n labels: Record<string, string> | null;\n volumes: Record<string, unknown> | null;\n suggestedTmpfs: string[];\n}\n\nexport interface InspectImageOptions {\n /**\n * HTTP client. **Production callers SHOULD use the default** (which is\n * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /\n * metadata at connect time). Tests pass canned implementations.\n * Browser/Deno consumers pass their own SSRF-guarded fetch since\n * `createGuardedFetch()` throws on non-Node runtimes.\n */\n fetch?: typeof fetch;\n /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */\n logger?: (reason: string) => void;\n}\n\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n\ninterface ParsedRef {\n registry: string;\n name: string;\n tag: string | null;\n digest: string | null;\n}\n\nexport async function inspectImage(\n imageRef: string,\n opts: InspectImageOptions = {},\n): Promise<ImageInfo | null> {\n const logger = opts.logger ?? defaultLogger;\n const fetchImpl: typeof fetch = opts.fetch ?? createDefaultGuardedFetch();\n\n let parsed: ParsedRef;\n try {\n parsed = parseRef(imageRef);\n } catch (err) {\n logger(\n `inspect-image: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n\n const ref = parsed.digest ?? parsed.tag ?? 'latest';\n try {\n let authHeader: string | null = null;\n if (parsed.registry === 'docker.io') {\n const token = await getDockerHubToken(parsed.name, fetchImpl);\n authHeader = `Bearer ${token}`;\n }\n\n // Step 1: fetch manifest (may be an index → pick platform → refetch).\n let manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n ref,\n authHeader,\n fetchImpl,\n );\n if (\n manifestRes.contentType.includes('manifest.list') ||\n manifestRes.contentType.includes('image.index') ||\n isManifestIndex(manifestRes.manifest)\n ) {\n const child = pickPlatformManifest(manifestRes.manifest);\n if (!child || typeof child.digest !== 'string') {\n throw new Error('multi-arch index has no usable child manifest');\n }\n manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n child.digest,\n authHeader,\n fetchImpl,\n );\n }\n\n // Step 2: fetch the config blob — the actual image config lives there.\n const config = manifestRes.manifest.config as\n | { digest?: unknown }\n | undefined;\n if (!config || typeof config.digest !== 'string') {\n throw new Error('manifest has no config descriptor');\n }\n const configBlob = await fetchBlobJson(\n parsed.registry,\n parsed.name,\n config.digest,\n authHeader,\n fetchImpl,\n );\n const c = (configBlob.config ?? {}) as Record<string, unknown>;\n\n const out: ImageInfo = {\n image: `${parsed.registry}/${parsed.name}${parsed.digest ? '@' + parsed.digest : ':' + (parsed.tag ?? 'latest')}`,\n digest: manifestRes.digest ?? parsed.digest ?? null,\n ports: pickPorts(c.ExposedPorts),\n env: parseEnv(c.Env),\n cmd: Array.isArray(c.Cmd) ? (c.Cmd as string[]) : null,\n entrypoint: Array.isArray(c.Entrypoint)\n ? (c.Entrypoint as string[])\n : null,\n user: typeof c.User === 'string' ? c.User : '',\n workingDir: typeof c.WorkingDir === 'string' ? c.WorkingDir : '',\n healthcheck:\n c.Healthcheck !== null &&\n typeof c.Healthcheck === 'object' &&\n !Array.isArray(c.Healthcheck)\n ? (c.Healthcheck as Record<string, unknown>)\n : null,\n labels:\n c.Labels !== null &&\n typeof c.Labels === 'object' &&\n !Array.isArray(c.Labels)\n ? (c.Labels as Record<string, string>)\n : null,\n volumes:\n c.Volumes !== null &&\n typeof c.Volumes === 'object' &&\n !Array.isArray(c.Volumes)\n ? (c.Volumes as Record<string, unknown>)\n : null,\n suggestedTmpfs: [],\n };\n out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [\n ...(out.cmd ?? []),\n ...(out.entrypoint ?? []),\n ]);\n\n return out;\n } catch (err) {\n logger(`inspect-image: ${formatErrorChain(err)}`);\n return null;\n }\n}\n\n/**\n * Walk an Error's `cause` chain and join all message strings. undici wraps\n * connection errors (including SSRF blocks from our custom Dispatcher) in\n * a fetch-side TypeError with the underlying cause nested via `.cause`.\n * Surfacing the chain in the logger gives the user the real reason (e.g.,\n * \"SSRF blocked: 127.0.0.1 ... loopback\") instead of an opaque\n * \"fetch failed\".\n */\nfunction formatErrorChain(err: unknown): string {\n const parts: string[] = [];\n let current: unknown = err;\n let depth = 0;\n // Defensive bound — sane Error chains are <5 levels; cap at 10 to avoid\n // pathological cycles.\n while (current !== null && current !== undefined && depth < 10) {\n if (current instanceof Error) {\n parts.push(current.message);\n current = (current as Error & { cause?: unknown }).cause;\n } else {\n parts.push(String(current));\n current = undefined;\n }\n depth += 1;\n }\n return parts.join(' | ');\n}\n\nlet cachedDefaultFetch: typeof fetch | undefined;\nfunction createDefaultGuardedFetch(): typeof fetch {\n if (!cachedDefaultFetch) {\n cachedDefaultFetch = createGuardedFetch();\n }\n return cachedDefaultFetch;\n}\n\nfunction parseRef(ref: string): ParsedRef {\n // \"<reg>/<name>@sha256:<digest>\" | \"<reg>/<name>:<tag>\" | \"<name>\" | \"<name>:<tag>\"\n let registry = 'docker.io';\n let name: string;\n let tag: string | null = null;\n let digest: string | null = null;\n\n let rest = ref;\n const atIdx = rest.indexOf('@');\n if (atIdx >= 0) {\n digest = rest.slice(atIdx + 1);\n rest = rest.slice(0, atIdx);\n }\n\n // Detect registry segment: head before first `/` is a registry only if\n // it has a `.` or `:` (port) or is `localhost`.\n const firstSlash = rest.indexOf('/');\n if (firstSlash > 0) {\n const head = rest.slice(0, firstSlash);\n if (head === 'localhost' || head.includes('.') || head.includes(':')) {\n registry = head;\n rest = rest.slice(firstSlash + 1);\n }\n }\n\n if (!digest) {\n const colonIdx = rest.lastIndexOf(':');\n if (colonIdx >= 0) {\n tag = rest.slice(colonIdx + 1);\n name = rest.slice(0, colonIdx);\n } else {\n name = rest;\n tag = 'latest';\n }\n } else {\n name = rest;\n }\n\n // Docker Hub library prefix for single-segment names (\"nginx\" → \"library/nginx\").\n if (registry === 'docker.io' && !name.includes('/')) {\n name = `library/${name}`;\n }\n\n // Validate URL-interpolated fields against OCI Distribution Spec grammar\n // BEFORE the URL is constructed. The ref strings reach the user via\n // `DeploySpec.image`, so malformed input must be rejected here.\n for (const component of name.split('/')) {\n if (!OCI_NAME_COMPONENT.test(component)) {\n throw new Error(`invalid name component \"${component}\" in image ref`);\n }\n }\n if (tag !== null && !OCI_TAG.test(tag)) {\n throw new Error(`invalid tag \"${tag}\" in image ref`);\n }\n if (digest !== null && !OCI_DIGEST.test(digest)) {\n throw new Error(\n `invalid digest \"${digest}\" in image ref (expected sha256:<64-hex>)`,\n );\n }\n\n return { registry, name, tag, digest };\n}\n\nfunction registryHost(registry: string): string {\n // Docker Hub's image API lives at registry-1.docker.io even though the\n // canonical \"registry\" name is docker.io.\n return registry === 'docker.io' ? 'registry-1.docker.io' : registry;\n}\n\nasync function getDockerHubToken(\n name: string,\n fetchImpl: typeof fetch,\n): Promise<string> {\n // Docker Hub requires anonymous access still go through a token grant.\n // Surface 429 specifically — anonymous pulls are rate-limited per-IP\n // and a 60-min wait fixes it. Without this special case the user sees\n // the same fail-soft `null` outcome as a hard 401 with no signal that\n // the situation is temporary.\n const res = await capturingFetch(\n `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`,\n {},\n fetchImpl,\n );\n if (res.status === 429) {\n throw new Error(\n 'Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)',\n );\n }\n if (res.status !== 200) {\n throw new Error(`Docker Hub token: HTTP ${res.status}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('Docker Hub token: invalid JSON');\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n typeof (parsed as { token?: unknown }).token !== 'string'\n ) {\n throw new Error('Docker Hub token: missing `token` in response');\n }\n return (parsed as { token: string }).token;\n}\n\nasync function fetchManifest(\n registry: string,\n name: string,\n ref: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{\n manifest: Record<string, unknown>;\n contentType: string;\n digest: string | null;\n}> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/manifests/${ref}`;\n const headers: Record<string, string> = { Accept: ACCEPT_MANIFEST };\n if (authHeader) headers.Authorization = authHeader;\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `registry returned ${res.status} on manifest fetch (auth required? private registry?)`,\n );\n }\n if (res.status === 404) {\n // Digest-pinned refs use `@sha256:...`; tag refs use `:tag`. Pick the\n // right separator so the error message doesn't show\n // `registry/name:sha256:...` mistakenly.\n const sep = ref.startsWith('sha256:') ? '@' : ':';\n throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);\n }\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on manifest fetch`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('manifest is not valid JSON');\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('manifest is not a JSON object');\n }\n return {\n manifest: parsed as Record<string, unknown>,\n contentType: res.headers.get('content-type') ?? '',\n digest: res.headers.get('docker-content-digest'),\n };\n}\n\nasync function fetchBlobJson(\n registry: string,\n name: string,\n digest: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{ config?: unknown }> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/blobs/${digest}`;\n const headers: Record<string, string> = {};\n if (authHeader) headers.Authorization = authHeader;\n // undici fetch follows redirects by default; registries 307 → CDN.\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on blob fetch`);\n }\n try {\n return JSON.parse(res.body) as { config?: unknown };\n } catch {\n throw new Error('blob is not valid JSON');\n }\n}\n\ninterface CapturedResponse {\n status: number;\n headers: Headers;\n body: string;\n}\n\n/**\n * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed\n * body-size cap. Throws on overflow, timeout, or read error so the outer\n * try/catch produces the fail-soft `null` return.\n */\nasync function capturingFetch(\n url: string,\n init: RequestInit,\n fetchImpl: typeof fetch,\n): Promise<CapturedResponse> {\n const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetchImpl(url, { ...init, signal });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new Error(`request timeout on ${url}`);\n }\n throw err;\n }\n // Stream the body with a manual chunk-accumulation cap. Avoids the\n // unbounded `await response.text()` path that would let a hostile\n // registry exhaust memory.\n const reader = response.body?.getReader();\n if (!reader) {\n return { status: response.status, headers: response.headers, body: '' };\n }\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const decoder = new TextDecoder();\n let body = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n totalBytes += value.length;\n if (totalBytes > MAX_BODY_BYTES) {\n await reader.cancel();\n throw new Error(\n `response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`,\n );\n }\n chunks.push(value);\n }\n }\n body = decoder.decode(concatUint8Arrays(chunks));\n } finally {\n reader.releaseLock();\n }\n return { status: response.status, headers: response.headers, body };\n}\n\nfunction concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) return new Uint8Array(0);\n if (chunks.length === 1) {\n const only = chunks[0];\n if (only !== undefined) return only;\n }\n let total = 0;\n for (const c of chunks) total += c.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n}\n\nfunction pickPorts(exposedPorts: unknown): string[] {\n if (\n exposedPorts === null ||\n typeof exposedPorts !== 'object' ||\n Array.isArray(exposedPorts)\n ) {\n return [];\n }\n return Object.keys(exposedPorts as Record<string, unknown>).sort();\n}\n\nfunction parseEnv(env: unknown): Record<string, string> {\n if (!Array.isArray(env)) return {};\n const out: Record<string, string> = {};\n for (const kv of env) {\n if (typeof kv !== 'string') continue;\n const i = kv.indexOf('=');\n if (i > 0) {\n const key = kv.slice(0, i);\n const value = kv.slice(i + 1);\n out[key] = value;\n } else {\n out[kv] = '';\n }\n }\n return out;\n}\n\nfunction isManifestIndex(m: Record<string, unknown>): boolean {\n return Array.isArray(m.manifests);\n}\n\nfunction pickPlatformManifest(\n index: Record<string, unknown>,\n): Record<string, unknown> | null {\n const list = index.manifests;\n if (!Array.isArray(list)) return null;\n const linuxAmd64 = list.find(\n (m): m is Record<string, unknown> =>\n m !== null &&\n typeof m === 'object' &&\n (m as { platform?: unknown }).platform !== null &&\n typeof (m as { platform?: unknown }).platform === 'object' &&\n (m as { platform: { os?: unknown } }).platform.os === 'linux' &&\n (m as { platform: { architecture?: unknown } }).platform.architecture ===\n 'amd64',\n );\n if (linuxAmd64) return linuxAmd64;\n // Fall back to first entry.\n const first = list[0];\n if (first !== null && typeof first === 'object' && !Array.isArray(first)) {\n return first as Record<string, unknown>;\n }\n return null;\n}\n\nfunction suggestedTmpfsFor(\n name: string,\n cmdAndEntrypoint: ReadonlyArray<string>,\n): string[] {\n const haystack = [name, ...cmdAndEntrypoint].join(' ').toLowerCase();\n for (const hint of TMPFS_HINTS) {\n if (haystack.includes(hint.match)) return [...hint.paths];\n }\n return [];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"inspect-image.js","names":[],"sources":["../../src/internals/inspect-image.ts"],"sourcesContent":["import { createGuardedFetch } from './guarded-fetch.js';\n\n/**\n * Inspect a public container image via the OCI Distribution API. Returns\n * the manifest digest, exposed ports, image defaults (env / cmd /\n * entrypoint / user / workingDir), healthcheck, labels, volumes, and a\n * heuristic `suggestedTmpfs` list for known-good Fred image families.\n *\n * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`\n * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6\n * normalization — see `guarded-fetch.ts` for the design).\n *\n * **Fail-soft contract:** returns `null` on every non-fatal failure mode:\n * - 401 / 403 (private registry / auth required)\n * - 429 (Docker Hub rate-limit)\n * - OCI grammar violation in the `imageRef`\n * - Manifest body exceeding the 10 MiB cap\n * - Request timeout (10s)\n * - Unparseable manifest / blob JSON\n * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)\n * Callers treat `null` as \"no info, ask the user\".\n * Diagnostics flow through `opts.logger` instead of stderr.\n *\n * ## Security — SSRF (production callers MUST read)\n *\n * `imageRef` is user-controlled (it comes from `DeploySpec.image`).\n * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`\n * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would\n * cause this function to probe internal services on the host. The CJS\n * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to\n * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which\n * blocks at connect time.\n *\n * Opt-out-of-safety semantics (parent's PR-2 directive): the default\n * `opts.fetch = createGuardedFetch()` is safe by construction. Callers\n * pass their own `opts.fetch` ONLY for tests (canned responses) or\n * unusual production cases (e.g. a trusted private registry on an RFC\n * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s\n * JSDoc for the production-guard contract.\n */\n\nconst ACCEPT_MANIFEST = [\n 'application/vnd.oci.image.index.v1+json',\n 'application/vnd.oci.image.manifest.v1+json',\n 'application/vnd.docker.distribution.manifest.list.v2+json',\n 'application/vnd.docker.distribution.manifest.v2+json',\n].join(', ');\n\n// OCI Distribution Spec v1.1 grammar for URL-interpolated fields. Validate\n// BEFORE the URL is constructed; the `imageRef` flag is user-controlled,\n// so a malformed input like `foo/bar:..%2F..%2Fconfig` must be rejected\n// here rather than forwarded to the registry.\nconst OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\\.|_|__|-+)[a-z0-9]+)*$/;\nconst OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;\nconst OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;\n\n// Body-size cap (10 MiB). Real-world configs are <100 KB; even JVM-rich\n// images rarely exceed a few MB. Anything over 10 MiB indicates a hostile\n// or buggy registry; abort rather than risk OOM.\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n// Request timeout — 10s. Registry queries should be fast; longer waits\n// indicate a hung registry.\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n// Heuristic table: image base name or resolved Cmd/Entrypoint contains\n// one of these tokens → suggest the corresponding tmpfs paths. Order:\n// longer/more-specific tokens first when there's ambiguity.\nconst TMPFS_HINTS: ReadonlyArray<{\n readonly match: string;\n readonly paths: readonly string[];\n}> = [\n { match: 'wordpress', paths: ['/run/lock', '/var/run/apache2'] },\n { match: 'mariadb', paths: ['/run/mysqld'] },\n { match: 'postgres', paths: ['/var/run/postgresql'] },\n { match: 'mysql', paths: ['/var/run/mysqld'] },\n { match: 'nginx', paths: ['/var/cache/nginx', '/var/run'] },\n];\n\nexport interface ImageInfo {\n image: string;\n digest: string | null;\n ports: string[];\n env: Record<string, string>;\n cmd: string[] | null;\n entrypoint: string[] | null;\n user: string;\n workingDir: string;\n healthcheck: Record<string, unknown> | null;\n labels: Record<string, string> | null;\n volumes: Record<string, unknown> | null;\n suggestedTmpfs: string[];\n}\n\nexport interface InspectImageOptions {\n /**\n * HTTP client. **Production callers SHOULD use the default** (which is\n * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /\n * metadata at connect time). Tests pass canned implementations.\n * Browser/Deno consumers pass their own SSRF-guarded fetch since\n * `createGuardedFetch()` throws on non-Node runtimes.\n */\n fetch?: typeof fetch;\n /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */\n logger?: (reason: string) => void;\n}\n\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n\ninterface ParsedRef {\n registry: string;\n name: string;\n tag: string | null;\n digest: string | null;\n}\n\nexport async function inspectImage(\n imageRef: string,\n opts: InspectImageOptions = {},\n): Promise<ImageInfo | null> {\n const logger = opts.logger ?? defaultLogger;\n const fetchImpl: typeof fetch = opts.fetch ?? createDefaultGuardedFetch();\n\n let parsed: ParsedRef;\n try {\n parsed = parseRef(imageRef);\n } catch (err) {\n logger(\n `inspect-image: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n\n const ref = parsed.digest ?? parsed.tag ?? 'latest';\n try {\n let authHeader: string | null = null;\n if (parsed.registry === 'docker.io') {\n const token = await getDockerHubToken(parsed.name, fetchImpl);\n authHeader = `Bearer ${token}`;\n }\n\n // Step 1: fetch manifest (may be an index → pick platform → refetch).\n let manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n ref,\n authHeader,\n fetchImpl,\n );\n if (\n manifestRes.contentType.includes('manifest.list') ||\n manifestRes.contentType.includes('image.index') ||\n isManifestIndex(manifestRes.manifest)\n ) {\n const child = pickPlatformManifest(manifestRes.manifest);\n if (!child || typeof child.digest !== 'string') {\n throw new Error('multi-arch index has no usable child manifest');\n }\n manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n child.digest,\n authHeader,\n fetchImpl,\n );\n }\n\n // Step 2: fetch the config blob — the actual image config lives there.\n const config = manifestRes.manifest.config as\n | { digest?: unknown }\n | undefined;\n if (!config || typeof config.digest !== 'string') {\n throw new Error('manifest has no config descriptor');\n }\n const configBlob = await fetchBlobJson(\n parsed.registry,\n parsed.name,\n config.digest,\n authHeader,\n fetchImpl,\n );\n const c = (configBlob.config ?? {}) as Record<string, unknown>;\n\n const out: ImageInfo = {\n image: `${parsed.registry}/${parsed.name}${parsed.digest ? '@' + parsed.digest : ':' + (parsed.tag ?? 'latest')}`,\n digest: manifestRes.digest ?? parsed.digest ?? null,\n ports: pickPorts(c.ExposedPorts),\n env: parseEnv(c.Env),\n cmd: Array.isArray(c.Cmd) ? (c.Cmd as string[]) : null,\n entrypoint: Array.isArray(c.Entrypoint)\n ? (c.Entrypoint as string[])\n : null,\n user: typeof c.User === 'string' ? c.User : '',\n workingDir: typeof c.WorkingDir === 'string' ? c.WorkingDir : '',\n healthcheck:\n c.Healthcheck !== null &&\n typeof c.Healthcheck === 'object' &&\n !Array.isArray(c.Healthcheck)\n ? (c.Healthcheck as Record<string, unknown>)\n : null,\n labels:\n c.Labels !== null &&\n typeof c.Labels === 'object' &&\n !Array.isArray(c.Labels)\n ? (c.Labels as Record<string, string>)\n : null,\n volumes:\n c.Volumes !== null &&\n typeof c.Volumes === 'object' &&\n !Array.isArray(c.Volumes)\n ? (c.Volumes as Record<string, unknown>)\n : null,\n suggestedTmpfs: [],\n };\n out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [\n ...(out.cmd ?? []),\n ...(out.entrypoint ?? []),\n ]);\n\n return out;\n } catch (err) {\n logger(`inspect-image: ${formatErrorChain(err)}`);\n return null;\n }\n}\n\n/**\n * Walk an Error's `cause` chain and join all message strings. undici wraps\n * connection errors (including SSRF blocks from our custom Dispatcher) in\n * a fetch-side TypeError with the underlying cause nested via `.cause`.\n * Surfacing the chain in the logger gives the user the real reason (e.g.,\n * \"SSRF blocked: 127.0.0.1 ... loopback\") instead of an opaque\n * \"fetch failed\".\n */\nfunction formatErrorChain(err: unknown): string {\n const parts: string[] = [];\n let current: unknown = err;\n let depth = 0;\n // Defensive bound — sane Error chains are <5 levels; cap at 10 to avoid\n // pathological cycles.\n while (current !== null && current !== undefined && depth < 10) {\n if (current instanceof Error) {\n parts.push(current.message);\n current = (current as Error & { cause?: unknown }).cause;\n } else {\n parts.push(String(current));\n current = undefined;\n }\n depth += 1;\n }\n return parts.join(' | ');\n}\n\nlet cachedDefaultFetch: typeof fetch | undefined;\nfunction createDefaultGuardedFetch(): typeof fetch {\n if (!cachedDefaultFetch) {\n cachedDefaultFetch = createGuardedFetch();\n }\n return cachedDefaultFetch;\n}\n\nfunction parseRef(ref: string): ParsedRef {\n // \"<reg>/<name>@sha256:<digest>\" | \"<reg>/<name>:<tag>\" | \"<name>\" | \"<name>:<tag>\"\n let registry = 'docker.io';\n let name: string;\n let tag: string | null = null;\n let digest: string | null = null;\n\n let rest = ref;\n const atIdx = rest.indexOf('@');\n if (atIdx >= 0) {\n digest = rest.slice(atIdx + 1);\n rest = rest.slice(0, atIdx);\n }\n\n // Detect registry segment: head before first `/` is a registry only if\n // it has a `.` or `:` (port) or is `localhost`.\n const firstSlash = rest.indexOf('/');\n if (firstSlash > 0) {\n const head = rest.slice(0, firstSlash);\n if (head === 'localhost' || head.includes('.') || head.includes(':')) {\n registry = head;\n rest = rest.slice(firstSlash + 1);\n }\n }\n\n if (!digest) {\n const colonIdx = rest.lastIndexOf(':');\n if (colonIdx >= 0) {\n tag = rest.slice(colonIdx + 1);\n name = rest.slice(0, colonIdx);\n } else {\n name = rest;\n tag = 'latest';\n }\n } else {\n name = rest;\n }\n\n // Docker Hub library prefix for single-segment names (\"nginx\" → \"library/nginx\").\n if (registry === 'docker.io' && !name.includes('/')) {\n name = `library/${name}`;\n }\n\n // Validate URL-interpolated fields against OCI Distribution Spec grammar\n // BEFORE the URL is constructed. The ref strings reach the user via\n // `DeploySpec.image`, so malformed input must be rejected here.\n for (const component of name.split('/')) {\n if (!OCI_NAME_COMPONENT.test(component)) {\n throw new Error(`invalid name component \"${component}\" in image ref`);\n }\n }\n if (tag !== null && !OCI_TAG.test(tag)) {\n throw new Error(`invalid tag \"${tag}\" in image ref`);\n }\n if (digest !== null && !OCI_DIGEST.test(digest)) {\n throw new Error(\n `invalid digest \"${digest}\" in image ref (expected sha256:<64-hex>)`,\n );\n }\n\n return { registry, name, tag, digest };\n}\n\nfunction registryHost(registry: string): string {\n // Docker Hub's image API lives at registry-1.docker.io even though the\n // canonical \"registry\" name is docker.io.\n return registry === 'docker.io' ? 'registry-1.docker.io' : registry;\n}\n\nasync function getDockerHubToken(\n name: string,\n fetchImpl: typeof fetch,\n): Promise<string> {\n // Docker Hub requires anonymous access still go through a token grant.\n // Surface 429 specifically — anonymous pulls are rate-limited per-IP\n // and a 60-min wait fixes it. Without this special case the user sees\n // the same fail-soft `null` outcome as a hard 401 with no signal that\n // the situation is temporary.\n const res = await capturingFetch(\n `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`,\n {},\n fetchImpl,\n );\n if (res.status === 429) {\n throw new Error(\n 'Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)',\n );\n }\n if (res.status !== 200) {\n throw new Error(`Docker Hub token: HTTP ${res.status}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('Docker Hub token: invalid JSON');\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n typeof (parsed as { token?: unknown }).token !== 'string'\n ) {\n throw new Error('Docker Hub token: missing `token` in response');\n }\n return (parsed as { token: string }).token;\n}\n\nasync function fetchManifest(\n registry: string,\n name: string,\n ref: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{\n manifest: Record<string, unknown>;\n contentType: string;\n digest: string | null;\n}> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/manifests/${ref}`;\n const headers: Record<string, string> = { Accept: ACCEPT_MANIFEST };\n if (authHeader) headers.Authorization = authHeader;\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `registry returned ${res.status} on manifest fetch (auth required? private registry?)`,\n );\n }\n if (res.status === 404) {\n // Digest-pinned refs use `@sha256:...`; tag refs use `:tag`. Pick the\n // right separator so the error message doesn't show\n // `registry/name:sha256:...` mistakenly.\n const sep = ref.startsWith('sha256:') ? '@' : ':';\n throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);\n }\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on manifest fetch`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('manifest is not valid JSON');\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('manifest is not a JSON object');\n }\n return {\n manifest: parsed as Record<string, unknown>,\n contentType: res.headers.get('content-type') ?? '',\n digest: res.headers.get('docker-content-digest'),\n };\n}\n\nasync function fetchBlobJson(\n registry: string,\n name: string,\n digest: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{ config?: unknown }> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/blobs/${digest}`;\n const headers: Record<string, string> = {};\n if (authHeader) headers.Authorization = authHeader;\n // undici fetch follows redirects by default; registries 307 → CDN.\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on blob fetch`);\n }\n try {\n return JSON.parse(res.body) as { config?: unknown };\n } catch {\n throw new Error('blob is not valid JSON');\n }\n}\n\ninterface CapturedResponse {\n status: number;\n headers: Headers;\n body: string;\n}\n\n/**\n * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed\n * body-size cap. Throws on overflow, timeout, or read error so the outer\n * try/catch produces the fail-soft `null` return.\n */\nasync function capturingFetch(\n url: string,\n init: RequestInit,\n fetchImpl: typeof fetch,\n): Promise<CapturedResponse> {\n const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetchImpl(url, { ...init, signal });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new Error(`request timeout on ${url}`);\n }\n throw err;\n }\n // Stream the body with a manual chunk-accumulation cap. Avoids the\n // unbounded `await response.text()` path that would let a hostile\n // registry exhaust memory.\n const reader = response.body?.getReader();\n if (!reader) {\n return { status: response.status, headers: response.headers, body: '' };\n }\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const decoder = new TextDecoder();\n let body = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n totalBytes += value.length;\n if (totalBytes > MAX_BODY_BYTES) {\n await reader.cancel();\n throw new Error(\n `response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`,\n );\n }\n chunks.push(value);\n }\n }\n body = decoder.decode(concatUint8Arrays(chunks));\n } finally {\n reader.releaseLock();\n }\n return { status: response.status, headers: response.headers, body };\n}\n\nfunction concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) return new Uint8Array(0);\n if (chunks.length === 1) {\n const only = chunks[0];\n if (only !== undefined) return only;\n }\n let total = 0;\n for (const c of chunks) total += c.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n}\n\nfunction pickPorts(exposedPorts: unknown): string[] {\n if (\n exposedPorts === null ||\n typeof exposedPorts !== 'object' ||\n Array.isArray(exposedPorts)\n ) {\n return [];\n }\n return Object.keys(exposedPorts as Record<string, unknown>).sort();\n}\n\nfunction parseEnv(env: unknown): Record<string, string> {\n if (!Array.isArray(env)) return {};\n const out: Record<string, string> = {};\n for (const kv of env) {\n if (typeof kv !== 'string') continue;\n const i = kv.indexOf('=');\n if (i > 0) {\n const key = kv.slice(0, i);\n const value = kv.slice(i + 1);\n out[key] = value;\n } else {\n out[kv] = '';\n }\n }\n return out;\n}\n\nfunction isManifestIndex(m: Record<string, unknown>): boolean {\n return Array.isArray(m.manifests);\n}\n\nfunction pickPlatformManifest(\n index: Record<string, unknown>,\n): Record<string, unknown> | null {\n const list = index.manifests;\n if (!Array.isArray(list)) return null;\n const linuxAmd64 = list.find(\n (m): m is Record<string, unknown> =>\n m !== null &&\n typeof m === 'object' &&\n (m as { platform?: unknown }).platform !== null &&\n typeof (m as { platform?: unknown }).platform === 'object' &&\n (m as { platform: { os?: unknown } }).platform.os === 'linux' &&\n (m as { platform: { architecture?: unknown } }).platform.architecture ===\n 'amd64',\n );\n if (linuxAmd64) return linuxAmd64;\n // Fall back to first entry.\n const first = list[0];\n if (first !== null && typeof first === 'object' && !Array.isArray(first)) {\n return first as Record<string, unknown>;\n }\n return null;\n}\n\nfunction suggestedTmpfsFor(\n name: string,\n cmdAndEntrypoint: ReadonlyArray<string>,\n): string[] {\n const haystack = [name, ...cmdAndEntrypoint].join(' ').toLowerCase();\n for (const hint of TMPFS_HINTS) {\n if (haystack.includes(hint.match)) return [...hint.paths];\n }\n return [];\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"lease-items.d.ts","names":[],"sources":["../../src/internals/lease-items.ts"],"mappings":";;AAiBA;;;;;AASA;;;;;AAmBA;;;;;UA5BiB,mBAAA;EACf,WAAA;EACA,YAAA;AAAA;;;;;iBAOc,eAAA,CAAgB,OAAA;;;;;;iBAmBhB,aAAA,CAAc,GAAA,YAAe,mBAAA;;;;;;;;;;;iBA2B7B,SAAA,CAAU,OAAA,WAAkB,SAAA"}
1
+ {"version":3,"file":"lease-items.d.ts","names":[],"sources":["../../src/internals/lease-items.ts"],"mappings":";;AAiBA;;;;AAEc;AAOd;;;;AAAgD;AAmBhD;;;;AAAgE;UA5B/C,mBAAA;EACf,WAAA;EACA,YAAY;AAAA;AAqD+C;;;;AAAA,iBA9C7C,eAAA,CAAgB,OAAgB;;;;;;iBAmBhC,aAAA,CAAc,GAAA,YAAe,mBAAmB;;;;;;;;;;;iBA2BhD,SAAA,CAAU,OAAA,WAAkB,SAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"lease-items.js","names":[],"sources":["../../src/internals/lease-items.ts"],"sourcesContent":["/**\n * Shared decoding for `leases_by_tenant` responses. Walks `leases[]`,\n * matches by UUID, normalizes each item's serviceName/customDomain across\n * snake_case/camelCase variants. `verify-domain-state.ts` is the in-package\n * consumer for PR 1; PR 4's `manageDomain` / `troubleshoot` will also consume.\n *\n * Exports:\n * - `pickLeasesArray(payload)` — tolerate `{ leases: [...] }` (current\n * chain shape) and a bare array. Throws on anything else.\n * - `normalizeItem(rawItem)` — return `{ serviceName, customDomain }`\n * with empty-string defaults; accepts both camelCase and snake_case.\n * - `findLease(payload, leaseUuid)` — `pickLeasesArray` + UUID lookup.\n * Case-insensitive; tolerates `uuid` / `lease_uuid` / `leaseUuid` keys.\n * Returns the matched lease object (raw shape) or `null`. Throws\n * `TypeError` when `leaseUuid` is not a string.\n */\n\nexport interface NormalizedLeaseItem {\n serviceName: string;\n customDomain: string;\n}\n\n/**\n * Tolerate either `{ leases: [...] }` (current chain shape) or a bare\n * array. Throws on anything else.\n */\nexport function pickLeasesArray(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (\n payload !== null &&\n typeof payload === 'object' &&\n Array.isArray((payload as { leases?: unknown }).leases)\n ) {\n return (payload as { leases: unknown[] }).leases;\n }\n throw new Error(\n 'leases_by_tenant response: expected `leases[]` array or bare array',\n );\n}\n\n/**\n * Normalize a raw lease-item record (chain snake_case OR proto-decoded\n * camelCase) into `{ serviceName, customDomain }` with empty-string\n * defaults on missing fields.\n */\nexport function normalizeItem(raw: unknown): NormalizedLeaseItem {\n if (raw === null || typeof raw !== 'object') {\n return { serviceName: '', customDomain: '' };\n }\n const r = raw as {\n serviceName?: unknown;\n service_name?: unknown;\n customDomain?: unknown;\n custom_domain?: unknown;\n };\n const serviceName =\n readStringOrEmpty(r.serviceName) || readStringOrEmpty(r.service_name);\n const customDomain =\n readStringOrEmpty(r.customDomain) || readStringOrEmpty(r.custom_domain);\n return { serviceName, customDomain };\n}\n\n/**\n * Find a lease by UUID inside a `leases_by_tenant` response. Lookup is\n * case-insensitive and tolerates `uuid`, `lease_uuid`, or `leaseUuid`\n * fields on the lease object. Returns the raw lease record or `null`.\n *\n * Throws `TypeError` if `leaseUuid` is not a string. Both production\n * callers (verify-domain-state, future manageDomain) pre-validate against\n * a UUID regex, but the helper guards anyway — a clear error beats a\n * \"Cannot read properties of …\" stack trace.\n */\nexport function findLease(payload: unknown, leaseUuid: string): unknown | null {\n if (typeof leaseUuid !== 'string') {\n const got = leaseUuid === null ? 'null' : typeof leaseUuid;\n throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);\n }\n const leases = pickLeasesArray(payload);\n const target = leaseUuid.toLowerCase();\n for (const lease of leases) {\n if (lease === null || typeof lease !== 'object') continue;\n const r = lease as {\n uuid?: unknown;\n lease_uuid?: unknown;\n leaseUuid?: unknown;\n };\n const u = r.uuid ?? r.lease_uuid ?? r.leaseUuid;\n if (typeof u === 'string' && u.toLowerCase() === target) {\n return lease;\n }\n }\n return null;\n}\n\nfunction readStringOrEmpty(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],"mappings":";;;;;AA0BA,SAAgB,gBAAgB,SAA6B;AAC3D,KAAI,MAAM,QAAQ,QAAQ,CAAE,QAAO;AACnC,KACE,YAAY,QACZ,OAAO,YAAY,YACnB,MAAM,QAAS,QAAiC,OAAO,CAEvD,QAAQ,QAAkC;AAE5C,OAAM,IAAI,MACR,qEACD;;;;;;;AAQH,SAAgB,cAAc,KAAmC;AAC/D,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,QAAO;EAAE,aAAa;EAAI,cAAc;EAAI;CAE9C,MAAM,IAAI;AAUV,QAAO;EAAE,aAHP,kBAAkB,EAAE,YAAY,IAAI,kBAAkB,EAAE,aAAa;EAGjD,cADpB,kBAAkB,EAAE,aAAa,IAAI,kBAAkB,EAAE,cAAc;EACrC;;;;;;;;;;;;AAatC,SAAgB,UAAU,SAAkB,WAAmC;AAC7E,KAAI,OAAO,cAAc,SAEvB,OAAM,IAAI,UAAU,8CADR,cAAc,OAAO,SAAS,OAAO,YACuB;CAE1E,MAAM,SAAS,gBAAgB,QAAQ;CACvC,MAAM,SAAS,UAAU,aAAa;AACtC,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU;EACjD,MAAM,IAAI;EAKV,MAAM,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE;AACtC,MAAI,OAAO,MAAM,YAAY,EAAE,aAAa,KAAK,OAC/C,QAAO;;AAGX,QAAO;;AAGT,SAAS,kBAAkB,OAAwB;AACjD,QAAO,OAAO,UAAU,WAAW,QAAQ"}
1
+ {"version":3,"file":"lease-items.js","names":[],"sources":["../../src/internals/lease-items.ts"],"sourcesContent":["/**\n * Shared decoding for `leases_by_tenant` responses. Walks `leases[]`,\n * matches by UUID, normalizes each item's serviceName/customDomain across\n * snake_case/camelCase variants. `verify-domain-state.ts` is the in-package\n * consumer for PR 1; PR 4's `manageDomain` / `troubleshoot` will also consume.\n *\n * Exports:\n * - `pickLeasesArray(payload)` — tolerate `{ leases: [...] }` (current\n * chain shape) and a bare array. Throws on anything else.\n * - `normalizeItem(rawItem)` — return `{ serviceName, customDomain }`\n * with empty-string defaults; accepts both camelCase and snake_case.\n * - `findLease(payload, leaseUuid)` — `pickLeasesArray` + UUID lookup.\n * Case-insensitive; tolerates `uuid` / `lease_uuid` / `leaseUuid` keys.\n * Returns the matched lease object (raw shape) or `null`. Throws\n * `TypeError` when `leaseUuid` is not a string.\n */\n\nexport interface NormalizedLeaseItem {\n serviceName: string;\n customDomain: string;\n}\n\n/**\n * Tolerate either `{ leases: [...] }` (current chain shape) or a bare\n * array. Throws on anything else.\n */\nexport function pickLeasesArray(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (\n payload !== null &&\n typeof payload === 'object' &&\n Array.isArray((payload as { leases?: unknown }).leases)\n ) {\n return (payload as { leases: unknown[] }).leases;\n }\n throw new Error(\n 'leases_by_tenant response: expected `leases[]` array or bare array',\n );\n}\n\n/**\n * Normalize a raw lease-item record (chain snake_case OR proto-decoded\n * camelCase) into `{ serviceName, customDomain }` with empty-string\n * defaults on missing fields.\n */\nexport function normalizeItem(raw: unknown): NormalizedLeaseItem {\n if (raw === null || typeof raw !== 'object') {\n return { serviceName: '', customDomain: '' };\n }\n const r = raw as {\n serviceName?: unknown;\n service_name?: unknown;\n customDomain?: unknown;\n custom_domain?: unknown;\n };\n const serviceName =\n readStringOrEmpty(r.serviceName) || readStringOrEmpty(r.service_name);\n const customDomain =\n readStringOrEmpty(r.customDomain) || readStringOrEmpty(r.custom_domain);\n return { serviceName, customDomain };\n}\n\n/**\n * Find a lease by UUID inside a `leases_by_tenant` response. Lookup is\n * case-insensitive and tolerates `uuid`, `lease_uuid`, or `leaseUuid`\n * fields on the lease object. Returns the raw lease record or `null`.\n *\n * Throws `TypeError` if `leaseUuid` is not a string. Both production\n * callers (verify-domain-state, future manageDomain) pre-validate against\n * a UUID regex, but the helper guards anyway — a clear error beats a\n * \"Cannot read properties of …\" stack trace.\n */\nexport function findLease(payload: unknown, leaseUuid: string): unknown | null {\n if (typeof leaseUuid !== 'string') {\n const got = leaseUuid === null ? 'null' : typeof leaseUuid;\n throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);\n }\n const leases = pickLeasesArray(payload);\n const target = leaseUuid.toLowerCase();\n for (const lease of leases) {\n if (lease === null || typeof lease !== 'object') continue;\n const r = lease as {\n uuid?: unknown;\n lease_uuid?: unknown;\n leaseUuid?: unknown;\n };\n const u = r.uuid ?? r.lease_uuid ?? r.leaseUuid;\n if (typeof u === 'string' && u.toLowerCase() === target) {\n return lease;\n }\n }\n return null;\n}\n\nfunction readStringOrEmpty(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],"mappings":";;;;;AA0BA,SAAgB,gBAAgB,SAA6B;CAC3D,IAAI,MAAM,QAAQ,OAAO,GAAG,OAAO;CACnC,IACE,YAAY,QACZ,OAAO,YAAY,YACnB,MAAM,QAAS,QAAiC,MAAM,GAEtD,OAAQ,QAAkC;CAE5C,MAAM,IAAI,MACR,oEACF;AACF;;;;;;AAOA,SAAgB,cAAc,KAAmC;CAC/D,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UACjC,OAAO;EAAE,aAAa;EAAI,cAAc;CAAG;CAE7C,MAAM,IAAI;CAUV,OAAO;EAAE,aAHP,kBAAkB,EAAE,WAAW,KAAK,kBAAkB,EAAE,YAAY;EAGhD,cADpB,kBAAkB,EAAE,YAAY,KAAK,kBAAkB,EAAE,aAAa;CACrC;AACrC;;;;;;;;;;;AAYA,SAAgB,UAAU,SAAkB,WAAmC;CAC7E,IAAI,OAAO,cAAc,UAEvB,MAAM,IAAI,UAAU,8CADR,cAAc,OAAO,SAAS,OAAO,WACsB;CAEzE,MAAM,SAAS,gBAAgB,OAAO;CACtC,MAAM,SAAS,UAAU,YAAY;CACrC,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EACjD,MAAM,IAAI;EAKV,MAAM,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE;EACtC,IAAI,OAAO,MAAM,YAAY,EAAE,YAAY,MAAM,QAC/C,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAwB;CACjD,OAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"lease-state.d.ts","names":[],"sources":["../../src/internals/lease-state.ts"],"mappings":";;;cAyCa,eAAA,EAAiB,WAAA,CAAY,cAAA;;AAA1C;;;;;AAmCA;;;;;AAcA;;;;;;;;;;;iBAdgB,MAAA,CACd,KAAA,gCACC,cAAA;;iBAYa,UAAA,CAAW,IAAA"}
1
+ {"version":3,"file":"lease-state.d.ts","names":[],"sources":["../../src/internals/lease-state.ts"],"mappings":";;;cAyCa,eAAA,EAAiB,WAAW,CAAC,cAAA;;AAA1C;;;;AAAwD;AAmCxD;;;;AAEiB;AAYjB;;;;AAAmD;;;;;;;iBAdnC,MAAA,CACd,KAAA,gCACC,cAAc;;iBAYD,UAAA,CAAW,IAAwB"}
@@ -1 +1 @@
1
- {"version":3,"file":"lease-state.js","names":[],"sources":["../../src/internals/lease-state.ts"],"sourcesContent":["import type { LeaseStateName } from '../types.js';\n\n/**\n * Canonical chain lease-state enum table + decode helpers, used by\n * `classify-deploy-response.ts` and (later) `deploy-app.ts` /\n * `close-lease.ts` / `troubleshoot.ts` to translate integer state codes\n * the chain emits into the typed `LeaseStateName` literal union.\n *\n * Aligned with the current `@manifest-network/manifestjs@2.4.1` `LeaseState`\n * proto (see\n * `node_modules/@manifest-network/manifestjs/dist/codegen/liftedinit/billing/v1/types.d.ts`):\n *\n * ```\n * 0 → LEASE_STATE_UNSPECIFIED\n * 1 → LEASE_STATE_PENDING — lease awaiting provider acknowledgement; credit locked, billing not started\n * 2 → LEASE_STATE_ACTIVE — provider acknowledged, resources provisioned, billing accruing\n * 3 → LEASE_STATE_CLOSED — lease closed normally; final settlement occurred\n * 4 → LEASE_STATE_REJECTED — provider rejected the lease; credit returned to tenant\n * 5 → LEASE_STATE_EXPIRED — lease expired while in PENDING (provider did not acknowledge within the timeout); credit returned. Pre-active terminal state, NOT a post-active expiry\n * ```\n *\n * The proto's `UNRECOGNIZED = -1` enum convenience is NOT included in the\n * STATES map — that value is a TS-enum sentinel for \"unknown decode,\" never\n * a chain emit.\n *\n * `LEASE_STATE_INSUFFICIENT_FUNDS` is retained as an unreachable variant in\n * the frozen `LeaseStateName` union and in `TERMINAL_STATES` for forward-\n * compat — the chain doesn't emit it under v2.1.0, but the public type\n * permits it and the no-op set entry guards against a future chain\n * regression that re-emits it (defense-in-depth).\n */\n\nconst STATES = {\n 0: 'LEASE_STATE_UNSPECIFIED',\n 1: 'LEASE_STATE_PENDING',\n 2: 'LEASE_STATE_ACTIVE',\n 3: 'LEASE_STATE_CLOSED',\n 4: 'LEASE_STATE_REJECTED',\n 5: 'LEASE_STATE_EXPIRED',\n} as const satisfies Record<number, LeaseStateName>;\n\nexport const TERMINAL_STATES: ReadonlySet<LeaseStateName> =\n new Set<LeaseStateName>([\n 'LEASE_STATE_CLOSED',\n 'LEASE_STATE_REJECTED',\n 'LEASE_STATE_EXPIRED',\n // Retained as defense-in-depth: unreachable from decode() on the\n // current chain (v2.1.0 proto drops INSUFFICIENT_FUNDS), but still a\n // legal LeaseStateName variant. If a future chain regression re-emits\n // it, terminal-state checks downstream still classify correctly without\n // a coordinated update across deploy-app / close-lease / troubleshoot.\n 'LEASE_STATE_INSUFFICIENT_FUNDS',\n ]);\n\n/**\n * Decode an integer-or-string lease state into the canonical\n * `LEASE_STATE_*` name. Returns `undefined` for unrecognized input so\n * callers can distinguish \"no info\" from a known state. Callers that\n * need a display sentinel may widen the return type via `|| 'UNKNOWN'`\n * (or similar) for logging/UI purposes only — `'UNKNOWN'` is NOT a\n * `LeaseStateName` variant, so the widening explicitly opts out of the\n * type narrowing the union provides.\n *\n * Accepts either:\n * - a numeric (or numeric-coercible string) integer matching a STATES key,\n * - a string that already starts with `LEASE_STATE_` (passthrough), in\n * which case the value is returned verbatim — the chain's JSON form\n * emits the name directly via `leaseStateToJSON`, so this branch\n * handles both the codec.toJSON() shape and raw integer emit paths.\n *\n * Unrecognized strings (no `LEASE_STATE_` prefix) and out-of-range integers\n * return `undefined`. The passthrough does not validate the suffix against\n * `LeaseStateName` because the chain proto is the source of truth — if a\n * future enum variant appears, it'll flow through; if a malformed string\n * appears, the caller's typed handling catches it.\n */\nexport function decode(\n state: number | string | undefined,\n): LeaseStateName | undefined {\n if (typeof state === 'string' && state.startsWith('LEASE_STATE_')) {\n return state as LeaseStateName;\n }\n const n = Number(state);\n if (Number.isInteger(n) && n in STATES) {\n return STATES[n as keyof typeof STATES];\n }\n return undefined;\n}\n\n/** True iff `name` is in the `TERMINAL_STATES` set. Accepts any string for caller convenience. */\nexport function isTerminal(name: string | undefined): boolean {\n if (typeof name !== 'string') return false;\n return TERMINAL_STATES.has(name as LeaseStateName);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,SAAS;CACb,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACJ;AAED,MAAa,kBACX,IAAI,IAAoB;CACtB;CACA;CACA;CAMA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBJ,SAAgB,OACd,OAC4B;AAC5B,KAAI,OAAO,UAAU,YAAY,MAAM,WAAW,eAAe,CAC/D,QAAO;CAET,MAAM,IAAI,OAAO,MAAM;AACvB,KAAI,OAAO,UAAU,EAAE,IAAI,KAAK,OAC9B,QAAO,OAAO;;;AAMlB,SAAgB,WAAW,MAAmC;AAC5D,KAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAO,gBAAgB,IAAI,KAAuB"}
1
+ {"version":3,"file":"lease-state.js","names":[],"sources":["../../src/internals/lease-state.ts"],"sourcesContent":["import type { LeaseStateName } from '../types.js';\n\n/**\n * Canonical chain lease-state enum table + decode helpers, used by\n * `classify-deploy-response.ts` and (later) `deploy-app.ts` /\n * `close-lease.ts` / `troubleshoot.ts` to translate integer state codes\n * the chain emits into the typed `LeaseStateName` literal union.\n *\n * Aligned with the current `@manifest-network/manifestjs@2.4.1` `LeaseState`\n * proto (see\n * `node_modules/@manifest-network/manifestjs/dist/codegen/liftedinit/billing/v1/types.d.ts`):\n *\n * ```\n * 0 → LEASE_STATE_UNSPECIFIED\n * 1 → LEASE_STATE_PENDING — lease awaiting provider acknowledgement; credit locked, billing not started\n * 2 → LEASE_STATE_ACTIVE — provider acknowledged, resources provisioned, billing accruing\n * 3 → LEASE_STATE_CLOSED — lease closed normally; final settlement occurred\n * 4 → LEASE_STATE_REJECTED — provider rejected the lease; credit returned to tenant\n * 5 → LEASE_STATE_EXPIRED — lease expired while in PENDING (provider did not acknowledge within the timeout); credit returned. Pre-active terminal state, NOT a post-active expiry\n * ```\n *\n * The proto's `UNRECOGNIZED = -1` enum convenience is NOT included in the\n * STATES map — that value is a TS-enum sentinel for \"unknown decode,\" never\n * a chain emit.\n *\n * `LEASE_STATE_INSUFFICIENT_FUNDS` is retained as an unreachable variant in\n * the frozen `LeaseStateName` union and in `TERMINAL_STATES` for forward-\n * compat — the chain doesn't emit it under v2.1.0, but the public type\n * permits it and the no-op set entry guards against a future chain\n * regression that re-emits it (defense-in-depth).\n */\n\nconst STATES = {\n 0: 'LEASE_STATE_UNSPECIFIED',\n 1: 'LEASE_STATE_PENDING',\n 2: 'LEASE_STATE_ACTIVE',\n 3: 'LEASE_STATE_CLOSED',\n 4: 'LEASE_STATE_REJECTED',\n 5: 'LEASE_STATE_EXPIRED',\n} as const satisfies Record<number, LeaseStateName>;\n\nexport const TERMINAL_STATES: ReadonlySet<LeaseStateName> =\n new Set<LeaseStateName>([\n 'LEASE_STATE_CLOSED',\n 'LEASE_STATE_REJECTED',\n 'LEASE_STATE_EXPIRED',\n // Retained as defense-in-depth: unreachable from decode() on the\n // current chain (v2.1.0 proto drops INSUFFICIENT_FUNDS), but still a\n // legal LeaseStateName variant. If a future chain regression re-emits\n // it, terminal-state checks downstream still classify correctly without\n // a coordinated update across deploy-app / close-lease / troubleshoot.\n 'LEASE_STATE_INSUFFICIENT_FUNDS',\n ]);\n\n/**\n * Decode an integer-or-string lease state into the canonical\n * `LEASE_STATE_*` name. Returns `undefined` for unrecognized input so\n * callers can distinguish \"no info\" from a known state. Callers that\n * need a display sentinel may widen the return type via `|| 'UNKNOWN'`\n * (or similar) for logging/UI purposes only — `'UNKNOWN'` is NOT a\n * `LeaseStateName` variant, so the widening explicitly opts out of the\n * type narrowing the union provides.\n *\n * Accepts either:\n * - a numeric (or numeric-coercible string) integer matching a STATES key,\n * - a string that already starts with `LEASE_STATE_` (passthrough), in\n * which case the value is returned verbatim — the chain's JSON form\n * emits the name directly via `leaseStateToJSON`, so this branch\n * handles both the codec.toJSON() shape and raw integer emit paths.\n *\n * Unrecognized strings (no `LEASE_STATE_` prefix) and out-of-range integers\n * return `undefined`. The passthrough does not validate the suffix against\n * `LeaseStateName` because the chain proto is the source of truth — if a\n * future enum variant appears, it'll flow through; if a malformed string\n * appears, the caller's typed handling catches it.\n */\nexport function decode(\n state: number | string | undefined,\n): LeaseStateName | undefined {\n if (typeof state === 'string' && state.startsWith('LEASE_STATE_')) {\n return state as LeaseStateName;\n }\n const n = Number(state);\n if (Number.isInteger(n) && n in STATES) {\n return STATES[n as keyof typeof STATES];\n }\n return undefined;\n}\n\n/** True iff `name` is in the `TERMINAL_STATES` set. Accepts any string for caller convenience. */\nexport function isTerminal(name: string | undefined): boolean {\n if (typeof name !== 'string') return false;\n return TERMINAL_STATES.has(name as LeaseStateName);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,SAAS;CACb,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;AACL;AAEA,MAAa,kBACX,IAAI,IAAoB;CACtB;CACA;CACA;CAMA;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBH,SAAgB,OACd,OAC4B;CAC5B,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,cAAc,GAC9D,OAAO;CAET,MAAM,IAAI,OAAO,KAAK;CACtB,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,QAC9B,OAAO,OAAO;AAGlB;;AAGA,SAAgB,WAAW,MAAmC;CAC5D,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,OAAO,gBAAgB,IAAI,IAAsB;AACnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-deployment-plan.d.ts","names":[],"sources":["../../src/internals/render-deployment-plan.ts"],"mappings":";;UAgHiB,yBAAA;;EAEf,IAAA,EAAM,IAAA;EAFkC;EAIxC,QAAA,GAAW,QAAA;EAAQ;EAEnB,KAAA;EAJM;EAMN,IAAA;EAJW;EAMX,QAAA;EAFA;EAIA,YAAA;EAAA;EAEA,mBAAA;AAAA;AAAA,iBAGc,oBAAA,CACd,KAAA,EAAO,yBAAA,GACN,mBAAA"}
1
+ {"version":3,"file":"render-deployment-plan.d.ts","names":[],"sources":["../../src/internals/render-deployment-plan.ts"],"mappings":";;UAgHiB,yBAAA;;EAEf,IAAA,EAAM,IAAA;EAFkC;EAIxC,QAAA,GAAW,QAAQ;EAAA;EAEnB,KAAA;EAJM;EAMN,IAAA;EAJW;EAMX,QAAA;EAFA;EAIA,YAAA;EAAA;EAEA,mBAAA;AAAA;AAAA,iBAGc,oBAAA,CACd,KAAA,EAAO,yBAAA,GACN,mBAAmB"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-deployment-plan.js","names":[],"sources":["../../src/internals/render-deployment-plan.ts"],"sourcesContent":["import type { DeploymentPlanBlock, FeeEstimate, Plan } from '../types.js';\nimport {\n type DenomMap,\n EMPTY_DENOM_MAP,\n humanizeBalances,\n humanizeCoin,\n} from './humanize-denom.js';\n\n/**\n * Render the canonical `DeploymentPlan` block for `deployApp`'s\n * confirmation step. Consumes the typed `Plan` + `FeeEstimate {coins, gas}`\n * shape.\n *\n * **Why this is a renderer, not a builder:** the function consumes a\n * fully-resolved `Plan` (summary + readiness + fees) plus orchestrator-\n * supplied trim data (image / size / metaHash / customDomain). It\n * doesn't compose those inputs — `deployApp.ts` (commit B) constructs\n * the `Plan` from chain queries + estimates and threads it here.\n *\n * **Sync, pure-decision function** (per Q4 Bii pattern): no I/O, no\n * mutation, no implicit lookups. Caller pre-loads the `DenomMap` via\n * `await loadChainDenomMap(chainDataFile)` and passes it in. Default\n * fallback is the no-op `EMPTY_DENOM_MAP` — raw on-chain denoms render\n * verbatim. The `(empty)` literal continues to mark missing balances.\n *\n * **Fee humanization:** the new `FeeEstimate {coins: Coin[], gas}` shape\n * preserves multi-coin precision. The CJS read pre-humanized strings\n * (`--tx-fee \"0.0023 MFX\"`); the TS port humanizes `fees.coins[0]` at\n * render time using `humanizeCoin`, then concatenates with `(gas <n>)`.\n * Multi-coin fees: humanizes all coins with `humanizeBalances` (comma-\n * separated) and renders the result verbatim — gas suffix is appended\n * once.\n *\n * **`setDomain` fee sentinel:** when `plan.fees.setDomain` is the\n * `{notEstimated: true, reason}` sentinel (approach-3 no-representative-\n * lease fallback), the line emits the explicit \"(not estimated — no\n * representative lease...)\" message preserving the CJS's user-facing\n * \"skipped\" semantics.\n *\n * Provider line is intentionally absent (chain selects internally; format-\n * success.ts emits it post-deploy).\n */\n\n/**\n * Same-denom single-coin: sum as `BigInt` (the underlying on-chain\n * unit), then humanize the total. Different denom OR multi-coin:\n * `\"<a> + <b>\"` concat (mirrors the CJS's `sumHumanFees` fallback).\n *\n * Copilot review fix (PR #58 r3250445951): the prior `sumHumanFees`\n * parsed humanized strings to float64, summed, and re-formatted —\n * breaking the BigInt invariant the rest of the denom-humanization\n * pipeline maintains (`humanize-denom.ts:_fmtScaledAmount` is\n * BigInt-based). Realistic create-lease + set-domain fees were tiny\n * so the hit rate was low; the inconsistency was real, and amounts\n * above `Number.MAX_SAFE_INTEGER` (2^53-1) would silently round.\n *\n * Operates on the underlying `FeeEstimate.coins` arrays directly so\n * BigInt precision is preserved through the sum. Humanization\n * happens once, at the end.\n */\nfunction sumFees(a: FeeEstimate, b: FeeEstimate, denomMap: DenomMap): string {\n // Same-denom single-coin: BigInt sum, then humanize.\n if (a.coins.length === 1 && b.coins.length === 1) {\n const ca = a.coins[0];\n const cb = b.coins[0];\n if (ca && cb && ca.denom === cb.denom) {\n const sum = (BigInt(ca.amount) + BigInt(cb.amount)).toString();\n return humanizeCoin(sum, ca.denom, denomMap);\n }\n }\n // Different denom or multi-coin: fall back to concat, mirroring the\n // CJS's behavior. Humanize each side independently.\n return `${humanizeFeeAmount(a, denomMap)} + ${humanizeFeeAmount(b, denomMap)}`;\n}\n\n/**\n * Render a `FeeEstimate {coins, gas}` as the user-facing fee string.\n * Empty coins → `(empty)` literal (CJS parity). Single coin → humanized\n * `\"<amount> <symbol>\"`. Multi-coin → comma-joined.\n */\nfunction humanizeFeeAmount(fee: FeeEstimate, denomMap: DenomMap): string {\n if (fee.coins.length === 0) return '(empty)';\n if (fee.coins.length === 1) {\n const c = fee.coins[0];\n if (c === undefined) return '(empty)';\n return humanizeCoin(c.amount, c.denom, denomMap);\n }\n return humanizeBalances(fee.coins, denomMap);\n}\n\nfunction formatFeeLine(humanFee: string, gas: number): string {\n return `${humanFee} (gas ${gas})`;\n}\n\nfunction formatSkuPrice(plan: Plan, denomMap: DenomMap): string {\n const sku = plan.readiness.sku;\n if (sku === null) return '(unknown — SKU has no listed price)';\n return `${humanizeCoin(sku.price.amount, sku.price.denom, denomMap)} / hour`;\n}\n\nfunction formatWallet(plan: Plan, denomMap: DenomMap): string {\n return humanizeBalances(plan.readiness.walletBalances, denomMap);\n}\n\nfunction formatCredits(plan: Plan, denomMap: DenomMap): string {\n const credits = plan.readiness.credits;\n if (credits === null) return 'none';\n const balances = credits.availableBalances;\n if (!Array.isArray(balances) || balances.length === 0) return '(empty)';\n return humanizeBalances(balances, denomMap);\n}\n\nexport interface RenderDeploymentPlanInput {\n /** Frozen Plan (summary + readiness + fees). */\n plan: Plan;\n /** Pre-loaded denom map. Default: `EMPTY_DENOM_MAP` (raw on-chain rendering). */\n denomMap?: DenomMap;\n /** Primary image reference — first service's image for stacks. */\n image: string;\n /** SKU tier name (e.g. `docker-micro`, `small`). */\n size: string;\n /** Manifest meta-hash hex from `build_manifest_preview`. */\n metaHash: string;\n /** Optional custom-domain FQDN; presence drives the two-tx fee layout. */\n customDomain?: string;\n /** Optional stack-service holding the custom domain. */\n customDomainService?: string;\n}\n\nexport function renderDeploymentPlan(\n input: RenderDeploymentPlanInput,\n): DeploymentPlanBlock {\n const denomMap = input.denomMap ?? EMPTY_DENOM_MAP;\n const { summary } = input.plan;\n\n const manifestLine =\n `${summary.format ?? 'single'}, services=${summary.serviceCount}, ` +\n `ports=${summary.portCount}, env=${summary.envCount}`;\n\n const hasDomain =\n typeof input.customDomain === 'string' && input.customDomain.length > 0;\n\n // Create-lease fee — always present in PlanFees.\n const createFee = input.plan.fees.createLease;\n const createHuman = humanizeFeeAmount(createFee, denomMap);\n const createFeeLine = formatFeeLine(createHuman, createFee.gas);\n\n const lines: string[] = [\n 'DeploymentPlan',\n ` Image: ${input.image}`,\n ` Size: ${input.size}`,\n ` Manifest: ${manifestLine}`,\n ` meta_hash: ${input.metaHash}`,\n ];\n\n if (hasDomain) {\n const target =\n typeof input.customDomainService === 'string' &&\n input.customDomainService.length > 0\n ? `-> service ${input.customDomainService}`\n : '-> single-service lease';\n lines.push(` Custom domain: ${input.customDomain} ${target}`);\n }\n\n lines.push(\n ` SKU price: ${formatSkuPrice(input.plan, denomMap)}`,\n );\n\n if (hasDomain) {\n // Two-tx layout: labeled lines + Total fee. Honors approach-3\n // `notEstimated` sentinel for set-domain pre-broadcast estimation\n // fallback (no representative lease).\n const setDomain = input.plan.fees.setDomain;\n let setDomainLine: string;\n // Capture the typed `FeeEstimate` reference (when the set-domain\n // fee is a real estimate, not the sentinel) so the total-line\n // BigInt sum can operate on `coins` directly via `sumFees`. The\n // prior code parsed humanized strings to float64 — see\n // `sumFees`'s docstring for the precision-loss rationale.\n let setDomainReal: FeeEstimate | null = null;\n if (setDomain === undefined) {\n setDomainLine =\n '(not estimated — agent skipped pre-broadcast simulation, policy violation)';\n } else if ('notEstimated' in setDomain) {\n setDomainLine = `(not estimated — ${setDomain.reason})`;\n } else {\n setDomainReal = setDomain;\n setDomainLine = formatFeeLine(\n humanizeFeeAmount(setDomain, denomMap),\n setDomain.gas,\n );\n }\n\n lines.push(` Tx fee (create-lease): ${createFeeLine}`);\n lines.push(` Tx fee (set-domain): ${setDomainLine}`);\n\n // Total only when both fees are real numbers. Sentinel set-domain\n // fees fall through to the placeholder.\n const totalLine =\n setDomainReal !== null\n ? sumFees(createFee, setDomainReal, denomMap)\n : '(partial — see fee lines above)';\n lines.push(` Total fee: ${totalLine}`);\n } else {\n lines.push(` Tx fee: ${createFeeLine}`);\n }\n\n lines.push(\n ` Wallet: ${formatWallet(input.plan, denomMap)}`,\n );\n lines.push(\n ` Credits: ${formatCredits(input.plan, denomMap)}`,\n );\n\n return { text: lines.join('\\n') };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-deployment-plan.js","names":[],"sources":["../../src/internals/render-deployment-plan.ts"],"sourcesContent":["import type { DeploymentPlanBlock, FeeEstimate, Plan } from '../types.js';\nimport {\n type DenomMap,\n EMPTY_DENOM_MAP,\n humanizeBalances,\n humanizeCoin,\n} from './humanize-denom.js';\n\n/**\n * Render the canonical `DeploymentPlan` block for `deployApp`'s\n * confirmation step. Consumes the typed `Plan` + `FeeEstimate {coins, gas}`\n * shape.\n *\n * **Why this is a renderer, not a builder:** the function consumes a\n * fully-resolved `Plan` (summary + readiness + fees) plus orchestrator-\n * supplied trim data (image / size / metaHash / customDomain). It\n * doesn't compose those inputs — `deployApp.ts` (commit B) constructs\n * the `Plan` from chain queries + estimates and threads it here.\n *\n * **Sync, pure-decision function** (per Q4 Bii pattern): no I/O, no\n * mutation, no implicit lookups. Caller pre-loads the `DenomMap` via\n * `await loadChainDenomMap(chainDataFile)` and passes it in. Default\n * fallback is the no-op `EMPTY_DENOM_MAP` — raw on-chain denoms render\n * verbatim. The `(empty)` literal continues to mark missing balances.\n *\n * **Fee humanization:** the new `FeeEstimate {coins: Coin[], gas}` shape\n * preserves multi-coin precision. The CJS read pre-humanized strings\n * (`--tx-fee \"0.0023 MFX\"`); the TS port humanizes `fees.coins[0]` at\n * render time using `humanizeCoin`, then concatenates with `(gas <n>)`.\n * Multi-coin fees: humanizes all coins with `humanizeBalances` (comma-\n * separated) and renders the result verbatim — gas suffix is appended\n * once.\n *\n * **`setDomain` fee sentinel:** when `plan.fees.setDomain` is the\n * `{notEstimated: true, reason}` sentinel (approach-3 no-representative-\n * lease fallback), the line emits the explicit \"(not estimated — no\n * representative lease...)\" message preserving the CJS's user-facing\n * \"skipped\" semantics.\n *\n * Provider line is intentionally absent (chain selects internally; format-\n * success.ts emits it post-deploy).\n */\n\n/**\n * Same-denom single-coin: sum as `BigInt` (the underlying on-chain\n * unit), then humanize the total. Different denom OR multi-coin:\n * `\"<a> + <b>\"` concat (mirrors the CJS's `sumHumanFees` fallback).\n *\n * Copilot review fix (PR #58 r3250445951): the prior `sumHumanFees`\n * parsed humanized strings to float64, summed, and re-formatted —\n * breaking the BigInt invariant the rest of the denom-humanization\n * pipeline maintains (`humanize-denom.ts:_fmtScaledAmount` is\n * BigInt-based). Realistic create-lease + set-domain fees were tiny\n * so the hit rate was low; the inconsistency was real, and amounts\n * above `Number.MAX_SAFE_INTEGER` (2^53-1) would silently round.\n *\n * Operates on the underlying `FeeEstimate.coins` arrays directly so\n * BigInt precision is preserved through the sum. Humanization\n * happens once, at the end.\n */\nfunction sumFees(a: FeeEstimate, b: FeeEstimate, denomMap: DenomMap): string {\n // Same-denom single-coin: BigInt sum, then humanize.\n if (a.coins.length === 1 && b.coins.length === 1) {\n const ca = a.coins[0];\n const cb = b.coins[0];\n if (ca && cb && ca.denom === cb.denom) {\n const sum = (BigInt(ca.amount) + BigInt(cb.amount)).toString();\n return humanizeCoin(sum, ca.denom, denomMap);\n }\n }\n // Different denom or multi-coin: fall back to concat, mirroring the\n // CJS's behavior. Humanize each side independently.\n return `${humanizeFeeAmount(a, denomMap)} + ${humanizeFeeAmount(b, denomMap)}`;\n}\n\n/**\n * Render a `FeeEstimate {coins, gas}` as the user-facing fee string.\n * Empty coins → `(empty)` literal (CJS parity). Single coin → humanized\n * `\"<amount> <symbol>\"`. Multi-coin → comma-joined.\n */\nfunction humanizeFeeAmount(fee: FeeEstimate, denomMap: DenomMap): string {\n if (fee.coins.length === 0) return '(empty)';\n if (fee.coins.length === 1) {\n const c = fee.coins[0];\n if (c === undefined) return '(empty)';\n return humanizeCoin(c.amount, c.denom, denomMap);\n }\n return humanizeBalances(fee.coins, denomMap);\n}\n\nfunction formatFeeLine(humanFee: string, gas: number): string {\n return `${humanFee} (gas ${gas})`;\n}\n\nfunction formatSkuPrice(plan: Plan, denomMap: DenomMap): string {\n const sku = plan.readiness.sku;\n if (sku === null) return '(unknown — SKU has no listed price)';\n return `${humanizeCoin(sku.price.amount, sku.price.denom, denomMap)} / hour`;\n}\n\nfunction formatWallet(plan: Plan, denomMap: DenomMap): string {\n return humanizeBalances(plan.readiness.walletBalances, denomMap);\n}\n\nfunction formatCredits(plan: Plan, denomMap: DenomMap): string {\n const credits = plan.readiness.credits;\n if (credits === null) return 'none';\n const balances = credits.availableBalances;\n if (!Array.isArray(balances) || balances.length === 0) return '(empty)';\n return humanizeBalances(balances, denomMap);\n}\n\nexport interface RenderDeploymentPlanInput {\n /** Frozen Plan (summary + readiness + fees). */\n plan: Plan;\n /** Pre-loaded denom map. Default: `EMPTY_DENOM_MAP` (raw on-chain rendering). */\n denomMap?: DenomMap;\n /** Primary image reference — first service's image for stacks. */\n image: string;\n /** SKU tier name (e.g. `docker-micro`, `small`). */\n size: string;\n /** Manifest meta-hash hex from `build_manifest_preview`. */\n metaHash: string;\n /** Optional custom-domain FQDN; presence drives the two-tx fee layout. */\n customDomain?: string;\n /** Optional stack-service holding the custom domain. */\n customDomainService?: string;\n}\n\nexport function renderDeploymentPlan(\n input: RenderDeploymentPlanInput,\n): DeploymentPlanBlock {\n const denomMap = input.denomMap ?? EMPTY_DENOM_MAP;\n const { summary } = input.plan;\n\n const manifestLine =\n `${summary.format ?? 'single'}, services=${summary.serviceCount}, ` +\n `ports=${summary.portCount}, env=${summary.envCount}`;\n\n const hasDomain =\n typeof input.customDomain === 'string' && input.customDomain.length > 0;\n\n // Create-lease fee — always present in PlanFees.\n const createFee = input.plan.fees.createLease;\n const createHuman = humanizeFeeAmount(createFee, denomMap);\n const createFeeLine = formatFeeLine(createHuman, createFee.gas);\n\n const lines: string[] = [\n 'DeploymentPlan',\n ` Image: ${input.image}`,\n ` Size: ${input.size}`,\n ` Manifest: ${manifestLine}`,\n ` meta_hash: ${input.metaHash}`,\n ];\n\n if (hasDomain) {\n const target =\n typeof input.customDomainService === 'string' &&\n input.customDomainService.length > 0\n ? `-> service ${input.customDomainService}`\n : '-> single-service lease';\n lines.push(` Custom domain: ${input.customDomain} ${target}`);\n }\n\n lines.push(\n ` SKU price: ${formatSkuPrice(input.plan, denomMap)}`,\n );\n\n if (hasDomain) {\n // Two-tx layout: labeled lines + Total fee. Honors approach-3\n // `notEstimated` sentinel for set-domain pre-broadcast estimation\n // fallback (no representative lease).\n const setDomain = input.plan.fees.setDomain;\n let setDomainLine: string;\n // Capture the typed `FeeEstimate` reference (when the set-domain\n // fee is a real estimate, not the sentinel) so the total-line\n // BigInt sum can operate on `coins` directly via `sumFees`. The\n // prior code parsed humanized strings to float64 — see\n // `sumFees`'s docstring for the precision-loss rationale.\n let setDomainReal: FeeEstimate | null = null;\n if (setDomain === undefined) {\n setDomainLine =\n '(not estimated — agent skipped pre-broadcast simulation, policy violation)';\n } else if ('notEstimated' in setDomain) {\n setDomainLine = `(not estimated — ${setDomain.reason})`;\n } else {\n setDomainReal = setDomain;\n setDomainLine = formatFeeLine(\n humanizeFeeAmount(setDomain, denomMap),\n setDomain.gas,\n );\n }\n\n lines.push(` Tx fee (create-lease): ${createFeeLine}`);\n lines.push(` Tx fee (set-domain): ${setDomainLine}`);\n\n // Total only when both fees are real numbers. Sentinel set-domain\n // fees fall through to the placeholder.\n const totalLine =\n setDomainReal !== null\n ? sumFees(createFee, setDomainReal, denomMap)\n : '(partial — see fee lines above)';\n lines.push(` Total fee: ${totalLine}`);\n } else {\n lines.push(` Tx fee: ${createFeeLine}`);\n }\n\n lines.push(\n ` Wallet: ${formatWallet(input.plan, denomMap)}`,\n );\n lines.push(\n ` Credits: ${formatCredits(input.plan, denomMap)}`,\n );\n\n return { text: lines.join('\\n') };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-intent-recap.d.ts","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"mappings":";;;;;AAuCA;;;;;;;;;AAoBA;;;;;;;;;;;;;;;;;;;UApBiB,sBAAA;;EAEf,IAAA,EAAM,UAAA;;EAEN,WAAA;AAAA;AAAA,iBAgBc,iBAAA,CAAkB,KAAA,EAAO,sBAAA"}
1
+ {"version":3,"file":"render-intent-recap.d.ts","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"mappings":";;;;;AAuCA;;;;;;;;AAIa;AAgBb;;;;AAA+D;;;;;;;;;;;;;;;UApB9C,sBAAA;;EAEf,IAAA,EAAM,UAAU;;EAEhB,WAAA;AAAA;AAAA,iBAgBc,iBAAA,CAAkB,KAA6B,EAAtB,sBAAsB"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-intent-recap.js","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"sourcesContent":["import type {\n DeploySpec,\n ServiceDef,\n SingleServiceSpec,\n StackSpec,\n} from '../types.js';\nimport { isStackSpec, normalizeServices } from './spec-normalize.js';\n\n/**\n * Render the structural portion of the intent-recap block shown to the user\n * before any chain round-trips in the deploy-app orchestrator.\n *\n * The 4 deterministic items the recap covers:\n *\n * 1. Deployment surface (service count + per-service `name — image`)\n * 2. Connectivity (per-port ingress posture)\n * 3. Redacted sensitive-key inventory (env / label keys only; never values)\n * 4. Custom-domain + dual-tx clarifier + mainnet warning (when applicable)\n *\n * The 2 LLM-judgment items (\"what you provided vs auto-detected\", \"heads-up:\n * obvious gaps\") stay in prose — the orchestrator appends them between the\n * deterministic block and the `AskUserQuestion` prompt.\n *\n * **Sensitive-value posture:** env values and label values are NEVER\n * surfaced; only keys appear. Mirrors `summarizeSpec`'s contract. FQDNs are\n * not secrets so `customDomain` is surfaced verbatim.\n *\n * **Port-shape handling:** the CJS supports two runtime shapes for ports:\n * - Legacy single-service: `port: number` → renders one ingress=true entry.\n * - Services-map: `ports: Record<portKey, { ingress?: boolean }>` → one\n * entry per port-key with the declared ingress flag (default false).\n *\n * The frozen TS contract narrows `ServiceDef.ports` to `number[]` for the\n * common case; this port also handles the historical Record shape at runtime\n * (matching `summarizeSpec`'s defensive widening) so callers passing\n * unknown-typed input from JSON.parse don't silently drop ports.\n */\n\n/** Render output is a multi-paragraph plain-text block, ready to print verbatim. */\nexport interface RenderIntentRecapInput {\n /** The structured deploy spec (frozen `DeploySpec` shape). */\n spec: DeploySpec;\n /** Active chain — drives the mainnet permanence warning. */\n activeChain: 'testnet' | 'mainnet';\n}\n\ninterface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** Image string. Falls back to `(unknown image)` when missing. */\n image: string;\n /** Per-port ingress posture, in declaration order. */\n ports: { port: string; ingress: boolean }[];\n /** Sorted env keys (values redacted). */\n envKeys: string[];\n /** Sorted label keys (values redacted). */\n labelKeys: string[];\n}\n\nexport function renderIntentRecap(input: RenderIntentRecapInput): string {\n if (input.activeChain !== 'testnet' && input.activeChain !== 'mainnet') {\n throw new TypeError(\n `renderIntentRecap: activeChain must be \"testnet\" or \"mainnet\"; got \"${String(input.activeChain)}\"`,\n );\n }\n\n const services = projectServices(input.spec);\n\n const blocks: string[] = [\n renderServiceList(services, input.activeChain),\n renderConnectivity(services),\n renderRedactedInventory(services),\n ];\n const domainBlock = renderCustomDomain(input.spec, input.activeChain);\n if (domainBlock !== null) {\n blocks.push(domainBlock);\n }\n\n return blocks.join('\\n\\n');\n}\n\nfunction projectServices(spec: DeploySpec): NormalizedService[] {\n return normalizeServices(spec).map(({ name, raw }): NormalizedService => {\n const rawRecord = raw as unknown as Record<string, unknown>;\n const image =\n typeof rawRecord.image === 'string' && rawRecord.image.length > 0\n ? rawRecord.image\n : '(unknown image)';\n const ports =\n name === null\n ? extractPortsLegacy((raw as SingleServiceSpec).port)\n : extractPorts((raw as ServiceDef).ports);\n return {\n name,\n image,\n ports,\n envKeys: extractKeys(rawRecord.env),\n labelKeys: extractKeys(rawRecord.labels),\n };\n });\n}\n\n/**\n * Services-map shape: `{ \"80\": { ingress?: boolean }, \"9090\": { ... } }`.\n * Ingress flag may be absent — default `false` matches Fred's cluster-private\n * default. Also handles the typed `number[]` shape (frozen `ServiceDef.ports`)\n * by treating each entry as ingress=false (services-map default).\n */\nfunction extractPorts(ports: unknown): { port: string; ingress: boolean }[] {\n if (Array.isArray(ports)) {\n return ports\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: false }));\n }\n if (ports !== null && typeof ports === 'object') {\n return Object.entries(ports as Record<string, unknown>).map(\n ([port, cfg]) => ({\n port,\n ingress: !!(\n cfg !== null &&\n typeof cfg === 'object' &&\n (cfg as { ingress?: unknown }).ingress\n ),\n }),\n );\n }\n return [];\n}\n\n/**\n * Legacy single-service shape: bare `port: number`. Fred treats this as\n * ingress=true by default — that's the whole point of the simplified\n * shape.\n *\n * Also handles the `number[]` form (the frozen-contract array form):\n * returns one `{ port, ingress: true }` entry per array element, each\n * with `ingress: true` matching the single-service convention. Returns\n * `[]` for any other value (undefined, non-number scalar, non-array\n * object).\n *\n * M2 fix: prior JSDoc incorrectly stated \"Returns `[]` for any other\n * value (including `number[]`...)\" — empirically wrong per the\n * `Array.isArray(port)` branch below.\n */\nfunction extractPortsLegacy(\n port: number | number[] | undefined,\n): { port: string; ingress: boolean }[] {\n if (typeof port === 'number') {\n return [{ port: String(port), ingress: true }];\n }\n if (Array.isArray(port)) {\n return port\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: true }));\n }\n return [];\n}\n\nfunction extractKeys(obj: unknown): string[] {\n if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return [];\n return Object.keys(obj as Record<string, unknown>).sort();\n}\n\nfunction renderServiceList(\n services: NormalizedService[],\n activeChain: 'testnet' | 'mainnet',\n): string {\n const count = services.length;\n const noun = count === 1 ? 'service' : 'services';\n const lines = [`Deploying ${count} ${noun} on ${activeChain}:`];\n for (const svc of services) {\n const prefix = svc.name === null ? '' : `${svc.name} — `;\n lines.push(` - ${prefix}${svc.image}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderConnectivity(services: NormalizedService[]): string {\n const lines = ['Connectivity:'];\n let total = 0;\n for (const svc of services) {\n if (svc.ports.length === 0) continue;\n for (const p of svc.ports) {\n total += 1;\n const prefix =\n svc.name === null ? `port ${p.port}` : `${svc.name} port ${p.port}`;\n const reach = p.ingress\n ? \"publicly reachable via the provider's HTTPS subdomain\"\n : 'internal only (cluster-private)';\n lines.push(` - ${prefix}: ${reach}`);\n }\n }\n if (total === 0) {\n lines.push(\n ' (no ports declared — the deployment will not expose any network surface)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderRedactedInventory(services: NormalizedService[]): string {\n // Always render the section header even if everything is empty — the user\n // should know we'd have shown values if there were any. This is also\n // documentation of the redaction discipline.\n const lines = [\n 'Sensitive values are redacted in this recap (keys only, never values):',\n ];\n let anything = false;\n for (const svc of services) {\n const prefix = svc.name === null ? 'this service' : svc.name;\n const parts: string[] = [];\n if (svc.envKeys.length > 0) {\n anything = true;\n parts.push(`env keys [${svc.envKeys.join(', ')}]`);\n }\n if (svc.labelKeys.length > 0) {\n anything = true;\n parts.push(`label keys [${svc.labelKeys.join(', ')}]`);\n }\n if (parts.length === 0) {\n lines.push(` - ${prefix}: no env or labels supplied`);\n } else {\n lines.push(` - ${prefix}: ${parts.join('; ')}`);\n }\n }\n if (!anything) {\n lines.push(\n ' - (no env or labels supplied across any service — nothing to redact)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderCustomDomain(\n spec: DeploySpec,\n activeChain: 'testnet' | 'mainnet',\n): string | null {\n const customDomain = (spec as { customDomain?: unknown }).customDomain;\n if (typeof customDomain !== 'string' || customDomain.length === 0) {\n return null;\n }\n // `serviceName` is only legal on StackSpec; for SingleServiceSpec the\n // single service implicitly receives the domain.\n const serviceName = isStackSpec(spec)\n ? (spec as StackSpec).serviceName\n : undefined;\n const target =\n typeof serviceName === 'string' && serviceName.length > 0\n ? `service ${serviceName}`\n : 'single-service lease';\n const lines = [`Custom domain: ${customDomain} → ${target}`];\n lines.push('');\n lines.push(\n 'Note: when a custom domain is set, deploy_app broadcasts TWO billing\\n' +\n 'transactions atomically: create-lease AND set-item-custom-domain. The\\n' +\n 'single permission prompt that fires later covers BOTH; this textual\\n' +\n 'recap is your per-tx review.',\n );\n if (activeChain === 'mainnet') {\n lines.push('');\n lines.push(\n `Mainnet warning: this transaction permanently associates ${customDomain}\\n` +\n 'with this lease on-chain until you --clear it via\\n' +\n '/manifest-agent:manage-domain or close the lease. FQDN squatting is\\n' +\n 'irreversible.',\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-intent-recap.js","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"sourcesContent":["import type {\n DeploySpec,\n ServiceDef,\n SingleServiceSpec,\n StackSpec,\n} from '../types.js';\nimport { isStackSpec, normalizeServices } from './spec-normalize.js';\n\n/**\n * Render the structural portion of the intent-recap block shown to the user\n * before any chain round-trips in the deploy-app orchestrator.\n *\n * The 4 deterministic items the recap covers:\n *\n * 1. Deployment surface (service count + per-service `name — image`)\n * 2. Connectivity (per-port ingress posture)\n * 3. Redacted sensitive-key inventory (env / label keys only; never values)\n * 4. Custom-domain + dual-tx clarifier + mainnet warning (when applicable)\n *\n * The 2 LLM-judgment items (\"what you provided vs auto-detected\", \"heads-up:\n * obvious gaps\") stay in prose — the orchestrator appends them between the\n * deterministic block and the `AskUserQuestion` prompt.\n *\n * **Sensitive-value posture:** env values and label values are NEVER\n * surfaced; only keys appear. Mirrors `summarizeSpec`'s contract. FQDNs are\n * not secrets so `customDomain` is surfaced verbatim.\n *\n * **Port-shape handling:** the CJS supports two runtime shapes for ports:\n * - Legacy single-service: `port: number` → renders one ingress=true entry.\n * - Services-map: `ports: Record<portKey, { ingress?: boolean }>` → one\n * entry per port-key with the declared ingress flag (default false).\n *\n * The frozen TS contract narrows `ServiceDef.ports` to `number[]` for the\n * common case; this port also handles the historical Record shape at runtime\n * (matching `summarizeSpec`'s defensive widening) so callers passing\n * unknown-typed input from JSON.parse don't silently drop ports.\n */\n\n/** Render output is a multi-paragraph plain-text block, ready to print verbatim. */\nexport interface RenderIntentRecapInput {\n /** The structured deploy spec (frozen `DeploySpec` shape). */\n spec: DeploySpec;\n /** Active chain — drives the mainnet permanence warning. */\n activeChain: 'testnet' | 'mainnet';\n}\n\ninterface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** Image string. Falls back to `(unknown image)` when missing. */\n image: string;\n /** Per-port ingress posture, in declaration order. */\n ports: { port: string; ingress: boolean }[];\n /** Sorted env keys (values redacted). */\n envKeys: string[];\n /** Sorted label keys (values redacted). */\n labelKeys: string[];\n}\n\nexport function renderIntentRecap(input: RenderIntentRecapInput): string {\n if (input.activeChain !== 'testnet' && input.activeChain !== 'mainnet') {\n throw new TypeError(\n `renderIntentRecap: activeChain must be \"testnet\" or \"mainnet\"; got \"${String(input.activeChain)}\"`,\n );\n }\n\n const services = projectServices(input.spec);\n\n const blocks: string[] = [\n renderServiceList(services, input.activeChain),\n renderConnectivity(services),\n renderRedactedInventory(services),\n ];\n const domainBlock = renderCustomDomain(input.spec, input.activeChain);\n if (domainBlock !== null) {\n blocks.push(domainBlock);\n }\n\n return blocks.join('\\n\\n');\n}\n\nfunction projectServices(spec: DeploySpec): NormalizedService[] {\n return normalizeServices(spec).map(({ name, raw }): NormalizedService => {\n const rawRecord = raw as unknown as Record<string, unknown>;\n const image =\n typeof rawRecord.image === 'string' && rawRecord.image.length > 0\n ? rawRecord.image\n : '(unknown image)';\n const ports =\n name === null\n ? extractPortsLegacy((raw as SingleServiceSpec).port)\n : extractPorts((raw as ServiceDef).ports);\n return {\n name,\n image,\n ports,\n envKeys: extractKeys(rawRecord.env),\n labelKeys: extractKeys(rawRecord.labels),\n };\n });\n}\n\n/**\n * Services-map shape: `{ \"80\": { ingress?: boolean }, \"9090\": { ... } }`.\n * Ingress flag may be absent — default `false` matches Fred's cluster-private\n * default. Also handles the typed `number[]` shape (frozen `ServiceDef.ports`)\n * by treating each entry as ingress=false (services-map default).\n */\nfunction extractPorts(ports: unknown): { port: string; ingress: boolean }[] {\n if (Array.isArray(ports)) {\n return ports\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: false }));\n }\n if (ports !== null && typeof ports === 'object') {\n return Object.entries(ports as Record<string, unknown>).map(\n ([port, cfg]) => ({\n port,\n ingress: !!(\n cfg !== null &&\n typeof cfg === 'object' &&\n (cfg as { ingress?: unknown }).ingress\n ),\n }),\n );\n }\n return [];\n}\n\n/**\n * Legacy single-service shape: bare `port: number`. Fred treats this as\n * ingress=true by default — that's the whole point of the simplified\n * shape.\n *\n * Also handles the `number[]` form (the frozen-contract array form):\n * returns one `{ port, ingress: true }` entry per array element, each\n * with `ingress: true` matching the single-service convention. Returns\n * `[]` for any other value (undefined, non-number scalar, non-array\n * object).\n *\n * M2 fix: prior JSDoc incorrectly stated \"Returns `[]` for any other\n * value (including `number[]`...)\" — empirically wrong per the\n * `Array.isArray(port)` branch below.\n */\nfunction extractPortsLegacy(\n port: number | number[] | undefined,\n): { port: string; ingress: boolean }[] {\n if (typeof port === 'number') {\n return [{ port: String(port), ingress: true }];\n }\n if (Array.isArray(port)) {\n return port\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: true }));\n }\n return [];\n}\n\nfunction extractKeys(obj: unknown): string[] {\n if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return [];\n return Object.keys(obj as Record<string, unknown>).sort();\n}\n\nfunction renderServiceList(\n services: NormalizedService[],\n activeChain: 'testnet' | 'mainnet',\n): string {\n const count = services.length;\n const noun = count === 1 ? 'service' : 'services';\n const lines = [`Deploying ${count} ${noun} on ${activeChain}:`];\n for (const svc of services) {\n const prefix = svc.name === null ? '' : `${svc.name} — `;\n lines.push(` - ${prefix}${svc.image}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderConnectivity(services: NormalizedService[]): string {\n const lines = ['Connectivity:'];\n let total = 0;\n for (const svc of services) {\n if (svc.ports.length === 0) continue;\n for (const p of svc.ports) {\n total += 1;\n const prefix =\n svc.name === null ? `port ${p.port}` : `${svc.name} port ${p.port}`;\n const reach = p.ingress\n ? \"publicly reachable via the provider's HTTPS subdomain\"\n : 'internal only (cluster-private)';\n lines.push(` - ${prefix}: ${reach}`);\n }\n }\n if (total === 0) {\n lines.push(\n ' (no ports declared — the deployment will not expose any network surface)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderRedactedInventory(services: NormalizedService[]): string {\n // Always render the section header even if everything is empty — the user\n // should know we'd have shown values if there were any. This is also\n // documentation of the redaction discipline.\n const lines = [\n 'Sensitive values are redacted in this recap (keys only, never values):',\n ];\n let anything = false;\n for (const svc of services) {\n const prefix = svc.name === null ? 'this service' : svc.name;\n const parts: string[] = [];\n if (svc.envKeys.length > 0) {\n anything = true;\n parts.push(`env keys [${svc.envKeys.join(', ')}]`);\n }\n if (svc.labelKeys.length > 0) {\n anything = true;\n parts.push(`label keys [${svc.labelKeys.join(', ')}]`);\n }\n if (parts.length === 0) {\n lines.push(` - ${prefix}: no env or labels supplied`);\n } else {\n lines.push(` - ${prefix}: ${parts.join('; ')}`);\n }\n }\n if (!anything) {\n lines.push(\n ' - (no env or labels supplied across any service — nothing to redact)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderCustomDomain(\n spec: DeploySpec,\n activeChain: 'testnet' | 'mainnet',\n): string | null {\n const customDomain = (spec as { customDomain?: unknown }).customDomain;\n if (typeof customDomain !== 'string' || customDomain.length === 0) {\n return null;\n }\n // `serviceName` is only legal on StackSpec; for SingleServiceSpec the\n // single service implicitly receives the domain.\n const serviceName = isStackSpec(spec)\n ? (spec as StackSpec).serviceName\n : undefined;\n const target =\n typeof serviceName === 'string' && serviceName.length > 0\n ? `service ${serviceName}`\n : 'single-service lease';\n const lines = [`Custom domain: ${customDomain} → ${target}`];\n lines.push('');\n lines.push(\n 'Note: when a custom domain is set, deploy_app broadcasts TWO billing\\n' +\n 'transactions atomically: create-lease AND set-item-custom-domain. The\\n' +\n 'single permission prompt that fires later covers BOTH; this textual\\n' +\n 'recap is your per-tx review.',\n );\n if (activeChain === 'mainnet') {\n lines.push('');\n lines.push(\n `Mainnet warning: this transaction permanently associates ${customDomain}\\n` +\n 'with this lease on-chain until you --clear it via\\n' +\n '/manifest-agent:manage-domain or close the lease. FQDN squatting is\\n' +\n 'irreversible.',\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-partial-success-prompt.d.ts","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"mappings":";;;UAmCiB,+BAAA;;EAEf,SAAA;EAF8C;EAI9C,YAAA;EAJ8C;EAM9C,MAAA;EAFA;EAIA,qBAAA;AAAA;AAAA,UAGe,oBAAA;EAHM;EAKrB,MAAA;EAFmC;;;;EAOnC,OAAA,EAAS,gBAAA;AAAA;AAAA,iBAGK,0BAAA,CACd,KAAA,EAAO,+BAAA,GACN,oBAAA"}
1
+ {"version":3,"file":"render-partial-success-prompt.d.ts","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"mappings":";;;UAmCiB,+BAAA;;EAEf,SAAA;EAF8C;EAI9C,YAAA;EAJ8C;EAM9C,MAAA;EAFA;EAIA,qBAAA;AAAA;AAAA,UAGe,oBAAA;EAHM;EAKrB,MAAA;EAFmC;;;;EAOnC,OAAA,EAAS,gBAAgB;AAAA;AAAA,iBAGX,0BAAA,CACd,KAAA,EAAO,+BAAA,GACN,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"render-partial-success-prompt.js","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"sourcesContent":["import type { RecoveryOptionId } from '../types.js';\n\n/**\n * Render the prompt body + recovery-option set for `deployApp`'s\n * partial-success recovery branch. Uses a fixed structural template —\n * wording stays consistent across runs regardless of LLM paraphrase\n * tendencies.\n *\n * **Conditional inserts:** wording differs by whether a `customDomain`\n * was requested. When absent, the failure description shifts to a\n * generic \"manifest upload or readiness poll failed\" framing and the\n * `retry_set_domain` recovery option is omitted (it's meaningless\n * without a domain request to retry).\n *\n * The CJS emits a single-line JSON object on stdout\n * (`{ prompt, options: string[] }`). The TS port returns a structured\n * `PartialSuccessPrompt` with typed `RecoveryOptionId`s, so the\n * `deployApp` orchestrator can route directly into the inline-closure\n * recovery dispatch (per gate-2 verdict) without intermediate\n * stringification.\n *\n * **Note on `decodedState`:** the caller is expected to have already\n * decoded the chain integer / `LEASE_STATE_*` string via\n * `lease-state.ts:decode` and pass the canonical name (or a\n * `UNKNOWN(<raw>)` sentinel for unrecognized values). This module does\n * not re-decode — the canonical name is shown verbatim in the lease\n * status line so an explicit `LEASE_STATE_` prefix is preserved (the\n * partial-success path surfaces the raw lease state for diagnostic\n * fidelity, unlike `formatSuccess` which strips the prefix for display).\n */\n\n/** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nexport interface RenderPartialSuccessPromptInput {\n /** The lease UUID created on-chain (validated RFC 4122). */\n leaseUuid: string;\n /** Canonical lease state name (e.g. `LEASE_STATE_PENDING`) — pre-decoded. */\n decodedState: string;\n /** Failure reason from the MCP error envelope or `classifyDeployError`. */\n reason: string;\n /** Optional: FQDN the user requested. Presence drives wording + retry option. */\n requestedCustomDomain?: string;\n}\n\nexport interface PartialSuccessPrompt {\n /** Multi-line prompt body to pass to AskUserQuestion. */\n prompt: string;\n /**\n * Recovery options offered to the user. `retry_set_domain` is omitted\n * when no domain was requested. Order matches the CJS's option list.\n */\n options: RecoveryOptionId[];\n}\n\nexport function renderPartialSuccessPrompt(\n input: RenderPartialSuccessPromptInput,\n): PartialSuccessPrompt {\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new TypeError(\n `renderPartialSuccessPrompt: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (\n typeof input.decodedState !== 'string' ||\n input.decodedState.length === 0\n ) {\n throw new TypeError(\n 'renderPartialSuccessPrompt: decodedState must be a non-empty string',\n );\n }\n if (typeof input.reason !== 'string' || input.reason.length === 0) {\n throw new TypeError(\n 'renderPartialSuccessPrompt: reason must be a non-empty string',\n );\n }\n\n const hasDomain =\n typeof input.requestedCustomDomain === 'string' &&\n input.requestedCustomDomain.length > 0;\n\n const lines: string[] = [\n 'Deploy partially succeeded:',\n ` - Lease ${input.leaseUuid} was created on-chain (state: ${input.decodedState}).`,\n ];\n if (hasDomain) {\n lines.push(\n ` - The set-domain step for ${input.requestedCustomDomain} did NOT complete: ${input.reason}.`,\n ' The manifest was therefore NEVER uploaded to the provider — no app is running on this lease.',\n );\n } else {\n lines.push(\n ` - The manifest upload or readiness poll failed: ${input.reason}.`,\n ' The provider may or may not have started the app.',\n );\n }\n lines.push('', 'What do you want to do?');\n\n const options: RecoveryOptionId[] = [];\n if (hasDomain) {\n options.push('retry_set_domain');\n }\n options.push('salvage_without_domain');\n // CJS emits \"Cancel or close the lease\" as a single user-facing option\n // (3 total when hasDomain, 2 when not). The typed `RecoveryOptionId`\n // vocabulary splits this into two discrete IDs (`cancel_lease`,\n // `close_lease`) for the orchestrator's typed dispatch — `cancel_lease`\n // applies pre-active (abort without on-chain close); `close_lease`\n // applies post-active (on-chain close-lease tx). To preserve the CJS's\n // observable user-option count, we surface the more-general\n // `close_lease` here as the unified terminal choice; the orchestrator's\n // inline-closure dispatch (see `deploy-app.ts`, gate-2 verdict) inspects\n // the lease state at recovery time and routes to the precise terminal\n // tx. `cancel_lease` remains reachable from verify-recover-driven\n // non-user-prompted paths (e.g. terminal lease detected before user\n // input is solicited).\n options.push('close_lease');\n\n return { prompt: lines.join('\\n'), options };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,UACJ;AAuBF,SAAgB,2BACd,OACsB;AACtB,KAAI,CAAC,QAAQ,KAAK,MAAM,UAAU,CAChC,OAAM,IAAI,UACR,8DAA8D,MAAM,UAAU,GAC/E;AAEH,KACE,OAAO,MAAM,iBAAiB,YAC9B,MAAM,aAAa,WAAW,EAE9B,OAAM,IAAI,UACR,sEACD;AAEH,KAAI,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,WAAW,EAC9D,OAAM,IAAI,UACR,gEACD;CAGH,MAAM,YACJ,OAAO,MAAM,0BAA0B,YACvC,MAAM,sBAAsB,SAAS;CAEvC,MAAM,QAAkB,CACtB,+BACA,aAAa,MAAM,UAAU,gCAAgC,MAAM,aAAa,IACjF;AACD,KAAI,UACF,OAAM,KACJ,+BAA+B,MAAM,sBAAsB,qBAAqB,MAAM,OAAO,IAC7F,mGACD;KAED,OAAM,KACJ,qDAAqD,MAAM,OAAO,IAClE,wDACD;AAEH,OAAM,KAAK,IAAI,0BAA0B;CAEzC,MAAM,UAA8B,EAAE;AACtC,KAAI,UACF,SAAQ,KAAK,mBAAmB;AAElC,SAAQ,KAAK,yBAAyB;AActC,SAAQ,KAAK,cAAc;AAE3B,QAAO;EAAE,QAAQ,MAAM,KAAK,KAAK;EAAE;EAAS"}
1
+ {"version":3,"file":"render-partial-success-prompt.js","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"sourcesContent":["import type { RecoveryOptionId } from '../types.js';\n\n/**\n * Render the prompt body + recovery-option set for `deployApp`'s\n * partial-success recovery branch. Uses a fixed structural template —\n * wording stays consistent across runs regardless of LLM paraphrase\n * tendencies.\n *\n * **Conditional inserts:** wording differs by whether a `customDomain`\n * was requested. When absent, the failure description shifts to a\n * generic \"manifest upload or readiness poll failed\" framing and the\n * `retry_set_domain` recovery option is omitted (it's meaningless\n * without a domain request to retry).\n *\n * The CJS emits a single-line JSON object on stdout\n * (`{ prompt, options: string[] }`). The TS port returns a structured\n * `PartialSuccessPrompt` with typed `RecoveryOptionId`s, so the\n * `deployApp` orchestrator can route directly into the inline-closure\n * recovery dispatch (per gate-2 verdict) without intermediate\n * stringification.\n *\n * **Note on `decodedState`:** the caller is expected to have already\n * decoded the chain integer / `LEASE_STATE_*` string via\n * `lease-state.ts:decode` and pass the canonical name (or a\n * `UNKNOWN(<raw>)` sentinel for unrecognized values). This module does\n * not re-decode — the canonical name is shown verbatim in the lease\n * status line so an explicit `LEASE_STATE_` prefix is preserved (the\n * partial-success path surfaces the raw lease state for diagnostic\n * fidelity, unlike `formatSuccess` which strips the prefix for display).\n */\n\n/** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nexport interface RenderPartialSuccessPromptInput {\n /** The lease UUID created on-chain (validated RFC 4122). */\n leaseUuid: string;\n /** Canonical lease state name (e.g. `LEASE_STATE_PENDING`) — pre-decoded. */\n decodedState: string;\n /** Failure reason from the MCP error envelope or `classifyDeployError`. */\n reason: string;\n /** Optional: FQDN the user requested. Presence drives wording + retry option. */\n requestedCustomDomain?: string;\n}\n\nexport interface PartialSuccessPrompt {\n /** Multi-line prompt body to pass to AskUserQuestion. */\n prompt: string;\n /**\n * Recovery options offered to the user. `retry_set_domain` is omitted\n * when no domain was requested. Order matches the CJS's option list.\n */\n options: RecoveryOptionId[];\n}\n\nexport function renderPartialSuccessPrompt(\n input: RenderPartialSuccessPromptInput,\n): PartialSuccessPrompt {\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new TypeError(\n `renderPartialSuccessPrompt: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (\n typeof input.decodedState !== 'string' ||\n input.decodedState.length === 0\n ) {\n throw new TypeError(\n 'renderPartialSuccessPrompt: decodedState must be a non-empty string',\n );\n }\n if (typeof input.reason !== 'string' || input.reason.length === 0) {\n throw new TypeError(\n 'renderPartialSuccessPrompt: reason must be a non-empty string',\n );\n }\n\n const hasDomain =\n typeof input.requestedCustomDomain === 'string' &&\n input.requestedCustomDomain.length > 0;\n\n const lines: string[] = [\n 'Deploy partially succeeded:',\n ` - Lease ${input.leaseUuid} was created on-chain (state: ${input.decodedState}).`,\n ];\n if (hasDomain) {\n lines.push(\n ` - The set-domain step for ${input.requestedCustomDomain} did NOT complete: ${input.reason}.`,\n ' The manifest was therefore NEVER uploaded to the provider — no app is running on this lease.',\n );\n } else {\n lines.push(\n ` - The manifest upload or readiness poll failed: ${input.reason}.`,\n ' The provider may or may not have started the app.',\n );\n }\n lines.push('', 'What do you want to do?');\n\n const options: RecoveryOptionId[] = [];\n if (hasDomain) {\n options.push('retry_set_domain');\n }\n options.push('salvage_without_domain');\n // CJS emits \"Cancel or close the lease\" as a single user-facing option\n // (3 total when hasDomain, 2 when not). The typed `RecoveryOptionId`\n // vocabulary splits this into two discrete IDs (`cancel_lease`,\n // `close_lease`) for the orchestrator's typed dispatch — `cancel_lease`\n // applies pre-active (abort without on-chain close); `close_lease`\n // applies post-active (on-chain close-lease tx). To preserve the CJS's\n // observable user-option count, we surface the more-general\n // `close_lease` here as the unified terminal choice; the orchestrator's\n // inline-closure dispatch (see `deploy-app.ts`, gate-2 verdict) inspects\n // the lease state at recovery time and routes to the precise terminal\n // tx. `cancel_lease` remains reachable from verify-recover-driven\n // non-user-prompted paths (e.g. terminal lease detected before user\n // input is solicited).\n options.push('close_lease');\n\n return { prompt: lines.join('\\n'), options };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,UACJ;AAuBF,SAAgB,2BACd,OACsB;CACtB,IAAI,CAAC,QAAQ,KAAK,MAAM,SAAS,GAC/B,MAAM,IAAI,UACR,8DAA8D,MAAM,UAAU,EAChF;CAEF,IACE,OAAO,MAAM,iBAAiB,YAC9B,MAAM,aAAa,WAAW,GAE9B,MAAM,IAAI,UACR,qEACF;CAEF,IAAI,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,WAAW,GAC9D,MAAM,IAAI,UACR,+DACF;CAGF,MAAM,YACJ,OAAO,MAAM,0BAA0B,YACvC,MAAM,sBAAsB,SAAS;CAEvC,MAAM,QAAkB,CACtB,+BACA,aAAa,MAAM,UAAU,gCAAgC,MAAM,aAAa,GAClF;CACA,IAAI,WACF,MAAM,KACJ,+BAA+B,MAAM,sBAAsB,qBAAqB,MAAM,OAAO,IAC7F,kGACF;MAEA,MAAM,KACJ,qDAAqD,MAAM,OAAO,IAClE,uDACF;CAEF,MAAM,KAAK,IAAI,yBAAyB;CAExC,MAAM,UAA8B,CAAC;CACrC,IAAI,WACF,QAAQ,KAAK,kBAAkB;CAEjC,QAAQ,KAAK,wBAAwB;CAcrC,QAAQ,KAAK,aAAa;CAE1B,OAAO;EAAE,QAAQ,MAAM,KAAK,IAAI;EAAG;CAAQ;AAC7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"save-manifest.d.ts","names":[],"sources":["../../src/internals/save-manifest.ts"],"mappings":";;AAmDA;;;;;;;;;;;;;;;AAyCA;;;;;AAMA;;;;;;;;;;;;AA2BA;;;;;;;;;;UA1EiB,iBAAA;EA4EN;EA1ET,SAAA;EA0E2B;EAxE3B,KAAA;;EAEA,IAAA;;EAEA,QAAA;;EAEA,OAAA;;;;;;EAMA,YAAA;;;;;;;;;;;;;;;EAeA,OAAA;;EAEA,YAAA;;;;;EAKA,uBAAA;AAAA;AAAA,UAGe,kBAAA;;EAEf,YAAA;AAAA;;cAIW,iBAAA,SAA0B,KAAA;EAAA,SAC5B,IAAA;cAUG,IAAA,EAAM,iBAAA,UAA2B,OAAA;AAAA;;;;;;;;;iBAgBzB,YAAA,CACpB,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,kBAAA"}
1
+ {"version":3,"file":"save-manifest.d.ts","names":[],"sources":["../../src/internals/save-manifest.ts"],"mappings":";;AAmDA;;;;;;;;;;;;;;AAsCyB;AAGzB;;;;AAEc;AAId;;;;;;;;;;;AAW8D;AAgB9D;;;;;;;;;;UA1EiB,iBAAA;EA4EN;EA1ET,SAAA;EA0E2B;EAxE3B,KAAA;;EAEA,IAAA;;EAEA,QAAA;;EAEA,OAAA;;;;;;EAMA,YAAA;;;;;;;;;;;;;;;EAeA,OAAA;;EAEA,YAAA;;;;;EAKA,uBAAA;AAAA;AAAA,UAGe,kBAAA;;EAEf,YAAY;AAAA;;cAID,iBAAA,SAA0B,KAAK;EAAA,SACjC,IAAA;cAUG,IAAA,EAAM,iBAAA,UAA2B,OAAA;AAAA;;;;;;;;;iBAgBzB,YAAA,CACpB,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,kBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"save-manifest.js","names":[],"sources":["../../src/internals/save-manifest.ts"],"sourcesContent":["/**\n * Persist a deployed manifest as a schema-version-3 wrapper to disk.\n *\n * Target directory is a **function argument** (`dataDir: string`) rather\n * than an env-var read: per gate-1 verdict, agent-core's `platform: 'neutral'`\n * build target forbids env-var reads at this layer; callers are responsible\n * for resolving the manifests dir from their own env context and passing it in.\n *\n * **Wrapper shape (schema_version 3):**\n *\n * ```\n * {\n * schema_version: 3,\n * lease_uuid, deployed_at_iso, deployed_at_unix,\n * chain_id, image, size, meta_hash_hex,\n * format, // \"single\" or \"stack\"\n * manifest_json, // string — canonical Fred-rendered JSON\n * custom_domain?, // (v3) FQDN attached to the lease item\n * custom_domain_service_name? // (v3) stack-lease service holder\n * }\n * ```\n *\n * Schema-version compat: v2 wrappers remain readable by all downstream\n * helpers; missing v3 fields are tolerated as undefined.\n *\n * **Audit guarantee:** SHA-256 of the bytes about to be persisted (after\n * normalizing the heredoc-/Write-added trailing newline) MUST equal\n * `metaHash`. Catches paste errors, accidental spec-vs-manifest_json\n * swaps, and transit corruption. Mismatch throws a typed\n * `SaveManifestError` (`code: 'sha256_mismatch'`).\n *\n * **Filesystem layout:** `<dataDir>/manifests/<lease_uuid>.json` with\n * mode 0600; parent `<dataDir>` and `<dataDir>/manifests` ensured at\n * mode 0700 (chmod-tightens an existing parent that was previously\n * looser).\n *\n * **Dynamic node-import discipline** (mirrors `guarded-fetch.ts`): the\n * `node:fs` / `node:path` / `node:crypto` imports are deferred to call\n * time so module load doesn't violate the `platform: 'neutral'` build\n * target. A `typeof process` check throws a clear \"Node-only API\" error\n * if invoked outside a Node-like runtime.\n */\n\n/** SHA-256 hex digest — 64 lowercase hex chars. */\nconst META_HASH_RE = /^[0-9a-f]{64}$/i;\n\n/** RFC 4122 UUID — 36 chars. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Input contract for `saveManifest`. All `*Hex` / UUID fields are validated. */\nexport interface SaveManifestInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Canonical primary image reference (for the wrapper's `image` field). */\n image: string;\n /** SKU name (e.g. `docker-micro`) for the wrapper's `size` field. */\n size: string;\n /** SHA-256 hex of the canonical manifest JSON, from `build_manifest_preview`. */\n metaHash: string;\n /** Chain ID — e.g. `manifest-ledger-testnet-1`. */\n chainId: string;\n /**\n * Canonical Fred-rendered manifest JSON (as a STRING, not a parsed\n * object). The SHA-256 of these bytes (after trimming trailing\n * whitespace) MUST equal `metaHash`.\n */\n manifestJson: string;\n /**\n * Target data directory — the function writes\n * `<dataDir>/manifests/<lease_uuid>.json`. Per gate-1 verdict,\n * supplied by the caller (no env-var read).\n *\n * **MUST be a dedicated manifest-storage directory.** This function\n * `chmod`-tightens any pre-existing `dataDir` (and its `manifests/`\n * subdirectory) to mode `0o700`. Do NOT pass shared parents like\n * `$HOME`, `~/.config`, or a generic data root — doing so would tighten\n * those directories' permissions and potentially break other processes\n * that depend on them. Callers must resolve this to a dedicated\n * subdirectory (e.g. `$XDG_DATA_HOME/manifest-agent/`); the `manifests/`\n * subdir is created inside automatically.\n */\n dataDir: string;\n /** Optional custom-domain FQDN attached to the lease item. */\n customDomain?: string;\n /**\n * Optional stack-lease service name that holds the custom domain.\n * Meaningless without `customDomain`; throws if supplied alone.\n */\n customDomainServiceName?: string;\n}\n\nexport interface SaveManifestResult {\n /** Absolute path to the persisted wrapper. */\n manifestPath: string;\n}\n\n/** Typed error surface for the I/O + validation failure paths. */\nexport class SaveManifestError extends Error {\n readonly code:\n | 'sha256_mismatch'\n | 'manifest_not_object'\n | 'invalid_uuid'\n | 'invalid_meta_hash'\n | 'invalid_data_dir'\n | 'service_name_without_domain'\n | 'manifest_parse_failed'\n | 'platform_unsupported';\n\n constructor(code: SaveManifestError['code'], message: string) {\n super(message);\n this.name = 'SaveManifestError';\n this.code = code;\n Object.setPrototypeOf(this, SaveManifestError.prototype);\n }\n}\n\n/**\n * Persist the manifest wrapper. Returns the absolute output path.\n *\n * Throws `SaveManifestError` for shape / validation failures; lets raw\n * I/O errors (EACCES, ENOSPC, etc.) propagate so the orchestrator can\n * decide whether to suppress them per step-16's \"save-fail → success\n * still returned\" contract.\n */\nexport async function saveManifest(\n input: SaveManifestInput,\n): Promise<SaveManifestResult> {\n if (\n typeof process === 'undefined' ||\n typeof process.versions?.node !== 'string'\n ) {\n throw new SaveManifestError(\n 'platform_unsupported',\n 'saveManifest: requires Node.js runtime (node:fs / node:crypto / node:path)',\n );\n }\n // Copilot review fix (PR #58 r3267373130): reject empty / whitespace-\n // only / non-string `dataDir` BEFORE any I/O. `pathResolve('')` returns\n // `process.cwd()`, and the later `chmodSync(absoluteDataDir, 0o700)`\n // would then tighten the caller's working directory — a real safety\n // hazard if a misconfigured env (`MANIFEST_DATA_DIR=\"\"`) reaches\n // here. Failing fast at the boundary keeps the hazard from\n // materializing.\n if (typeof input.dataDir !== 'string' || input.dataDir.trim().length === 0) {\n throw new SaveManifestError(\n 'invalid_data_dir',\n `saveManifest: dataDir must be a non-empty path; got ${\n typeof input.dataDir === 'string'\n ? `\"${input.dataDir}\"`\n : input.dataDir === null\n ? 'null'\n : typeof input.dataDir\n }.`,\n );\n }\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new SaveManifestError(\n 'invalid_uuid',\n `saveManifest: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (!META_HASH_RE.test(input.metaHash)) {\n throw new SaveManifestError(\n 'invalid_meta_hash',\n `saveManifest: metaHash must be a 64-character SHA-256 hex digest; got \"${input.metaHash}\"`,\n );\n }\n if (input.customDomainServiceName && !input.customDomain) {\n throw new SaveManifestError(\n 'service_name_without_domain',\n 'saveManifest: customDomainServiceName requires customDomain',\n );\n }\n\n // Trim trailing newline (heredoc/Write convention) so the SHA-256 of the\n // persisted bytes matches the meta_hash_hex returned by\n // build_manifest_preview.\n const trimmed = input.manifestJson.trimEnd();\n\n // Parse for shape sanity + format derivation. Failures throw a typed\n // `SaveManifestError(manifest_parse_failed)` rather than the raw\n // SyntaxError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new SaveManifestError(\n 'manifest_parse_failed',\n `saveManifest: manifestJson is not valid JSON: ${reason}`,\n );\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new SaveManifestError(\n 'manifest_not_object',\n 'saveManifest: manifestJson must encode a JSON object',\n );\n }\n const parsedRec = parsed as Record<string, unknown>;\n const isStack =\n parsedRec.services !== null &&\n typeof parsedRec.services === 'object' &&\n !Array.isArray(parsedRec.services);\n const format: 'single' | 'stack' = isStack ? 'stack' : 'single';\n\n // Dynamic imports — node-only deps deferred per the `platform: 'neutral'`\n // build target. Mirrors `guarded-fetch.ts`'s lazy-init pattern.\n const { mkdirSync, chmodSync, writeFileSync, renameSync } = await import(\n 'node:fs'\n );\n const { createHash, randomUUID } = await import('node:crypto');\n const { join, resolve: pathResolve } = await import('node:path');\n\n // SHA-256 audit: catches the most common foot-gun (passing the\n // structured spec where the canonical manifest_json was expected).\n const computedHash = createHash('sha256').update(trimmed).digest('hex');\n if (computedHash !== input.metaHash.toLowerCase()) {\n throw new SaveManifestError(\n 'sha256_mismatch',\n `saveManifest: SHA-256 mismatch. metaHash claims ${input.metaHash} but manifestJson content hashes to ${computedHash}. The wrong content was probably written (e.g. the structured spec instead of the canonical manifest_json string).`,\n );\n }\n\n // C5 fix: resolve dataDir to absolute BEFORE constructing paths.\n // `SaveManifestResult.manifestPath` is documented as absolute; the\n // prior `join(input.dataDir, ...)` returned a relative path when the\n // caller passed a relative dataDir. `path.resolve()` normalizes\n // against the process CWD if input is relative; idempotent for\n // already-absolute inputs.\n const absoluteDataDir = pathResolve(input.dataDir);\n\n // Ensure dataDir + manifests/ exist with tight perms. chmod after mkdir\n // so a pre-existing looser parent gets tightened (mkdir won't chmod\n // existing dirs).\n const manifestsDir = join(absoluteDataDir, 'manifests');\n mkdirSync(absoluteDataDir, { recursive: true, mode: 0o700 });\n chmodSync(absoluteDataDir, 0o700);\n mkdirSync(manifestsDir, { recursive: true, mode: 0o700 });\n chmodSync(manifestsDir, 0o700);\n\n // Copilot review fix (PR #58 r3267708600): single-source the deploy\n // timestamp. The prior code called `new Date().toISOString()` and\n // `Math.floor(Date.now() / 1000)` separately — two distinct clock\n // reads. If the function spans a second boundary, the iso + unix\n // fields refer to different instants, violating the audit\n // metadata's internal-consistency invariant (any tooling cross-\n // checking the pair would flag the drift).\n const deployedAt = new Date();\n const wrapper: Record<string, unknown> = {\n schema_version: 3,\n lease_uuid: input.leaseUuid,\n deployed_at_iso: deployedAt.toISOString(),\n deployed_at_unix: Math.floor(deployedAt.getTime() / 1000),\n chain_id: input.chainId,\n image: input.image,\n size: input.size,\n meta_hash_hex: input.metaHash.toLowerCase(),\n format,\n manifest_json: trimmed,\n };\n if (input.customDomain) {\n wrapper.custom_domain = input.customDomain;\n }\n if (input.customDomainServiceName) {\n wrapper.custom_domain_service_name = input.customDomainServiceName;\n }\n\n const outPath = join(manifestsDir, `${input.leaseUuid}.json`);\n // Atomic write: temp file in same dir + rename. Survives crash mid-write\n // without leaving a partial file at the canonical name. The randomUUID\n // suffix avoids collisions if multiple concurrent saves target the same\n // lease (rare, but the CJS's atomicWrite helper uses the same pattern).\n const tmpPath = `${outPath}.tmp-${randomUUID()}`;\n writeFileSync(tmpPath, `${JSON.stringify(wrapper, null, 2)}\\n`, {\n mode: 0o600,\n });\n renameSync(tmpPath, outPath);\n\n return { manifestPath: outPath };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"save-manifest.js","names":[],"sources":["../../src/internals/save-manifest.ts"],"sourcesContent":["/**\n * Persist a deployed manifest as a schema-version-3 wrapper to disk.\n *\n * Target directory is a **function argument** (`dataDir: string`) rather\n * than an env-var read: per gate-1 verdict, agent-core's `platform: 'neutral'`\n * build target forbids env-var reads at this layer; callers are responsible\n * for resolving the manifests dir from their own env context and passing it in.\n *\n * **Wrapper shape (schema_version 3):**\n *\n * ```\n * {\n * schema_version: 3,\n * lease_uuid, deployed_at_iso, deployed_at_unix,\n * chain_id, image, size, meta_hash_hex,\n * format, // \"single\" or \"stack\"\n * manifest_json, // string — canonical Fred-rendered JSON\n * custom_domain?, // (v3) FQDN attached to the lease item\n * custom_domain_service_name? // (v3) stack-lease service holder\n * }\n * ```\n *\n * Schema-version compat: v2 wrappers remain readable by all downstream\n * helpers; missing v3 fields are tolerated as undefined.\n *\n * **Audit guarantee:** SHA-256 of the bytes about to be persisted (after\n * normalizing the heredoc-/Write-added trailing newline) MUST equal\n * `metaHash`. Catches paste errors, accidental spec-vs-manifest_json\n * swaps, and transit corruption. Mismatch throws a typed\n * `SaveManifestError` (`code: 'sha256_mismatch'`).\n *\n * **Filesystem layout:** `<dataDir>/manifests/<lease_uuid>.json` with\n * mode 0600; parent `<dataDir>` and `<dataDir>/manifests` ensured at\n * mode 0700 (chmod-tightens an existing parent that was previously\n * looser).\n *\n * **Dynamic node-import discipline** (mirrors `guarded-fetch.ts`): the\n * `node:fs` / `node:path` / `node:crypto` imports are deferred to call\n * time so module load doesn't violate the `platform: 'neutral'` build\n * target. A `typeof process` check throws a clear \"Node-only API\" error\n * if invoked outside a Node-like runtime.\n */\n\n/** SHA-256 hex digest — 64 lowercase hex chars. */\nconst META_HASH_RE = /^[0-9a-f]{64}$/i;\n\n/** RFC 4122 UUID — 36 chars. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Input contract for `saveManifest`. All `*Hex` / UUID fields are validated. */\nexport interface SaveManifestInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Canonical primary image reference (for the wrapper's `image` field). */\n image: string;\n /** SKU name (e.g. `docker-micro`) for the wrapper's `size` field. */\n size: string;\n /** SHA-256 hex of the canonical manifest JSON, from `build_manifest_preview`. */\n metaHash: string;\n /** Chain ID — e.g. `manifest-ledger-testnet-1`. */\n chainId: string;\n /**\n * Canonical Fred-rendered manifest JSON (as a STRING, not a parsed\n * object). The SHA-256 of these bytes (after trimming trailing\n * whitespace) MUST equal `metaHash`.\n */\n manifestJson: string;\n /**\n * Target data directory — the function writes\n * `<dataDir>/manifests/<lease_uuid>.json`. Per gate-1 verdict,\n * supplied by the caller (no env-var read).\n *\n * **MUST be a dedicated manifest-storage directory.** This function\n * `chmod`-tightens any pre-existing `dataDir` (and its `manifests/`\n * subdirectory) to mode `0o700`. Do NOT pass shared parents like\n * `$HOME`, `~/.config`, or a generic data root — doing so would tighten\n * those directories' permissions and potentially break other processes\n * that depend on them. Callers must resolve this to a dedicated\n * subdirectory (e.g. `$XDG_DATA_HOME/manifest-agent/`); the `manifests/`\n * subdir is created inside automatically.\n */\n dataDir: string;\n /** Optional custom-domain FQDN attached to the lease item. */\n customDomain?: string;\n /**\n * Optional stack-lease service name that holds the custom domain.\n * Meaningless without `customDomain`; throws if supplied alone.\n */\n customDomainServiceName?: string;\n}\n\nexport interface SaveManifestResult {\n /** Absolute path to the persisted wrapper. */\n manifestPath: string;\n}\n\n/** Typed error surface for the I/O + validation failure paths. */\nexport class SaveManifestError extends Error {\n readonly code:\n | 'sha256_mismatch'\n | 'manifest_not_object'\n | 'invalid_uuid'\n | 'invalid_meta_hash'\n | 'invalid_data_dir'\n | 'service_name_without_domain'\n | 'manifest_parse_failed'\n | 'platform_unsupported';\n\n constructor(code: SaveManifestError['code'], message: string) {\n super(message);\n this.name = 'SaveManifestError';\n this.code = code;\n Object.setPrototypeOf(this, SaveManifestError.prototype);\n }\n}\n\n/**\n * Persist the manifest wrapper. Returns the absolute output path.\n *\n * Throws `SaveManifestError` for shape / validation failures; lets raw\n * I/O errors (EACCES, ENOSPC, etc.) propagate so the orchestrator can\n * decide whether to suppress them per step-16's \"save-fail → success\n * still returned\" contract.\n */\nexport async function saveManifest(\n input: SaveManifestInput,\n): Promise<SaveManifestResult> {\n if (\n typeof process === 'undefined' ||\n typeof process.versions?.node !== 'string'\n ) {\n throw new SaveManifestError(\n 'platform_unsupported',\n 'saveManifest: requires Node.js runtime (node:fs / node:crypto / node:path)',\n );\n }\n // Copilot review fix (PR #58 r3267373130): reject empty / whitespace-\n // only / non-string `dataDir` BEFORE any I/O. `pathResolve('')` returns\n // `process.cwd()`, and the later `chmodSync(absoluteDataDir, 0o700)`\n // would then tighten the caller's working directory — a real safety\n // hazard if a misconfigured env (`MANIFEST_DATA_DIR=\"\"`) reaches\n // here. Failing fast at the boundary keeps the hazard from\n // materializing.\n if (typeof input.dataDir !== 'string' || input.dataDir.trim().length === 0) {\n throw new SaveManifestError(\n 'invalid_data_dir',\n `saveManifest: dataDir must be a non-empty path; got ${\n typeof input.dataDir === 'string'\n ? `\"${input.dataDir}\"`\n : input.dataDir === null\n ? 'null'\n : typeof input.dataDir\n }.`,\n );\n }\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new SaveManifestError(\n 'invalid_uuid',\n `saveManifest: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (!META_HASH_RE.test(input.metaHash)) {\n throw new SaveManifestError(\n 'invalid_meta_hash',\n `saveManifest: metaHash must be a 64-character SHA-256 hex digest; got \"${input.metaHash}\"`,\n );\n }\n if (input.customDomainServiceName && !input.customDomain) {\n throw new SaveManifestError(\n 'service_name_without_domain',\n 'saveManifest: customDomainServiceName requires customDomain',\n );\n }\n\n // Trim trailing newline (heredoc/Write convention) so the SHA-256 of the\n // persisted bytes matches the meta_hash_hex returned by\n // build_manifest_preview.\n const trimmed = input.manifestJson.trimEnd();\n\n // Parse for shape sanity + format derivation. Failures throw a typed\n // `SaveManifestError(manifest_parse_failed)` rather than the raw\n // SyntaxError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new SaveManifestError(\n 'manifest_parse_failed',\n `saveManifest: manifestJson is not valid JSON: ${reason}`,\n );\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new SaveManifestError(\n 'manifest_not_object',\n 'saveManifest: manifestJson must encode a JSON object',\n );\n }\n const parsedRec = parsed as Record<string, unknown>;\n const isStack =\n parsedRec.services !== null &&\n typeof parsedRec.services === 'object' &&\n !Array.isArray(parsedRec.services);\n const format: 'single' | 'stack' = isStack ? 'stack' : 'single';\n\n // Dynamic imports — node-only deps deferred per the `platform: 'neutral'`\n // build target. Mirrors `guarded-fetch.ts`'s lazy-init pattern.\n const { mkdirSync, chmodSync, writeFileSync, renameSync } = await import(\n 'node:fs'\n );\n const { createHash, randomUUID } = await import('node:crypto');\n const { join, resolve: pathResolve } = await import('node:path');\n\n // SHA-256 audit: catches the most common foot-gun (passing the\n // structured spec where the canonical manifest_json was expected).\n const computedHash = createHash('sha256').update(trimmed).digest('hex');\n if (computedHash !== input.metaHash.toLowerCase()) {\n throw new SaveManifestError(\n 'sha256_mismatch',\n `saveManifest: SHA-256 mismatch. metaHash claims ${input.metaHash} but manifestJson content hashes to ${computedHash}. The wrong content was probably written (e.g. the structured spec instead of the canonical manifest_json string).`,\n );\n }\n\n // C5 fix: resolve dataDir to absolute BEFORE constructing paths.\n // `SaveManifestResult.manifestPath` is documented as absolute; the\n // prior `join(input.dataDir, ...)` returned a relative path when the\n // caller passed a relative dataDir. `path.resolve()` normalizes\n // against the process CWD if input is relative; idempotent for\n // already-absolute inputs.\n const absoluteDataDir = pathResolve(input.dataDir);\n\n // Ensure dataDir + manifests/ exist with tight perms. chmod after mkdir\n // so a pre-existing looser parent gets tightened (mkdir won't chmod\n // existing dirs).\n const manifestsDir = join(absoluteDataDir, 'manifests');\n mkdirSync(absoluteDataDir, { recursive: true, mode: 0o700 });\n chmodSync(absoluteDataDir, 0o700);\n mkdirSync(manifestsDir, { recursive: true, mode: 0o700 });\n chmodSync(manifestsDir, 0o700);\n\n // Copilot review fix (PR #58 r3267708600): single-source the deploy\n // timestamp. The prior code called `new Date().toISOString()` and\n // `Math.floor(Date.now() / 1000)` separately — two distinct clock\n // reads. If the function spans a second boundary, the iso + unix\n // fields refer to different instants, violating the audit\n // metadata's internal-consistency invariant (any tooling cross-\n // checking the pair would flag the drift).\n const deployedAt = new Date();\n const wrapper: Record<string, unknown> = {\n schema_version: 3,\n lease_uuid: input.leaseUuid,\n deployed_at_iso: deployedAt.toISOString(),\n deployed_at_unix: Math.floor(deployedAt.getTime() / 1000),\n chain_id: input.chainId,\n image: input.image,\n size: input.size,\n meta_hash_hex: input.metaHash.toLowerCase(),\n format,\n manifest_json: trimmed,\n };\n if (input.customDomain) {\n wrapper.custom_domain = input.customDomain;\n }\n if (input.customDomainServiceName) {\n wrapper.custom_domain_service_name = input.customDomainServiceName;\n }\n\n const outPath = join(manifestsDir, `${input.leaseUuid}.json`);\n // Atomic write: temp file in same dir + rename. Survives crash mid-write\n // without leaving a partial file at the canonical name. The randomUUID\n // suffix avoids collisions if multiple concurrent saves target the same\n // lease (rare, but the CJS's atomicWrite helper uses the same pattern).\n const tmpPath = `${outPath}.tmp-${randomUUID()}`;\n writeFileSync(tmpPath, `${JSON.stringify(wrapper, null, 2)}\\n`, {\n mode: 0o600,\n });\n renameSync(tmpPath, outPath);\n\n return { manifestPath: outPath };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"secret-denylist.d.ts","names":[],"sources":["../../src/internals/secret-denylist.ts"],"mappings":";;AAwBA;;;;;AAGA;;;;;AAkBA;;;;;;;;;;;;cArBa,mBAAA,EAAmB,MAAA;AAAA,cAGnB,wBAAA,EAA0B,WAAA;;;;;;;;;;;;;iBAkBvB,aAAA,CAAc,KAAA"}
1
+ {"version":3,"file":"secret-denylist.d.ts","names":[],"sources":["../../src/internals/secret-denylist.ts"],"mappings":";;AAwBA;;;;AACmG;AAEnG;;;;AAAkD;AAkBlD;;;;AAA4C;;;;;;;;cArB/B,mBAAA,EAAmB,MACmE;AAAA,cAEtF,wBAAA,EAA0B,WAAW;;;;;;;;;;;;;iBAkBlC,aAAA,CAAc,KAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"secret-denylist.js","names":[],"sources":["../../src/internals/secret-denylist.ts"],"sourcesContent":["/**\n * Secret-key denylist + prototype-pollution guard. Used by\n * `verify-recover.ts` to scrub a verifier's diagnostic payload before it\n * reaches the host callback (or the journal record in ENG-124).\n *\n * Two exports:\n *\n * - `SECRET_KEY_DENYLIST` — case-insensitive substring match on KEY names\n * only; values are never inspected. Narrow on purpose: covers the\n * high-confidence sensitive shapes (mnemonic + keyfile password) plus\n * credential-shaped suffixes that catch obvious caller mistakes\n * (`api[_-]?key`, `private[_-]?key`, `secret[_-]?key`, `auth[_-]?token`,\n * `bearer[_-]?token`). The blanket `token` and `secret` keywords are NOT\n * here — this is a blockchain context where `gas_token`, `fee_token`,\n * `token_id`, `token_symbol` are legitimate non-sensitive field names.\n *\n * - `stripDenylist` (in `verify-recover.ts`) — recursive walker over\n * objects + arrays; drops any key matching the denylist regex; ALSO\n * skips the three prototype-pollution-capable keys `__proto__`,\n * `constructor`, `prototype` because `JSON.parse` materializes them as\n * own properties that a bare `out[k] = v` assignment would treat as a\n * prototype mutation.\n */\n\nexport const SECRET_KEY_DENYLIST =\n /(mnemonic|password|private[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token|bearer[_-]?token)/i;\n\nexport const PROTOTYPE_POLLUTION_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\n/**\n * Recursively walk a value and remove any object keys that:\n * - Match `SECRET_KEY_DENYLIST` (case-insensitive substring on key name), or\n * - Are one of the prototype-pollution keys (`__proto__`, `constructor`,\n * `prototype`).\n *\n * Arrays are walked element-wise. Primitives (string/number/boolean/null/\n * undefined) pass through untouched.\n *\n * Returns `unknown` because the structural shape changes: object inputs may\n * have fewer keys than they started with. Callers narrow at use sites.\n */\nexport function stripDenylist(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((v) => stripDenylist(v));\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (PROTOTYPE_POLLUTION_KEYS.has(k)) continue;\n if (SECRET_KEY_DENYLIST.test(k)) continue;\n out[k] = stripDenylist(v);\n }\n return out;\n }\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,sBACX;AAEF,MAAa,2BAAgD,IAAI,IAAI;CACnE;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;AAcF,SAAgB,cAAc,OAAyB;AACrD,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,MAAM,cAAc,EAAE,CAAC;AAE3C,KAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,MAA+B,EAAE;AACvC,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAiC,EAAE;AACrE,OAAI,yBAAyB,IAAI,EAAE,CAAE;AACrC,OAAI,oBAAoB,KAAK,EAAE,CAAE;AACjC,OAAI,KAAK,cAAc,EAAE;;AAE3B,SAAO;;AAET,QAAO"}
1
+ {"version":3,"file":"secret-denylist.js","names":[],"sources":["../../src/internals/secret-denylist.ts"],"sourcesContent":["/**\n * Secret-key denylist + prototype-pollution guard. Used by\n * `verify-recover.ts` to scrub a verifier's diagnostic payload before it\n * reaches the host callback (or the journal record in ENG-124).\n *\n * Two exports:\n *\n * - `SECRET_KEY_DENYLIST` — case-insensitive substring match on KEY names\n * only; values are never inspected. Narrow on purpose: covers the\n * high-confidence sensitive shapes (mnemonic + keyfile password) plus\n * credential-shaped suffixes that catch obvious caller mistakes\n * (`api[_-]?key`, `private[_-]?key`, `secret[_-]?key`, `auth[_-]?token`,\n * `bearer[_-]?token`). The blanket `token` and `secret` keywords are NOT\n * here — this is a blockchain context where `gas_token`, `fee_token`,\n * `token_id`, `token_symbol` are legitimate non-sensitive field names.\n *\n * - `stripDenylist` (in `verify-recover.ts`) — recursive walker over\n * objects + arrays; drops any key matching the denylist regex; ALSO\n * skips the three prototype-pollution-capable keys `__proto__`,\n * `constructor`, `prototype` because `JSON.parse` materializes them as\n * own properties that a bare `out[k] = v` assignment would treat as a\n * prototype mutation.\n */\n\nexport const SECRET_KEY_DENYLIST =\n /(mnemonic|password|private[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token|bearer[_-]?token)/i;\n\nexport const PROTOTYPE_POLLUTION_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\n/**\n * Recursively walk a value and remove any object keys that:\n * - Match `SECRET_KEY_DENYLIST` (case-insensitive substring on key name), or\n * - Are one of the prototype-pollution keys (`__proto__`, `constructor`,\n * `prototype`).\n *\n * Arrays are walked element-wise. Primitives (string/number/boolean/null/\n * undefined) pass through untouched.\n *\n * Returns `unknown` because the structural shape changes: object inputs may\n * have fewer keys than they started with. Callers narrow at use sites.\n */\nexport function stripDenylist(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((v) => stripDenylist(v));\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (PROTOTYPE_POLLUTION_KEYS.has(k)) continue;\n if (SECRET_KEY_DENYLIST.test(k)) continue;\n out[k] = stripDenylist(v);\n }\n return out;\n }\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,sBACX;AAEF,MAAa,2BAAgD,IAAI,IAAI;CACnE;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;AAcD,SAAgB,cAAc,OAAyB;CACrD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,MAAM,cAAc,CAAC,CAAC;CAE1C,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,KAAgC,GAAG;GACrE,IAAI,yBAAyB,IAAI,CAAC,GAAG;GACrC,IAAI,oBAAoB,KAAK,CAAC,GAAG;GACjC,IAAI,KAAK,cAAc,CAAC;EAC1B;EACA,OAAO;CACT;CACA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"spec-normalize.d.ts","names":[],"sources":["../../src/internals/spec-normalize.ts"],"mappings":";;;;;AAgCA;;;;;;;;;;AAmBA;;;;;AAyBA;;;;;;iBA5CgB,WAAA,CACd,IAAA,EAAM,UAAA,sBACL,IAAA,IAAQ,SAAA;;;;AAiDX;;;iBAhCgB,UAAA,CAAW,IAAA,EAAM,UAAA;;;;;;AA+DjC;;UAtCiB,iBAAA;EAsC2C;EApC1D,IAAA;EAoC4B;EAlC5B,GAAA,EAAK,UAAA,GAAa,iBAAA;AAAA;AAAA,iBAGJ,iBAAA,CACd,IAAA,EAAM,UAAA,sBACL,iBAAA;AAwFH;;;;;;;;;;;;;;AAAA,iBA3DgB,aAAA,CAAc,IAAA,EAAM,UAAA,GAAa,WAAA;;;;;;;;;;;;;;;;;;iBA2DjC,YAAA,CAAa,IAAA,EAAM,UAAA"}
1
+ {"version":3,"file":"spec-normalize.d.ts","names":[],"sources":["../../src/internals/spec-normalize.ts"],"mappings":";;;;;AAgCA;;;;;;;;;AAEoB;AAiBpB;;;;AAA8D;AAyB9D;;;;;;iBA5CgB,WAAA,CACd,IAAA,EAAM,UAAA,sBACL,IAAA,IAAQ,SAAS;;;AA8CiB;AAGrC;;;iBAhCgB,UAAA,CAAW,IAAmC,EAA7B,UAAU;;;;;AAkCvB;AA6BpB;;UAtCiB,iBAAA;EAsC2C;EApC1D,IAAA;EAoC4B;EAlC5B,GAAA,EAAK,UAAA,GAAa,iBAAiB;AAAA;AAAA,iBAGrB,iBAAA,CACd,IAAA,EAAM,UAAA,sBACL,iBAAiB;AAwFpB;;;;AAAgE;;;;;;;;;;AAAhE,iBA3DgB,aAAA,CAAc,IAAA,EAAM,UAAA,GAAa,WAAW;;;;;;;;;;;;;;;;;;iBA2D5C,YAAA,CAAa,IAAmC,EAA7B,UAAU"}