pi-oracle 0.1.2 → 0.1.4

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 CHANGED
@@ -73,13 +73,13 @@ Not promised yet:
73
73
 
74
74
  ## Install
75
75
 
76
- Planned npm install path once published:
76
+ npm:
77
77
 
78
78
  ```bash
79
79
  pi install npm:pi-oracle
80
80
  ```
81
81
 
82
- Current GitHub install path:
82
+ GitHub:
83
83
 
84
84
  ```bash
85
85
  pi install https://github.com/fitchmultz/pi-oracle
@@ -5,6 +5,7 @@ import { appendFile, chmod, lstat, mkdir, readFile, rename, rm, writeFile } from
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { getCookies } from "@steipete/sweet-cookie";
8
+ import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
8
9
 
9
10
  const rawConfig = process.argv[2];
10
11
  if (!rawConfig) {
@@ -367,42 +368,6 @@ function stripQuery(url) {
367
368
  }
368
369
  }
369
370
 
370
- function normalizeSameSite(value) {
371
- if (value === "Lax" || value === "Strict" || value === "None") return value;
372
- return undefined;
373
- }
374
-
375
- function normalizeExpiration(expires) {
376
- if (!expires || Number.isNaN(expires)) return undefined;
377
- const value = Number(expires);
378
- if (!Number.isFinite(value) || value <= 0) return undefined;
379
- // Chrome cookie readers can surface expiries in a few formats:
380
- // - Unix seconds (~1.7e9 in 2026)
381
- // - Unix milliseconds (~1.7e12)
382
- // - WebKit microseconds since 1601 (~1.3e16)
383
- if (value > 10_000_000_000_000) return Math.round(value / 1_000_000 - 11644473600);
384
- if (value > 10_000_000_000) return Math.round(value / 1000);
385
- return Math.round(value);
386
- }
387
-
388
- function normalizeCookie(cookie, fallbackHost) {
389
- if (!cookie?.name) return undefined;
390
- const domain = typeof cookie.domain === "string" && cookie.domain.trim() ? cookie.domain.trim() : fallbackHost;
391
- if (!domain) return undefined;
392
-
393
- const expires = normalizeExpiration(cookie.expires);
394
- return {
395
- name: cookie.name,
396
- value: cookie.value ?? "",
397
- domain,
398
- path: cookie.path || "/",
399
- expires,
400
- httpOnly: cookie.httpOnly ?? false,
401
- secure: cookie.secure ?? true,
402
- sameSite: normalizeSameSite(cookie.sameSite),
403
- };
404
- }
405
-
406
371
  function cookieOrigins() {
407
372
  return Array.from(new Set([stripQuery(config.browser.chatUrl), ...CHATGPT_COOKIE_ORIGINS]));
408
373
  }
@@ -432,23 +397,20 @@ async function readSourceCookies() {
432
397
  await log(`sweet-cookie warnings: ${warnings.join(" | ")}`);
433
398
  }
434
399
 
435
- const fallbackHost = new URL(config.browser.chatUrl).hostname;
436
- const merged = new Map();
437
- for (const cookie of cookies) {
438
- const normalized = normalizeCookie(cookie, fallbackHost);
439
- if (!normalized) continue;
440
- const key = `${normalized.domain}:${normalized.name}`;
441
- if (!merged.has(key)) merged.set(key, normalized);
442
- }
443
-
444
- const normalizedCookies = Array.from(merged.values());
400
+ const filtered = filterImportableAuthCookies(cookies, config.browser.chatUrl);
401
+ let normalizedCookies = filtered.cookies;
445
402
  await log(
446
- `Read ${normalizedCookies.length} merged cookies: ${normalizedCookies.map((cookie) => `${cookie.name}@${cookie.domain}`).join(", ")}`,
403
+ `Read ${normalizedCookies.length} filtered auth cookies: ${normalizedCookies.map((cookie) => `${cookie.name}@${cookie.domain}`).join(", ")}`,
447
404
  );
405
+ if (filtered.dropped.length) {
406
+ await log(
407
+ `Dropped ${filtered.dropped.length} non-importable cookies: ` +
408
+ filtered.dropped.map(({ cookie, reason }) => `${cookie.name}@${cookie.domain}(${reason})`).join(", "),
409
+ );
410
+ }
448
411
 
449
412
  const hasSessionToken = normalizedCookies.some((cookie) => cookie.name.startsWith("__Secure-next-auth.session-token"));
450
413
  const hasAccountCookie = normalizedCookies.some((cookie) => cookie.name === "_account");
451
- const fedrampCookie = normalizedCookies.find((cookie) => cookie.name === "_account_is_fedramp");
452
414
  await log(`Cookie presence: sessionToken=${hasSessionToken} account=${hasAccountCookie}`);
453
415
 
454
416
  if (!hasSessionToken) {
@@ -458,18 +420,11 @@ async function readSourceCookies() {
458
420
  }
459
421
 
460
422
  if (!hasAccountCookie) {
461
- const isFedramp = /^(1|true|yes)$/i.test(String(fedrampCookie?.value || ""));
462
- const fallbackAccountValue = isFedramp ? "fedramp" : "personal";
463
- normalizedCookies.push({
464
- name: "_account",
465
- value: fallbackAccountValue,
466
- domain: new URL(config.browser.chatUrl).hostname,
467
- path: "/",
468
- secure: true,
469
- httpOnly: false,
470
- sameSite: "Lax",
471
- });
472
- await log(`Synthesized missing _account cookie with value=${fallbackAccountValue}`);
423
+ const ensured = ensureAccountCookie(normalizedCookies, config.browser.chatUrl);
424
+ normalizedCookies = ensured.cookies;
425
+ if (ensured.synthesized) {
426
+ await log(`Synthesized missing _account cookie with value=${ensured.value}`);
427
+ }
473
428
  }
474
429
 
475
430
  return normalizedCookies;
@@ -658,6 +613,15 @@ function classifyChatPage({ url, snapshot, body, probe }) {
658
613
  };
659
614
  }
660
615
 
616
+ if (/http error 431|request header or cookie too large/i.test(text)) {
617
+ return {
618
+ state: "login_required",
619
+ message:
620
+ `Imported auth hit HTTP 431 during ChatGPT auth resolution, which usually means the imported cookie set is too large or stale. ` +
621
+ `Inspect ${LOG_PATH}.`,
622
+ };
623
+ }
624
+
661
625
  const outagePatterns = [
662
626
  /something went wrong/i,
663
627
  /a network error occurred/i,
@@ -0,0 +1,155 @@
1
+ const AUTH_COOKIE_NAME_PATTERNS = [
2
+ /^__Secure-next-auth\.session-token(?:\.|$)/,
3
+ /^__Secure-next-auth\.callback-url$/,
4
+ /^_account$/,
5
+ /^_account_is_fedramp$/,
6
+ /^_puid$/,
7
+ /^unified_session_manifest$/,
8
+ /^oai-(?:client-auth-info|client-auth-session|sc|did|hlib|asli|last-model-config|chat-web-route)$/,
9
+ /^auth-session-minimized(?:-client-checksum)?$/,
10
+ /^(?:login_session|auth_provider|hydra_redirect|iss_context|rg_context)$/,
11
+ /^cf_clearance$/,
12
+ ];
13
+
14
+ const DROPPED_COOKIE_NAME_PATTERNS = [
15
+ /^_ga(?:_|$)/,
16
+ /^_uet/,
17
+ /^_rdt_uuid$/,
18
+ /^(?:marketing|analytics)_consent$/,
19
+ /^__cf_bm$/,
20
+ /^__cflb$/,
21
+ /^_cfuvid$/,
22
+ /^_dd_s$/,
23
+ /^g_state$/,
24
+ /^country$/,
25
+ /^oai-nav-state$/,
26
+ /^oai-login-csrf/,
27
+ /^__Secure-next-auth\.state$/,
28
+ /^__Host-next-auth\.csrf-token$/,
29
+ ];
30
+
31
+ const BASE_ALLOWED_COOKIE_HOSTS = new Set([
32
+ 'chatgpt.com',
33
+ 'chat.openai.com',
34
+ 'openai.com',
35
+ 'auth.openai.com',
36
+ 'sentinel.openai.com',
37
+ 'atlas.openai.com',
38
+ 'ws.chatgpt.com',
39
+ ]);
40
+
41
+ function normalizeSameSite(value) {
42
+ if (value === 'Lax' || value === 'Strict' || value === 'None') return value;
43
+ return undefined;
44
+ }
45
+
46
+ function normalizeExpiration(expires) {
47
+ if (!expires || Number.isNaN(expires)) return undefined;
48
+ const value = Number(expires);
49
+ if (!Number.isFinite(value) || value <= 0) return undefined;
50
+ if (value > 10_000_000_000_000) return Math.round(value / 1_000_000 - 11644473600);
51
+ if (value > 10_000_000_000) return Math.round(value / 1000);
52
+ return Math.round(value);
53
+ }
54
+
55
+ function normalizeDomain(domain, fallbackHost) {
56
+ const raw = typeof domain === 'string' && domain.trim() ? domain.trim() : fallbackHost;
57
+ if (!raw) return undefined;
58
+ return raw.replace(/^\.+/, '').toLowerCase();
59
+ }
60
+
61
+ function allowedCookieHosts(chatUrl) {
62
+ const hosts = new Set(BASE_ALLOWED_COOKIE_HOSTS);
63
+ try {
64
+ hosts.add(new URL(chatUrl).hostname.toLowerCase());
65
+ } catch {
66
+ // ignore invalid URL here; caller validation happens elsewhere
67
+ }
68
+ return hosts;
69
+ }
70
+
71
+ function isAllowedCookieDomain(domain, chatUrl) {
72
+ const hosts = allowedCookieHosts(chatUrl);
73
+ return hosts.has(domain);
74
+ }
75
+
76
+ function matchesAny(patterns, value) {
77
+ return patterns.some((pattern) => pattern.test(value));
78
+ }
79
+
80
+ export function normalizeImportedCookie(cookie, fallbackHost) {
81
+ if (!cookie?.name) return undefined;
82
+ const domain = normalizeDomain(cookie.domain, fallbackHost);
83
+ if (!domain) return undefined;
84
+ return {
85
+ name: cookie.name,
86
+ value: cookie.value ?? '',
87
+ domain,
88
+ path: cookie.path || '/',
89
+ expires: normalizeExpiration(cookie.expires),
90
+ httpOnly: cookie.httpOnly ?? false,
91
+ secure: cookie.secure ?? true,
92
+ sameSite: normalizeSameSite(cookie.sameSite),
93
+ };
94
+ }
95
+
96
+ export function classifyImportedCookie(cookie, chatUrl) {
97
+ if (matchesAny(DROPPED_COOKIE_NAME_PATTERNS, cookie.name)) return 'noise';
98
+ if (!isAllowedCookieDomain(cookie.domain, chatUrl)) return 'foreign-domain';
99
+ if (!matchesAny(AUTH_COOKIE_NAME_PATTERNS, cookie.name)) return 'non-auth';
100
+ return 'keep';
101
+ }
102
+
103
+ export function filterImportableAuthCookies(cookies, chatUrl) {
104
+ const fallbackHost = (() => {
105
+ try {
106
+ return new URL(chatUrl).hostname;
107
+ } catch {
108
+ return 'chatgpt.com';
109
+ }
110
+ })();
111
+
112
+ const merged = new Map();
113
+ const dropped = [];
114
+ for (const cookie of cookies) {
115
+ const normalized = normalizeImportedCookie(cookie, fallbackHost);
116
+ if (!normalized) continue;
117
+ const disposition = classifyImportedCookie(normalized, chatUrl);
118
+ if (disposition !== 'keep') {
119
+ dropped.push({ cookie: normalized, reason: disposition });
120
+ continue;
121
+ }
122
+ const key = `${normalized.domain}:${normalized.name}`;
123
+ if (!merged.has(key)) merged.set(key, normalized);
124
+ }
125
+
126
+ return { cookies: Array.from(merged.values()), dropped };
127
+ }
128
+
129
+ export function ensureAccountCookie(cookies, chatUrl) {
130
+ const next = [...cookies];
131
+ const hasAccountCookie = next.some((cookie) => cookie.name === '_account');
132
+ if (hasAccountCookie) return { cookies: next, synthesized: false };
133
+
134
+ const fedrampCookie = next.find((cookie) => cookie.name === '_account_is_fedramp');
135
+ const isFedramp = /^(1|true|yes)$/i.test(String(fedrampCookie?.value || ''));
136
+ const fallbackAccountValue = isFedramp ? 'fedramp' : 'personal';
137
+ const domain = (() => {
138
+ try {
139
+ return new URL(chatUrl).hostname;
140
+ } catch {
141
+ return 'chatgpt.com';
142
+ }
143
+ })();
144
+
145
+ next.push({
146
+ name: '_account',
147
+ value: fallbackAccountValue,
148
+ domain,
149
+ path: '/',
150
+ secure: true,
151
+ httpOnly: false,
152
+ sameSite: 'Lax',
153
+ });
154
+ return { cookies: next, synthesized: true, value: fallbackAccountValue };
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "extensions": ["./extensions/oracle/index.ts"]
28
28
  },
29
29
  "scripts": {
30
- "check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
30
+ "check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
31
31
  "sanity:oracle": "tsx scripts/oracle-sanity.ts",
32
32
  "pack:check": "npm pack --dry-run"
33
33
  },