@manifest-network/manifest-agent-core 0.9.0 → 0.11.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.
- package/README.md +1 -1
- package/dist/close-lease.d.ts +3 -2
- package/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js +4 -3
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +3 -2
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +245 -77
- package/dist/deploy-app.js.map +1 -1
- package/dist/internals/build-fred-input.d.ts +38 -0
- package/dist/internals/build-fred-input.d.ts.map +1 -0
- package/dist/internals/build-fred-input.js +147 -0
- package/dist/internals/build-fred-input.js.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts +28 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.js +94 -0
- package/dist/internals/evaluate-readiness-from-fred.js.map +1 -0
- package/dist/internals/format-success.js.map +1 -1
- package/dist/internals/guarded-fetch.d.ts +2 -138
- package/dist/internals/guarded-fetch.js +1 -241
- package/dist/internals/humanize-denom.js.map +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/lease-items.js +1 -4
- package/dist/internals/lease-items.js.map +1 -1
- package/dist/internals/render-deployment-plan.js.map +1 -1
- package/dist/internals/verify-recover.js.map +1 -1
- package/dist/manage-domain.d.ts +3 -2
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js +4 -3
- package/dist/manage-domain.js.map +1 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -5
- package/dist/internals/guarded-fetch.d.ts.map +0 -1
- package/dist/internals/guarded-fetch.js.map +0 -1
|
@@ -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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"}
|
|
@@ -35,10 +35,7 @@ function normalizeItem(raw) {
|
|
|
35
35
|
* "Cannot read properties of …" stack trace.
|
|
36
36
|
*/
|
|
37
37
|
function findLease(payload, leaseUuid) {
|
|
38
|
-
if (typeof leaseUuid !== "string") {
|
|
39
|
-
const got = leaseUuid === null ? "null" : typeof leaseUuid;
|
|
40
|
-
throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);
|
|
41
|
-
}
|
|
38
|
+
if (typeof leaseUuid !== "string") throw new TypeError(`findLease: leaseUuid must be a string, got ${leaseUuid === null ? "null" : typeof leaseUuid}`);
|
|
42
39
|
const leases = pickLeasesArray(payload);
|
|
43
40
|
const target = leaseUuid.toLowerCase();
|
|
44
41
|
for (const lease of leases) {
|
|
@@ -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,UAAU
|
|
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 +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":"verify-recover.js","names":[],"sources":["../../src/internals/verify-recover.ts"],"sourcesContent":["import type {\n FailureEnvelope,\n RecoveryChoice,\n RecoveryOption,\n} from '../types.js';\nimport { stripDenylist } from './secret-denylist.js';\n\n/**\n * In-process verify-and-recover driver. Uses an inline async verifier\n * function (L7: agent-core MUST NOT spawn subprocesses).\n *\n * Out of scope (subprocess-only concerns):\n * - verifier-script path sanitization (no path; verifier is a function)\n * - stdin-source indirection (verifier receives typed context)\n * - argv interpolation (verifier closes over context)\n * - `{{var}}` template interpolation on user_message (recovery options\n * carry their own typed-diagnostic closures via `buildRecoveryOptions`)\n * - `timeout` / `maxBuffer` operational caps (no subprocess; an optional\n * AbortController-based timeout can be added per-verifier if needed)\n * - `NODE_ENV` test-override env vars (none of the above need them)\n *\n * Keeps (in-process security still relevant):\n * - `SECRET_KEY_DENYLIST` strip on the diagnostic before it reaches\n * `buildFailureEnvelope` / `buildRecoveryOptions` / the host callback / the result.\n * - Prototype-pollution guard on `__proto__` / `constructor` / `prototype`\n * in the diagnostic walk (defense for verifier-output objects that\n * could have come via `JSON.parse`).\n * - Branch dispatch: `branches[outcome]` → `branches.__other__` →\n * synthesized `unclassified` fallback (CJS calls it `'other'`; the TS\n * port uses `'__other__'` to avoid collisions with a literal outcome\n * string `'other'`).\n *\n * Branch IDs are an internal, closed-set string-literal union — they\n * identify branches for journal/logging purposes but are NOT part of the\n * public type contract (Option A from ENG-128). The public surface for\n * recovery is the frozen `RecoveryOption[]` array, materialized by each\n * branch's inline `buildRecoveryOptions(diag)` closure.\n */\n\n/** Closed-set internal branch identifier. Surfaces via journal/log only. */\nexport type BranchId =\n | 'partial_success_domain'\n | 'lease_terminal'\n | 'domain_verification_mismatch'\n | 'domain_not_found'\n | 'pending_drift'\n | 'unclassified';\n\n/**\n * Per-branch behavior contract. Authored inline at each high-level\n * function's call site (deployApp, manageDomain, etc.) so the closures\n * can bind diagnostic data into the surfaced label/description text.\n */\nexport interface VerificationBranch<TDiag = Record<string, unknown>> {\n /** Internal id for journal write + log; not surfaced to host callbacks directly. */\n readonly branchId: BranchId;\n /** Pass-through tags for the ENG-124 journal `recovery_actions[]`. Empty when not journaling. */\n readonly journalActionTags: readonly string[];\n /** Synthesize the public `FailureEnvelope` (frozen contract) from the post-strip diagnostic. */\n buildFailureEnvelope: (diagnostic: TDiag) => FailureEnvelope;\n /**\n * Materialize the `RecoveryOption[]` for the host's `onFailure` callback.\n * Returning an empty array marks the branch as inform-only:\n * `verifyAndRecover` will return the failure envelope without invoking\n * `onFailure` so callers don't waste a user prompt asking what to do\n * when there's nothing to choose between.\n */\n buildRecoveryOptions: (diagnostic: TDiag) => RecoveryOption[];\n}\n\n/** Verifier function — async; receives typed context; returns typed outcome + free-form diagnostic. */\nexport type Verifier<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> = (context: TContext) => Promise<VerifierResult<TOutcome, TDiag>>;\n\n/** Shape returned by every verifier. `outcome` drives branch selection; `diagnostic` flows into the branch's closures. */\nexport interface VerifierResult<TOutcome extends string, TDiag> {\n outcome: TOutcome;\n diagnostic: TDiag;\n}\n\n/**\n * Verification spec — declarative description of how to verify post-state\n * and dispatch to a recovery branch. Mirrors the CJS spec shape with\n * the subprocess-specific fields dropped.\n *\n * `__other__` is the catch-all branch key, equivalent to the CJS's `'other'`.\n * Renamed to avoid collisions with an outcome literally equal to `'other'`.\n */\nexport interface VerificationSpec<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> {\n readonly verifier: Verifier<TContext, TOutcome, TDiag>;\n /** Outcome values that count as success — no branch dispatch, host's `onFailure` is NOT called. */\n readonly successValues: readonly TOutcome[];\n /** Branch dictionary keyed by outcome string. `__other__` is the catch-all fallback. */\n readonly branches: Partial<\n Record<TOutcome | '__other__', VerificationBranch<TDiag>>\n >;\n}\n\nexport interface VerifyAndRecoverResult<\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> {\n result: 'success' | 'failure';\n verifierOutcome: TOutcome;\n /** `null` on success; the matched branch's id (or `'unclassified'`) on failure. */\n branchId: BranchId | null;\n journalActionTags: readonly string[];\n /** Post-strip diagnostic. Same value the branch closures received. */\n diagnostic: TDiag;\n /** Present iff failure. The synthesized public-surface envelope. */\n failure?: FailureEnvelope;\n /** Present iff failure AND `onFailure` was called AND it returned (i.e., a non-empty `RecoveryOption[]` was presented). */\n recoveryChoice?: RecoveryChoice;\n}\n\nexport interface VerifyAndRecoverCallbacks {\n /**\n * Rich-form failure handler used by `deployApp`. Receives the\n * `FailureEnvelope` synthesized by the matched branch + the closure-\n * built `RecoveryOption[]` and returns the user's pick.\n *\n * Simple-form callers (manageDomain / closeLease / troubleshoot) wrap\n * via an adapter in PR 4 — they don't pass an `onFailure` here directly.\n */\n onFailure?: (\n failure: FailureEnvelope,\n options: RecoveryOption[],\n ) => Promise<RecoveryChoice>;\n}\n\n/**\n * Run the verifier; classify the outcome; on failure, build the public\n * envelope + recovery options and (optionally) invoke the host's\n * `onFailure` callback for a user pick.\n *\n * Throws synchronously on:\n * - Spec runtime-shape violations (missing verifier function, non-array\n * successValues, non-object branches).\n * - Verifier-returned shape violations (missing `outcome` key,\n * non-string `outcome`, missing `diagnostic` key, non-object\n * `diagnostic`).\n * Propagates any error the verifier itself throws.\n */\nexport async function verifyAndRecover<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n>(\n spec: VerificationSpec<TContext, TOutcome, TDiag>,\n context: TContext,\n callbacks: VerifyAndRecoverCallbacks = {},\n): Promise<VerifyAndRecoverResult<TOutcome, TDiag>> {\n validateSpec(spec);\n\n const verifierResult = await spec.verifier(context);\n validateVerifierResult(verifierResult);\n\n // Strip secret-shaped keys + prototype-pollution keys from the\n // diagnostic BEFORE it flows into any branch closure, host callback,\n // or the result object. The strip is the same posture `_journal.cjs`'s\n // `validateRecord` enforces on the write side.\n const diagnostic = stripDenylist(verifierResult.diagnostic) as TDiag;\n const outcome = verifierResult.outcome;\n\n const isSuccess = spec.successValues.includes(outcome);\n if (isSuccess) {\n return {\n result: 'success',\n verifierOutcome: outcome,\n branchId: null,\n journalActionTags: [],\n diagnostic,\n };\n }\n\n // Failure path: dispatch to named branch, `__other__` fallback, or\n // synthesized `unclassified`.\n const branch = selectBranch<TOutcome, TDiag>(spec.branches, outcome);\n const failure = branch.buildFailureEnvelope(diagnostic);\n const options = branch.buildRecoveryOptions(diagnostic);\n\n // Inform-only branches (lease_terminal, unclassified) return [] for\n // RecoveryOption[]. Surface the failure envelope without prompting\n // the host — there's no choice to present.\n if (options.length === 0 || callbacks.onFailure === undefined) {\n return {\n result: 'failure',\n verifierOutcome: outcome,\n branchId: branch.branchId,\n journalActionTags: branch.journalActionTags,\n diagnostic,\n failure,\n };\n }\n\n const recoveryChoice = await callbacks.onFailure(failure, options);\n return {\n result: 'failure',\n verifierOutcome: outcome,\n branchId: branch.branchId,\n journalActionTags: branch.journalActionTags,\n diagnostic,\n failure,\n recoveryChoice,\n };\n}\n\nfunction validateSpec<TContext, TOutcome extends string, TDiag>(\n spec: VerificationSpec<TContext, TOutcome, TDiag>,\n): void {\n if (spec === null || typeof spec !== 'object') {\n throw new Error('verifyAndRecover: spec must be an object');\n }\n if (typeof spec.verifier !== 'function') {\n throw new Error('verifyAndRecover: spec.verifier must be a function');\n }\n if (!Array.isArray(spec.successValues)) {\n throw new Error('verifyAndRecover: spec.successValues must be an array');\n }\n // `typeof null === 'object'` would otherwise let a `branches: null` value\n // slip past a bare typeof check and silently route every failure through\n // the synthesized `unclassified` branch. Explicit guard mirrors the\n // CJS's null-check at line 256-263 of verify-recover.cjs.\n if (\n spec.branches === null ||\n typeof spec.branches !== 'object' ||\n Array.isArray(spec.branches)\n ) {\n throw new Error('verifyAndRecover: spec.branches must be an object');\n }\n}\n\nfunction validateVerifierResult(\n value: unknown,\n): asserts value is VerifierResult<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(\n 'verifyAndRecover: verifier must return an object with shape { outcome, diagnostic }',\n );\n }\n const r = value as { outcome?: unknown; diagnostic?: unknown };\n if (typeof r.outcome !== 'string') {\n throw new Error(\n 'verifyAndRecover: verifier result is missing the required \"outcome\" string field',\n );\n }\n if (\n r.diagnostic === null ||\n typeof r.diagnostic !== 'object' ||\n Array.isArray(r.diagnostic)\n ) {\n throw new Error(\n 'verifyAndRecover: verifier result is missing a \"diagnostic\" object field',\n );\n }\n}\n\nfunction selectBranch<TOutcome extends string, TDiag>(\n branches: Partial<Record<TOutcome | '__other__', VerificationBranch<TDiag>>>,\n outcome: TOutcome,\n): VerificationBranch<TDiag> {\n const named = branches[outcome];\n if (named !== undefined) return named;\n const other = branches.__other__;\n if (other !== undefined) return other;\n return synthesizeUnclassified<TDiag>(outcome);\n}\n\n/**\n * Fabricate the `unclassified` fallback when no named branch and no\n * `__other__` catch-all match. Mirrors the CJS behavior at line 222-232:\n * journal action tag is `verify-unclassified`; the recovery options list\n * is empty (inform-only); the failure envelope conveys the unrecognized\n * outcome verbatim in `reason`.\n */\nfunction synthesizeUnclassified<TDiag>(\n outcome: string,\n): VerificationBranch<TDiag> {\n return {\n branchId: 'unclassified',\n journalActionTags: ['verify-unclassified'],\n buildFailureEnvelope: () => ({\n outcome: 'failed',\n reason: `Verifier returned outcome '${outcome}' — unrecognized; no branch matched.`,\n }),\n buildRecoveryOptions: () => [],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsJA,eAAsB,iBAKpB,MACA,SACA,YAAuC,EAAE,EACS;AAClD,cAAa,KAAK;CAElB,MAAM,iBAAiB,MAAM,KAAK,SAAS,QAAQ;AACnD,wBAAuB,eAAe;CAMtC,MAAM,aAAa,cAAc,eAAe,WAAW;CAC3D,MAAM,UAAU,eAAe;AAG/B,KADkB,KAAK,cAAc,SAAS,QAAQ,CAEpD,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU;EACV,mBAAmB,EAAE;EACrB;EACD;CAKH,MAAM,SAAS,aAA8B,KAAK,UAAU,QAAQ;CACpE,MAAM,UAAU,OAAO,qBAAqB,WAAW;CACvD,MAAM,UAAU,OAAO,qBAAqB,WAAW;AAKvD,KAAI,QAAQ,WAAW,KAAK,UAAU,cAAc,KAAA,EAClD,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU,OAAO;EACjB,mBAAmB,OAAO;EAC1B;EACA;EACD;CAGH,MAAM,iBAAiB,MAAM,UAAU,UAAU,SAAS,QAAQ;AAClE,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU,OAAO;EACjB,mBAAmB,OAAO;EAC1B;EACA;EACA;EACD;;AAGH,SAAS,aACP,MACM;AACN,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,OAAO,KAAK,aAAa,WAC3B,OAAM,IAAI,MAAM,qDAAqD;AAEvE,KAAI,CAAC,MAAM,QAAQ,KAAK,cAAc,CACpC,OAAM,IAAI,MAAM,wDAAwD;AAM1E,KACE,KAAK,aAAa,QAClB,OAAO,KAAK,aAAa,YACzB,MAAM,QAAQ,KAAK,SAAS,CAE5B,OAAM,IAAI,MAAM,oDAAoD;;AAIxE,SAAS,uBACP,OACkD;AAClD,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,MACR,sFACD;CAEH,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,YAAY,SACvB,OAAM,IAAI,MACR,qFACD;AAEH,KACE,EAAE,eAAe,QACjB,OAAO,EAAE,eAAe,YACxB,MAAM,QAAQ,EAAE,WAAW,CAE3B,OAAM,IAAI,MACR,6EACD;;AAIL,SAAS,aACP,UACA,SAC2B;CAC3B,MAAM,QAAQ,SAAS;AACvB,KAAI,UAAU,KAAA,EAAW,QAAO;CAChC,MAAM,QAAQ,SAAS;AACvB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,QAAO,uBAA8B,QAAQ;;;;;;;;;AAU/C,SAAS,uBACP,SAC2B;AAC3B,QAAO;EACL,UAAU;EACV,mBAAmB,CAAC,sBAAsB;EAC1C,6BAA6B;GAC3B,SAAS;GACT,QAAQ,8BAA8B,QAAQ;GAC/C;EACD,4BAA4B,EAAE;EAC/B"}
|
|
1
|
+
{"version":3,"file":"verify-recover.js","names":[],"sources":["../../src/internals/verify-recover.ts"],"sourcesContent":["import type {\n FailureEnvelope,\n RecoveryChoice,\n RecoveryOption,\n} from '../types.js';\nimport { stripDenylist } from './secret-denylist.js';\n\n/**\n * In-process verify-and-recover driver. Uses an inline async verifier\n * function (L7: agent-core MUST NOT spawn subprocesses).\n *\n * Out of scope (subprocess-only concerns):\n * - verifier-script path sanitization (no path; verifier is a function)\n * - stdin-source indirection (verifier receives typed context)\n * - argv interpolation (verifier closes over context)\n * - `{{var}}` template interpolation on user_message (recovery options\n * carry their own typed-diagnostic closures via `buildRecoveryOptions`)\n * - `timeout` / `maxBuffer` operational caps (no subprocess; an optional\n * AbortController-based timeout can be added per-verifier if needed)\n * - `NODE_ENV` test-override env vars (none of the above need them)\n *\n * Keeps (in-process security still relevant):\n * - `SECRET_KEY_DENYLIST` strip on the diagnostic before it reaches\n * `buildFailureEnvelope` / `buildRecoveryOptions` / the host callback / the result.\n * - Prototype-pollution guard on `__proto__` / `constructor` / `prototype`\n * in the diagnostic walk (defense for verifier-output objects that\n * could have come via `JSON.parse`).\n * - Branch dispatch: `branches[outcome]` → `branches.__other__` →\n * synthesized `unclassified` fallback (CJS calls it `'other'`; the TS\n * port uses `'__other__'` to avoid collisions with a literal outcome\n * string `'other'`).\n *\n * Branch IDs are an internal, closed-set string-literal union — they\n * identify branches for journal/logging purposes but are NOT part of the\n * public type contract (Option A from ENG-128). The public surface for\n * recovery is the frozen `RecoveryOption[]` array, materialized by each\n * branch's inline `buildRecoveryOptions(diag)` closure.\n */\n\n/** Closed-set internal branch identifier. Surfaces via journal/log only. */\nexport type BranchId =\n | 'partial_success_domain'\n | 'lease_terminal'\n | 'domain_verification_mismatch'\n | 'domain_not_found'\n | 'pending_drift'\n | 'unclassified';\n\n/**\n * Per-branch behavior contract. Authored inline at each high-level\n * function's call site (deployApp, manageDomain, etc.) so the closures\n * can bind diagnostic data into the surfaced label/description text.\n */\nexport interface VerificationBranch<TDiag = Record<string, unknown>> {\n /** Internal id for journal write + log; not surfaced to host callbacks directly. */\n readonly branchId: BranchId;\n /** Pass-through tags for the ENG-124 journal `recovery_actions[]`. Empty when not journaling. */\n readonly journalActionTags: readonly string[];\n /** Synthesize the public `FailureEnvelope` (frozen contract) from the post-strip diagnostic. */\n buildFailureEnvelope: (diagnostic: TDiag) => FailureEnvelope;\n /**\n * Materialize the `RecoveryOption[]` for the host's `onFailure` callback.\n * Returning an empty array marks the branch as inform-only:\n * `verifyAndRecover` will return the failure envelope without invoking\n * `onFailure` so callers don't waste a user prompt asking what to do\n * when there's nothing to choose between.\n */\n buildRecoveryOptions: (diagnostic: TDiag) => RecoveryOption[];\n}\n\n/** Verifier function — async; receives typed context; returns typed outcome + free-form diagnostic. */\nexport type Verifier<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> = (context: TContext) => Promise<VerifierResult<TOutcome, TDiag>>;\n\n/** Shape returned by every verifier. `outcome` drives branch selection; `diagnostic` flows into the branch's closures. */\nexport interface VerifierResult<TOutcome extends string, TDiag> {\n outcome: TOutcome;\n diagnostic: TDiag;\n}\n\n/**\n * Verification spec — declarative description of how to verify post-state\n * and dispatch to a recovery branch. Mirrors the CJS spec shape with\n * the subprocess-specific fields dropped.\n *\n * `__other__` is the catch-all branch key, equivalent to the CJS's `'other'`.\n * Renamed to avoid collisions with an outcome literally equal to `'other'`.\n */\nexport interface VerificationSpec<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> {\n readonly verifier: Verifier<TContext, TOutcome, TDiag>;\n /** Outcome values that count as success — no branch dispatch, host's `onFailure` is NOT called. */\n readonly successValues: readonly TOutcome[];\n /** Branch dictionary keyed by outcome string. `__other__` is the catch-all fallback. */\n readonly branches: Partial<\n Record<TOutcome | '__other__', VerificationBranch<TDiag>>\n >;\n}\n\nexport interface VerifyAndRecoverResult<\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n> {\n result: 'success' | 'failure';\n verifierOutcome: TOutcome;\n /** `null` on success; the matched branch's id (or `'unclassified'`) on failure. */\n branchId: BranchId | null;\n journalActionTags: readonly string[];\n /** Post-strip diagnostic. Same value the branch closures received. */\n diagnostic: TDiag;\n /** Present iff failure. The synthesized public-surface envelope. */\n failure?: FailureEnvelope;\n /** Present iff failure AND `onFailure` was called AND it returned (i.e., a non-empty `RecoveryOption[]` was presented). */\n recoveryChoice?: RecoveryChoice;\n}\n\nexport interface VerifyAndRecoverCallbacks {\n /**\n * Rich-form failure handler used by `deployApp`. Receives the\n * `FailureEnvelope` synthesized by the matched branch + the closure-\n * built `RecoveryOption[]` and returns the user's pick.\n *\n * Simple-form callers (manageDomain / closeLease / troubleshoot) wrap\n * via an adapter in PR 4 — they don't pass an `onFailure` here directly.\n */\n onFailure?: (\n failure: FailureEnvelope,\n options: RecoveryOption[],\n ) => Promise<RecoveryChoice>;\n}\n\n/**\n * Run the verifier; classify the outcome; on failure, build the public\n * envelope + recovery options and (optionally) invoke the host's\n * `onFailure` callback for a user pick.\n *\n * Throws synchronously on:\n * - Spec runtime-shape violations (missing verifier function, non-array\n * successValues, non-object branches).\n * - Verifier-returned shape violations (missing `outcome` key,\n * non-string `outcome`, missing `diagnostic` key, non-object\n * `diagnostic`).\n * Propagates any error the verifier itself throws.\n */\nexport async function verifyAndRecover<\n TContext,\n TOutcome extends string,\n TDiag = Record<string, unknown>,\n>(\n spec: VerificationSpec<TContext, TOutcome, TDiag>,\n context: TContext,\n callbacks: VerifyAndRecoverCallbacks = {},\n): Promise<VerifyAndRecoverResult<TOutcome, TDiag>> {\n validateSpec(spec);\n\n const verifierResult = await spec.verifier(context);\n validateVerifierResult(verifierResult);\n\n // Strip secret-shaped keys + prototype-pollution keys from the\n // diagnostic BEFORE it flows into any branch closure, host callback,\n // or the result object. The strip is the same posture `_journal.cjs`'s\n // `validateRecord` enforces on the write side.\n const diagnostic = stripDenylist(verifierResult.diagnostic) as TDiag;\n const outcome = verifierResult.outcome;\n\n const isSuccess = spec.successValues.includes(outcome);\n if (isSuccess) {\n return {\n result: 'success',\n verifierOutcome: outcome,\n branchId: null,\n journalActionTags: [],\n diagnostic,\n };\n }\n\n // Failure path: dispatch to named branch, `__other__` fallback, or\n // synthesized `unclassified`.\n const branch = selectBranch<TOutcome, TDiag>(spec.branches, outcome);\n const failure = branch.buildFailureEnvelope(diagnostic);\n const options = branch.buildRecoveryOptions(diagnostic);\n\n // Inform-only branches (lease_terminal, unclassified) return [] for\n // RecoveryOption[]. Surface the failure envelope without prompting\n // the host — there's no choice to present.\n if (options.length === 0 || callbacks.onFailure === undefined) {\n return {\n result: 'failure',\n verifierOutcome: outcome,\n branchId: branch.branchId,\n journalActionTags: branch.journalActionTags,\n diagnostic,\n failure,\n };\n }\n\n const recoveryChoice = await callbacks.onFailure(failure, options);\n return {\n result: 'failure',\n verifierOutcome: outcome,\n branchId: branch.branchId,\n journalActionTags: branch.journalActionTags,\n diagnostic,\n failure,\n recoveryChoice,\n };\n}\n\nfunction validateSpec<TContext, TOutcome extends string, TDiag>(\n spec: VerificationSpec<TContext, TOutcome, TDiag>,\n): void {\n if (spec === null || typeof spec !== 'object') {\n throw new Error('verifyAndRecover: spec must be an object');\n }\n if (typeof spec.verifier !== 'function') {\n throw new Error('verifyAndRecover: spec.verifier must be a function');\n }\n if (!Array.isArray(spec.successValues)) {\n throw new Error('verifyAndRecover: spec.successValues must be an array');\n }\n // `typeof null === 'object'` would otherwise let a `branches: null` value\n // slip past a bare typeof check and silently route every failure through\n // the synthesized `unclassified` branch. Explicit guard mirrors the\n // CJS's null-check at line 256-263 of verify-recover.cjs.\n if (\n spec.branches === null ||\n typeof spec.branches !== 'object' ||\n Array.isArray(spec.branches)\n ) {\n throw new Error('verifyAndRecover: spec.branches must be an object');\n }\n}\n\nfunction validateVerifierResult(\n value: unknown,\n): asserts value is VerifierResult<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(\n 'verifyAndRecover: verifier must return an object with shape { outcome, diagnostic }',\n );\n }\n const r = value as { outcome?: unknown; diagnostic?: unknown };\n if (typeof r.outcome !== 'string') {\n throw new Error(\n 'verifyAndRecover: verifier result is missing the required \"outcome\" string field',\n );\n }\n if (\n r.diagnostic === null ||\n typeof r.diagnostic !== 'object' ||\n Array.isArray(r.diagnostic)\n ) {\n throw new Error(\n 'verifyAndRecover: verifier result is missing a \"diagnostic\" object field',\n );\n }\n}\n\nfunction selectBranch<TOutcome extends string, TDiag>(\n branches: Partial<Record<TOutcome | '__other__', VerificationBranch<TDiag>>>,\n outcome: TOutcome,\n): VerificationBranch<TDiag> {\n const named = branches[outcome];\n if (named !== undefined) return named;\n const other = branches.__other__;\n if (other !== undefined) return other;\n return synthesizeUnclassified<TDiag>(outcome);\n}\n\n/**\n * Fabricate the `unclassified` fallback when no named branch and no\n * `__other__` catch-all match. Mirrors the CJS behavior at line 222-232:\n * journal action tag is `verify-unclassified`; the recovery options list\n * is empty (inform-only); the failure envelope conveys the unrecognized\n * outcome verbatim in `reason`.\n */\nfunction synthesizeUnclassified<TDiag>(\n outcome: string,\n): VerificationBranch<TDiag> {\n return {\n branchId: 'unclassified',\n journalActionTags: ['verify-unclassified'],\n buildFailureEnvelope: () => ({\n outcome: 'failed',\n reason: `Verifier returned outcome '${outcome}' — unrecognized; no branch matched.`,\n }),\n buildRecoveryOptions: () => [],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsJA,eAAsB,iBAKpB,MACA,SACA,YAAuC,EAAE,EACS;AAClD,cAAa,KAAK;CAElB,MAAM,iBAAiB,MAAM,KAAK,SAAS,QAAQ;AACnD,wBAAuB,eAAe;CAMtC,MAAM,aAAa,cAAc,eAAe,WAAW;CAC3D,MAAM,UAAU,eAAe;AAG/B,KADkB,KAAK,cAAc,SAAS,QACjC,CACX,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU;EACV,mBAAmB,EAAE;EACrB;EACD;CAKH,MAAM,SAAS,aAA8B,KAAK,UAAU,QAAQ;CACpE,MAAM,UAAU,OAAO,qBAAqB,WAAW;CACvD,MAAM,UAAU,OAAO,qBAAqB,WAAW;AAKvD,KAAI,QAAQ,WAAW,KAAK,UAAU,cAAc,KAAA,EAClD,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU,OAAO;EACjB,mBAAmB,OAAO;EAC1B;EACA;EACD;CAGH,MAAM,iBAAiB,MAAM,UAAU,UAAU,SAAS,QAAQ;AAClE,QAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,UAAU,OAAO;EACjB,mBAAmB,OAAO;EAC1B;EACA;EACA;EACD;;AAGH,SAAS,aACP,MACM;AACN,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,OAAM,IAAI,MAAM,2CAA2C;AAE7D,KAAI,OAAO,KAAK,aAAa,WAC3B,OAAM,IAAI,MAAM,qDAAqD;AAEvE,KAAI,CAAC,MAAM,QAAQ,KAAK,cAAc,CACpC,OAAM,IAAI,MAAM,wDAAwD;AAM1E,KACE,KAAK,aAAa,QAClB,OAAO,KAAK,aAAa,YACzB,MAAM,QAAQ,KAAK,SAAS,CAE5B,OAAM,IAAI,MAAM,oDAAoD;;AAIxE,SAAS,uBACP,OACkD;AAClD,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,MACR,sFACD;CAEH,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,YAAY,SACvB,OAAM,IAAI,MACR,qFACD;AAEH,KACE,EAAE,eAAe,QACjB,OAAO,EAAE,eAAe,YACxB,MAAM,QAAQ,EAAE,WAAW,CAE3B,OAAM,IAAI,MACR,6EACD;;AAIL,SAAS,aACP,UACA,SAC2B;CAC3B,MAAM,QAAQ,SAAS;AACvB,KAAI,UAAU,KAAA,EAAW,QAAO;CAChC,MAAM,QAAQ,SAAS;AACvB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,QAAO,uBAA8B,QAAQ;;;;;;;;;AAU/C,SAAS,uBACP,SAC2B;AAC3B,QAAO;EACL,UAAU;EACV,mBAAmB,CAAC,sBAAsB;EAC1C,6BAA6B;GAC3B,SAAS;GACT,QAAQ,8BAA8B,QAAQ;GAC/C;EACD,4BAA4B,EAAE;EAC/B"}
|
package/dist/manage-domain.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { ManageDomainArgs, ManageDomainCallbacks, ManageDomainOptions, ManageDom
|
|
|
4
4
|
/**
|
|
5
5
|
* Set / clear / look up a lease item's custom domain.
|
|
6
6
|
*
|
|
7
|
-
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation
|
|
8
|
-
*
|
|
7
|
+
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
|
|
8
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
9
|
+
* `'no'` (deliberate user cancellation — ENG-272).
|
|
9
10
|
* @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
|
|
10
11
|
* from the `setItemCustomDomain()` broadcast step in `set` / `clear`
|
|
11
12
|
* paths. Broadcast errors do NOT invoke `onFailure` — that callback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manage-domain.d.ts","names":[],"sources":["../src/manage-domain.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"manage-domain.d.ts","names":[],"sources":["../src/manage-domain.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkHsB,YAAA,CACpB,IAAA,EAAM,gBAAA,EACN,SAAA,EAAW,qBAAA,EACX,IAAA,EAAM,mBAAA,GACL,OAAA,CAAQ,kBAAA"}
|
package/dist/manage-domain.js
CHANGED
|
@@ -47,8 +47,9 @@ const NOT_FOUND_RES = [
|
|
|
47
47
|
/**
|
|
48
48
|
* Set / clear / look up a lease item's custom domain.
|
|
49
49
|
*
|
|
50
|
-
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation
|
|
51
|
-
*
|
|
50
|
+
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
|
|
51
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
52
|
+
* `'no'` (deliberate user cancellation — ENG-272).
|
|
52
53
|
* @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
|
|
53
54
|
* from the `setItemCustomDomain()` broadcast step in `set` / `clear`
|
|
54
55
|
* paths. Broadcast errors do NOT invoke `onFailure` — that callback
|
|
@@ -80,7 +81,7 @@ async function manageDomain(args, callbacks, opts) {
|
|
|
80
81
|
const fqdn = args.action === "set" ? args.fqdn.trim() : "";
|
|
81
82
|
const block = renderConfirmationBlock(args);
|
|
82
83
|
if (callbacks.onConfirm) {
|
|
83
|
-
if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.
|
|
84
|
+
if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, `User declined to proceed with manage-domain ${args.action}.`);
|
|
84
85
|
}
|
|
85
86
|
callbacks.onProgress?.({ kind: "user_confirmed" });
|
|
86
87
|
const setOpts = args.action === "set" ? serviceName ? { serviceName } : void 0 : {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manage-domain.js","names":[],"sources":["../src/manage-domain.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate setting, clearing, or looking up a\n * lease item's custom domain.\n *\n * Composition (mirrors `deploy-app.ts`'s shape):\n *\n * - `set` / `clear` render a confirmation block, optionally call\n * `onConfirm`, broadcast `setItemCustomDomain` against the agent's\n * bound chain client, then verify the post-broadcast on-chain state\n * via `verifyAndRecover` driving `verify-domain-state` over a direct\n * `billing.v1.lease({ leaseUuid })` single-lease query (tenant-\n * agnostic, no pagination edge cases). Branches are inline closures\n * bound to the per-action context; recovery options are intentionally\n * empty so the verifier surfaces failures via the simple-form\n * `onFailure({ reason })` adapter rather than the rich-form\n * `RecoveryOption[]` prompt (manage-domain has no recovery primitives\n * the orchestrator can dispatch; the user re-runs after a real fix).\n *\n * - `lookup` skips broadcast/verify and resolves the FQDN via the\n * `lease_by_custom_domain` chain query; returns `null` lease when\n * the FQDN isn't claimed.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n setItemCustomDomain,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n type VerifyDomainOutcome,\n type VerifyDomainResult,\n verifyDomainState,\n} from './internals/verify-domain-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n DeploymentPlanBlock,\n ManageDomainArgs,\n ManageDomainCallbacks,\n ManageDomainOptions,\n ManageDomainResult,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n// RFC 1123 hostname: each label 1-63 chars, alphanumeric + hyphens, no leading/\n// trailing hyphen; total ≤253 chars; ≥2 labels (FQDN, not single-label host).\n// Rejects scheme prefixes ('http://'), whitespace, trailing dots, and raw\n// unicode (ASCII punycode `xn--...` is accepted — it matches the regex's\n// `[A-Za-z0-9-]` label character class, which is the standard wire form\n// for IDN labels).\n//\n// Client-side typo gate only. The chain's `MsgSetItemCustomDomain` keeper is\n// the authoritative validator (canonical lowercase, reserved-suffix rules,\n// FQDN format). This anchored regex catches the obvious-malformed-input\n// cases pre-broadcast so we don't waste a tx on `\"\"`, `\" \"`, `\"http://x.y\"`,\n// or `\"not a domain\"`. Anything that passes here still goes through the\n// chain's own validation.\nconst FQDN_RE =\n /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;\n\nconst SCHEME_PREFIX_RE = /^https?:\\/\\//i;\n\n/**\n * Cosmos SDK / gRPC NotFound message patterns. Match against\n * `Error.message` to distinguish chain-keeper NotFound (treated as\n * \"unclaimed FQDN\" → typed `null` result) from real failures (treated\n * as `QUERY_FAILED` throws). Anchored loose patterns to tolerate\n * different keeper / transport formatting (\"not found\", \"NotFound\",\n * \"no such record\", \"does not exist\").\n *\n * Per CLAUDE.md: use `String.prototype.match()` over `RegExp.test()`\n * to avoid the CI security hook's false-positive on shell-execution\n * tokens.\n */\nconst NOT_FOUND_RES: readonly RegExp[] = [\n /not.?found/i,\n /no.?such/i,\n /does.?not.?exist/i,\n];\n\n/**\n * Set / clear / look up a lease item's custom domain.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation or when\n * `onConfirm` returns `'no'`.\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `setItemCustomDomain()` broadcast step in `set` / `clear`\n * paths. Broadcast errors do NOT invoke `onFailure` — that callback\n * is reserved for post-broadcast verification failures.\n * `setItemCustomDomain` already raises a structured `ManifestMCPError`\n * from the core package; wrapping it again at this layer would be\n * redundant. Callers wanting to react to broadcast errors should\n * catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches a `not_found` / `mismatch` outcome (after `onFailure` has\n * been invoked so the caller can react).\n * @throws `ManifestMCPError(QUERY_FAILED)` when a chain query raises a\n * non-NotFound error (RPC / transport / decoding failure). Two paths\n * surface this:\n * - the `lookup` chain query (`lease_by_custom_domain`); the keeper's\n * `NotFound` on an unclaimed FQDN is surfaced as a typed\n * `{ lease: null }` result, not a throw.\n * - the post-broadcast verify chain query (`billing.v1.lease`) in\n * the `set` / `clear` paths (wrapped inside the verifier closure\n * so the failure flows through `onFailure({ reason })` before the\n * throw).\n * Structured `ManifestMCPError`s raised by the chain client are\n * re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function manageDomain(\n args: ManageDomainArgs,\n callbacks: ManageDomainCallbacks,\n opts: ManageDomainOptions,\n): Promise<ManageDomainResult> {\n validateArgs(args);\n\n if (args.action === 'lookup') {\n return await lookupDomain(args.fqdn, callbacks, opts);\n }\n\n const serviceName = args.serviceName;\n // Trim the FQDN silently for `set` (Copilot review PR #60, comment\n // 3276519081): align with the underlying `setItemCustomDomain`\n // primitive (which trims at `packages/core/src/tools/setItemCustomDomain.ts:78-81`)\n // and with `lookupDomain` (which already trims). `validateArgs`\n // continues to reject the empty / whitespace-only case via\n // `args.fqdn.trim() === ''`; this just normalizes the surviving\n // input.\n const fqdn = args.action === 'set' ? args.fqdn.trim() : '';\n\n // --- Confirmation block ---------------------------------------------\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `User declined to proceed with manage-domain ${args.action}.`,\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n // --- Broadcast ------------------------------------------------------\n const setOpts =\n args.action === 'set'\n ? serviceName\n ? { serviceName }\n : undefined\n : {\n clear: true as const,\n ...(serviceName ? { serviceName } : {}),\n };\n await setItemCustomDomain(opts.clientManager, args.leaseUuid, fqdn, setOpts);\n\n // --- Verify ---------------------------------------------------------\n // Direct single-lease query (Copilot review PR #60, comment 3275999569):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n //\n // We wrap the single-lease result as `{ leases: [result.lease] }`\n // (or an empty array if the chain returns no match) so\n // `verifyDomainState` stays untouched — its `findLease` walks the\n // same shape, and a `not_found` outcome falls out naturally when the\n // wrapper array is empty.\n const spec: VerificationSpec<\n unknown,\n VerifyDomainOutcome,\n VerifyDomainResult\n > = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419210): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during ${args.action}-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n const leases = lease === null || lease === undefined ? [] : [lease];\n const decoded = verifyDomainState(\n { leases },\n {\n leaseUuid: args.leaseUuid,\n ...(serviceName ? { serviceName } : {}),\n expected: fqdn,\n },\n );\n return { outcome: decoded.outcome, diagnostic: decoded };\n },\n successValues: ['match'],\n branches: {\n mismatch: {\n branchId: 'domain_verification_mismatch',\n journalActionTags: ['domain-verification-mismatch'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n args.action === 'set'\n ? `Chain shows custom_domain=\"${d.actual ?? ''}\" for lease ${args.leaseUuid}; expected \"${fqdn}\".`\n : `Chain still shows custom_domain=\"${d.actual ?? ''}\" for lease ${args.leaseUuid}; expected cleared.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'domain_not_found',\n journalActionTags: ['domain-verification-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not found when verifying domain state.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n const verified = verifyResult.result === 'success';\n const finalCustomDomain = deriveFinalCustomDomain(\n verifyResult.diagnostic,\n args.action,\n );\n\n if (!verified) {\n const reason =\n verifyResult.failure?.reason ??\n `manage-domain ${args.action} verification failed.`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n const result: ManageDomainResult = {\n action: args.action,\n leaseUuid: args.leaseUuid,\n verified,\n finalCustomDomain,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: ManageDomainArgs): void {\n if (\n args.action !== 'set' &&\n args.action !== 'clear' &&\n args.action !== 'lookup'\n ) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain: unknown action \"${(args as { action?: string }).action}\".`,\n );\n }\n if (args.action === 'lookup') {\n if (typeof args.fqdn !== 'string' || args.fqdn.trim() === '') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'manageDomain lookup: fqdn must be a non-empty string.',\n );\n }\n return;\n }\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain ${args.action}: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n if (args.action === 'set') {\n if (typeof args.fqdn !== 'string' || args.fqdn.trim() === '') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'manageDomain set: fqdn must be a non-empty string.',\n );\n }\n // Trim silently (Copilot review PR #60, comment 3276519081):\n // align with `setItemCustomDomain` (which trims at\n // `packages/core/src/tools/setItemCustomDomain.ts:78-81`) and with\n // `lookupDomain` (which already trims). Validation gates below\n // run against the trimmed candidate; the broadcast and confirm\n // block (rendered in the caller) also use the trimmed form.\n const candidate = args.fqdn.trim();\n if (candidate.match(SCHEME_PREFIX_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain set: fqdn must be a bare hostname (no scheme); got \"${args.fqdn}\".`,\n );\n }\n if (!candidate.match(FQDN_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain set: fqdn \"${args.fqdn}\" is not a valid RFC 1123 hostname (≤253 chars, ≥2 dot-separated labels of 1-63 alphanumeric/hyphen chars; no leading/trailing hyphens).`,\n );\n }\n }\n}\n\nfunction renderConfirmationBlock(\n args: Exclude<ManageDomainArgs, { action: 'lookup' }>,\n): DeploymentPlanBlock {\n const lines: string[] = [];\n if (args.action === 'set') {\n lines.push(`Set custom domain on lease ${args.leaseUuid}:`);\n // Display the trimmed FQDN — matches the value that will be\n // broadcast (per the silent-trim semantics aligned with\n // setItemCustomDomain) so users don't see whitespace in the\n // confirm prompt that won't appear on-chain.\n lines.push(` FQDN: ${args.fqdn.trim()}`);\n if (args.serviceName) {\n lines.push(` Service: ${args.serviceName}`);\n }\n lines.push('');\n lines.push('Proceed?');\n } else {\n lines.push(`Clear custom domain on lease ${args.leaseUuid}:`);\n if (args.serviceName) {\n lines.push(` Service: ${args.serviceName}`);\n }\n lines.push('');\n lines.push('Proceed?');\n }\n return { text: lines.join('\\n') };\n}\n\nasync function lookupDomain(\n fqdn: string,\n callbacks: ManageDomainCallbacks,\n opts: ManageDomainOptions,\n): Promise<ManageDomainResult> {\n const customDomain = fqdn.trim();\n let result: unknown;\n try {\n // Pull `getQueryClient()` INSIDE the try (Copilot review PR #60,\n // comment 3276719558). `getQueryClient()` can throw\n // `INVALID_CONFIG` (neither rpcUrl nor restUrl set) or\n // `RPC_CONNECTION_FAILED` (connect failure). Catching here routes\n // those init-time failures through the same `onFailure` +\n // QUERY_FAILED / structured-passthrough normalization the chain-\n // query failure mode already gets. The set/clear verifier closure\n // (in `manageDomain` body above) already wraps `getQueryClient()`\n // since commit d9793c1; this brings `lookupDomain` to parity.\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.leaseByCustomDomain({\n customDomain,\n });\n } catch (err) {\n // Narrowed disambiguation (Copilot review PR #60): the chain keeper\n // raises a NotFound-shaped error when the FQDN is unclaimed (cosmjs/\n // grpc surfaces this as a plain `Error` whose message matches\n // `/not.?found|no.?such|does.?not.?exist/i`). Only that case is\n // collapsed to the typed `{ lease: null }` result. Every other\n // failure mode (RPC transport, decoding, structured\n // `ManifestMCPError`, etc.) flows through `onFailure({ reason })`\n // then a typed throw — matching the lease-package's\n // `lease_by_custom_domain` handler (packages/lease/src/index.ts:442)\n // and `getBalance`'s `catchNotFound` pattern (packages/core/src/\n // tools/getBalance.ts:4). The bare `catch` was masking real failures.\n if (isNotFoundError(err)) {\n const notFoundResult: ManageDomainResult = {\n action: 'lookup',\n fqdn: customDomain,\n lease: null,\n };\n callbacks.onComplete?.(notFoundResult);\n return notFoundResult;\n }\n const reason = `lease_by_custom_domain lookup failed for \"${customDomain}\": ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const uuid = readLeaseUuid((result as { lease?: unknown })?.lease);\n const lookupResult: ManageDomainResult = {\n action: 'lookup',\n fqdn: customDomain,\n lease: uuid ? { leaseUuid: uuid } : null,\n };\n // Symmetric `onComplete` fire (Copilot review PR #60, comment\n // 3288656598). Pre-fix, the lookup path returned without invoking\n // `onComplete` — asymmetric vs the set/clear paths in this same\n // function and vs `closeLease` / `troubleshootDeployment`. Was\n // documented as \"intentional\" in PR_DESCRIPTION.md Risks #1 but\n // was really an oversight rationalized post-hoc. Now consistent.\n callbacks.onComplete?.(lookupResult);\n return lookupResult;\n}\n\nfunction isNotFoundError(err: unknown): boolean {\n // Pass-through guard for structured failures: a `ManifestMCPError` is\n // always a real, intentional error — never silently re-classified as\n // \"FQDN unclaimed\" even if its message happens to contain \"not found\".\n if (err instanceof ManifestMCPError) return false;\n if (!(err instanceof Error)) return false;\n const msg = err.message;\n return NOT_FOUND_RES.some((re) => msg.match(re) !== null);\n}\n\nfunction readLeaseUuid(lease: unknown): string | undefined {\n if (lease === null || typeof lease !== 'object') return undefined;\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 return typeof u === 'string' && u.length > 0 ? u : undefined;\n}\n\nfunction deriveFinalCustomDomain(\n diagnostic: VerifyDomainResult,\n action: 'set' | 'clear',\n): string | null {\n if (action === 'clear') return null;\n const actual = diagnostic.actual ?? '';\n return actual.length > 0 ? actual : null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manage-domain.js","names":[],"sources":["../src/manage-domain.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate setting, clearing, or looking up a\n * lease item's custom domain.\n *\n * Composition (mirrors `deploy-app.ts`'s shape):\n *\n * - `set` / `clear` render a confirmation block, optionally call\n * `onConfirm`, broadcast `setItemCustomDomain` against the agent's\n * bound chain client, then verify the post-broadcast on-chain state\n * via `verifyAndRecover` driving `verify-domain-state` over a direct\n * `billing.v1.lease({ leaseUuid })` single-lease query (tenant-\n * agnostic, no pagination edge cases). Branches are inline closures\n * bound to the per-action context; recovery options are intentionally\n * empty so the verifier surfaces failures via the simple-form\n * `onFailure({ reason })` adapter rather than the rich-form\n * `RecoveryOption[]` prompt (manage-domain has no recovery primitives\n * the orchestrator can dispatch; the user re-runs after a real fix).\n *\n * - `lookup` skips broadcast/verify and resolves the FQDN via the\n * `lease_by_custom_domain` chain query; returns `null` lease when\n * the FQDN isn't claimed.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n setItemCustomDomain,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n type VerifyDomainOutcome,\n type VerifyDomainResult,\n verifyDomainState,\n} from './internals/verify-domain-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n DeploymentPlanBlock,\n ManageDomainArgs,\n ManageDomainCallbacks,\n ManageDomainOptions,\n ManageDomainResult,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n// RFC 1123 hostname: each label 1-63 chars, alphanumeric + hyphens, no leading/\n// trailing hyphen; total ≤253 chars; ≥2 labels (FQDN, not single-label host).\n// Rejects scheme prefixes ('http://'), whitespace, trailing dots, and raw\n// unicode (ASCII punycode `xn--...` is accepted — it matches the regex's\n// `[A-Za-z0-9-]` label character class, which is the standard wire form\n// for IDN labels).\n//\n// Client-side typo gate only. The chain's `MsgSetItemCustomDomain` keeper is\n// the authoritative validator (canonical lowercase, reserved-suffix rules,\n// FQDN format). This anchored regex catches the obvious-malformed-input\n// cases pre-broadcast so we don't waste a tx on `\"\"`, `\" \"`, `\"http://x.y\"`,\n// or `\"not a domain\"`. Anything that passes here still goes through the\n// chain's own validation.\nconst FQDN_RE =\n /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;\n\nconst SCHEME_PREFIX_RE = /^https?:\\/\\//i;\n\n/**\n * Cosmos SDK / gRPC NotFound message patterns. Match against\n * `Error.message` to distinguish chain-keeper NotFound (treated as\n * \"unclaimed FQDN\" → typed `null` result) from real failures (treated\n * as `QUERY_FAILED` throws). Anchored loose patterns to tolerate\n * different keeper / transport formatting (\"not found\", \"NotFound\",\n * \"no such record\", \"does not exist\").\n *\n * Per CLAUDE.md: use `String.prototype.match()` over `RegExp.test()`\n * to avoid the CI security hook's false-positive on shell-execution\n * tokens.\n */\nconst NOT_FOUND_RES: readonly RegExp[] = [\n /not.?found/i,\n /no.?such/i,\n /does.?not.?exist/i,\n];\n\n/**\n * Set / clear / look up a lease item's custom domain.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.\n * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns\n * `'no'` (deliberate user cancellation — ENG-272).\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `setItemCustomDomain()` broadcast step in `set` / `clear`\n * paths. Broadcast errors do NOT invoke `onFailure` — that callback\n * is reserved for post-broadcast verification failures.\n * `setItemCustomDomain` already raises a structured `ManifestMCPError`\n * from the core package; wrapping it again at this layer would be\n * redundant. Callers wanting to react to broadcast errors should\n * catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches a `not_found` / `mismatch` outcome (after `onFailure` has\n * been invoked so the caller can react).\n * @throws `ManifestMCPError(QUERY_FAILED)` when a chain query raises a\n * non-NotFound error (RPC / transport / decoding failure). Two paths\n * surface this:\n * - the `lookup` chain query (`lease_by_custom_domain`); the keeper's\n * `NotFound` on an unclaimed FQDN is surfaced as a typed\n * `{ lease: null }` result, not a throw.\n * - the post-broadcast verify chain query (`billing.v1.lease`) in\n * the `set` / `clear` paths (wrapped inside the verifier closure\n * so the failure flows through `onFailure({ reason })` before the\n * throw).\n * Structured `ManifestMCPError`s raised by the chain client are\n * re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function manageDomain(\n args: ManageDomainArgs,\n callbacks: ManageDomainCallbacks,\n opts: ManageDomainOptions,\n): Promise<ManageDomainResult> {\n validateArgs(args);\n\n if (args.action === 'lookup') {\n return await lookupDomain(args.fqdn, callbacks, opts);\n }\n\n const serviceName = args.serviceName;\n // Trim the FQDN silently for `set` (Copilot review PR #60, comment\n // 3276519081): align with the underlying `setItemCustomDomain`\n // primitive (which trims at `packages/core/src/tools/setItemCustomDomain.ts:78-81`)\n // and with `lookupDomain` (which already trims). `validateArgs`\n // continues to reject the empty / whitespace-only case via\n // `args.fqdn.trim() === ''`; this just normalizes the surviving\n // input.\n const fqdn = args.action === 'set' ? args.fqdn.trim() : '';\n\n // --- Confirmation block ---------------------------------------------\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n `User declined to proceed with manage-domain ${args.action}.`,\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n // --- Broadcast ------------------------------------------------------\n const setOpts =\n args.action === 'set'\n ? serviceName\n ? { serviceName }\n : undefined\n : {\n clear: true as const,\n ...(serviceName ? { serviceName } : {}),\n };\n await setItemCustomDomain(opts.clientManager, args.leaseUuid, fqdn, setOpts);\n\n // --- Verify ---------------------------------------------------------\n // Direct single-lease query (Copilot review PR #60, comment 3275999569):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n //\n // We wrap the single-lease result as `{ leases: [result.lease] }`\n // (or an empty array if the chain returns no match) so\n // `verifyDomainState` stays untouched — its `findLease` walks the\n // same shape, and a `not_found` outcome falls out naturally when the\n // wrapper array is empty.\n const spec: VerificationSpec<\n unknown,\n VerifyDomainOutcome,\n VerifyDomainResult\n > = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419210): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during ${args.action}-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n const leases = lease === null || lease === undefined ? [] : [lease];\n const decoded = verifyDomainState(\n { leases },\n {\n leaseUuid: args.leaseUuid,\n ...(serviceName ? { serviceName } : {}),\n expected: fqdn,\n },\n );\n return { outcome: decoded.outcome, diagnostic: decoded };\n },\n successValues: ['match'],\n branches: {\n mismatch: {\n branchId: 'domain_verification_mismatch',\n journalActionTags: ['domain-verification-mismatch'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n args.action === 'set'\n ? `Chain shows custom_domain=\"${d.actual ?? ''}\" for lease ${args.leaseUuid}; expected \"${fqdn}\".`\n : `Chain still shows custom_domain=\"${d.actual ?? ''}\" for lease ${args.leaseUuid}; expected cleared.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'domain_not_found',\n journalActionTags: ['domain-verification-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not found when verifying domain state.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n const verified = verifyResult.result === 'success';\n const finalCustomDomain = deriveFinalCustomDomain(\n verifyResult.diagnostic,\n args.action,\n );\n\n if (!verified) {\n const reason =\n verifyResult.failure?.reason ??\n `manage-domain ${args.action} verification failed.`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n const result: ManageDomainResult = {\n action: args.action,\n leaseUuid: args.leaseUuid,\n verified,\n finalCustomDomain,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: ManageDomainArgs): void {\n if (\n args.action !== 'set' &&\n args.action !== 'clear' &&\n args.action !== 'lookup'\n ) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain: unknown action \"${(args as { action?: string }).action}\".`,\n );\n }\n if (args.action === 'lookup') {\n if (typeof args.fqdn !== 'string' || args.fqdn.trim() === '') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'manageDomain lookup: fqdn must be a non-empty string.',\n );\n }\n return;\n }\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain ${args.action}: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n if (args.action === 'set') {\n if (typeof args.fqdn !== 'string' || args.fqdn.trim() === '') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'manageDomain set: fqdn must be a non-empty string.',\n );\n }\n // Trim silently (Copilot review PR #60, comment 3276519081):\n // align with `setItemCustomDomain` (which trims at\n // `packages/core/src/tools/setItemCustomDomain.ts:78-81`) and with\n // `lookupDomain` (which already trims). Validation gates below\n // run against the trimmed candidate; the broadcast and confirm\n // block (rendered in the caller) also use the trimmed form.\n const candidate = args.fqdn.trim();\n if (candidate.match(SCHEME_PREFIX_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain set: fqdn must be a bare hostname (no scheme); got \"${args.fqdn}\".`,\n );\n }\n if (!candidate.match(FQDN_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `manageDomain set: fqdn \"${args.fqdn}\" is not a valid RFC 1123 hostname (≤253 chars, ≥2 dot-separated labels of 1-63 alphanumeric/hyphen chars; no leading/trailing hyphens).`,\n );\n }\n }\n}\n\nfunction renderConfirmationBlock(\n args: Exclude<ManageDomainArgs, { action: 'lookup' }>,\n): DeploymentPlanBlock {\n const lines: string[] = [];\n if (args.action === 'set') {\n lines.push(`Set custom domain on lease ${args.leaseUuid}:`);\n // Display the trimmed FQDN — matches the value that will be\n // broadcast (per the silent-trim semantics aligned with\n // setItemCustomDomain) so users don't see whitespace in the\n // confirm prompt that won't appear on-chain.\n lines.push(` FQDN: ${args.fqdn.trim()}`);\n if (args.serviceName) {\n lines.push(` Service: ${args.serviceName}`);\n }\n lines.push('');\n lines.push('Proceed?');\n } else {\n lines.push(`Clear custom domain on lease ${args.leaseUuid}:`);\n if (args.serviceName) {\n lines.push(` Service: ${args.serviceName}`);\n }\n lines.push('');\n lines.push('Proceed?');\n }\n return { text: lines.join('\\n') };\n}\n\nasync function lookupDomain(\n fqdn: string,\n callbacks: ManageDomainCallbacks,\n opts: ManageDomainOptions,\n): Promise<ManageDomainResult> {\n const customDomain = fqdn.trim();\n let result: unknown;\n try {\n // Pull `getQueryClient()` INSIDE the try (Copilot review PR #60,\n // comment 3276719558). `getQueryClient()` can throw\n // `INVALID_CONFIG` (neither rpcUrl nor restUrl set) or\n // `RPC_CONNECTION_FAILED` (connect failure). Catching here routes\n // those init-time failures through the same `onFailure` +\n // QUERY_FAILED / structured-passthrough normalization the chain-\n // query failure mode already gets. The set/clear verifier closure\n // (in `manageDomain` body above) already wraps `getQueryClient()`\n // since commit d9793c1; this brings `lookupDomain` to parity.\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.leaseByCustomDomain({\n customDomain,\n });\n } catch (err) {\n // Narrowed disambiguation (Copilot review PR #60): the chain keeper\n // raises a NotFound-shaped error when the FQDN is unclaimed (cosmjs/\n // grpc surfaces this as a plain `Error` whose message matches\n // `/not.?found|no.?such|does.?not.?exist/i`). Only that case is\n // collapsed to the typed `{ lease: null }` result. Every other\n // failure mode (RPC transport, decoding, structured\n // `ManifestMCPError`, etc.) flows through `onFailure({ reason })`\n // then a typed throw — matching the lease-package's\n // `lease_by_custom_domain` handler (packages/lease/src/index.ts:442)\n // and `getBalance`'s `catchNotFound` pattern (packages/core/src/\n // tools/getBalance.ts:4). The bare `catch` was masking real failures.\n if (isNotFoundError(err)) {\n const notFoundResult: ManageDomainResult = {\n action: 'lookup',\n fqdn: customDomain,\n lease: null,\n };\n callbacks.onComplete?.(notFoundResult);\n return notFoundResult;\n }\n const reason = `lease_by_custom_domain lookup failed for \"${customDomain}\": ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const uuid = readLeaseUuid((result as { lease?: unknown })?.lease);\n const lookupResult: ManageDomainResult = {\n action: 'lookup',\n fqdn: customDomain,\n lease: uuid ? { leaseUuid: uuid } : null,\n };\n // Symmetric `onComplete` fire (Copilot review PR #60, comment\n // 3288656598). Pre-fix, the lookup path returned without invoking\n // `onComplete` — asymmetric vs the set/clear paths in this same\n // function and vs `closeLease` / `troubleshootDeployment`. Was\n // documented as \"intentional\" in PR_DESCRIPTION.md Risks #1 but\n // was really an oversight rationalized post-hoc. Now consistent.\n callbacks.onComplete?.(lookupResult);\n return lookupResult;\n}\n\nfunction isNotFoundError(err: unknown): boolean {\n // Pass-through guard for structured failures: a `ManifestMCPError` is\n // always a real, intentional error — never silently re-classified as\n // \"FQDN unclaimed\" even if its message happens to contain \"not found\".\n if (err instanceof ManifestMCPError) return false;\n if (!(err instanceof Error)) return false;\n const msg = err.message;\n return NOT_FOUND_RES.some((re) => msg.match(re) !== null);\n}\n\nfunction readLeaseUuid(lease: unknown): string | undefined {\n if (lease === null || typeof lease !== 'object') return undefined;\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 return typeof u === 'string' && u.length > 0 ? u : undefined;\n}\n\nfunction deriveFinalCustomDomain(\n diagnostic: VerifyDomainResult,\n action: 'set' | 'clear',\n): string | null {\n if (action === 'clear') return null;\n const actual = diagnostic.actual ?? '';\n return actual.length > 0 ? actual : null;\n}\n"],"mappings":"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"}
|
package/dist/troubleshoot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"troubleshoot.js","names":["decodeLeaseState"],"sources":["../src/troubleshoot.ts"],"sourcesContent":["/**\n * Public entry point: produce a markdown-formatted diagnostic report\n * for a given lease.\n *\n * Chain-only (no provider HTTP calls — `TroubleshootOptions` has no\n * `walletProvider` for ADR-036 auth). Composes:\n *\n * - `queryClient.liftedinit.billing.v1.lease({ leaseUuid })` for the\n * authoritative chain-side lease record.\n * - `lease-state.decode` + `isTerminal` to translate the integer\n * state into a canonical `LEASE_STATE_*` name and a\n * guidance-routing terminal/non-terminal classification.\n * - `lease-items.normalizeItem` to surface each item's serviceName\n * and customDomain regardless of snake/camelCase payload shape.\n *\n * The returned `markdown` is plain text with markdown formatting — host\n * surfaces can render it in chat directly or embed in a richer\n * diagnostic UI.\n *\n * **Scope:** chain-only. `TroubleshootOptions` carries no `walletProvider`,\n * so provider-side diagnostics (`appStatus` / `getLeaseProvision` /\n * `getAppLogs`) are out of scope for this function. If the report\n * surfaces a recovery-worthy state (e.g. terminal / drift), the caller\n * composes `closeLease()` separately — agent-core's simple-form\n * `onFailure({ reason })` does not carry recovery options, so the\n * orchestration of \"report → decide → close\" lives at the host surface.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n} from '@manifest-network/manifest-mcp-core';\nimport { normalizeItem } from './internals/lease-items.js';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport type {\n LeaseStateName,\n TroubleshootArgs,\n TroubleshootCallbacks,\n TroubleshootOptions,\n TroubleshootReport,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Generate a diagnostic markdown report for `args.leaseUuid`.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.\n * @throws `ManifestMCPError(QUERY_FAILED)` in two cases, both with\n * `onFailure({ reason })` invoked first:\n * - the chain query rejects with a plain `Error` (RPC / transport\n * / decoding failure); or\n * - the chain query succeeds but returns `{ lease: null }` /\n * `undefined` (lease UUID not on-chain — the `billing.v1.lease`\n * no-such-lease response shape).\n * Structured `ManifestMCPError`s raised by the chain client\n * (e.g. `INVALID_CONFIG` from missing rpc/rest url config,\n * `RPC_CONNECTION_FAILED` from connect failure) are re-thrown\n * as-is with their original code, with `onFailure` invoked first.\n */\nexport async function troubleshootDeployment(\n args: TroubleshootArgs,\n callbacks: TroubleshootCallbacks,\n opts: TroubleshootOptions,\n): Promise<TroubleshootReport> {\n validateArgs(args);\n\n let leasePayload: unknown;\n try {\n // Pull `getQueryClient()` INSIDE the try (Copilot review PR #60,\n // comment 3276719462). `getQueryClient()` can throw\n // `INVALID_CONFIG` (neither rpcUrl nor restUrl set) or\n // `RPC_CONNECTION_FAILED` (connect failure). Catching here routes\n // those init-time failures through the same `onFailure` +\n // QUERY_FAILED / structured-passthrough normalization the chain-\n // query failure mode already gets — three modes, one disambiguation.\n const queryClient = await opts.clientManager.getQueryClient();\n const result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n leasePayload = result.lease;\n } catch (err) {\n // Preserve structured `ManifestMCPError`s from the chain client\n // (Copilot review PR #60, comment 3276172289). Wrapping every\n // failure as `QUERY_FAILED` erases upstream error codes — a real\n // `INVALID_CONFIG` from the chain layer should surface to callers\n // with that code, not be collapsed to a less-specific category.\n // Mirrors the disambiguation `manage-domain.ts:lookupDomain`\n // adopted in commit aaa5cc5. Note: chain-NotFound for\n // `billing.v1.lease({ leaseUuid })` returns `{ lease: null }`\n // (handled below), so errors landing here are genuinely transport\n // or structured failures.\n const reason = `Failed to query lease ${args.leaseUuid}: ${err instanceof Error ? err.message : String(err)}`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n\n if (leasePayload === null || leasePayload === undefined) {\n const reason = `Lease ${args.leaseUuid} not found on chain.`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n\n const markdown = renderReport(args.leaseUuid, leasePayload);\n const report: TroubleshootReport = { markdown };\n callbacks.onComplete?.(report);\n return report;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: TroubleshootArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `troubleshootDeployment: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\ninterface LeaseShape {\n uuid?: unknown;\n state?: unknown;\n providerUuid?: unknown;\n provider_uuid?: unknown;\n createdAt?: unknown;\n created_at?: unknown;\n closedAt?: unknown;\n closed_at?: unknown;\n items?: unknown;\n}\n\nfunction renderReport(leaseUuid: string, lease: unknown): string {\n const l = (lease ?? {}) as LeaseShape;\n const rawState = l.state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n const stateLabel = stateName ?? `UNKNOWN(${String(rawState)})`;\n const providerUuid =\n readString(l.providerUuid) || readString(l.provider_uuid) || '(unknown)';\n const createdAt = readTimestamp(l.createdAt) ?? readTimestamp(l.created_at);\n const closedAt = readTimestamp(l.closedAt) ?? readTimestamp(l.closed_at);\n\n const rawItems = Array.isArray(l.items) ? l.items : [];\n const items = rawItems.map(normalizeItem);\n\n const lines: string[] = [];\n lines.push(`# Lease diagnostic — ${leaseUuid}`);\n lines.push('');\n lines.push('## Chain state');\n lines.push('');\n lines.push(`- **State:** ${stateLabel}`);\n lines.push(`- **Provider:** ${providerUuid}`);\n if (createdAt) lines.push(`- **Created:** ${createdAt}`);\n if (closedAt) lines.push(`- **Closed:** ${closedAt}`);\n lines.push('');\n\n lines.push('## Items');\n lines.push('');\n if (items.length === 0) {\n lines.push('_No items found on this lease._');\n } else {\n for (const item of items) {\n const svc = item.serviceName.length > 0 ? item.serviceName : '(default)';\n const dom =\n item.customDomain.length > 0 ? item.customDomain : '(no custom domain)';\n lines.push(`- **${svc}** → ${dom}`);\n }\n }\n lines.push('');\n\n lines.push('## Guidance');\n lines.push('');\n for (const tip of guidanceFor(stateName)) {\n lines.push(`- ${tip}`);\n }\n\n return lines.join('\\n');\n}\n\nfunction guidanceFor(state: LeaseStateName | undefined): string[] {\n if (state === undefined) {\n return [\n 'Lease state could not be decoded. Re-query in a moment, or check the chain client logs for transport errors.',\n ];\n }\n if (isTerminal(state)) {\n return [\n `Lease is in terminal state \\`${state}\\`. No further provider activity expected.`,\n 'To redeploy, create a new lease via `deployApp`.',\n ];\n }\n switch (state) {\n case 'LEASE_STATE_PENDING':\n return [\n 'Lease is awaiting provider acknowledgement.',\n 'If pending persists for more than a few minutes, the provider may be offline; consider closing and redeploying.',\n ];\n case 'LEASE_STATE_ACTIVE':\n return [\n 'Lease is active on the provider. App-level status / logs require a provider HTTP call with an ADR-036 auth token (out of scope for this chain-only diagnostic).',\n ];\n default:\n return [\n `Lease state is \\`${state}\\`. Review the chain proto for the expected next transition.`,\n ];\n }\n}\n\nfunction readString(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction readTimestamp(value: unknown): string | undefined {\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string' && value.length > 0) return value;\n return undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"troubleshoot.js","names":["decodeLeaseState"],"sources":["../src/troubleshoot.ts"],"sourcesContent":["/**\n * Public entry point: produce a markdown-formatted diagnostic report\n * for a given lease.\n *\n * Chain-only (no provider HTTP calls — `TroubleshootOptions` has no\n * `walletProvider` for ADR-036 auth). Composes:\n *\n * - `queryClient.liftedinit.billing.v1.lease({ leaseUuid })` for the\n * authoritative chain-side lease record.\n * - `lease-state.decode` + `isTerminal` to translate the integer\n * state into a canonical `LEASE_STATE_*` name and a\n * guidance-routing terminal/non-terminal classification.\n * - `lease-items.normalizeItem` to surface each item's serviceName\n * and customDomain regardless of snake/camelCase payload shape.\n *\n * The returned `markdown` is plain text with markdown formatting — host\n * surfaces can render it in chat directly or embed in a richer\n * diagnostic UI.\n *\n * **Scope:** chain-only. `TroubleshootOptions` carries no `walletProvider`,\n * so provider-side diagnostics (`appStatus` / `getLeaseProvision` /\n * `getAppLogs`) are out of scope for this function. If the report\n * surfaces a recovery-worthy state (e.g. terminal / drift), the caller\n * composes `closeLease()` separately — agent-core's simple-form\n * `onFailure({ reason })` does not carry recovery options, so the\n * orchestration of \"report → decide → close\" lives at the host surface.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n} from '@manifest-network/manifest-mcp-core';\nimport { normalizeItem } from './internals/lease-items.js';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport type {\n LeaseStateName,\n TroubleshootArgs,\n TroubleshootCallbacks,\n TroubleshootOptions,\n TroubleshootReport,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Generate a diagnostic markdown report for `args.leaseUuid`.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.\n * @throws `ManifestMCPError(QUERY_FAILED)` in two cases, both with\n * `onFailure({ reason })` invoked first:\n * - the chain query rejects with a plain `Error` (RPC / transport\n * / decoding failure); or\n * - the chain query succeeds but returns `{ lease: null }` /\n * `undefined` (lease UUID not on-chain — the `billing.v1.lease`\n * no-such-lease response shape).\n * Structured `ManifestMCPError`s raised by the chain client\n * (e.g. `INVALID_CONFIG` from missing rpc/rest url config,\n * `RPC_CONNECTION_FAILED` from connect failure) are re-thrown\n * as-is with their original code, with `onFailure` invoked first.\n */\nexport async function troubleshootDeployment(\n args: TroubleshootArgs,\n callbacks: TroubleshootCallbacks,\n opts: TroubleshootOptions,\n): Promise<TroubleshootReport> {\n validateArgs(args);\n\n let leasePayload: unknown;\n try {\n // Pull `getQueryClient()` INSIDE the try (Copilot review PR #60,\n // comment 3276719462). `getQueryClient()` can throw\n // `INVALID_CONFIG` (neither rpcUrl nor restUrl set) or\n // `RPC_CONNECTION_FAILED` (connect failure). Catching here routes\n // those init-time failures through the same `onFailure` +\n // QUERY_FAILED / structured-passthrough normalization the chain-\n // query failure mode already gets — three modes, one disambiguation.\n const queryClient = await opts.clientManager.getQueryClient();\n const result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n leasePayload = result.lease;\n } catch (err) {\n // Preserve structured `ManifestMCPError`s from the chain client\n // (Copilot review PR #60, comment 3276172289). Wrapping every\n // failure as `QUERY_FAILED` erases upstream error codes — a real\n // `INVALID_CONFIG` from the chain layer should surface to callers\n // with that code, not be collapsed to a less-specific category.\n // Mirrors the disambiguation `manage-domain.ts:lookupDomain`\n // adopted in commit aaa5cc5. Note: chain-NotFound for\n // `billing.v1.lease({ leaseUuid })` returns `{ lease: null }`\n // (handled below), so errors landing here are genuinely transport\n // or structured failures.\n const reason = `Failed to query lease ${args.leaseUuid}: ${err instanceof Error ? err.message : String(err)}`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n\n if (leasePayload === null || leasePayload === undefined) {\n const reason = `Lease ${args.leaseUuid} not found on chain.`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n\n const markdown = renderReport(args.leaseUuid, leasePayload);\n const report: TroubleshootReport = { markdown };\n callbacks.onComplete?.(report);\n return report;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: TroubleshootArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `troubleshootDeployment: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\ninterface LeaseShape {\n uuid?: unknown;\n state?: unknown;\n providerUuid?: unknown;\n provider_uuid?: unknown;\n createdAt?: unknown;\n created_at?: unknown;\n closedAt?: unknown;\n closed_at?: unknown;\n items?: unknown;\n}\n\nfunction renderReport(leaseUuid: string, lease: unknown): string {\n const l = (lease ?? {}) as LeaseShape;\n const rawState = l.state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n const stateLabel = stateName ?? `UNKNOWN(${String(rawState)})`;\n const providerUuid =\n readString(l.providerUuid) || readString(l.provider_uuid) || '(unknown)';\n const createdAt = readTimestamp(l.createdAt) ?? readTimestamp(l.created_at);\n const closedAt = readTimestamp(l.closedAt) ?? readTimestamp(l.closed_at);\n\n const rawItems = Array.isArray(l.items) ? l.items : [];\n const items = rawItems.map(normalizeItem);\n\n const lines: string[] = [];\n lines.push(`# Lease diagnostic — ${leaseUuid}`);\n lines.push('');\n lines.push('## Chain state');\n lines.push('');\n lines.push(`- **State:** ${stateLabel}`);\n lines.push(`- **Provider:** ${providerUuid}`);\n if (createdAt) lines.push(`- **Created:** ${createdAt}`);\n if (closedAt) lines.push(`- **Closed:** ${closedAt}`);\n lines.push('');\n\n lines.push('## Items');\n lines.push('');\n if (items.length === 0) {\n lines.push('_No items found on this lease._');\n } else {\n for (const item of items) {\n const svc = item.serviceName.length > 0 ? item.serviceName : '(default)';\n const dom =\n item.customDomain.length > 0 ? item.customDomain : '(no custom domain)';\n lines.push(`- **${svc}** → ${dom}`);\n }\n }\n lines.push('');\n\n lines.push('## Guidance');\n lines.push('');\n for (const tip of guidanceFor(stateName)) {\n lines.push(`- ${tip}`);\n }\n\n return lines.join('\\n');\n}\n\nfunction guidanceFor(state: LeaseStateName | undefined): string[] {\n if (state === undefined) {\n return [\n 'Lease state could not be decoded. Re-query in a moment, or check the chain client logs for transport errors.',\n ];\n }\n if (isTerminal(state)) {\n return [\n `Lease is in terminal state \\`${state}\\`. No further provider activity expected.`,\n 'To redeploy, create a new lease via `deployApp`.',\n ];\n }\n switch (state) {\n case 'LEASE_STATE_PENDING':\n return [\n 'Lease is awaiting provider acknowledgement.',\n 'If pending persists for more than a few minutes, the provider may be offline; consider closing and redeploying.',\n ];\n case 'LEASE_STATE_ACTIVE':\n return [\n 'Lease is active on the provider. App-level status / logs require a provider HTTP call with an ADR-036 auth token (out of scope for this chain-only diagnostic).',\n ];\n default:\n return [\n `Lease state is \\`${state}\\`. Review the chain proto for the expected next transition.`,\n ];\n }\n}\n\nfunction readString(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction readTimestamp(value: unknown): string | undefined {\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string' && value.length > 0) return value;\n return undefined;\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -73,6 +73,15 @@ interface DeployAppOptions extends AgentCoreRuntime {
|
|
|
73
73
|
* "save-fail still emits success" contract.
|
|
74
74
|
*/
|
|
75
75
|
dataDir?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional override for the `wait_for_app_ready` polling timeout (in
|
|
78
|
+
* milliseconds). Defaults to 480_000 (8 minutes) — generous for
|
|
79
|
+
* first-time provisioning (image pulls, k8s scheduling); fred's own
|
|
80
|
+
* default of 120_000 is too aggressive for cold-start providers.
|
|
81
|
+
* Set lower in tests to exercise timeout paths without slowing the
|
|
82
|
+
* suite.
|
|
83
|
+
*/
|
|
84
|
+
waitForReadyTimeoutMs?: number;
|
|
76
85
|
}
|
|
77
86
|
/**
|
|
78
87
|
* Per-call options for `manageDomain` (PR 4). Doesn't broadcast through
|
|
@@ -186,6 +195,12 @@ type ProgressEvent = {
|
|
|
186
195
|
} | {
|
|
187
196
|
kind: 'deploy_response_classified';
|
|
188
197
|
outcome: 'active' | 'needs_wait' | 'failed';
|
|
198
|
+
} | {
|
|
199
|
+
kind: 'polling_for_readiness';
|
|
200
|
+
leaseUuid: string;
|
|
201
|
+
attempt: number;
|
|
202
|
+
elapsedMs: number;
|
|
203
|
+
state?: LeaseStateName;
|
|
189
204
|
} | {
|
|
190
205
|
kind: 'app_ready_confirmed';
|
|
191
206
|
leaseUuid: string;
|
|
@@ -196,6 +211,10 @@ type ProgressEvent = {
|
|
|
196
211
|
} | {
|
|
197
212
|
kind: 'success_rendered';
|
|
198
213
|
result: DeployResult;
|
|
214
|
+
} | {
|
|
215
|
+
kind: 'partial_success_prompt_rendered';
|
|
216
|
+
prompt: string;
|
|
217
|
+
leaseUuid: string;
|
|
199
218
|
};
|
|
200
219
|
type FailureEnvelope = {
|
|
201
220
|
outcome: 'partially_succeeded';
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAkBiB,IAAA;EACf,KAAA;EACA,MAAA;AAAA;AAAA,UAGe,WAAA;EACf,KAAA,EAAO,IAAA;EACP,GAAA;AAAA;AAAA,UAYe,WAAA;EACf,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,QAAA;EAjBZ;EAmBH,MAAA,CAAO,KAAA,WAAgB,WAAA;EAPR;EASf,GAAA;AAAA;;;AAJF;;UAoCiB,gBAAA;EAlCmB;EAoClC,aAAA,EAAe,mBAAA;EApCR;EAsCP,OAAA,UAAiB,UAAA,CAAW,KAAA;AAAA;;;AAJ9B;;;UAYiB,gBAAA,SAAyB,gBAAA;EAVxC;;;;;;;AAUF;;;;;;;;;;;;;;;EAuBE,cAAA,EAAgB,cAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manifest-network/manifest-agent-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "TypeScript orchestration surface for Manifest agent flows (deploy / manage-domain / troubleshoot / close-lease). Type contract only — see ENG-127.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,10 +43,8 @@
|
|
|
43
43
|
"dist"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@manifest-network/manifest-mcp-core": "^0.
|
|
47
|
-
"@manifest-network/manifest-mcp-fred": "^0.
|
|
48
|
-
"ipaddr.js": "2.4.0",
|
|
49
|
-
"undici": "8.2.0"
|
|
46
|
+
"@manifest-network/manifest-mcp-core": "^0.11.0",
|
|
47
|
+
"@manifest-network/manifest-mcp-fred": "^0.11.0"
|
|
50
48
|
},
|
|
51
49
|
"devDependencies": {
|
|
52
50
|
"@types/node": "22.15.29",
|