pi-unsloth-webtools 0.3.1 → 0.4.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/README.md +52 -3
- package/agent-dir.ts +19 -0
- package/cache.ts +154 -0
- package/engines.ts +70 -8
- package/html-to-md.ts +12 -3
- package/index.ts +24 -6
- package/package.json +4 -1
- package/pdf.ts +1 -1
- package/settings.ts +114 -0
- package/web-access.ts +48 -27
- package/web-fetch.ts +311 -106
- package/web-search.ts +7 -1
package/web-access.ts
CHANGED
|
@@ -6,7 +6,13 @@ const MAX_CACHEABLE_DOMAIN_LEN = 253;
|
|
|
6
6
|
const SITE_FILTER_LIMIT = 8;
|
|
7
7
|
const DOTTED_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/;
|
|
8
8
|
const PORT_RE = /^[0-9]{1,5}$/;
|
|
9
|
+
export const MAX_SIGNAL_TIMEOUT_MS = 2 ** 31 - 1;
|
|
9
10
|
|
|
11
|
+
function throwInvalidDomain(value: unknown): never {
|
|
12
|
+
throw new Error(`Invalid website domain: ${String(value)}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const INVALID_HOST_REASON = "Blocked: the URL has an invalid hostname or port.";
|
|
10
16
|
export interface WebsitePolicy {
|
|
11
17
|
allowedDomains: string[];
|
|
12
18
|
blockedDomains: string[];
|
|
@@ -19,18 +25,19 @@ export function normalizeDomain(value: unknown): string {
|
|
|
19
25
|
[...domain].some((char) => char.charCodeAt(0) < 32) ||
|
|
20
26
|
["\\", "/", "@", "?", "#"].some((char) => domain.includes(char))
|
|
21
27
|
) {
|
|
22
|
-
|
|
28
|
+
throwInvalidDomain(value);
|
|
23
29
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const startsBracket = domain.startsWith("[");
|
|
31
|
+
const endsBracket = domain.endsWith("]");
|
|
32
|
+
if (startsBracket !== endsBracket) {
|
|
33
|
+
throwInvalidDomain(value);
|
|
27
34
|
}
|
|
28
|
-
const stripped = (
|
|
35
|
+
const stripped = (startsBracket ? domain.slice(1, -1) : domain).replace(/\.+$/, "");
|
|
29
36
|
if (/^[0-9a-fA-F:.]+$/.test(stripped) && stripped.includes(":")) {
|
|
30
37
|
try {
|
|
31
38
|
return compressIpv6(stripped);
|
|
32
39
|
} catch {
|
|
33
|
-
|
|
40
|
+
throwInvalidDomain(value);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
if (stripped.includes(":")) {
|
|
@@ -52,13 +59,13 @@ export function normalizeDomain(value: unknown): string {
|
|
|
52
59
|
try {
|
|
53
60
|
asciiDomain = domainToASCII(stripped).toLowerCase();
|
|
54
61
|
} catch {
|
|
55
|
-
|
|
62
|
+
throwInvalidDomain(value);
|
|
56
63
|
}
|
|
57
64
|
if (
|
|
58
65
|
asciiDomain.length > 253 ||
|
|
59
66
|
!asciiDomain.split(".").every((label) => DOMAIN_LABEL_RE.test(label))
|
|
60
67
|
) {
|
|
61
|
-
|
|
68
|
+
throwInvalidDomain(value);
|
|
62
69
|
}
|
|
63
70
|
return asciiDomain;
|
|
64
71
|
}
|
|
@@ -194,7 +201,7 @@ export function checkUrlAccess(
|
|
|
194
201
|
try {
|
|
195
202
|
parsed = new URL(candidate);
|
|
196
203
|
} catch {
|
|
197
|
-
return [false,
|
|
204
|
+
return [false, INVALID_HOST_REASON, ""];
|
|
198
205
|
}
|
|
199
206
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
200
207
|
if (scheme !== "http" && scheme !== "https") {
|
|
@@ -204,20 +211,16 @@ export function checkUrlAccess(
|
|
|
204
211
|
return [false, "Blocked: URLs with credentials or encoded hostnames are not allowed.", ""];
|
|
205
212
|
}
|
|
206
213
|
if (!parsed.hostname) {
|
|
207
|
-
return [false,
|
|
214
|
+
return [false, INVALID_HOST_REASON, ""];
|
|
208
215
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return [false, "Blocked: the URL has an invalid hostname or port.", ""];
|
|
212
|
-
}
|
|
213
|
-
} catch {
|
|
214
|
-
return [false, "Blocked: the URL has an invalid hostname or port.", ""];
|
|
216
|
+
if (parsed.port && !(PORT_RE.test(parsed.port) && Number(parsed.port) >= 1 && Number(parsed.port) <= 65535)) {
|
|
217
|
+
return [false, INVALID_HOST_REASON, ""];
|
|
215
218
|
}
|
|
216
219
|
let hostname: string;
|
|
217
220
|
try {
|
|
218
221
|
hostname = normalizeDomain(parsed.hostname);
|
|
219
222
|
} catch {
|
|
220
|
-
return [false,
|
|
223
|
+
return [false, INVALID_HOST_REASON, ""];
|
|
221
224
|
}
|
|
222
225
|
if (!policyAllows(hostname, policy)) {
|
|
223
226
|
return [false, `Blocked: the website access policy disallows ${hostname}.`, hostname];
|
|
@@ -323,7 +326,7 @@ const GITHUB_NON_OWNER_SEGMENTS = new Set([
|
|
|
323
326
|
|
|
324
327
|
const GITHUB_NAME_RE = /^[A-Za-z0-9_.\-]{1,100}$/;
|
|
325
328
|
|
|
326
|
-
function
|
|
329
|
+
function parseGithubRepo(url: string): { owner: string; repo: string; rest: string[] } | null {
|
|
327
330
|
let parsed: URL;
|
|
328
331
|
try {
|
|
329
332
|
parsed = new URL(url);
|
|
@@ -333,22 +336,40 @@ function githubRepoOwnerRepo(url: string): [string, string] | null {
|
|
|
333
336
|
const host = (parsed.hostname ?? "").toLowerCase();
|
|
334
337
|
if (host !== "github.com" && host !== "www.github.com") return null;
|
|
335
338
|
const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
|
|
336
|
-
if (parts.length
|
|
337
|
-
const
|
|
339
|
+
if (parts.length < 2) return null;
|
|
340
|
+
const owner = parts[0];
|
|
341
|
+
const repoRaw = parts[1];
|
|
338
342
|
if (GITHUB_NON_OWNER_SEGMENTS.has(owner.toLowerCase())) return null;
|
|
339
|
-
const
|
|
340
|
-
if (!GITHUB_NAME_RE.test(owner) || !GITHUB_NAME_RE.test(
|
|
341
|
-
return
|
|
343
|
+
const repo = repoRaw.endsWith(".git") ? repoRaw.slice(0, -4) : repoRaw;
|
|
344
|
+
if (!GITHUB_NAME_RE.test(owner) || !GITHUB_NAME_RE.test(repo)) return null;
|
|
345
|
+
return { owner, repo, rest: parts.slice(2) };
|
|
346
|
+
}
|
|
347
|
+
function githubRepoRoot(url: string): { owner: string; repo: string; rest: string[] } | null {
|
|
348
|
+
const parsed = parseGithubRepo(url);
|
|
349
|
+
if (!parsed || parsed.rest.length !== 0) return null;
|
|
350
|
+
return parsed;
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
export function githubRepoReadmeApiUrl(url: string): string | null {
|
|
345
|
-
const
|
|
346
|
-
|
|
354
|
+
const parsed = githubRepoRoot(url);
|
|
355
|
+
if (!parsed) return null;
|
|
356
|
+
return `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/readme`;
|
|
347
357
|
}
|
|
348
358
|
|
|
349
359
|
export function githubRepoRawReadmeUrl(url: string): string | null {
|
|
350
|
-
const
|
|
351
|
-
|
|
360
|
+
const parsed = githubRepoRoot(url);
|
|
361
|
+
if (!parsed) return null;
|
|
362
|
+
return `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/HEAD/README.md`;
|
|
363
|
+
}
|
|
364
|
+
export function githubRawContentUrl(url: string): string | null {
|
|
365
|
+
const parsed = parseGithubRepo(url);
|
|
366
|
+
if (!parsed || parsed.rest.length < 2) return null;
|
|
367
|
+
const [kind, ref] = parsed.rest;
|
|
368
|
+
if (kind !== "blob" && kind !== "raw") return null;
|
|
369
|
+
if (!ref) return null;
|
|
370
|
+
const filePath = parsed.rest.slice(2).join("/");
|
|
371
|
+
if (!filePath) return null;
|
|
372
|
+
return `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/${ref}/${filePath}`;
|
|
352
373
|
}
|
|
353
374
|
|
|
354
375
|
function ipv4Octets(ip: string): number[] | null {
|