@cosmicdrift/kumiko-http 0.286.0 → 0.288.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/package.json +1 -1
- package/src/changes.json +6 -0
- package/src/index.ts +1 -0
- package/src/policy.ts +24 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.288.0",
|
|
4
4
|
"description": "SSRF-safe egress fetch for Kumiko: a single egress(policy) entry point that enforces external/internal/tenant-supplied host policy, DNS-rebinding-safe resolve-then-pin, and no external runtime dependencies.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.287.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "export isPublicHost for create-time SSRF precheck without a real fetch",
|
|
6
|
+
"detail": "Adds `isPublicHost(raw: string, lookupFn?: typeof lookup): Promise<boolean>` to\n`@cosmicdrift/kumiko-http` (and re-exported from `@cosmicdrift/kumiko-framework/http`).\nIt runs the same range-table and DNS-rebinding-safe single-resolution check\n`egress()` uses internally via `resolvePublicHost`, but performs no connection —\nuseful for validating tenant-supplied URLs (e.g. webhook registration) up front.\nNever throws: parse failures, non-http(s) schemes, embedded credentials, DNS\nfailures, and blocked ranges all resolve to `false` rather than rejecting."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.284.0",
|
|
4
10
|
"type": "improvement",
|
package/src/index.ts
CHANGED
package/src/policy.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Egress trust boundary. `EgressPolicy` and the checks below back the single
|
|
2
|
-
// exported way to speak outward, `egress()` in ./egress.ts
|
|
3
|
-
//
|
|
4
|
-
// call sites bind a policy once via
|
|
5
|
-
// table or the allowlist check
|
|
2
|
+
// exported way to speak outward, `egress()` in ./egress.ts. Only `isPublicHost`
|
|
3
|
+
// reaches the package's public barrel (index.ts), as a boolean pre-check for
|
|
4
|
+
// create-time validation — call sites otherwise bind a policy once via
|
|
5
|
+
// `egress(policy)` and never see the range table or the allowlist check
|
|
6
|
+
// directly.
|
|
6
7
|
//
|
|
7
8
|
// `external` and `tenant-supplied` resolve to the identical set of checks
|
|
8
9
|
// below (deny private/reserved/link-local + no redirects). They stay
|
|
@@ -177,6 +178,25 @@ export async function resolvePublicHost(
|
|
|
177
178
|
return { address: chosen.address, family: chosen.family === 6 ? 6 : 4 };
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// Create-time validation for tenant-supplied URLs (e.g. webhook registration)
|
|
182
|
+
// where rejecting at registration is better UX than failing later at fetch
|
|
183
|
+
// time. Runs the same resolution as `egress()` without connecting anywhere,
|
|
184
|
+
// and never throws — bad input (unparsable URL, non-http(s) scheme, embedded
|
|
185
|
+
// credentials, DNS failure, blocked range) all just resolve to `false`.
|
|
186
|
+
export async function isPublicHost(
|
|
187
|
+
raw: string,
|
|
188
|
+
lookupFn: typeof lookup = lookup,
|
|
189
|
+
): Promise<boolean> {
|
|
190
|
+
try {
|
|
191
|
+
const url = new URL(raw);
|
|
192
|
+
assertHttpScheme(url);
|
|
193
|
+
await resolvePublicHost(url, lookupFn);
|
|
194
|
+
return true;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
// Explicit allowlist check for `internal` — the hostname (not the resolved
|
|
181
201
|
// IP) must appear verbatim (case-insensitive) in `allowHosts`. No range
|
|
182
202
|
// check: `internal` targets are deliberately private/cluster-local.
|