@bacnh85/pi-web 0.10.3 → 0.10.5

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.5 (2026-09-13)
4
+
5
+ ### Added
6
+
7
+ - **`GEMINI_WEB_SECURE_1PSIDTS` env** — the rotating `__Secure-1PSIDTS`
8
+ session cookie, injected into the client jar pre-init. Google stopped
9
+ serving SNlM0e/cookie rotations to plain clients; without it the session
10
+ is partially authed (chat works, `accessToken` unresolved, sensitive
11
+ surfaces refuse). With it: full auth (`accessToken` resolves).
12
+
13
+ ## 0.10.4 (2026-09-13)
14
+
15
+ ### Fixed
16
+
17
+ - **Truthful image extensions**: saved files are typed by magic bytes, not
18
+ URL/file extension — Z.ai GLM-Image serves JPEG behind a `.png` URL, which
19
+ previously produced mislabeled files and inline blocks.
20
+
3
21
  ## 0.10.3 (2026-09-13)
4
22
 
5
23
  ### Fixed
package/README.md CHANGED
@@ -32,6 +32,7 @@ Variables:
32
32
  | `CRAWL4AI_API_TOKEN` | No (3) | — | Required if Crawl4AI auth enabled |
33
33
  | `GEMINI_WEB_SECURE_1PSID` | No (4) | — | `__Secure-1PSID` cookie from gemini.google.com — enables authed `web_research` (Deep Research) |
34
34
  | `GEMINI_WEB_PROXY` | No | — | Proxy URL for Gemini web calls (escape hatch if Google blocks the IP) |
35
+ | `GEMINI_WEB_SECURE_1PSIDTS` | No (6) | — | Rotating `__Secure-1PSIDTS` cookie — restores full auth when Google withholds SNlM0e from plain clients (recommended for image surfaces) |
35
36
  | `ZAI_API_KEY` | No (5) | — | Z.ai API key — enables the `web_image` `zai` provider (GLM-Image via the official `api.z.ai`); `Z_AI_API_KEY` also accepted |
36
37
  | `WEB_IMAGE_API_BASE_URL` | No | — | `web_image` `custom` provider: any OpenAI-compatible images endpoint (e.g. `https://api.openai.com/v1`) |
37
38
  | `WEB_IMAGE_API_KEY` | No | — | Bearer key for the `custom` endpoint |
@@ -45,6 +46,7 @@ Variables:
45
46
  > (2) Required for hosted Firecrawl; optional for self-hosted instances without auth.
46
47
  > (3) Required for Crawl4AI v0.9+ default config.
47
48
  > (4) Without it `web_research mode=ask` still works in guest mode (Flash-only); `mode=research` errors with setup steps.
49
+ > (6) Copy the current value from DevTools (Application → Cookies) alongside `__Secure-1PSID`; it rotates, so refresh it when auth degrades.
48
50
  > (5) `web_image` works with zero config via Gemini guest mode (availability varies by region/account); `zai` activates when `ZAI_API_KEY` is present, `custom` when `WEB_IMAGE_API_BASE_URL` is set.
49
51
 
50
52
  Secrets are never printed; `web_status` reports only presence/source.
@@ -14,13 +14,15 @@ import { findEnvValue } from "./config";
14
14
  export interface GeminiWebConfig {
15
15
  psid?: string;
16
16
  psidSource: string;
17
+ psidts?: string;
17
18
  proxy?: string;
18
19
  }
19
20
 
20
21
  export function loadGeminiWebConfig(cwd = process.cwd(), includeCwdEnv = false): GeminiWebConfig {
21
22
  const psid = findEnvValue("GEMINI_WEB_SECURE_1PSID", cwd, includeCwdEnv);
23
+ const psidts = findEnvValue("GEMINI_WEB_SECURE_1PSIDTS", cwd, includeCwdEnv);
22
24
  const proxy = findEnvValue("GEMINI_WEB_PROXY", cwd, includeCwdEnv);
23
- return { psid: psid.value, psidSource: psid.value ? psid.source : "not set", proxy: proxy.value };
25
+ return { psid: psid.value, psidSource: psid.value ? psid.source : "not set", psidts: psidts.value, proxy: proxy.value };
24
26
  }
25
27
 
26
28
  // ---------------------------------------------------------------------------
@@ -66,7 +68,7 @@ export interface GeminiClientLike {
66
68
  }
67
69
 
