@manifest-network/manifest-agent-core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/close-lease.d.ts +3 -2
- package/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js +4 -3
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +3 -2
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +245 -77
- package/dist/deploy-app.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internals/build-fred-input.d.ts +38 -0
- package/dist/internals/build-fred-input.d.ts.map +1 -0
- package/dist/internals/build-fred-input.js +147 -0
- package/dist/internals/build-fred-input.js.map +1 -0
- package/dist/internals/classify-deploy-error.d.ts +13 -9
- package/dist/internals/classify-deploy-error.d.ts.map +1 -1
- package/dist/internals/classify-deploy-error.js +15 -11
- package/dist/internals/classify-deploy-error.js.map +1 -1
- package/dist/internals/classify-deploy-response.d.ts.map +1 -1
- package/dist/internals/classify-deploy-response.js.map +1 -1
- package/dist/internals/connection.d.ts.map +1 -1
- package/dist/internals/connection.js.map +1 -1
- package/dist/internals/evaluate-readiness-from-fred.d.ts +28 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.js +94 -0
- package/dist/internals/evaluate-readiness-from-fred.js.map +1 -0
- package/dist/internals/evaluate-readiness.d.ts.map +1 -1
- package/dist/internals/evaluate-readiness.js.map +1 -1
- package/dist/internals/find-sku-uuid.d.ts.map +1 -1
- package/dist/internals/find-sku-uuid.js.map +1 -1
- package/dist/internals/format-success.d.ts.map +1 -1
- package/dist/internals/format-success.js.map +1 -1
- package/dist/internals/guarded-fetch.d.ts +2 -138
- package/dist/internals/guarded-fetch.js +1 -241
- package/dist/internals/humanize-denom.d.ts.map +1 -1
- package/dist/internals/humanize-denom.js.map +1 -1
- package/dist/internals/inspect-image.d.ts.map +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/lease-items.d.ts.map +1 -1
- package/dist/internals/lease-items.js +1 -4
- package/dist/internals/lease-items.js.map +1 -1
- package/dist/internals/lease-state.d.ts.map +1 -1
- package/dist/internals/lease-state.js.map +1 -1
- package/dist/internals/render-deployment-plan.d.ts.map +1 -1
- package/dist/internals/render-deployment-plan.js.map +1 -1
- package/dist/internals/render-intent-recap.d.ts.map +1 -1
- package/dist/internals/render-intent-recap.js.map +1 -1
- package/dist/internals/render-partial-success-prompt.d.ts.map +1 -1
- package/dist/internals/render-partial-success-prompt.js.map +1 -1
- package/dist/internals/save-manifest.d.ts.map +1 -1
- package/dist/internals/save-manifest.js.map +1 -1
- package/dist/internals/secret-denylist.d.ts.map +1 -1
- package/dist/internals/secret-denylist.js.map +1 -1
- package/dist/internals/spec-normalize.d.ts.map +1 -1
- package/dist/internals/spec-normalize.js.map +1 -1
- package/dist/internals/verify-domain-state.d.ts.map +1 -1
- package/dist/internals/verify-domain-state.js.map +1 -1
- package/dist/internals/verify-recover.d.ts.map +1 -1
- package/dist/internals/verify-recover.js.map +1 -1
- package/dist/manage-domain.d.ts +3 -2
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js +4 -3
- package/dist/manage-domain.js.map +1 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -7
- package/dist/internals/guarded-fetch.d.ts.map +0 -1
- package/dist/internals/guarded-fetch.js.map +0 -1
|
@@ -1,138 +1,2 @@
|
|
|
1
|
-
|
|
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
|
|
1
|
+
import { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, GuardedFetch, createGuardedFetch, isBlocked } from "@manifest-network/manifest-mcp-core/guarded-fetch";
|
|
2
|
+
export { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, type GuardedFetch, createGuardedFetch, isBlocked };
|
|
@@ -1,242 +1,2 @@
|
|
|
1
|
-
import
|
|
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
|
|
1
|
+
import { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, createGuardedFetch, isBlocked } from "@manifest-network/manifest-mcp-core/guarded-fetch";
|
|
240
2
|
export { BLOCKED_RANGES_IPV4, BLOCKED_RANGES_IPV6, createGuardedFetch, isBlocked };
|
|
241
|
-
|
|
242
|
-
//# sourceMappingURL=guarded-fetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"humanize-denom.d.ts","names":[],"sources":["../../src/internals/humanize-denom.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"humanize-denom.d.ts","names":[],"sources":["../../src/internals/humanize-denom.ts"],"mappings":";;;;;;;AA+KoB;AAwBpB;cAjJa,eAAA,EAAiB,QAA4C;AAAA,iBAEpD,iBAAA,CACpB,iBAAA,YACC,OAAO,CAAC,QAAA;;;;;;AA+IS;;;;iBAtEJ,gBAAA,CAAiB,MAAA,UAAgB,QAAgB;;;;;;iBAuBjD,YAAA,CACd,MAAA,UACA,KAAA,6BACA,QAAA,EAAU,QAAQ;;;;;iBAgBJ,gBAAA,CACd,QAAA,EAAU,aAAA;EAAgB,KAAA;EAAgB,MAAA;AAAA,cAC1C,QAAA,EAAU,QAAQ;;;;;;;iBAwBJ,aAAA,CACd,KAAA,6BACA,QAAA,EAAU,QAAQ"}
|
|
@@ -1 +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"}
|
|
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;AAAK;AAEzE,eAAsB,kBACpB,mBACmB;CACnB,IAAI,CAAC,mBAAmB,OAAO;CAC/B,IACE,OAAO,YAAY,eACnB,OAAO,QAAQ,UAAU,SAAS,UAIlC,MAAM,IAAI,MACR,2GACF;CAEF,IAAI;CACJ,IAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,KAAK,MAAM,aAAa,mBAAmB,MAAM,CAAC;CAC1D,SAAS,KAAK;EAMZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAC/D,QAAQ,KACN,kCAAkC,kBAAkB,IAAI,QAAQ,0DAElE;EACA,OAAO;CACT;CAMA,MAAM,sBAAM,IAAI,IAAyB;CACzC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,YAAa,IAAgC;EACnD,IAAI,MAAM,QAAQ,SAAS;QACpB,MAAM,KAAK,WACd,IACE,MAAM,QACN,OAAO,MAAM,YACb,OAAQ,EAA0B,UAAU,YAC5C,OAAQ,EAA2B,WAAW,UAC9C;IACA,MAAM,QAAQ;IACd,IAAI,IAAI,MAAM,OAAO;KACnB,QAAQ,MAAM;KACd,UAAU;IACZ,CAAC;GACH;;CAGN;CAEA,OAAO;EACL,SAAS,UAAU;GACjB,IAAI,OAAO,UAAU,UAAU,OAAO;GACtC,OAAO,IAAI,IAAI,KAAK,KAAK;EAC3B;EACA;CACF;AACF;;;;;;;;;;AAWA,SAAgB,iBAAiB,QAAgB,UAA0B;CACzE,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,MAAM;CACxB,QAAQ;EACN,OAAO,OAAO,MAAM;CACtB;CACA,MAAM,WAAW,SAAS;CAC1B,IAAI,UAAU,SAAS,CAAC;CACxB,MAAM,UAAU,OAAO,OAAO,QAAQ;CACtC,MAAM,QAAQ,SAAS;CAEvB,MAAM,WADO,SAAS,SACD,SAAS,EAAE,SAAS,UAAU,GAAG,EAAE,QAAQ,OAAO,EAAE;CACzE,IAAI,MAAM,QAAQ,SAAS,IAAI,GAAG,MAAM,GAAG,YAAY,GAAG;CAC1D,IAAI,UAAU,MAAM,IAAI;CACxB,OAAO;AACT;;;;;;AAOA,SAAgB,aACd,QACA,OACA,UACQ;CACR,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO,GAAG;CACrD,MAAM,SAAS,SAAS,OAAO,KAAK;CACpC,IAAI,QACF,OAAO,GAAG,iBAAiB,QAAQ,OAAO,QAAQ,EAAE,GAAG,OAAO;CAIhE,OAAO,GAAG,OAAO,GAAG;AACtB;;;;;AAMA,SAAgB,iBACd,UACA,UACQ;CACR,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,GAAG,OAAO;CAC9D,OAAO,SACJ,KAAK,MAAM;EASV,OAAO,aAPL,MAAM,QAAQ,OAAO,MAAM,YAAY,YAAY,KAAK,EAAE,UAAU,OAChE,OAAO,EAAE,MAAM,IACf,KAEJ,MAAM,QAAQ,OAAO,MAAM,YAAY,WAAW,IAC7C,EAAE,QACH,KAAA,GAC6B,QAAQ;CAC7C,CAAC,EACA,KAAK,IAAI;AACd;;;;;;;AAQA,SAAgB,cACd,OACA,UACQ;CACR,IAAI,CAAC,OAAO,OAAO,OAAO,SAAS,EAAE;CAErC,OADe,SAAS,OAAO,KACnB,GAAG,UAAU;AAC3B"}
|
|
@@ -1 +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,
|
|
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,KAAK;EAZpB;EAcA,MAAA,IAAU,MAAA;AAAA;AAAA,iBAcU,YAAA,CACpB,QAAA,UACA,IAAA,GAAM,mBAAA,GACL,OAAA,CAAQ,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-image.js","names":[],"sources":["../../src/internals/inspect-image.ts"],"sourcesContent":["import { createGuardedFetch } from './guarded-fetch.js';\n\n/**\n * Inspect a public container image via the OCI Distribution API. Returns\n * the manifest digest, exposed ports, image defaults (env / cmd /\n * entrypoint / user / workingDir), healthcheck, labels, volumes, and a\n * heuristic `suggestedTmpfs` list for known-good Fred image families.\n *\n * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`\n * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6\n * normalization — see `guarded-fetch.ts` for the design).\n *\n * **Fail-soft contract:** returns `null` on every non-fatal failure mode:\n * - 401 / 403 (private registry / auth required)\n * - 429 (Docker Hub rate-limit)\n * - OCI grammar violation in the `imageRef`\n * - Manifest body exceeding the 10 MiB cap\n * - Request timeout (10s)\n * - Unparseable manifest / blob JSON\n * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)\n * Callers treat `null` as \"no info, ask the user\".\n * Diagnostics flow through `opts.logger` instead of stderr.\n *\n * ## Security — SSRF (production callers MUST read)\n *\n * `imageRef` is user-controlled (it comes from `DeploySpec.image`).\n * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`\n * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would\n * cause this function to probe internal services on the host. The CJS\n * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to\n * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which\n * blocks at connect time.\n *\n * Opt-out-of-safety semantics (parent's PR-2 directive): the default\n * `opts.fetch = createGuardedFetch()` is safe by construction. Callers\n * pass their own `opts.fetch` ONLY for tests (canned responses) or\n * unusual production cases (e.g. a trusted private registry on an RFC\n * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s\n * JSDoc for the production-guard contract.\n */\n\nconst ACCEPT_MANIFEST = [\n 'application/vnd.oci.image.index.v1+json',\n 'application/vnd.oci.image.manifest.v1+json',\n 'application/vnd.docker.distribution.manifest.list.v2+json',\n 'application/vnd.docker.distribution.manifest.v2+json',\n].join(', ');\n\n// OCI Distribution Spec v1.1 grammar for URL-interpolated fields. Validate\n// BEFORE the URL is constructed; the `imageRef` flag is user-controlled,\n// so a malformed input like `foo/bar:..%2F..%2Fconfig` must be rejected\n// here rather than forwarded to the registry.\nconst OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\\.|_|__|-+)[a-z0-9]+)*$/;\nconst OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;\nconst OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;\n\n// Body-size cap (10 MiB). Real-world configs are <100 KB; even JVM-rich\n// images rarely exceed a few MB. Anything over 10 MiB indicates a hostile\n// or buggy registry; abort rather than risk OOM.\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n// Request timeout — 10s. Registry queries should be fast; longer waits\n// indicate a hung registry.\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n// Heuristic table: image base name or resolved Cmd/Entrypoint contains\n// one of these tokens → suggest the corresponding tmpfs paths. Order:\n// longer/more-specific tokens first when there's ambiguity.\nconst TMPFS_HINTS: ReadonlyArray<{\n readonly match: string;\n readonly paths: readonly string[];\n}> = [\n { match: 'wordpress', paths: ['/run/lock', '/var/run/apache2'] },\n { match: 'mariadb', paths: ['/run/mysqld'] },\n { match: 'postgres', paths: ['/var/run/postgresql'] },\n { match: 'mysql', paths: ['/var/run/mysqld'] },\n { match: 'nginx', paths: ['/var/cache/nginx', '/var/run'] },\n];\n\nexport interface ImageInfo {\n image: string;\n digest: string | null;\n ports: string[];\n env: Record<string, string>;\n cmd: string[] | null;\n entrypoint: string[] | null;\n user: string;\n workingDir: string;\n healthcheck: Record<string, unknown> | null;\n labels: Record<string, string> | null;\n volumes: Record<string, unknown> | null;\n suggestedTmpfs: string[];\n}\n\nexport interface InspectImageOptions {\n /**\n * HTTP client. **Production callers SHOULD use the default** (which is\n * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /\n * metadata at connect time). Tests pass canned implementations.\n * Browser/Deno consumers pass their own SSRF-guarded fetch since\n * `createGuardedFetch()` throws on non-Node runtimes.\n */\n fetch?: typeof fetch;\n /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */\n logger?: (reason: string) => void;\n}\n\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n\ninterface ParsedRef {\n registry: string;\n name: string;\n tag: string | null;\n digest: string | null;\n}\n\nexport async function inspectImage(\n imageRef: string,\n opts: InspectImageOptions = {},\n): Promise<ImageInfo | null> {\n const logger = opts.logger ?? defaultLogger;\n const fetchImpl: typeof fetch = opts.fetch ?? createDefaultGuardedFetch();\n\n let parsed: ParsedRef;\n try {\n parsed = parseRef(imageRef);\n } catch (err) {\n logger(\n `inspect-image: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n\n const ref = parsed.digest ?? parsed.tag ?? 'latest';\n try {\n let authHeader: string | null = null;\n if (parsed.registry === 'docker.io') {\n const token = await getDockerHubToken(parsed.name, fetchImpl);\n authHeader = `Bearer ${token}`;\n }\n\n // Step 1: fetch manifest (may be an index → pick platform → refetch).\n let manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n ref,\n authHeader,\n fetchImpl,\n );\n if (\n manifestRes.contentType.includes('manifest.list') ||\n manifestRes.contentType.includes('image.index') ||\n isManifestIndex(manifestRes.manifest)\n ) {\n const child = pickPlatformManifest(manifestRes.manifest);\n if (!child || typeof child.digest !== 'string') {\n throw new Error('multi-arch index has no usable child manifest');\n }\n manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n child.digest,\n authHeader,\n fetchImpl,\n );\n }\n\n // Step 2: fetch the config blob — the actual image config lives there.\n const config = manifestRes.manifest.config as\n | { digest?: unknown }\n | undefined;\n if (!config || typeof config.digest !== 'string') {\n throw new Error('manifest has no config descriptor');\n }\n const configBlob = await fetchBlobJson(\n parsed.registry,\n parsed.name,\n config.digest,\n authHeader,\n fetchImpl,\n );\n const c = (configBlob.config ?? {}) as Record<string, unknown>;\n\n const out: ImageInfo = {\n image: `${parsed.registry}/${parsed.name}${parsed.digest ? '@' + parsed.digest : ':' + (parsed.tag ?? 'latest')}`,\n digest: manifestRes.digest ?? parsed.digest ?? null,\n ports: pickPorts(c.ExposedPorts),\n env: parseEnv(c.Env),\n cmd: Array.isArray(c.Cmd) ? (c.Cmd as string[]) : null,\n entrypoint: Array.isArray(c.Entrypoint)\n ? (c.Entrypoint as string[])\n : null,\n user: typeof c.User === 'string' ? c.User : '',\n workingDir: typeof c.WorkingDir === 'string' ? c.WorkingDir : '',\n healthcheck:\n c.Healthcheck !== null &&\n typeof c.Healthcheck === 'object' &&\n !Array.isArray(c.Healthcheck)\n ? (c.Healthcheck as Record<string, unknown>)\n : null,\n labels:\n c.Labels !== null &&\n typeof c.Labels === 'object' &&\n !Array.isArray(c.Labels)\n ? (c.Labels as Record<string, string>)\n : null,\n volumes:\n c.Volumes !== null &&\n typeof c.Volumes === 'object' &&\n !Array.isArray(c.Volumes)\n ? (c.Volumes as Record<string, unknown>)\n : null,\n suggestedTmpfs: [],\n };\n out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [\n ...(out.cmd ?? []),\n ...(out.entrypoint ?? []),\n ]);\n\n return out;\n } catch (err) {\n logger(`inspect-image: ${formatErrorChain(err)}`);\n return null;\n }\n}\n\n/**\n * Walk an Error's `cause` chain and join all message strings. undici wraps\n * connection errors (including SSRF blocks from our custom Dispatcher) in\n * a fetch-side TypeError with the underlying cause nested via `.cause`.\n * Surfacing the chain in the logger gives the user the real reason (e.g.,\n * \"SSRF blocked: 127.0.0.1 ... loopback\") instead of an opaque\n * \"fetch failed\".\n */\nfunction formatErrorChain(err: unknown): string {\n const parts: string[] = [];\n let current: unknown = err;\n let depth = 0;\n // Defensive bound — sane Error chains are <5 levels; cap at 10 to avoid\n // pathological cycles.\n while (current !== null && current !== undefined && depth < 10) {\n if (current instanceof Error) {\n parts.push(current.message);\n current = (current as Error & { cause?: unknown }).cause;\n } else {\n parts.push(String(current));\n current = undefined;\n }\n depth += 1;\n }\n return parts.join(' | ');\n}\n\nlet cachedDefaultFetch: typeof fetch | undefined;\nfunction createDefaultGuardedFetch(): typeof fetch {\n if (!cachedDefaultFetch) {\n cachedDefaultFetch = createGuardedFetch();\n }\n return cachedDefaultFetch;\n}\n\nfunction parseRef(ref: string): ParsedRef {\n // \"<reg>/<name>@sha256:<digest>\" | \"<reg>/<name>:<tag>\" | \"<name>\" | \"<name>:<tag>\"\n let registry = 'docker.io';\n let name: string;\n let tag: string | null = null;\n let digest: string | null = null;\n\n let rest = ref;\n const atIdx = rest.indexOf('@');\n if (atIdx >= 0) {\n digest = rest.slice(atIdx + 1);\n rest = rest.slice(0, atIdx);\n }\n\n // Detect registry segment: head before first `/` is a registry only if\n // it has a `.` or `:` (port) or is `localhost`.\n const firstSlash = rest.indexOf('/');\n if (firstSlash > 0) {\n const head = rest.slice(0, firstSlash);\n if (head === 'localhost' || head.includes('.') || head.includes(':')) {\n registry = head;\n rest = rest.slice(firstSlash + 1);\n }\n }\n\n if (!digest) {\n const colonIdx = rest.lastIndexOf(':');\n if (colonIdx >= 0) {\n tag = rest.slice(colonIdx + 1);\n name = rest.slice(0, colonIdx);\n } else {\n name = rest;\n tag = 'latest';\n }\n } else {\n name = rest;\n }\n\n // Docker Hub library prefix for single-segment names (\"nginx\" → \"library/nginx\").\n if (registry === 'docker.io' && !name.includes('/')) {\n name = `library/${name}`;\n }\n\n // Validate URL-interpolated fields against OCI Distribution Spec grammar\n // BEFORE the URL is constructed. The ref strings reach the user via\n // `DeploySpec.image`, so malformed input must be rejected here.\n for (const component of name.split('/')) {\n if (!OCI_NAME_COMPONENT.test(component)) {\n throw new Error(`invalid name component \"${component}\" in image ref`);\n }\n }\n if (tag !== null && !OCI_TAG.test(tag)) {\n throw new Error(`invalid tag \"${tag}\" in image ref`);\n }\n if (digest !== null && !OCI_DIGEST.test(digest)) {\n throw new Error(\n `invalid digest \"${digest}\" in image ref (expected sha256:<64-hex>)`,\n );\n }\n\n return { registry, name, tag, digest };\n}\n\nfunction registryHost(registry: string): string {\n // Docker Hub's image API lives at registry-1.docker.io even though the\n // canonical \"registry\" name is docker.io.\n return registry === 'docker.io' ? 'registry-1.docker.io' : registry;\n}\n\nasync function getDockerHubToken(\n name: string,\n fetchImpl: typeof fetch,\n): Promise<string> {\n // Docker Hub requires anonymous access still go through a token grant.\n // Surface 429 specifically — anonymous pulls are rate-limited per-IP\n // and a 60-min wait fixes it. Without this special case the user sees\n // the same fail-soft `null` outcome as a hard 401 with no signal that\n // the situation is temporary.\n const res = await capturingFetch(\n `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`,\n {},\n fetchImpl,\n );\n if (res.status === 429) {\n throw new Error(\n 'Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)',\n );\n }\n if (res.status !== 200) {\n throw new Error(`Docker Hub token: HTTP ${res.status}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('Docker Hub token: invalid JSON');\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n typeof (parsed as { token?: unknown }).token !== 'string'\n ) {\n throw new Error('Docker Hub token: missing `token` in response');\n }\n return (parsed as { token: string }).token;\n}\n\nasync function fetchManifest(\n registry: string,\n name: string,\n ref: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{\n manifest: Record<string, unknown>;\n contentType: string;\n digest: string | null;\n}> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/manifests/${ref}`;\n const headers: Record<string, string> = { Accept: ACCEPT_MANIFEST };\n if (authHeader) headers.Authorization = authHeader;\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `registry returned ${res.status} on manifest fetch (auth required? private registry?)`,\n );\n }\n if (res.status === 404) {\n // Digest-pinned refs use `@sha256:...`; tag refs use `:tag`. Pick the\n // right separator so the error message doesn't show\n // `registry/name:sha256:...` mistakenly.\n const sep = ref.startsWith('sha256:') ? '@' : ':';\n throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);\n }\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on manifest fetch`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('manifest is not valid JSON');\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('manifest is not a JSON object');\n }\n return {\n manifest: parsed as Record<string, unknown>,\n contentType: res.headers.get('content-type') ?? '',\n digest: res.headers.get('docker-content-digest'),\n };\n}\n\nasync function fetchBlobJson(\n registry: string,\n name: string,\n digest: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{ config?: unknown }> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/blobs/${digest}`;\n const headers: Record<string, string> = {};\n if (authHeader) headers.Authorization = authHeader;\n // undici fetch follows redirects by default; registries 307 → CDN.\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on blob fetch`);\n }\n try {\n return JSON.parse(res.body) as { config?: unknown };\n } catch {\n throw new Error('blob is not valid JSON');\n }\n}\n\ninterface CapturedResponse {\n status: number;\n headers: Headers;\n body: string;\n}\n\n/**\n * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed\n * body-size cap. Throws on overflow, timeout, or read error so the outer\n * try/catch produces the fail-soft `null` return.\n */\nasync function capturingFetch(\n url: string,\n init: RequestInit,\n fetchImpl: typeof fetch,\n): Promise<CapturedResponse> {\n const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetchImpl(url, { ...init, signal });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new Error(`request timeout on ${url}`);\n }\n throw err;\n }\n // Stream the body with a manual chunk-accumulation cap. Avoids the\n // unbounded `await response.text()` path that would let a hostile\n // registry exhaust memory.\n const reader = response.body?.getReader();\n if (!reader) {\n return { status: response.status, headers: response.headers, body: '' };\n }\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const decoder = new TextDecoder();\n let body = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n totalBytes += value.length;\n if (totalBytes > MAX_BODY_BYTES) {\n await reader.cancel();\n throw new Error(\n `response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`,\n );\n }\n chunks.push(value);\n }\n }\n body = decoder.decode(concatUint8Arrays(chunks));\n } finally {\n reader.releaseLock();\n }\n return { status: response.status, headers: response.headers, body };\n}\n\nfunction concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) return new Uint8Array(0);\n if (chunks.length === 1) {\n const only = chunks[0];\n if (only !== undefined) return only;\n }\n let total = 0;\n for (const c of chunks) total += c.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n}\n\nfunction pickPorts(exposedPorts: unknown): string[] {\n if (\n exposedPorts === null ||\n typeof exposedPorts !== 'object' ||\n Array.isArray(exposedPorts)\n ) {\n return [];\n }\n return Object.keys(exposedPorts as Record<string, unknown>).sort();\n}\n\nfunction parseEnv(env: unknown): Record<string, string> {\n if (!Array.isArray(env)) return {};\n const out: Record<string, string> = {};\n for (const kv of env) {\n if (typeof kv !== 'string') continue;\n const i = kv.indexOf('=');\n if (i > 0) {\n const key = kv.slice(0, i);\n const value = kv.slice(i + 1);\n out[key] = value;\n } else {\n out[kv] = '';\n }\n }\n return out;\n}\n\nfunction isManifestIndex(m: Record<string, unknown>): boolean {\n return Array.isArray(m.manifests);\n}\n\nfunction pickPlatformManifest(\n index: Record<string, unknown>,\n): Record<string, unknown> | null {\n const list = index.manifests;\n if (!Array.isArray(list)) return null;\n const linuxAmd64 = list.find(\n (m): m is Record<string, unknown> =>\n m !== null &&\n typeof m === 'object' &&\n (m as { platform?: unknown }).platform !== null &&\n typeof (m as { platform?: unknown }).platform === 'object' &&\n (m as { platform: { os?: unknown } }).platform.os === 'linux' &&\n (m as { platform: { architecture?: unknown } }).platform.architecture ===\n 'amd64',\n );\n if (linuxAmd64) return linuxAmd64;\n // Fall back to first entry.\n const first = list[0];\n if (first !== null && typeof first === 'object' && !Array.isArray(first)) {\n return first as Record<string, unknown>;\n }\n return null;\n}\n\nfunction suggestedTmpfsFor(\n name: string,\n cmdAndEntrypoint: ReadonlyArray<string>,\n): string[] {\n const haystack = [name, ...cmdAndEntrypoint].join(' ').toLowerCase();\n for (const hint of TMPFS_HINTS) {\n if (haystack.includes(hint.match)) return [...hint.paths];\n }\n return [];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;AAMZ,MAAM,qBAAqB;AAC3B,MAAM,UAAU;AAChB,MAAM,aAAa;AAKnB,MAAM,iBAAiB,KAAK,OAAO;AAInC,MAAM,qBAAqB;AAK3B,MAAM,cAGD;CACH;EAAE,OAAO;EAAa,OAAO,CAAC,aAAa,mBAAmB;EAAE;CAChE;EAAE,OAAO;EAAW,OAAO,CAAC,cAAc;EAAE;CAC5C;EAAE,OAAO;EAAY,OAAO,CAAC,sBAAsB;EAAE;CACrD;EAAE,OAAO;EAAS,OAAO,CAAC,kBAAkB;EAAE;CAC9C;EAAE,OAAO;EAAS,OAAO,CAAC,oBAAoB,WAAW;EAAE;CAC5D;AA8BD,MAAM,iBAA2C,WAAW;AAC1D,SAAQ,KAAK,OAAO;;AAUtB,eAAsB,aACpB,UACA,OAA4B,EAAE,EACH;CAC3B,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,YAA0B,KAAK,SAAS,2BAA2B;CAEzE,IAAI;AACJ,KAAI;AACF,WAAS,SAAS,SAAS;UACpB,KAAK;AACZ,SACE,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACnE;AACD,SAAO;;CAGT,MAAM,MAAM,OAAO,UAAU,OAAO,OAAO;AAC3C,KAAI;EACF,IAAI,aAA4B;AAChC,MAAI,OAAO,aAAa,YAEtB,cAAa,UADC,MAAM,kBAAkB,OAAO,MAAM,UAAU;EAK/D,IAAI,cAAc,MAAM,cACtB,OAAO,UACP,OAAO,MACP,KACA,YACA,UACD;AACD,MACE,YAAY,YAAY,SAAS,gBAAgB,IACjD,YAAY,YAAY,SAAS,cAAc,IAC/C,gBAAgB,YAAY,SAAS,EACrC;GACA,MAAM,QAAQ,qBAAqB,YAAY,SAAS;AACxD,OAAI,CAAC,SAAS,OAAO,MAAM,WAAW,SACpC,OAAM,IAAI,MAAM,gDAAgD;AAElE,iBAAc,MAAM,cAClB,OAAO,UACP,OAAO,MACP,MAAM,QACN,YACA,UACD;;EAIH,MAAM,SAAS,YAAY,SAAS;AAGpC,MAAI,CAAC,UAAU,OAAO,OAAO,WAAW,SACtC,OAAM,IAAI,MAAM,oCAAoC;EAStD,MAAM,KAPa,MAAM,cACvB,OAAO,UACP,OAAO,MACP,OAAO,QACP,YACA,UACD,EACqB,UAAU,EAAE;EAElC,MAAM,MAAiB;GACrB,OAAO,GAAG,OAAO,SAAS,GAAG,OAAO,OAAO,OAAO,SAAS,MAAM,OAAO,SAAS,OAAO,OAAO,OAAO;GACtG,QAAQ,YAAY,UAAU,OAAO,UAAU;GAC/C,OAAO,UAAU,EAAE,aAAa;GAChC,KAAK,SAAS,EAAE,IAAI;GACpB,KAAK,MAAM,QAAQ,EAAE,IAAI,GAAI,EAAE,MAAmB;GAClD,YAAY,MAAM,QAAQ,EAAE,WAAW,GAClC,EAAE,aACH;GACJ,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;GAC5C,YAAY,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;GAC9D,aACE,EAAE,gBAAgB,QAClB,OAAO,EAAE,gBAAgB,YACzB,CAAC,MAAM,QAAQ,EAAE,YAAY,GACxB,EAAE,cACH;GACN,QACE,EAAE,WAAW,QACb,OAAO,EAAE,WAAW,YACpB,CAAC,MAAM,QAAQ,EAAE,OAAO,GACnB,EAAE,SACH;GACN,SACE,EAAE,YAAY,QACd,OAAO,EAAE,YAAY,YACrB,CAAC,MAAM,QAAQ,EAAE,QAAQ,GACpB,EAAE,UACH;GACN,gBAAgB,EAAE;GACnB;AACD,MAAI,iBAAiB,kBAAkB,OAAO,MAAM,CAClD,GAAI,IAAI,OAAO,EAAE,EACjB,GAAI,IAAI,cAAc,EAAE,CACzB,CAAC;AAEF,SAAO;UACA,KAAK;AACZ,SAAO,kBAAkB,iBAAiB,IAAI,GAAG;AACjD,SAAO;;;;;;;;;;;AAYX,SAAS,iBAAiB,KAAsB;CAC9C,MAAM,QAAkB,EAAE;CAC1B,IAAI,UAAmB;CACvB,IAAI,QAAQ;AAGZ,QAAO,YAAY,QAAQ,YAAY,KAAA,KAAa,QAAQ,IAAI;AAC9D,MAAI,mBAAmB,OAAO;AAC5B,SAAM,KAAK,QAAQ,QAAQ;AAC3B,aAAW,QAAwC;SAC9C;AACL,SAAM,KAAK,OAAO,QAAQ,CAAC;AAC3B,aAAU,KAAA;;AAEZ,WAAS;;AAEX,QAAO,MAAM,KAAK,MAAM;;AAG1B,IAAI;AACJ,SAAS,4BAA0C;AACjD,KAAI,CAAC,mBACH,sBAAqB,oBAAoB;AAE3C,QAAO;;AAGT,SAAS,SAAS,KAAwB;CAExC,IAAI,WAAW;CACf,IAAI;CACJ,IAAI,MAAqB;CACzB,IAAI,SAAwB;CAE5B,IAAI,OAAO;CACX,MAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,KAAI,SAAS,GAAG;AACd,WAAS,KAAK,MAAM,QAAQ,EAAE;AAC9B,SAAO,KAAK,MAAM,GAAG,MAAM;;CAK7B,MAAM,aAAa,KAAK,QAAQ,IAAI;AACpC,KAAI,aAAa,GAAG;EAClB,MAAM,OAAO,KAAK,MAAM,GAAG,WAAW;AACtC,MAAI,SAAS,eAAe,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,EAAE;AACpE,cAAW;AACX,UAAO,KAAK,MAAM,aAAa,EAAE;;;AAIrC,KAAI,CAAC,QAAQ;EACX,MAAM,WAAW,KAAK,YAAY,IAAI;AACtC,MAAI,YAAY,GAAG;AACjB,SAAM,KAAK,MAAM,WAAW,EAAE;AAC9B,UAAO,KAAK,MAAM,GAAG,SAAS;SACzB;AACL,UAAO;AACP,SAAM;;OAGR,QAAO;AAIT,KAAI,aAAa,eAAe,CAAC,KAAK,SAAS,IAAI,CACjD,QAAO,WAAW;AAMpB,MAAK,MAAM,aAAa,KAAK,MAAM,IAAI,CACrC,KAAI,CAAC,mBAAmB,KAAK,UAAU,CACrC,OAAM,IAAI,MAAM,2BAA2B,UAAU,gBAAgB;AAGzE,KAAI,QAAQ,QAAQ,CAAC,QAAQ,KAAK,IAAI,CACpC,OAAM,IAAI,MAAM,gBAAgB,IAAI,gBAAgB;AAEtD,KAAI,WAAW,QAAQ,CAAC,WAAW,KAAK,OAAO,CAC7C,OAAM,IAAI,MACR,mBAAmB,OAAO,2CAC3B;AAGH,QAAO;EAAE;EAAU;EAAM;EAAK;EAAQ;;AAGxC,SAAS,aAAa,UAA0B;AAG9C,QAAO,aAAa,cAAc,yBAAyB;;AAG7D,eAAe,kBACb,MACA,WACiB;CAMjB,MAAM,MAAM,MAAM,eAChB,4EAA4E,KAAK,QACjF,EAAE,EACF,UACD;AACD,KAAI,IAAI,WAAW,IACjB,OAAM,IAAI,MACR,yGACD;AAEH,KAAI,IAAI,WAAW,IACjB,OAAM,IAAI,MAAM,0BAA0B,IAAI,SAAS;CAEzD,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI,KAAK;SACvB;AACN,QAAM,IAAI,MAAM,iCAAiC;;AAEnD,KACE,WAAW,QACX,OAAO,WAAW,YAClB,OAAQ,OAA+B,UAAU,SAEjD,OAAM,IAAI,MAAM,gDAAgD;AAElE,QAAQ,OAA6B;;AAGvC,eAAe,cACb,UACA,MACA,KACA,YACA,WAKC;CAED,MAAM,MAAM,WADC,aAAa,SAAS,CACP,MAAM,KAAK,aAAa;CACpD,MAAM,UAAkC,EAAE,QAAQ,iBAAiB;AACnE,KAAI,WAAY,SAAQ,gBAAgB;CACxC,MAAM,MAAM,MAAM,eAAe,KAAK,EAAE,SAAS,EAAE,UAAU;AAC7D,KAAI,IAAI,WAAW,OAAO,IAAI,WAAW,IACvC,OAAM,IAAI,MACR,qBAAqB,IAAI,OAAO,uDACjC;AAEH,KAAI,IAAI,WAAW,KAAK;EAItB,MAAM,MAAM,IAAI,WAAW,UAAU,GAAG,MAAM;AAC9C,QAAM,IAAI,MAAM,oBAAoB,SAAS,GAAG,OAAO,MAAM,MAAM;;AAErE,KAAI,IAAI,WAAW,IACjB,OAAM,IAAI,MAAM,qBAAqB,IAAI,OAAO,oBAAoB;CAEtE,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI,KAAK;SACvB;AACN,QAAM,IAAI,MAAM,6BAA6B;;AAE/C,KAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,gCAAgC;AAElD,QAAO;EACL,UAAU;EACV,aAAa,IAAI,QAAQ,IAAI,eAAe,IAAI;EAChD,QAAQ,IAAI,QAAQ,IAAI,wBAAwB;EACjD;;AAGH,eAAe,cACb,UACA,MACA,QACA,YACA,WAC+B;CAE/B,MAAM,MAAM,WADC,aAAa,SAAS,CACP,MAAM,KAAK,SAAS;CAChD,MAAM,UAAkC,EAAE;AAC1C,KAAI,WAAY,SAAQ,gBAAgB;CAExC,MAAM,MAAM,MAAM,eAAe,KAAK,EAAE,SAAS,EAAE,UAAU;AAC7D,KAAI,IAAI,WAAW,IACjB,OAAM,IAAI,MAAM,qBAAqB,IAAI,OAAO,gBAAgB;AAElE,KAAI;AACF,SAAO,KAAK,MAAM,IAAI,KAAK;SACrB;AACN,QAAM,IAAI,MAAM,yBAAyB;;;;;;;;AAe7C,eAAe,eACb,KACA,MACA,WAC2B;CAC3B,MAAM,SAAS,YAAY,QAAQ,mBAAmB;CACtD,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,UAAU,KAAK;GAAE,GAAG;GAAM;GAAQ,CAAC;UAC7C,KAAK;AACZ,MAAI,eAAe,SAAS,IAAI,SAAS,eACvC,OAAM,IAAI,MAAM,sBAAsB,MAAM;AAE9C,QAAM;;CAKR,MAAM,SAAS,SAAS,MAAM,WAAW;AACzC,KAAI,CAAC,OACH,QAAO;EAAE,QAAQ,SAAS;EAAQ,SAAS,SAAS;EAAS,MAAM;EAAI;CAEzE,MAAM,SAAuB,EAAE;CAC/B,IAAI,aAAa;CACjB,MAAM,UAAU,IAAI,aAAa;CACjC,IAAI,OAAO;AACX,KAAI;AACF,SAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AACV,OAAI,OAAO;AACT,kBAAc,MAAM;AACpB,QAAI,aAAa,gBAAgB;AAC/B,WAAM,OAAO,QAAQ;AACrB,WAAM,IAAI,MACR,0BAA0B,eAAe,kBAAkB,MAC5D;;AAEH,WAAO,KAAK,MAAM;;;AAGtB,SAAO,QAAQ,OAAO,kBAAkB,OAAO,CAAC;WACxC;AACR,SAAO,aAAa;;AAEtB,QAAO;EAAE,QAAQ,SAAS;EAAQ,SAAS,SAAS;EAAS;EAAM;;AAGrE,SAAS,kBAAkB,QAAkC;AAC3D,KAAI,OAAO,WAAW,EAAG,QAAO,IAAI,WAAW,EAAE;AACjD,KAAI,OAAO,WAAW,GAAG;EACvB,MAAM,OAAO,OAAO;AACpB,MAAI,SAAS,KAAA,EAAW,QAAO;;CAEjC,IAAI,QAAQ;AACZ,MAAK,MAAM,KAAK,OAAQ,UAAS,EAAE;CACnC,MAAM,MAAM,IAAI,WAAW,MAAM;CACjC,IAAI,SAAS;AACb,MAAK,MAAM,KAAK,QAAQ;AACtB,MAAI,IAAI,GAAG,OAAO;AAClB,YAAU,EAAE;;AAEd,QAAO;;AAGT,SAAS,UAAU,cAAiC;AAClD,KACE,iBAAiB,QACjB,OAAO,iBAAiB,YACxB,MAAM,QAAQ,aAAa,CAE3B,QAAO,EAAE;AAEX,QAAO,OAAO,KAAK,aAAwC,CAAC,MAAM;;AAGpE,SAAS,SAAS,KAAsC;AACtD,KAAI,CAAC,MAAM,QAAQ,IAAI,CAAE,QAAO,EAAE;CAClC,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,MAAM,KAAK;AACpB,MAAI,OAAO,OAAO,SAAU;EAC5B,MAAM,IAAI,GAAG,QAAQ,IAAI;AACzB,MAAI,IAAI,GAAG;GACT,MAAM,MAAM,GAAG,MAAM,GAAG,EAAE;AAE1B,OAAI,OADU,GAAG,MAAM,IAAI,EAAE;QAG7B,KAAI,MAAM;;AAGd,QAAO;;AAGT,SAAS,gBAAgB,GAAqC;AAC5D,QAAO,MAAM,QAAQ,EAAE,UAAU;;AAGnC,SAAS,qBACP,OACgC;CAChC,MAAM,OAAO,MAAM;AACnB,KAAI,CAAC,MAAM,QAAQ,KAAK,CAAE,QAAO;CACjC,MAAM,aAAa,KAAK,MACrB,MACC,MAAM,QACN,OAAO,MAAM,YACZ,EAA6B,aAAa,QAC3C,OAAQ,EAA6B,aAAa,YACjD,EAAqC,SAAS,OAAO,WACrD,EAA+C,SAAS,iBACvD,QACL;AACD,KAAI,WAAY,QAAO;CAEvB,MAAM,QAAQ,KAAK;AACnB,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,CACtE,QAAO;AAET,QAAO;;AAGT,SAAS,kBACP,MACA,kBACU;CACV,MAAM,WAAW,CAAC,MAAM,GAAG,iBAAiB,CAAC,KAAK,IAAI,CAAC,aAAa;AACpE,MAAK,MAAM,QAAQ,YACjB,KAAI,SAAS,SAAS,KAAK,MAAM,CAAE,QAAO,CAAC,GAAG,KAAK,MAAM;AAE3D,QAAO,EAAE"}
|
|
1
|
+
{"version":3,"file":"inspect-image.js","names":[],"sources":["../../src/internals/inspect-image.ts"],"sourcesContent":["import { createGuardedFetch } from './guarded-fetch.js';\n\n/**\n * Inspect a public container image via the OCI Distribution API. Returns\n * the manifest digest, exposed ports, image defaults (env / cmd /\n * entrypoint / user / workingDir), healthcheck, labels, volumes, and a\n * heuristic `suggestedTmpfs` list for known-good Fred image families.\n *\n * HTTPS requests go through `opts.fetch`, defaulting to `createGuardedFetch()`\n * (DIY undici Dispatcher + RFC-cited block ranges + IPv4-mapped IPv6\n * normalization — see `guarded-fetch.ts` for the design).\n *\n * **Fail-soft contract:** returns `null` on every non-fatal failure mode:\n * - 401 / 403 (private registry / auth required)\n * - 429 (Docker Hub rate-limit)\n * - OCI grammar violation in the `imageRef`\n * - Manifest body exceeding the 10 MiB cap\n * - Request timeout (10s)\n * - Unparseable manifest / blob JSON\n * - SSRF block (default fetch refuses RFC 1918 / loopback / etc.)\n * Callers treat `null` as \"no info, ask the user\".\n * Diagnostics flow through `opts.logger` instead of stderr.\n *\n * ## Security — SSRF (production callers MUST read)\n *\n * `imageRef` is user-controlled (it comes from `DeploySpec.image`).\n * Without an SSRF guard, an image ref like `169.254.169.254:80/foo:bar`\n * (cloud-metadata) or `127.0.0.1:6379/foo:bar` (local Redis) would\n * cause this function to probe internal services on the host. The CJS\n * blocks this via its SSRF-aware HTTPS agent; the TS port delegates to\n * the caller's `opts.fetch`, defaulting to `createGuardedFetch()` which\n * blocks at connect time.\n *\n * Opt-out-of-safety semantics (parent's PR-2 directive): the default\n * `opts.fetch = createGuardedFetch()` is safe by construction. Callers\n * pass their own `opts.fetch` ONLY for tests (canned responses) or\n * unusual production cases (e.g. a trusted private registry on an RFC\n * 1918 IP, after explicit allow-listing). See `createGuardedFetch`'s\n * JSDoc for the production-guard contract.\n */\n\nconst ACCEPT_MANIFEST = [\n 'application/vnd.oci.image.index.v1+json',\n 'application/vnd.oci.image.manifest.v1+json',\n 'application/vnd.docker.distribution.manifest.list.v2+json',\n 'application/vnd.docker.distribution.manifest.v2+json',\n].join(', ');\n\n// OCI Distribution Spec v1.1 grammar for URL-interpolated fields. Validate\n// BEFORE the URL is constructed; the `imageRef` flag is user-controlled,\n// so a malformed input like `foo/bar:..%2F..%2Fconfig` must be rejected\n// here rather than forwarded to the registry.\nconst OCI_NAME_COMPONENT = /^[a-z0-9]+(?:(?:\\.|_|__|-+)[a-z0-9]+)*$/;\nconst OCI_TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;\nconst OCI_DIGEST = /^sha256:[0-9a-f]{64}$/;\n\n// Body-size cap (10 MiB). Real-world configs are <100 KB; even JVM-rich\n// images rarely exceed a few MB. Anything over 10 MiB indicates a hostile\n// or buggy registry; abort rather than risk OOM.\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n// Request timeout — 10s. Registry queries should be fast; longer waits\n// indicate a hung registry.\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n// Heuristic table: image base name or resolved Cmd/Entrypoint contains\n// one of these tokens → suggest the corresponding tmpfs paths. Order:\n// longer/more-specific tokens first when there's ambiguity.\nconst TMPFS_HINTS: ReadonlyArray<{\n readonly match: string;\n readonly paths: readonly string[];\n}> = [\n { match: 'wordpress', paths: ['/run/lock', '/var/run/apache2'] },\n { match: 'mariadb', paths: ['/run/mysqld'] },\n { match: 'postgres', paths: ['/var/run/postgresql'] },\n { match: 'mysql', paths: ['/var/run/mysqld'] },\n { match: 'nginx', paths: ['/var/cache/nginx', '/var/run'] },\n];\n\nexport interface ImageInfo {\n image: string;\n digest: string | null;\n ports: string[];\n env: Record<string, string>;\n cmd: string[] | null;\n entrypoint: string[] | null;\n user: string;\n workingDir: string;\n healthcheck: Record<string, unknown> | null;\n labels: Record<string, string> | null;\n volumes: Record<string, unknown> | null;\n suggestedTmpfs: string[];\n}\n\nexport interface InspectImageOptions {\n /**\n * HTTP client. **Production callers SHOULD use the default** (which is\n * `createGuardedFetch()`, blocking RFC 1918 / loopback / link-local /\n * metadata at connect time). Tests pass canned implementations.\n * Browser/Deno consumers pass their own SSRF-guarded fetch since\n * `createGuardedFetch()` throws on non-Node runtimes.\n */\n fetch?: typeof fetch;\n /** Sink for fail-soft diagnostics. Defaults to `console.warn`. */\n logger?: (reason: string) => void;\n}\n\nconst defaultLogger: (reason: string) => void = (reason) => {\n console.warn(reason);\n};\n\ninterface ParsedRef {\n registry: string;\n name: string;\n tag: string | null;\n digest: string | null;\n}\n\nexport async function inspectImage(\n imageRef: string,\n opts: InspectImageOptions = {},\n): Promise<ImageInfo | null> {\n const logger = opts.logger ?? defaultLogger;\n const fetchImpl: typeof fetch = opts.fetch ?? createDefaultGuardedFetch();\n\n let parsed: ParsedRef;\n try {\n parsed = parseRef(imageRef);\n } catch (err) {\n logger(\n `inspect-image: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n\n const ref = parsed.digest ?? parsed.tag ?? 'latest';\n try {\n let authHeader: string | null = null;\n if (parsed.registry === 'docker.io') {\n const token = await getDockerHubToken(parsed.name, fetchImpl);\n authHeader = `Bearer ${token}`;\n }\n\n // Step 1: fetch manifest (may be an index → pick platform → refetch).\n let manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n ref,\n authHeader,\n fetchImpl,\n );\n if (\n manifestRes.contentType.includes('manifest.list') ||\n manifestRes.contentType.includes('image.index') ||\n isManifestIndex(manifestRes.manifest)\n ) {\n const child = pickPlatformManifest(manifestRes.manifest);\n if (!child || typeof child.digest !== 'string') {\n throw new Error('multi-arch index has no usable child manifest');\n }\n manifestRes = await fetchManifest(\n parsed.registry,\n parsed.name,\n child.digest,\n authHeader,\n fetchImpl,\n );\n }\n\n // Step 2: fetch the config blob — the actual image config lives there.\n const config = manifestRes.manifest.config as\n | { digest?: unknown }\n | undefined;\n if (!config || typeof config.digest !== 'string') {\n throw new Error('manifest has no config descriptor');\n }\n const configBlob = await fetchBlobJson(\n parsed.registry,\n parsed.name,\n config.digest,\n authHeader,\n fetchImpl,\n );\n const c = (configBlob.config ?? {}) as Record<string, unknown>;\n\n const out: ImageInfo = {\n image: `${parsed.registry}/${parsed.name}${parsed.digest ? '@' + parsed.digest : ':' + (parsed.tag ?? 'latest')}`,\n digest: manifestRes.digest ?? parsed.digest ?? null,\n ports: pickPorts(c.ExposedPorts),\n env: parseEnv(c.Env),\n cmd: Array.isArray(c.Cmd) ? (c.Cmd as string[]) : null,\n entrypoint: Array.isArray(c.Entrypoint)\n ? (c.Entrypoint as string[])\n : null,\n user: typeof c.User === 'string' ? c.User : '',\n workingDir: typeof c.WorkingDir === 'string' ? c.WorkingDir : '',\n healthcheck:\n c.Healthcheck !== null &&\n typeof c.Healthcheck === 'object' &&\n !Array.isArray(c.Healthcheck)\n ? (c.Healthcheck as Record<string, unknown>)\n : null,\n labels:\n c.Labels !== null &&\n typeof c.Labels === 'object' &&\n !Array.isArray(c.Labels)\n ? (c.Labels as Record<string, string>)\n : null,\n volumes:\n c.Volumes !== null &&\n typeof c.Volumes === 'object' &&\n !Array.isArray(c.Volumes)\n ? (c.Volumes as Record<string, unknown>)\n : null,\n suggestedTmpfs: [],\n };\n out.suggestedTmpfs = suggestedTmpfsFor(parsed.name, [\n ...(out.cmd ?? []),\n ...(out.entrypoint ?? []),\n ]);\n\n return out;\n } catch (err) {\n logger(`inspect-image: ${formatErrorChain(err)}`);\n return null;\n }\n}\n\n/**\n * Walk an Error's `cause` chain and join all message strings. undici wraps\n * connection errors (including SSRF blocks from our custom Dispatcher) in\n * a fetch-side TypeError with the underlying cause nested via `.cause`.\n * Surfacing the chain in the logger gives the user the real reason (e.g.,\n * \"SSRF blocked: 127.0.0.1 ... loopback\") instead of an opaque\n * \"fetch failed\".\n */\nfunction formatErrorChain(err: unknown): string {\n const parts: string[] = [];\n let current: unknown = err;\n let depth = 0;\n // Defensive bound — sane Error chains are <5 levels; cap at 10 to avoid\n // pathological cycles.\n while (current !== null && current !== undefined && depth < 10) {\n if (current instanceof Error) {\n parts.push(current.message);\n current = (current as Error & { cause?: unknown }).cause;\n } else {\n parts.push(String(current));\n current = undefined;\n }\n depth += 1;\n }\n return parts.join(' | ');\n}\n\nlet cachedDefaultFetch: typeof fetch | undefined;\nfunction createDefaultGuardedFetch(): typeof fetch {\n if (!cachedDefaultFetch) {\n cachedDefaultFetch = createGuardedFetch();\n }\n return cachedDefaultFetch;\n}\n\nfunction parseRef(ref: string): ParsedRef {\n // \"<reg>/<name>@sha256:<digest>\" | \"<reg>/<name>:<tag>\" | \"<name>\" | \"<name>:<tag>\"\n let registry = 'docker.io';\n let name: string;\n let tag: string | null = null;\n let digest: string | null = null;\n\n let rest = ref;\n const atIdx = rest.indexOf('@');\n if (atIdx >= 0) {\n digest = rest.slice(atIdx + 1);\n rest = rest.slice(0, atIdx);\n }\n\n // Detect registry segment: head before first `/` is a registry only if\n // it has a `.` or `:` (port) or is `localhost`.\n const firstSlash = rest.indexOf('/');\n if (firstSlash > 0) {\n const head = rest.slice(0, firstSlash);\n if (head === 'localhost' || head.includes('.') || head.includes(':')) {\n registry = head;\n rest = rest.slice(firstSlash + 1);\n }\n }\n\n if (!digest) {\n const colonIdx = rest.lastIndexOf(':');\n if (colonIdx >= 0) {\n tag = rest.slice(colonIdx + 1);\n name = rest.slice(0, colonIdx);\n } else {\n name = rest;\n tag = 'latest';\n }\n } else {\n name = rest;\n }\n\n // Docker Hub library prefix for single-segment names (\"nginx\" → \"library/nginx\").\n if (registry === 'docker.io' && !name.includes('/')) {\n name = `library/${name}`;\n }\n\n // Validate URL-interpolated fields against OCI Distribution Spec grammar\n // BEFORE the URL is constructed. The ref strings reach the user via\n // `DeploySpec.image`, so malformed input must be rejected here.\n for (const component of name.split('/')) {\n if (!OCI_NAME_COMPONENT.test(component)) {\n throw new Error(`invalid name component \"${component}\" in image ref`);\n }\n }\n if (tag !== null && !OCI_TAG.test(tag)) {\n throw new Error(`invalid tag \"${tag}\" in image ref`);\n }\n if (digest !== null && !OCI_DIGEST.test(digest)) {\n throw new Error(\n `invalid digest \"${digest}\" in image ref (expected sha256:<64-hex>)`,\n );\n }\n\n return { registry, name, tag, digest };\n}\n\nfunction registryHost(registry: string): string {\n // Docker Hub's image API lives at registry-1.docker.io even though the\n // canonical \"registry\" name is docker.io.\n return registry === 'docker.io' ? 'registry-1.docker.io' : registry;\n}\n\nasync function getDockerHubToken(\n name: string,\n fetchImpl: typeof fetch,\n): Promise<string> {\n // Docker Hub requires anonymous access still go through a token grant.\n // Surface 429 specifically — anonymous pulls are rate-limited per-IP\n // and a 60-min wait fixes it. Without this special case the user sees\n // the same fail-soft `null` outcome as a hard 401 with no signal that\n // the situation is temporary.\n const res = await capturingFetch(\n `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${name}:pull`,\n {},\n fetchImpl,\n );\n if (res.status === 429) {\n throw new Error(\n 'Docker Hub token: HTTP 429 (anonymous pulls rate-limited per-IP; retry after ~60 min, or authenticate)',\n );\n }\n if (res.status !== 200) {\n throw new Error(`Docker Hub token: HTTP ${res.status}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('Docker Hub token: invalid JSON');\n }\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n typeof (parsed as { token?: unknown }).token !== 'string'\n ) {\n throw new Error('Docker Hub token: missing `token` in response');\n }\n return (parsed as { token: string }).token;\n}\n\nasync function fetchManifest(\n registry: string,\n name: string,\n ref: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{\n manifest: Record<string, unknown>;\n contentType: string;\n digest: string | null;\n}> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/manifests/${ref}`;\n const headers: Record<string, string> = { Accept: ACCEPT_MANIFEST };\n if (authHeader) headers.Authorization = authHeader;\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `registry returned ${res.status} on manifest fetch (auth required? private registry?)`,\n );\n }\n if (res.status === 404) {\n // Digest-pinned refs use `@sha256:...`; tag refs use `:tag`. Pick the\n // right separator so the error message doesn't show\n // `registry/name:sha256:...` mistakenly.\n const sep = ref.startsWith('sha256:') ? '@' : ':';\n throw new Error(`image not found: ${registry}/${name}${sep}${ref}`);\n }\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on manifest fetch`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(res.body);\n } catch {\n throw new Error('manifest is not valid JSON');\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('manifest is not a JSON object');\n }\n return {\n manifest: parsed as Record<string, unknown>,\n contentType: res.headers.get('content-type') ?? '',\n digest: res.headers.get('docker-content-digest'),\n };\n}\n\nasync function fetchBlobJson(\n registry: string,\n name: string,\n digest: string,\n authHeader: string | null,\n fetchImpl: typeof fetch,\n): Promise<{ config?: unknown }> {\n const host = registryHost(registry);\n const url = `https://${host}/v2/${name}/blobs/${digest}`;\n const headers: Record<string, string> = {};\n if (authHeader) headers.Authorization = authHeader;\n // undici fetch follows redirects by default; registries 307 → CDN.\n const res = await capturingFetch(url, { headers }, fetchImpl);\n if (res.status !== 200) {\n throw new Error(`registry returned ${res.status} on blob fetch`);\n }\n try {\n return JSON.parse(res.body) as { config?: unknown };\n } catch {\n throw new Error('blob is not valid JSON');\n }\n}\n\ninterface CapturedResponse {\n status: number;\n headers: Headers;\n body: string;\n}\n\n/**\n * Wrap fetch with `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` and a streamed\n * body-size cap. Throws on overflow, timeout, or read error so the outer\n * try/catch produces the fail-soft `null` return.\n */\nasync function capturingFetch(\n url: string,\n init: RequestInit,\n fetchImpl: typeof fetch,\n): Promise<CapturedResponse> {\n const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetchImpl(url, { ...init, signal });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new Error(`request timeout on ${url}`);\n }\n throw err;\n }\n // Stream the body with a manual chunk-accumulation cap. Avoids the\n // unbounded `await response.text()` path that would let a hostile\n // registry exhaust memory.\n const reader = response.body?.getReader();\n if (!reader) {\n return { status: response.status, headers: response.headers, body: '' };\n }\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const decoder = new TextDecoder();\n let body = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n totalBytes += value.length;\n if (totalBytes > MAX_BODY_BYTES) {\n await reader.cancel();\n throw new Error(\n `response body exceeded ${MAX_BODY_BYTES} bytes (cap) on ${url}`,\n );\n }\n chunks.push(value);\n }\n }\n body = decoder.decode(concatUint8Arrays(chunks));\n } finally {\n reader.releaseLock();\n }\n return { status: response.status, headers: response.headers, body };\n}\n\nfunction concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) return new Uint8Array(0);\n if (chunks.length === 1) {\n const only = chunks[0];\n if (only !== undefined) return only;\n }\n let total = 0;\n for (const c of chunks) total += c.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const c of chunks) {\n out.set(c, offset);\n offset += c.length;\n }\n return out;\n}\n\nfunction pickPorts(exposedPorts: unknown): string[] {\n if (\n exposedPorts === null ||\n typeof exposedPorts !== 'object' ||\n Array.isArray(exposedPorts)\n ) {\n return [];\n }\n return Object.keys(exposedPorts as Record<string, unknown>).sort();\n}\n\nfunction parseEnv(env: unknown): Record<string, string> {\n if (!Array.isArray(env)) return {};\n const out: Record<string, string> = {};\n for (const kv of env) {\n if (typeof kv !== 'string') continue;\n const i = kv.indexOf('=');\n if (i > 0) {\n const key = kv.slice(0, i);\n const value = kv.slice(i + 1);\n out[key] = value;\n } else {\n out[kv] = '';\n }\n }\n return out;\n}\n\nfunction isManifestIndex(m: Record<string, unknown>): boolean {\n return Array.isArray(m.manifests);\n}\n\nfunction pickPlatformManifest(\n index: Record<string, unknown>,\n): Record<string, unknown> | null {\n const list = index.manifests;\n if (!Array.isArray(list)) return null;\n const linuxAmd64 = list.find(\n (m): m is Record<string, unknown> =>\n m !== null &&\n typeof m === 'object' &&\n (m as { platform?: unknown }).platform !== null &&\n typeof (m as { platform?: unknown }).platform === 'object' &&\n (m as { platform: { os?: unknown } }).platform.os === 'linux' &&\n (m as { platform: { architecture?: unknown } }).platform.architecture ===\n 'amd64',\n );\n if (linuxAmd64) return linuxAmd64;\n // Fall back to first entry.\n const first = list[0];\n if (first !== null && typeof first === 'object' && !Array.isArray(first)) {\n return first as Record<string, unknown>;\n }\n return null;\n}\n\nfunction suggestedTmpfsFor(\n name: string,\n cmdAndEntrypoint: ReadonlyArray<string>,\n): string[] {\n const haystack = [name, ...cmdAndEntrypoint].join(' ').toLowerCase();\n for (const hint of TMPFS_HINTS) {\n if (haystack.includes(hint.match)) return [...hint.paths];\n }\n return [];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lease-items.d.ts","names":[],"sources":["../../src/internals/lease-items.ts"],"mappings":";;AAiBA
|
|
1
|
+
{"version":3,"file":"lease-items.d.ts","names":[],"sources":["../../src/internals/lease-items.ts"],"mappings":";;AAiBA;;;;AAEc;AAOd;;;;AAAgD;AAmBhD;;;;AAAgE;UA5B/C,mBAAA;EACf,WAAA;EACA,YAAY;AAAA;AAqD+C;;;;AAAA,iBA9C7C,eAAA,CAAgB,OAAgB;;;;;;iBAmBhC,aAAA,CAAc,GAAA,YAAe,mBAAmB;;;;;;;;;;;iBA2BhD,SAAA,CAAU,OAAA,WAAkB,SAAiB"}
|
|
@@ -35,10 +35,7 @@ function normalizeItem(raw) {
|
|
|
35
35
|
* "Cannot read properties of …" stack trace.
|
|
36
36
|
*/
|
|
37
37
|
function findLease(payload, leaseUuid) {
|
|
38
|
-
if (typeof leaseUuid !== "string") {
|
|
39
|
-
const got = leaseUuid === null ? "null" : typeof leaseUuid;
|
|
40
|
-
throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);
|
|
41
|
-
}
|
|
38
|
+
if (typeof leaseUuid !== "string") throw new TypeError(`findLease: leaseUuid must be a string, got ${leaseUuid === null ? "null" : typeof leaseUuid}`);
|
|
42
39
|
const leases = pickLeasesArray(payload);
|
|
43
40
|
const target = leaseUuid.toLowerCase();
|
|
44
41
|
for (const lease of leases) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lease-items.js","names":[],"sources":["../../src/internals/lease-items.ts"],"sourcesContent":["/**\n * Shared decoding for `leases_by_tenant` responses. Walks `leases[]`,\n * matches by UUID, normalizes each item's serviceName/customDomain across\n * snake_case/camelCase variants. `verify-domain-state.ts` is the in-package\n * consumer for PR 1; PR 4's `manageDomain` / `troubleshoot` will also consume.\n *\n * Exports:\n * - `pickLeasesArray(payload)` — tolerate `{ leases: [...] }` (current\n * chain shape) and a bare array. Throws on anything else.\n * - `normalizeItem(rawItem)` — return `{ serviceName, customDomain }`\n * with empty-string defaults; accepts both camelCase and snake_case.\n * - `findLease(payload, leaseUuid)` — `pickLeasesArray` + UUID lookup.\n * Case-insensitive; tolerates `uuid` / `lease_uuid` / `leaseUuid` keys.\n * Returns the matched lease object (raw shape) or `null`. Throws\n * `TypeError` when `leaseUuid` is not a string.\n */\n\nexport interface NormalizedLeaseItem {\n serviceName: string;\n customDomain: string;\n}\n\n/**\n * Tolerate either `{ leases: [...] }` (current chain shape) or a bare\n * array. Throws on anything else.\n */\nexport function pickLeasesArray(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (\n payload !== null &&\n typeof payload === 'object' &&\n Array.isArray((payload as { leases?: unknown }).leases)\n ) {\n return (payload as { leases: unknown[] }).leases;\n }\n throw new Error(\n 'leases_by_tenant response: expected `leases[]` array or bare array',\n );\n}\n\n/**\n * Normalize a raw lease-item record (chain snake_case OR proto-decoded\n * camelCase) into `{ serviceName, customDomain }` with empty-string\n * defaults on missing fields.\n */\nexport function normalizeItem(raw: unknown): NormalizedLeaseItem {\n if (raw === null || typeof raw !== 'object') {\n return { serviceName: '', customDomain: '' };\n }\n const r = raw as {\n serviceName?: unknown;\n service_name?: unknown;\n customDomain?: unknown;\n custom_domain?: unknown;\n };\n const serviceName =\n readStringOrEmpty(r.serviceName) || readStringOrEmpty(r.service_name);\n const customDomain =\n readStringOrEmpty(r.customDomain) || readStringOrEmpty(r.custom_domain);\n return { serviceName, customDomain };\n}\n\n/**\n * Find a lease by UUID inside a `leases_by_tenant` response. Lookup is\n * case-insensitive and tolerates `uuid`, `lease_uuid`, or `leaseUuid`\n * fields on the lease object. Returns the raw lease record or `null`.\n *\n * Throws `TypeError` if `leaseUuid` is not a string. Both production\n * callers (verify-domain-state, future manageDomain) pre-validate against\n * a UUID regex, but the helper guards anyway — a clear error beats a\n * \"Cannot read properties of …\" stack trace.\n */\nexport function findLease(payload: unknown, leaseUuid: string): unknown | null {\n if (typeof leaseUuid !== 'string') {\n const got = leaseUuid === null ? 'null' : typeof leaseUuid;\n throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);\n }\n const leases = pickLeasesArray(payload);\n const target = leaseUuid.toLowerCase();\n for (const lease of leases) {\n if (lease === null || typeof lease !== 'object') continue;\n const r = lease as {\n uuid?: unknown;\n lease_uuid?: unknown;\n leaseUuid?: unknown;\n };\n const u = r.uuid ?? r.lease_uuid ?? r.leaseUuid;\n if (typeof u === 'string' && u.toLowerCase() === target) {\n return lease;\n }\n }\n return null;\n}\n\nfunction readStringOrEmpty(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],"mappings":";;;;;AA0BA,SAAgB,gBAAgB,SAA6B;
|
|
1
|
+
{"version":3,"file":"lease-items.js","names":[],"sources":["../../src/internals/lease-items.ts"],"sourcesContent":["/**\n * Shared decoding for `leases_by_tenant` responses. Walks `leases[]`,\n * matches by UUID, normalizes each item's serviceName/customDomain across\n * snake_case/camelCase variants. `verify-domain-state.ts` is the in-package\n * consumer for PR 1; PR 4's `manageDomain` / `troubleshoot` will also consume.\n *\n * Exports:\n * - `pickLeasesArray(payload)` — tolerate `{ leases: [...] }` (current\n * chain shape) and a bare array. Throws on anything else.\n * - `normalizeItem(rawItem)` — return `{ serviceName, customDomain }`\n * with empty-string defaults; accepts both camelCase and snake_case.\n * - `findLease(payload, leaseUuid)` — `pickLeasesArray` + UUID lookup.\n * Case-insensitive; tolerates `uuid` / `lease_uuid` / `leaseUuid` keys.\n * Returns the matched lease object (raw shape) or `null`. Throws\n * `TypeError` when `leaseUuid` is not a string.\n */\n\nexport interface NormalizedLeaseItem {\n serviceName: string;\n customDomain: string;\n}\n\n/**\n * Tolerate either `{ leases: [...] }` (current chain shape) or a bare\n * array. Throws on anything else.\n */\nexport function pickLeasesArray(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (\n payload !== null &&\n typeof payload === 'object' &&\n Array.isArray((payload as { leases?: unknown }).leases)\n ) {\n return (payload as { leases: unknown[] }).leases;\n }\n throw new Error(\n 'leases_by_tenant response: expected `leases[]` array or bare array',\n );\n}\n\n/**\n * Normalize a raw lease-item record (chain snake_case OR proto-decoded\n * camelCase) into `{ serviceName, customDomain }` with empty-string\n * defaults on missing fields.\n */\nexport function normalizeItem(raw: unknown): NormalizedLeaseItem {\n if (raw === null || typeof raw !== 'object') {\n return { serviceName: '', customDomain: '' };\n }\n const r = raw as {\n serviceName?: unknown;\n service_name?: unknown;\n customDomain?: unknown;\n custom_domain?: unknown;\n };\n const serviceName =\n readStringOrEmpty(r.serviceName) || readStringOrEmpty(r.service_name);\n const customDomain =\n readStringOrEmpty(r.customDomain) || readStringOrEmpty(r.custom_domain);\n return { serviceName, customDomain };\n}\n\n/**\n * Find a lease by UUID inside a `leases_by_tenant` response. Lookup is\n * case-insensitive and tolerates `uuid`, `lease_uuid`, or `leaseUuid`\n * fields on the lease object. Returns the raw lease record or `null`.\n *\n * Throws `TypeError` if `leaseUuid` is not a string. Both production\n * callers (verify-domain-state, future manageDomain) pre-validate against\n * a UUID regex, but the helper guards anyway — a clear error beats a\n * \"Cannot read properties of …\" stack trace.\n */\nexport function findLease(payload: unknown, leaseUuid: string): unknown | null {\n if (typeof leaseUuid !== 'string') {\n const got = leaseUuid === null ? 'null' : typeof leaseUuid;\n throw new TypeError(`findLease: leaseUuid must be a string, got ${got}`);\n }\n const leases = pickLeasesArray(payload);\n const target = leaseUuid.toLowerCase();\n for (const lease of leases) {\n if (lease === null || typeof lease !== 'object') continue;\n const r = lease as {\n uuid?: unknown;\n lease_uuid?: unknown;\n leaseUuid?: unknown;\n };\n const u = r.uuid ?? r.lease_uuid ?? r.leaseUuid;\n if (typeof u === 'string' && u.toLowerCase() === target) {\n return lease;\n }\n }\n return null;\n}\n\nfunction readStringOrEmpty(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],"mappings":";;;;;AA0BA,SAAgB,gBAAgB,SAA6B;CAC3D,IAAI,MAAM,QAAQ,OAAO,GAAG,OAAO;CACnC,IACE,YAAY,QACZ,OAAO,YAAY,YACnB,MAAM,QAAS,QAAiC,MAAM,GAEtD,OAAQ,QAAkC;CAE5C,MAAM,IAAI,MACR,oEACF;AACF;;;;;;AAOA,SAAgB,cAAc,KAAmC;CAC/D,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UACjC,OAAO;EAAE,aAAa;EAAI,cAAc;CAAG;CAE7C,MAAM,IAAI;CAUV,OAAO;EAAE,aAHP,kBAAkB,EAAE,WAAW,KAAK,kBAAkB,EAAE,YAAY;EAGhD,cADpB,kBAAkB,EAAE,YAAY,KAAK,kBAAkB,EAAE,aAAa;CACrC;AACrC;;;;;;;;;;;AAYA,SAAgB,UAAU,SAAkB,WAAmC;CAC7E,IAAI,OAAO,cAAc,UAEvB,MAAM,IAAI,UAAU,8CADR,cAAc,OAAO,SAAS,OAAO,WACsB;CAEzE,MAAM,SAAS,gBAAgB,OAAO;CACtC,MAAM,SAAS,UAAU,YAAY;CACrC,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EACjD,MAAM,IAAI;EAKV,MAAM,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE;EACtC,IAAI,OAAO,MAAM,YAAY,EAAE,YAAY,MAAM,QAC/C,OAAO;CAEX;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAwB;CACjD,OAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lease-state.d.ts","names":[],"sources":["../../src/internals/lease-state.ts"],"mappings":";;;cAyCa,eAAA,EAAiB,
|
|
1
|
+
{"version":3,"file":"lease-state.d.ts","names":[],"sources":["../../src/internals/lease-state.ts"],"mappings":";;;cAyCa,eAAA,EAAiB,WAAW,CAAC,cAAA;;AAA1C;;;;AAAwD;AAmCxD;;;;AAEiB;AAYjB;;;;AAAmD;;;;;;;iBAdnC,MAAA,CACd,KAAA,gCACC,cAAc;;iBAYD,UAAA,CAAW,IAAwB"}
|