@miruzero/zero-agent 0.3.6 → 0.3.7
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 +31 -22
- package/package.json +6 -3
- package/src/agent.js +193 -167
- package/src/args.js +1 -3
- package/src/autostart.js +44 -68
- package/src/capabilities.js +95 -138
- package/src/chatgpt/auth/index.js +114 -0
- package/src/chatgpt/config/paths.js +25 -0
- package/src/chatgpt/index.js +5 -0
- package/src/chatgpt/session/direct-transport.js +68 -0
- package/src/chatgpt/session/exchange.js +17 -0
- package/src/chatgpt/session/index.js +102 -0
- package/src/chatgpt/session/local-storage.js +106 -0
- package/src/chatgpt/transport/direct/http-client.js +191 -0
- package/src/chatgpt-auth.js +17 -111
- package/src/chatgpt-runtime.js +8 -0
- package/src/chatgpt-session-exchange.js +8 -0
- package/src/chatgpt-session.js +26 -0
- package/src/cli.js +108 -71
- package/src/command/exec.js +34 -0
- package/src/command/filesystem/append-file.js +7 -12
- package/src/command/filesystem/copy-directory.js +8 -12
- package/src/command/filesystem/copy-file.js +8 -13
- package/src/command/filesystem/create-directory.js +7 -11
- package/src/command/filesystem/create-file.js +8 -13
- package/src/command/filesystem/delete-directory.js +7 -11
- package/src/command/filesystem/delete-file.js +8 -12
- package/src/command/filesystem/exists.js +6 -10
- package/src/command/filesystem/get-file-info.js +7 -12
- package/src/command/filesystem/glob-files.js +8 -13
- package/src/command/filesystem/grep-files.js +9 -15
- package/src/command/filesystem/hash-file.js +6 -11
- package/src/command/filesystem/index.js +33 -34
- package/src/command/filesystem/list-directory.js +6 -10
- package/src/command/filesystem/move-directory.js +9 -14
- package/src/command/filesystem/move-file.js +8 -13
- package/src/command/filesystem/path-policy.js +33 -16
- package/src/command/filesystem/read-file.js +7 -11
- package/src/command/filesystem/read-files.js +5 -8
- package/src/command/filesystem/rename-file.js +5 -8
- package/src/command/filesystem/replace-file.js +7 -12
- package/src/command/filesystem/text.js +5 -7
- package/src/command/filesystem/touch-file.js +8 -13
- package/src/command/filesystem/truncate-file.js +6 -10
- package/src/command/filesystem/walk.js +36 -39
- package/src/command/filesystem/write-file.js +8 -13
- package/src/command/index.js +19 -24
- package/src/command/path/index.js +46 -53
- package/src/config.js +21 -33
- package/src/device-paths.js +53 -0
- package/src/enrollment-client.js +19 -0
- package/src/http.js +27 -2
- package/src/identity-state.js +27 -0
- package/src/index.js +2 -4
- package/src/instance.js +85 -0
- package/src/local-server.js +174 -0
- package/src/local-tools.js +205 -205
- package/src/shared-zero/catalog.js +33 -0
- package/src/shared-zero/contract.js +18 -0
- package/src/shared-zero/descriptor.js +25 -0
- package/src/shared-zero/errors.js +12 -0
- package/src/shared-zero/index.js +9 -0
- package/src/shared-zero/runtime.js +14 -0
- package/src/shared-zero/target.js +19 -0
- package/src/shared-zero/tool-id.js +26 -0
- package/src/storage-roots.js +21 -0
- package/src/zero-runtime.js +1 -11
- package/src/zero.js +78 -0
|
@@ -0,0 +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
|
+
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);
|
|
98
|
+
exports.extractSessionMarker = (storage) => localStorage.extractSessionMarker(storage);
|
|
99
|
+
exports.extractSessionSelection = (storage) => localStorage.extractSessionSelection(storage);
|
|
100
|
+
exports.acquireLocalSessionMarker = (input = {}) => localStorage.acquireLocalSessionMarker(input);
|
|
101
|
+
exports.normalizeResult = (result) => exchange.normalizeResult(result);
|
|
102
|
+
exports.exchangeSession = (input = {}) => exchange.exchangeSession(input);
|
|
@@ -0,0 +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.extractSessionSelection = (storage = {}) => {
|
|
55
|
+
const raw = storage[exports.SESSION_STORAGE_KEY];
|
|
56
|
+
if (!raw) return null;
|
|
57
|
+
let sessions;
|
|
58
|
+
try { sessions = JSON.parse(raw); } catch { return null; }
|
|
59
|
+
if (!Array.isArray(sessions)) return null;
|
|
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 };
|
|
70
|
+
};
|
|
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' });
|
|
106
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const paths = require('../../config/paths');
|
|
7
|
+
|
|
8
|
+
const RETRY_AFTER_CAP_MS = 30000;
|
|
9
|
+
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 30000;
|
|
10
|
+
const MAX_RATE_LIMIT_COOLDOWN_MS = 15 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const cleanHeaders = (input = {}) => Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null));
|
|
13
|
+
const mergeSetCookies = (cookieString, setCookie) => {
|
|
14
|
+
const jar = new Map();
|
|
15
|
+
for (const pair of String(cookieString || '').split(';')) {
|
|
16
|
+
const index = pair.indexOf('=');
|
|
17
|
+
if (index > 0) jar.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim());
|
|
18
|
+
}
|
|
19
|
+
for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) {
|
|
20
|
+
const first = String(entry || '').split(';')[0];
|
|
21
|
+
const index = first.indexOf('=');
|
|
22
|
+
if (index > 0) jar.set(first.slice(0, index).trim(), first.slice(index + 1).trim());
|
|
23
|
+
}
|
|
24
|
+
return [...jar].map(([name, value]) => `${name}=${value}`).join('; ');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const rateLimitFileOf = (token, options = {}) => {
|
|
28
|
+
if (options.rateLimitFile === false) return null;
|
|
29
|
+
if (typeof options.rateLimitFile === 'string' && options.rateLimitFile.trim()) return path.resolve(options.rateLimitFile);
|
|
30
|
+
if (options.rateLimitFile === undefined && process.env.NODE_TEST_CONTEXT) return null;
|
|
31
|
+
return paths.rateLimitFile(options.env || process.env, token);
|
|
32
|
+
};
|
|
33
|
+
const readRateLimitState = (file) => {
|
|
34
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
35
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
36
|
+
};
|
|
37
|
+
const writeRateLimitState = (file, state) => {
|
|
38
|
+
if (!file) return;
|
|
39
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
41
|
+
fs.writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
42
|
+
fs.renameSync(temp, file);
|
|
43
|
+
};
|
|
44
|
+
const cleanupExpiredRateLimitStates = (options = {}) => {
|
|
45
|
+
if (options.rateLimitFile === false) return;
|
|
46
|
+
const env = options.env || process.env;
|
|
47
|
+
const files = [];
|
|
48
|
+
if (typeof options.rateLimitFile === 'string' && options.rateLimitFile.trim()) {
|
|
49
|
+
files.push(path.resolve(options.rateLimitFile));
|
|
50
|
+
} else {
|
|
51
|
+
const dir = paths.rateLimitDir(env);
|
|
52
|
+
try {
|
|
53
|
+
for (const name of fs.readdirSync(dir)) {
|
|
54
|
+
if (/^rate-limit-[a-f0-9]{16}\.json$/i.test(name)) files.push(path.join(dir, name));
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const retryAt = Date.parse(readRateLimitState(file)?.retry_not_before || '');
|
|
61
|
+
if (!Number.isFinite(retryAt) || retryAt > now) continue;
|
|
62
|
+
try { fs.rmSync(file, { force: true }); } catch {}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const localRateLimitResponse = (token, options, parseBody) => {
|
|
67
|
+
const file = rateLimitFileOf(token, options);
|
|
68
|
+
const state = readRateLimitState(file);
|
|
69
|
+
const retryAt = Date.parse(state?.retry_not_before || '');
|
|
70
|
+
if (!Number.isFinite(retryAt) || retryAt <= Date.now()) return null;
|
|
71
|
+
const retryAfterMs = retryAt - Date.now();
|
|
72
|
+
return {
|
|
73
|
+
status: 429,
|
|
74
|
+
...(parseBody ? { json: { detail: 'Too many requests' } } : { text: '{"detail":"Too many requests"}' }),
|
|
75
|
+
contentType: 'application/json',
|
|
76
|
+
headers: { 'retry-after': String(Math.max(1, Math.ceil(retryAfterMs / 1000))) },
|
|
77
|
+
localRateLimit: true
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const recordRateLimitState = (response, token, options = {}) => {
|
|
81
|
+
const file = rateLimitFileOf(token, options);
|
|
82
|
+
if (!file || !response) return;
|
|
83
|
+
if (response.status === 429) {
|
|
84
|
+
const previous = readRateLimitState(file);
|
|
85
|
+
const consecutive = previous?.status === 429 ? Number(previous.consecutive_429 || 0) + 1 : 1;
|
|
86
|
+
const exponentialMs = Math.min(DEFAULT_RATE_LIMIT_COOLDOWN_MS * (2 ** Math.min(consecutive - 1, 8)), MAX_RATE_LIMIT_COOLDOWN_MS);
|
|
87
|
+
const serverMs = exports.parseRetryAfterMs(response.headers || {}) || 0;
|
|
88
|
+
const cooldownMs = Math.max(exponentialMs, serverMs);
|
|
89
|
+
writeRateLimitState(file, {
|
|
90
|
+
status: 429,
|
|
91
|
+
consecutive_429: consecutive,
|
|
92
|
+
observed_at: new Date().toISOString(),
|
|
93
|
+
retry_after_ms: cooldownMs,
|
|
94
|
+
retry_not_before: new Date(Date.now() + cooldownMs).toISOString()
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (response.status >= 200 && response.status < 400) {
|
|
99
|
+
const state = readRateLimitState(file);
|
|
100
|
+
const retryAt = Date.parse(state?.retry_not_before || '');
|
|
101
|
+
if (Number.isFinite(retryAt) && retryAt <= Date.now()) {
|
|
102
|
+
try { fs.rmSync(file, { force: true }); } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
exports.persistResponseHeaders = (sessionHeaders = {}, responseHeaders = {}, options = {}) => {
|
|
108
|
+
const setCookie = responseHeaders['set-cookie'];
|
|
109
|
+
if (!setCookie) return { changed: false, persisted: false };
|
|
110
|
+
const currentCookie = sessionHeaders.Cookie || sessionHeaders.cookie || '';
|
|
111
|
+
const cookie = mergeSetCookies(currentCookie, setCookie);
|
|
112
|
+
const changed = cookie !== currentCookie;
|
|
113
|
+
if (!changed) return { changed: false, persisted: false };
|
|
114
|
+
sessionHeaders.Cookie = cookie;
|
|
115
|
+
delete sessionHeaders.cookie;
|
|
116
|
+
if (options.persistSession === false) return { changed: true, persisted: false };
|
|
117
|
+
const sessionFile = options.sessionFile || paths.sessionFile(options.env || process.env);
|
|
118
|
+
if (!fs.existsSync(sessionFile)) return { changed: true, persisted: false };
|
|
119
|
+
const data = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
|
120
|
+
if (data.headers && typeof data.headers === 'object') {
|
|
121
|
+
data.headers.Cookie = cookie;
|
|
122
|
+
delete data.headers.cookie;
|
|
123
|
+
} else data.cookie = cookie;
|
|
124
|
+
data.updatedAt = new Date().toISOString();
|
|
125
|
+
fs.writeFileSync(sessionFile, `${JSON.stringify(data, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
126
|
+
return { changed: true, persisted: true };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
exports.buildHeaders = (token, sessionHeaders = {}, target, targetRoute) => {
|
|
130
|
+
const targetPath = target.split('?')[0];
|
|
131
|
+
return cleanHeaders({ ...sessionHeaders, ...(token ? { authorization: `Bearer ${token}` } : {}), 'x-openai-target-path': targetPath, 'x-openai-target-route': targetRoute || targetPath });
|
|
132
|
+
};
|
|
133
|
+
exports.parseRetryAfterMs = (headers = {}) => {
|
|
134
|
+
const raw = headers['retry-after'];
|
|
135
|
+
if (!raw) return null;
|
|
136
|
+
const seconds = Number(raw);
|
|
137
|
+
if (Number.isFinite(seconds)) return Math.min(Math.max(seconds, 0) * 1000, RETRY_AFTER_CAP_MS);
|
|
138
|
+
const at = Date.parse(raw);
|
|
139
|
+
if (Number.isFinite(at)) return Math.min(Math.max(at - Date.now(), 0), RETRY_AFTER_CAP_MS);
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
const safeParse = (text) => { try { return JSON.parse(text); } catch { return null; } };
|
|
143
|
+
const rawRequest = (target, token, sessionHeaders, options, parseBody) => new Promise((resolve, reject) => {
|
|
144
|
+
const payload = options.body === undefined ? null : Buffer.from(JSON.stringify(options.body));
|
|
145
|
+
let headers = exports.buildHeaders(token, sessionHeaders, target, options.route);
|
|
146
|
+
if (options.headers) headers = cleanHeaders({ ...headers, ...options.headers });
|
|
147
|
+
if (payload) { headers['content-type'] = 'application/json'; headers['content-length'] = String(payload.length); }
|
|
148
|
+
const req = https.request(`https://chatgpt.com${target}`, { method: options.method || 'GET', headers }, (res) => {
|
|
149
|
+
const chunks = [];
|
|
150
|
+
let ended = false;
|
|
151
|
+
const aborted = () => {
|
|
152
|
+
const error = new Error(`ChatGPT upstream stream aborted: ${target}`);
|
|
153
|
+
error.code = 'UPSTREAM_ABORTED';
|
|
154
|
+
reject(error);
|
|
155
|
+
};
|
|
156
|
+
res.on('data', (chunk) => { options.onChunk?.(chunk); chunks.push(chunk); });
|
|
157
|
+
res.on('end', () => {
|
|
158
|
+
ended = true;
|
|
159
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
160
|
+
const response = { status: res.statusCode, ...(parseBody ? { json: safeParse(text) } : { text }), contentType: res.headers['content-type'] || null, headers: { ...res.headers } };
|
|
161
|
+
exports.persistResponseHeaders(sessionHeaders, response.headers, options);
|
|
162
|
+
resolve(response);
|
|
163
|
+
});
|
|
164
|
+
res.on('aborted', aborted);
|
|
165
|
+
res.on('error', reject);
|
|
166
|
+
res.on('close', () => { if (!ended) aborted(); });
|
|
167
|
+
});
|
|
168
|
+
req.on('error', reject);
|
|
169
|
+
if (Number.isFinite(options.idleTimeoutMs) && options.idleTimeoutMs > 0 && typeof req.setTimeout === 'function') {
|
|
170
|
+
req.setTimeout(options.idleTimeoutMs, () => {
|
|
171
|
+
const error = new Error(`ChatGPT upstream stream idle timeout: ${target}`);
|
|
172
|
+
error.code = 'UPSTREAM_IDLE_TIMEOUT';
|
|
173
|
+
req.destroy(error);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (payload) req.write(payload);
|
|
177
|
+
req.end();
|
|
178
|
+
});
|
|
179
|
+
const guardedRequest = async (target, token, sessionHeaders, options, parseBody) => {
|
|
180
|
+
cleanupExpiredRateLimitStates(options);
|
|
181
|
+
const limited = localRateLimitResponse(token, options, parseBody);
|
|
182
|
+
if (limited) return limited;
|
|
183
|
+
const response = await rawRequest(target, token, sessionHeaders, options, parseBody);
|
|
184
|
+
recordRateLimitState(response, token, options);
|
|
185
|
+
return response;
|
|
186
|
+
};
|
|
187
|
+
exports.requestJson = (target, token, sessionHeaders = {}, options = {}) =>
|
|
188
|
+
guardedRequest(target, token, sessionHeaders, options, true);
|
|
189
|
+
exports.requestText = (target, token, sessionHeaders = {}, options = {}) =>
|
|
190
|
+
guardedRequest(target, token, sessionHeaders, options, false);
|
|
191
|
+
|
package/src/chatgpt-auth.js
CHANGED
|
@@ -1,121 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
3
|
+
const runtime = require('./chatgpt-runtime');
|
|
7
4
|
|
|
8
5
|
const provider = 'chatgpt';
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
exports.resolveAuthFile = (env = process.env) => runtime.load(env).auth.file(env);
|
|
8
|
+
exports.defaultAuthFile = exports.resolveAuthFile;
|
|
9
|
+
exports.defaultMcpHome = (env = process.env) => runtime.load(env).paths.stateDir(env);
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const parseMaybeJson = (text) => {
|
|
18
|
-
try {
|
|
19
|
-
return { ok: true, value: JSON.parse(text) };
|
|
20
|
-
} catch (error) {
|
|
21
|
-
return { ok: false, error: error.message || String(error) };
|
|
22
|
-
}
|
|
11
|
+
exports.status = ({ env = process.env } = {}) => {
|
|
12
|
+
const auth = runtime.load(env).auth.status({ env });
|
|
13
|
+
return { ok: true, provider, auth, file: auth.file || exports.resolveAuthFile(env) };
|
|
23
14
|
};
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
exports.ensure = async ({ env = process.env } = {}) => runtime.load(env).auth.ensure({ env });
|
|
17
|
+
exports.login = async ({ env = process.env } = {}) => runtime.load(env).auth.login({ env });
|
|
18
|
+
|
|
19
|
+
exports.importAuth = ({ input, env = process.env, source = 'zero-agent' } = {}) => {
|
|
26
20
|
const text = String(input || '').trim();
|
|
27
|
-
if (!text) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return
|
|
21
|
+
if (!text) throw Object.assign(new Error('input is required'), { code: 'BAD_AUTH_INPUT' });
|
|
22
|
+
let body;
|
|
23
|
+
try { body = JSON.parse(text); }
|
|
24
|
+
catch { throw Object.assign(new Error('ChatGPT auth input must be JSON session data'), { code: 'BAD_AUTH_INPUT' }); }
|
|
25
|
+
const auth = runtime.load(env).auth.importSession({ body, env, source });
|
|
26
|
+
return { ok: true, provider, file: auth.file, auth };
|
|
33
27
|
};
|
|
34
|
-
const status = ({ env = process.env } = {}) => {
|
|
35
|
-
const file = resolveAuthFile(env);
|
|
36
|
-
if (!fs.existsSync(file)) {
|
|
37
|
-
return {
|
|
38
|
-
ok: true,
|
|
39
|
-
provider,
|
|
40
|
-
auth: { present: false, status: 'missing', redacted: true },
|
|
41
|
-
file
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const stat = fs.statSync(file);
|
|
45
|
-
let storedStatus = 'imported-test-fixture';
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
|
-
storedStatus = parsed.status || storedStatus;
|
|
49
|
-
} catch {}
|
|
50
|
-
return {
|
|
51
|
-
ok: true,
|
|
52
|
-
provider,
|
|
53
|
-
auth: {
|
|
54
|
-
present: true,
|
|
55
|
-
status: storedStatus,
|
|
56
|
-
redacted: true,
|
|
57
|
-
updated_at: stat.mtime.toISOString(),
|
|
58
|
-
size: stat.size
|
|
59
|
-
},
|
|
60
|
-
file
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const importAuth = ({ input, env = process.env, source = 'zero-agent' } = {}) => {
|
|
65
|
-
const text = ensureInput(input);
|
|
66
|
-
const parsed = parseMaybeJson(text);
|
|
67
|
-
const timestamp = nowIso();
|
|
68
|
-
const payload = {
|
|
69
|
-
provider,
|
|
70
|
-
mode: 'dev-fixture',
|
|
71
|
-
status: 'imported-test-fixture',
|
|
72
|
-
source,
|
|
73
|
-
created_at: timestamp,
|
|
74
|
-
updated_at: timestamp,
|
|
75
|
-
validation: {
|
|
76
|
-
session: 'not-run',
|
|
77
|
-
sentinel: 'not-run',
|
|
78
|
-
capability_probe: 'not-run'
|
|
79
|
-
},
|
|
80
|
-
security: {
|
|
81
|
-
raw_secret_return_allowed: false,
|
|
82
|
-
logs_allowed: false,
|
|
83
|
-
rotation_required: false
|
|
84
|
-
},
|
|
85
|
-
imported: {
|
|
86
|
-
format: parsed.ok ? 'json' : 'text',
|
|
87
|
-
parse_error: parsed.ok ? null : parsed.error,
|
|
88
|
-
sha256: sha256(text),
|
|
89
|
-
byte_length: Buffer.byteLength(text, 'utf8')
|
|
90
|
-
},
|
|
91
|
-
session: parsed.ok ? parsed.value : { raw_text: text }
|
|
92
|
-
};
|
|
93
|
-
const file = resolveAuthFile(env);
|
|
94
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
95
|
-
fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
96
|
-
return {
|
|
97
|
-
ok: true,
|
|
98
|
-
provider,
|
|
99
|
-
file,
|
|
100
|
-
auth: {
|
|
101
|
-
present: true,
|
|
102
|
-
status: payload.status,
|
|
103
|
-
mode: payload.mode,
|
|
104
|
-
redacted: true,
|
|
105
|
-
updated_at: payload.updated_at,
|
|
106
|
-
input_format: payload.imported.format,
|
|
107
|
-
input_sha256: payload.imported.sha256,
|
|
108
|
-
byte_length: payload.imported.byte_length
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const chatgptAuth = {
|
|
114
|
-
defaultMcpHome,
|
|
115
|
-
defaultAuthFile,
|
|
116
|
-
resolveAuthFile,
|
|
117
|
-
status,
|
|
118
|
-
importAuth
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
Object.assign(exports, chatgptAuth);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtime = require('./chatgpt-runtime');
|
|
4
|
+
const session = (env = process.env) => runtime.load(env).session;
|
|
5
|
+
|
|
6
|
+
exports.SESSION_SOURCE = session().SESSION_SOURCE;
|
|
7
|
+
exports.normalizeResult = (result, env = process.env) => session(env).normalizeResult(result);
|
|
8
|
+
exports.exchangeSession = (input = {}) => session(input.env || process.env).exchangeSession(input);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const runtime = require('./chatgpt-runtime');
|
|
4
|
+
|
|
5
|
+
const session = (env = process.env) => runtime.load(env).session;
|
|
6
|
+
|
|
7
|
+
exports.SESSION_STORAGE_KEY = session().SESSION_STORAGE_KEY;
|
|
8
|
+
exports.SESSION_SOURCE = session().SESSION_SOURCE;
|
|
9
|
+
|
|
10
|
+
exports.resolveChromeDataRoot = (env = process.env) => session(env).resolveChromeDataRoot(env);
|
|
11
|
+
exports.detectProfile = (chromeRoot, env = process.env) => session(env).detectProfile(chromeRoot, env);
|
|
12
|
+
exports.copySnapshot = (source, destination, env = process.env) => session(env).copySnapshot(source, destination);
|
|
13
|
+
exports.decodeStorageKey = (value, env = process.env) => session(env).decodeStorageKey(value);
|
|
14
|
+
exports.decodeStorageValue = (value, env = process.env) => session(env).decodeStorageValue(value);
|
|
15
|
+
exports.readLocalStorage = (databasePath, env = process.env) => session(env).readLocalStorage(databasePath);
|
|
16
|
+
exports.extractSessionMarker = (storage, env = process.env) => session(env).extractSessionMarker(storage);
|
|
17
|
+
exports.acquireLocalSessionMarker = ({ env = process.env } = {}) => session(env).acquireLocalSessionMarker({ env });
|
|
18
|
+
|
|
19
|
+
exports.isAuthenticatedSession = (body, env = process.env) => {
|
|
20
|
+
try { session(env).validate({ status: 200, body }); return true; }
|
|
21
|
+
catch { return false; }
|
|
22
|
+
};
|
|
23
|
+
exports.validateFinalSession = (result, env = process.env) => session(env).validate(result);
|
|
24
|
+
exports.resolveTransport = (env = process.env) => session(env).resolveTransport(env);
|
|
25
|
+
exports.fetchSession = ({ env = process.env, transport } = {}) =>
|
|
26
|
+
session(env).acquire({ env, ...(transport ? { transport } : {}) });
|