@manifest-network/manifest-agent-core 0.19.0 → 0.20.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/dist/_virtual/_@oxc-project_runtime@0.140.0/helpers/esm/defineProperty.js +12 -0
- package/dist/_virtual/_@oxc-project_runtime@0.140.0/helpers/esm/toPrimitive.js +14 -0
- package/dist/_virtual/_@oxc-project_runtime@0.140.0/helpers/esm/toPropertyKey.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.140.0/helpers/esm/typeof.js +11 -0
- package/dist/close-lease.d.ts +0 -1
- package/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +0 -1
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +4 -2
- package/dist/deploy-app.js.map +1 -1
- package/dist/deploy-app.test-d.d.ts +1 -1
- package/dist/deploy-app.test-d.js +1 -1
- package/dist/deploy-app.test-d.js.map +1 -1
- package/dist/internals/cancellation.d.ts +0 -1
- package/dist/internals/cancellation.d.ts.map +1 -1
- package/dist/internals/cancellation.js.map +1 -1
- package/dist/internals/classify-deploy-error.d.ts.map +1 -1
- package/dist/internals/classify-deploy-response.d.ts.map +1 -1
- package/dist/internals/classify-deploy-response.js.map +1 -1
- package/dist/internals/connection.d.ts.map +1 -1
- package/dist/internals/connection.js.map +1 -1
- package/dist/internals/evaluate-readiness-from-fred.d.ts +1 -1
- package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -1
- package/dist/internals/evaluate-readiness-from-fred.js.map +1 -1
- package/dist/internals/evaluate-readiness.d.ts +7 -3
- package/dist/internals/evaluate-readiness.d.ts.map +1 -1
- package/dist/internals/evaluate-readiness.js.map +1 -1
- package/dist/internals/format-success.d.ts +0 -1
- package/dist/internals/format-success.d.ts.map +1 -1
- package/dist/internals/format-success.js.map +1 -1
- package/dist/internals/humanize-denom.d.ts +0 -1
- package/dist/internals/humanize-denom.d.ts.map +1 -1
- package/dist/internals/humanize-denom.js.map +1 -1
- package/dist/internals/inspect-image.d.ts.map +1 -1
- package/dist/internals/inspect-image.js +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/lease-items.d.ts.map +1 -1
- package/dist/internals/lease-state.d.ts +0 -1
- package/dist/internals/lease-state.d.ts.map +1 -1
- package/dist/internals/lease-state.js +1 -1
- package/dist/internals/lease-state.js.map +1 -1
- package/dist/internals/render-deployment-plan.d.ts +1 -0
- package/dist/internals/render-deployment-plan.d.ts.map +1 -1
- package/dist/internals/render-deployment-plan.js +4 -3
- package/dist/internals/render-deployment-plan.js.map +1 -1
- package/dist/internals/render-intent-recap.d.ts +0 -1
- package/dist/internals/render-intent-recap.d.ts.map +1 -1
- package/dist/internals/render-intent-recap.js.map +1 -1
- package/dist/internals/render-partial-success-prompt.d.ts +0 -1
- package/dist/internals/render-partial-success-prompt.d.ts.map +1 -1
- package/dist/internals/save-manifest.d.ts.map +1 -1
- package/dist/internals/save-manifest.js.map +1 -1
- package/dist/internals/secret-denylist.d.ts.map +1 -1
- package/dist/internals/secret-denylist.js +1 -1
- package/dist/internals/secret-denylist.js.map +1 -1
- package/dist/internals/spec-normalize.d.ts +0 -1
- package/dist/internals/spec-normalize.d.ts.map +1 -1
- package/dist/internals/spec-normalize.js.map +1 -1
- package/dist/internals/verify-domain-state.d.ts.map +1 -1
- package/dist/internals/verify-domain-state.js.map +1 -1
- package/dist/internals/verify-recover.d.ts +0 -1
- package/dist/internals/verify-recover.d.ts.map +1 -1
- package/dist/manage-domain.d.ts +0 -1
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js.map +1 -1
- package/dist/node_modules/@vitest/pretty-format/dist/index.js +3 -3
- package/dist/node_modules/@vitest/pretty-format/dist/index.js.map +1 -1
- package/dist/node_modules/@vitest/runner/dist/chunk-artifact.js +20 -15
- package/dist/node_modules/@vitest/runner/dist/chunk-artifact.js.map +1 -1
- package/dist/node_modules/@vitest/runner/dist/index.js +2 -1
- package/dist/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +1 -1
- package/dist/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js.map +1 -1
- package/dist/node_modules/@vitest/utils/dist/display.js +1 -1
- package/dist/node_modules/@vitest/utils/dist/display.js.map +1 -1
- package/dist/node_modules/@vitest/utils/dist/helpers.js +1 -1
- package/dist/node_modules/@vitest/utils/dist/helpers.js.map +1 -1
- package/dist/node_modules/@vitest/utils/dist/source-map.js +4 -4
- package/dist/node_modules/@vitest/utils/dist/source-map.js.map +1 -1
- package/dist/node_modules/tinyrainbow/dist/index.js +1 -1
- package/dist/node_modules/vite/dist/node/module-runner.js +8 -3
- package/dist/node_modules/vite/dist/node/module-runner.js.map +1 -1
- package/dist/node_modules/vitest/dist/index.js +2 -2
- package/dist/troubleshoot.d.ts +0 -1
- package/dist/troubleshoot.d.ts.map +1 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EMPTY_DENOM_MAP, humanizeBalances, humanizeCoin } from "./humanize-denom.js";
|
|
2
|
+
import { sanitizeForDisplay } from "@manifest-network/manifest-mcp-core";
|
|
2
3
|
//#region src/internals/render-deployment-plan.ts
|
|
3
4
|
/**
|
|
4
5
|
* Render the canonical `DeploymentPlan` block for `deployApp`'s
|
|
@@ -81,7 +82,7 @@ function formatFeeLine(humanFee, gas) {
|
|
|
81
82
|
function formatSkuPrice(plan, denomMap) {
|
|
82
83
|
const sku = plan.readiness.sku;
|
|
83
84
|
if (sku === null) return "(unknown — SKU has no listed price)";
|
|
84
|
-
return `${humanizeCoin(sku.price.amount, sku.price.denom, denomMap)} / hour`;
|
|
85
|
+
return `${sanitizeForDisplay(humanizeCoin(sku.price.amount, sku.price.denom, denomMap))} / hour`;
|
|
85
86
|
}
|
|
86
87
|
function formatWallet(plan, denomMap) {
|
|
87
88
|
return humanizeBalances(plan.readiness.walletBalances, denomMap);
|
|
@@ -104,9 +105,9 @@ function renderDeploymentPlan(input) {
|
|
|
104
105
|
const lines = [
|
|
105
106
|
"DeploymentPlan",
|
|
106
107
|
` Image: ${input.image}`,
|
|
107
|
-
` Size: ${input.size}`
|
|
108
|
+
` Size: ${sanitizeForDisplay(input.size, 64, "(unnamed SKU)")}`
|
|
108
109
|
];
|
|
109
|
-
if (hasProvider) lines.push(` Provider: ${input.providerUuid}`);
|
|
110
|
+
if (hasProvider) lines.push(` Provider: ${sanitizeForDisplay(input.providerUuid, 64, "(unknown provider)")}`);
|
|
110
111
|
lines.push(` Manifest: ${manifestLine}`, ` meta_hash: ${input.metaHash}`);
|
|
111
112
|
if (hasDomain) {
|
|
112
113
|
const target = typeof input.customDomainService === "string" && input.customDomainService.length > 0 ? `-> service ${input.customDomainService}` : "-> single-service lease";
|
|
@@ -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 (ENG-258):** rendered immediately after `Size:` when a\n * pinned `providerUuid` is supplied (resolved by the SKU disambiguator so the\n * user sees which provider they are deploying to). Omitted when the field is\n * absent or empty (single-provider SKU — chain selects internally).\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 * Pinned provider UUID resolved by the SKU disambiguator (ENG-258).\n * When non-empty, a `Provider:` line is rendered right after `Size:`\n * so the user sees which provider they are deploying to.\n */\n providerUuid?: 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 hasProvider =\n typeof input.providerUuid === 'string' && input.providerUuid.length > 0;\n\n const lines: string[] = [\n 'DeploymentPlan',\n ` Image: ${input.image}`,\n ` Size: ${input.size}`,\n ];\n\n if (hasProvider) {\n lines.push(` Provider: ${input.providerUuid}`);\n }\n\n lines.push(\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 { sanitizeForDisplay } from '@manifest-network/manifest-mcp-core';\nimport 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 (ENG-258):** rendered immediately after `Size:` when a\n * pinned `providerUuid` is supplied (resolved by the SKU disambiguator so the\n * user sees which provider they are deploying to). Omitted when the field is\n * absent or empty (single-provider SKU — chain selects internally).\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 // SKU price amount + denom are provider-controlled on-chain strings, and\n // humanizeCoin renders an unknown denom (and a non-numeric amount) verbatim —\n // sanitize the composed value so it cannot forge a plan line (ENG-555).\n return `${sanitizeForDisplay(\n humanizeCoin(sku.price.amount, sku.price.denom, denomMap),\n )} / 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 * Pinned provider UUID resolved by the SKU disambiguator (ENG-258).\n * When non-empty, a `Provider:` line is rendered right after `Size:`\n * so the user sees which provider they are deploying to.\n */\n providerUuid?: 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 hasProvider =\n typeof input.providerUuid === 'string' && input.providerUuid.length > 0;\n\n const lines: string[] = [\n 'DeploymentPlan',\n ` Image: ${input.image}`,\n // SKU tier name is a provider-controlled on-chain string — sanitize so an\n // embedded newline/control char cannot forge a plan line (ENG-555).\n ` Size: ${sanitizeForDisplay(input.size, 64, '(unnamed SKU)')}`,\n ];\n\n if (hasProvider) {\n // `ProviderUuid` is UUID-shaped by design (`parseProviderUuid` validates via\n // assertUuid), but it reaches THIS display path via the UNCHECKED\n // `asProviderUuid` trust-cast (sku-resolution / deploy-app), so this path\n // cannot assume it is well-formed. Strip control/format bytes defensively\n // rather than allowlist a UUID grammar this path has not re-validated — a\n // legit-but-unvalidated id must still render (see the pinned-provider test).\n lines.push(\n ` Provider: ${sanitizeForDisplay(\n input.providerUuid,\n 64,\n '(unknown provider)',\n )}`,\n );\n }\n\n lines.push(\n ` Manifest: ${manifestLine}`,\n ` meta_hash: ${input.metaHash}`,\n );\n\n if (hasDomain) {\n const target =\n typeof input.customDomainService === 'string' &&\n input.customDomainService.length > 0\n ? `-> service ${input.customDomainService}`\n : '-> single-service lease';\n lines.push(` Custom domain: ${input.customDomain} ${target}`);\n }\n\n lines.push(\n ` SKU price: ${formatSkuPrice(input.plan, denomMap)}`,\n );\n\n if (hasDomain) {\n // Two-tx layout: labeled lines + Total fee. Honors approach-3\n // `notEstimated` sentinel for set-domain pre-broadcast estimation\n // fallback (no representative lease).\n const setDomain = input.plan.fees.setDomain;\n let setDomainLine: string;\n // Capture the typed `FeeEstimate` reference (when the set-domain\n // fee is a real estimate, not the sentinel) so the total-line\n // BigInt sum can operate on `coins` directly via `sumFees`. The\n // prior code parsed humanized strings to float64 — see\n // `sumFees`'s docstring for the precision-loss rationale.\n let setDomainReal: FeeEstimate | null = null;\n if (setDomain === undefined) {\n setDomainLine =\n '(not estimated — agent skipped pre-broadcast simulation, policy violation)';\n } else if ('notEstimated' in setDomain) {\n setDomainLine = `(not estimated — ${setDomain.reason})`;\n } else {\n setDomainReal = setDomain;\n setDomainLine = formatFeeLine(\n humanizeFeeAmount(setDomain, denomMap),\n setDomain.gas,\n );\n }\n\n lines.push(` Tx fee (create-lease): ${createFeeLine}`);\n lines.push(` Tx fee (set-domain): ${setDomainLine}`);\n\n // Total only when both fees are real numbers. Sentinel set-domain\n // fees fall through to the placeholder.\n const totalLine =\n setDomainReal !== null\n ? sumFees(createFee, setDomainReal, denomMap)\n : '(partial — see fee lines above)';\n lines.push(` Total fee: ${totalLine}`);\n } else {\n lines.push(` Tx fee: ${createFeeLine}`);\n }\n\n lines.push(\n ` Wallet: ${formatWallet(input.plan, denomMap)}`,\n );\n lines.push(\n ` Credits: ${formatCredits(input.plan, denomMap)}`,\n );\n\n return { text: lines.join('\\n') };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-intent-recap.d.ts","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-intent-recap.d.ts","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAoCiB;;EAEf,MAAM;;EAEN;;iBAgBc,kBAAkB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-intent-recap.js","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"sourcesContent":["import type { AppDeploySpec, ServiceConfig } from '../types.js';\nimport { isStackSpec, normalizeServices } from './spec-normalize.js';\n\n/**\n * Render the structural portion of the intent-recap block shown to the user\n * before any chain round-trips in the deploy-app orchestrator.\n *\n * The 4 deterministic items the recap covers:\n *\n * 1. Deployment surface (service count + per-service `name — image`)\n * 2. Connectivity (per-port ingress posture)\n * 3. Redacted sensitive-key inventory (env / label keys only; never values)\n * 4. Custom-domain + dual-tx clarifier + mainnet warning (when applicable)\n *\n * The 2 LLM-judgment items (\"what you provided vs auto-detected\", \"heads-up:\n * obvious gaps\") stay in prose — the orchestrator appends them between the\n * deterministic block and the `AskUserQuestion` prompt.\n *\n * **Sensitive-value posture:** env values and label values are NEVER\n * surfaced; only keys appear. Mirrors `summarizeSpec`'s contract. FQDNs are\n * not secrets so `customDomain` is surfaced verbatim.\n *\n * **Port-shape handling:** two runtime shapes for ports:\n * - Single-service: `port: number` → renders one ingress=true entry.\n * - Stack service: `ServiceConfig.ports: Record<portKey, {}>` (canonical\n * map, e.g. `{ '80/tcp': {} }`) → one entry per port-key (ingress\n * default false). The recap renders the map KEY string (`'80/tcp'`),\n * not a bare number.\n *\n * `extractPorts` also handles the historical `number[]` Record-or-array\n * shapes at runtime (matching `summarizeSpec`'s defensive widening) so\n * callers passing unknown-typed input from JSON.parse don't silently drop\n * ports.\n */\n\n/** Render output is a multi-paragraph plain-text block, ready to print verbatim. */\nexport interface RenderIntentRecapInput {\n /** The structured deploy spec (canonical `AppDeploySpec` shape). */\n spec: AppDeploySpec;\n /** Active chain — drives the mainnet permanence warning. */\n activeChain: 'testnet' | 'mainnet';\n}\n\ninterface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** Image string. Falls back to `(unknown image)` when missing. */\n image: string;\n /** Per-port ingress posture, in declaration order. */\n ports: { port: string; ingress: boolean }[];\n /** Sorted env keys (values redacted). */\n envKeys: string[];\n /** Sorted label keys (values redacted). */\n labelKeys: string[];\n}\n\nexport function renderIntentRecap(input: RenderIntentRecapInput): string {\n if (input.activeChain !== 'testnet' && input.activeChain !== 'mainnet') {\n throw new TypeError(\n `renderIntentRecap: activeChain must be \"testnet\" or \"mainnet\"; got \"${String(input.activeChain)}\"`,\n );\n }\n\n const services = projectServices(input.spec);\n\n const blocks: string[] = [\n renderServiceList(services, input.activeChain),\n renderConnectivity(services),\n renderRedactedInventory(services),\n ];\n const domainBlock = renderCustomDomain(input.spec, input.activeChain);\n if (domainBlock !== null) {\n blocks.push(domainBlock);\n }\n\n return blocks.join('\\n\\n');\n}\n\nfunction projectServices(spec: AppDeploySpec): NormalizedService[] {\n return normalizeServices(spec).map(({ name, raw }): NormalizedService => {\n const rawRecord = raw as unknown as Record<string, unknown>;\n const image =\n typeof rawRecord.image === 'string' && rawRecord.image.length > 0\n ? rawRecord.image\n : '(unknown image)';\n const ports =\n name === null\n ? extractPortsLegacy((raw as AppDeploySpec).port)\n : extractPorts((raw as ServiceConfig).ports);\n return {\n name,\n image,\n ports,\n envKeys: extractKeys(rawRecord.env),\n labelKeys: extractKeys(rawRecord.labels),\n };\n });\n}\n\n/**\n * Services-map shape: `{ \"80/tcp\": { ingress?: boolean }, \"9090\": { ... } }`\n * (`ServiceConfig.ports` is a `Record<string, …>` port map). Ingress flag may\n * be absent — default `false` matches Fred's cluster-private default. Also\n * tolerates a legacy `number[]` shape defensively by treating each entry as\n * ingress=false (services-map default).\n */\nfunction extractPorts(ports: unknown): { port: string; ingress: boolean }[] {\n if (Array.isArray(ports)) {\n return ports\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: false }));\n }\n if (ports !== null && typeof ports === 'object') {\n return Object.entries(ports as Record<string, unknown>).map(\n ([port, cfg]) => ({\n port,\n ingress: !!(\n cfg !== null &&\n typeof cfg === 'object' &&\n (cfg as { ingress?: unknown }).ingress\n ),\n }),\n );\n }\n return [];\n}\n\n/**\n * Legacy single-service shape: bare `port: number`. Fred treats this as\n * ingress=true by default — that's the whole point of the simplified\n * shape.\n *\n * Also handles the `number[]` form (the frozen-contract array form):\n * returns one `{ port, ingress: true }` entry per array element, each\n * with `ingress: true` matching the single-service convention. Returns\n * `[]` for any other value (undefined, non-number scalar, non-array\n * object).\n *\n * M2 fix: prior JSDoc incorrectly stated \"Returns `[]` for any other\n * value (including `number[]`...)\" — empirically wrong per the\n * `Array.isArray(port)` branch below.\n */\nfunction extractPortsLegacy(\n port: number | number[] | undefined,\n): { port: string; ingress: boolean }[] {\n if (typeof port === 'number') {\n return [{ port: String(port), ingress: true }];\n }\n if (Array.isArray(port)) {\n return port\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: true }));\n }\n return [];\n}\n\nfunction extractKeys(obj: unknown): string[] {\n if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return [];\n return Object.keys(obj as Record<string, unknown>).sort();\n}\n\nfunction renderServiceList(\n services: NormalizedService[],\n activeChain: 'testnet' | 'mainnet',\n): string {\n const count = services.length;\n const noun = count === 1 ? 'service' : 'services';\n const lines = [`Deploying ${count} ${noun} on ${activeChain}:`];\n for (const svc of services) {\n const prefix = svc.name === null ? '' : `${svc.name} — `;\n lines.push(` - ${prefix}${svc.image}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderConnectivity(services: NormalizedService[]): string {\n const lines = ['Connectivity:'];\n let total = 0;\n for (const svc of services) {\n if (svc.ports.length === 0) continue;\n for (const p of svc.ports) {\n total += 1;\n const prefix =\n svc.name === null ? `port ${p.port}` : `${svc.name} port ${p.port}`;\n const reach = p.ingress\n ? \"publicly reachable via the provider's HTTPS subdomain\"\n : 'internal only (cluster-private)';\n lines.push(` - ${prefix}: ${reach}`);\n }\n }\n if (total === 0) {\n lines.push(\n ' (no ports declared — the deployment will not expose any network surface)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderRedactedInventory(services: NormalizedService[]): string {\n // Always render the section header even if everything is empty — the user\n // should know we'd have shown values if there were any. This is also\n // documentation of the redaction discipline.\n const lines = [\n 'Sensitive values are redacted in this recap (keys only, never values):',\n ];\n let anything = false;\n for (const svc of services) {\n const prefix = svc.name === null ? 'this service' : svc.name;\n const parts: string[] = [];\n if (svc.envKeys.length > 0) {\n anything = true;\n parts.push(`env keys [${svc.envKeys.join(', ')}]`);\n }\n if (svc.labelKeys.length > 0) {\n anything = true;\n parts.push(`label keys [${svc.labelKeys.join(', ')}]`);\n }\n if (parts.length === 0) {\n lines.push(` - ${prefix}: no env or labels supplied`);\n } else {\n lines.push(` - ${prefix}: ${parts.join('; ')}`);\n }\n }\n if (!anything) {\n lines.push(\n ' - (no env or labels supplied across any service — nothing to redact)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderCustomDomain(\n spec: AppDeploySpec,\n activeChain: 'testnet' | 'mainnet',\n): string | null {\n const customDomain = spec.customDomain;\n if (typeof customDomain !== 'string' || customDomain.length === 0) {\n return null;\n }\n // `serviceName` only disambiguates a stack service; a single-service\n // spec's single service implicitly receives the domain.\n const serviceName = isStackSpec(spec) ? spec.serviceName : undefined;\n const target =\n typeof serviceName === 'string' && serviceName.length > 0\n ? `service ${serviceName}`\n : 'single-service lease';\n const lines = [`Custom domain: ${customDomain} → ${target}`];\n lines.push('');\n lines.push(\n 'Note: when a custom domain is set, deploy_app broadcasts TWO billing\\n' +\n 'transactions atomically: create-lease AND set-item-custom-domain. The\\n' +\n 'single permission prompt that fires later covers BOTH; this textual\\n' +\n 'recap is your per-tx review.',\n );\n if (activeChain === 'mainnet') {\n lines.push('');\n lines.push(\n `Mainnet warning: this transaction permanently associates ${customDomain}\\n` +\n 'with this lease on-chain until you --clear it via\\n' +\n '/manifest-agent:manage-domain or close the lease. FQDN squatting is\\n' +\n 'irreversible.',\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"render-intent-recap.js","names":[],"sources":["../../src/internals/render-intent-recap.ts"],"sourcesContent":["import type { AppDeploySpec, ServiceConfig } from '../types.js';\nimport { isStackSpec, normalizeServices } from './spec-normalize.js';\n\n/**\n * Render the structural portion of the intent-recap block shown to the user\n * before any chain round-trips in the deploy-app orchestrator.\n *\n * The 4 deterministic items the recap covers:\n *\n * 1. Deployment surface (service count + per-service `name — image`)\n * 2. Connectivity (per-port ingress posture)\n * 3. Redacted sensitive-key inventory (env / label keys only; never values)\n * 4. Custom-domain + dual-tx clarifier + mainnet warning (when applicable)\n *\n * The 2 LLM-judgment items (\"what you provided vs auto-detected\", \"heads-up:\n * obvious gaps\") stay in prose — the orchestrator appends them between the\n * deterministic block and the `AskUserQuestion` prompt.\n *\n * **Sensitive-value posture:** env values and label values are NEVER\n * surfaced; only keys appear. Mirrors `summarizeSpec`'s contract. FQDNs are\n * not secrets so `customDomain` is surfaced verbatim.\n *\n * **Port-shape handling:** two runtime shapes for ports:\n * - Single-service: `port: number` → renders one ingress=true entry.\n * - Stack service: `ServiceConfig.ports: Record<portKey, {}>` (canonical\n * map, e.g. `{ '80/tcp': {} }`) → one entry per port-key (ingress\n * default false). The recap renders the map KEY string (`'80/tcp'`),\n * not a bare number.\n *\n * `extractPorts` also handles the historical `number[]` Record-or-array\n * shapes at runtime (matching `summarizeSpec`'s defensive widening) so\n * callers passing unknown-typed input from JSON.parse don't silently drop\n * ports.\n */\n\n/** Render output is a multi-paragraph plain-text block, ready to print verbatim. */\nexport interface RenderIntentRecapInput {\n /** The structured deploy spec (canonical `AppDeploySpec` shape). */\n spec: AppDeploySpec;\n /** Active chain — drives the mainnet permanence warning. */\n activeChain: 'testnet' | 'mainnet';\n}\n\ninterface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** Image string. Falls back to `(unknown image)` when missing. */\n image: string;\n /** Per-port ingress posture, in declaration order. */\n ports: { port: string; ingress: boolean }[];\n /** Sorted env keys (values redacted). */\n envKeys: string[];\n /** Sorted label keys (values redacted). */\n labelKeys: string[];\n}\n\nexport function renderIntentRecap(input: RenderIntentRecapInput): string {\n if (input.activeChain !== 'testnet' && input.activeChain !== 'mainnet') {\n throw new TypeError(\n `renderIntentRecap: activeChain must be \"testnet\" or \"mainnet\"; got \"${String(input.activeChain)}\"`,\n );\n }\n\n const services = projectServices(input.spec);\n\n const blocks: string[] = [\n renderServiceList(services, input.activeChain),\n renderConnectivity(services),\n renderRedactedInventory(services),\n ];\n const domainBlock = renderCustomDomain(input.spec, input.activeChain);\n if (domainBlock !== null) {\n blocks.push(domainBlock);\n }\n\n return blocks.join('\\n\\n');\n}\n\nfunction projectServices(spec: AppDeploySpec): NormalizedService[] {\n return normalizeServices(spec).map(({ name, raw }): NormalizedService => {\n const rawRecord = raw as unknown as Record<string, unknown>;\n const image =\n typeof rawRecord.image === 'string' && rawRecord.image.length > 0\n ? rawRecord.image\n : '(unknown image)';\n const ports =\n name === null\n ? extractPortsLegacy((raw as AppDeploySpec).port)\n : extractPorts((raw as ServiceConfig).ports);\n return {\n name,\n image,\n ports,\n envKeys: extractKeys(rawRecord.env),\n labelKeys: extractKeys(rawRecord.labels),\n };\n });\n}\n\n/**\n * Services-map shape: `{ \"80/tcp\": { ingress?: boolean }, \"9090\": { ... } }`\n * (`ServiceConfig.ports` is a `Record<string, …>` port map). Ingress flag may\n * be absent — default `false` matches Fred's cluster-private default. Also\n * tolerates a legacy `number[]` shape defensively by treating each entry as\n * ingress=false (services-map default).\n */\nfunction extractPorts(ports: unknown): { port: string; ingress: boolean }[] {\n if (Array.isArray(ports)) {\n return ports\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: false }));\n }\n if (ports !== null && typeof ports === 'object') {\n return Object.entries(ports as Record<string, unknown>).map(\n ([port, cfg]) => ({\n port,\n ingress: !!(\n cfg !== null &&\n typeof cfg === 'object' &&\n (cfg as { ingress?: unknown }).ingress\n ),\n }),\n );\n }\n return [];\n}\n\n/**\n * Legacy single-service shape: bare `port: number`. Fred treats this as\n * ingress=true by default — that's the whole point of the simplified\n * shape.\n *\n * Also handles the `number[]` form (the frozen-contract array form):\n * returns one `{ port, ingress: true }` entry per array element, each\n * with `ingress: true` matching the single-service convention. Returns\n * `[]` for any other value (undefined, non-number scalar, non-array\n * object).\n *\n * M2 fix: prior JSDoc incorrectly stated \"Returns `[]` for any other\n * value (including `number[]`...)\" — empirically wrong per the\n * `Array.isArray(port)` branch below.\n */\nfunction extractPortsLegacy(\n port: number | number[] | undefined,\n): { port: string; ingress: boolean }[] {\n if (typeof port === 'number') {\n return [{ port: String(port), ingress: true }];\n }\n if (Array.isArray(port)) {\n return port\n .filter((p): p is number => typeof p === 'number')\n .map((p) => ({ port: String(p), ingress: true }));\n }\n return [];\n}\n\nfunction extractKeys(obj: unknown): string[] {\n if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return [];\n return Object.keys(obj as Record<string, unknown>).sort();\n}\n\nfunction renderServiceList(\n services: NormalizedService[],\n activeChain: 'testnet' | 'mainnet',\n): string {\n const count = services.length;\n const noun = count === 1 ? 'service' : 'services';\n const lines = [`Deploying ${count} ${noun} on ${activeChain}:`];\n for (const svc of services) {\n const prefix = svc.name === null ? '' : `${svc.name} — `;\n lines.push(` - ${prefix}${svc.image}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderConnectivity(services: NormalizedService[]): string {\n const lines = ['Connectivity:'];\n let total = 0;\n for (const svc of services) {\n if (svc.ports.length === 0) continue;\n for (const p of svc.ports) {\n total += 1;\n const prefix =\n svc.name === null ? `port ${p.port}` : `${svc.name} port ${p.port}`;\n const reach = p.ingress\n ? \"publicly reachable via the provider's HTTPS subdomain\"\n : 'internal only (cluster-private)';\n lines.push(` - ${prefix}: ${reach}`);\n }\n }\n if (total === 0) {\n lines.push(\n ' (no ports declared — the deployment will not expose any network surface)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderRedactedInventory(services: NormalizedService[]): string {\n // Always render the section header even if everything is empty — the user\n // should know we'd have shown values if there were any. This is also\n // documentation of the redaction discipline.\n const lines = [\n 'Sensitive values are redacted in this recap (keys only, never values):',\n ];\n let anything = false;\n for (const svc of services) {\n const prefix = svc.name === null ? 'this service' : svc.name;\n const parts: string[] = [];\n if (svc.envKeys.length > 0) {\n anything = true;\n parts.push(`env keys [${svc.envKeys.join(', ')}]`);\n }\n if (svc.labelKeys.length > 0) {\n anything = true;\n parts.push(`label keys [${svc.labelKeys.join(', ')}]`);\n }\n if (parts.length === 0) {\n lines.push(` - ${prefix}: no env or labels supplied`);\n } else {\n lines.push(` - ${prefix}: ${parts.join('; ')}`);\n }\n }\n if (!anything) {\n lines.push(\n ' - (no env or labels supplied across any service — nothing to redact)',\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderCustomDomain(\n spec: AppDeploySpec,\n activeChain: 'testnet' | 'mainnet',\n): string | null {\n const customDomain = spec.customDomain;\n if (typeof customDomain !== 'string' || customDomain.length === 0) {\n return null;\n }\n // `serviceName` only disambiguates a stack service; a single-service\n // spec's single service implicitly receives the domain.\n const serviceName = isStackSpec(spec) ? spec.serviceName : undefined;\n const target =\n typeof serviceName === 'string' && serviceName.length > 0\n ? `service ${serviceName}`\n : 'single-service lease';\n const lines = [`Custom domain: ${customDomain} → ${target}`];\n lines.push('');\n lines.push(\n 'Note: when a custom domain is set, deploy_app broadcasts TWO billing\\n' +\n 'transactions atomically: create-lease AND set-item-custom-domain. The\\n' +\n 'single permission prompt that fires later covers BOTH; this textual\\n' +\n 'recap is your per-tx review.',\n );\n if (activeChain === 'mainnet') {\n lines.push('');\n lines.push(\n `Mainnet warning: this transaction permanently associates ${customDomain}\\n` +\n 'with this lease on-chain until you --clear it via\\n' +\n '/manifest-agent:manage-domain or close the lease. FQDN squatting is\\n' +\n 'irreversible.',\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":";;AAwDA,SAAgB,kBAAkB,OAAuC;CACvE,IAAI,MAAM,gBAAgB,aAAa,MAAM,gBAAgB,WAC3D,MAAM,IAAI,UACR,uEAAuE,OAAO,MAAM,WAAW,EAAE,EACnG;CAGF,MAAM,WAAW,gBAAgB,MAAM,IAAI;CAE3C,MAAM,SAAmB;EACvB,kBAAkB,UAAU,MAAM,WAAW;EAC7C,mBAAmB,QAAQ;EAC3B,wBAAwB,QAAQ;CAClC;CACA,MAAM,cAAc,mBAAmB,MAAM,MAAM,MAAM,WAAW;CACpE,IAAI,gBAAgB,MAClB,OAAO,KAAK,WAAW;CAGzB,OAAO,OAAO,KAAK,MAAM;AAC3B;AAEA,SAAS,gBAAgB,MAA0C;CACjE,OAAO,kBAAkB,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,UAA6B;EACvE,MAAM,YAAY;EASlB,OAAO;GACL;GACA,OATA,OAAO,UAAU,UAAU,YAAY,UAAU,MAAM,SAAS,IAC5D,UAAU,QACV;GAQJ,OANA,SAAS,OACL,mBAAoB,IAAsB,IAAI,IAC9C,aAAc,IAAsB,KAAK;GAK7C,SAAS,YAAY,UAAU,GAAG;GAClC,WAAW,YAAY,UAAU,MAAM;EACzC;CACF,CAAC;AACH;;;;;;;;AASA,SAAS,aAAa,OAAsD;CAC1E,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MACJ,QAAQ,MAAmB,OAAO,MAAM,QAAQ,CAAC,CACjD,KAAK,OAAO;EAAE,MAAM,OAAO,CAAC;EAAG,SAAS;CAAM,EAAE;CAErD,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO,OAAO,QAAQ,KAAgC,CAAC,CAAC,KACrD,CAAC,MAAM,UAAU;EAChB;EACA,SAAS,CAAC,EACR,QAAQ,QACR,OAAO,QAAQ,YACd,IAA8B;CAEnC,EACF;CAEF,OAAO,CAAC;AACV;;;;;;;;;;;;;;;;AAiBA,SAAS,mBACP,MACsC;CACtC,IAAI,OAAO,SAAS,UAClB,OAAO,CAAC;EAAE,MAAM,OAAO,IAAI;EAAG,SAAS;CAAK,CAAC;CAE/C,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KACJ,QAAQ,MAAmB,OAAO,MAAM,QAAQ,CAAC,CACjD,KAAK,OAAO;EAAE,MAAM,OAAO,CAAC;EAAG,SAAS;CAAK,EAAE;CAEpD,OAAO,CAAC;AACV;AAEA,SAAS,YAAY,KAAwB;CAC3C,IAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC;CAC3E,OAAO,OAAO,KAAK,GAA8B,CAAC,CAAC,KAAK;AAC1D;AAEA,SAAS,kBACP,UACA,aACQ;CACR,MAAM,QAAQ,SAAS;CAEvB,MAAM,QAAQ,CAAC,aAAa,MAAM,GADrB,UAAU,IAAI,YAAY,WACG,MAAM,YAAY,EAAE;CAC9D,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,SAAS,IAAI,SAAS,OAAO,KAAK,GAAG,IAAI,KAAK;EACpD,MAAM,KAAK,OAAO,SAAS,IAAI,OAAO;CACxC;CACA,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,mBAAmB,UAAuC;CACjE,MAAM,QAAQ,CAAC,eAAe;CAC9B,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,UAAU;EAC1B,IAAI,IAAI,MAAM,WAAW,GAAG;EAC5B,KAAK,MAAM,KAAK,IAAI,OAAO;GACzB,SAAS;GACT,MAAM,SACJ,IAAI,SAAS,OAAO,QAAQ,EAAE,SAAS,GAAG,IAAI,KAAK,QAAQ,EAAE;GAC/D,MAAM,QAAQ,EAAE,UACZ,0DACA;GACJ,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACtC;CACF;CACA,IAAI,UAAU,GACZ,MAAM,KACJ,4EACF;CAEF,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,wBAAwB,UAAuC;CAItE,MAAM,QAAQ,CACZ,wEACF;CACA,IAAI,WAAW;CACf,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,SAAS,IAAI,SAAS,OAAO,iBAAiB,IAAI;EACxD,MAAM,QAAkB,CAAC;EACzB,IAAI,IAAI,QAAQ,SAAS,GAAG;GAC1B,WAAW;GACX,MAAM,KAAK,aAAa,IAAI,QAAQ,KAAK,IAAI,EAAE,EAAE;EACnD;EACA,IAAI,IAAI,UAAU,SAAS,GAAG;GAC5B,WAAW;GACX,MAAM,KAAK,eAAe,IAAI,UAAU,KAAK,IAAI,EAAE,EAAE;EACvD;EACA,IAAI,MAAM,WAAW,GACnB,MAAM,KAAK,OAAO,OAAO,4BAA4B;OAErD,MAAM,KAAK,OAAO,OAAO,IAAI,MAAM,KAAK,IAAI,GAAG;CAEnD;CACA,IAAI,CAAC,UACH,MAAM,KACJ,wEACF;CAEF,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,mBACP,MACA,aACe;CACf,MAAM,eAAe,KAAK;CAC1B,IAAI,OAAO,iBAAiB,YAAY,aAAa,WAAW,GAC9D,OAAO;CAIT,MAAM,cAAc,YAAY,IAAI,IAAI,KAAK,cAAc,KAAA;CAK3D,MAAM,QAAQ,CAAC,kBAAkB,aAAa,KAH5C,OAAO,gBAAgB,YAAY,YAAY,SAAS,IACpD,WAAW,gBACX,wBACqD;CAC3D,MAAM,KAAK,EAAE;CACb,MAAM,KACJ,gPAIF;CACA,IAAI,gBAAgB,WAAW;EAC7B,MAAM,KAAK,EAAE;EACb,MAAM,KACJ,4DAA4D,aAAa;;cAI3E;CACF;CACA,OAAO,MAAM,KAAK,IAAI;AACxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-partial-success-prompt.d.ts","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-partial-success-prompt.d.ts","names":[],"sources":["../../src/internals/render-partial-success-prompt.ts"],"mappings":";;UAmCiB;;EAEf;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf;;;;;EAKA,SAAS;;iBAGK,2BACd,OAAO,kCACN"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-manifest.d.ts","names":[],"sources":["../../src/internals/save-manifest.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"save-manifest.d.ts","names":[],"sources":["../../src/internals/save-manifest.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmDiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;;;;;;;;;EAeA;;EAEA;;;;;EAKA;;UAGe;;EAEf;;;cAIW,0BAA0B;WAC5B;EAUG,YAAA,MAAM,2BAA2B;;;;;;;;;;iBAgBzB,aACpB,OAAO,oBACN,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-manifest.js","names":[],"sources":["../../src/internals/save-manifest.ts"],"sourcesContent":["/**\n * Persist a deployed manifest as a schema-version-3 wrapper to disk.\n *\n * Target directory is a **function argument** (`dataDir: string`) rather\n * than an env-var read: per gate-1 verdict, agent-core's `platform: 'neutral'`\n * build target forbids env-var reads at this layer; callers are responsible\n * for resolving the manifests dir from their own env context and passing it in.\n *\n * **Wrapper shape (schema_version 3):**\n *\n * ```\n * {\n * schema_version: 3,\n * lease_uuid, deployed_at_iso, deployed_at_unix,\n * chain_id, image, size, meta_hash_hex,\n * format, // \"single\" or \"stack\"\n * manifest_json, // string — canonical Fred-rendered JSON\n * custom_domain?, // (v3) FQDN attached to the lease item\n * custom_domain_service_name? // (v3) stack-lease service holder\n * }\n * ```\n *\n * Schema-version compat: v2 wrappers remain readable by all downstream\n * helpers; missing v3 fields are tolerated as undefined.\n *\n * **Audit guarantee:** SHA-256 of the bytes about to be persisted (after\n * normalizing the heredoc-/Write-added trailing newline) MUST equal\n * `metaHash`. Catches paste errors, accidental spec-vs-manifest_json\n * swaps, and transit corruption. Mismatch throws a typed\n * `SaveManifestError` (`code: 'sha256_mismatch'`).\n *\n * **Filesystem layout:** `<dataDir>/manifests/<lease_uuid>.json` with\n * mode 0600; parent `<dataDir>` and `<dataDir>/manifests` ensured at\n * mode 0700 (chmod-tightens an existing parent that was previously\n * looser).\n *\n * **Dynamic node-import discipline** (mirrors `guarded-fetch.ts`): the\n * `node:fs` / `node:path` / `node:crypto` imports are deferred to call\n * time so module load doesn't violate the `platform: 'neutral'` build\n * target. A `typeof process` check throws a clear \"Node-only API\" error\n * if invoked outside a Node-like runtime.\n */\n\n/** SHA-256 hex digest — 64 lowercase hex chars. */\nconst META_HASH_RE = /^[0-9a-f]{64}$/i;\n\n/** RFC 4122 UUID — 36 chars. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Input contract for `saveManifest`. All `*Hex` / UUID fields are validated. */\nexport interface SaveManifestInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Canonical primary image reference (for the wrapper's `image` field). */\n image: string;\n /** SKU name (e.g. `docker-micro`) for the wrapper's `size` field. */\n size: string;\n /** SHA-256 hex of the canonical manifest JSON, from `build_manifest_preview`. */\n metaHash: string;\n /** Chain ID — e.g. `manifest-ledger-testnet-1`. */\n chainId: string;\n /**\n * Canonical Fred-rendered manifest JSON (as a STRING, not a parsed\n * object). The SHA-256 of these bytes (after trimming trailing\n * whitespace) MUST equal `metaHash`.\n */\n manifestJson: string;\n /**\n * Target data directory — the function writes\n * `<dataDir>/manifests/<lease_uuid>.json`. Per gate-1 verdict,\n * supplied by the caller (no env-var read).\n *\n * **MUST be a dedicated manifest-storage directory.** This function\n * `chmod`-tightens any pre-existing `dataDir` (and its `manifests/`\n * subdirectory) to mode `0o700`. Do NOT pass shared parents like\n * `$HOME`, `~/.config`, or a generic data root — doing so would tighten\n * those directories' permissions and potentially break other processes\n * that depend on them. Callers must resolve this to a dedicated\n * subdirectory (e.g. `$XDG_DATA_HOME/manifest-agent/`); the `manifests/`\n * subdir is created inside automatically.\n */\n dataDir: string;\n /** Optional custom-domain FQDN attached to the lease item. */\n customDomain?: string;\n /**\n * Optional stack-lease service name that holds the custom domain.\n * Meaningless without `customDomain`; throws if supplied alone.\n */\n customDomainServiceName?: string;\n}\n\nexport interface SaveManifestResult {\n /** Absolute path to the persisted wrapper. */\n manifestPath: string;\n}\n\n/** Typed error surface for the I/O + validation failure paths. */\nexport class SaveManifestError extends Error {\n readonly code:\n | 'sha256_mismatch'\n | 'manifest_not_object'\n | 'invalid_uuid'\n | 'invalid_meta_hash'\n | 'invalid_data_dir'\n | 'service_name_without_domain'\n | 'manifest_parse_failed'\n | 'platform_unsupported';\n\n constructor(code: SaveManifestError['code'], message: string) {\n super(message);\n this.name = 'SaveManifestError';\n this.code = code;\n Object.setPrototypeOf(this, SaveManifestError.prototype);\n }\n}\n\n/**\n * Persist the manifest wrapper. Returns the absolute output path.\n *\n * Throws `SaveManifestError` for shape / validation failures; lets raw\n * I/O errors (EACCES, ENOSPC, etc.) propagate so the orchestrator can\n * decide whether to suppress them per step-16's \"save-fail → success\n * still returned\" contract.\n */\nexport async function saveManifest(\n input: SaveManifestInput,\n): Promise<SaveManifestResult> {\n if (\n typeof process === 'undefined' ||\n typeof process.versions?.node !== 'string'\n ) {\n throw new SaveManifestError(\n 'platform_unsupported',\n 'saveManifest: requires Node.js runtime (node:fs / node:crypto / node:path)',\n );\n }\n // Copilot review fix (PR #58 r3267373130): reject empty / whitespace-\n // only / non-string `dataDir` BEFORE any I/O. `pathResolve('')` returns\n // `process.cwd()`, and the later `chmodSync(absoluteDataDir, 0o700)`\n // would then tighten the caller's working directory — a real safety\n // hazard if a misconfigured env (`MANIFEST_DATA_DIR=\"\"`) reaches\n // here. Failing fast at the boundary keeps the hazard from\n // materializing.\n if (typeof input.dataDir !== 'string' || input.dataDir.trim().length === 0) {\n throw new SaveManifestError(\n 'invalid_data_dir',\n `saveManifest: dataDir must be a non-empty path; got ${\n typeof input.dataDir === 'string'\n ? `\"${input.dataDir}\"`\n : input.dataDir === null\n ? 'null'\n : typeof input.dataDir\n }.`,\n );\n }\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new SaveManifestError(\n 'invalid_uuid',\n `saveManifest: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (!META_HASH_RE.test(input.metaHash)) {\n throw new SaveManifestError(\n 'invalid_meta_hash',\n `saveManifest: metaHash must be a 64-character SHA-256 hex digest; got \"${input.metaHash}\"`,\n );\n }\n if (input.customDomainServiceName && !input.customDomain) {\n throw new SaveManifestError(\n 'service_name_without_domain',\n 'saveManifest: customDomainServiceName requires customDomain',\n );\n }\n\n // Trim trailing newline (heredoc/Write convention) so the SHA-256 of the\n // persisted bytes matches the meta_hash_hex returned by\n // build_manifest_preview.\n const trimmed = input.manifestJson.trimEnd();\n\n // Parse for shape sanity + format derivation. Failures throw a typed\n // `SaveManifestError(manifest_parse_failed)` rather than the raw\n // SyntaxError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new SaveManifestError(\n 'manifest_parse_failed',\n `saveManifest: manifestJson is not valid JSON: ${reason}`,\n );\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new SaveManifestError(\n 'manifest_not_object',\n 'saveManifest: manifestJson must encode a JSON object',\n );\n }\n const parsedRec = parsed as Record<string, unknown>;\n const isStack =\n parsedRec.services !== null &&\n typeof parsedRec.services === 'object' &&\n !Array.isArray(parsedRec.services);\n const format: 'single' | 'stack' = isStack ? 'stack' : 'single';\n\n // Dynamic imports — node-only deps deferred per the `platform: 'neutral'`\n // build target. Mirrors `guarded-fetch.ts`'s lazy-init pattern.\n const { mkdirSync, chmodSync, writeFileSync, renameSync } = await import(\n 'node:fs'\n );\n const { createHash, randomUUID } = await import('node:crypto');\n const { join, resolve: pathResolve } = await import('node:path');\n\n // SHA-256 audit: catches the most common foot-gun (passing the\n // structured spec where the canonical manifest_json was expected).\n const computedHash = createHash('sha256').update(trimmed).digest('hex');\n if (computedHash !== input.metaHash.toLowerCase()) {\n throw new SaveManifestError(\n 'sha256_mismatch',\n `saveManifest: SHA-256 mismatch. metaHash claims ${input.metaHash} but manifestJson content hashes to ${computedHash}. The wrong content was probably written (e.g. the structured spec instead of the canonical manifest_json string).`,\n );\n }\n\n // C5 fix: resolve dataDir to absolute BEFORE constructing paths.\n // `SaveManifestResult.manifestPath` is documented as absolute; the\n // prior `join(input.dataDir, ...)` returned a relative path when the\n // caller passed a relative dataDir. `path.resolve()` normalizes\n // against the process CWD if input is relative; idempotent for\n // already-absolute inputs.\n const absoluteDataDir = pathResolve(input.dataDir);\n\n // Ensure dataDir + manifests/ exist with tight perms. chmod after mkdir\n // so a pre-existing looser parent gets tightened (mkdir won't chmod\n // existing dirs).\n const manifestsDir = join(absoluteDataDir, 'manifests');\n mkdirSync(absoluteDataDir, { recursive: true, mode: 0o700 });\n chmodSync(absoluteDataDir, 0o700);\n mkdirSync(manifestsDir, { recursive: true, mode: 0o700 });\n chmodSync(manifestsDir, 0o700);\n\n // Copilot review fix (PR #58 r3267708600): single-source the deploy\n // timestamp. The prior code called `new Date().toISOString()` and\n // `Math.floor(Date.now() / 1000)` separately — two distinct clock\n // reads. If the function spans a second boundary, the iso + unix\n // fields refer to different instants, violating the audit\n // metadata's internal-consistency invariant (any tooling cross-\n // checking the pair would flag the drift).\n const deployedAt = new Date();\n const wrapper: Record<string, unknown> = {\n schema_version: 3,\n lease_uuid: input.leaseUuid,\n deployed_at_iso: deployedAt.toISOString(),\n deployed_at_unix: Math.floor(deployedAt.getTime() / 1000),\n chain_id: input.chainId,\n image: input.image,\n size: input.size,\n meta_hash_hex: input.metaHash.toLowerCase(),\n format,\n manifest_json: trimmed,\n };\n if (input.customDomain) {\n wrapper.custom_domain = input.customDomain;\n }\n if (input.customDomainServiceName) {\n wrapper.custom_domain_service_name = input.customDomainServiceName;\n }\n\n const outPath = join(manifestsDir, `${input.leaseUuid}.json`);\n // Atomic write: temp file in same dir + rename. Survives crash mid-write\n // without leaving a partial file at the canonical name. The randomUUID\n // suffix avoids collisions if multiple concurrent saves target the same\n // lease (rare, but the CJS's atomicWrite helper uses the same pattern).\n const tmpPath = `${outPath}.tmp-${randomUUID()}`;\n writeFileSync(tmpPath, `${JSON.stringify(wrapper, null, 2)}\\n`, {\n mode: 0o600,\n });\n renameSync(tmpPath, outPath);\n\n return { manifestPath: outPath };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"save-manifest.js","names":[],"sources":["../../src/internals/save-manifest.ts"],"sourcesContent":["/**\n * Persist a deployed manifest as a schema-version-3 wrapper to disk.\n *\n * Target directory is a **function argument** (`dataDir: string`) rather\n * than an env-var read: per gate-1 verdict, agent-core's `platform: 'neutral'`\n * build target forbids env-var reads at this layer; callers are responsible\n * for resolving the manifests dir from their own env context and passing it in.\n *\n * **Wrapper shape (schema_version 3):**\n *\n * ```\n * {\n * schema_version: 3,\n * lease_uuid, deployed_at_iso, deployed_at_unix,\n * chain_id, image, size, meta_hash_hex,\n * format, // \"single\" or \"stack\"\n * manifest_json, // string — canonical Fred-rendered JSON\n * custom_domain?, // (v3) FQDN attached to the lease item\n * custom_domain_service_name? // (v3) stack-lease service holder\n * }\n * ```\n *\n * Schema-version compat: v2 wrappers remain readable by all downstream\n * helpers; missing v3 fields are tolerated as undefined.\n *\n * **Audit guarantee:** SHA-256 of the bytes about to be persisted (after\n * normalizing the heredoc-/Write-added trailing newline) MUST equal\n * `metaHash`. Catches paste errors, accidental spec-vs-manifest_json\n * swaps, and transit corruption. Mismatch throws a typed\n * `SaveManifestError` (`code: 'sha256_mismatch'`).\n *\n * **Filesystem layout:** `<dataDir>/manifests/<lease_uuid>.json` with\n * mode 0600; parent `<dataDir>` and `<dataDir>/manifests` ensured at\n * mode 0700 (chmod-tightens an existing parent that was previously\n * looser).\n *\n * **Dynamic node-import discipline** (mirrors `guarded-fetch.ts`): the\n * `node:fs` / `node:path` / `node:crypto` imports are deferred to call\n * time so module load doesn't violate the `platform: 'neutral'` build\n * target. A `typeof process` check throws a clear \"Node-only API\" error\n * if invoked outside a Node-like runtime.\n */\n\n/** SHA-256 hex digest — 64 lowercase hex chars. */\nconst META_HASH_RE = /^[0-9a-f]{64}$/i;\n\n/** RFC 4122 UUID — 36 chars. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Input contract for `saveManifest`. All `*Hex` / UUID fields are validated. */\nexport interface SaveManifestInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Canonical primary image reference (for the wrapper's `image` field). */\n image: string;\n /** SKU name (e.g. `docker-micro`) for the wrapper's `size` field. */\n size: string;\n /** SHA-256 hex of the canonical manifest JSON, from `build_manifest_preview`. */\n metaHash: string;\n /** Chain ID — e.g. `manifest-ledger-testnet-1`. */\n chainId: string;\n /**\n * Canonical Fred-rendered manifest JSON (as a STRING, not a parsed\n * object). The SHA-256 of these bytes (after trimming trailing\n * whitespace) MUST equal `metaHash`.\n */\n manifestJson: string;\n /**\n * Target data directory — the function writes\n * `<dataDir>/manifests/<lease_uuid>.json`. Per gate-1 verdict,\n * supplied by the caller (no env-var read).\n *\n * **MUST be a dedicated manifest-storage directory.** This function\n * `chmod`-tightens any pre-existing `dataDir` (and its `manifests/`\n * subdirectory) to mode `0o700`. Do NOT pass shared parents like\n * `$HOME`, `~/.config`, or a generic data root — doing so would tighten\n * those directories' permissions and potentially break other processes\n * that depend on them. Callers must resolve this to a dedicated\n * subdirectory (e.g. `$XDG_DATA_HOME/manifest-agent/`); the `manifests/`\n * subdir is created inside automatically.\n */\n dataDir: string;\n /** Optional custom-domain FQDN attached to the lease item. */\n customDomain?: string;\n /**\n * Optional stack-lease service name that holds the custom domain.\n * Meaningless without `customDomain`; throws if supplied alone.\n */\n customDomainServiceName?: string;\n}\n\nexport interface SaveManifestResult {\n /** Absolute path to the persisted wrapper. */\n manifestPath: string;\n}\n\n/** Typed error surface for the I/O + validation failure paths. */\nexport class SaveManifestError extends Error {\n readonly code:\n | 'sha256_mismatch'\n | 'manifest_not_object'\n | 'invalid_uuid'\n | 'invalid_meta_hash'\n | 'invalid_data_dir'\n | 'service_name_without_domain'\n | 'manifest_parse_failed'\n | 'platform_unsupported';\n\n constructor(code: SaveManifestError['code'], message: string) {\n super(message);\n this.name = 'SaveManifestError';\n this.code = code;\n Object.setPrototypeOf(this, SaveManifestError.prototype);\n }\n}\n\n/**\n * Persist the manifest wrapper. Returns the absolute output path.\n *\n * Throws `SaveManifestError` for shape / validation failures; lets raw\n * I/O errors (EACCES, ENOSPC, etc.) propagate so the orchestrator can\n * decide whether to suppress them per step-16's \"save-fail → success\n * still returned\" contract.\n */\nexport async function saveManifest(\n input: SaveManifestInput,\n): Promise<SaveManifestResult> {\n if (\n typeof process === 'undefined' ||\n typeof process.versions?.node !== 'string'\n ) {\n throw new SaveManifestError(\n 'platform_unsupported',\n 'saveManifest: requires Node.js runtime (node:fs / node:crypto / node:path)',\n );\n }\n // Copilot review fix (PR #58 r3267373130): reject empty / whitespace-\n // only / non-string `dataDir` BEFORE any I/O. `pathResolve('')` returns\n // `process.cwd()`, and the later `chmodSync(absoluteDataDir, 0o700)`\n // would then tighten the caller's working directory — a real safety\n // hazard if a misconfigured env (`MANIFEST_DATA_DIR=\"\"`) reaches\n // here. Failing fast at the boundary keeps the hazard from\n // materializing.\n if (typeof input.dataDir !== 'string' || input.dataDir.trim().length === 0) {\n throw new SaveManifestError(\n 'invalid_data_dir',\n `saveManifest: dataDir must be a non-empty path; got ${\n typeof input.dataDir === 'string'\n ? `\"${input.dataDir}\"`\n : input.dataDir === null\n ? 'null'\n : typeof input.dataDir\n }.`,\n );\n }\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new SaveManifestError(\n 'invalid_uuid',\n `saveManifest: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (!META_HASH_RE.test(input.metaHash)) {\n throw new SaveManifestError(\n 'invalid_meta_hash',\n `saveManifest: metaHash must be a 64-character SHA-256 hex digest; got \"${input.metaHash}\"`,\n );\n }\n if (input.customDomainServiceName && !input.customDomain) {\n throw new SaveManifestError(\n 'service_name_without_domain',\n 'saveManifest: customDomainServiceName requires customDomain',\n );\n }\n\n // Trim trailing newline (heredoc/Write convention) so the SHA-256 of the\n // persisted bytes matches the meta_hash_hex returned by\n // build_manifest_preview.\n const trimmed = input.manifestJson.trimEnd();\n\n // Parse for shape sanity + format derivation. Failures throw a typed\n // `SaveManifestError(manifest_parse_failed)` rather than the raw\n // SyntaxError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new SaveManifestError(\n 'manifest_parse_failed',\n `saveManifest: manifestJson is not valid JSON: ${reason}`,\n );\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new SaveManifestError(\n 'manifest_not_object',\n 'saveManifest: manifestJson must encode a JSON object',\n );\n }\n const parsedRec = parsed as Record<string, unknown>;\n const isStack =\n parsedRec.services !== null &&\n typeof parsedRec.services === 'object' &&\n !Array.isArray(parsedRec.services);\n const format: 'single' | 'stack' = isStack ? 'stack' : 'single';\n\n // Dynamic imports — node-only deps deferred per the `platform: 'neutral'`\n // build target. Mirrors `guarded-fetch.ts`'s lazy-init pattern.\n const { mkdirSync, chmodSync, writeFileSync, renameSync } = await import(\n 'node:fs'\n );\n const { createHash, randomUUID } = await import('node:crypto');\n const { join, resolve: pathResolve } = await import('node:path');\n\n // SHA-256 audit: catches the most common foot-gun (passing the\n // structured spec where the canonical manifest_json was expected).\n const computedHash = createHash('sha256').update(trimmed).digest('hex');\n if (computedHash !== input.metaHash.toLowerCase()) {\n throw new SaveManifestError(\n 'sha256_mismatch',\n `saveManifest: SHA-256 mismatch. metaHash claims ${input.metaHash} but manifestJson content hashes to ${computedHash}. The wrong content was probably written (e.g. the structured spec instead of the canonical manifest_json string).`,\n );\n }\n\n // C5 fix: resolve dataDir to absolute BEFORE constructing paths.\n // `SaveManifestResult.manifestPath` is documented as absolute; the\n // prior `join(input.dataDir, ...)` returned a relative path when the\n // caller passed a relative dataDir. `path.resolve()` normalizes\n // against the process CWD if input is relative; idempotent for\n // already-absolute inputs.\n const absoluteDataDir = pathResolve(input.dataDir);\n\n // Ensure dataDir + manifests/ exist with tight perms. chmod after mkdir\n // so a pre-existing looser parent gets tightened (mkdir won't chmod\n // existing dirs).\n const manifestsDir = join(absoluteDataDir, 'manifests');\n mkdirSync(absoluteDataDir, { recursive: true, mode: 0o700 });\n chmodSync(absoluteDataDir, 0o700);\n mkdirSync(manifestsDir, { recursive: true, mode: 0o700 });\n chmodSync(manifestsDir, 0o700);\n\n // Copilot review fix (PR #58 r3267708600): single-source the deploy\n // timestamp. The prior code called `new Date().toISOString()` and\n // `Math.floor(Date.now() / 1000)` separately — two distinct clock\n // reads. If the function spans a second boundary, the iso + unix\n // fields refer to different instants, violating the audit\n // metadata's internal-consistency invariant (any tooling cross-\n // checking the pair would flag the drift).\n const deployedAt = new Date();\n const wrapper: Record<string, unknown> = {\n schema_version: 3,\n lease_uuid: input.leaseUuid,\n deployed_at_iso: deployedAt.toISOString(),\n deployed_at_unix: Math.floor(deployedAt.getTime() / 1000),\n chain_id: input.chainId,\n image: input.image,\n size: input.size,\n meta_hash_hex: input.metaHash.toLowerCase(),\n format,\n manifest_json: trimmed,\n };\n if (input.customDomain) {\n wrapper.custom_domain = input.customDomain;\n }\n if (input.customDomainServiceName) {\n wrapper.custom_domain_service_name = input.customDomainServiceName;\n }\n\n const outPath = join(manifestsDir, `${input.leaseUuid}.json`);\n // Atomic write: temp file in same dir + rename. Survives crash mid-write\n // without leaving a partial file at the canonical name. The randomUUID\n // suffix avoids collisions if multiple concurrent saves target the same\n // lease (rare, but the CJS's atomicWrite helper uses the same pattern).\n const tmpPath = `${outPath}.tmp-${randomUUID()}`;\n writeFileSync(tmpPath, `${JSON.stringify(wrapper, null, 2)}\\n`, {\n mode: 0o600,\n });\n renameSync(tmpPath, outPath);\n\n return { manifestPath: outPath };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-denylist.d.ts","names":[],"sources":["../../src/internals/secret-denylist.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-denylist.d.ts","names":[],"sources":["../../src/internals/secret-denylist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAwBa,qBAAmB;cAGnB,0BAA0B;;;;;;;;;;;;;iBAkBvB,cAAc"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* prototype mutation.
|
|
24
24
|
*/
|
|
25
25
|
const SECRET_KEY_DENYLIST = /(mnemonic|password|private[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token|bearer[_-]?token)/i;
|
|
26
|
-
const PROTOTYPE_POLLUTION_KEYS = new Set([
|
|
26
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
27
27
|
"__proto__",
|
|
28
28
|
"constructor",
|
|
29
29
|
"prototype"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-denylist.js","names":[],"sources":["../../src/internals/secret-denylist.ts"],"sourcesContent":["/**\n * Secret-key denylist + prototype-pollution guard. Used by\n * `verify-recover.ts` to scrub a verifier's diagnostic payload before it\n * reaches the host callback (or the journal record in ENG-124).\n *\n * Two exports:\n *\n * - `SECRET_KEY_DENYLIST` — case-insensitive substring match on KEY names\n * only; values are never inspected. Narrow on purpose: covers the\n * high-confidence sensitive shapes (mnemonic + keyfile password) plus\n * credential-shaped suffixes that catch obvious caller mistakes\n * (`api[_-]?key`, `private[_-]?key`, `secret[_-]?key`, `auth[_-]?token`,\n * `bearer[_-]?token`). The blanket `token` and `secret` keywords are NOT\n * here — this is a blockchain context where `gas_token`, `fee_token`,\n * `token_id`, `token_symbol` are legitimate non-sensitive field names.\n *\n * - `stripDenylist` (in `verify-recover.ts`) — recursive walker over\n * objects + arrays; drops any key matching the denylist regex; ALSO\n * skips the three prototype-pollution-capable keys `__proto__`,\n * `constructor`, `prototype` because `JSON.parse` materializes them as\n * own properties that a bare `out[k] = v` assignment would treat as a\n * prototype mutation.\n */\n\nexport const SECRET_KEY_DENYLIST =\n /(mnemonic|password|private[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token|bearer[_-]?token)/i;\n\nexport const PROTOTYPE_POLLUTION_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\n/**\n * Recursively walk a value and remove any object keys that:\n * - Match `SECRET_KEY_DENYLIST` (case-insensitive substring on key name), or\n * - Are one of the prototype-pollution keys (`__proto__`, `constructor`,\n * `prototype`).\n *\n * Arrays are walked element-wise. Primitives (string/number/boolean/null/\n * undefined) pass through untouched.\n *\n * Returns `unknown` because the structural shape changes: object inputs may\n * have fewer keys than they started with. Callers narrow at use sites.\n */\nexport function stripDenylist(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((v) => stripDenylist(v));\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (PROTOTYPE_POLLUTION_KEYS.has(k)) continue;\n if (SECRET_KEY_DENYLIST.test(k)) continue;\n out[k] = stripDenylist(v);\n }\n return out;\n }\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,sBACX;AAEF,MAAa,
|
|
1
|
+
{"version":3,"file":"secret-denylist.js","names":[],"sources":["../../src/internals/secret-denylist.ts"],"sourcesContent":["/**\n * Secret-key denylist + prototype-pollution guard. Used by\n * `verify-recover.ts` to scrub a verifier's diagnostic payload before it\n * reaches the host callback (or the journal record in ENG-124).\n *\n * Two exports:\n *\n * - `SECRET_KEY_DENYLIST` — case-insensitive substring match on KEY names\n * only; values are never inspected. Narrow on purpose: covers the\n * high-confidence sensitive shapes (mnemonic + keyfile password) plus\n * credential-shaped suffixes that catch obvious caller mistakes\n * (`api[_-]?key`, `private[_-]?key`, `secret[_-]?key`, `auth[_-]?token`,\n * `bearer[_-]?token`). The blanket `token` and `secret` keywords are NOT\n * here — this is a blockchain context where `gas_token`, `fee_token`,\n * `token_id`, `token_symbol` are legitimate non-sensitive field names.\n *\n * - `stripDenylist` (in `verify-recover.ts`) — recursive walker over\n * objects + arrays; drops any key matching the denylist regex; ALSO\n * skips the three prototype-pollution-capable keys `__proto__`,\n * `constructor`, `prototype` because `JSON.parse` materializes them as\n * own properties that a bare `out[k] = v` assignment would treat as a\n * prototype mutation.\n */\n\nexport const SECRET_KEY_DENYLIST =\n /(mnemonic|password|private[_-]?key|secret[_-]?key|api[_-]?key|auth[_-]?token|bearer[_-]?token)/i;\n\nexport const PROTOTYPE_POLLUTION_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\n/**\n * Recursively walk a value and remove any object keys that:\n * - Match `SECRET_KEY_DENYLIST` (case-insensitive substring on key name), or\n * - Are one of the prototype-pollution keys (`__proto__`, `constructor`,\n * `prototype`).\n *\n * Arrays are walked element-wise. Primitives (string/number/boolean/null/\n * undefined) pass through untouched.\n *\n * Returns `unknown` because the structural shape changes: object inputs may\n * have fewer keys than they started with. Callers narrow at use sites.\n */\nexport function stripDenylist(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((v) => stripDenylist(v));\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (PROTOTYPE_POLLUTION_KEYS.has(k)) continue;\n if (SECRET_KEY_DENYLIST.test(k)) continue;\n out[k] = stripDenylist(v);\n }\n return out;\n }\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,sBACX;AAEF,MAAa,2CAAgD,IAAI,IAAI;CACnE;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;AAcD,SAAgB,cAAc,OAAyB;CACrD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,MAAM,cAAc,CAAC,CAAC;CAE1C,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,KAAgC,GAAG;GACrE,IAAI,yBAAyB,IAAI,CAAC,GAAG;GACrC,IAAI,oBAAoB,KAAK,CAAC,GAAG;GACjC,IAAI,KAAK,cAAc,CAAC;EAC1B;EACA,OAAO;CACT;CACA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-normalize.d.ts","names":[],"sources":["../../src/internals/spec-normalize.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"spec-normalize.d.ts","names":[],"sources":["../../src/internals/spec-normalize.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BgB,YACd,gBACC,QAAQ;EAAkB,UAAU,eAAe;;;;;;;;iBAmBtC,WAAW;;;;;;;;UAyBV;;EAEf;;EAEA,KAAK,gBAAgB;;iBAGP,kBAAkB,gBAAgB;;;;;;;;;;;;;;;;iBA8BlC,cAAc,gBAAgB;;;;;;;;;;;;;;;;;;iBA2D9B,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-normalize.js","names":[],"sources":["../../src/internals/spec-normalize.ts"],"sourcesContent":["import type { AppDeploySpec, ServiceConfig, SpecSummary } from '../types.js';\n\n/**\n * Spec normalization + summarization helpers. Exports `isStack`,\n * `firstImage`, `normalizeServices`, `summarizeSpec`, and `validateSpec`\n * (the latter surfaces pre-broadcast shape violations).\n *\n * The single canonical spec type (`AppDeploySpec`, ENG-310) covers two\n * shapes, discriminated by the presence of `services`:\n * - **stack** — `{ services: { <name>: ServiceConfig }, customDomain?, serviceName?, size, ... }`\n * - **single-service** — `{ image, port?, env?, customDomain?, size, ... }`\n *\n * `normalizeServices` collapses the two shapes into a single iterable form\n * so callers (Plan summary, manifest builder, etc.) walk one structure\n * regardless of which form the user passed.\n *\n * Validation: `validateSpec` stays a `void` `TypeError`-thrower on shape\n * violations. The high-level `deployApp` entry wrappers\n * (`deploy-app.ts`) map that `TypeError` to\n * `ManifestMCPError(INVALID_CONFIG)` at the public-API boundary, so this\n * helper does not depend on core's error type directly.\n */\n\n/**\n * True when `spec` uses the services-map (stack) shape. Discriminates on\n * the canonical `services` key being present and defined (ENG-310). The\n * non-null-object / non-array guard is retained as defense-in-depth so an\n * `unknown`-cast caller smuggling `services: null` / `[]` / `'str'` is NOT\n * mistaken for a stack (which would crash `Object.entries` downstream).\n */\nexport function isStackSpec(\n spec: unknown,\n): spec is AppDeploySpec & { services: Record<string, ServiceConfig> } {\n if (spec === null || spec === undefined || typeof spec !== 'object')\n return false;\n const record = spec as { services?: unknown };\n if (!('services' in record) || record.services === undefined) return false;\n const services = record.services;\n return (\n services !== null &&\n typeof services === 'object' &&\n !Array.isArray(services)\n );\n}\n\n/**\n * Return the canonical first image string for a spec. For legacy single-\n * service: `spec.image`. For stack: the first non-empty `image` in\n * `Object.values(spec.services)`. Returns `null` when neither shape\n * carries an image (or `spec` is malformed).\n */\nexport function firstImage(spec: unknown): string | null {\n if (spec === null || spec === undefined || typeof spec !== 'object')\n return null;\n const single = spec as Partial<AppDeploySpec>;\n if (typeof single.image === 'string' && single.image.length > 0) {\n return single.image;\n }\n if (isStackSpec(spec)) {\n for (const svc of Object.values(spec.services)) {\n if (svc !== null && typeof svc === 'object') {\n const image = (svc as Partial<ServiceConfig>).image;\n if (typeof image === 'string' && image.length > 0) return image;\n }\n }\n }\n return null;\n}\n\n/**\n * Walk a spec as `[{name, raw}]` where:\n * - `name === null` for legacy single-service (only one entry, raw is the spec itself).\n * - `name === <key>` for each services-map entry; `raw` is the per-service ServiceConfig.\n *\n * Stable iteration order matches `Object.entries` (insertion order in v8/modern engines).\n */\nexport interface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** The per-service object exactly as the spec stores it. No field projection. */\n raw: ServiceConfig | AppDeploySpec;\n}\n\nexport function normalizeServices(spec: unknown): NormalizedService[] {\n if (isStackSpec(spec)) {\n return Object.entries(spec.services).map(([name, raw]) => ({\n name,\n raw: (raw ?? {}) as ServiceConfig,\n }));\n }\n return [\n {\n name: null,\n raw: (spec ?? {}) as AppDeploySpec,\n },\n ];\n}\n\n/**\n * Produce the frozen `SpecSummary` shape for inclusion in the `Plan`\n * (camelCase fields: `serviceCount`, etc.).\n *\n * Port count rules:\n * - single-service `port: number` → +1 port.\n * - `ServiceConfig.ports` map (canonical `{ '<port>/<proto>': {} }`) →\n * +key count.\n * - A bare `port`/`ports` array smuggled in by an `unknown`-cast caller\n * is still counted defensively (+1 / +length) below.\n *\n * Env key uniqueness is computed across services (one `env_keys` set\n * spans the whole spec); `envCount` is the size of that set; `envKeys`\n * is sorted ascending.\n */\nexport function summarizeSpec(spec: unknown): SpecSummary {\n const format: 'single' | 'stack' = isStackSpec(spec) ? 'stack' : 'single';\n const services = normalizeServices(spec);\n\n let portCount = 0;\n const envKeys = new Set<string>();\n const images: string[] = [];\n\n for (const { raw: svc } of services) {\n if (svc !== null && typeof svc === 'object') {\n const svcRecord = svc as unknown as Record<string, unknown>;\n const image = svcRecord.image;\n if (typeof image === 'string' && image.length > 0) images.push(image);\n\n const port = svcRecord.port;\n if (typeof port === 'number') portCount += 1;\n else if (Array.isArray(port)) portCount += port.length;\n\n const ports = svcRecord.ports;\n if (Array.isArray(ports)) {\n portCount += ports.length;\n } else if (ports !== null && typeof ports === 'object') {\n portCount += Object.keys(ports).length;\n }\n\n const env = svcRecord.env;\n if (env !== null && typeof env === 'object' && !Array.isArray(env)) {\n for (const k of Object.keys(env)) envKeys.add(k);\n }\n }\n }\n\n return {\n format,\n serviceCount: services.length,\n portCount,\n envCount: envKeys.size,\n envKeys: Array.from(envKeys).sort(),\n images,\n };\n}\n\n/**\n * Validate an `AppDeploySpec` shape pre-broadcast. Throws `TypeError` on\n * the first violation. The canonical type already enforces most\n * structural rules at compile time; this runtime check defends against\n * `unknown`-cast callers and `JSON.parse`-decoded inputs (and the\n * image-XOR-services invariant the type cannot express).\n *\n * Rules (mirror fred's `deployApp.ts` input validation):\n * - `spec` must be a non-null object.\n * - Stack: `services` must have ≥1 entry; each entry's `image` must be a\n * non-empty string.\n * - Single: `image` must be a non-empty string.\n * - Mutually exclusive `image` AND `services` not allowed.\n *\n * The high-level `deployApp` in PR 3 layers domain checks on top\n * (`customDomain` shape, `serviceName` membership, etc.).\n */\nexport function validateSpec(spec: unknown): void {\n if (spec === null || spec === undefined || typeof spec !== 'object') {\n throw new TypeError('validateSpec: spec must be a non-null object');\n }\n const record = spec as unknown as Record<string, unknown>;\n\n // Mutual-exclusion gate uses KEY presence (not value validity). This\n // closes the bypass where a caller supplies a malformed `image` value\n // (empty string, number, null) alongside a valid `services` map: the\n // value-based check would silently treat `image` as \"absent\" and accept\n // the spec, but the caller's intent was ambiguous (which shape did they\n // mean?). Rejecting on key-presence forces the caller to delete one key\n // before submission and removes the ambiguity.\n const hasImageKey = 'image' in record;\n const hasServicesKey = 'services' in record;\n if (hasImageKey && hasServicesKey) {\n throw new TypeError(\n 'validateSpec: spec has both `image` and `services` keys; these are mutually exclusive (regardless of value validity)',\n );\n }\n\n // Downstream value-validity check (after the mutual-exclusion gate has\n // ruled out the ambiguous case). An `image` key with a non-string or\n // empty-string value still fails here when `services` is absent.\n const hasImage = typeof record.image === 'string' && record.image.length > 0;\n const hasServices = isStackSpec(spec);\n if (!hasImage && !hasServices) {\n throw new TypeError(\n 'validateSpec: spec must declare either `image` (single-service) or `services` (stack)',\n );\n }\n\n // Copilot review fix (PR #58 r3266786899): `customDomain` shape at\n // the boundary. Historically the orchestrator's `buildFredDeployInput`\n // mapper used an `if (customDomain)` truthiness check that silently\n // dropped `''`, `null`, `false`, `0`, `NaN` from the emitted\n // `fredInput`, so a spec like `{ ..., customDomain: '' }` deployed with\n // the domain silently not claimed. ENG-310 deleted that mapper: the\n // loss-free broadcast spread now forwards `customDomain` to fred\n // verbatim, so an empty/whitespace value would instead reach fred as a\n // claim for an invalid domain. Either way the fix is the same — reject\n // a malformed `customDomain` here, at the boundary, before it can do harm.\n //\n // Boundary check: when `customDomain` is present, it must be a\n // non-empty string. `undefined` (key absent) is fine; that's the\n // \"no domain requested\" case. Fires before the stack-customDomain\n // serviceName check (r3249684707) so the user gets a clear\n // customDomain-shape error rather than a misleading\n // requires-serviceName one.\n // Copilot review fix (PR #58 r3267373001): reject whitespace-only\n // strings AND strings with surrounding whitespace (option (i) from\n // the team-lead's brief — strict; let the caller send a clean,\n // already-trimmed value rather than silently trim for them). The\n // prior `cd.length === 0` predicate accepted `' '`, `'\\t\\n'`,\n // and `' app.example.com '`; fred would either accept the\n // surrounding whitespace as part of the domain (correctness bug)\n // or trim-and-reject (worse UX than agent-core's clear error).\n if ('customDomain' in record) {\n const cd = record.customDomain;\n if (cd !== undefined) {\n const isCleanNonEmptyString =\n typeof cd === 'string' && cd.length > 0 && cd.trim() === cd;\n if (!isCleanNonEmptyString) {\n const got =\n typeof cd === 'string'\n ? cd.trim().length === 0\n ? `\"${cd}\"`\n : `\"${cd}\" (has surrounding whitespace)`\n : cd === null\n ? 'null'\n : typeof cd;\n throw new TypeError(\n `validateSpec: \\`customDomain\\` must be a non-empty trimmed string or absent (got ${got}).`,\n );\n }\n }\n }\n\n if (hasServices) {\n const entries = Object.entries(spec.services);\n if (entries.length === 0) {\n throw new TypeError(\n 'validateSpec: stack spec `services` must have at least one entry',\n );\n }\n for (const [name, svc] of entries) {\n if (svc === null || typeof svc !== 'object') {\n throw new TypeError(\n `validateSpec: stack service \"${name}\" must be a non-null object`,\n );\n }\n const image = (svc as Partial<ServiceConfig>).image;\n if (typeof image !== 'string' || image.length === 0) {\n throw new TypeError(\n `validateSpec: stack service \"${name}\" must declare a non-empty \\`image\\` string`,\n );\n }\n }\n\n // Copilot review fix (PR #58 r3249684707): a stack spec with a\n // `customDomain` MUST declare which service receives the domain\n // via `serviceName`, and that value must be a key in `services`.\n // Without this guard, `customDomainServiceOf` in `deploy-app.ts`\n // returns `undefined`, planning proceeds with no target, renderers\n // misrepresent the claim, and fred rejects the set-domain tx\n // ONLY after `create-lease` commits — leaving the user with an\n // orphan lease + a failed domain claim. Catching this at\n // validate-time is fail-fast at the boundary.\n //\n // Single-service specs are unaffected: their `customDomain` is\n // claimed against the implicit single lease item — no\n // serviceName disambiguation needed.\n const stackDomain = (spec as Partial<AppDeploySpec>).customDomain;\n if (typeof stackDomain === 'string' && stackDomain.length > 0) {\n const stackServiceName = (spec as Partial<AppDeploySpec>).serviceName;\n if (\n typeof stackServiceName !== 'string' ||\n stackServiceName.length === 0\n ) {\n throw new TypeError(\n 'validateSpec: stack spec with `customDomain` requires `serviceName` identifying which service receives the domain.',\n );\n }\n // Copilot review fix (PR #58 r3250331968): use an own-key check.\n // The `in` operator walks the prototype chain, so `serviceName:\n // 'constructor'` (or `'toString'`, `'hasOwnProperty'`, etc.)\n // would falsely pass against a `services` map that doesn't\n // declare those names. Mirrors fred's own choice at\n // `packages/fred/src/tools/deployApp.ts:254` for cross-package\n // symmetry. `Object.keys().includes()` (not `Object.hasOwn`,\n // which is ES2022 and our `tsdown.config.ts` targets ES2020).\n if (!Object.keys(spec.services).includes(stackServiceName)) {\n throw new TypeError(\n `validateSpec: stack spec \\`serviceName\\` \"${stackServiceName}\" must be a key in \\`services\\` (got services: [${Object.keys(spec.services).join(', ')}]).`,\n );\n }\n }\n } else {\n // Single-service spec port requirement.\n //\n // Copilot review fix (PR #58 r3249097051): fred's image-mode rejects\n // portless inputs with `port is required when using image`\n // (`packages/fred/src/tools/deployApp.ts:202` +\n // `packages/fred/src/tools/buildManifestPreview.ts:181`). Without\n // an agent-core boundary check the orchestrator would pass\n // `port: undefined` straight through the loss-free broadcast spread\n // to fred, surfacing fred's error mid-orchestration (after readiness\n // check + plan render). Failing fast at validate time produces a\n // clearer message and avoids partial work.\n //\n // The escape hatch for genuinely internal-only services is the\n // stack spec — service-level `ports` is optional, so a stack with\n // `{ services: { mysvc: { image, env } } }` deploys without ports.\n //\n // Copilot review fix (PR #58 r3249294877): tighten the predicate to\n // a finite positive integer in the TCP port range. The prior\n // `typeof p === 'number'` check accepted `0`, `NaN`, `Infinity`,\n // negative numbers, non-integers, and out-of-range ports —\n // partially defeating the fail-fast intent. Fred catches `port: 0`\n // via `!input.port`, but the other shapes either flow through to a\n // less helpful error or get coerced silently. The shared predicate\n // `isValidPortNumber` (below) is the single source of truth.\n // Read as `unknown`: the canonical AppDeploySpec.port is `number`, but\n // this runtime guard also accepts a legacy `number[]` smuggled in by an\n // `unknown`-cast / JSON.parse caller (the array branch below) — typing it\n // `unknown` keeps `Array.isArray` from narrowing to `never` (ENG-310).\n const port: unknown = (spec as { port?: unknown }).port;\n const hasValidPort =\n isValidPortNumber(port) ||\n (Array.isArray(port) && port.length > 0 && port.every(isValidPortNumber));\n if (!hasValidPort) {\n throw new TypeError(\n 'validateSpec: single-service specs require at least one port (port must be a finite positive integer in the TCP range (1-65535), or a non-empty array of such); got ' +\n `port=${JSON.stringify(port)}. For internal-only services, use a stack spec instead.`,\n );\n }\n }\n}\n\n/**\n * Predicate: `p` is a finite positive integer in the TCP port range\n * (1-65535). Used by `validateSpec` to gate single-service `port`\n * shapes against the broad `typeof === 'number'` bypass.\n *\n * Co-located in this module because it's exclusive to the port-\n * validation boundary; if a future caller needs the same check,\n * promote it to a shared utility then.\n */\nfunction isValidPortNumber(p: unknown): p is number {\n return typeof p === 'number' && Number.isInteger(p) && p > 0 && p <= 65535;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"spec-normalize.js","names":[],"sources":["../../src/internals/spec-normalize.ts"],"sourcesContent":["import type { AppDeploySpec, ServiceConfig, SpecSummary } from '../types.js';\n\n/**\n * Spec normalization + summarization helpers. Exports `isStack`,\n * `firstImage`, `normalizeServices`, `summarizeSpec`, and `validateSpec`\n * (the latter surfaces pre-broadcast shape violations).\n *\n * The single canonical spec type (`AppDeploySpec`, ENG-310) covers two\n * shapes, discriminated by the presence of `services`:\n * - **stack** — `{ services: { <name>: ServiceConfig }, customDomain?, serviceName?, size, ... }`\n * - **single-service** — `{ image, port?, env?, customDomain?, size, ... }`\n *\n * `normalizeServices` collapses the two shapes into a single iterable form\n * so callers (Plan summary, manifest builder, etc.) walk one structure\n * regardless of which form the user passed.\n *\n * Validation: `validateSpec` stays a `void` `TypeError`-thrower on shape\n * violations. The high-level `deployApp` entry wrappers\n * (`deploy-app.ts`) map that `TypeError` to\n * `ManifestMCPError(INVALID_CONFIG)` at the public-API boundary, so this\n * helper does not depend on core's error type directly.\n */\n\n/**\n * True when `spec` uses the services-map (stack) shape. Discriminates on\n * the canonical `services` key being present and defined (ENG-310). The\n * non-null-object / non-array guard is retained as defense-in-depth so an\n * `unknown`-cast caller smuggling `services: null` / `[]` / `'str'` is NOT\n * mistaken for a stack (which would crash `Object.entries` downstream).\n */\nexport function isStackSpec(\n spec: unknown,\n): spec is AppDeploySpec & { services: Record<string, ServiceConfig> } {\n if (spec === null || spec === undefined || typeof spec !== 'object')\n return false;\n const record = spec as { services?: unknown };\n if (!('services' in record) || record.services === undefined) return false;\n const services = record.services;\n return (\n services !== null &&\n typeof services === 'object' &&\n !Array.isArray(services)\n );\n}\n\n/**\n * Return the canonical first image string for a spec. For legacy single-\n * service: `spec.image`. For stack: the first non-empty `image` in\n * `Object.values(spec.services)`. Returns `null` when neither shape\n * carries an image (or `spec` is malformed).\n */\nexport function firstImage(spec: unknown): string | null {\n if (spec === null || spec === undefined || typeof spec !== 'object')\n return null;\n const single = spec as Partial<AppDeploySpec>;\n if (typeof single.image === 'string' && single.image.length > 0) {\n return single.image;\n }\n if (isStackSpec(spec)) {\n for (const svc of Object.values(spec.services)) {\n if (svc !== null && typeof svc === 'object') {\n const image = (svc as Partial<ServiceConfig>).image;\n if (typeof image === 'string' && image.length > 0) return image;\n }\n }\n }\n return null;\n}\n\n/**\n * Walk a spec as `[{name, raw}]` where:\n * - `name === null` for legacy single-service (only one entry, raw is the spec itself).\n * - `name === <key>` for each services-map entry; `raw` is the per-service ServiceConfig.\n *\n * Stable iteration order matches `Object.entries` (insertion order in v8/modern engines).\n */\nexport interface NormalizedService {\n /** `null` for legacy single-service; the services-map key for stack leases. */\n name: string | null;\n /** The per-service object exactly as the spec stores it. No field projection. */\n raw: ServiceConfig | AppDeploySpec;\n}\n\nexport function normalizeServices(spec: unknown): NormalizedService[] {\n if (isStackSpec(spec)) {\n return Object.entries(spec.services).map(([name, raw]) => ({\n name,\n raw: (raw ?? {}) as ServiceConfig,\n }));\n }\n return [\n {\n name: null,\n raw: (spec ?? {}) as AppDeploySpec,\n },\n ];\n}\n\n/**\n * Produce the frozen `SpecSummary` shape for inclusion in the `Plan`\n * (camelCase fields: `serviceCount`, etc.).\n *\n * Port count rules:\n * - single-service `port: number` → +1 port.\n * - `ServiceConfig.ports` map (canonical `{ '<port>/<proto>': {} }`) →\n * +key count.\n * - A bare `port`/`ports` array smuggled in by an `unknown`-cast caller\n * is still counted defensively (+1 / +length) below.\n *\n * Env key uniqueness is computed across services (one `env_keys` set\n * spans the whole spec); `envCount` is the size of that set; `envKeys`\n * is sorted ascending.\n */\nexport function summarizeSpec(spec: unknown): SpecSummary {\n const format: 'single' | 'stack' = isStackSpec(spec) ? 'stack' : 'single';\n const services = normalizeServices(spec);\n\n let portCount = 0;\n const envKeys = new Set<string>();\n const images: string[] = [];\n\n for (const { raw: svc } of services) {\n if (svc !== null && typeof svc === 'object') {\n const svcRecord = svc as unknown as Record<string, unknown>;\n const image = svcRecord.image;\n if (typeof image === 'string' && image.length > 0) images.push(image);\n\n const port = svcRecord.port;\n if (typeof port === 'number') portCount += 1;\n else if (Array.isArray(port)) portCount += port.length;\n\n const ports = svcRecord.ports;\n if (Array.isArray(ports)) {\n portCount += ports.length;\n } else if (ports !== null && typeof ports === 'object') {\n portCount += Object.keys(ports).length;\n }\n\n const env = svcRecord.env;\n if (env !== null && typeof env === 'object' && !Array.isArray(env)) {\n for (const k of Object.keys(env)) envKeys.add(k);\n }\n }\n }\n\n return {\n format,\n serviceCount: services.length,\n portCount,\n envCount: envKeys.size,\n envKeys: Array.from(envKeys).sort(),\n images,\n };\n}\n\n/**\n * Validate an `AppDeploySpec` shape pre-broadcast. Throws `TypeError` on\n * the first violation. The canonical type already enforces most\n * structural rules at compile time; this runtime check defends against\n * `unknown`-cast callers and `JSON.parse`-decoded inputs (and the\n * image-XOR-services invariant the type cannot express).\n *\n * Rules (mirror fred's `deployApp.ts` input validation):\n * - `spec` must be a non-null object.\n * - Stack: `services` must have ≥1 entry; each entry's `image` must be a\n * non-empty string.\n * - Single: `image` must be a non-empty string.\n * - Mutually exclusive `image` AND `services` not allowed.\n *\n * The high-level `deployApp` in PR 3 layers domain checks on top\n * (`customDomain` shape, `serviceName` membership, etc.).\n */\nexport function validateSpec(spec: unknown): void {\n if (spec === null || spec === undefined || typeof spec !== 'object') {\n throw new TypeError('validateSpec: spec must be a non-null object');\n }\n const record = spec as unknown as Record<string, unknown>;\n\n // Mutual-exclusion gate uses KEY presence (not value validity). This\n // closes the bypass where a caller supplies a malformed `image` value\n // (empty string, number, null) alongside a valid `services` map: the\n // value-based check would silently treat `image` as \"absent\" and accept\n // the spec, but the caller's intent was ambiguous (which shape did they\n // mean?). Rejecting on key-presence forces the caller to delete one key\n // before submission and removes the ambiguity.\n const hasImageKey = 'image' in record;\n const hasServicesKey = 'services' in record;\n if (hasImageKey && hasServicesKey) {\n throw new TypeError(\n 'validateSpec: spec has both `image` and `services` keys; these are mutually exclusive (regardless of value validity)',\n );\n }\n\n // Downstream value-validity check (after the mutual-exclusion gate has\n // ruled out the ambiguous case). An `image` key with a non-string or\n // empty-string value still fails here when `services` is absent.\n const hasImage = typeof record.image === 'string' && record.image.length > 0;\n const hasServices = isStackSpec(spec);\n if (!hasImage && !hasServices) {\n throw new TypeError(\n 'validateSpec: spec must declare either `image` (single-service) or `services` (stack)',\n );\n }\n\n // Copilot review fix (PR #58 r3266786899): `customDomain` shape at\n // the boundary. Historically the orchestrator's `buildFredDeployInput`\n // mapper used an `if (customDomain)` truthiness check that silently\n // dropped `''`, `null`, `false`, `0`, `NaN` from the emitted\n // `fredInput`, so a spec like `{ ..., customDomain: '' }` deployed with\n // the domain silently not claimed. ENG-310 deleted that mapper: the\n // loss-free broadcast spread now forwards `customDomain` to fred\n // verbatim, so an empty/whitespace value would instead reach fred as a\n // claim for an invalid domain. Either way the fix is the same — reject\n // a malformed `customDomain` here, at the boundary, before it can do harm.\n //\n // Boundary check: when `customDomain` is present, it must be a\n // non-empty string. `undefined` (key absent) is fine; that's the\n // \"no domain requested\" case. Fires before the stack-customDomain\n // serviceName check (r3249684707) so the user gets a clear\n // customDomain-shape error rather than a misleading\n // requires-serviceName one.\n // Copilot review fix (PR #58 r3267373001): reject whitespace-only\n // strings AND strings with surrounding whitespace (option (i) from\n // the team-lead's brief — strict; let the caller send a clean,\n // already-trimmed value rather than silently trim for them). The\n // prior `cd.length === 0` predicate accepted `' '`, `'\\t\\n'`,\n // and `' app.example.com '`; fred would either accept the\n // surrounding whitespace as part of the domain (correctness bug)\n // or trim-and-reject (worse UX than agent-core's clear error).\n if ('customDomain' in record) {\n const cd = record.customDomain;\n if (cd !== undefined) {\n const isCleanNonEmptyString =\n typeof cd === 'string' && cd.length > 0 && cd.trim() === cd;\n if (!isCleanNonEmptyString) {\n const got =\n typeof cd === 'string'\n ? cd.trim().length === 0\n ? `\"${cd}\"`\n : `\"${cd}\" (has surrounding whitespace)`\n : cd === null\n ? 'null'\n : typeof cd;\n throw new TypeError(\n `validateSpec: \\`customDomain\\` must be a non-empty trimmed string or absent (got ${got}).`,\n );\n }\n }\n }\n\n if (hasServices) {\n const entries = Object.entries(spec.services);\n if (entries.length === 0) {\n throw new TypeError(\n 'validateSpec: stack spec `services` must have at least one entry',\n );\n }\n for (const [name, svc] of entries) {\n if (svc === null || typeof svc !== 'object') {\n throw new TypeError(\n `validateSpec: stack service \"${name}\" must be a non-null object`,\n );\n }\n const image = (svc as Partial<ServiceConfig>).image;\n if (typeof image !== 'string' || image.length === 0) {\n throw new TypeError(\n `validateSpec: stack service \"${name}\" must declare a non-empty \\`image\\` string`,\n );\n }\n }\n\n // Copilot review fix (PR #58 r3249684707): a stack spec with a\n // `customDomain` MUST declare which service receives the domain\n // via `serviceName`, and that value must be a key in `services`.\n // Without this guard, `customDomainServiceOf` in `deploy-app.ts`\n // returns `undefined`, planning proceeds with no target, renderers\n // misrepresent the claim, and fred rejects the set-domain tx\n // ONLY after `create-lease` commits — leaving the user with an\n // orphan lease + a failed domain claim. Catching this at\n // validate-time is fail-fast at the boundary.\n //\n // Single-service specs are unaffected: their `customDomain` is\n // claimed against the implicit single lease item — no\n // serviceName disambiguation needed.\n const stackDomain = (spec as Partial<AppDeploySpec>).customDomain;\n if (typeof stackDomain === 'string' && stackDomain.length > 0) {\n const stackServiceName = (spec as Partial<AppDeploySpec>).serviceName;\n if (\n typeof stackServiceName !== 'string' ||\n stackServiceName.length === 0\n ) {\n throw new TypeError(\n 'validateSpec: stack spec with `customDomain` requires `serviceName` identifying which service receives the domain.',\n );\n }\n // Copilot review fix (PR #58 r3250331968): use an own-key check.\n // The `in` operator walks the prototype chain, so `serviceName:\n // 'constructor'` (or `'toString'`, `'hasOwnProperty'`, etc.)\n // would falsely pass against a `services` map that doesn't\n // declare those names. Mirrors fred's own choice at\n // `packages/fred/src/tools/deployApp.ts:254` for cross-package\n // symmetry. `Object.keys().includes()` (not `Object.hasOwn`,\n // which is ES2022 and our `tsdown.config.ts` targets ES2020).\n if (!Object.keys(spec.services).includes(stackServiceName)) {\n throw new TypeError(\n `validateSpec: stack spec \\`serviceName\\` \"${stackServiceName}\" must be a key in \\`services\\` (got services: [${Object.keys(spec.services).join(', ')}]).`,\n );\n }\n }\n } else {\n // Single-service spec port requirement.\n //\n // Copilot review fix (PR #58 r3249097051): fred's image-mode rejects\n // portless inputs with `port is required when using image`\n // (`packages/fred/src/tools/deployApp.ts:202` +\n // `packages/fred/src/tools/buildManifestPreview.ts:181`). Without\n // an agent-core boundary check the orchestrator would pass\n // `port: undefined` straight through the loss-free broadcast spread\n // to fred, surfacing fred's error mid-orchestration (after readiness\n // check + plan render). Failing fast at validate time produces a\n // clearer message and avoids partial work.\n //\n // The escape hatch for genuinely internal-only services is the\n // stack spec — service-level `ports` is optional, so a stack with\n // `{ services: { mysvc: { image, env } } }` deploys without ports.\n //\n // Copilot review fix (PR #58 r3249294877): tighten the predicate to\n // a finite positive integer in the TCP port range. The prior\n // `typeof p === 'number'` check accepted `0`, `NaN`, `Infinity`,\n // negative numbers, non-integers, and out-of-range ports —\n // partially defeating the fail-fast intent. Fred catches `port: 0`\n // via `!input.port`, but the other shapes either flow through to a\n // less helpful error or get coerced silently. The shared predicate\n // `isValidPortNumber` (below) is the single source of truth.\n // Read as `unknown`: the canonical AppDeploySpec.port is `number`, but\n // this runtime guard also accepts a legacy `number[]` smuggled in by an\n // `unknown`-cast / JSON.parse caller (the array branch below) — typing it\n // `unknown` keeps `Array.isArray` from narrowing to `never` (ENG-310).\n const port: unknown = (spec as { port?: unknown }).port;\n const hasValidPort =\n isValidPortNumber(port) ||\n (Array.isArray(port) && port.length > 0 && port.every(isValidPortNumber));\n if (!hasValidPort) {\n throw new TypeError(\n 'validateSpec: single-service specs require at least one port (port must be a finite positive integer in the TCP range (1-65535), or a non-empty array of such); got ' +\n `port=${JSON.stringify(port)}. For internal-only services, use a stack spec instead.`,\n );\n }\n }\n}\n\n/**\n * Predicate: `p` is a finite positive integer in the TCP port range\n * (1-65535). Used by `validateSpec` to gate single-service `port`\n * shapes against the broad `typeof === 'number'` bypass.\n *\n * Co-located in this module because it's exclusive to the port-\n * validation boundary; if a future caller needs the same check,\n * promote it to a shared utility then.\n */\nfunction isValidPortNumber(p: unknown): p is number {\n return typeof p === 'number' && Number.isInteger(p) && p > 0 && p <= 65535;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-domain-state.d.ts","names":[],"sources":["../../src/internals/verify-domain-state.ts"],"mappings":";KA8BY
|
|
1
|
+
{"version":3,"file":"verify-domain-state.d.ts","names":[],"sources":["../../src/internals/verify-domain-state.ts"],"mappings":";KA8BY;UAEK;EACf,SAAS;;EAET;;EAEA;;UAGe;EACf;;EAEA;;EAEA;;iBAGc,kBACd,wBACA,MAAM,mBACL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-domain-state.js","names":[],"sources":["../../src/internals/verify-domain-state.ts"],"sourcesContent":["import { findLease, normalizeItem } from './lease-items.js';\n\n/**\n * Verify a lease item's `customDomain` against an expected value after a\n * `set_item_custom_domain` broadcast.\n *\n * Decodes the same lease shape as `lease-items.ts`, then compares the\n * matched item's `customDomain` to the expected FQDN (or empty string for\n * clear-mode). Used by the in-process `verifyAndRecover` driver in PR 1\n * and by the high-level `manageDomain` set/clear flows in PR 4.\n *\n * Outcome semantics:\n * - `'match'` — actual `customDomain` equals expected\n * - `'mismatch'` — actual differs from expected (item carries `actual` for surfacing)\n * - `'not_found'` — lease UUID not present in the verification payload, OR multi-item lease but no `serviceName` supplied, OR `serviceName` not present in the lease's items\n *\n * Single-item leases (legacy 1-item lease with `serviceName === ''`) ignore\n * the `serviceName` argument and always use the only item. Multi-item\n * stack leases require `serviceName` to address the target item.\n *\n * Throws `TypeError` for malformed args (non-string leaseUuid, leaseUuid\n * that doesn't match UUID grammar). The CJS exits 1 via stderr; the TS\n * port surfaces a typed error instead of a synthetic `not_found` result\n * so caller-side argument bugs don't masquerade as a chain-state outcome.\n */\n\n/** Anchored UUID-shape regex (8-4-4-4-12, version-byte lenient — matches `_uuid.cjs#UUID_RE`). */\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\nexport type VerifyDomainOutcome = 'match' | 'mismatch' | 'not_found';\n\nexport interface VerifyDomainResult {\n outcome: VerifyDomainOutcome;\n /** Present when outcome is 'match' or 'mismatch'. The lease item's actual customDomain. */\n actual?: string;\n /** Present when outcome is 'not_found'. Human-readable detail. */\n reason?: string;\n}\n\nexport interface VerifyDomainArgs {\n leaseUuid: string;\n /** DNS label addressing an item inside a stack lease. Omit / leave empty for legacy 1-item leases. */\n serviceName?: string;\n /** FQDN to compare against the chain's stored value. Use '' for clear-mode (post-clear verification). */\n expected: string;\n}\n\nexport function verifyDomainState(\n leasesPayload: unknown,\n args: VerifyDomainArgs,\n): VerifyDomainResult {\n if (typeof args.leaseUuid !== 'string') {\n throw new TypeError(\n `verifyDomainState: leaseUuid must be a string, got ${typeof args.leaseUuid}`,\n );\n }\n if (!UUID_RE.test(args.leaseUuid)) {\n throw new TypeError(\n `verifyDomainState: leaseUuid must be a UUID; got \"${args.leaseUuid}\"`,\n );\n }\n if (typeof args.expected !== 'string') {\n throw new TypeError(\n `verifyDomainState: expected must be a string (use '' for clear-mode), got ${typeof args.expected}`,\n );\n }\n\n const lease = findLease(leasesPayload, args.leaseUuid);\n if (lease === null) {\n return {\n outcome: 'not_found',\n reason: 'lease UUID not found in verification payload',\n };\n }\n\n // The lease shape is opaque to TS — pickLeasesArray + findLease validate\n // structural keys but the items array can be missing or non-array.\n const rawItems = (lease as { items?: unknown }).items;\n const itemsArray = Array.isArray(rawItems) ? rawItems : [];\n const items = itemsArray.map(normalizeItem);\n\n const singleItem = items.length === 1 && items[0]?.serviceName === '';\n const requestedService = (args.serviceName ?? '').trim();\n\n let item: ReturnType<typeof normalizeItem> | undefined;\n if (singleItem) {\n item = items[0];\n } else if (requestedService === '') {\n return {\n outcome: 'not_found',\n reason: 'lease has multiple items but --service-name was not supplied',\n };\n } else {\n item = items.find((i) => i.serviceName === requestedService);\n if (!item) {\n return {\n outcome: 'not_found',\n reason: `service-name \"${requestedService}\" not found in lease items`,\n };\n }\n }\n\n const actual = item?.customDomain ?? '';\n const outcome: VerifyDomainOutcome =\n actual === args.expected ? 'match' : 'mismatch';\n return { outcome, actual };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,UACJ;AAoBF,SAAgB,kBACd,eACA,MACoB;CACpB,IAAI,OAAO,KAAK,cAAc,UAC5B,MAAM,IAAI,UACR,sDAAsD,OAAO,KAAK,WACpE;CAEF,IAAI,CAAC,QAAQ,KAAK,KAAK,SAAS,GAC9B,MAAM,IAAI,UACR,qDAAqD,KAAK,UAAU,EACtE;CAEF,IAAI,OAAO,KAAK,aAAa,UAC3B,MAAM,IAAI,UACR,6EAA6E,OAAO,KAAK,UAC3F;CAGF,MAAM,QAAQ,UAAU,eAAe,KAAK,SAAS;CACrD,IAAI,UAAU,MACZ,OAAO;EACL,SAAS;EACT,QAAQ;CACV;CAKF,MAAM,WAAY,MAA8B;CAEhD,MAAM,SADa,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"verify-domain-state.js","names":[],"sources":["../../src/internals/verify-domain-state.ts"],"sourcesContent":["import { findLease, normalizeItem } from './lease-items.js';\n\n/**\n * Verify a lease item's `customDomain` against an expected value after a\n * `set_item_custom_domain` broadcast.\n *\n * Decodes the same lease shape as `lease-items.ts`, then compares the\n * matched item's `customDomain` to the expected FQDN (or empty string for\n * clear-mode). Used by the in-process `verifyAndRecover` driver in PR 1\n * and by the high-level `manageDomain` set/clear flows in PR 4.\n *\n * Outcome semantics:\n * - `'match'` — actual `customDomain` equals expected\n * - `'mismatch'` — actual differs from expected (item carries `actual` for surfacing)\n * - `'not_found'` — lease UUID not present in the verification payload, OR multi-item lease but no `serviceName` supplied, OR `serviceName` not present in the lease's items\n *\n * Single-item leases (legacy 1-item lease with `serviceName === ''`) ignore\n * the `serviceName` argument and always use the only item. Multi-item\n * stack leases require `serviceName` to address the target item.\n *\n * Throws `TypeError` for malformed args (non-string leaseUuid, leaseUuid\n * that doesn't match UUID grammar). The CJS exits 1 via stderr; the TS\n * port surfaces a typed error instead of a synthetic `not_found` result\n * so caller-side argument bugs don't masquerade as a chain-state outcome.\n */\n\n/** Anchored UUID-shape regex (8-4-4-4-12, version-byte lenient — matches `_uuid.cjs#UUID_RE`). */\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\nexport type VerifyDomainOutcome = 'match' | 'mismatch' | 'not_found';\n\nexport interface VerifyDomainResult {\n outcome: VerifyDomainOutcome;\n /** Present when outcome is 'match' or 'mismatch'. The lease item's actual customDomain. */\n actual?: string;\n /** Present when outcome is 'not_found'. Human-readable detail. */\n reason?: string;\n}\n\nexport interface VerifyDomainArgs {\n leaseUuid: string;\n /** DNS label addressing an item inside a stack lease. Omit / leave empty for legacy 1-item leases. */\n serviceName?: string;\n /** FQDN to compare against the chain's stored value. Use '' for clear-mode (post-clear verification). */\n expected: string;\n}\n\nexport function verifyDomainState(\n leasesPayload: unknown,\n args: VerifyDomainArgs,\n): VerifyDomainResult {\n if (typeof args.leaseUuid !== 'string') {\n throw new TypeError(\n `verifyDomainState: leaseUuid must be a string, got ${typeof args.leaseUuid}`,\n );\n }\n if (!UUID_RE.test(args.leaseUuid)) {\n throw new TypeError(\n `verifyDomainState: leaseUuid must be a UUID; got \"${args.leaseUuid}\"`,\n );\n }\n if (typeof args.expected !== 'string') {\n throw new TypeError(\n `verifyDomainState: expected must be a string (use '' for clear-mode), got ${typeof args.expected}`,\n );\n }\n\n const lease = findLease(leasesPayload, args.leaseUuid);\n if (lease === null) {\n return {\n outcome: 'not_found',\n reason: 'lease UUID not found in verification payload',\n };\n }\n\n // The lease shape is opaque to TS — pickLeasesArray + findLease validate\n // structural keys but the items array can be missing or non-array.\n const rawItems = (lease as { items?: unknown }).items;\n const itemsArray = Array.isArray(rawItems) ? rawItems : [];\n const items = itemsArray.map(normalizeItem);\n\n const singleItem = items.length === 1 && items[0]?.serviceName === '';\n const requestedService = (args.serviceName ?? '').trim();\n\n let item: ReturnType<typeof normalizeItem> | undefined;\n if (singleItem) {\n item = items[0];\n } else if (requestedService === '') {\n return {\n outcome: 'not_found',\n reason: 'lease has multiple items but --service-name was not supplied',\n };\n } else {\n item = items.find((i) => i.serviceName === requestedService);\n if (!item) {\n return {\n outcome: 'not_found',\n reason: `service-name \"${requestedService}\" not found in lease items`,\n };\n }\n }\n\n const actual = item?.customDomain ?? '';\n const outcome: VerifyDomainOutcome =\n actual === args.expected ? 'match' : 'mismatch';\n return { outcome, actual };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,UACJ;AAoBF,SAAgB,kBACd,eACA,MACoB;CACpB,IAAI,OAAO,KAAK,cAAc,UAC5B,MAAM,IAAI,UACR,sDAAsD,OAAO,KAAK,WACpE;CAEF,IAAI,CAAC,QAAQ,KAAK,KAAK,SAAS,GAC9B,MAAM,IAAI,UACR,qDAAqD,KAAK,UAAU,EACtE;CAEF,IAAI,OAAO,KAAK,aAAa,UAC3B,MAAM,IAAI,UACR,6EAA6E,OAAO,KAAK,UAC3F;CAGF,MAAM,QAAQ,UAAU,eAAe,KAAK,SAAS;CACrD,IAAI,UAAU,MACZ,OAAO;EACL,SAAS;EACT,QAAQ;CACV;CAKF,MAAM,WAAY,MAA8B;CAEhD,MAAM,SADa,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,EAAA,CAChC,IAAI,aAAa;CAE1C,MAAM,aAAa,MAAM,WAAW,KAAK,MAAM,EAAE,EAAE,gBAAgB;CACnE,MAAM,oBAAoB,KAAK,eAAe,GAAA,CAAI,KAAK;CAEvD,IAAI;CACJ,IAAI,YACF,OAAO,MAAM;MACR,IAAI,qBAAqB,IAC9B,OAAO;EACL,SAAS;EACT,QAAQ;CACV;MACK;EACL,OAAO,MAAM,MAAM,MAAM,EAAE,gBAAgB,gBAAgB;EAC3D,IAAI,CAAC,MACH,OAAO;GACL,SAAS;GACT,QAAQ,iBAAiB,iBAAiB;EAC5C;CAEJ;CAEA,MAAM,SAAS,MAAM,gBAAgB;CAGrC,OAAO;EAAE,SADP,WAAW,KAAK,WAAW,UAAU;EACrB;CAAO;AAC3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-recover.d.ts","names":[],"sources":["../../src/internals/verify-recover.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify-recover.d.ts","names":[],"sources":["../../src/internals/verify-recover.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwCY;;;;;;UAaK,mBAAmB,QAAQ;;WAEjC,UAAU;;WAEV;;EAET,uBAAuB,YAAY,UAAU;;;;;;;;EAQ7C,uBAAuB,YAAY,UAAU;;;KAInC,SACV,UACA,yBACA,QAAQ,4BACL,SAAS,aAAa,QAAQ,eAAe,UAAU;;UAG3C,eAAe,yBAAyB;EACvD,SAAS;EACT,YAAY;;;;;;;;;;UAWG,iBACf,UACA,yBACA,QAAQ;WAEC,UAAU,SAAS,UAAU,UAAU;;WAEvC,wBAAwB;;WAExB,UAAU,QACjB,OAAO,wBAAwB,mBAAmB;;UAIrC,uBACf,yBACA,QAAQ;EAER;EACA,iBAAiB;;EAEjB,UAAU;EACV;;EAEA,YAAY;;EAEZ,UAAU;;EAEV,iBAAiB;;UAGF;;;;;;;;;EASf,aACE,SAAS,iBACT,SAAS,qBACN,QAAQ;;;;;;;;;;;;;;;iBAgBO,iBACpB,UACA,yBACA,QAAQ,yBAER,MAAM,iBAAiB,UAAU,UAAU,QAC3C,SAAS,UACT,YAAW,4BACV,QAAQ,uBAAuB,UAAU"}
|
package/dist/manage-domain.d.ts
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqGsB,aACpB,MAAM,kBACN,WAAW,uBACX,MAAM,sBACL,QAAQ"}
|