@bacnh85/pi-web 0.10.1 → 0.10.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.2 (2026-09-13)
4
+
5
+ ### Fixed
6
+
7
+ - `.gif` downloads inline as `image/gif` (was mislabeled `image/png`).
8
+ - **SSRF guard on gateway-supplied image URLs**: downloads to
9
+ loopback/private/link-local hosts (e.g. cloud metadata 169.254.169.254)
10
+ are refused before any request is made — the URL is surfaced instead.
11
+ - **25 MB download cap**: oversized image downloads are not written to disk;
12
+ the URL is surfaced instead.
13
+ - pi-hub catalog/README description updated to match 0.10.x features.
14
+
3
15
  ## 0.10.1 (2026-09-13)
4
16
 
5
17
  ### Added
@@ -87,13 +87,15 @@ const engineSchema = {
87
87
 
88
88
  // Saved image file → inline image block (the 0.6.2 vision-loop lesson: the
89
89
  // generating model should see its own output).
90
- async function toImageBlock(file: string): Promise<{ type: "image"; data: string; mimeType: string }> {
90
+ export async function toImageBlock(file: string): Promise<{ type: "image"; data: string; mimeType: string }> {
91
91
  const data = (await fs.promises.readFile(file)).toString("base64");
92
92
  const lower = file.toLowerCase();
93
93
  const mimeType = lower.endsWith(".jpg") || lower.endsWith(".jpeg")
94
94
  ? "image/jpeg"
95
95
  : lower.endsWith(".webp")
96
96
  ? "image/webp"
97
+ : lower.endsWith(".gif")
98
+ ? "image/gif"
97
99
  : "image/png";
98
100
  return { type: "image" as const, data, mimeType };
99
101
  }
@@ -6,6 +6,7 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { findEnvValue } from "./config";
9
+ import { isLocalUrl } from "./chrome";
9
10
  import {
10
11
  describeGeminiError,
11
12
  geminiGenerateImage,
@@ -169,6 +170,9 @@ export interface ApiImageResult {
169
170
  model?: string;
170
171
  }
171
172
 
173
+ /** Hard cap on downloaded image size — gateways can point at arbitrary URLs. */
174
+ export const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
175
+
172
176
  export class ImageApiError extends Error {
173
177
  constructor(
174
178
  public status: number,
@@ -231,6 +235,7 @@ export async function apiGenerateImage(opts: {
231
235
  } else if (typeof item?.url === "string" && item.url) {
232
236
  // A failed download must not waste the generation: surface the URL.
233
237
  try {
238
+ if (isLocalUrl(item.url)) throw new Error("image host is private/loopback (SSRF-guarded)");
234
239
  paths.push(await downloadImage(fetchImpl, item.url, opts.outDir, i, opts.signal, opts.timeoutMs));
235
240
  } catch (err) {
236
241
  urls.push(item.url);
@@ -265,6 +270,7 @@ async function downloadImage(
265
270
  });
266
271
  if (!res.ok) throw new ImageApiError(res.status, `image download failed (HTTP ${res.status})`);
267
272
  const buf = Buffer.from(await res.arrayBuffer());
273
+ if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`image exceeds the ${MAX_DOWNLOAD_BYTES}-byte download cap`);
268
274
  const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFor(url)}`);
269
275
  fs.writeFileSync(file, buf);
270
276
  return file;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-web",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, free upstream image generation, and one-off gateway chat.",
5
5
  "type": "module",
6
6
  "license": "MIT",