@manifest-network/manifest-agent-core 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +39 -0
  2. package/dist/close-lease.d.ts +33 -0
  3. package/dist/close-lease.d.ts.map +1 -0
  4. package/dist/close-lease.js +138 -0
  5. package/dist/close-lease.js.map +1 -0
  6. package/dist/deploy-app.d.ts +24 -0
  7. package/dist/deploy-app.d.ts.map +1 -0
  8. package/dist/deploy-app.js +446 -0
  9. package/dist/deploy-app.js.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.js +7 -0
  12. package/dist/internals/classify-deploy-error.d.ts +41 -0
  13. package/dist/internals/classify-deploy-error.d.ts.map +1 -0
  14. package/dist/internals/classify-deploy-error.js +79 -0
  15. package/dist/internals/classify-deploy-error.js.map +1 -0
  16. package/dist/internals/classify-deploy-response.d.ts +56 -0
  17. package/dist/internals/classify-deploy-response.d.ts.map +1 -0
  18. package/dist/internals/classify-deploy-response.js +33 -0
  19. package/dist/internals/classify-deploy-response.js.map +1 -0
  20. package/dist/internals/connection.d.ts +76 -0
  21. package/dist/internals/connection.d.ts.map +1 -0
  22. package/dist/internals/connection.js +94 -0
  23. package/dist/internals/connection.js.map +1 -0
  24. package/dist/internals/evaluate-readiness.d.ts +55 -0
  25. package/dist/internals/evaluate-readiness.d.ts.map +1 -0
  26. package/dist/internals/evaluate-readiness.js +131 -0
  27. package/dist/internals/evaluate-readiness.js.map +1 -0
  28. package/dist/internals/find-sku-uuid.d.ts +40 -0
  29. package/dist/internals/find-sku-uuid.d.ts.map +1 -0
  30. package/dist/internals/find-sku-uuid.js +20 -0
  31. package/dist/internals/find-sku-uuid.js.map +1 -0
  32. package/dist/internals/format-success.d.ts +35 -0
  33. package/dist/internals/format-success.d.ts.map +1 -0
  34. package/dist/internals/format-success.js +80 -0
  35. package/dist/internals/format-success.js.map +1 -0
  36. package/dist/internals/guarded-fetch.d.ts +138 -0
  37. package/dist/internals/guarded-fetch.d.ts.map +1 -0
  38. package/dist/internals/guarded-fetch.js +242 -0
  39. package/dist/internals/guarded-fetch.js.map +1 -0
  40. package/dist/internals/humanize-denom.d.ts +45 -0
  41. package/dist/internals/humanize-denom.d.ts.map +1 -0
  42. package/dist/internals/humanize-denom.js +105 -0
  43. package/dist/internals/humanize-denom.js.map +1 -0
  44. package/dist/internals/inspect-image.d.ts +31 -0
  45. package/dist/internals/inspect-image.d.ts.map +1 -0
  46. package/dist/internals/inspect-image.js +345 -0
  47. package/dist/internals/inspect-image.js.map +1 -0
  48. package/dist/internals/lease-items.d.ts +46 -0
  49. package/dist/internals/lease-items.d.ts.map +1 -0
  50. package/dist/internals/lease-items.js +58 -0
  51. package/dist/internals/lease-items.js.map +1 -0
  52. package/dist/internals/lease-state.d.ts +32 -0
  53. package/dist/internals/lease-state.d.ts.map +1 -0
  54. package/dist/internals/lease-state.js +80 -0
  55. package/dist/internals/lease-state.js.map +1 -0
  56. package/dist/internals/render-deployment-plan.d.ts +22 -0
  57. package/dist/internals/render-deployment-plan.d.ts.map +1 -0
  58. package/dist/internals/render-deployment-plan.js +135 -0
  59. package/dist/internals/render-deployment-plan.js.map +1 -0
  60. package/dist/internals/render-intent-recap.d.ts +43 -0
  61. package/dist/internals/render-intent-recap.d.ts.map +1 -0
  62. package/dist/internals/render-intent-recap.js +136 -0
  63. package/dist/internals/render-intent-recap.js.map +1 -0
  64. package/dist/internals/render-partial-success-prompt.d.ts +26 -0
  65. package/dist/internals/render-partial-success-prompt.d.ts.map +1 -0
  66. package/dist/internals/render-partial-success-prompt.js +53 -0
  67. package/dist/internals/render-partial-success-prompt.js.map +1 -0
  68. package/dist/internals/save-manifest.d.ts +105 -0
  69. package/dist/internals/save-manifest.d.ts.map +1 -0
  70. package/dist/internals/save-manifest.js +122 -0
  71. package/dist/internals/save-manifest.js.map +1 -0
  72. package/dist/internals/secret-denylist.d.ts +42 -0
  73. package/dist/internals/secret-denylist.d.ts.map +1 -0
  74. package/dist/internals/secret-denylist.js +59 -0
  75. package/dist/internals/secret-denylist.js.map +1 -0
  76. package/dist/internals/spec-normalize.d.ts +84 -0
  77. package/dist/internals/spec-normalize.d.ts.map +1 -0
  78. package/dist/internals/spec-normalize.js +169 -0
  79. package/dist/internals/spec-normalize.js.map +1 -0
  80. package/dist/internals/verify-domain-state.d.ts +20 -0
  81. package/dist/internals/verify-domain-state.d.ts.map +1 -0
  82. package/dist/internals/verify-domain-state.js +63 -0
  83. package/dist/internals/verify-domain-state.js.map +1 -0
  84. package/dist/internals/verify-recover.d.ts +120 -0
  85. package/dist/internals/verify-recover.d.ts.map +1 -0
  86. package/dist/internals/verify-recover.js +91 -0
  87. package/dist/internals/verify-recover.js.map +1 -0
  88. package/dist/manage-domain.d.ts +36 -0
  89. package/dist/manage-domain.d.ts.map +1 -0
  90. package/dist/manage-domain.js +230 -0
  91. package/dist/manage-domain.js.map +1 -0
  92. package/dist/troubleshoot.d.ts +23 -0
  93. package/dist/troubleshoot.d.ts.map +1 -0
  94. package/dist/troubleshoot.js +124 -0
  95. package/dist/troubleshoot.js.map +1 -0
  96. package/dist/types.d.ts +294 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +0 -0
  99. package/package.json +56 -0
