@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,45 @@
1
+ import { DenomLookup, DenomMap } from "../types.js";
2
+
3
+ //#region src/internals/humanize-denom.d.ts
4
+ /**
5
+ * No-op `DenomMap` for callers without chain-data context. All lookups
6
+ * return `null`; `humanizeCoin` falls back to raw on-chain denoms.
7
+ * Exported so synchronous decision functions (e.g. `evaluateReadiness`)
8
+ * can default to it without needing to invoke the async loader.
9
+ */
10
+ declare const EMPTY_DENOM_MAP: DenomMap;
11
+ declare function loadChainDenomMap(chainDataFilePath?: string): Promise<DenomMap>;
12
+ /**
13
+ * Convert a smallest-unit amount string → human decimal string with up to
14
+ * `exponent` decimals, trimming trailing zeros for readability. Uses BigInt
15
+ * for the integer part so precision survives large balances; only the
16
+ * fractional remainder is divided.
17
+ *
18
+ * Exported for unit testing of the scaling logic in isolation (mirrors the
19
+ * CJS's `_fmtScaledAmount` test hook).
20
+ */
21
+ declare function _fmtScaledAmount(amount: string, exponent: number): string;
22
+ /**
23
+ * Render a single coin as `"<amount> <symbol>"` (when the denom is in the
24
+ * map) or `"<amount> <denom>"` verbatim (when unknown). Falls back to
25
+ * `"<amount>"` only when `denom` is null/undefined.
26
+ */
27
+ declare function humanizeCoin(amount: string, denom: string | null | undefined, denomMap: DenomMap): string;
28
+ /**
29
+ * Join multiple coins with `", "` (space after comma). Empty array →
30
+ * literal `"(empty)"` per CJS parity.
31
+ */
32
+ declare function humanizeBalances(balances: ReadonlyArray<{
33
+ denom?: string;
34
+ amount?: string | null;
35
+ }> | unknown, denomMap: DenomMap): string;
36
+ /**
37
+ * Return the friendly symbol for a chain denom (`"umfx"` → `"MFX"`) via
38
+ * the same lookup `humanizeCoin` uses. Falls back to the raw denom on
39
+ * unknown input. Avoids the brittle pattern of formatting `"0 MFX"` and
40
+ * string-splitting to recover `"MFX"`.
41
+ */
42
+ declare function denomToSymbol(denom: string | null | undefined, denomMap: DenomMap): string;
43
+ //#endregion
44
+ export { type DenomLookup, type DenomMap, EMPTY_DENOM_MAP, _fmtScaledAmount, denomToSymbol, humanizeBalances, humanizeCoin, loadChainDenomMap };
45
+ //# sourceMappingURL=humanize-denom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"humanize-denom.d.ts","names":[],"sources":["../../src/internals/humanize-denom.ts"],"mappings":";;;;;;;;AAuMA;cAjJa,eAAA,EAAiB,QAAA;AAAA,iBAER,iBAAA,CACpB,iBAAA,YACC,OAAA,CAAQ,QAAA;;;;;;;;;;iBAyEK,gBAAA,CAAiB,MAAA,UAAgB,QAAA;;;;;;iBAuBjC,YAAA,CACd,MAAA,UACA,KAAA,6BACA,QAAA,EAAU,QAAA;;;;;iBAgBI,gBAAA,CACd,QAAA,EAAU,aAAA;EAAgB,KAAA;EAAgB,MAAA;AAAA,cAC1C,QAAA,EAAU,QAAA;;;;;;;iBAwBI,aAAA,CACd,KAAA,6BACA,QAAA,EAAU,QAAA"}
@@ -0,0 +1,105 @@
1
+ //#region src/internals/humanize-denom.ts
2
+ const KNOWN_EXPONENT = 6;
3
+ /**
4
+ * No-op `DenomMap` for callers without chain-data context. All lookups
5
+ * return `null`; `humanizeCoin` falls back to raw on-chain denoms.
6
+ * Exported so synchronous decision functions (e.g. `evaluateReadiness`)
7
+ * can default to it without needing to invoke the async loader.
8
+ */
9
+ const EMPTY_DENOM_MAP = {
10
+ lookup: () => null,
11
+ raw: null
12
+ };
13
+ async function loadChainDenomMap(chainDataFilePath) {
14
+ if (!chainDataFilePath) return EMPTY_DENOM_MAP;
15
+ if (typeof process === "undefined" || typeof process.versions?.node !== "string") throw new Error("loadChainDenomMap: chainDataFilePath requires a Node.js runtime (node:fs unavailable in this environment)");
16
+ let raw;
17
+ try {
18
+ const { readFileSync } = await import("node:fs");
19
+ raw = JSON.parse(readFileSync(chainDataFilePath, "utf8"));
20
+ } catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ console.warn(`humanize-denom: failed to load ${chainDataFilePath}: ${message}; balances and fees will render with raw on-chain denoms.`);
23
+ return EMPTY_DENOM_MAP;
24
+ }
25
+ const map = /* @__PURE__ */ new Map();
26
+ if (raw !== null && typeof raw === "object") {
27
+ const feeTokens = raw.feeTokens;
28
+ if (Array.isArray(feeTokens)) {
29
+ for (const t of feeTokens) if (t !== null && typeof t === "object" && typeof t.denom === "string" && typeof t.symbol === "string") {
30
+ const token = t;
31
+ map.set(token.denom, {
32
+ symbol: token.symbol,
33
+ exponent: KNOWN_EXPONENT
34
+ });
35
+ }
36
+ }
37
+ }
38
+ return {
39
+ lookup: (denom) => {
40
+ if (typeof denom !== "string") return null;
41
+ return map.get(denom) ?? null;
42
+ },
43
+ raw
44
+ };
45
+ }
46
+ /**
47
+ * Convert a smallest-unit amount string → human decimal string with up to
48
+ * `exponent` decimals, trimming trailing zeros for readability. Uses BigInt
49
+ * for the integer part so precision survives large balances; only the
50
+ * fractional remainder is divided.
51
+ *
52
+ * Exported for unit testing of the scaling logic in isolation (mirrors the
53
+ * CJS's `_fmtScaledAmount` test hook).
54
+ */
55
+ function _fmtScaledAmount(amount, exponent) {
56
+ let digits;
57
+ try {
58
+ digits = BigInt(amount);
59
+ } catch {
60
+ return String(amount);
61
+ }
62
+ const negative = digits < 0n;
63
+ if (negative) digits = -digits;
64
+ const divisor = 10n ** BigInt(exponent);
65
+ const whole = digits / divisor;
66
+ const fracStr = (digits % divisor).toString().padStart(exponent, "0").replace(/0+$/, "");
67
+ let out = fracStr.length > 0 ? `${whole}.${fracStr}` : `${whole}`;
68
+ if (negative) out = `-${out}`;
69
+ return out;
70
+ }
71
+ /**
72
+ * Render a single coin as `"<amount> <symbol>"` (when the denom is in the
73
+ * map) or `"<amount> <denom>"` verbatim (when unknown). Falls back to
74
+ * `"<amount>"` only when `denom` is null/undefined.
75
+ */
76
+ function humanizeCoin(amount, denom, denomMap) {
77
+ if (denom === void 0 || denom === null) return `${amount}`;
78
+ const lookup = denomMap.lookup(denom);
79
+ if (lookup) return `${_fmtScaledAmount(amount, lookup.exponent)} ${lookup.symbol}`;
80
+ return `${amount} ${denom}`;
81
+ }
82
+ /**
83
+ * Join multiple coins with `", "` (space after comma). Empty array →
84
+ * literal `"(empty)"` per CJS parity.
85
+ */
86
+ function humanizeBalances(balances, denomMap) {
87
+ if (!Array.isArray(balances) || balances.length === 0) return "(empty)";
88
+ return balances.map((b) => {
89
+ return humanizeCoin(b !== null && typeof b === "object" && "amount" in b && b.amount != null ? String(b.amount) : "0", b !== null && typeof b === "object" && "denom" in b ? b.denom : void 0, denomMap);
90
+ }).join(", ");
91
+ }
92
+ /**
93
+ * Return the friendly symbol for a chain denom (`"umfx"` → `"MFX"`) via
94
+ * the same lookup `humanizeCoin` uses. Falls back to the raw denom on
95
+ * unknown input. Avoids the brittle pattern of formatting `"0 MFX"` and
96
+ * string-splitting to recover `"MFX"`.
97
+ */
98
+ function denomToSymbol(denom, denomMap) {
99
+ if (!denom) return String(denom ?? "");
100
+ return denomMap.lookup(denom)?.symbol ?? denom;
101
+ }
102
+ //#endregion
103
+ export { EMPTY_DENOM_MAP, _fmtScaledAmount, denomToSymbol, humanizeBalances, humanizeCoin, loadChainDenomMap };
104
+
105
+ //# sourceMappingURL=humanize-denom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"humanize-denom.js","names":[],"sources":["../../src/internals/humanize-denom.ts"],"sourcesContent":["/**\n * Convert chain-side coin amounts (always in the smallest unit) into the\n * human-readable display the user actually wants to see — e.g.\n * `1800000 factory/.../upwr` → `1.8 PWR`, `0.057738 PWR` built from\n * `57738 factory/.../upwr`, etc.\n *\n * The denom → symbol mapping is sourced from a chain registry JSON file\n * (`{ feeTokens: [{ denom, symbol, ... }] }` — every token the chain\n * accepts as gas). Callers pass the chain-data file path and forward the\n * resulting `DenomMap` to whichever helper renders balances; this module\n * just reads, parses, and looks up.\n *\n * Conversion factor: cosmos convention is 6 decimals for `u`-prefixed\n * tokens (umfx, upwr — including factory-wrapped variants). Anything else\n * is rendered untouched (denom kept as-is, amount printed as integer)\n * because we can't safely guess its exponent.\n *\n * **Dynamic node-import discipline** (mirrors `guarded-fetch.ts` +\n * `save-manifest.ts`): the `node:fs` import is deferred to call time so\n * module load doesn't violate the package's `platform: 'neutral'` build\n * target. `loadChainDenomMap` is therefore async; consumers must\n * `await` the result. The other 3 exports (`humanizeCoin`,\n * `humanizeBalances`, `denomToSymbol`) remain pure-sync since they take\n * a pre-loaded `DenomMap` as input.\n *\n * Exports (all 4 preserved per qa-engineer's review pin — PR 2's internal\n * callers use a subset; PR 3 will surface the rest):\n * - `loadChainDenomMap(chainDataFilePath?)` (ASYNC) — returns\n * `Promise<DenomMap>`. Missing / unreadable path → no-op map\n * (lookup always returns `null`). Read failures emit `console.warn`\n * matching the connection.ts precedent from PR 1.\n * - `humanizeCoin(amount, denom, denomMap)` — `\"<amount> <symbol>\"` or\n * `\"<amount> <denom>\"` on unknown denom.\n * - `humanizeBalances(coins, denomMap)` — joins multiple coins with\n * `\", \"`. Empty array → `\"(empty)\"` literal.\n * - `denomToSymbol(denom, denomMap)` — bare symbol or raw denom fallback.\n */\n\nimport type { DenomLookup, DenomMap } from '../types.js';\n\n// Re-export the public types for convenience to existing internal consumers\n// (this file's pre-PR-3 history exported DenomLookup + DenomMap directly).\n// Public consumers should import from `@manifest-network/manifest-agent-core`\n// (which re-exports `../types.js`); internal consumers can use either path.\nexport type { DenomLookup, DenomMap };\n\nconst KNOWN_EXPONENT = 6;\n\n/**\n * No-op `DenomMap` for callers without chain-data context. All lookups\n * return `null`; `humanizeCoin` falls back to raw on-chain denoms.\n * Exported so synchronous decision functions (e.g. `evaluateReadiness`)\n * can default to it without needing to invoke the async loader.\n */\nexport const EMPTY_DENOM_MAP: DenomMap = { lookup: () => null, raw: null };\n\nexport async function loadChainDenomMap(\n chainDataFilePath?: string,\n): Promise<DenomMap> {\n if (!chainDataFilePath) return EMPTY_DENOM_MAP;\n if (\n typeof process === 'undefined' ||\n typeof process.versions?.node !== 'string'\n ) {\n // Lazy node-only dep — refuse outside Node-like runtimes rather than\n // silently no-op'ing (which would hide a misconfiguration).\n throw new Error(\n 'loadChainDenomMap: chainDataFilePath requires a Node.js runtime (node:fs unavailable in this environment)',\n );\n }\n let raw: unknown;\n try {\n const { readFileSync } = await import('node:fs');\n raw = JSON.parse(readFileSync(chainDataFilePath, 'utf8'));\n } catch (err) {\n // CJS parity: warn loudly when a path was passed but read/parse failed.\n // A corrupted chain file silently downgrades all balance/fee rendering to\n // raw chain denoms across the package, and the user only notices because\n // the DeploymentPlan looks weird (\"0.000037 PWR\" vs \"37 upwr\"). Matches\n // connection.ts's `console.warn` default established in PR 1.\n const message = err instanceof Error ? err.message : String(err);\n console.warn(\n `humanize-denom: failed to load ${chainDataFilePath}: ${message}; ` +\n `balances and fees will render with raw on-chain denoms.`,\n );\n return EMPTY_DENOM_MAP;\n }\n\n // Normalize the feeTokens list into a denom → { symbol, exponent } map.\n // Every Manifest fee token uses 6 decimals (the leading `u` is the micro\n // prefix). Tokens not in feeTokens are unknown to us; the fallback branch\n // in humanizeCoin handles them.\n const map = new Map<string, DenomLookup>();\n if (raw !== null && typeof raw === 'object') {\n const feeTokens = (raw as { feeTokens?: unknown }).feeTokens;\n if (Array.isArray(feeTokens)) {\n for (const t of feeTokens) {\n if (\n t !== null &&\n typeof t === 'object' &&\n typeof (t as { denom?: unknown }).denom === 'string' &&\n typeof (t as { symbol?: unknown }).symbol === 'string'\n ) {\n const token = t as { denom: string; symbol: string };\n map.set(token.denom, {\n symbol: token.symbol,\n exponent: KNOWN_EXPONENT,\n });\n }\n }\n }\n }\n\n return {\n lookup: (denom) => {\n if (typeof denom !== 'string') return null;\n return map.get(denom) ?? null;\n },\n raw,\n };\n}\n\n/**\n * Convert a smallest-unit amount string → human decimal string with up to\n * `exponent` decimals, trimming trailing zeros for readability. Uses BigInt\n * for the integer part so precision survives large balances; only the\n * fractional remainder is divided.\n *\n * Exported for unit testing of the scaling logic in isolation (mirrors the\n * CJS's `_fmtScaledAmount` test hook).\n */\nexport function _fmtScaledAmount(amount: string, exponent: number): string {\n let digits: bigint;\n try {\n digits = BigInt(amount);\n } catch {\n return String(amount);\n }\n const negative = digits < 0n;\n if (negative) digits = -digits;\n const divisor = 10n ** BigInt(exponent);\n const whole = digits / divisor;\n const frac = digits % divisor;\n const fracStr = frac.toString().padStart(exponent, '0').replace(/0+$/, '');\n let out = fracStr.length > 0 ? `${whole}.${fracStr}` : `${whole}`;\n if (negative) out = `-${out}`;\n return out;\n}\n\n/**\n * Render a single coin as `\"<amount> <symbol>\"` (when the denom is in the\n * map) or `\"<amount> <denom>\"` verbatim (when unknown). Falls back to\n * `\"<amount>\"` only when `denom` is null/undefined.\n */\nexport function humanizeCoin(\n amount: string,\n denom: string | null | undefined,\n denomMap: DenomMap,\n): string {\n if (denom === undefined || denom === null) return `${amount}`;\n const lookup = denomMap.lookup(denom);\n if (lookup) {\n return `${_fmtScaledAmount(amount, lookup.exponent)} ${lookup.symbol}`;\n }\n // Best-effort unknown-denom rendering — keep the raw denom so the user\n // can still identify it, and don't guess at scaling.\n return `${amount} ${denom}`;\n}\n\n/**\n * Join multiple coins with `\", \"` (space after comma). Empty array →\n * literal `\"(empty)\"` per CJS parity.\n */\nexport function humanizeBalances(\n balances: ReadonlyArray<{ denom?: string; amount?: string | null }> | unknown,\n denomMap: DenomMap,\n): string {\n if (!Array.isArray(balances) || balances.length === 0) return '(empty)';\n return balances\n .map((b) => {\n const amount =\n b !== null && typeof b === 'object' && 'amount' in b && b.amount != null\n ? String(b.amount)\n : '0';\n const denom =\n b !== null && typeof b === 'object' && 'denom' in b\n ? (b.denom as string | null | undefined)\n : undefined;\n return humanizeCoin(amount, denom, denomMap);\n })\n .join(', ');\n}\n\n/**\n * Return the friendly symbol for a chain denom (`\"umfx\"` → `\"MFX\"`) via\n * the same lookup `humanizeCoin` uses. Falls back to the raw denom on\n * unknown input. Avoids the brittle pattern of formatting `\"0 MFX\"` and\n * string-splitting to recover `\"MFX\"`.\n */\nexport function denomToSymbol(\n denom: string | null | undefined,\n denomMap: DenomMap,\n): string {\n if (!denom) return String(denom ?? '');\n const lookup = denomMap.lookup(denom);\n return lookup?.symbol ?? denom;\n}\n"],"mappings":";AA8CA,MAAM,iBAAiB;;;;;;;AAQvB,MAAa,kBAA4B;CAAE,cAAc;CAAM,KAAK;CAAM;AAE1E,eAAsB,kBACpB,mBACmB;AACnB,KAAI,CAAC,kBAAmB,QAAO;AAC/B,KACE,OAAO,YAAY,eACnB,OAAO,QAAQ,UAAU,SAAS,SAIlC,OAAM,IAAI,MACR,4GACD;CAEH,IAAI;AACJ,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;AACtC,QAAM,KAAK,MAAM,aAAa,mBAAmB,OAAO,CAAC;UAClD,KAAK;EAMZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,UAAQ,KACN,kCAAkC,kBAAkB,IAAI,QAAQ,2DAEjE;AACD,SAAO;;CAOT,MAAM,sBAAM,IAAI,KAA0B;AAC1C,KAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,YAAa,IAAgC;AACnD,MAAI,MAAM,QAAQ,UAAU;QACrB,MAAM,KAAK,UACd,KACE,MAAM,QACN,OAAO,MAAM,YACb,OAAQ,EAA0B,UAAU,YAC5C,OAAQ,EAA2B,WAAW,UAC9C;IACA,MAAM,QAAQ;AACd,QAAI,IAAI,MAAM,OAAO;KACnB,QAAQ,MAAM;KACd,UAAU;KACX,CAAC;;;;AAMV,QAAO;EACL,SAAS,UAAU;AACjB,OAAI,OAAO,UAAU,SAAU,QAAO;AACtC,UAAO,IAAI,IAAI,MAAM,IAAI;;EAE3B;EACD;;;;;;;;;;;AAYH,SAAgB,iBAAiB,QAAgB,UAA0B;CACzE,IAAI;AACJ,KAAI;AACF,WAAS,OAAO,OAAO;SACjB;AACN,SAAO,OAAO,OAAO;;CAEvB,MAAM,WAAW,SAAS;AAC1B,KAAI,SAAU,UAAS,CAAC;CACxB,MAAM,UAAU,OAAO,OAAO,SAAS;CACvC,MAAM,QAAQ,SAAS;CAEvB,MAAM,WADO,SAAS,SACD,UAAU,CAAC,SAAS,UAAU,IAAI,CAAC,QAAQ,OAAO,GAAG;CAC1E,IAAI,MAAM,QAAQ,SAAS,IAAI,GAAG,MAAM,GAAG,YAAY,GAAG;AAC1D,KAAI,SAAU,OAAM,IAAI;AACxB,QAAO;;;;;;;AAQT,SAAgB,aACd,QACA,OACA,UACQ;AACR,KAAI,UAAU,KAAA,KAAa,UAAU,KAAM,QAAO,GAAG;CACrD,MAAM,SAAS,SAAS,OAAO,MAAM;AACrC,KAAI,OACF,QAAO,GAAG,iBAAiB,QAAQ,OAAO,SAAS,CAAC,GAAG,OAAO;AAIhE,QAAO,GAAG,OAAO,GAAG;;;;;;AAOtB,SAAgB,iBACd,UACA,UACQ;AACR,KAAI,CAAC,MAAM,QAAQ,SAAS,IAAI,SAAS,WAAW,EAAG,QAAO;AAC9D,QAAO,SACJ,KAAK,MAAM;AASV,SAAO,aAPL,MAAM,QAAQ,OAAO,MAAM,YAAY,YAAY,KAAK,EAAE,UAAU,OAChE,OAAO,EAAE,OAAO,GAChB,KAEJ,MAAM,QAAQ,OAAO,MAAM,YAAY,WAAW,IAC7C,EAAE,QACH,KAAA,GAC6B,SAAS;GAC5C,CACD,KAAK,KAAK;;;;;;;;AASf,SAAgB,cACd,OACA,UACQ;AACR,KAAI,CAAC,MAAO,QAAO,OAAO,SAAS,GAAG;AAEtC,QADe,SAAS,OAAO,MAAM,EACtB,UAAU"}
@@ -0,0 +1,31 @@
1
+ //#region src/internals/inspect-image.d.ts
2
+ interface ImageInfo {
3
+ image: string;
4
+ digest: string | null;
5
+ ports: string[];
6
+ env: Record<string, string>;
7
+ cmd: string[] | null;
8
+ entrypoint: string[] | null;
9
+ user: string;
10
+ workingDir: string;
11
+ healthcheck: Record<string, unknown> | null;
12
+ labels: Record<string, string> | null;
13
+ volumes: Record<string, unknown> | null;
14
+ suggestedTmpfs: string[];
15
+ }
16
+ interface InspectImageOptions {
17
+ /**
18
+ * HTTP client. **Production callers SHOULD use the default** (which is
19
+ * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /
20
+ * metadata at connect time). Tests pass canned implementations.
21
+ * Browser/Deno consumers pass their own SSRF-guarded fetch since
22
+ * `createGuardedFetch()` throws on non-Node runtimes.
23
+ */
24
+ fetch?: typeof fetch;
25
+ /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */
26
+ logger?: (reason: string) => void;
27
+ }
28
+ declare function inspectImage(imageRef: string, opts?: InspectImageOptions): Promise<ImageInfo | null>;
29
+ //#endregion
30
+ export { ImageInfo, InspectImageOptions, inspectImage };
31
+ //# sourceMappingURL=inspect-image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-image.d.ts","names":[],"sources":["../../src/internals/inspect-image.ts"],"mappings":";UA+EiB,SAAA;EACf,KAAA;EACA,MAAA;EACA,KAAA;EACA,GAAA,EAAK,MAAA;EACL,GAAA;EACA,UAAA;EACA,IAAA;EACA,UAAA;EACA,WAAA,EAAa,MAAA;EACb,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,MAAA;EACT,cAAA;AAAA;AAAA,UAGe,mBAAA;EAVf;;;;;;;EAkBA,KAAA,UAAe,KAAA;EAZf;EAcA,MAAA,IAAU,MAAA;AAAA;AAAA,iBAcU,YAAA,CACpB,QAAA,UACA,IAAA,GAAM,mBAAA,GACL,OAAA,CAAQ,SAAA"}
@@ -0,0 +1,345 @@
1
+ import { createGuardedFetch } from "./guarded-fetch.js";
2
+ //#region src/internals/inspect-image.ts
3
+ /**
4
+ * Inspect a public container image via the OCI Distribution API. Returns
5
+ * the manifest digest, exposed ports, image defaults (env / cmd /
6
+ * entrypoint / user / workingDir), healthcheck, labels, volumes, and a
7
+ * heuristic `suggestedTmpfs` list for known-good Fred image families.
8
+ *
9
+ * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`
10
+ * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6
11
+ * normalization — see `guarded-fetch.ts` for the design).
12
+ *
13
+ * **Fail-soft contract:** returns `null` on every non-fatal failure mode:
14
+ * - 401 / 403 (private registry / auth required)
15
+ * - 429 (Docker Hub rate-limit)
16
+ * - OCI grammar violation in the `imageRef`
17
+ * - Manifest body exceeding the 10 MiB cap
18
+ * - Request timeout (10s)
19
+ * - Unparseable manifest / blob JSON
20
+ * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)
21
+ * Callers treat `null` as "no info, ask the user".
22
+ * Diagnostics flow through `opts.logger` instead of stderr.
23
+ *
24
+ * ## Security — SSRF (production callers MUST read)
25
+ *
26
+ * `imageRef` is user-controlled (it comes from `DeploySpec.image`).
27
+ * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`
28
+ * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would
29
+ * cause this function to probe internal services on the host. The CJS
30
+ * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to
31
+ * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which
32
+ * blocks at connect time.
33
+ *
34
+ * Opt-out-of-safety semantics (parent's PR-2 directive): the default
35
+ * `opts.fetch = createGuardedFetch()` is safe by construction. Callers
36
+ * pass their own `opts.fetch` ONLY for tests (canned responses) or
37
+ * unusual production cases (e.g. a trusted private registry on an RFC
38
+ * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s
39
+ * JSDoc for the production-guard contract.
40
+ */
41
+ const ACCEPT_MANIFEST = [
42
+ "application/vnd.oci.image.index.v1+json",
43
+ "application/vnd.oci.image.manifest.v1+json",
44
+ "application/vnd.docker.distribution.manifest.list.v2+json",
45
+ "application/vnd.docker.distribution.manifest.v2+json"
46
+ ].join(", ");
47
+ const OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\.|_|__|-+)[a-z0-9]+)*$/;
48
+ const OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
49
+ const OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;
50
+ const MAX_BODY_BYTES = 10 * 1024 * 1024;
51
+ const REQUEST_TIMEOUT_MS = 1e4;
52
+ const TMPFS_HINTS = [
53
+ {
54
+ match: "wordpress",
55
+ paths: ["/run/lock", "/var/run/apache2"]
56
+ },
57
+ {
58
+ match: "mariadb",
59
+ paths: ["/run/mysqld"]
60
+ },
61
+ {
62
+ match: "postgres",
63
+ paths: ["/var/run/postgresql"]
64
+ },
65
+ {
66
+ match: "mysql",
67
+ paths: ["/var/run/mysqld"]
68
+ },
69
+ {
70
+ match: "nginx",
71
+ paths: ["/var/cache/nginx", "/var/run"]
72
+ }
73
+ ];
74
+ const defaultLogger = (reason) => {
75
+ console.warn(reason);
76
+ };
77
+ async function inspectImage(imageRef, opts = {}) {
78
+ const logger = opts.logger ?? defaultLogger;
79
+ const fetchImpl = opts.fetch ?? createDefaultGuardedFetch();
80
+ let parsed;
81
+ try {
82
+ parsed = parseRef(imageRef);
83
+ } catch (err) {
84
+ logger(`inspect-image: ${err instanceof Error ? err.message : String(err)}`);
85
+ return null;
86
+ }
87
+ const ref = parsed.digest ?? parsed.tag ?? "latest";
88
+ try {
89
+ let authHeader = null;
90
+ if (parsed.registry === "docker.io") authHeader = `Bearer ${await getDockerHubToken(parsed.name, fetchImpl)}`;
91
+ let manifestRes = await fetchManifest(parsed.registry, parsed.name, ref, authHeader, fetchImpl);
92
+ if (manifestRes.contentType.includes("manifest.list") || manifestRes.contentType.includes("image.index") || isManifestIndex(manifestRes.manifest)) {
93
+ const child = pickPlatformManifest(manifestRes.manifest);
94
+ if (!child || typeof child.digest !== "string") throw new Error("multi-arch index has no usable child manifest");
95
+ manifestRes = await fetchManifest(parsed.registry, parsed.name, child.digest, authHeader, fetchImpl);
96
+ }
97
+ const config = manifestRes.manifest.config;
98
+ if (!config || typeof config.digest !== "string") throw new Error("manifest has no config descriptor");
99
+ const c = (await fetchBlobJson(parsed.registry, parsed.name, config.digest, authHeader, fetchImpl)).config ?? {};
100
+ const out = {
101
+ image: `${parsed.registry}/${parsed.name}${parsed.digest ? "@" + parsed.digest : ":" + (parsed.tag ?? "latest")}`,
102
+ digest: manifestRes.digest ?? parsed.digest ?? null,
103
+ ports: pickPorts(c.ExposedPorts),
104
+ env: parseEnv(c.Env),
105
+ cmd: Array.isArray(c.Cmd) ? c.Cmd : null,
106
+ entrypoint: Array.isArray(c.Entrypoint) ? c.Entrypoint : null,
107
+ user: typeof c.User === "string" ? c.User : "",
108
+ workingDir: typeof c.WorkingDir === "string" ? c.WorkingDir : "",
109
+ healthcheck: c.Healthcheck !== null && typeof c.Healthcheck === "object" && !Array.isArray(c.Healthcheck) ? c.Healthcheck : null,
110
+ labels: c.Labels !== null && typeof c.Labels === "object" && !Array.isArray(c.Labels) ? c.Labels : null,
111
+ volumes: c.Volumes !== null && typeof c.Volumes === "object" && !Array.isArray(c.Volumes) ? c.Volumes : null,
112
+ suggestedTmpfs: []
113
+ };
114
+ out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [...out.cmd ?? [], ...out.entrypoint ?? []]);
115
+ return out;
116
+ } catch (err) {
117
+ logger(`inspect-image: ${formatErrorChain(err)}`);
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Walk an Error's `cause` chain and join all message strings. undici wraps
123
+ * connection errors (including SSRF blocks from our custom Dispatcher) in
124
+ * a fetch-side TypeError with the underlying cause nested via `.cause`.
125
+ * Surfacing the chain in the logger gives the user the real reason (e.g.,
126
+ * "SSRF blocked: 127.0.0.1 ... loopback") instead of an opaque
127
+ * "fetch failed".
128
+ */
129
+ function formatErrorChain(err) {
130
+ const parts = [];
131
+ let current = err;
132
+ let depth = 0;
133
+ while (current !== null && current !== void 0 && depth < 10) {
134
+ if (current instanceof Error) {
135
+ parts.push(current.message);
136
+ current = current.cause;
137
+ } else {
138
+ parts.push(String(current));
139
+ current = void 0;
140
+ }
141
+ depth += 1;
142
+ }
143
+ return parts.join(" | ");
144
+ }
145
+ let cachedDefaultFetch;
146
+ function createDefaultGuardedFetch() {
147
+ if (!cachedDefaultFetch) cachedDefaultFetch = createGuardedFetch();
148
+ return cachedDefaultFetch;
149
+ }
150
+ function parseRef(ref) {
151
+ let registry = "docker.io";
152
+ let name;
153
+ let tag = null;
154
+ let digest = null;
155
+ let rest = ref;
156
+ const atIdx = rest.indexOf("@");
157
+ if (atIdx >= 0) {
158
+ digest = rest.slice(atIdx + 1);
159
+ rest = rest.slice(0, atIdx);
160
+ }
161
+ const firstSlash = rest.indexOf("/");
162
+ if (firstSlash > 0) {
163
+ const head = rest.slice(0, firstSlash);
164
+ if (head === "localhost" || head.includes(".") || head.includes(":")) {
165
+ registry = head;
166
+ rest = rest.slice(firstSlash + 1);
167
+ }
168
+ }
169
+ if (!digest) {
170
+ const colonIdx = rest.lastIndexOf(":");
171
+ if (colonIdx >= 0) {
172
+ tag = rest.slice(colonIdx + 1);
173
+ name = rest.slice(0, colonIdx);
174
+ } else {
175
+ name = rest;
176
+ tag = "latest";
177
+ }
178
+ } else name = rest;
179
+ if (registry === "docker.io" && !name.includes("/")) name = `library/${name}`;
180
+ for (const component of name.split("/")) if (!OCI_NAME_COMPONENT.test(component)) throw new Error(`invalid name component "${component}" in image ref`);
181
+ if (tag !== null && !OCI_TAG.test(tag)) throw new Error(`invalid tag "${tag}" in image ref`);
182
+ if (digest !== null && !OCI_DIGEST.test(digest)) throw new Error(`invalid digest "${digest}" in image ref (expected sha256:<64-hex>)`);
183
+ return {
184
+ registry,
185
+ name,
186
+ tag,
187
+ digest
188
+ };
189
+ }
190
+ function registryHost(registry) {
191
+ return registry === "docker.io" ? "registry-1.docker.io" : registry;
192
+ }
193
+ async function getDockerHubToken(name, fetchImpl) {
194
+ const res = await capturingFetch(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`, {}, fetchImpl);
195
+ if (res.status === 429) throw new Error("Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)");
196
+ if (res.status !== 200) throw new Error(`Docker Hub token: HTTP ${res.status}`);
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(res.body);
200
+ } catch {
201
+ throw new Error("Docker Hub token: invalid JSON");
202
+ }
203
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.token !== "string") throw new Error("Docker Hub token: missing `token` in response");
204
+ return parsed.token;
205
+ }
206
+ async function fetchManifest(registry, name, ref, authHeader, fetchImpl) {
207
+ const url = `https://${registryHost(registry)}/v2/${name}/manifests/${ref}`;
208
+ const headers = { Accept: ACCEPT_MANIFEST };
209
+ if (authHeader) headers.Authorization = authHeader;
210
+ const res = await capturingFetch(url, { headers }, fetchImpl);
211
+ if (res.status === 401 || res.status === 403) throw new Error(`registry returned ${res.status} on manifest fetch (auth required? private registry?)`);
212
+ if (res.status === 404) {
213
+ const sep = ref.startsWith("sha256:") ? "@" : ":";
214
+ throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);
215
+ }
216
+ if (res.status !== 200) throw new Error(`registry returned ${res.status} on manifest fetch`);
217
+ let parsed;
218
+ try {
219
+ parsed = JSON.parse(res.body);
220
+ } catch {
221
+ throw new Error("manifest is not valid JSON");
222
+ }
223
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("manifest is not a JSON object");
224
+ return {
225
+ manifest: parsed,
226
+ contentType: res.headers.get("content-type") ?? "",
227
+ digest: res.headers.get("docker-content-digest")
228
+ };
229
+ }
230
+ async function fetchBlobJson(registry, name, digest, authHeader, fetchImpl) {
231
+ const url = `https://${registryHost(registry)}/v2/${name}/blobs/${digest}`;
232
+ const headers = {};
233
+ if (authHeader) headers.Authorization = authHeader;
234
+ const res = await capturingFetch(url, { headers }, fetchImpl);
235
+ if (res.status !== 200) throw new Error(`registry returned ${res.status} on blob fetch`);
236
+ try {
237
+ return JSON.parse(res.body);
238
+ } catch {
239
+ throw new Error("blob is not valid JSON");
240
+ }
241
+ }
242
+ /**
243
+ * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed
244
+ * body-size cap. Throws on overflow, timeout, or read error so the outer
245
+ * try/catch produces the fail-soft `null` return.
246
+ */
247
+ async function capturingFetch(url, init, fetchImpl) {
248
+ const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
249
+ let response;
250
+ try {
251
+ response = await fetchImpl(url, {
252
+ ...init,
253
+ signal
254
+ });
255
+ } catch (err) {
256
+ if (err instanceof Error && err.name === "TimeoutError") throw new Error(`request timeout on ${url}`);
257
+ throw err;
258
+ }
259
+ const reader = response.body?.getReader();
260
+ if (!reader) return {
261
+ status: response.status,
262
+ headers: response.headers,
263
+ body: ""
264
+ };
265
+ const chunks = [];
266
+ let totalBytes = 0;
267
+ const decoder = new TextDecoder();
268
+ let body = "";
269
+ try {
270
+ while (true) {
271
+ const { done, value } = await reader.read();
272
+ if (done) break;
273
+ if (value) {
274
+ totalBytes += value.length;
275
+ if (totalBytes > MAX_BODY_BYTES) {
276
+ await reader.cancel();
277
+ throw new Error(`response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`);
278
+ }
279
+ chunks.push(value);
280
+ }
281
+ }
282
+ body = decoder.decode(concatUint8Arrays(chunks));
283
+ } finally {
284
+ reader.releaseLock();
285
+ }
286
+ return {
287
+ status: response.status,
288
+ headers: response.headers,
289
+ body
290
+ };
291
+ }
292
+ function concatUint8Arrays(chunks) {
293
+ if (chunks.length === 0) return new Uint8Array(0);
294
+ if (chunks.length === 1) {
295
+ const only = chunks[0];
296
+ if (only !== void 0) return only;
297
+ }
298
+ let total = 0;
299
+ for (const c of chunks) total += c.length;
300
+ const out = new Uint8Array(total);
301
+ let offset = 0;
302
+ for (const c of chunks) {
303
+ out.set(c, offset);
304
+ offset += c.length;
305
+ }
306
+ return out;
307
+ }
308
+ function pickPorts(exposedPorts) {
309
+ if (exposedPorts === null || typeof exposedPorts !== "object" || Array.isArray(exposedPorts)) return [];
310
+ return Object.keys(exposedPorts).sort();
311
+ }
312
+ function parseEnv(env) {
313
+ if (!Array.isArray(env)) return {};
314
+ const out = {};
315
+ for (const kv of env) {
316
+ if (typeof kv !== "string") continue;
317
+ const i = kv.indexOf("=");
318
+ if (i > 0) {
319
+ const key = kv.slice(0, i);
320
+ out[key] = kv.slice(i + 1);
321
+ } else out[kv] = "";
322
+ }
323
+ return out;
324
+ }
325
+ function isManifestIndex(m) {
326
+ return Array.isArray(m.manifests);
327
+ }
328
+ function pickPlatformManifest(index) {
329
+ const list = index.manifests;
330
+ if (!Array.isArray(list)) return null;
331
+ const linuxAmd64 = list.find((m) => m !== null && typeof m === "object" && m.platform !== null && typeof m.platform === "object" && m.platform.os === "linux" && m.platform.architecture === "amd64");
332
+ if (linuxAmd64) return linuxAmd64;
333
+ const first = list[0];
334
+ if (first !== null && typeof first === "object" && !Array.isArray(first)) return first;
335
+ return null;
336
+ }
337
+ function suggestedTmpfsFor(name, cmdAndEntrypoint) {
338
+ const haystack = [name, ...cmdAndEntrypoint].join(" ").toLowerCase();
339
+ for (const hint of TMPFS_HINTS) if (haystack.includes(hint.match)) return [...hint.paths];
340
+ return [];
341
+ }
342
+ //#endregion
343
+ export { inspectImage };
344
+
345
+ //# sourceMappingURL=inspect-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-image.js","names":[],"sources":["../../src/internals/inspect-image.ts"],"sourcesContent":["import { createGuardedFetch } from './guarded-fetch.js';\n\n/**\n * Inspect a public container image via the OCI Distribution API. Returns\n * the manifest digest, exposed ports, image defaults (env / cmd /\n * entrypoint / user / workingDir), healthcheck, labels, volumes, and a\n * heuristic `suggestedTmpfs` list for known-good Fred image families.\n *\n * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`\n * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6\n * normalization — see `guarded-fetch.ts` for the design).\n *\n * **Fail-soft contract:** returns `null` on every non-fatal failure mode:\n * - 401 / 403 (private registry / auth required)\n * - 429 (Docker Hub rate-limit)\n * - OCI grammar violation in the `imageRef`\n * - Manifest body exceeding the 10 MiB cap\n * - Request timeout (10s)\n * - Unparseable manifest / blob JSON\n * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)\n * Callers treat `null` as \"no info, ask the user\".\n * Diagnostics flow through `opts.logger` instead of stderr.\n *\n * ## Security — SSRF (production callers MUST read)\n *\n * `imageRef` is user-controlled (it comes from `DeploySpec.image`).\n * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`\n * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would\n * cause this function to probe internal services on the host. The CJS\n * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to\n * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which\n * blocks at connect time.\n *\n * Opt-out-of-safety semantics (parent's PR-2 directive): the default\n * `opts.fetch = createGuardedFetch()` is safe by construction. Callers\n * pass their own `opts.fetch` ONLY for tests (canned responses) or\n * unusual production cases (e.g. a trusted private registry on an RFC\n * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s\n * JSDoc for the production-guard contract.\n */\n\nconst ACCEPT_MANIFEST = [\n 'application/vnd.oci.image.index.v1+json',\n 'application/vnd.oci.image.manifest.v1+json',\n 'application/vnd.docker.distribution.manifest.list.v2+json',\n 'application/vnd.docker.distribution.manifest.v2+json',\n].join(', ');\n\n// OCI Distribution Spec v1.1 grammar for URL-interpolated fields. Validate\n// BEFORE the URL is constructed; the `imageRef` flag is user-controlled,\n// so a malformed input like `foo/bar:..%2F..%2Fconfig` must be rejected\n// here rather than forwarded to the registry.\nconst OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\\.|_|__|-+)[a-z0-9]+)*$/;\nconst OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;\nconst OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;\n\n// Body-size cap (10 MiB). Real-world configs are <100 KB; even JVM-rich\n// images rarely exceed a few MB. Anything over 10 MiB indicates a hostile\n// or buggy registry; abort rather than risk OOM.\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n// Request timeout — 10s. Registry queries should be fast; longer waits\n// indicate a hung registry.\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n// Heuristic table: image base name or resolved Cmd/Entrypoint contains\n// one of these tokens → suggest the corresponding tmpfs paths. Order:\n// longer/more-specific tokens first when there's ambiguity.\nconst TMPFS_HINTS: ReadonlyArray<{\n readonly match: string;\n readonly paths: readonly string[];\n}> = [\n { match: 'wordpress', paths: ['/run/lock', '/var/run/apache2'] },\n { match: 'mariadb', paths: ['/run/mysqld'] },\n { match: 'postgres', paths: ['/var/run/postgresql'] },\n { match: 'mysql', paths: ['/var/run/mysqld'] },\n { match: 'nginx', paths: ['/var/cache/nginx', '/var/run'] },\n];\n\nexport interface ImageInfo {\n image: string;\n digest: string | null;\n ports: string[];\n env: Record<string, string>;\n cmd: string[] | null;\n entrypoint: string[] | null;\n user: string;\n workingDir: string;\n healthcheck: Record<string, unknown> | null;\n labels: Record<string, string> | null;\n volumes: Record<string, unknown> | null;\n suggestedTmpfs: string[];\n}\n\nexport interface InspectImageOptions {\n /**\n * HTTP client. **Production callers SHOULD use the default** (which is\n * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /\n * metadata at connect time). Tests pass canned implementations.\n * Browser/Deno consumers pass their own SSRF-guarded fetch since\n * `createGuardedFetch()` throws on non-Node runtimes.\n */\n fetch?: typeof fetch;\n /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */\n logger?: (reason: string) => void;\n}\n\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n\ninterface ParsedRef {\n registry: string;\n name: string;\n tag: string | null;\n digest: string | null;\n}\n\nexport async function inspectImage(\n imageRef: string,\n opts: InspectImageOptions = {},\n): Promise<ImageInfo | null> {\n const logger = opts.logger ?? defaultLogger;\n const fetchImpl: typeof fetch = opts.fetch ?? createDefaultGuardedFetch();\n\n let parsed: ParsedRef;\n try {\n parsed = parseRef(imageRef);\n } catch (err) {\n logger(\n `inspect-image: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n\n const ref = parsed.digest ?? parsed.tag ?? 'latest';\n try {\n let authHeader: string | null = null;\n if (parsed.registry === 'docker.io') {\n const token = await getDockerHubToken(parsed.name, fetchImpl);\n authHeader = `Bearer ${token}`;\n }\n\n // Step 1: fetch manifest (may be an index → pick platform → refetch).\n let manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n ref,\n authHeader,\n fetchImpl,\n );\n if (\n manifestRes.contentType.includes('manifest.list') ||\n manifestRes.contentType.includes('image.index') ||\n isManifestIndex(manifestRes.manifest)\n ) {\n const child = pickPlatformManifest(manifestRes.manifest);\n if (!child || typeof child.digest !== 'string') {\n throw new Error('multi-arch index has no usable child manifest');\n }\n manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n child.digest,\n authHeader,\n fetchImpl,\n );\n }\n\n // Step 2: fetch the config blob — the actual image config lives there.\n const config = manifestRes.manifest.config as\n | { digest?: unknown }\n | undefined;\n if (!config || typeof config.digest !== 'string') {\n throw new Error('manifest has no config descriptor');\n }\n const configBlob = await fetchBlobJson(\n parsed.registry,\n parsed.name,\n config.digest,\n authHeader,\n fetchImpl,\n );\n const c = (configBlob.config ?? {}) as Record<string, unknown>;\n\n const out: ImageInfo = {\n image: `${parsed.registry}/${parsed.name}${parsed.digest ? '@' + parsed.digest : ':' + (parsed.tag ?? 'latest')}`,\n digest: manifestRes.digest ?? parsed.digest ?? null,\n ports: pickPorts(c.ExposedPorts),\n env: parseEnv(c.Env),\n cmd: Array.isArray(c.Cmd) ? (c.Cmd as string[]) : null,\n entrypoint: Array.isArray(c.Entrypoint)\n ? (c.Entrypoint as string[])\n : null,\n user: typeof c.User === 'string' ? c.User : '',\n workingDir: typeof c.WorkingDir === 'string' ? c.WorkingDir : '',\n healthcheck:\n c.Healthcheck !== null &&\n typeof c.Healthcheck === 'object' &&\n !Array.isArray(c.Healthcheck)\n ? (c.Healthcheck as Record<string, unknown>)\n : null,\n labels:\n c.Labels !== null &&\n typeof c.Labels === 'object' &&\n !Array.isArray(c.Labels)\n ? (c.Labels as Record<string, string>)\n : null,\n volumes:\n c.Volumes !== null &&\n typeof c.Volumes === 'object' &&\n !Array.isArray(c.Volumes)\n ? (c.Volumes as Record<string, unknown>)\n : null,\n suggestedTmpfs: [],\n };\n out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [\n ...(out.cmd ?? []),\n ...(out.entrypoint ?? []),\n ]);\n\n return out;\n } catch (err) {\n logger(`inspect-image: ${formatErrorChain(err)}`);\n return null;\n }\n}\n\n/**\n * Walk an Error's `cause` chain and join all message strings. undici wraps\n * connection errors (including SSRF blocks from our custom Dispatcher) in\n * a fetch-side TypeError with the underlying cause nested via `.cause`.\n * Surfacing the chain in the logger gives the user the real reason (e.g.,\n * \"SSRF blocked: 127.0.0.1 ... loopback\") instead of an opaque\n * \"fetch failed\".\n */\nfunction formatErrorChain(err: unknown): string {\n const parts: string[] = [];\n let current: unknown = err;\n let depth = 0;\n // Defensive bound — sane Error chains are <5 levels; cap at 10 to avoid\n // pathological cycles.\n while (current !== null && current !== undefined && depth < 10) {\n if (current instanceof Error) {\n parts.push(current.message);\n current = (current as Error & { cause?: unknown }).cause;\n } else {\n parts.push(String(current));\n current = undefined;\n }\n depth += 1;\n }\n return parts.join(' | ');\n}\n\nlet cachedDefaultFetch: typeof fetch | undefined;\nfunction createDefaultGuardedFetch(): typeof fetch {\n if (!cachedDefaultFetch) {\n cachedDefaultFetch = createGuardedFetch();\n }\n return cachedDefaultFetch;\n}\n\nfunction parseRef(ref: string): ParsedRef {\n // \"<reg>/<name>@sha256:<digest>\" | \"<reg>/<name>:<tag>\" | \"<name>\" | \"<name>:<tag>\"\n let registry = 'docker.io';\n let name: string;\n let tag: string | null = null;\n let digest: string | null = null;\n\n let rest = ref;\n const atIdx = rest.indexOf('@');\n if (atIdx >= 0) {\n digest = rest.slice(atIdx + 1);\n rest = rest.slice(0, atIdx);\n }\n\n // Detect registry segment: head before first `/` is a registry only if\n // it has a `.` or `:` (port) or is `localhost`.\n const firstSlash = rest.indexOf('/');\n if (firstSlash > 0) {\n const head = rest.slice(0, firstSlash);\n if (head === 'localhost' || head.includes('.') || head.includes(':')) {\n registry = head;\n rest = rest.slice(firstSlash + 1);\n }\n }\n\n if (!digest) {\n const colonIdx = rest.lastIndexOf(':');\n if (colonIdx >= 0) {\n tag = rest.slice(colonIdx + 1);\n name = rest.slice(0, colonIdx);\n } else {\n name = rest;\n tag = 'latest';\n }\n } else {\n name = rest;\n }\n\n // Docker Hub library prefix for single-segment names (\"nginx\" → \"library/nginx\").\n if (registry === 'docker.io' && !name.includes('/')) {\n name = `library/${name}`;\n }\n\n // Validate URL-interpolated fields against OCI Distribution Spec grammar\n // BEFORE the URL is constructed. The ref strings reach the user via\n // `DeploySpec.image`, so malformed input must be rejected here.\n for (const component of name.split('/')) {\n if (!OCI_NAME_COMPONENT.test(component)) {\n throw new Error(`invalid name component \"${component}\" in image ref`);\n }\n }\n if (tag !== null && !OCI_TAG.test(tag)) {\n throw new Error(`invalid tag \"${tag}\" in image ref`);\n }\n if (digest !== null && !OCI_DIGEST.test(digest)) {\n throw new Error(\n `invalid digest \"${digest}\" in image ref (expected sha256:<64-hex>)`,\n );\n }\n\n return { registry, name, tag, digest };\n}\n\nfunction registryHost(registry: string): string {\n // Docker Hub's image API lives at registry-1.docker.io even though the\n // canonical \"registry\" name is docker.io.\n return registry === 'docker.io' ? 'registry-1.docker.io' : registry;\n}\n\nasync function getDockerHubToken(\n name: string,\n fetchImpl: typeof fetch,\n): Promise<string> {\n // Docker Hub requires anonymous access still go through a token grant.\n // Surface 429 specifically — anonymous pulls are rate-limited per-IP\n // and a 60-min wait fixes it. Without this special case the user sees\n // the same fail-soft `null` outcome as a hard 401 with no signal that\n // the situation is temporary.\n const res = await capturingFetch(\n `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`,\n {},\n fetchImpl,\n );\n if (res.status === 429) {\n throw new Error(\n 'Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)',\n );\n }\n if (res.status !== 200) {\n throw new Error(`Docker Hub token: HTTP ${res.status}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('Docker Hub token: invalid JSON');\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n typeof (parsed as { token?: unknown }).token !== 'string'\n ) {\n throw new Error('Docker Hub token: missing `token` in response');\n }\n return (parsed as { token: string }).token;\n}\n\nasync function fetchManifest(\n registry: string,\n name: string,\n ref: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{\n manifest: Record<string, unknown>;\n contentType: string;\n digest: string | null;\n}> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/manifests/${ref}`;\n const headers: Record<string, string> = { Accept: ACCEPT_MANIFEST };\n if (authHeader) headers.Authorization = authHeader;\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `registry returned ${res.status} on manifest fetch (auth required? private registry?)`,\n );\n }\n if (res.status === 404) {\n // Digest-pinned refs use `@sha256:...`; tag refs use `:tag`. Pick the\n // right separator so the error message doesn't show\n // `registry/name:sha256:...` mistakenly.\n const sep = ref.startsWith('sha256:') ? '@' : ':';\n throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);\n }\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on manifest fetch`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('manifest is not valid JSON');\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('manifest is not a JSON object');\n }\n return {\n manifest: parsed as Record<string, unknown>,\n contentType: res.headers.get('content-type') ?? '',\n digest: res.headers.get('docker-content-digest'),\n };\n}\n\nasync function fetchBlobJson(\n registry: string,\n name: string,\n digest: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{ config?: unknown }> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/blobs/${digest}`;\n const headers: Record<string, string> = {};\n if (authHeader) headers.Authorization = authHeader;\n // undici fetch follows redirects by default; registries 307 → CDN.\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on blob fetch`);\n }\n try {\n return JSON.parse(res.body) as { config?: unknown };\n } catch {\n throw new Error('blob is not valid JSON');\n }\n}\n\ninterface CapturedResponse {\n status: number;\n headers: Headers;\n body: string;\n}\n\n/**\n * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed\n * body-size cap. Throws on overflow, timeout, or read error so the outer\n * try/catch produces the fail-soft `null` return.\n */\nasync function capturingFetch(\n url: string,\n init: RequestInit,\n fetchImpl: typeof fetch,\n): Promise<CapturedResponse> {\n const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetchImpl(url, { ...init, signal });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new Error(`request timeout on ${url}`);\n }\n throw err;\n }\n // Stream the body with a manual chunk-accumulation cap. Avoids the\n // unbounded `await response.text()` path that would let a hostile\n // registry exhaust memory.\n const reader = response.body?.getReader();\n if (!reader) {\n return { status: response.status, headers: response.headers, body: '' };\n }\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const decoder = new TextDecoder();\n let body = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n totalBytes += value.length;\n if (totalBytes > MAX_BODY_BYTES) {\n await reader.cancel();\n throw new Error(\n `response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`,\n );\n }\n chunks.push(value);\n }\n }\n body = decoder.decode(concatUint8Arrays(chunks));\n } finally {\n reader.releaseLock();\n }\n return { status: response.status, headers: response.headers, body };\n}\n\nfunction concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) return new Uint8Array(0);\n if (chunks.length === 1) {\n const only = chunks[0];\n if (only !== undefined) return only;\n }\n let total = 0;\n for (const c of chunks) total += c.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n}\n\nfunction pickPorts(exposedPorts: unknown): string[] {\n if (\n exposedPorts === null ||\n typeof exposedPorts !== 'object' ||\n Array.isArray(exposedPorts)\n ) {\n return [];\n }\n return Object.keys(exposedPorts as Record<string, unknown>).sort();\n}\n\nfunction parseEnv(env: unknown): Record<string, string> {\n if (!Array.isArray(env)) return {};\n const out: Record<string, string> = {};\n for (const kv of env) {\n if (typeof kv !== 'string') continue;\n const i = kv.indexOf('=');\n if (i > 0) {\n const key = kv.slice(0, i);\n const value = kv.slice(i + 1);\n out[key] = value;\n } else {\n out[kv] = '';\n }\n }\n return out;\n}\n\nfunction isManifestIndex(m: Record<string, unknown>): boolean {\n return Array.isArray(m.manifests);\n}\n\nfunction pickPlatformManifest(\n index: Record<string, unknown>,\n): Record<string, unknown> | null {\n const list = index.manifests;\n if (!Array.isArray(list)) return null;\n const linuxAmd64 = list.find(\n (m): m is Record<string, unknown> =>\n m !== null &&\n typeof m === 'object' &&\n (m as { platform?: unknown }).platform !== null &&\n typeof (m as { platform?: unknown }).platform === 'object' &&\n (m as { platform: { os?: unknown } }).platform.os === 'linux' &&\n (m as { platform: { architecture?: unknown } }).platform.architecture ===\n 'amd64',\n );\n if (linuxAmd64) return linuxAmd64;\n // Fall back to first entry.\n const first = list[0];\n if (first !== null && typeof first === 'object' && !Array.isArray(first)) {\n return first as Record<string, unknown>;\n }\n return null;\n}\n\nfunction suggestedTmpfsFor(\n name: string,\n cmdAndEntrypoint: ReadonlyArray<string>,\n): string[] {\n const haystack = [name, ...cmdAndEntrypoint].join(' ').toLowerCase();\n for (const hint of TMPFS_HINTS) {\n if (haystack.includes(hint.match)) return [...hint.paths];\n }\n return [];\n}\n"],"mappings":"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"}