68
70
  export type GeminiClientFactory = (
69
- opts: { secure_1psid?: string; proxy?: string },
71
+ opts: { secure_1psid?: string; secure_1psidts?: string; proxy?: string },
70
72
  ) => GeminiClientLike | Promise<GeminiClientLike>;
71
73
 
72
74
  // Cached per config (psid|proxy) so a config change re-creates the client.
@@ -137,14 +139,24 @@ export async function loadDefaultFactory(): Promise<GeminiClientFactory> {
137
139
  }
138
140
  // ponytail: generous per-request cap (covers research); per-mode ask/research
139
141
  // timeouts are enforced by raceGuard below.
140
- return (opts) => new Gemini({ secure_1psid: opts.secure_1psid, proxy: opts.proxy ?? null, timeout: 1_800_000 });
142
+ // ponytail: Google's rotating __Secure-1PSIDTS is required for sensitive
143
+ // surfaces (image generation refuses with "You might be signed out" when it
144
+ // is missing); the page HTML no longer carries SNlM0e/rotations for plain
145
+ // clients, so the user passes it explicitly and we inject it pre-init.
146
+ return (opts) => {
147
+ const client = new Gemini({ secure_1psid: opts.secure_1psid, proxy: opts.proxy ?? null, timeout: 1_800_000 });
148
+ if (opts.secure_1psidts) {
149
+ (client as unknown as { cookies: Record<string, string> }).cookies["__Secure-1PSIDTS"] = opts.secure_1psidts;
150
+ }
151
+ return client;
152
+ };
141
153
  }
142
154
 
143
155
  async function getClient(config: GeminiWebConfig, factory?: GeminiClientFactory): Promise<GeminiClientLike> {
144
- const key = `${config.psid ?? ""}|${config.proxy ?? ""}`;
156
+ const key = `${config.psid ?? ""}|${config.psidts ?? ""}|${config.proxy ?? ""}`;
145
157
  if (cached?.key === key) return cached.client;
146
158
  const make = factory ?? (await loadDefaultFactory());
147
- const client = await make({ secure_1psid: config.psid, proxy: config.proxy });
159
+ const client = await make({ secure_1psid: config.psid, secure_1psidts: config.psidts, proxy: config.proxy });
148
160
  cached = { key, client };
149
161
  return client;
150
162
  }
@@ -238,7 +238,7 @@ export async function apiGenerateImage(opts: {
238
238
  for (let i = 0; i < items.length; i++) {
239
239
  const item = items[i];
240
240
  if (typeof item?.b64_json === "string" && item.b64_json) {
241
- paths.push(writeB64(opts.outDir, item.b64_json, i));
241
+ paths.push(writeB64(opts.outDir, Buffer.from(item.b64_json, "base64"), i));
242
242
  } else if (typeof item?.url === "string" && item.url) {
243
243
  // A failed download must not waste the generation: surface the URL.
244
244
  try {
@@ -256,9 +256,19 @@ export async function apiGenerateImage(opts: {
256
256
  return { paths, urls, model: typeof payload?.model === "string" ? payload.model : opts.model };
257
257
  }
258
258
 
259
- function writeB64(outDir: string, b64: string, i: number): string {
260
- const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}.png`);
261
- fs.writeFileSync(file, Buffer.from(b64, "base64"));
259
+ // Some gateways serve JPEG/WebP bytes behind a .png URL (Z.ai GLM-Image does)
260
+ // trust the magic bytes, not the URL/file extension.
261
+ function extFromBytes(buf: Buffer, fallback: string): string {
262
+ if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return ".png";
263
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return ".jpg";
264
+ if (buf.length >= 6 && buf.toString("ascii", 0, 3) === "GIF") return ".gif";
265
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") return ".webp";
266
+ return fallback;
267
+ }
268
+
269
+ function writeB64(outDir: string, buf: Buffer, i: number): string {
270
+ const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFromBytes(buf, ".png")}`);
271
+ fs.writeFileSync(file, buf);
262
272
  return file;
263
273
  }
264
274
 
@@ -290,7 +300,7 @@ async function downloadImage(
290
300
  if (!res.ok) throw new ImageApiError(res.status, `image download failed (HTTP ${res.status})`);
291
301
  const buf = Buffer.from(await res.arrayBuffer());
292
302
  if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`image exceeds the ${MAX_DOWNLOAD_BYTES}-byte download cap`);
293
- const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFor(url)}`);
303
+ const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFromBytes(buf, extFor(url))}`);
294
304
  fs.writeFileSync(file, buf);
295
305
  return file;
296
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-web",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
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",