@zeldrisho/pi-web-fetch 0.9.0 → 0.9.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/CHANGELOG.md +12 -1
- package/package.json +7 -7
- package/src/fetch.ts +54 -11
- package/src/network-transport.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.1] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Upgrade Defuddle to `^0.19.4` for improved metadata and Markdown extraction, HTML sanitization, SVG safety, and support for declarative shadow DOM and MathML content
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Extract HTML correctly when servers send duplicated equivalent `Content-Type` media types, and reject conflicting media-type declarations instead of returning raw HTML
|
|
19
|
+
|
|
10
20
|
## [0.9.0] - 2026-09-17
|
|
11
21
|
|
|
12
22
|
### Added
|
|
@@ -148,7 +158,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
148
158
|
|
|
149
159
|
- Add bounded public page fetching ([b979496](https://github.com/zeldrisho/pi-packages/commit/b979496b32de1cead172ba570307e4a4a7b3421d))
|
|
150
160
|
|
|
151
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.9.
|
|
161
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.9.1...HEAD
|
|
162
|
+
[0.9.1]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.9.0...pi-web-fetch-v0.9.1
|
|
152
163
|
[0.9.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.8.2...pi-web-fetch-v0.9.0
|
|
153
164
|
[0.8.2]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.8.1...pi-web-fetch-v0.8.2
|
|
154
165
|
[0.8.1]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.8.0...pi-web-fetch-v0.8.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeldrisho/pi-web-fetch",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Pi extension for secure, bounded public web page fetching and Markdown extraction",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"defuddle": "^0.19.
|
|
32
|
+
"defuddle": "^0.19.4",
|
|
33
33
|
"linkedom": "^0.18.13"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
37
|
-
"@earendil-works/pi-tui": "^0.
|
|
38
|
-
"typebox": "^1.3.
|
|
36
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
37
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
38
|
+
"typebox": "^1.3.30",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.
|
|
41
|
-
"vite-plus": "0.3.
|
|
40
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
|
|
41
|
+
"vite-plus": "0.3.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/fetch.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FETCH_MAX_BYTES,
|
|
12
12
|
readResponseBytes,
|
|
13
13
|
responseHeader,
|
|
14
|
+
responseHeaderValues,
|
|
14
15
|
} from "./network-transport";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -171,6 +172,43 @@ export interface FetchRemoteDependencies extends RedirectDependencies {
|
|
|
171
172
|
timeoutMs?: number;
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Returns a usable media type from a response header.
|
|
177
|
+
*
|
|
178
|
+
* Some servers incorrectly emit an equivalent media type more than once (for example,
|
|
179
|
+
* `text/html,text/html; charset=utf-8`). Treat that as the declared type, but reject
|
|
180
|
+
* conflicting media types rather than guessing which representation to parse.
|
|
181
|
+
*/
|
|
182
|
+
function normalizeContentType(header: string): string | undefined {
|
|
183
|
+
const mediaTypes: string[] = [];
|
|
184
|
+
let current = "";
|
|
185
|
+
let quoted = false;
|
|
186
|
+
|
|
187
|
+
for (const character of header) {
|
|
188
|
+
if (character === '"') quoted = !quoted;
|
|
189
|
+
|
|
190
|
+
if (character === "," && !quoted) {
|
|
191
|
+
mediaTypes.push(current);
|
|
192
|
+
current = "";
|
|
193
|
+
} else current += character;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
mediaTypes.push(current);
|
|
197
|
+
const normalized: string[] = [];
|
|
198
|
+
|
|
199
|
+
for (const value of mediaTypes) {
|
|
200
|
+
const mediaType = value.split(";", 1)[0].trim().toLowerCase();
|
|
201
|
+
|
|
202
|
+
if (mediaType) normalized.push(mediaType);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (normalized.length === 0 || normalized.some((value) => value !== normalized[0])) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return normalized[0];
|
|
210
|
+
}
|
|
211
|
+
|
|
174
212
|
/**
|
|
175
213
|
* Converts a successful HTTP response into a complete document.
|
|
176
214
|
*
|
|
@@ -200,8 +238,10 @@ async function documentFromResponse(
|
|
|
200
238
|
throw new Error(`web_fetch returned HTTP ${status}.${authenticationHint}`);
|
|
201
239
|
}
|
|
202
240
|
|
|
203
|
-
const contentTypeHeader =
|
|
204
|
-
|
|
241
|
+
const contentTypeHeader =
|
|
242
|
+
responseHeaderValues(response, "content-type").join(", ") || "text/plain";
|
|
243
|
+
|
|
244
|
+
const contentType = normalizeContentType(contentTypeHeader);
|
|
205
245
|
|
|
206
246
|
const linkHints = parseLinkHeaderForAgentHints(
|
|
207
247
|
responseHeader(response, "link") ?? "",
|
|
@@ -209,18 +249,21 @@ async function documentFromResponse(
|
|
|
209
249
|
);
|
|
210
250
|
|
|
211
251
|
const allowed =
|
|
212
|
-
contentType
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
contentType !== undefined &&
|
|
253
|
+
(contentType.startsWith("text/") ||
|
|
254
|
+
[
|
|
255
|
+
"application/json",
|
|
256
|
+
"application/markdown",
|
|
257
|
+
"application/x-markdown",
|
|
258
|
+
"application/xml",
|
|
259
|
+
"application/xhtml+xml",
|
|
260
|
+
].includes(contentType));
|
|
220
261
|
|
|
221
262
|
if (!allowed) {
|
|
222
263
|
response.destroy();
|
|
223
|
-
throw new Error(
|
|
264
|
+
throw new Error(
|
|
265
|
+
`web_fetch does not support ${contentType ?? (contentTypeHeader || "this content type")}.`,
|
|
266
|
+
);
|
|
224
267
|
}
|
|
225
268
|
|
|
226
269
|
const raw = decodeResponse(
|
package/src/network-transport.ts
CHANGED
|
@@ -188,6 +188,25 @@ export function responseHeader(response: IncomingMessage, name: string): string
|
|
|
188
188
|
return Array.isArray(value) ? value[0] : value;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
export function responseHeaderValues(response: IncomingMessage, name: string): string[] {
|
|
192
|
+
const values: string[] = [];
|
|
193
|
+
const lowerName = name.toLowerCase();
|
|
194
|
+
|
|
195
|
+
const rawHeaders = response.rawHeaders ?? [];
|
|
196
|
+
|
|
197
|
+
for (let index = 0; index + 1 < rawHeaders.length; index += 2) {
|
|
198
|
+
if (rawHeaders[index].toLowerCase() === lowerName) {
|
|
199
|
+
values.push(rawHeaders[index + 1]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (values.length > 0) return values;
|
|
204
|
+
|
|
205
|
+
const value = response.headers[name];
|
|
206
|
+
|
|
207
|
+
return value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
/**
|
|
192
211
|
* Read and buffer the complete response body.
|
|
193
212
|
*
|