@@ -0,0 +1,40 @@
1
+ import { CosmosClientManager } from "@manifest-network/manifest-mcp-core";
2
+
3
+ //#region src/internals/find-sku-uuid.d.ts
4
+ /**
5
+ * Resolve a SKU-tier name (e.g. `'docker-micro'`, `'small'`) to its on-chain
6
+ * UUID + the UUID of its publishing provider. Mirrors fred's internal
7
+ * `findSkuUuid` helper at `packages/fred/src/tools/deployApp.ts:L62`.
8
+ *
9
+ * Used by `deploy-app.ts`'s pre-broadcast fee-estimation step (the
10
+ * `cosmosEstimateFee('billing', 'create-lease', ...)` call needs the SKU
11
+ * UUID as part of the item-arg string format `sku-uuid:quantity[:service-name]`).
12
+ *
13
+ * **Why duplicated (not imported from fred):** fred's `findSkuUuid` is an
14
+ * internal helper, not re-exported from `packages/fred/src/index.ts`.
15
+ * Per team-lead-2's Path 2 verdict (vs Path 1 export-from-fred), the
16
+ * helper is duplicated here because:
17
+ *
18
+ * 1. Stateless query helper — zero cross-instance drift risk
19
+ * (unlike `AuthTimestampTracker` which has cross-call state).
20
+ * 2. fred's barrel deliberately keeps this helper internal; making it
21
+ * public would be an architectural decision, not an oversight fix.
22
+ * 3. The duplicated logic uses only already-exported core symbols
23
+ * (`createPagination`, `MAX_PAGE_LIMIT`, `ManifestMCPError`).
24
+ * 4. Drift bounded by the shared `@manifest-network/manifestjs@2.4.1`
25
+ * proto pin — both fred and agent-core import the same SKU types.
26
+ *
27
+ * Throws `ManifestMCPError(QUERY_FAILED)` when no active SKU matches
28
+ * the requested `size`. Error message includes the available SKU names
29
+ * for caller-side debugging.
30
+ */
31
+ interface SkuResolution {
32
+ /** On-chain SKU UUID. Used in `create-lease` item-arg construction. */
33
+ readonly skuUuid: string;
34
+ /** Publishing provider's UUID. Required by some downstream chain queries. */
35
+ readonly providerUuid: string;
36
+ }
37
+ declare function findSkuUuid(clientManager: CosmosClientManager, size: string): Promise<SkuResolution>;
38
+ //#endregion
39
+ export { SkuResolution, findSkuUuid };
40
+ //# sourceMappingURL=find-sku-uuid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-sku-uuid.d.ts","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"mappings":";;;;;AAmCA;;;;;AAOA;;;;;;;;;;;;;;;;;;;;UAPiB,aAAA;;WAEN,OAAA;;WAEA,YAAA;AAAA;AAAA,iBAGW,WAAA,CACpB,aAAA,EAAe,mBAAA,EACf,IAAA,WACC,OAAA,CAAQ,aAAA"}
@@ -0,0 +1,20 @@
1
+ import { MAX_PAGE_LIMIT, ManifestMCPError, ManifestMCPErrorCode, createPagination } from "@manifest-network/manifest-mcp-core";
2
+ //#region src/internals/find-sku-uuid.ts
3
+ async function findSkuUuid(clientManager, size) {
4
+ const queryClient = await clientManager.getQueryClient();
5
+ const pagination = createPagination(MAX_PAGE_LIMIT);
6
+ const result = await queryClient.liftedinit.sku.v1.sKUs({
7
+ activeOnly: true,
8
+ pagination
9
+ });
10
+ for (const sku of result.skus) if (sku.name === size) return {
11
+ skuUuid: sku.uuid,
12
+ providerUuid: sku.providerUuid
13
+ };
14
+ const available = result.skus.map((s) => s.name);
15
+ throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, `SKU tier "${size}" not found. Available: ${available.join(", ")}`);
16
+ }
17
+ //#endregion
18
+ export { findSkuUuid };
19
+
20
+ //# sourceMappingURL=find-sku-uuid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-sku-uuid.js","names":[],"sources":["../../src/internals/find-sku-uuid.ts"],"sourcesContent":["import {\n type CosmosClientManager,\n createPagination,\n MAX_PAGE_LIMIT,\n ManifestMCPError,\n ManifestMCPErrorCode,\n} from '@manifest-network/manifest-mcp-core';\n\n/**\n * Resolve a SKU-tier name (e.g. `'docker-micro'`, `'small'`) to its on-chain\n * UUID + the UUID of its publishing provider. Mirrors fred's internal\n * `findSkuUuid` helper at `packages/fred/src/tools/deployApp.ts:L62`.\n *\n * Used by `deploy-app.ts`'s pre-broadcast fee-estimation step (the\n * `cosmosEstimateFee('billing', 'create-lease', ...)` call needs the SKU\n * UUID as part of the item-arg string format `sku-uuid:quantity[:service-name]`).\n *\n * **Why duplicated (not imported from fred):** fred's `findSkuUuid` is an\n * internal helper, not re-exported from `packages/fred/src/index.ts`.\n * Per team-lead-2's Path 2 verdict (vs Path 1 export-from-fred), the\n * helper is duplicated here because:\n *\n * 1. Stateless query helper — zero cross-instance drift risk\n * (unlike `AuthTimestampTracker` which has cross-call state).\n * 2. fred's barrel deliberately keeps this helper internal; making it\n * public would be an architectural decision, not an oversight fix.\n * 3. The duplicated logic uses only already-exported core symbols\n * (`createPagination`, `MAX_PAGE_LIMIT`, `ManifestMCPError`).\n * 4. Drift bounded by the shared `@manifest-network/manifestjs@2.4.1`\n * proto pin — both fred and agent-core import the same SKU types.\n *\n * Throws `ManifestMCPError(QUERY_FAILED)` when no active SKU matches\n * the requested `size`. Error message includes the available SKU names\n * for caller-side debugging.\n */\nexport interface SkuResolution {\n /** On-chain SKU UUID. Used in `create-lease` item-arg construction. */\n readonly skuUuid: string;\n /** Publishing provider's UUID. Required by some downstream chain queries. */\n readonly providerUuid: string;\n}\n\nexport async function findSkuUuid(\n clientManager: CosmosClientManager,\n size: string,\n): Promise<SkuResolution> {\n const queryClient = await clientManager.getQueryClient();\n const pagination = createPagination(MAX_PAGE_LIMIT);\n const result = await queryClient.liftedinit.sku.v1.sKUs({\n activeOnly: true,\n pagination,\n });\n\n for (const sku of result.skus) {\n if (sku.name === size) {\n return { skuUuid: sku.uuid, providerUuid: sku.providerUuid };\n }\n }\n\n const available = result.skus.map((s) => s.name);\n throw new ManifestMCPError(\n ManifestMCPErrorCode.QUERY_FAILED,\n `SKU tier \"${size}\" not found. Available: ${available.join(', ')}`,\n );\n}\n"],"mappings":";;AA0CA,eAAsB,YACpB,eACA,MACwB;CACxB,MAAM,cAAc,MAAM,cAAc,gBAAgB;CACxD,MAAM,aAAa,iBAAiB,eAAe;CACnD,MAAM,SAAS,MAAM,YAAY,WAAW,IAAI,GAAG,KAAK;EACtD,YAAY;EACZ;EACD,CAAC;AAEF,MAAK,MAAM,OAAO,OAAO,KACvB,KAAI,IAAI,SAAS,KACf,QAAO;EAAE,SAAS,IAAI;EAAM,cAAc,IAAI;EAAc;CAIhE,MAAM,YAAY,OAAO,KAAK,KAAK,MAAM,EAAE,KAAK;AAChD,OAAM,IAAI,iBACR,qBAAqB,cACrB,aAAa,KAAK,0BAA0B,UAAU,KAAK,KAAK,GACjE"}
@@ -0,0 +1,35 @@
1
+ import { RunningEndpoint } from "./connection.js";
2
+
3
+ //#region src/internals/format-success.d.ts
4
+ /** Deploy response shape consumed by `formatSuccess`. Subset of fred's `mcp__manifest-fred__deploy_app` response. */
5
+ interface DeployResponse {
6
+ /** Chain lease state — int (raw) or `LEASE_STATE_*` string (codec). */
7
+ state?: number | string;
8
+ /** Provider UUID — rendered as-is (catalog has no friendly-name field). */
9
+ provider_uuid?: string;
10
+ /** Custom domain attached to the lease item; populated when set-domain confirmed. */
11
+ custom_domain?: string;
12
+ /** Provider connection payload — walked by `connection.ts`. */
13
+ connection?: unknown;
14
+ /**
15
+ * Top-level URL — legacy fallback when provider reports
16
+ * `connection.host` / `connection.ports` shape rather than
17
+ * `connection.instances`. Used when no FQDN can be extracted from
18
+ * `connection`. Defensive: `classify-deploy-response.ts:43-51,76-80`
19
+ * already defends against the same legacy shape; mirroring here keeps
20
+ * the renderer consistent with the classifier so a fred response of
21
+ * `{ url: 'https://app.example.com/' }` renders an Ingress line
22
+ * rather than `(none …)`.
23
+ */
24
+ url?: string;
25
+ }
26
+ interface FormatSuccessInput {
27
+ /** Validated lease UUID (RFC 4122 v1-v5). */
28
+ leaseUuid: string;
29
+ /** Deploy response from `deploy_app` (or equivalent atomic broadcast). */
30
+ deployResponse: DeployResponse;
31
+ }
32
+ declare function formatSuccess(input: FormatSuccessInput): string;
33
+ //#endregion
34
+ export { DeployResponse, FormatSuccessInput, type RunningEndpoint, formatSuccess };
35
+ //# sourceMappingURL=format-success.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-success.d.ts","names":[],"sources":["../../src/internals/format-success.ts"],"mappings":";;;;UA4CiB,cAAA;EAAA;EAEf,KAAA;;EAEA,aAAA;EAFA;EAIA,aAAA;EAAA;EAEA,UAAA;EAWA;;;AAGF;;;;;;;EAHE,GAAA;AAAA;AAAA,UAGe,kBAAA;EAOY;EAL3B,SAAA;EAKmC;EAHnC,cAAA,EAAgB,cAAA;AAAA;AAAA,iBAGF,aAAA,CAAc,KAAA,EAAO,kBAAA"}
@@ -0,0 +1,80 @@
1
+ import { decode } from "./lease-state.js";
2
+ import { extractRunningEndpoints, formatEndpointAsIngress, normalizeFredUrl } from "./connection.js";
3
+ //#region src/internals/format-success.ts
4
+ /**
5
+ * Render the user-facing "Deployed." block for a successful `deployApp` run.
6
+ *
7
+ * Takes `lease-uuid` and `deploy_response` via the typed `FormatSuccessInput`.
8
+ *
9
+ * Output is plain text suitable for direct chat display. Designed to be
10
+ * printed verbatim by `deployApp` (and downstream renderers) — no
11
+ * paraphrasing or surrounding prose.
12
+ *
13
+ * **Lease-state decoding:** `deploy_response.state` may be an integer (raw
14
+ * chain emit) or a `LEASE_STATE_*` string (codec.toJSON form). Both flow
15
+ * through `lease-state.ts:decode`. Unknown values render as
16
+ * `UNKNOWN(<raw>)` so the raw remains visible.
17
+ *
18
+ * **Multi-instance / multi-service stacks** emit `Ingresses:` followed by
19
+ * one bare FQDN per UNIQUE FQDN across running instances. Instances
20
+ * sharing an FQDN (e.g. replicas behind one subdomain) are deduped by
21
+ * `extractRunningEndpoints`.
22
+ *
23
+ * **Custom-domain line** is emitted BEFORE the Ingress block when the
24
+ * deploy response carries a `custom_domain` (the set-domain tx confirmed
25
+ * alongside create-lease). The "(provisioning)" qualifier reflects that
26
+ * the chain tx confirmed but the provider may still be issuing the cert.
27
+ *
28
+ * **Provider rendering**: the CJS once attempted to resolve a friendly
29
+ * provider name via `browse_catalog` and dropped it when upstream's
30
+ * catalog shape carried no `name` field. The TS port keeps the
31
+ * `provider_uuid` rendering for the same reason — if upstream later adds
32
+ * `name`, restore via a thin helper.
33
+ */
34
+ /** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */
35
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36
+ function formatSuccess(input) {
37
+ if (!UUID_RE.test(input.leaseUuid)) throw new TypeError(`formatSuccess: leaseUuid must be a UUID; got "${input.leaseUuid}"`);
38
+ if (input.deployResponse === null || typeof input.deployResponse !== "object") throw new TypeError("formatSuccess: deployResponse must be a non-null object");
39
+ const dr = input.deployResponse;
40
+ const providerName = typeof dr.provider_uuid === "string" && dr.provider_uuid.length > 0 ? dr.provider_uuid : "(unknown)";
41
+ const stateName = decodeStateName(dr.state);
42
+ const ingresses = extractRunningEndpoints(dr.connection).map(formatEndpointAsIngress).filter((s) => typeof s === "string" && s.length > 0);
43
+ const hasIngress = ingresses.length > 0 || typeof dr.url === "string" && dr.url.length > 0;
44
+ const lines = [
45
+ "Deployed.",
46
+ ` Provider: ${providerName}`,
47
+ ` Lease UUID: ${input.leaseUuid}`,
48
+ ` Lease Status: ${stateName}`
49
+ ];
50
+ if (typeof dr.custom_domain === "string" && dr.custom_domain.length > 0) {
51
+ lines.push(` Custom domain (provisioning): https://${dr.custom_domain}/`);
52
+ lines.push(hasIngress ? " — TLS may take a few minutes; the Ingress URL below works immediately." : " — TLS may take a few minutes.");
53
+ }
54
+ if (ingresses.length === 0) if (typeof dr.url === "string" && dr.url.length > 0) lines.push(` Ingress: ${normalizeFredUrl(dr.url)}`);
55
+ else lines.push(" Ingress: (none — service is internal or no FQDN reported)");
56
+ else if (ingresses.length === 1) lines.push(` Ingress: ${ingresses[0]}`);
57
+ else {
58
+ lines.push(" Ingresses:");
59
+ for (const fqdn of ingresses) lines.push(` - ${fqdn}`);
60
+ }
61
+ lines.push("");
62
+ lines.push(`For logs / status: /manifest-agent:troubleshoot-deployment ${input.leaseUuid}`);
63
+ return lines.join("\n");
64
+ }
65
+ /**
66
+ * Return the user-facing form of a lease state. The `LEASE_STATE_` prefix
67
+ * is stripped for display (e.g. `LEASE_STATE_ACTIVE` → `ACTIVE`). Unknown
68
+ * decodes render as `UNKNOWN(<raw>)` so the raw remains visible; absent
69
+ * state renders as `(unknown)`.
70
+ */
71
+ function decodeStateName(state) {
72
+ if (state === void 0) return "(unknown)";
73
+ const canonical = decode(state);
74
+ if (canonical !== void 0) return canonical.slice(12);
75
+ return `UNKNOWN(${String(state)})`;
76
+ }
77
+ //#endregion
78
+ export { formatSuccess };
79
+
80
+ //# sourceMappingURL=format-success.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-success.js","names":["decodeLeaseState"],"sources":["../../src/internals/format-success.ts"],"sourcesContent":["import {\n extractRunningEndpoints,\n formatEndpointAsIngress,\n normalizeFredUrl,\n type RunningEndpoint,\n} from './connection.js';\nimport { decode as decodeLeaseState } from './lease-state.js';\n\n/**\n * Render the user-facing \"Deployed.\" block for a successful `deployApp` run.\n *\n * Takes `lease-uuid` and `deploy_response` via the typed `FormatSuccessInput`.\n *\n * Output is plain text suitable for direct chat display. Designed to be\n * printed verbatim by `deployApp` (and downstream renderers) — no\n * paraphrasing or surrounding prose.\n *\n * **Lease-state decoding:** `deploy_response.state` may be an integer (raw\n * chain emit) or a `LEASE_STATE_*` string (codec.toJSON form). Both flow\n * through `lease-state.ts:decode`. Unknown values render as\n * `UNKNOWN(<raw>)` so the raw remains visible.\n *\n * **Multi-instance / multi-service stacks** emit `Ingresses:` followed by\n * one bare FQDN per UNIQUE FQDN across running instances. Instances\n * sharing an FQDN (e.g. replicas behind one subdomain) are deduped by\n * `extractRunningEndpoints`.\n *\n * **Custom-domain line** is emitted BEFORE the Ingress block when the\n * deploy response carries a `custom_domain` (the set-domain tx confirmed\n * alongside create-lease). The \"(provisioning)\" qualifier reflects that\n * the chain tx confirmed but the provider may still be issuing the cert.\n *\n * **Provider rendering**: the CJS once attempted to resolve a friendly\n * provider name via `browse_catalog` and dropped it when upstream's\n * catalog shape carried no `name` field. The TS port keeps the\n * `provider_uuid` rendering for the same reason — if upstream later adds\n * `name`, restore via a thin helper.\n */\n\n/** RFC 4122 UUID — 36 chars, hex + 4 hyphens, lowercase or upper. */\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/** Deploy response shape consumed by `formatSuccess`. Subset of fred's `mcp__manifest-fred__deploy_app` response. */\nexport interface DeployResponse {\n /** Chain lease state — int (raw) or `LEASE_STATE_*` string (codec). */\n state?: number | string;\n /** Provider UUID — rendered as-is (catalog has no friendly-name field). */\n provider_uuid?: string;\n /** Custom domain attached to the lease item; populated when set-domain confirmed. */\n custom_domain?: string;\n /** Provider connection payload — walked by `connection.ts`. */\n connection?: unknown;\n /**\n * Top-level URL — legacy fallback when provider reports\n * `connection.host` / `connection.ports` shape rather than\n * `connection.instances`. Used when no FQDN can be extracted from\n * `connection`. Defensive: `classify-deploy-response.ts:43-51,76-80`\n * already defends against the same legacy shape; mirroring here keeps\n * the renderer consistent with the classifier so a fred response of\n * `{ url: 'https://app.example.com/' }` renders an Ingress line\n * rather than `(none …)`.\n */\n url?: string;\n}\n\nexport interface FormatSuccessInput {\n /** Validated lease UUID (RFC 4122 v1-v5). */\n leaseUuid: string;\n /** Deploy response from `deploy_app` (or equivalent atomic broadcast). */\n deployResponse: DeployResponse;\n}\n\nexport function formatSuccess(input: FormatSuccessInput): string {\n if (!UUID_RE.test(input.leaseUuid)) {\n throw new TypeError(\n `formatSuccess: leaseUuid must be a UUID; got \"${input.leaseUuid}\"`,\n );\n }\n if (\n input.deployResponse === null ||\n typeof input.deployResponse !== 'object'\n ) {\n throw new TypeError(\n 'formatSuccess: deployResponse must be a non-null object',\n );\n }\n\n const dr = input.deployResponse;\n const providerName =\n typeof dr.provider_uuid === 'string' && dr.provider_uuid.length > 0\n ? dr.provider_uuid\n : '(unknown)';\n const stateName = decodeStateName(dr.state);\n const endpoints = extractRunningEndpoints(dr.connection);\n const ingresses = endpoints\n .map(formatEndpointAsIngress)\n .filter((s): s is string => typeof s === 'string' && s.length > 0);\n // Copilot review fix (PR #58 r3250192778): the custom-domain block's\n // TLS note (\\\"the Ingress URL below works immediately\\\") promises a\n // URL that may not exist when `connection.instances` is empty AND\n // there's no top-level `url`. Compute ingress availability up-front\n // so the custom-domain block can branch its second line accordingly.\n const hasIngress =\n ingresses.length > 0 || (typeof dr.url === 'string' && dr.url.length > 0);\n\n const lines: string[] = [\n 'Deployed.',\n ` Provider: ${providerName}`,\n ` Lease UUID: ${input.leaseUuid}`,\n ` Lease Status: ${stateName}`,\n ];\n\n // Custom-domain block — chain tx confirmed, provider may still be\n // provisioning. Present BEFORE Ingress so the user sees the requested\n // endpoint first, alongside the immediately-working provider FQDN (if\n // any). The TLS note's \"Ingress URL below works immediately\" promise\n // only fires when an Ingress is actually present (r3250192778).\n if (typeof dr.custom_domain === 'string' && dr.custom_domain.length > 0) {\n lines.push(` Custom domain (provisioning): https://${dr.custom_domain}/`);\n lines.push(\n hasIngress\n ? ' — TLS may take a few minutes; the Ingress URL below works immediately.'\n : ' — TLS may take a few minutes.',\n );\n }\n\n if (ingresses.length === 0) {\n // Legacy fallback: when no FQDN can be extracted from `connection`\n // (e.g. providers reporting the older `connection.host` / `ports`\n // shape rather than `connection.instances`), fred may still surface\n // the URL at the top level. `normalizeFredUrl` is the shared helper\n // (mirrored across `classify-deploy-response.ts`, this renderer,\n // and `deploy-app.ts`'s `DeployResult.urls` fallback). The\n // `(none …)` fallback stays for the truly-empty case.\n if (typeof dr.url === 'string' && dr.url.length > 0) {\n lines.push(` Ingress: ${normalizeFredUrl(dr.url)}`);\n } else {\n lines.push(\n ' Ingress: (none — service is internal or no FQDN reported)',\n );\n }\n } else if (ingresses.length === 1) {\n lines.push(` Ingress: ${ingresses[0]}`);\n } else {\n lines.push(' Ingresses:');\n for (const fqdn of ingresses) {\n lines.push(` - ${fqdn}`);\n }\n }\n lines.push('');\n lines.push(\n `For logs / status: /manifest-agent:troubleshoot-deployment ${input.leaseUuid}`,\n );\n\n return lines.join('\\n');\n}\n\n/**\n * Return the user-facing form of a lease state. The `LEASE_STATE_` prefix\n * is stripped for display (e.g. `LEASE_STATE_ACTIVE` → `ACTIVE`). Unknown\n * decodes render as `UNKNOWN(<raw>)` so the raw remains visible; absent\n * state renders as `(unknown)`.\n */\nfunction decodeStateName(state: number | string | undefined): string {\n if (state === undefined) return '(unknown)';\n const canonical = decodeLeaseState(state);\n if (canonical !== undefined) {\n return canonical.slice('LEASE_STATE_'.length);\n }\n return `UNKNOWN(${String(state)})`;\n}\n\n// Re-export for callers that want to walk endpoints themselves without\n// re-importing from `./connection.js`. Keeps `format-success.ts` the\n// single consumer-facing entry for success-rendering plumbing.\nexport type { RunningEndpoint };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,UACJ;AAgCF,SAAgB,cAAc,OAAmC;AAC/D,KAAI,CAAC,QAAQ,KAAK,MAAM,UAAU,CAChC,OAAM,IAAI,UACR,iDAAiD,MAAM,UAAU,GAClE;AAEH,KACE,MAAM,mBAAmB,QACzB,OAAO,MAAM,mBAAmB,SAEhC,OAAM,IAAI,UACR,0DACD;CAGH,MAAM,KAAK,MAAM;CACjB,MAAM,eACJ,OAAO,GAAG,kBAAkB,YAAY,GAAG,cAAc,SAAS,IAC9D,GAAG,gBACH;CACN,MAAM,YAAY,gBAAgB,GAAG,MAAM;CAE3C,MAAM,YADY,wBAAwB,GAAG,WAAW,CAErD,IAAI,wBAAwB,CAC5B,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,EAAE;CAMpE,MAAM,aACJ,UAAU,SAAS,KAAM,OAAO,GAAG,QAAQ,YAAY,GAAG,IAAI,SAAS;CAEzE,MAAM,QAAkB;EACtB;EACA,oBAAoB;EACpB,oBAAoB,MAAM;EAC1B,oBAAoB;EACrB;AAOD,KAAI,OAAO,GAAG,kBAAkB,YAAY,GAAG,cAAc,SAAS,GAAG;AACvE,QAAM,KAAK,4CAA4C,GAAG,cAAc,GAAG;AAC3E,QAAM,KACJ,aACI,+EACA,oCACL;;AAGH,KAAI,UAAU,WAAW,EAQvB,KAAI,OAAO,GAAG,QAAQ,YAAY,GAAG,IAAI,SAAS,EAChD,OAAM,KAAK,oBAAoB,iBAAiB,GAAG,IAAI,GAAG;KAE1D,OAAM,KACJ,oEACD;UAEM,UAAU,WAAW,EAC9B,OAAM,KAAK,oBAAoB,UAAU,KAAK;MACzC;AACL,QAAM,KAAK,eAAe;AAC1B,OAAK,MAAM,QAAQ,UACjB,OAAM,KAAK,SAAS,OAAO;;AAG/B,OAAM,KAAK,GAAG;AACd,OAAM,KACJ,+DAA+D,MAAM,YACtE;AAED,QAAO,MAAM,KAAK,KAAK;;;;;;;;AASzB,SAAS,gBAAgB,OAA4C;AACnE,KAAI,UAAU,KAAA,EAAW,QAAO;CAChC,MAAM,YAAYA,OAAiB,MAAM;AACzC,KAAI,cAAc,KAAA,EAChB,QAAO,UAAU,MAAM,GAAsB;AAE/C,QAAO,WAAW,OAAO,MAAM,CAAC"}
@@ -0,0 +1,138 @@
1
+ //#region src/internals/guarded-fetch.d.ts
2
+ /**
3
+ * SSRF-guarded `fetch` factory. A Node-native undici Dispatcher that
4
+ * DNS-resolves once at connect time and rejects any address whose
5
+ * `ipaddr.js` range is not `'unicast'`.
6
+ *
7
+ * Why DIY rather than `request-filtering-agent`: the library only works with
8
+ * `http`/`https`.Agent (legacy http API) and explicitly does NOT plug into
9
+ * undici / native `fetch` per its v3.2.0 README. Re-routing the same
10
+ * blocking semantics through undici's Dispatcher hook lets agent-core's
11
+ * `inspectImage` (and future consumers) use native `fetch` while preserving
12
+ * the same SSRF posture.
13
+ *
14
+ * Design (architect-blessed):
15
+ * - **`ipaddr.js`'s `range()` is the source of truth.** Same approach as
16
+ * `request-filtering-agent`: block any IP whose range is not `'unicast'`.
17
+ * This covers loopback / private / link-local / multicast / broadcast /
18
+ * reserved / carrier-grade-NAT / unspecified / ipv4Mapped / etc. via the
19
+ * library's well-maintained RFC-classification table.
20
+ * - **IPv4-mapped IPv6 normalization** (security-critical). An attacker
21
+ * writing `::ffff:127.0.0.1` would otherwise sit in `ipaddr.js`'s
22
+ * `'ipv4Mapped'` IPv6 range — coincidentally blocked, but for the
23
+ * structural reason ("v4-mapped form") rather than the security reason
24
+ * ("loopback target"). We normalize first so the block is justified.
25
+ * Without this step, a v4-mapped form of a PUBLIC IPv4 (`::ffff:8.8.8.8`)
26
+ * would also be blocked (wrong outcome — public IP is fine via
27
+ * v4-mapped). Normalization gets both cases right.
28
+ * - **DNS-resolve INSIDE the connect hook** to close the TOCTOU window
29
+ * between resolve and TCP connect. The resolved IP gets substituted as
30
+ * the connect hostname so the kernel doesn't re-resolve.
31
+ * - **Module-level singleton Dispatcher**, lazy-instantiated on first
32
+ * `createGuardedFetch()` invocation. Mirrors the CJS singleton-agent
33
+ * pattern; avoids the aggressive `setGlobalDispatcher()` side-effect.
34
+ * - **Construction-time runtime check** (`typeof process === 'undefined'`)
35
+ * throws a clear error on browser/Deno so the failure is actionable, not
36
+ * a confusing mid-fetch module-resolution error.
37
+ * - **Redirect safety:** undici re-fires the connect hook on every cross-
38
+ * host redirect; same-host redirects reuse the checked socket. The fetch
39
+ * closure does NOT need `redirect: 'manual'` — default `follow` is safe
40
+ * by construction.
41
+ *
42
+ * Blocked-range exports (`BLOCKED_RANGES_IPV4`, `BLOCKED_RANGES_IPV6`) are
43
+ * provided for audit + test purposes: they enumerate the `ipaddr.js`
44
+ * `range()` classifications we treat as non-`unicast` with their RFC
45
+ * citations, so a reviewer can grep the code without consulting the
46
+ * `ipaddr.js` source.
47
+ *
48
+ * Cross-platform note: agent-core's `tsdown.config.ts` targets
49
+ * `platform: 'neutral'`. `ipaddr.js` is isomorphic (pure JS, no node:*
50
+ * imports), so the static import is fine. `undici` and `node:dns/promises`
51
+ * + `node:net` are Node-only — dynamic-imported INSIDE the lazy singleton
52
+ * creation so the package stays importable from browsers / Deno (calling
53
+ * `createGuardedFetch()` from those throws the construction-time error
54
+ * with actionable guidance).
55
+ */
56
+ type GuardedFetch = typeof fetch;
57
+ /**
58
+ * `ipaddr.js`-classified IPv4 range labels we block (i.e., everything
59
+ * except `'unicast'`). Exposed as a module-level constant so the audit
60
+ * trail is greppable and a future range-list update is a focused edit.
61
+ *
62
+ * RFC citations included for each label for audit visibility — `ipaddr.js`
63
+ * owns the actual CIDR tables that map IPs to these labels.
64
+ */
65
+ declare const BLOCKED_RANGES_IPV4: ReadonlyArray<{
66
+ readonly range: string;
67
+ readonly rfc: string;
68
+ }>;
69
+ /**
70
+ * `ipaddr.js`-classified IPv6 range labels we block. Note: `'ipv4Mapped'`
71
+ * is NOT included here because we normalize IPv4-mapped IPv6 addresses to
72
+ * their underlying IPv4 form BEFORE the range check — otherwise a v4-
73
+ * mapped form of a public IP (`::ffff:8.8.8.8`) would be wrongly blocked,
74
+ * and a v4-mapped form of a private IP (`::ffff:127.0.0.1`) would be
75
+ * blocked only structurally (not for the security reason).
76
+ */
77
+ declare const BLOCKED_RANGES_IPV6: ReadonlyArray<{
78
+ readonly range: string;
79
+ readonly rfc: string;
80
+ }>;
81
+ /**
82
+ * SSRF block-check for a single IP string. **Allow-list policy:** only
83
+ * ipaddr.js's `'unicast'` classification is permitted; every other range
84
+ * label is blocked.
85
+ *
86
+ * The prior deny-list implementation iterated BLOCKED_RANGES_* and let
87
+ * anything not explicitly enumerated fall through as "allowed" — a
88
+ * security-critical bias error. IPv6 categories like `6to4` (which can
89
+ * wrap loopback or RFC 1918 IPs as `2002:7f00::/24` etc.), `teredo`,
90
+ * `rfc6052` (NAT64), and `discard` were ALL un-named and therefore
91
+ * allowed-by-omission. Under the allow-list policy, these all
92
+ * default-deny along with any future ipaddr.js classification we
93
+ * haven't audited.
94
+ *
95
+ * Returned `{range, rfc}` descriptor sources:
96
+ * - **Named in BLOCKED_RANGES_IPV4 / BLOCKED_RANGES_IPV6** → returns
97
+ * that entry verbatim (carries the audited RFC citation).
98
+ * - **Unknown non-unicast label** → synthesizes
99
+ * `{range: <label>, rfc: 'ipaddr.js classification (default-deny non-unicast)'}`.
100
+ * The audit string is generic but the block decision is correct;
101
+ * a future PR can promote frequently-seen labels into
102
+ * BLOCKED_RANGES_* with proper RFC citations.
103
+ *
104
+ * IPv4-mapped IPv6 addresses (`::ffff:1.2.3.4`) are normalized to their
105
+ * IPv4 form before the range check so the security verdict tracks the
106
+ * underlying IP, not the structural wrapping.
107
+ *
108
+ * Throws `Error` on unparseable input — callers should catch and treat
109
+ * "unparseable" as "block" (defense-in-depth — better to refuse than to
110
+ * pass through to network on garbage input).
111
+ */
112
+ declare function isBlocked(ipString: string): {
113
+ range: string;
114
+ rfc: string;
115
+ } | null;
116
+ /**
117
+ * Build the SSRF-guarded fetch closure. Construction-time runtime check
118
+ * gates Node-only — browser / Deno consumers either pass their own
119
+ * `opts.fetch` to consumers like `inspectImage` or accept this error.
120
+ *
121
+ * The returned function matches `typeof fetch` and lazy-instantiates the
122
+ * undici Dispatcher on first invocation. Subsequent calls share the
123
+ * cached singleton.
124
+ *
125
+ * **Important: uses undici's own `fetch`**, not Node's built-in. Node's
126
+ * built-in fetch is backed by its bundled undici, which is pinned to
127
+ * Node's release-cycle version (Node 22 → undici 6.x). The npm-installed
128
+ * `undici` package may be newer, and the Dispatcher protocol between
129
+ * versions isn't guaranteed compatible (we observed "invalid
130
+ * onRequestStart method" when mixing Node 22's fetch with undici@8 Agent).
131
+ * Routing through undici's own fetch (same package version as the Agent)
132
+ * sidesteps the mismatch. The function signature stays identical to
133
+ * Node's `fetch` so consumers can't tell the difference.
134
+ */
135
+ declare function createGuardedFetch(): GuardedFetch;
136
+ //#endregion
137
+ export { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, GuardedFetch, createGuardedFetch, isBlocked };
138
+ //# sourceMappingURL=guarded-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-fetch.d.ts","names":[],"sources":["../../src/internals/guarded-fetch.ts"],"mappings":";;AAyDA;;;;;AAUA;;;;;;;;;AA+BA;;;;;;;;;AA2CA;;;;;;;;;AAqEA;;;;;;;;;;;;;;;;;;;;;KAzJY,YAAA,UAAsB,KAAA;;;;;;;;;cAUrB,mBAAA,EAAqB,aAAA;EAAA,SACvB,KAAA;EAAA,SACA,GAAA;AAAA;;;;;;;;;cA6BE,mBAAA,EAAqB,aAAA;EAAA,SACvB,KAAA;EAAA,SACA,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyCK,SAAA,CAAU,QAAA;EACxB,KAAA;EACA,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;iBAmEc,kBAAA,CAAA,GAAsB,YAAA"}
@@ -0,0 +1,242 @@
1
+ import ipaddr from "ipaddr.js";
2
+ //#region src/internals/guarded-fetch.ts
3
+ /**
4
+ * `ipaddr.js`-classified IPv4 range labels we block (i.e., everything
5
+ * except `'unicast'`). Exposed as a module-level constant so the audit
6
+ * trail is greppable and a future range-list update is a focused edit.
7
+ *
8
+ * RFC citations included for each label for audit visibility — `ipaddr.js`
9
+ * owns the actual CIDR tables that map IPs to these labels.
10
+ */
11
+ const BLOCKED_RANGES_IPV4 = [
12
+ {
13
+ range: "unspecified",
14
+ rfc: "RFC 1122 §3.2.1.3 — 0.0.0.0/8 (this network / meta)"
15
+ },
16
+ {
17
+ range: "private",
18
+ rfc: "RFC 1918 — 10/8, 172.16/12, 192.168/16 (private)"
19
+ },
20
+ {
21
+ range: "loopback",
22
+ rfc: "RFC 5735 — 127/8 (loopback)"
23
+ },
24
+ {
25
+ range: "linkLocal",
26
+ rfc: "RFC 3927 — 169.254/16 (link-local, incl. AWS/GCP/Azure metadata at 169.254.169.254)"
27
+ },
28
+ {
29
+ range: "carrierGradeNat",
30
+ rfc: "RFC 6598 — 100.64/10 (carrier-grade NAT)"
31
+ },
32
+ {
33
+ range: "broadcast",
34
+ rfc: "RFC 919 — 255.255.255.255 (limited broadcast)"
35
+ },
36
+ {
37
+ range: "multicast",
38
+ rfc: "RFC 5771 — 224/4 (multicast)"
39
+ },
40
+ {
41
+ range: "reserved",
42
+ rfc: "RFC 1112 / 6890 — 240/4, 192.0.0/24, 198.18/15 etc. (reserved)"
43
+ }
44
+ ];
45
+ /**
46
+ * `ipaddr.js`-classified IPv6 range labels we block. Note: `'ipv4Mapped'`
47
+ * is NOT included here because we normalize IPv4-mapped IPv6 addresses to
48
+ * their underlying IPv4 form BEFORE the range check — otherwise a v4-
49
+ * mapped form of a public IP (`::ffff:8.8.8.8`) would be wrongly blocked,
50
+ * and a v4-mapped form of a private IP (`::ffff:127.0.0.1`) would be
51
+ * blocked only structurally (not for the security reason).
52
+ */
53
+ const BLOCKED_RANGES_IPV6 = [
54
+ {
55
+ range: "unspecified",
56
+ rfc: "RFC 4291 — :: (unspecified)"
57
+ },
58
+ {
59
+ range: "loopback",
60
+ rfc: "RFC 4291 — ::1/128 (loopback)"
61
+ },
62
+ {
63
+ range: "linkLocal",
64
+ rfc: "RFC 4291 — fe80::/10 (link-local)"
65
+ },
66
+ {
67
+ range: "uniqueLocal",
68
+ rfc: "RFC 4193 — fc00::/7 (unique local / private)"
69
+ },
70
+ {
71
+ range: "multicast",
72
+ rfc: "RFC 4291 — ff00::/8 (multicast)"
73
+ },
74
+ {
75
+ range: "reserved",
76
+ rfc: "RFC 4291 / 5156 — various reserved blocks"
77
+ }
78
+ ];
79
+ /**
80
+ * SSRF block-check for a single IP string. **Allow-list policy:** only
81
+ * ipaddr.js's `'unicast'` classification is permitted; every other range
82
+ * label is blocked.
83
+ *
84
+ * The prior deny-list implementation iterated BLOCKED_RANGES_* and let
85
+ * anything not explicitly enumerated fall through as "allowed" — a
86
+ * security-critical bias error. IPv6 categories like `6to4` (which can
87
+ * wrap loopback or RFC 1918 IPs as `2002:7f00::/24` etc.), `teredo`,
88
+ * `rfc6052` (NAT64), and `discard` were ALL un-named and therefore
89
+ * allowed-by-omission. Under the allow-list policy, these all
90
+ * default-deny along with any future ipaddr.js classification we
91
+ * haven't audited.
92
+ *
93
+ * Returned `{range, rfc}` descriptor sources:
94
+ * - **Named in BLOCKED_RANGES_IPV4 / BLOCKED_RANGES_IPV6** → returns
95
+ * that entry verbatim (carries the audited RFC citation).
96
+ * - **Unknown non-unicast label** → synthesizes
97
+ * `{range: <label>, rfc: 'ipaddr.js classification (default-deny non-unicast)'}`.
98
+ * The audit string is generic but the block decision is correct;
99
+ * a future PR can promote frequently-seen labels into
100
+ * BLOCKED_RANGES_* with proper RFC citations.
101
+ *
102
+ * IPv4-mapped IPv6 addresses (`::ffff:1.2.3.4`) are normalized to their
103
+ * IPv4 form before the range check so the security verdict tracks the
104
+ * underlying IP, not the structural wrapping.
105
+ *
106
+ * Throws `Error` on unparseable input — callers should catch and treat
107
+ * "unparseable" as "block" (defense-in-depth — better to refuse than to
108
+ * pass through to network on garbage input).
109
+ */
110
+ function isBlocked(ipString) {
111
+ let parsed = ipaddr.parse(ipString);
112
+ if (parsed.kind() === "ipv6") {
113
+ const v6 = parsed;
114
+ if (v6.isIPv4MappedAddress()) parsed = v6.toIPv4Address();
115
+ }
116
+ const rangeLabel = parsed.range();
117
+ if (rangeLabel === "unicast") return null;
118
+ const named = (parsed.kind() === "ipv4" ? BLOCKED_RANGES_IPV4 : BLOCKED_RANGES_IPV6).find((r) => r.range === rangeLabel);
119
+ if (named) return named;
120
+ return {
121
+ range: rangeLabel,
122
+ rfc: "ipaddr.js classification (default-deny non-unicast)"
123
+ };
124
+ }
125
+ /**
126
+ * Cache the in-flight Promise (not the resolved value) so concurrent first-
127
+ * call racers share the same construction and don't double-build the
128
+ * undici Agent. Resolves to the singleton DispatcherCache. After
129
+ * resolution, subsequent calls await the already-settled Promise (cheap).
130
+ */
131
+ let cachedP;
132
+ /**
133
+ * Build the SSRF-guarded fetch closure. Construction-time runtime check
134
+ * gates Node-only — browser / Deno consumers either pass their own
135
+ * `opts.fetch` to consumers like `inspectImage` or accept this error.
136
+ *
137
+ * The returned function matches `typeof fetch` and lazy-instantiates the
138
+ * undici Dispatcher on first invocation. Subsequent calls share the
139
+ * cached singleton.
140
+ *
141
+ * **Important: uses undici's own `fetch`**, not Node's built-in. Node's
142
+ * built-in fetch is backed by its bundled undici, which is pinned to
143
+ * Node's release-cycle version (Node 22 → undici 6.x). The npm-installed
144
+ * `undici` package may be newer, and the Dispatcher protocol between
145
+ * versions isn't guaranteed compatible (we observed "invalid
146
+ * onRequestStart method" when mixing Node 22's fetch with undici@8 Agent).
147
+ * Routing through undici's own fetch (same package version as the Agent)
148
+ * sidesteps the mismatch. The function signature stays identical to
149
+ * Node's `fetch` so consumers can't tell the difference.
150
+ */
151
+ function createGuardedFetch() {
152
+ if (typeof process === "undefined" || !process.versions?.node) throw new Error("createGuardedFetch requires a Node.js runtime. On browser/Deno consumers, pass `opts.fetch` directly with your own SSRF-guarded implementation. See agent-core README.");
153
+ return async (input, init) => {
154
+ if (!cachedP) cachedP = buildSsrfDispatcher().catch((err) => {
155
+ cachedP = void 0;
156
+ throw err;
157
+ });
158
+ const c = await cachedP;
159
+ const initWithDispatcher = {
160
+ ...init ?? {},
161
+ dispatcher: c.dispatcher
162
+ };
163
+ return c.fetch(input, initWithDispatcher);
164
+ };
165
+ }
166
+ /**
167
+ * Lazy dynamic-import of Node-only modules so the package stays importable
168
+ * from non-Node consumers. The runtime check in `createGuardedFetch`
169
+ * already gates Node-only — this function is only reached on Node.
170
+ *
171
+ * Returns BOTH the dispatcher and undici's own `fetch` so the
172
+ * `createGuardedFetch` closure can route through undici directly (avoiding
173
+ * Node-bundled-undici vs npm-undici Dispatcher protocol mismatches).
174
+ */
175
+ async function buildSsrfDispatcher() {
176
+ const [undici, dnsModule, netModule] = await Promise.all([
177
+ import("undici"),
178
+ import("node:dns/promises"),
179
+ import("node:net")
180
+ ]);
181
+ const baseConnect = undici.buildConnector({});
182
+ return {
183
+ dispatcher: new undici.Agent({ connect: (options, callback) => {
184
+ const hostname = options.hostname ?? "";
185
+ resolveAndCheck(hostname, netModule, dnsModule).then((resolved) => {
186
+ if (resolved.blocked) {
187
+ callback(/* @__PURE__ */ new Error(`SSRF blocked: ${hostname} resolves to ${resolved.ip} which is in blocked range '${resolved.blocked.range}' (${resolved.blocked.rfc})`), null);
188
+ return;
189
+ }
190
+ baseConnect({
191
+ ...options,
192
+ hostname: resolved.ip
193
+ }, callback);
194
+ }).catch((err) => {
195
+ const msg = err instanceof Error ? err.message : String(err);
196
+ callback(/* @__PURE__ */ new Error(`SSRF blocked: refused to connect to ${hostname}: ${msg}`), null);
197
+ });
198
+ } }),
199
+ fetch: undici.fetch
200
+ };
201
+ }
202
+ /**
203
+ * Resolve the connection target's IP and check against the blocked-range
204
+ * sets. Handles three input cases:
205
+ * 1. Hostname is already an IP literal → check directly (no DNS lookup).
206
+ * 2. Hostname is an FQDN → resolve via `dns.lookup` (returns first
207
+ * address; matches Node's default connection behavior).
208
+ * 3. Resolution failure → throws, which the caller's `.catch` translates
209
+ * into a fail-closed SSRF-block error.
210
+ *
211
+ * **DELIBERATE DIVERGENCE FROM SPEC** — architect's spec said
212
+ * `dns.resolve4` / `dns.resolve6`; this implementation uses `dns.lookup`.
213
+ * Rationale: `dns.lookup` matches the kernel's actual connection-time
214
+ * resolution path (hosts file + nsswitch.conf + DNS in order), so the IP
215
+ * we check IS the IP the kernel would connect to. Using `resolve4/6`
216
+ * would consult DNS only and miss the hosts-file path — if an attacker
217
+ * could write to `/etc/hosts` (root only) the check would be incomplete
218
+ * because the kernel's actual connect would use a different address than
219
+ * the one we checked. Per threat model: hosts-file writes require root,
220
+ * so an attacker capable of writing there already owns the machine; this
221
+ * is "fixing the right problem" — the check should track what the kernel
222
+ * does, not its own model of resolution.
223
+ *
224
+ * Documented in PR 2 description for reviewer awareness. If the threat
225
+ * model expands to include shared-host scenarios where attacker-controlled
226
+ * hosts entries are realistic, switch to `resolve4/6` and accept the
227
+ * connect-time-mismatch risk.
228
+ */
229
+ async function resolveAndCheck(hostname, netModule, dnsModule) {
230
+ let ip;
231
+ if (netModule.isIP(hostname) !== 0) ip = hostname;
232
+ else ip = (await dnsModule.lookup(hostname, { verbatim: true })).address;
233
+ const blocked = isBlocked(ip);
234
+ return {
235
+ ip,
236
+ blocked
237
+ };
238
+ }
239
+ //#endregion
240
+ export { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, createGuardedFetch, isBlocked };
241
+
242
+ //# sourceMappingURL=guarded-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-fetch.js","names":[],"sources":["../../src/internals/guarded-fetch.ts"],"sourcesContent":["import ipaddr from 'ipaddr.js';\n\n/**\n * SSRF-guarded `fetch` factory. A Node-native undici Dispatcher that\n * DNS-resolves once at connect time and rejects any address whose\n * `ipaddr.js` range is not `'unicast'`.\n *\n * Why DIY rather than `request-filtering-agent`: the library only works with\n * `http`/`https`.Agent (legacy http API) and explicitly does NOT plug into\n * undici / native `fetch` per its v3.2.0 README. Re-routing the same\n * blocking semantics through undici's Dispatcher hook lets agent-core's\n * `inspectImage` (and future consumers) use native `fetch` while preserving\n * the same SSRF posture.\n *\n * Design (architect-blessed):\n * - **`ipaddr.js`'s `range()` is the source of truth.** Same approach as\n * `request-filtering-agent`: block any IP whose range is not `'unicast'`.\n * This covers loopback / private / link-local / multicast / broadcast /\n * reserved / carrier-grade-NAT / unspecified / ipv4Mapped / etc. via the\n * library's well-maintained RFC-classification table.\n * - **IPv4-mapped IPv6 normalization** (security-critical). An attacker\n * writing `::ffff:127.0.0.1` would otherwise sit in `ipaddr.js`'s\n * `'ipv4Mapped'` IPv6 range — coincidentally blocked, but for the\n * structural reason (\"v4-mapped form\") rather than the security reason\n * (\"loopback target\"). We normalize first so the block is justified.\n * Without this step, a v4-mapped form of a PUBLIC IPv4 (`::ffff:8.8.8.8`)\n * would also be blocked (wrong outcome — public IP is fine via\n * v4-mapped). Normalization gets both cases right.\n * - **DNS-resolve INSIDE the connect hook** to close the TOCTOU window\n * between resolve and TCP connect. The resolved IP gets substituted as\n * the connect hostname so the kernel doesn't re-resolve.\n * - **Module-level singleton Dispatcher**, lazy-instantiated on first\n * `createGuardedFetch()` invocation. Mirrors the CJS singleton-agent\n * pattern; avoids the aggressive `setGlobalDispatcher()` side-effect.\n * - **Construction-time runtime check** (`typeof process === 'undefined'`)\n * throws a clear error on browser/Deno so the failure is actionable, not\n * a confusing mid-fetch module-resolution error.\n * - **Redirect safety:** undici re-fires the connect hook on every cross-\n * host redirect; same-host redirects reuse the checked socket. The fetch\n * closure does NOT need `redirect: 'manual'` — default `follow` is safe\n * by construction.\n *\n * Blocked-range exports (`BLOCKED_RANGES_IPV4`, `BLOCKED_RANGES_IPV6`) are\n * provided for audit + test purposes: they enumerate the `ipaddr.js`\n * `range()` classifications we treat as non-`unicast` with their RFC\n * citations, so a reviewer can grep the code without consulting the\n * `ipaddr.js` source.\n *\n * Cross-platform note: agent-core's `tsdown.config.ts` targets\n * `platform: 'neutral'`. `ipaddr.js` is isomorphic (pure JS, no node:*\n * imports), so the static import is fine. `undici` and `node:dns/promises`\n * + `node:net` are Node-only — dynamic-imported INSIDE the lazy singleton\n * creation so the package stays importable from browsers / Deno (calling\n * `createGuardedFetch()` from those throws the construction-time error\n * with actionable guidance).\n */\n\nexport type GuardedFetch = typeof fetch;\n\n/**\n * `ipaddr.js`-classified IPv4 range labels we block (i.e., everything\n * except `'unicast'`). Exposed as a module-level constant so the audit\n * trail is greppable and a future range-list update is a focused edit.\n *\n * RFC citations included for each label for audit visibility — `ipaddr.js`\n * owns the actual CIDR tables that map IPs to these labels.\n */\nexport const BLOCKED_RANGES_IPV4: ReadonlyArray<{\n readonly range: string;\n readonly rfc: string;\n}> = [\n {\n range: 'unspecified',\n rfc: 'RFC 1122 §3.2.1.3 — 0.0.0.0/8 (this network / meta)',\n },\n { range: 'private', rfc: 'RFC 1918 — 10/8, 172.16/12, 192.168/16 (private)' },\n { range: 'loopback', rfc: 'RFC 5735 — 127/8 (loopback)' },\n {\n range: 'linkLocal',\n rfc: 'RFC 3927 — 169.254/16 (link-local, incl. AWS/GCP/Azure metadata at 169.254.169.254)',\n },\n { range: 'carrierGradeNat', rfc: 'RFC 6598 — 100.64/10 (carrier-grade NAT)' },\n { range: 'broadcast', rfc: 'RFC 919 — 255.255.255.255 (limited broadcast)' },\n { range: 'multicast', rfc: 'RFC 5771 — 224/4 (multicast)' },\n {\n range: 'reserved',\n rfc: 'RFC 1112 / 6890 — 240/4, 192.0.0/24, 198.18/15 etc. (reserved)',\n },\n];\n\n/**\n * `ipaddr.js`-classified IPv6 range labels we block. Note: `'ipv4Mapped'`\n * is NOT included here because we normalize IPv4-mapped IPv6 addresses to\n * their underlying IPv4 form BEFORE the range check — otherwise a v4-\n * mapped form of a public IP (`::ffff:8.8.8.8`) would be wrongly blocked,\n * and a v4-mapped form of a private IP (`::ffff:127.0.0.1`) would be\n * blocked only structurally (not for the security reason).\n */\nexport const BLOCKED_RANGES_IPV6: ReadonlyArray<{\n readonly range: string;\n readonly rfc: string;\n}> = [\n { range: 'unspecified', rfc: 'RFC 4291 — :: (unspecified)' },\n { range: 'loopback', rfc: 'RFC 4291 — ::1/128 (loopback)' },\n { range: 'linkLocal', rfc: 'RFC 4291 — fe80::/10 (link-local)' },\n { range: 'uniqueLocal', rfc: 'RFC 4193 — fc00::/7 (unique local / private)' },\n { range: 'multicast', rfc: 'RFC 4291 — ff00::/8 (multicast)' },\n { range: 'reserved', rfc: 'RFC 4291 / 5156 — various reserved blocks' },\n];\n\n/**\n * SSRF block-check for a single IP string. **Allow-list policy:** only\n * ipaddr.js's `'unicast'` classification is permitted; every other range\n * label is blocked.\n *\n * The prior deny-list implementation iterated BLOCKED_RANGES_* and let\n * anything not explicitly enumerated fall through as \"allowed\" — a\n * security-critical bias error. IPv6 categories like `6to4` (which can\n * wrap loopback or RFC 1918 IPs as `2002:7f00::/24` etc.), `teredo`,\n * `rfc6052` (NAT64), and `discard` were ALL un-named and therefore\n * allowed-by-omission. Under the allow-list policy, these all\n * default-deny along with any future ipaddr.js classification we\n * haven't audited.\n *\n * Returned `{range, rfc}` descriptor sources:\n * - **Named in BLOCKED_RANGES_IPV4 / BLOCKED_RANGES_IPV6** → returns\n * that entry verbatim (carries the audited RFC citation).\n * - **Unknown non-unicast label** → synthesizes\n * `{range: <label>, rfc: 'ipaddr.js classification (default-deny non-unicast)'}`.\n * The audit string is generic but the block decision is correct;\n * a future PR can promote frequently-seen labels into\n * BLOCKED_RANGES_* with proper RFC citations.\n *\n * IPv4-mapped IPv6 addresses (`::ffff:1.2.3.4`) are normalized to their\n * IPv4 form before the range check so the security verdict tracks the\n * underlying IP, not the structural wrapping.\n *\n * Throws `Error` on unparseable input — callers should catch and treat\n * \"unparseable\" as \"block\" (defense-in-depth — better to refuse than to\n * pass through to network on garbage input).\n */\nexport function isBlocked(ipString: string): {\n range: string;\n rfc: string;\n} | null {\n let parsed: ipaddr.IPv4 | ipaddr.IPv6 = ipaddr.parse(ipString);\n\n // IPv4-mapped IPv6 normalization — security-critical. See doc-comment.\n if (parsed.kind() === 'ipv6') {\n const v6 = parsed as ipaddr.IPv6;\n if (v6.isIPv4MappedAddress()) {\n parsed = v6.toIPv4Address();\n }\n }\n\n const rangeLabel = parsed.range();\n\n // Allow-list gate: `'unicast'` is the only category permitted.\n // Everything else defaults to block; the lookup below is for audit info.\n if (rangeLabel === 'unicast') {\n return null;\n }\n\n const list =\n parsed.kind() === 'ipv4' ? BLOCKED_RANGES_IPV4 : BLOCKED_RANGES_IPV6;\n // Both lists are short; linear scan is fine.\n const named = list.find((r) => r.range === rangeLabel);\n if (named) return named;\n\n // Default-deny fallback for unknown non-unicast labels (6to4, teredo,\n // rfc6052, discard, future categories ipaddr.js may add). The block\n // decision is correct; the audit string is generic.\n return {\n range: rangeLabel,\n rfc: 'ipaddr.js classification (default-deny non-unicast)',\n };\n}\n\ninterface DispatcherCache {\n dispatcher: unknown;\n fetch: typeof fetch;\n}\n\n/**\n * Cache the in-flight Promise (not the resolved value) so concurrent first-\n * call racers share the same construction and don't double-build the\n * undici Agent. Resolves to the singleton DispatcherCache. After\n * resolution, subsequent calls await the already-settled Promise (cheap).\n */\nlet cachedP: Promise<DispatcherCache> | undefined;\n\n/**\n * Build the SSRF-guarded fetch closure. Construction-time runtime check\n * gates Node-only — browser / Deno consumers either pass their own\n * `opts.fetch` to consumers like `inspectImage` or accept this error.\n *\n * The returned function matches `typeof fetch` and lazy-instantiates the\n * undici Dispatcher on first invocation. Subsequent calls share the\n * cached singleton.\n *\n * **Important: uses undici's own `fetch`**, not Node's built-in. Node's\n * built-in fetch is backed by its bundled undici, which is pinned to\n * Node's release-cycle version (Node 22 → undici 6.x). The npm-installed\n * `undici` package may be newer, and the Dispatcher protocol between\n * versions isn't guaranteed compatible (we observed \"invalid\n * onRequestStart method\" when mixing Node 22's fetch with undici@8 Agent).\n * Routing through undici's own fetch (same package version as the Agent)\n * sidesteps the mismatch. The function signature stays identical to\n * Node's `fetch` so consumers can't tell the difference.\n */\nexport function createGuardedFetch(): GuardedFetch {\n if (typeof process === 'undefined' || !process.versions?.node) {\n throw new Error(\n 'createGuardedFetch requires a Node.js runtime. On browser/Deno consumers, pass `opts.fetch` directly with your own SSRF-guarded implementation. See agent-core README.',\n );\n }\n\n return async (input, init) => {\n // Cache the Promise rather than the resolved value to dedup concurrent\n // first-call construction. Two callers racing through the lazy path\n // would otherwise both call `buildSsrfDispatcher()` and end up with\n // two undici Agents (no correctness bug, just double resource).\n //\n // Catch-and-reset on rejection: if `buildSsrfDispatcher()` ever fails\n // (e.g., dynamic `import('undici')` throws on a runtime that masquerades\n // as Node but lacks the module), the rejected Promise must NOT stay\n // cached — otherwise every subsequent `createGuardedFetch()` call would\n // re-throw the same error permanently. Clearing `cachedP` in the catch\n // arm lets a future caller retry construction.\n if (!cachedP) {\n cachedP = buildSsrfDispatcher().catch((err: unknown) => {\n cachedP = undefined;\n throw err;\n });\n }\n const c = await cachedP;\n // undici's fetch accepts a `dispatcher` option natively. Cast the init\n // to undici's expected shape — undici's fetch signature is structurally\n // compatible with global fetch but TS can't see through the dispatcher\n // field without a cast.\n const initWithDispatcher = {\n ...(init ?? {}),\n dispatcher: c.dispatcher,\n } as RequestInit;\n return c.fetch(input, initWithDispatcher);\n };\n}\n\n/**\n * Lazy dynamic-import of Node-only modules so the package stays importable\n * from non-Node consumers. The runtime check in `createGuardedFetch`\n * already gates Node-only — this function is only reached on Node.\n *\n * Returns BOTH the dispatcher and undici's own `fetch` so the\n * `createGuardedFetch` closure can route through undici directly (avoiding\n * Node-bundled-undici vs npm-undici Dispatcher protocol mismatches).\n */\nasync function buildSsrfDispatcher(): Promise<DispatcherCache> {\n const [undici, dnsModule, netModule] = await Promise.all([\n import('undici'),\n import('node:dns/promises'),\n import('node:net'),\n ]);\n\n const baseConnect = undici.buildConnector({});\n\n const dispatcher = new undici.Agent({\n connect: (options, callback) => {\n // Defensive: undici's Connect signature carries hostname + port + others.\n // We snapshot the original hostname for error messages; the resolved\n // IP gets substituted into `options` before the underlying TCP connect\n // so the kernel doesn't re-resolve (DNS-rebinding mitigation).\n const hostname = (options as { hostname?: string }).hostname ?? '';\n\n resolveAndCheck(hostname, netModule, dnsModule)\n .then((resolved) => {\n if (resolved.blocked) {\n callback(\n new Error(\n `SSRF blocked: ${hostname} resolves to ${resolved.ip} which is in blocked range '${resolved.blocked.range}' (${resolved.blocked.rfc})`,\n ),\n null,\n );\n return;\n }\n // Substitute the resolved IP so the kernel doesn't re-resolve.\n baseConnect(\n { ...options, hostname: resolved.ip } as Parameters<\n typeof baseConnect\n >[0],\n callback,\n );\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n // Fail closed — unparseable / unresolvable hostnames get rejected\n // rather than passed through. This keeps the SSRF posture intact\n // against DNS errors that might otherwise leak through.\n callback(\n new Error(\n `SSRF blocked: refused to connect to ${hostname}: ${msg}`,\n ),\n null,\n );\n });\n },\n });\n\n // undici's fetch has a structurally compatible signature with global fetch.\n // Cast at the boundary; downstream consumers see `typeof fetch`.\n return {\n dispatcher,\n fetch: undici.fetch as unknown as typeof fetch,\n };\n}\n\n/**\n * Resolve the connection target's IP and check against the blocked-range\n * sets. Handles three input cases:\n * 1. Hostname is already an IP literal → check directly (no DNS lookup).\n * 2. Hostname is an FQDN → resolve via `dns.lookup` (returns first\n * address; matches Node's default connection behavior).\n * 3. Resolution failure → throws, which the caller's `.catch` translates\n * into a fail-closed SSRF-block error.\n *\n * **DELIBERATE DIVERGENCE FROM SPEC** — architect's spec said\n * `dns.resolve4` / `dns.resolve6`; this implementation uses `dns.lookup`.\n * Rationale: `dns.lookup` matches the kernel's actual connection-time\n * resolution path (hosts file + nsswitch.conf + DNS in order), so the IP\n * we check IS the IP the kernel would connect to. Using `resolve4/6`\n * would consult DNS only and miss the hosts-file path — if an attacker\n * could write to `/etc/hosts` (root only) the check would be incomplete\n * because the kernel's actual connect would use a different address than\n * the one we checked. Per threat model: hosts-file writes require root,\n * so an attacker capable of writing there already owns the machine; this\n * is \"fixing the right problem\" — the check should track what the kernel\n * does, not its own model of resolution.\n *\n * Documented in PR 2 description for reviewer awareness. If the threat\n * model expands to include shared-host scenarios where attacker-controlled\n * hosts entries are realistic, switch to `resolve4/6` and accept the\n * connect-time-mismatch risk.\n */\nasync function resolveAndCheck(\n hostname: string,\n netModule: typeof import('node:net'),\n dnsModule: typeof import('node:dns/promises'),\n): Promise<{\n ip: string;\n blocked: { range: string; rfc: string } | null;\n}> {\n let ip: string;\n if (netModule.isIP(hostname) !== 0) {\n ip = hostname;\n } else {\n // `dns.lookup` calls `getaddrinfo` and follows the kernel's resolution\n // order (hosts → nsswitch → DNS, OS-defined), so the IP we check IS\n // the IP the kernel uses at connect(2) time. `dns.resolve4`/`resolve6`\n // would query system DNS only and miss `/etc/hosts` entries — a\n // `/etc/hosts evil.example.com 127.0.0.1` line would slip past the\n // SSRF check (DNS returns a clean IP) while the kernel connects to\n // loopback. Architect-endorsed correction to the original spec.\n //\n // `verbatim: true` preserves OS-defined IPv4/IPv6 ordering to avoid\n // reorder-induced check-vs-connect drift in mixed-family DNS responses.\n const result = await dnsModule.lookup(hostname, { verbatim: true });\n ip = result.address;\n }\n // Defense-in-depth: treat parse failures as blocks. ipaddr.parse throws\n // for malformed input — catch in the caller via the .catch wiring.\n const blocked = isBlocked(ip);\n return { ip, blocked };\n}\n"],"mappings":";;;;;;;;;;AAmEA,MAAa,sBAGR;CACH;EACE,OAAO;EACP,KAAK;EACN;CACD;EAAE,OAAO;EAAW,KAAK;EAAoD;CAC7E;EAAE,OAAO;EAAY,KAAK;EAA+B;CACzD;EACE,OAAO;EACP,KAAK;EACN;CACD;EAAE,OAAO;EAAmB,KAAK;EAA4C;CAC7E;EAAE,OAAO;EAAa,KAAK;EAAiD;CAC5E;EAAE,OAAO;EAAa,KAAK;EAAgC;CAC3D;EACE,OAAO;EACP,KAAK;EACN;CACF;;;;;;;;;AAUD,MAAa,sBAGR;CACH;EAAE,OAAO;EAAe,KAAK;EAA+B;CAC5D;EAAE,OAAO;EAAY,KAAK;EAAiC;CAC3D;EAAE,OAAO;EAAa,KAAK;EAAqC;CAChE;EAAE,OAAO;EAAe,KAAK;EAAgD;CAC7E;EAAE,OAAO;EAAa,KAAK;EAAmC;CAC9D;EAAE,OAAO;EAAY,KAAK;EAA6C;CACxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCD,SAAgB,UAAU,UAGjB;CACP,IAAI,SAAoC,OAAO,MAAM,SAAS;AAG9D,KAAI,OAAO,MAAM,KAAK,QAAQ;EAC5B,MAAM,KAAK;AACX,MAAI,GAAG,qBAAqB,CAC1B,UAAS,GAAG,eAAe;;CAI/B,MAAM,aAAa,OAAO,OAAO;AAIjC,KAAI,eAAe,UACjB,QAAO;CAMT,MAAM,SAFJ,OAAO,MAAM,KAAK,SAAS,sBAAsB,qBAEhC,MAAM,MAAM,EAAE,UAAU,WAAW;AACtD,KAAI,MAAO,QAAO;AAKlB,QAAO;EACL,OAAO;EACP,KAAK;EACN;;;;;;;;AAcH,IAAI;;;;;;;;;;;;;;;;;;;;AAqBJ,SAAgB,qBAAmC;AACjD,KAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,UAAU,KACvD,OAAM,IAAI,MACR,yKACD;AAGH,QAAO,OAAO,OAAO,SAAS;AAY5B,MAAI,CAAC,QACH,WAAU,qBAAqB,CAAC,OAAO,QAAiB;AACtD,aAAU,KAAA;AACV,SAAM;IACN;EAEJ,MAAM,IAAI,MAAM;EAKhB,MAAM,qBAAqB;GACzB,GAAI,QAAQ,EAAE;GACd,YAAY,EAAE;GACf;AACD,SAAO,EAAE,MAAM,OAAO,mBAAmB;;;;;;;;;;;;AAa7C,eAAe,sBAAgD;CAC7D,MAAM,CAAC,QAAQ,WAAW,aAAa,MAAM,QAAQ,IAAI;EACvD,OAAO;EACP,OAAO;EACP,OAAO;EACR,CAAC;CAEF,MAAM,cAAc,OAAO,eAAe,EAAE,CAAC;AA8C7C,QAAO;EACL,YA7CiB,IAAI,OAAO,MAAM,EAClC,UAAU,SAAS,aAAa;GAK9B,MAAM,WAAY,QAAkC,YAAY;AAEhE,mBAAgB,UAAU,WAAW,UAAU,CAC5C,MAAM,aAAa;AAClB,QAAI,SAAS,SAAS;AACpB,8BACE,IAAI,MACF,iBAAiB,SAAS,eAAe,SAAS,GAAG,8BAA8B,SAAS,QAAQ,MAAM,KAAK,SAAS,QAAQ,IAAI,GACrI,EACD,KACD;AACD;;AAGF,gBACE;KAAE,GAAG;KAAS,UAAU,SAAS;KAAI,EAGrC,SACD;KACD,CACD,OAAO,QAAiB;IACvB,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAI5D,6BACE,IAAI,MACF,uCAAuC,SAAS,IAAI,MACrD,EACD,KACD;KACD;KAEP,CAAC;EAMA,OAAO,OAAO;EACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,eAAe,gBACb,UACA,WACA,WAIC;CACD,IAAI;AACJ,KAAI,UAAU,KAAK,SAAS,KAAK,EAC/B,MAAK;KAaL,OADe,MAAM,UAAU,OAAO,UAAU,EAAE,UAAU,MAAM,CAAC,EACvD;CAId,MAAM,UAAU,UAAU,GAAG;AAC7B,QAAO;EAAE;EAAI;EAAS"}