@miruzero/zgpt 0.1.0 → 0.1.1
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/docs/architecture.md +29 -3
- package/package.json +1 -1
- package/src/auth/index.js +111 -84
- package/src/cli/conversations-tui.js +301 -0
- package/src/cli/formatter.js +39 -1
- package/src/cli.js +177 -113
- package/src/config/paths.js +3 -1
- package/src/connector/read.js +28 -4
- package/src/connector/write.js +7 -7
- package/src/conversation/context-config.js +68 -0
- package/src/conversation/index.js +11 -5
- package/src/conversation/read.js +10 -3
- package/src/conversation/recovery-state.js +51 -0
- package/src/conversation/recovery.js +85 -0
- package/src/conversation/temporary.js +3 -1
- package/src/conversation/write.js +118 -27
- package/src/file/index.js +151 -0
- package/src/index.js +5 -1
- package/src/memory/index.js +64 -0
- package/src/protocol/sse.js +2 -1
- package/src/runtime-context.js +27 -0
- package/src/session/direct-transport.js +68 -0
- package/src/session/exchange.js +9 -4
- package/src/session/index.js +100 -68
- package/src/session/local-storage.js +99 -68
- package/src/settings/index.js +18 -0
- package/src/share/index.js +25 -0
- package/src/transport/direct/http-client.js +189 -164
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const paths = require('../config/paths');
|
|
5
|
+
const direct = require('../transport/direct/http-client');
|
|
6
|
+
|
|
7
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
8
|
+
const mergeSetCookies = (cookieString, setCookie) => {
|
|
9
|
+
const jar = new Map();
|
|
10
|
+
const add = (entry) => {
|
|
11
|
+
const first = String(entry || '').split(';')[0];
|
|
12
|
+
const index = first.indexOf('=');
|
|
13
|
+
if (index > 0) jar.set(first.slice(0, index).trim(), first.slice(index + 1).trim());
|
|
14
|
+
};
|
|
15
|
+
for (const pair of String(cookieString || '').split(';')) add(pair);
|
|
16
|
+
for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) add(entry);
|
|
17
|
+
return [...jar].map(([name, value]) => `${name}=${value}`).join('; ');
|
|
18
|
+
};
|
|
19
|
+
const bootstrapHeaders = (env) => {
|
|
20
|
+
const state = readJson(paths.sessionFile(env)) || {};
|
|
21
|
+
const saved = state.headers && typeof state.headers === 'object' && !Array.isArray(state.headers) ? state.headers : {};
|
|
22
|
+
const { Cookie: _cookie, cookie: _cookieLower, authorization: _authorization, ...safe } = saved;
|
|
23
|
+
return {
|
|
24
|
+
...safe,
|
|
25
|
+
origin: safe.origin || 'https://chatgpt.com',
|
|
26
|
+
referer: safe.referer || 'https://chatgpt.com/',
|
|
27
|
+
accept: safe.accept || 'application/json'
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.createTransport = ({ requestJson = direct.requestJson } = {}) => async ({ sessionMarker, workspaceAccountId = null, env = process.env } = {}) => {
|
|
32
|
+
if (!sessionMarker || typeof sessionMarker !== 'string') {
|
|
33
|
+
throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
34
|
+
}
|
|
35
|
+
const sessionFile = paths.sessionFile(env);
|
|
36
|
+
const sessionHeaders = bootstrapHeaders(env);
|
|
37
|
+
const first = await requestJson('/api/auth/session', sessionMarker, sessionHeaders, {
|
|
38
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
39
|
+
});
|
|
40
|
+
if (![200, 403].includes(Number(first?.status))) {
|
|
41
|
+
throw Object.assign(new Error('ChatGPT session bootstrap round one failed'), { code: 'CHATGPT_SESSION_BOOTSTRAP_FAILED', status: first?.status || 0 });
|
|
42
|
+
}
|
|
43
|
+
const cookie = mergeSetCookies('', first?.headers?.['set-cookie']);
|
|
44
|
+
if (!cookie) {
|
|
45
|
+
throw Object.assign(new Error('ChatGPT session bootstrap did not return a cookie'), { code: 'CHATGPT_SESSION_BOOTSTRAP_COOKIE_MISSING', status: first?.status || 0 });
|
|
46
|
+
}
|
|
47
|
+
sessionHeaders.Cookie = cookie;
|
|
48
|
+
const switched = await requestJson('/account-switch/switch', null, sessionHeaders, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: workspaceAccountId ? { workspaceAccountId } : {},
|
|
51
|
+
route: '/account-switch/switch',
|
|
52
|
+
headers: { 'OAI-Account-Switch-Session-Token': sessionMarker },
|
|
53
|
+
env,
|
|
54
|
+
sessionFile,
|
|
55
|
+
persistSession: false
|
|
56
|
+
});
|
|
57
|
+
if (!switched || switched.status < 200 || switched.status >= 300) {
|
|
58
|
+
throw Object.assign(new Error('ChatGPT direct session exchange failed'), { code: 'CHATGPT_SESSION_EXCHANGE_FAILED', status: switched?.status || 0 });
|
|
59
|
+
}
|
|
60
|
+
const switchedCookie = mergeSetCookies(sessionHeaders.Cookie, switched?.headers?.['set-cookie']);
|
|
61
|
+
if (switchedCookie) sessionHeaders.Cookie = switchedCookie;
|
|
62
|
+
const second = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
63
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
64
|
+
});
|
|
65
|
+
return { status: Number(second?.status || 0), body: second?.json ?? null, sessionHeaders };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
exports.transport = exports.createTransport();
|
package/src/session/exchange.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
exports.SESSION_SOURCE = 'https://chatgpt.com/api/auth/session';
|
|
4
4
|
exports.normalizeResult = (result) => {
|
|
5
5
|
if (!result || typeof result !== 'object') throw Object.assign(new Error('ChatGPT session exchange returned an invalid result'), { code: 'CHATGPT_SESSION_EXCHANGE_INVALID_RESULT' });
|
|
6
|
-
return {
|
|
6
|
+
return {
|
|
7
|
+
status: Number(result.status || 0),
|
|
8
|
+
body: result.body ?? result.data ?? null,
|
|
9
|
+
source: exports.SESSION_SOURCE,
|
|
10
|
+
sessionHeaders: result.sessionHeaders && typeof result.sessionHeaders === 'object' ? { ...result.sessionHeaders } : null
|
|
11
|
+
};
|
|
7
12
|
};
|
|
8
|
-
exports.exchangeSession = async ({ sessionMarker, profile, env = process.env, transport } = {}) => {
|
|
13
|
+
exports.exchangeSession = async ({ sessionMarker, workspaceAccountId = null, profile, env = process.env, transport } = {}) => {
|
|
9
14
|
if (!sessionMarker || typeof sessionMarker !== 'string') throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
10
15
|
if (typeof transport !== 'function') throw Object.assign(new Error('ChatGPT session exchange transport is required'), { code: 'CHATGPT_SESSION_EXCHANGE_TRANSPORT_REQUIRED' });
|
|
11
|
-
return exports.normalizeResult(await transport({ sessionMarker, profile: profile || null, env, sessionSource: exports.SESSION_SOURCE }));
|
|
16
|
+
return exports.normalizeResult(await transport({ sessionMarker, workspaceAccountId, profile: profile || null, env, sessionSource: exports.SESSION_SOURCE }));
|
|
12
17
|
};
|
package/src/session/index.js
CHANGED
|
@@ -1,70 +1,102 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const paths = require('../config/paths');
|
|
6
|
-
const localStorage = require('./local-storage');
|
|
7
|
-
const exchange = require('./exchange');
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
const
|
|
37
|
-
return { present: true, status: '
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
exports.
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const paths = require('../config/paths');
|
|
6
|
+
const localStorage = require('./local-storage');
|
|
7
|
+
const exchange = require('./exchange');
|
|
8
|
+
const direct = require('../transport/direct/http-client');
|
|
9
|
+
const directTransport = require('./direct-transport');
|
|
10
|
+
|
|
11
|
+
const nowIso = () => new Date().toISOString();
|
|
12
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
13
|
+
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); };
|
|
14
|
+
|
|
15
|
+
exports.validate = (result) => {
|
|
16
|
+
const body = result?.body ?? result;
|
|
17
|
+
const status = result?.status ?? 200;
|
|
18
|
+
const authenticated = Boolean(body && typeof body === 'object' && !Array.isArray(body) && (body.user || body.account || body.accessToken || body.sessionToken));
|
|
19
|
+
if (status === 200 && authenticated) return { status, body, source: exchange.SESSION_SOURCE };
|
|
20
|
+
throw Object.assign(new Error('ChatGPT final session response is not authenticated'), { code: status === 200 ? 'CHATGPT_SESSION_NOT_AUTHENTICATED' : 'CHATGPT_SESSION_FETCH_FAILED', status });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
exports.resolveTransport = (env = process.env) => {
|
|
24
|
+
const modulePath = env.ZERO_CHATGPT_SESSION_TRANSPORT_MODULE;
|
|
25
|
+
if (!modulePath) return directTransport.transport;
|
|
26
|
+
const resolved = require.resolve(modulePath); delete require.cache[resolved];
|
|
27
|
+
const loaded = require(resolved);
|
|
28
|
+
const transport = typeof loaded === 'function' ? loaded : loaded.transport || loaded.exchangeSession;
|
|
29
|
+
if (typeof transport !== 'function') throw Object.assign(new Error('Configured ChatGPT session transport is invalid'), { code: 'CHATGPT_SESSION_TRANSPORT_INVALID' });
|
|
30
|
+
return transport;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.status = ({ env = process.env } = {}) => {
|
|
34
|
+
const file = paths.sessionFile(env);
|
|
35
|
+
if (!fs.existsSync(file)) return { present: false, status: 'missing', redacted: true, file };
|
|
36
|
+
const data = readJson(file);
|
|
37
|
+
if (!data) return { present: true, status: 'invalid', redacted: true, file };
|
|
38
|
+
const headerNames = data.headers && typeof data.headers === 'object' ? Object.keys(data.headers) : [];
|
|
39
|
+
return { present: true, status: 'present', redacted: true, source: data.source || null, expires: data.expires || null, updatedAt: data.updatedAt || null, headerNames, file };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
exports.acquire = async ({
|
|
43
|
+
env = process.env,
|
|
44
|
+
transport,
|
|
45
|
+
acquireMarker = localStorage.acquireLocalSessionMarker,
|
|
46
|
+
requestJson = direct.requestJson,
|
|
47
|
+
fresh = false,
|
|
48
|
+
persist = true
|
|
49
|
+
} = {}) => {
|
|
50
|
+
const file = paths.sessionFile(env);
|
|
51
|
+
const previous = readJson(file) || {};
|
|
52
|
+
let sessionHeaders = previous.headers && typeof previous.headers === 'object' && !Array.isArray(previous.headers)
|
|
53
|
+
? { ...previous.headers }
|
|
54
|
+
: null;
|
|
55
|
+
let profile = previous.profile || null;
|
|
56
|
+
let result = null;
|
|
57
|
+
|
|
58
|
+
if (sessionHeaders && !fresh) {
|
|
59
|
+
const fetched = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
60
|
+
route: '/api/auth/session', env, sessionFile: file
|
|
61
|
+
});
|
|
62
|
+
if (fetched?.status === 200) {
|
|
63
|
+
try { result = exports.validate({ status: fetched.status, body: fetched.json }); }
|
|
64
|
+
catch (error) { if (error?.code !== 'CHATGPT_SESSION_NOT_AUTHENTICATED') throw error; }
|
|
65
|
+
} else if (![401, 403].includes(Number(fetched?.status))) {
|
|
66
|
+
exports.validate({ status: fetched?.status || 0, body: fetched?.json });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!result) {
|
|
71
|
+
const local = await acquireMarker({ env });
|
|
72
|
+
profile = local.profile || null;
|
|
73
|
+
const selectedTransport = transport || exports.resolveTransport(env);
|
|
74
|
+
const exchanged = await exchange.exchangeSession({ sessionMarker: local.sessionMarker, workspaceAccountId: local.workspaceAccountId || null, profile, env, transport: selectedTransport });
|
|
75
|
+
result = exports.validate(exchanged);
|
|
76
|
+
if (exchanged.sessionHeaders && typeof exchanged.sessionHeaders === 'object') sessionHeaders = { ...exchanged.sessionHeaders };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (persist) {
|
|
80
|
+
const next = {
|
|
81
|
+
...(sessionHeaders ? { headers: sessionHeaders } : {}),
|
|
82
|
+
source: 'api-auth-session',
|
|
83
|
+
updatedAt: nowIso(),
|
|
84
|
+
expires: result.body?.expires || previous.expires || null,
|
|
85
|
+
profile
|
|
86
|
+
};
|
|
87
|
+
writeJson(file, next);
|
|
88
|
+
}
|
|
89
|
+
return { ...result, profile };
|
|
90
|
+
};exports.SESSION_STORAGE_KEY = localStorage.SESSION_STORAGE_KEY;
|
|
91
|
+
exports.SESSION_SOURCE = exchange.SESSION_SOURCE;
|
|
92
|
+
exports.resolveChromeDataRoot = (env = process.env) => localStorage.resolveChromeDataRoot(env);
|
|
93
|
+
exports.detectProfile = (chromeRoot, env = process.env) => localStorage.detectProfile(chromeRoot, env);
|
|
94
|
+
exports.copySnapshot = (source, destination) => localStorage.copySnapshot(source, destination);
|
|
95
|
+
exports.decodeStorageKey = (value) => localStorage.decodeStorageKey(value);
|
|
96
|
+
exports.decodeStorageValue = (value) => localStorage.decodeStorageValue(value);
|
|
97
|
+
exports.readLocalStorage = (databasePath) => localStorage.readLocalStorage(databasePath);
|
|
67
98
|
exports.extractSessionMarker = (storage) => localStorage.extractSessionMarker(storage);
|
|
68
|
-
exports.
|
|
69
|
-
exports.
|
|
99
|
+
exports.extractSessionSelection = (storage) => localStorage.extractSessionSelection(storage);
|
|
100
|
+
exports.acquireLocalSessionMarker = (input = {}) => localStorage.acquireLocalSessionMarker(input);
|
|
101
|
+
exports.normalizeResult = (result) => exchange.normalizeResult(result);
|
|
70
102
|
exports.exchangeSession = (input = {}) => exchange.exchangeSession(input);
|
|
@@ -1,75 +1,106 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const { ClassicLevel } = require('classic-level');
|
|
7
|
-
|
|
8
|
-
const DOMAIN = 'chatgpt.com';
|
|
9
|
-
const DEFAULT_PROFILE = 'Default';
|
|
10
|
-
exports.SESSION_STORAGE_KEY = 'oai/apps/accountSwitchSessions';
|
|
11
|
-
|
|
12
|
-
exports.resolveChromeDataRoot = (env = process.env) => {
|
|
13
|
-
const localAppData = env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
14
|
-
return path.join(localAppData, 'Google', 'Chrome', 'User Data');
|
|
15
|
-
};
|
|
16
|
-
exports.detectProfile = (chromeRoot, env = process.env) => {
|
|
17
|
-
if (env.ZERO_CHATGPT_CHROME_PROFILE) return env.ZERO_CHATGPT_CHROME_PROFILE;
|
|
18
|
-
try { return JSON.parse(fs.readFileSync(path.join(chromeRoot, 'Local State'), 'utf8')).profile?.last_used || DEFAULT_PROFILE; }
|
|
19
|
-
catch { return DEFAULT_PROFILE; }
|
|
20
|
-
};
|
|
21
|
-
const isSnapshotFile = (name) => name.endsWith('.ldb') || name.endsWith('.log') || name === 'CURRENT' || name.startsWith('MANIFEST-');
|
|
22
|
-
exports.copySnapshot = (source, destination) => {
|
|
23
|
-
if (!fs.existsSync(source)) throw Object.assign(new Error('ChatGPT local storage not found'), { code: 'CHATGPT_STORAGE_NOT_FOUND' });
|
|
24
|
-
fs.mkdirSync(destination, { recursive: true });
|
|
25
|
-
for (const name of fs.readdirSync(source)) {
|
|
26
|
-
if (!isSnapshotFile(name) || name.toLowerCase() === 'lock') continue;
|
|
27
|
-
try { fs.copyFileSync(path.join(source, name), path.join(destination, name)); } catch {}
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
exports.decodeStorageKey = (rawKey) => {
|
|
31
|
-
if (!rawKey?.length || rawKey[0] !== 0x5f) return null;
|
|
32
|
-
const full = rawKey.toString('utf8');
|
|
33
|
-
if (!full.toLowerCase().includes(DOMAIN)) return null;
|
|
34
|
-
const twoByte = rawKey.indexOf(Buffer.from([0x00, 0x01]));
|
|
35
|
-
if (twoByte !== -1) return rawKey.subarray(twoByte + 2).toString('utf8') || null;
|
|
36
|
-
const oneByte = rawKey.indexOf(0x00);
|
|
37
|
-
if (oneByte !== -1) return rawKey.subarray(oneByte + 1).toString('utf8') || null;
|
|
38
|
-
return full.replace(/^[^A-Za-z0-9_-]+/, '') || null;
|
|
39
|
-
};
|
|
40
|
-
exports.decodeStorageValue = (rawValue) => {
|
|
41
|
-
if (!rawValue?.length) return '';
|
|
42
|
-
const content = rawValue.subarray(1);
|
|
43
|
-
if (rawValue[0] === 0x00) return content.toString('utf16le');
|
|
44
|
-
return content.toString('utf8').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
|
45
|
-
};
|
|
46
|
-
exports.readLocalStorage = async (databasePath) => {
|
|
47
|
-
const storage = {};
|
|
48
|
-
const database = new ClassicLevel(databasePath, { keyEncoding: 'buffer', valueEncoding: 'buffer' });
|
|
49
|
-
await database.open();
|
|
50
|
-
try { for await (const [rawKey, rawValue] of database.iterator()) { const key = exports.decodeStorageKey(rawKey); if (key) storage[key] = exports.decodeStorageValue(rawValue); } }
|
|
51
|
-
finally { await database.close(); }
|
|
52
|
-
return storage;
|
|
53
|
-
};
|
|
54
|
-
exports.
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { ClassicLevel } = require('classic-level');
|
|
7
|
+
|
|
8
|
+
const DOMAIN = 'chatgpt.com';
|
|
9
|
+
const DEFAULT_PROFILE = 'Default';
|
|
10
|
+
exports.SESSION_STORAGE_KEY = 'oai/apps/accountSwitchSessions';
|
|
11
|
+
|
|
12
|
+
exports.resolveChromeDataRoot = (env = process.env) => {
|
|
13
|
+
const localAppData = env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
14
|
+
return path.join(localAppData, 'Google', 'Chrome', 'User Data');
|
|
15
|
+
};
|
|
16
|
+
exports.detectProfile = (chromeRoot, env = process.env) => {
|
|
17
|
+
if (env.ZERO_CHATGPT_CHROME_PROFILE) return env.ZERO_CHATGPT_CHROME_PROFILE;
|
|
18
|
+
try { return JSON.parse(fs.readFileSync(path.join(chromeRoot, 'Local State'), 'utf8')).profile?.last_used || DEFAULT_PROFILE; }
|
|
19
|
+
catch { return DEFAULT_PROFILE; }
|
|
20
|
+
};
|
|
21
|
+
const isSnapshotFile = (name) => name.endsWith('.ldb') || name.endsWith('.log') || name === 'CURRENT' || name.startsWith('MANIFEST-');
|
|
22
|
+
exports.copySnapshot = (source, destination) => {
|
|
23
|
+
if (!fs.existsSync(source)) throw Object.assign(new Error('ChatGPT local storage not found'), { code: 'CHATGPT_STORAGE_NOT_FOUND' });
|
|
24
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
25
|
+
for (const name of fs.readdirSync(source)) {
|
|
26
|
+
if (!isSnapshotFile(name) || name.toLowerCase() === 'lock') continue;
|
|
27
|
+
try { fs.copyFileSync(path.join(source, name), path.join(destination, name)); } catch {}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
exports.decodeStorageKey = (rawKey) => {
|
|
31
|
+
if (!rawKey?.length || rawKey[0] !== 0x5f) return null;
|
|
32
|
+
const full = rawKey.toString('utf8');
|
|
33
|
+
if (!full.toLowerCase().includes(DOMAIN)) return null;
|
|
34
|
+
const twoByte = rawKey.indexOf(Buffer.from([0x00, 0x01]));
|
|
35
|
+
if (twoByte !== -1) return rawKey.subarray(twoByte + 2).toString('utf8') || null;
|
|
36
|
+
const oneByte = rawKey.indexOf(0x00);
|
|
37
|
+
if (oneByte !== -1) return rawKey.subarray(oneByte + 1).toString('utf8') || null;
|
|
38
|
+
return full.replace(/^[^A-Za-z0-9_-]+/, '') || null;
|
|
39
|
+
};
|
|
40
|
+
exports.decodeStorageValue = (rawValue) => {
|
|
41
|
+
if (!rawValue?.length) return '';
|
|
42
|
+
const content = rawValue.subarray(1);
|
|
43
|
+
if (rawValue[0] === 0x00) return content.toString('utf16le');
|
|
44
|
+
return content.toString('utf8').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
|
45
|
+
};
|
|
46
|
+
exports.readLocalStorage = async (databasePath) => {
|
|
47
|
+
const storage = {};
|
|
48
|
+
const database = new ClassicLevel(databasePath, { keyEncoding: 'buffer', valueEncoding: 'buffer' });
|
|
49
|
+
await database.open();
|
|
50
|
+
try { for await (const [rawKey, rawValue] of database.iterator()) { const key = exports.decodeStorageKey(rawKey); if (key) storage[key] = exports.decodeStorageValue(rawValue); } }
|
|
51
|
+
finally { await database.close(); }
|
|
52
|
+
return storage;
|
|
53
|
+
};
|
|
54
|
+
exports.extractSessionSelection = (storage = {}) => {
|
|
55
55
|
const raw = storage[exports.SESSION_STORAGE_KEY];
|
|
56
56
|
if (!raw) return null;
|
|
57
57
|
let sessions;
|
|
58
58
|
try { sessions = JSON.parse(raw); } catch { return null; }
|
|
59
59
|
if (!Array.isArray(sessions)) return null;
|
|
60
|
-
|
|
60
|
+
const session = sessions.find((item) => typeof item?.sessionToken === 'string' && item.sessionToken.length > 0);
|
|
61
|
+
if (!session) return null;
|
|
62
|
+
const workspaces = Array.isArray(session.workspaces) ? session.workspaces : [];
|
|
63
|
+
const personal = workspaces.find((workspace) =>
|
|
64
|
+
workspace?.isPersonalWorkspace === true &&
|
|
65
|
+
workspace?.isDeactivated !== true &&
|
|
66
|
+
typeof workspace?.workspaceAccountId === 'string' &&
|
|
67
|
+
workspace.workspaceAccountId.length > 0
|
|
68
|
+
);
|
|
69
|
+
return { sessionMarker: session.sessionToken, workspaceAccountId: personal?.workspaceAccountId || null };
|
|
61
70
|
};
|
|
62
|
-
exports.
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-chatgpt-'));
|
|
67
|
-
const snapshot = path.join(tempRoot, 'leveldb');
|
|
68
|
-
try {
|
|
69
|
-
exports.copySnapshot(source, snapshot);
|
|
70
|
-
const storage = await exports.readLocalStorage(snapshot);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
exports.extractSessionMarker = (storage = {}) => exports.extractSessionSelection(storage)?.sessionMarker || null;
|
|
72
|
+
const readMarkerFromProfile = async (chromeRoot, profile) => {
|
|
73
|
+
const source = path.join(chromeRoot, profile, 'Local Storage', 'leveldb');
|
|
74
|
+
if (!fs.existsSync(source)) return null;
|
|
75
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-chatgpt-'));
|
|
76
|
+
const snapshot = path.join(tempRoot, 'leveldb');
|
|
77
|
+
try {
|
|
78
|
+
exports.copySnapshot(source, snapshot);
|
|
79
|
+
const storage = await exports.readLocalStorage(snapshot);
|
|
80
|
+
const selection = exports.extractSessionSelection(storage);
|
|
81
|
+
return selection ? { ...selection, profile } : null;
|
|
82
|
+
} finally { fs.rmSync(tempRoot, { recursive: true, force: true }); }
|
|
83
|
+
};
|
|
84
|
+
exports.acquireLocalSessionMarker = async ({ env = process.env } = {}) => {
|
|
85
|
+
const chromeRoot = exports.resolveChromeDataRoot(env);
|
|
86
|
+
const preferred = exports.detectProfile(chromeRoot, env);
|
|
87
|
+
const preferredResult = await readMarkerFromProfile(chromeRoot, preferred);
|
|
88
|
+
if (preferredResult) return preferredResult;
|
|
89
|
+
if (env.ZERO_CHATGPT_CHROME_PROFILE) {
|
|
90
|
+
throw Object.assign(new Error(`No ChatGPT local session marker was found in profile ${preferred}`), { code: 'CHATGPT_LOCAL_SESSION_NOT_FOUND', profile: preferred });
|
|
91
|
+
}
|
|
92
|
+
const candidates = [];
|
|
93
|
+
for (const entry of fs.readdirSync(chromeRoot, { withFileTypes: true })) {
|
|
94
|
+
if (!entry.isDirectory() || entry.name === preferred) continue;
|
|
95
|
+
const result = await readMarkerFromProfile(chromeRoot, entry.name);
|
|
96
|
+
if (result) candidates.push(result);
|
|
97
|
+
}
|
|
98
|
+
if (candidates.length === 1) return candidates[0];
|
|
99
|
+
if (candidates.length > 1) {
|
|
100
|
+
throw Object.assign(new Error('Multiple Chrome profiles contain ChatGPT local session markers'), {
|
|
101
|
+
code: 'CHATGPT_LOCAL_SESSION_AMBIGUOUS',
|
|
102
|
+
profiles: candidates.map((item) => item.profile)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
throw Object.assign(new Error('No ChatGPT local session marker was found'), { code: 'CHATGPT_LOCAL_SESSION_NOT_FOUND' });
|
|
75
106
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const direct = require('../transport/direct/http-client');
|
|
3
|
+
const context = require('../runtime-context');
|
|
4
|
+
const requireOk = (response, target) => {
|
|
5
|
+
if (!response || response.status < 200 || response.status >= 300) throw Object.assign(new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`), { status: response?.status || null, target });
|
|
6
|
+
return response.json || { success: true };
|
|
7
|
+
};
|
|
8
|
+
exports.getUser = async (options = {}) => {
|
|
9
|
+
options = await context.bind(options);
|
|
10
|
+
const target = '/backend-api/settings/user';
|
|
11
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { route: target }), target);
|
|
12
|
+
};
|
|
13
|
+
exports.updateAccountUserSetting = async (options = {}) => {
|
|
14
|
+
options = await context.bind(options);
|
|
15
|
+
if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body)) throw Object.assign(new Error('body is required'), { code: 'BAD_ARGS' });
|
|
16
|
+
const target = '/backend-api/settings/account_user_setting';
|
|
17
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'PATCH', retry: false, route: target, body: options.body }), target);
|
|
18
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const direct = require('../transport/direct/http-client');
|
|
3
|
+
const context = require('../runtime-context');
|
|
4
|
+
const requireOk = (response, target) => {
|
|
5
|
+
if (!response || response.status < 200 || response.status >= 300) throw Object.assign(new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`), { status: response?.status || null, target });
|
|
6
|
+
return response.json || { success: true };
|
|
7
|
+
};
|
|
8
|
+
const id = (value) => { if (typeof value !== 'string' || !value) throw Object.assign(new Error('shareId is required'), { code: 'BAD_ARGS' }); return encodeURIComponent(value); };
|
|
9
|
+
exports.create = async (options = {}) => {
|
|
10
|
+
options = await context.bind(options);
|
|
11
|
+
if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body)) throw Object.assign(new Error('body is required'), { code: 'BAD_ARGS' });
|
|
12
|
+
const target = '/backend-api/share/create';
|
|
13
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'POST', retry: false, route: target, body: options.body }), target);
|
|
14
|
+
};
|
|
15
|
+
exports.get = async (options = {}) => {
|
|
16
|
+
options = await context.bind(options);
|
|
17
|
+
const target = `/backend-api/share/${id(options.shareId)}`;
|
|
18
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { route: '/backend-api/share/{id}' }), target);
|
|
19
|
+
};
|
|
20
|
+
exports.update = async (options = {}) => {
|
|
21
|
+
options = await context.bind(options);
|
|
22
|
+
if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body)) throw Object.assign(new Error('body is required'), { code: 'BAD_ARGS' });
|
|
23
|
+
const target = `/backend-api/share/${id(options.shareId)}`;
|
|
24
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'PATCH', retry: false, route: '/backend-api/share/{id}', body: options.body }), target);
|
|
25
|
+
};
|