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
|
-
|
|
76
|
+
npm:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
pi install npm:pi-oracle
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
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
|
|
436
|
-
|
|
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}
|
|
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
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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.
|
|
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
|
},
|