@truly-private/omdsh-web-fetch-http 0.0.1
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +51 -0
- package/README.zh.md +51 -0
- package/lib/index.js +415 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +37 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/policy.d.ts +63 -0
- package/lib/types/provider.d.ts +49 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/web/web-fetch-http/README.md
|
|
5
|
+
README.md: 48d3318388f4d3ce74876ac53685744f023908bd
|
|
6
|
+
README.zh.md: 75ae8b7a6fcbe7e990f66089eda9ee2a2b0c7694
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @truly-private/omdsh-web-fetch-http
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
An anonymous public HTTP(S) `WebFetchProvider` for the harness [web capability seam](../web/README.md) (`ctx.web`). It retrieves a concrete URL and returns a status code plus bounded decoded content.
|
|
6
|
+
|
|
7
|
+
This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the key and it does not register a model-facing tool. It is a function/namespace plugin (`inject: ['web']`).
|
|
8
|
+
|
|
9
|
+
## Responsibility split
|
|
10
|
+
|
|
11
|
+
The provider owns **safe resource retrieval**: URL validation, HTTP transport, redirect policy, a resource-backstop timeout, abort propagation, byte caps, charset decoding, content-type classification, and binary rejection. `@truly-private/omdsh-tool-web` owns **presentation** (HTML→markdown, truncation formatting). A non-2xx HTTP response is a *result* (status code + decoded body), not an error; `WebError` is reserved for failures to safely retrieve or represent the resource.
|
|
12
|
+
|
|
13
|
+
The provider's `timeoutMs` is a resource backstop for direct `ctx.web.fetch()` callers and misconfigured deployments, not the model-facing tool-call budget. [`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) owns the `web_fetch` tool-call budget by arming `exec.signal`.
|
|
14
|
+
|
|
15
|
+
A shipping web-tool deployment sets the provider backstop above the tool budget, so model calls normally return `TOOL_TIMEOUT`. If the outer deadline reaches the provider first, the provider reports `WEB_ABORTED` and the outer policy replaces it with `TOOL_TIMEOUT`. `WEB_FETCH_TIMEOUT` therefore identifies a direct service caller whose provider budget elapsed.
|
|
16
|
+
|
|
17
|
+
## Transport hygiene
|
|
18
|
+
|
|
19
|
+
- Accepts only `http:` and `https:` URLs; rejects credentials in URLs (`WEB_BLOCKED_URL`) and over-long/malformed URLs (`WEB_INVALID_URL`).
|
|
20
|
+
- Enforces a max URL length, response byte cap (`WEB_FETCH_TOO_LARGE`), decoded body character cap, timeout (`WEB_FETCH_TIMEOUT`), and redirect hop cap.
|
|
21
|
+
- Propagates the caller's abort signal (`WEB_ABORTED`) into the network request and the streaming read.
|
|
22
|
+
- Follows only **same-origin** redirects; a cross-origin redirect fails with `WEB_REDIRECT_BLOCKED`, requiring a fresh tool call (the model of Claude Code's WebFetch).
|
|
23
|
+
- Sends an explicit product `User-Agent`, never a browser disguise.
|
|
24
|
+
- Rejects unsupported (e.g. binary) content types with `WEB_UNSUPPORTED_CONTENT_TYPE`.
|
|
25
|
+
|
|
26
|
+
## Config
|
|
27
|
+
|
|
28
|
+
| Key | Default | Meaning |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `maxUrlLength` | `2048` | Maximum accepted request URL length. |
|
|
31
|
+
| `maxResponseBytes` | `5_000_000` | Maximum response body size in bytes. |
|
|
32
|
+
| `maxBodyChars` | `100_000` | Maximum decoded body length in characters. |
|
|
33
|
+
| `timeoutMs` | `30_000` | Fetch timeout within Node's timer range — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-tool-call-timeout-policy`). |
|
|
34
|
+
| `maxRedirects` | `5` | Maximum same-origin redirect hops (`0` follows none). |
|
|
35
|
+
| `userAgent` | `deepseek-harness/…` | `User-Agent` header. |
|
|
36
|
+
|
|
37
|
+
The numeric limits are validated at plugin construction: every cap except `maxRedirects` must be a positive finite number, and `maxRedirects` must be a non-negative integer. An invalid value throws rather than silently constructing a provider with nonsensical limits.
|
|
38
|
+
|
|
39
|
+
## Model Experience
|
|
40
|
+
|
|
41
|
+
Indirectly, through [`dsh-tool-web`](../tool-web/README.md), which places this provider's `maxBodyChars`-bounded decoded text or markdown-shaped HTML under its fetch-result wrapper and retains provider failures while redirects, headers, and transport mechanics remain hidden.
|
|
42
|
+
|
|
43
|
+
#### KV Cache effect
|
|
44
|
+
|
|
45
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
46
|
+
|
|
47
|
+
## Known Limitations and Deferred Work
|
|
48
|
+
|
|
49
|
+
- **SSRF / private-network protection is deferred** — no blocking of private, loopback, link-local, multicast, or otherwise non-public destinations, no DNS-resolve-then-validate, no per-hop re-validation (see [the web capability seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets.
|
|
50
|
+
- **Only textual content decodes** — html/xhtml and `text/*`-plus-JSON/XML families; a missing `Content-Type` or any binary type throws `WEB_UNSUPPORTED_CONTENT_TYPE`, and text-extractable PDF decoding is named deferred work.
|
|
51
|
+
- **Charset comes only from the `Content-Type` header** (UTF-8 default) — an HTML `<meta charset>` declaration is ignored, and a declared-but-unrecognized charset label throws rather than falling back.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @truly-private/omdsh-web-fetch-http
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
一个匿名公共 HTTP(S) `WebFetchProvider`,用于 harness [web 能力 seam](../web/README.md)(`ctx.web`)。它获取具体 URL,返回状态码和长度受限的解码内容。
|
|
6
|
+
|
|
7
|
+
这是一个**实现**包:它向 `ctx.web` 注册提供方,不拥有该键,也不注册面向模型的工具。它是函数/命名空间插件(`inject: ['web']`)。
|
|
8
|
+
|
|
9
|
+
## 职责拆分
|
|
10
|
+
|
|
11
|
+
提供方拥有**安全资源获取**:URL 验证、HTTP 传输、重定向策略、资源兜底超时、中止传播、字节上限、charset 解码、内容类型分类与二进制拒绝。`@truly-private/omdsh-tool-web` 拥有**呈现**(HTML→markdown、截断格式)。非 2xx HTTP 响应是*结果*(状态码 + 解码主体),不是错误;`WebError` 只用于无法安全获取或表示资源的失败。
|
|
12
|
+
|
|
13
|
+
提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。
|
|
14
|
+
|
|
15
|
+
已交付的 web 工具部署会把提供方兜底设为高于工具预算,因此模型调用通常返回 `TOOL_TIMEOUT`。如果外层截止期限先于提供方的兜底超时触发,提供方会报告 `WEB_ABORTED`,外层策略再将其替换为 `TOOL_TIMEOUT`。因此,`WEB_FETCH_TIMEOUT` 表明直接服务调用方的提供方预算已经耗尽。
|
|
16
|
+
|
|
17
|
+
## 传输卫生
|
|
18
|
+
|
|
19
|
+
- 只接受 `http:` 和 `https:` URL;拒绝 URL 中的凭据(`WEB_BLOCKED_URL`)以及过长/格式错误的 URL(`WEB_INVALID_URL`)。
|
|
20
|
+
- 强制执行 URL 最大长度、响应字节上限(`WEB_FETCH_TOO_LARGE`)、解码主体字符上限、超时(`WEB_FETCH_TIMEOUT`)和重定向跳数上限。
|
|
21
|
+
- 把调用方的中止信号(`WEB_ABORTED`)传播到网络请求与流式读取。
|
|
22
|
+
- 只跟随**同源**重定向;跨源重定向以 `WEB_REDIRECT_BLOCKED` 失败,要求发起新的工具调用(沿用 Claude Code 的 WebFetch 模式)。
|
|
23
|
+
- 发送显式的产品 `User-Agent`,绝不伪装成浏览器。
|
|
24
|
+
- 不受支持的内容类型(例如二进制)以 `WEB_UNSUPPORTED_CONTENT_TYPE` 拒绝。
|
|
25
|
+
|
|
26
|
+
## 配置
|
|
27
|
+
|
|
28
|
+
| 配置键 | 默认值 | 含义 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `maxUrlLength` | `2048` | 接受的请求 URL 最大长度。 |
|
|
31
|
+
| `maxResponseBytes` | `5_000_000` | 响应主体最大字节数。 |
|
|
32
|
+
| `maxBodyChars` | `100_000` | 解码主体最大字符数。 |
|
|
33
|
+
| `timeoutMs` | `30_000` | Node 定时器范围内的抓取超时:直接 `ctx.web.fetch()` 调用方的资源兜底,而非面向模型的工具调用预算(后者属于 `dsh-tool-call-timeout-policy`)。 |
|
|
34
|
+
| `maxRedirects` | `5` | 同源重定向最大跳数(`0` 表示完全不跟随)。 |
|
|
35
|
+
| `userAgent` | `deepseek-harness/…` | `User-Agent` 标头。 |
|
|
36
|
+
|
|
37
|
+
数值限制会在插件构造时验证:除 `maxRedirects` 外,每个上限都必须是正的有限数;`maxRedirects` 必须是非负整数。无效值会抛出异常,不会静默构造限制荒谬的提供方。
|
|
38
|
+
|
|
39
|
+
## 模型体验
|
|
40
|
+
|
|
41
|
+
通过 [`dsh-tool-web`](../tool-web/README.md) 间接影响;该工具把此提供方经 `maxBodyChars` 限制的解码文本或由 HTML 转换得到的 markdown 置于抓取结果包装层中,并保留提供方失败;重定向、标头与传输机制保持隐藏。
|
|
42
|
+
|
|
43
|
+
#### KV Cache 影响
|
|
44
|
+
|
|
45
|
+
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
|
46
|
+
|
|
47
|
+
## 已知限制与暂缓事项
|
|
48
|
+
|
|
49
|
+
- **SSRF/私有网络防护暂缓**:不会阻止私有、loopback、link-local、multicast 或其他非公开目标,也不进行 DNS 解析后验证或逐跳重新验证(见 [web 能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。在此功能落地前,该提供方是 SSRF 原语;能够访问敏感内部网络目标的部署**禁止启用它**。
|
|
50
|
+
- **只解码文本内容**:包括 html/xhtml 与 `text/*` 加 JSON/XML 家族;缺少 `Content-Type` 或任何二进制类型都会抛出 `WEB_UNSUPPORTED_CONTENT_TYPE`,可提取文本的 PDF 解码属于明确的暂缓工作。
|
|
51
|
+
- **charset 只来自 `Content-Type` 标头**(默认为 UTF-8):HTML `<meta charset>` 声明会被忽略;声明但无法识别的 charset 标签会抛出异常,而非回退。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { WebError } from "@truly-private/omdsh-web";
|
|
3
|
+
import { deadline, timeoutOf } from "@truly-private/omdsh-timeout";
|
|
4
|
+
//#region lib/types/policy.js
|
|
5
|
+
/**
|
|
6
|
+
* URL validation and content-type classification for the local HTTP(S) fetch
|
|
7
|
+
* provider — the pure, network-free half. The provider's `fetch()` composes
|
|
8
|
+
* these with transport (redirect following, byte caps, decoding).
|
|
9
|
+
*
|
|
10
|
+
* @module @truly-private/omdsh-web-fetch-http/policy
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Validate a request URL against the basic transport hygiene the provider
|
|
14
|
+
* enforces before any network access: http(s) only, no embedded credentials,
|
|
15
|
+
* bounded length. Returns the parsed `URL`. Throws {@link WebError} otherwise.
|
|
16
|
+
* (SSRF / private-network blocking is deferred — see the package Agent Note.)
|
|
17
|
+
*
|
|
18
|
+
* @param input - the raw URL string from the fetch request.
|
|
19
|
+
* @param maxUrlLength - inclusive upper bound on `input`'s length.
|
|
20
|
+
* @returns the parsed `URL`.
|
|
21
|
+
*/
|
|
22
|
+
function validateFetchUrl(input, maxUrlLength) {
|
|
23
|
+
if (input.length > maxUrlLength) throw new WebError(`URL exceeds the maximum length of ${maxUrlLength}`, "WEB_INVALID_URL");
|
|
24
|
+
let url;
|
|
25
|
+
try {
|
|
26
|
+
url = new URL(input);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new WebError(`invalid URL: ${input}`, "WEB_INVALID_URL", { cause: error });
|
|
29
|
+
}
|
|
30
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new WebError(`unsupported URL scheme "${url.protocol}" (only http and https are allowed)`, "WEB_INVALID_URL");
|
|
31
|
+
if (url.username.length > 0 || url.password.length > 0) throw new WebError("credentials in URLs are not allowed", "WEB_BLOCKED_URL");
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Two URLs are same-origin when scheme, hostname, and port match. A redirect
|
|
36
|
+
* that crosses origins is refused so each new origin requires a fresh tool call
|
|
37
|
+
* (and thus a fresh provider/permission decision).
|
|
38
|
+
*
|
|
39
|
+
* @param a - one of the two URLs to compare.
|
|
40
|
+
* @param b - the other URL to compare.
|
|
41
|
+
* @returns true when `a` and `b` share scheme, hostname, and port.
|
|
42
|
+
*/
|
|
43
|
+
function isSameOrigin(a, b) {
|
|
44
|
+
return a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify a response `Content-Type` into a decodable body kind, or `undefined`
|
|
48
|
+
* for an unsupported (e.g. binary) type. `text/html` and `application/xhtml+xml`
|
|
49
|
+
* are `html`; other `text/*` plus a few structured text types are `text`.
|
|
50
|
+
*
|
|
51
|
+
* @param contentType - the raw `Content-Type` header, or `null` when the
|
|
52
|
+
* response carries none (unsupported).
|
|
53
|
+
* @returns the decodable kind, or `undefined` for an unsupported type.
|
|
54
|
+
*/
|
|
55
|
+
function classifyContentType(contentType) {
|
|
56
|
+
const mime = (contentType ?? "").replace(/;.*$/s, "").trim().toLowerCase();
|
|
57
|
+
if (mime === "text/html" || mime === "application/xhtml+xml") return "html";
|
|
58
|
+
if (mime.startsWith("text/")) return "text";
|
|
59
|
+
if (mime === "application/json" || mime === "application/xml" || mime.endsWith("+json") || mime.endsWith("+xml")) return "text";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract the `charset` parameter from a response `Content-Type`, lower-cased,
|
|
63
|
+
* or `undefined` when absent. The provider feeds this label to `TextDecoder`
|
|
64
|
+
* so a non-UTF-8 response is decoded with its declared encoding rather than
|
|
65
|
+
* silently mangled into replacement characters.
|
|
66
|
+
*
|
|
67
|
+
* @param contentType - the raw `Content-Type` header, or `null` when the
|
|
68
|
+
* response carries none.
|
|
69
|
+
* @returns the lower-cased charset label, or `undefined` when none is declared.
|
|
70
|
+
*/
|
|
71
|
+
function parseCharset(contentType) {
|
|
72
|
+
return /;\s*charset\s*=\s*"?([^";]+)"?/i.exec(contentType ?? "")?.[1]?.trim().toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a `TextDecoder` for the declared charset, falling back to UTF-8 when
|
|
76
|
+
* none is declared. Throws {@link WebError} `WEB_UNSUPPORTED_CONTENT_TYPE` when
|
|
77
|
+
* the label is present but not a charset `TextDecoder` recognizes — better to
|
|
78
|
+
* fail loudly than return mojibake.
|
|
79
|
+
*
|
|
80
|
+
* @param charset - the declared charset label (from {@link parseCharset}), or
|
|
81
|
+
* `undefined` to default to UTF-8.
|
|
82
|
+
* @returns a decoder for the declared (or defaulted) encoding.
|
|
83
|
+
*/
|
|
84
|
+
function decoderForCharset(charset) {
|
|
85
|
+
if (charset === void 0) return new TextDecoder("utf-8");
|
|
86
|
+
try {
|
|
87
|
+
return new TextDecoder(charset);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new WebError(`unsupported charset "${charset}"`, "WEB_UNSUPPORTED_CONTENT_TYPE", { cause: error });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region lib/types/provider.js
|
|
94
|
+
/**
|
|
95
|
+
* Safe HTTP(S) retrieval for `ctx.web`: validates URLs, follows only same-origin redirects,
|
|
96
|
+
* enforces time and size limits, classifies and decodes text, and leaves presentation to
|
|
97
|
+
* `@truly-private/omdsh-tool-web`. Requests carry no browser cookies or ambient credentials.
|
|
98
|
+
*
|
|
99
|
+
* Private-network and SSRF protection is not implemented; do not enable this provider where
|
|
100
|
+
* it can reach sensitive internal targets.
|
|
101
|
+
* @module @truly-private/omdsh-web-fetch-http/provider
|
|
102
|
+
*/
|
|
103
|
+
var __addDisposableResource = function(env, value, async) {
|
|
104
|
+
if (value !== null && value !== void 0) {
|
|
105
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
106
|
+
var dispose, inner;
|
|
107
|
+
if (async) {
|
|
108
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
109
|
+
dispose = value[Symbol.asyncDispose];
|
|
110
|
+
}
|
|
111
|
+
if (dispose === void 0) {
|
|
112
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
113
|
+
dispose = value[Symbol.dispose];
|
|
114
|
+
if (async) inner = dispose;
|
|
115
|
+
}
|
|
116
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
117
|
+
if (inner) dispose = function() {
|
|
118
|
+
try {
|
|
119
|
+
inner.call(this);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return Promise.reject(e);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
env.stack.push({
|
|
125
|
+
value,
|
|
126
|
+
dispose,
|
|
127
|
+
async
|
|
128
|
+
});
|
|
129
|
+
} else if (async) env.stack.push({ async: true });
|
|
130
|
+
return value;
|
|
131
|
+
};
|
|
132
|
+
var __disposeResources = (function(SuppressedError) {
|
|
133
|
+
return function(env) {
|
|
134
|
+
function fail(e) {
|
|
135
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
136
|
+
env.hasError = true;
|
|
137
|
+
}
|
|
138
|
+
var r, s = 0;
|
|
139
|
+
function next() {
|
|
140
|
+
while (r = env.stack.pop()) try {
|
|
141
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
142
|
+
if (r.dispose) {
|
|
143
|
+
var result = r.dispose.call(r.value);
|
|
144
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
|
|
145
|
+
fail(e);
|
|
146
|
+
return next();
|
|
147
|
+
});
|
|
148
|
+
} else s |= 1;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
fail(e);
|
|
151
|
+
}
|
|
152
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
153
|
+
if (env.hasError) throw env.error;
|
|
154
|
+
}
|
|
155
|
+
return next();
|
|
156
|
+
};
|
|
157
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
158
|
+
var e = new Error(message);
|
|
159
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
160
|
+
});
|
|
161
|
+
/** Stable id this provider registers under. */
|
|
162
|
+
const LOCAL_FETCH_PROVIDER_ID = "http";
|
|
163
|
+
/** The anonymous public HTTP(S) fetch provider. */
|
|
164
|
+
var HttpFetchProvider = class {
|
|
165
|
+
limits;
|
|
166
|
+
id = LOCAL_FETCH_PROVIDER_ID;
|
|
167
|
+
constructor(limits) {
|
|
168
|
+
this.limits = limits;
|
|
169
|
+
}
|
|
170
|
+
/** No credentials to check — an anonymous public fetcher is always usable. */
|
|
171
|
+
available() {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
async fetch(request, signal) {
|
|
175
|
+
const env_1 = {
|
|
176
|
+
stack: [],
|
|
177
|
+
error: void 0,
|
|
178
|
+
hasError: false
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
if (signal?.aborted) throw new WebError("web fetch aborted", "WEB_ABORTED");
|
|
182
|
+
const d = __addDisposableResource(env_1, deadline(signal, this.limits.timeoutMs, "WEB_FETCH_TIMEOUT"), false);
|
|
183
|
+
return await this.followAndRead(request.url, d.signal);
|
|
184
|
+
} catch (e_1) {
|
|
185
|
+
env_1.error = e_1;
|
|
186
|
+
env_1.hasError = true;
|
|
187
|
+
} finally {
|
|
188
|
+
__disposeResources(env_1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Follow same-origin redirects up to the hop cap, then read the final response. */
|
|
192
|
+
async followAndRead(initialUrl, signal) {
|
|
193
|
+
let currentUrl = validateFetchUrl(initialUrl, this.limits.maxUrlLength);
|
|
194
|
+
let redirectsFollowed = 0;
|
|
195
|
+
for (;;) {
|
|
196
|
+
const response = await this.requestOnce(currentUrl, signal);
|
|
197
|
+
if (isRedirectStatus(response.status)) {
|
|
198
|
+
if (redirectsFollowed >= this.limits.maxRedirects) {
|
|
199
|
+
await response.body?.cancel();
|
|
200
|
+
throw new WebError(`exceeded the maximum of ${this.limits.maxRedirects} redirects`, "WEB_REDIRECT_BLOCKED");
|
|
201
|
+
}
|
|
202
|
+
const location = response.headers.get("location");
|
|
203
|
+
if (location === null) {
|
|
204
|
+
await response.body?.cancel();
|
|
205
|
+
throw new WebError(`redirect response (HTTP ${response.status}) without a Location header`, "WEB_PROVIDER_ERROR");
|
|
206
|
+
}
|
|
207
|
+
const target = resolveRedirect(location, currentUrl);
|
|
208
|
+
let validatedTarget;
|
|
209
|
+
try {
|
|
210
|
+
validatedTarget = validateFetchUrl(target.toString(), this.limits.maxUrlLength);
|
|
211
|
+
if (!isSameOrigin(validatedTarget, currentUrl)) throw new WebError(`cross-origin redirect to ${validatedTarget.origin} is not followed automatically; retry against that URL directly`, "WEB_REDIRECT_BLOCKED");
|
|
212
|
+
} catch (error) {
|
|
213
|
+
await response.body?.cancel();
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
await response.body?.cancel();
|
|
217
|
+
currentUrl = validatedTarget;
|
|
218
|
+
redirectsFollowed++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
return await this.readBody(response, currentUrl, signal);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async requestOnce(url, signal) {
|
|
225
|
+
try {
|
|
226
|
+
return await fetch(url, {
|
|
227
|
+
method: "GET",
|
|
228
|
+
redirect: "manual",
|
|
229
|
+
headers: {
|
|
230
|
+
"user-agent": this.limits.userAgent,
|
|
231
|
+
"accept": "text/html,application/xhtml+xml,text/*;q=0.9,application/json;q=0.8"
|
|
232
|
+
},
|
|
233
|
+
signal
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw translateAbortOrNetwork(error, signal);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Read, byte-cap, classify, and decode the final response body. */
|
|
240
|
+
async readBody(response, finalUrl, signal) {
|
|
241
|
+
const contentType = response.headers.get("content-type");
|
|
242
|
+
const kind = classifyContentType(contentType);
|
|
243
|
+
if (kind === void 0) {
|
|
244
|
+
await response.body?.cancel();
|
|
245
|
+
throw new WebError(`unsupported content type "${contentType ?? "unknown"}"`, "WEB_UNSUPPORTED_CONTENT_TYPE");
|
|
246
|
+
}
|
|
247
|
+
let decoder;
|
|
248
|
+
try {
|
|
249
|
+
decoder = decoderForCharset(parseCharset(contentType));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
await response.body?.cancel();
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
const { bytes, truncatedByBytes } = await this.readCapped(response, signal);
|
|
255
|
+
const decoded = decoder.decode(bytes);
|
|
256
|
+
const truncatedByChars = decoded.length > this.limits.maxBodyChars;
|
|
257
|
+
const content = truncatedByChars ? decoded.slice(0, this.limits.maxBodyChars) : decoded;
|
|
258
|
+
const body = kind === "html" ? {
|
|
259
|
+
kind: "html",
|
|
260
|
+
content
|
|
261
|
+
} : {
|
|
262
|
+
kind: "text",
|
|
263
|
+
content
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
url: finalUrl.toString(),
|
|
267
|
+
statusCode: response.status,
|
|
268
|
+
body,
|
|
269
|
+
truncated: truncatedByBytes || truncatedByChars
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Read the response stream up to `maxResponseBytes`. A `Content-Length` over
|
|
274
|
+
* the cap rejects immediately with `WEB_FETCH_TOO_LARGE`; a stream that grows
|
|
275
|
+
* past the cap is cut short (`truncatedByBytes`) rather than rejected, so a
|
|
276
|
+
* server that under-reports still yields a bounded usable body.
|
|
277
|
+
*/
|
|
278
|
+
async readCapped(response, signal) {
|
|
279
|
+
const declared = response.headers.get("content-length");
|
|
280
|
+
if (declared !== null) {
|
|
281
|
+
const length = Number(declared);
|
|
282
|
+
if (Number.isFinite(length) && length > this.limits.maxResponseBytes) {
|
|
283
|
+
await response.body?.cancel();
|
|
284
|
+
throw new WebError(`response exceeds the maximum of ${this.limits.maxResponseBytes} bytes`, "WEB_FETCH_TOO_LARGE");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/* v8 ignore next -- a 2xx Response from fetch always exposes a body stream; the null guard is defensive. */
|
|
288
|
+
if (response.body === null) return {
|
|
289
|
+
bytes: new Uint8Array(0),
|
|
290
|
+
truncatedByBytes: false
|
|
291
|
+
};
|
|
292
|
+
const chunks = [];
|
|
293
|
+
let total = 0;
|
|
294
|
+
let truncatedByBytes = false;
|
|
295
|
+
const reader = response.body.getReader();
|
|
296
|
+
try {
|
|
297
|
+
for (;;) {
|
|
298
|
+
const { done, value } = await reader.read();
|
|
299
|
+
if (done) break;
|
|
300
|
+
const remaining = this.limits.maxResponseBytes - total;
|
|
301
|
+
if (value.byteLength > remaining) {
|
|
302
|
+
chunks.push(value.subarray(0, remaining));
|
|
303
|
+
total += remaining;
|
|
304
|
+
truncatedByBytes = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
chunks.push(value);
|
|
308
|
+
total += value.byteLength;
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
/* v8 ignore next -- mid-stream read fault needs a network drop after headers; translate path covered by request-phase tests. */
|
|
312
|
+
throw translateAbortOrNetwork(error, signal);
|
|
313
|
+
} finally {
|
|
314
|
+
/* v8 ignore next 4 -- cancel() after a completed/broken read settles without rejecting; unobserved best-effort cleanup. */
|
|
315
|
+
await reader.cancel().catch(() => {});
|
|
316
|
+
}
|
|
317
|
+
const bytes = new Uint8Array(total);
|
|
318
|
+
let offset = 0;
|
|
319
|
+
for (const chunk of chunks) {
|
|
320
|
+
bytes.set(chunk, offset);
|
|
321
|
+
offset += chunk.byteLength;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
bytes,
|
|
325
|
+
truncatedByBytes
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
/** HTTP redirect status codes that carry a `Location`. */
|
|
330
|
+
function isRedirectStatus(status) {
|
|
331
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
332
|
+
}
|
|
333
|
+
/** Resolve a (possibly relative) `Location` against the current URL. */
|
|
334
|
+
function resolveRedirect(location, base) {
|
|
335
|
+
try {
|
|
336
|
+
return new URL(location, base);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
/* v8 ignore next 2 -- URL resolution against a valid absolute base effectively never throws; defensive guard. */
|
|
339
|
+
throw new WebError(`invalid redirect Location "${location}"`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Translate a thrown fetch/stream error into a `WebError`, classified by the
|
|
344
|
+
* deadline signal rather than the thrown value (which differs by phase: the
|
|
345
|
+
* request-phase `fetch` rejects with the abort reason, while the read-phase
|
|
346
|
+
* reader surfaces a bare `AbortError`). `timeoutOf(signal, 'WEB_FETCH_TIMEOUT')`
|
|
347
|
+
* recovering OUR reason means our timeout fired (`WEB_FETCH_TIMEOUT`); any other
|
|
348
|
+
* abort — an upstream cancel, or a foreign/outer deadline's timeout under
|
|
349
|
+
* nesting — is `WEB_ABORTED`; a throw with the signal NOT aborted is a
|
|
350
|
+
* transport/network failure (`WEB_PROVIDER_ERROR`).
|
|
351
|
+
*/
|
|
352
|
+
function translateAbortOrNetwork(error, signal) {
|
|
353
|
+
const timeout = timeoutOf(signal, "WEB_FETCH_TIMEOUT");
|
|
354
|
+
if (timeout !== void 0) return new WebError("web fetch timed out", "WEB_FETCH_TIMEOUT", { cause: timeout });
|
|
355
|
+
if (signal.aborted) return new WebError("web fetch aborted", "WEB_ABORTED", { cause: error });
|
|
356
|
+
return new WebError(`web fetch failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region lib/types/index.js
|
|
360
|
+
/**
|
|
361
|
+
* `@truly-private/omdsh-web-fetch-http`: registers an anonymous public HTTP(S)
|
|
362
|
+
* `WebFetchProvider` with `ctx.web`. A function/namespace plugin (NOT a
|
|
363
|
+
* default-export service): it registers INTO the seam's fetch registry, like the
|
|
364
|
+
* search providers register into the search registry.
|
|
365
|
+
*
|
|
366
|
+
* @module @truly-private/omdsh-web-fetch-http
|
|
367
|
+
*/
|
|
368
|
+
const MAX_NODE_TIMER_DELAY_MS = 2147483647;
|
|
369
|
+
/** Default `User-Agent`: an explicit product agent, never a browser disguise. */
|
|
370
|
+
const DEFAULT_USER_AGENT = "deepseek-harness/0.0.1 (+https://github.com/deepseek-ai)";
|
|
371
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
372
|
+
const name = "web-fetch-http";
|
|
373
|
+
/** The web seam this provider registers into. */
|
|
374
|
+
const inject = ["web"];
|
|
375
|
+
const Config = z.object({
|
|
376
|
+
maxUrlLength: z.number().default(2048),
|
|
377
|
+
maxResponseBytes: z.number().default(5e6),
|
|
378
|
+
maxBodyChars: z.number().default(1e5),
|
|
379
|
+
timeoutMs: z.number().default(3e4),
|
|
380
|
+
maxRedirects: z.number().default(5),
|
|
381
|
+
userAgent: z.string().default(DEFAULT_USER_AGENT)
|
|
382
|
+
});
|
|
383
|
+
/** A resource limit (byte/char/length/timeout cap) must be a positive finite number. */
|
|
384
|
+
function assertPositiveFinite(name, value) {
|
|
385
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`web-fetch-http: ${name} must be a positive finite number`);
|
|
386
|
+
}
|
|
387
|
+
/** Node coerces larger timer delays to 1 ms, so reject them at configuration time. */
|
|
388
|
+
function assertTimeoutMs(value) {
|
|
389
|
+
assertPositiveFinite("timeoutMs", value);
|
|
390
|
+
if (value > MAX_NODE_TIMER_DELAY_MS) throw new Error(`web-fetch-http: timeoutMs must be no greater than ${MAX_NODE_TIMER_DELAY_MS}`);
|
|
391
|
+
}
|
|
392
|
+
/** The redirect hop cap must be a non-negative integer (0 follows no redirects). */
|
|
393
|
+
function assertNonNegativeInteger(name, value) {
|
|
394
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`web-fetch-http: ${name} must be a non-negative integer`);
|
|
395
|
+
}
|
|
396
|
+
/** Register the local HTTP(S) fetch provider with `ctx.web`. */
|
|
397
|
+
function apply(ctx, config) {
|
|
398
|
+
const resolved = config;
|
|
399
|
+
assertPositiveFinite("maxUrlLength", resolved.maxUrlLength);
|
|
400
|
+
assertPositiveFinite("maxResponseBytes", resolved.maxResponseBytes);
|
|
401
|
+
assertPositiveFinite("maxBodyChars", resolved.maxBodyChars);
|
|
402
|
+
assertTimeoutMs(resolved.timeoutMs);
|
|
403
|
+
assertNonNegativeInteger("maxRedirects", resolved.maxRedirects);
|
|
404
|
+
const limits = {
|
|
405
|
+
maxUrlLength: resolved.maxUrlLength,
|
|
406
|
+
maxResponseBytes: resolved.maxResponseBytes,
|
|
407
|
+
maxBodyChars: resolved.maxBodyChars,
|
|
408
|
+
timeoutMs: resolved.timeoutMs,
|
|
409
|
+
maxRedirects: resolved.maxRedirects,
|
|
410
|
+
userAgent: resolved.userAgent
|
|
411
|
+
};
|
|
412
|
+
ctx.web.registerFetchProvider(new HttpFetchProvider(limits));
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
export { Config, DEFAULT_USER_AGENT, HttpFetchProvider, LOCAL_FETCH_PROVIDER_ID, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@truly-private/omdsh-web-fetch-http`.
|
|
4
|
+
* @module @truly-private/omdsh-web-fetch-http/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@truly-private/omdsh-web-fetch-http";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "web-fetch-http-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@truly-private/omdsh-web-fetch-http`: registers an anonymous public HTTP(S)
|
|
3
|
+
* `WebFetchProvider` with `ctx.web`. A function/namespace plugin (NOT a
|
|
4
|
+
* default-export service): it registers INTO the seam's fetch registry, like the
|
|
5
|
+
* search providers register into the search registry.
|
|
6
|
+
*
|
|
7
|
+
* @module @truly-private/omdsh-web-fetch-http
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
export { LOCAL_FETCH_PROVIDER_ID, HttpFetchProvider, } from './provider.ts';
|
|
12
|
+
export type { HttpFetchLimits } from './provider.ts';
|
|
13
|
+
/** Default `User-Agent`: an explicit product agent, never a browser disguise. */
|
|
14
|
+
export declare const DEFAULT_USER_AGENT = "deepseek-harness/0.0.1 (+https://github.com/deepseek-ai)";
|
|
15
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
16
|
+
export declare const name = "web-fetch-http";
|
|
17
|
+
/** The web seam this provider registers into. */
|
|
18
|
+
export declare const inject: string[];
|
|
19
|
+
/** Plugin config: the provider's transport and size limits plus its `User-Agent` (all defaulted). */
|
|
20
|
+
export interface Config {
|
|
21
|
+
/** Maximum accepted request URL length. */
|
|
22
|
+
maxUrlLength?: number;
|
|
23
|
+
/** Maximum response body size in bytes. */
|
|
24
|
+
maxResponseBytes?: number;
|
|
25
|
+
/** Maximum decoded body length in characters. */
|
|
26
|
+
maxBodyChars?: number;
|
|
27
|
+
/** Default fetch timeout in milliseconds, within Node's timer range. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
/** Maximum number of same-origin redirect hops to follow. */
|
|
30
|
+
maxRedirects?: number;
|
|
31
|
+
/** `User-Agent` header sent on every request. */
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const Config: z<Config>;
|
|
35
|
+
/** Register the local HTTP(S) fetch provider with `ctx.web`. */
|
|
36
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@truly-private/omdsh-web-fetch-http`.
|
|
3
|
+
* @module @truly-private/omdsh-web-fetch-http/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "web-fetch-http-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation and content-type classification for the local HTTP(S) fetch
|
|
3
|
+
* provider — the pure, network-free half. The provider's `fetch()` composes
|
|
4
|
+
* these with transport (redirect following, byte caps, decoding).
|
|
5
|
+
*
|
|
6
|
+
* @module @truly-private/omdsh-web-fetch-http/policy
|
|
7
|
+
*/
|
|
8
|
+
/** The body kinds this provider decodes. */
|
|
9
|
+
export type FetchableKind = 'html' | 'text';
|
|
10
|
+
/**
|
|
11
|
+
* Validate a request URL against the basic transport hygiene the provider
|
|
12
|
+
* enforces before any network access: http(s) only, no embedded credentials,
|
|
13
|
+
* bounded length. Returns the parsed `URL`. Throws {@link WebError} otherwise.
|
|
14
|
+
* (SSRF / private-network blocking is deferred — see the package Agent Note.)
|
|
15
|
+
*
|
|
16
|
+
* @param input - the raw URL string from the fetch request.
|
|
17
|
+
* @param maxUrlLength - inclusive upper bound on `input`'s length.
|
|
18
|
+
* @returns the parsed `URL`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateFetchUrl(input: string, maxUrlLength: number): URL;
|
|
21
|
+
/**
|
|
22
|
+
* Two URLs are same-origin when scheme, hostname, and port match. A redirect
|
|
23
|
+
* that crosses origins is refused so each new origin requires a fresh tool call
|
|
24
|
+
* (and thus a fresh provider/permission decision).
|
|
25
|
+
*
|
|
26
|
+
* @param a - one of the two URLs to compare.
|
|
27
|
+
* @param b - the other URL to compare.
|
|
28
|
+
* @returns true when `a` and `b` share scheme, hostname, and port.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isSameOrigin(a: URL, b: URL): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Classify a response `Content-Type` into a decodable body kind, or `undefined`
|
|
33
|
+
* for an unsupported (e.g. binary) type. `text/html` and `application/xhtml+xml`
|
|
34
|
+
* are `html`; other `text/*` plus a few structured text types are `text`.
|
|
35
|
+
*
|
|
36
|
+
* @param contentType - the raw `Content-Type` header, or `null` when the
|
|
37
|
+
* response carries none (unsupported).
|
|
38
|
+
* @returns the decodable kind, or `undefined` for an unsupported type.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classifyContentType(contentType: string | null): FetchableKind | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Extract the `charset` parameter from a response `Content-Type`, lower-cased,
|
|
43
|
+
* or `undefined` when absent. The provider feeds this label to `TextDecoder`
|
|
44
|
+
* so a non-UTF-8 response is decoded with its declared encoding rather than
|
|
45
|
+
* silently mangled into replacement characters.
|
|
46
|
+
*
|
|
47
|
+
* @param contentType - the raw `Content-Type` header, or `null` when the
|
|
48
|
+
* response carries none.
|
|
49
|
+
* @returns the lower-cased charset label, or `undefined` when none is declared.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseCharset(contentType: string | null): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Build a `TextDecoder` for the declared charset, falling back to UTF-8 when
|
|
54
|
+
* none is declared. Throws {@link WebError} `WEB_UNSUPPORTED_CONTENT_TYPE` when
|
|
55
|
+
* the label is present but not a charset `TextDecoder` recognizes — better to
|
|
56
|
+
* fail loudly than return mojibake.
|
|
57
|
+
*
|
|
58
|
+
* @param charset - the declared charset label (from {@link parseCharset}), or
|
|
59
|
+
* `undefined` to default to UTF-8.
|
|
60
|
+
* @returns a decoder for the declared (or defaulted) encoding.
|
|
61
|
+
*/
|
|
62
|
+
export declare function decoderForCharset(charset: string | undefined): TextDecoder;
|
|
63
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe HTTP(S) retrieval for `ctx.web`: validates URLs, follows only same-origin redirects,
|
|
3
|
+
* enforces time and size limits, classifies and decodes text, and leaves presentation to
|
|
4
|
+
* `@truly-private/omdsh-tool-web`. Requests carry no browser cookies or ambient credentials.
|
|
5
|
+
*
|
|
6
|
+
* Private-network and SSRF protection is not implemented; do not enable this provider where
|
|
7
|
+
* it can reach sensitive internal targets.
|
|
8
|
+
* @module @truly-private/omdsh-web-fetch-http/provider
|
|
9
|
+
*/
|
|
10
|
+
import type { WebFetchProvider, WebFetchRequest, WebFetchResult } from '@truly-private/omdsh-web';
|
|
11
|
+
/** Resolved provider limits (the plugin's schemastery Config supplies defaults). */
|
|
12
|
+
export interface HttpFetchLimits {
|
|
13
|
+
/** Maximum accepted request URL length. */
|
|
14
|
+
maxUrlLength: number;
|
|
15
|
+
/** Maximum response body size in bytes (read is aborted past this). */
|
|
16
|
+
maxResponseBytes: number;
|
|
17
|
+
/** Maximum decoded body length in characters (truncated past this). */
|
|
18
|
+
maxBodyChars: number;
|
|
19
|
+
/** Default fetch timeout in milliseconds. */
|
|
20
|
+
timeoutMs: number;
|
|
21
|
+
/** Maximum number of (same-origin) redirect hops to follow. */
|
|
22
|
+
maxRedirects: number;
|
|
23
|
+
/** `User-Agent` header sent on every request. */
|
|
24
|
+
userAgent: string;
|
|
25
|
+
}
|
|
26
|
+
/** Stable id this provider registers under. */
|
|
27
|
+
export declare const LOCAL_FETCH_PROVIDER_ID = "http";
|
|
28
|
+
/** The anonymous public HTTP(S) fetch provider. */
|
|
29
|
+
export declare class HttpFetchProvider implements WebFetchProvider {
|
|
30
|
+
private readonly limits;
|
|
31
|
+
readonly id = "http";
|
|
32
|
+
constructor(limits: HttpFetchLimits);
|
|
33
|
+
/** No credentials to check — an anonymous public fetcher is always usable. */
|
|
34
|
+
available(): boolean;
|
|
35
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
36
|
+
/** Follow same-origin redirects up to the hop cap, then read the final response. */
|
|
37
|
+
private followAndRead;
|
|
38
|
+
private requestOnce;
|
|
39
|
+
/** Read, byte-cap, classify, and decode the final response body. */
|
|
40
|
+
private readBody;
|
|
41
|
+
/**
|
|
42
|
+
* Read the response stream up to `maxResponseBytes`. A `Content-Length` over
|
|
43
|
+
* the cap rejects immediately with `WEB_FETCH_TOO_LARGE`; a stream that grows
|
|
44
|
+
* past the cap is cut short (`truncatedByBytes`) rather than rejected, so a
|
|
45
|
+
* server that under-reports still yields a bounded usable body.
|
|
46
|
+
*/
|
|
47
|
+
private readCapped;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=provider.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@truly-private/omdsh-web-fetch-http",
|
|
3
|
+
"description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Truly-Private/oh-my-deepseek-harness.git",
|
|
11
|
+
"directory": "packages/web/web-fetch-http"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@truly-private/omdsh-invariants": "^0.0.1",
|
|
36
|
+
"@truly-private/omdsh-timeout": "^0.0.1",
|
|
37
|
+
"@truly-private/omdsh-web": "^0.0.1",
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@truly-private/omdsh-timeout": "^0.0.1",
|
|
45
|
+
"@truly-private/omdsh-web": "^0.0.1",
|
|
46
|
+
"@truly-private/omdsh-invariants": "^0.0.1",
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
48
|
+
}
|
|
49
|
+
}
|