@openlines/opl-pi-sht 0.1.18 → 0.1.20

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.
@@ -2,16 +2,45 @@ import { Readability } from "@mozilla/readability";
2
2
  import { parseHTML } from "linkedom";
3
3
  import TurndownService from "turndown";
4
4
  import { extractPdfBuffer } from "./pdf.js";
5
- import { errorMessage, isAbortError, isPdfUrl, isPdfContentType } from "./utils.js";
5
+ import { errorMessage, isAbortError, isPdfUrl, isPdfContentType, assertHttpUrl } from "./utils.js";
6
6
  import type { ExtractedContent } from "./types.js";
7
7
 
8
8
  const CONCURRENT_LIMIT = 3;
9
+ const MAX_FETCH_BYTES = 10 * 1024 * 1024; // 10 MB response cap
10
+ const FETCH_TIMEOUT_MS = 30_000;
9
11
 
10
12
  const td = new TurndownService({
11
13
  headingStyle: "atx",
12
14
  codeBlockStyle: "fenced",
13
15
  });
14
16
 
17
+ /** Read a response body into bytes, aborting past maxBytes (guards memory against huge payloads). */
18
+ async function readBufferCapped(response: Response, maxBytes: number): Promise<ArrayBuffer> {
19
+ const declared = Number(response.headers.get("content-length") ?? "0");
20
+ if (declared > maxBytes) throw new Error(`Response too large (${declared} bytes)`);
21
+ const reader = response.body?.getReader();
22
+ if (!reader) return await response.arrayBuffer();
23
+ const chunks: Uint8Array[] = [];
24
+ let total = 0;
25
+ for (;;) {
26
+ const { done, value } = await reader.read();
27
+ if (done) break;
28
+ total += value.byteLength;
29
+ if (total > maxBytes) {
30
+ await reader.cancel().catch(() => {});
31
+ throw new Error(`Response exceeds ${maxBytes} byte limit`);
32
+ }
33
+ chunks.push(value);
34
+ }
35
+ const merged = new Uint8Array(total);
36
+ let offset = 0;
37
+ for (const chunk of chunks) {
38
+ merged.set(chunk, offset);
39
+ offset += chunk.byteLength;
40
+ }
41
+ return merged.buffer;
42
+ }
43
+
15
44
  export async function fetchAllContent(
16
45
  urls: string[],
17
46
  signal?: AbortSignal
@@ -28,8 +57,12 @@ export async function fetchAllContent(
28
57
 
29
58
  async function fetchOne(url: string, signal?: AbortSignal): Promise<ExtractedContent> {
30
59
  try {
31
- const response = await fetch(url, {
32
- signal,
60
+ const target = assertHttpUrl(url);
61
+ const fetchSignal = signal
62
+ ? AbortSignal.any([signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
63
+ : AbortSignal.timeout(FETCH_TIMEOUT_MS);
64
+ const response = await fetch(target, {
65
+ signal: fetchSignal,
33
66
  headers: {
34
67
  "User-Agent": "Mozilla/5.0 (compatible; pi-web-access/1.0)",
35
68
  Accept: "text/html,application/xhtml+xml,application/pdf,*/*",
@@ -43,7 +76,7 @@ async function fetchOne(url: string, signal?: AbortSignal): Promise<ExtractedCon
43
76
  const contentType = response.headers.get("content-type") ?? "";
44
77
 
45
78
  if (isPdfUrl(url) || isPdfContentType(contentType)) {
46
- const buffer = await response.arrayBuffer();
79
+ const buffer = await readBufferCapped(response, MAX_FETCH_BYTES);
47
80
  const content = await extractPdfBuffer(buffer);
48
81
  return { url, title: url, content, error: null };
49
82
  }
@@ -51,7 +84,7 @@ async function fetchOne(url: string, signal?: AbortSignal): Promise<ExtractedCon
51
84
  const isHtml =
52
85
  contentType.includes("text/html") || contentType.includes("application/xhtml");
53
86
 
54
- const body = await response.text();
87
+ const body = new TextDecoder().decode(await readBufferCapped(response, MAX_FETCH_BYTES));
55
88
 
56
89
  if (!isHtml) {
57
90
  // Plain text, markdown, JSON, etc. — return as-is
@@ -27,9 +27,9 @@ export async function searchGemini(
27
27
 
28
28
  let response: Response;
29
29
  try {
30
- response = await fetch(`${base}/models/${model}:generateContent?key=${apiKey}`, {
30
+ response = await fetch(`${base}/models/${model}:generateContent`, {
31
31
  method: "POST",
32
- headers: { "Content-Type": "application/json" },
32
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
33
33
  body: JSON.stringify({
34
34
  contents: [{ role: "user", parts: [{ text: query }] }],
35
35
  tools: [{ google_search: {} }],
@@ -26,3 +26,17 @@ export function isPdfUrl(url: string): boolean {
26
26
  export function isPdfContentType(contentType: string): boolean {
27
27
  return contentType.toLowerCase().includes("application/pdf");
28
28
  }
29
+
30
+ /** Normalize a URL, allowing only http/https (file:/data:/etc. are rejected). */
31
+ export function assertHttpUrl(url: string): string {
32
+ let parsed: URL;
33
+ try {
34
+ parsed = new URL(url);
35
+ } catch {
36
+ throw new Error(`Invalid URL: ${url}`);
37
+ }
38
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
39
+ throw new Error(`Unsupported protocol "${parsed.protocol}//" — only http/https`);
40
+ }
41
+ return parsed.href;
42
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openlines/opl-pi-sht",
3
- "version": "0.1.18",
4
- "description": "Pi coding-agent extension kit: token-efficient context, safe modes, fingerprinted /init, model benchmarking, web access, and browser automation.",
3
+ "version": "0.1.20",
4
+ "description": "opl-pi-sht (shouldHaveThat) Pi coding-agent extension kit: token-efficient context, safe modes, fingerprinted /init, model benchmarking, web access, and browser automation.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/linellazatin/opl-pi-sht.git"