@miruzero/zgpt 0.1.0

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.
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ exports.SENTINEL_HEADER_NAMES = [
4
+ 'openai-sentinel-chat-requirements-token',
5
+ 'openai-sentinel-proof-token',
6
+ 'openai-sentinel-turnstile-token'
7
+ ];
8
+
9
+ exports.takeSentinelHeaders = (headers = {}) => Object.fromEntries(
10
+ exports.SENTINEL_HEADER_NAMES
11
+ .filter((name) => typeof headers[name] === 'string' && headers[name])
12
+ .map((name) => [name, headers[name]])
13
+ );
14
+
15
+ exports.observationFromUpdate = (value, phase = 'r') => {
16
+ const parts = String(value || '').split('.');
17
+ return parts[0] === 'ois1' && parts[2] ? `v1.${phase}.p.${parts[2]}` : null;
18
+ };
19
+
20
+ exports.applyIntegrityUpdate = (headers, responseHeaders, phase = 'r') => {
21
+ const observation = exports.observationFromUpdate(responseHeaders?.['x-oai-is-update'], phase);
22
+ if (observation) headers['x-oai-is-client-observation'] = observation;
23
+ return observation;
24
+ };
25
+
26
+ exports.streamObservation = (value) => String(value || '').replace(/^v1\.[rs]\.p\./, 'v1.s.p.');
@@ -0,0 +1,214 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+
5
+ const createUserMessage = (text, id = crypto.randomUUID(), metadata = {}) => ({
6
+ id,
7
+ author: { role: 'user' },
8
+ create_time: Date.now() / 1000,
9
+ content: { content_type: 'text', parts: [String(text)] },
10
+ metadata: {
11
+ selected_sources: [],
12
+ serialization_metadata: { custom_symbol_offsets: [] },
13
+ submission_mode: 'manual_send',
14
+ ...metadata
15
+ }
16
+ });
17
+
18
+ // targeted_reply (quote chip ของเว็บ): user message แปะ metadata + hidden system message ตามหลัง
19
+ // spec จากซอร์สเว็บ (Hzi/_Pi) + capture จริง — label ตัดยาวสุด 80 ตัวอักษร
20
+ const targetedReplyLabel = (text) => {
21
+ const trimmed = String(text).trim();
22
+ return trimmed.length <= 80 ? trimmed : `${trimmed.slice(0, 77).trimEnd()}...`;
23
+ };
24
+
25
+ const targetedReplyMetadata = (targetedReply) => {
26
+ if (!targetedReply || !targetedReply.text) return {};
27
+ return {
28
+ targeted_reply: String(targetedReply.text),
29
+ targeted_reply_label: targetedReply.label || targetedReplyLabel(targetedReply.text),
30
+ ...(targetedReply.sourceMessageId ? { targeted_reply_source_message_id: targetedReply.sourceMessageId } : {}),
31
+ ...(targetedReply.sourceRange ? { targeted_reply_source_range: targetedReply.sourceRange } : {})
32
+ };
33
+ };
34
+
35
+ const createHiddenSystemMessage = (text) => ({
36
+ id: crypto.randomUUID(),
37
+ author: { role: 'system' },
38
+ content: { content_type: 'text', parts: [String(text)] },
39
+ metadata: { exclude_after_next_user_message: true, is_visually_hidden_from_conversation: true }
40
+ });
41
+
42
+ const createHiddenTargetedReplyMessage = (text) =>
43
+ createHiddenSystemMessage(`The user is referring to this in particular:\n${String(text)}`);
44
+
45
+ const systemHintMetadata = (systemHints = [], mentions = []) => ({
46
+ ...(systemHints.length ? { system_hints: systemHints } : {}),
47
+ ...(mentions.length ? { serialization_metadata: { custom_symbol_offsets: mentions.map(({ id, startIndex, endIndex }) => ({ id, symbol: 'ecosystemMention', startIndex, endIndex })) } } : {})
48
+ });
49
+
50
+ const applyAttachments = (message, attachments = []) => {
51
+ if (!Array.isArray(attachments) || attachments.length === 0) return message;
52
+ const kinds = new Set(attachments.map((item) => item?.kind));
53
+ if ([...kinds].some((kind) => kind !== 'image' && kind !== 'document')) {
54
+ throw new Error('unverified attachment projection');
55
+ }
56
+ if (kinds.size > 1) throw new Error('unverified mixed attachment projection');
57
+ if (attachments.some((item) => !item?.attachment || (item.kind === 'image' && !item.part))) {
58
+ throw new Error('unverified attachment projection');
59
+ }
60
+ message.metadata.attachments = attachments.map((item) => ({ ...item.attachment }));
61
+ if (kinds.has('image')) {
62
+ message.content = {
63
+ content_type: 'multimodal_text',
64
+ parts: [...attachments.map((item) => ({ ...item.part })), message.content.parts[0]]
65
+ };
66
+ }
67
+ return message;
68
+ };
69
+
70
+ const conversationMode = (projectId) => projectId
71
+ ? { kind: 'gizmo_interaction', gizmo_id: projectId }
72
+ : { kind: 'primary_assistant' };
73
+
74
+
75
+ const DEFAULT_MODEL = 'gpt-5-6-thinking';
76
+ const DEFAULT_THINKING_EFFORT = 'extended';
77
+ const DEFAULT_MODEL_RESPONSE_CONTRACTS = Object.freeze([{
78
+ id: 'photo_upload_action.v1',
79
+ protocol_version: 1,
80
+ presets: ['cap:image', 'cap:file', 'placement:end']
81
+ }]);
82
+
83
+ const defaultClientContextualInfo = (temporaryChat = false) => temporaryChat
84
+ ? {
85
+ app_name: 'chatgpt.com',
86
+ app_surface: 'codex_browser',
87
+ has_web_push_capabilities: true,
88
+ web_push_notification_permission: 'granted'
89
+ }
90
+ : { app_name: 'chatgpt.com' };
91
+
92
+ const resolveWebProtocolContext = ({ timezone = null, timezoneOffsetMin = null, modelResponseContracts = null, clientContextualInfo = null, temporaryChat = false } = {}) => ({
93
+ timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || null,
94
+ timezone_offset_min: Number.isFinite(timezoneOffsetMin) ? timezoneOffsetMin : new Date().getTimezoneOffset(),
95
+ model_response_contracts: Array.isArray(modelResponseContracts) ? modelResponseContracts : DEFAULT_MODEL_RESPONSE_CONTRACTS,
96
+ client_contextual_info: clientContextualInfo && typeof clientContextualInfo === 'object'
97
+ ? { ...defaultClientContextualInfo(temporaryChat), ...clientContextualInfo }
98
+ : defaultClientContextualInfo(temporaryChat)
99
+ });
100
+
101
+ const buildPrepareBody = ({
102
+ projectId = null,
103
+ model = DEFAULT_MODEL,
104
+ thinkingEffort = DEFAULT_THINKING_EFFORT,
105
+ systemHints = [],
106
+ localFunctionNames = null,
107
+ timezone = null,
108
+ timezoneOffsetMin = null,
109
+ modelResponseContracts = null,
110
+ clientContextualInfo = null,
111
+ temporaryChat = false,
112
+ attachments = [],
113
+ includeForkFromSharedPost = false
114
+ }) => ({
115
+ action: 'next',
116
+ ...(includeForkFromSharedPost ? { fork_from_shared_post: false } : {}),
117
+ parent_message_id: 'client-created-root',
118
+ model,
119
+ thinking_effort: thinkingEffort,
120
+ client_prepare_state: 'none',
121
+ conversation_mode: conversationMode(projectId),
122
+ system_hints: systemHints,
123
+ supports_buffering: true,
124
+ supported_encodings: ['v1'],
125
+ ...(Array.isArray(attachments) && attachments.length ? { attachment_mime_types: attachments.map((item) => item.attachment.mime_type) } : {}),
126
+ ...resolveWebProtocolContext({ timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat }),
127
+ ...(Array.isArray(localFunctionNames) && localFunctionNames.length ? { local_function_names: [...localFunctionNames] } : {}),
128
+ client_prepare_dispatch: 'debounced',
129
+ client_prepare_source: 'window_focus'
130
+ });
131
+
132
+ const buildSendBody = ({
133
+ message,
134
+ projectId = null,
135
+ model = DEFAULT_MODEL,
136
+ thinkingEffort = DEFAULT_THINKING_EFFORT,
137
+ systemHints = [],
138
+ systemHintMentions = [],
139
+ targetedReply = null,
140
+ hiddenSystemMessages = [],
141
+ hideUserMessage = false,
142
+ timezone = null,
143
+ timezoneOffsetMin = null,
144
+ modelResponseContracts = null,
145
+ clientContextualInfo = null,
146
+ localFunctionNames = null,
147
+ enableMessageFollowups = true,
148
+ temporaryChat = false,
149
+ historyAndTrainingDisabled = null,
150
+ isDoNotRemember = null,
151
+ temporaryChatRequestsPersonalization = null,
152
+ browserContext = null,
153
+ attachments = [],
154
+ forceUseSse = null,
155
+ forceUseSearch = null,
156
+ forceParagen = false,
157
+ isOnboardingConversation = false,
158
+ stream = null,
159
+ serviceTier = null,
160
+ forceParallelSwitch = 'auto',
161
+ paragenCotSummaryDisplayOverride = 'allow',
162
+ includeForkFromSharedPost = false
163
+ }) => ({
164
+ action: 'next',
165
+ ...(includeForkFromSharedPost ? { fork_from_shared_post: false } : {}),
166
+ ...(temporaryChat === true || historyAndTrainingDisabled === true ? { history_and_training_disabled: true } : {}),
167
+ ...(temporaryChat === true || typeof isDoNotRemember === 'boolean' ? { is_do_not_remember: typeof isDoNotRemember === 'boolean' ? isDoNotRemember : false } : {}),
168
+ ...(typeof temporaryChatRequestsPersonalization === 'boolean' ? { temporary_chat_requests_personalization: temporaryChatRequestsPersonalization } : {}),
169
+ parent_message_id: 'client-created-root',
170
+ model,
171
+ thinking_effort: thinkingEffort,
172
+ client_prepare_state: temporaryChat === true ? 'none' : 'success',
173
+ conversation_mode: conversationMode(projectId),
174
+ enable_message_followups: enableMessageFollowups,
175
+ ...(forceUseSse != null ? { force_use_sse: forceUseSse } : {}),
176
+ ...(forceUseSearch != null ? { force_use_search: forceUseSearch } : {}),
177
+ ...(forceParagen === true ? { force_paragen: true } : {}),
178
+ system_hints: systemHints,
179
+ ...(isOnboardingConversation === true ? { is_onboarding_conversation: true } : {}),
180
+ supports_buffering: true,
181
+ supported_encodings: ['v1'],
182
+ ...resolveWebProtocolContext({ timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat }),
183
+ ...(Array.isArray(localFunctionNames) && localFunctionNames.length ? { local_function_names: [...localFunctionNames] } : {}),
184
+ ...(browserContext && typeof browserContext === 'object' ? { browser_context: { ...browserContext } } : {}),
185
+ ...(serviceTier ? { service_tier: serviceTier } : {}),
186
+ ...(forceParallelSwitch ? { force_parallel_switch: forceParallelSwitch } : {}),
187
+ ...(paragenCotSummaryDisplayOverride ? { paragen_cot_summary_display_override: paragenCotSummaryDisplayOverride } : {}),
188
+ ...(stream != null ? { stream } : {}),
189
+ messages: [
190
+ applyAttachments(createUserMessage(message, undefined, {
191
+ ...(projectId ? { gizmo_id: projectId } : {}),
192
+ ...(serviceTier ? { service_tier: serviceTier } : {}),
193
+ ...systemHintMetadata(systemHints, systemHintMentions),
194
+ ...targetedReplyMetadata(targetedReply),
195
+ ...(hideUserMessage ? {
196
+ exclude_after_next_user_message: true,
197
+ is_visually_hidden_from_conversation: true
198
+ } : {})
199
+ }), attachments),
200
+ ...(Array.isArray(hiddenSystemMessages)
201
+ ? hiddenSystemMessages
202
+ .filter((text) => typeof text === 'string' && text.trim())
203
+ .map((text) => createHiddenSystemMessage(text))
204
+ : []),
205
+ ...(targetedReply?.text ? [createHiddenTargetedReplyMessage(targetedReply.text)] : [])
206
+ ]
207
+ });
208
+
209
+
210
+ exports.DEFAULT_MODEL = DEFAULT_MODEL;
211
+ exports.DEFAULT_THINKING_EFFORT = DEFAULT_THINKING_EFFORT;
212
+ exports.DEFAULT_MODEL_RESPONSE_CONTRACTS = DEFAULT_MODEL_RESPONSE_CONTRACTS;
213
+ exports.buildPrepareBody = buildPrepareBody;
214
+ exports.buildSendBody = buildSendBody;
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const requirements = require('./sentinel-requirements');
4
+ const proof = require('./sentinel-proof');
5
+
6
+ const PREPARE = '/backend-api/sentinel/chat-requirements/prepare';
7
+ const FINALIZE = '/backend-api/sentinel/chat-requirements/finalize';
8
+
9
+ const requireStep = (response, step) => {
10
+ if (!response || response.status !== 200 || !response.json) {
11
+ const error = new Error(`sentinel ${step} failed (${response?.status || 'no-status'})`);
12
+ error.code = 'SENTINEL_FAILED';
13
+ error.status = response?.status || null;
14
+ throw error;
15
+ }
16
+ return response.json;
17
+ };
18
+
19
+ exports.fetchChatRequirements = async ({ token, sessionHeaders = {}, userAgent, requestJson }) => {
20
+ if (typeof requestJson !== 'function') throw new TypeError('requestJson is required');
21
+ const ua = userAgent || sessionHeaders['user-agent'] || sessionHeaders['User-Agent'] || '';
22
+ const p = requirements.buildToken(ua);
23
+ const prepare = requireStep(await requestJson(PREPARE, token, sessionHeaders, {
24
+ method: 'POST', body: { p }, route: PREPARE
25
+ }), 'prepare');
26
+ if (!prepare.prepare_token) throw Object.assign(new Error('sentinel prepare missing prepare_token'), { code: 'SENTINEL_PREPARE_INVALID' });
27
+ const finalize = requireStep(await requestJson(FINALIZE, token, sessionHeaders, {
28
+ method: 'POST', body: { prepare_token: prepare.prepare_token }, route: FINALIZE
29
+ }), 'finalize');
30
+ if (!finalize.token) throw Object.assign(new Error('sentinel finalize missing token'), { code: 'SENTINEL_FINALIZE_INVALID' });
31
+ const proofToken = prepare.proofofwork?.required
32
+ ? proof.solve(prepare.proofofwork.seed, prepare.proofofwork.difficulty, ua)
33
+ : null;
34
+ return { token: finalize.token, persona: finalize.persona || null, proofToken };
35
+ };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const requirements = require('./sentinel-requirements');
5
+
6
+ exports.solve = (seed, difficulty, ua, scripts, build) => {
7
+ const target = Buffer.from(String(difficulty || '0'), 'hex');
8
+ const value = requirements.browserConfig(ua, scripts, build);
9
+ for (let i = 0; i < 500000; i += 1) {
10
+ value[3] = i;
11
+ value[9] = i >> 1;
12
+ const encoded = requirements.encode(value);
13
+ const digest = crypto.createHash('sha3-512').update(String(seed)).update(encoded).digest();
14
+ if (Buffer.compare(digest.subarray(0, target.length), target) <= 0) return `gAAAAAB${encoded}`;
15
+ }
16
+ const error = new Error('failed to solve sentinel proof');
17
+ error.code = 'SENTINEL_PROOF_FAILED';
18
+ throw error;
19
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const { performance } = require('node:perf_hooks');
5
+
6
+ const DEFAULT_SCRIPT = 'https://chatgpt.com/backend-api/sentinel/sdk.js';
7
+ const pick = (items) => items[Math.floor(Math.random() * items.length)];
8
+ const b64 = (value) => Buffer.from(JSON.stringify(value)).toString('base64');
9
+
10
+ exports.browserConfig = (ua, scripts = [DEFAULT_SCRIPT], build = '') => {
11
+ const perf = performance.now();
12
+ return [
13
+ pick([3000, 4000, 5000]), new Date().toString(), 4294705152, 1, ua,
14
+ pick(scripts.length ? scripts : [DEFAULT_SCRIPT]), build,
15
+ 'en-US', 'en-US,es-US,en,es', Math.random(),
16
+ pick(['webdriver-false', 'vendor-Google Inc.', 'hardwareConcurrency-32', 'pdfViewerEnabled-true']),
17
+ pick(['location', 'documentURI', 'compatMode']),
18
+ pick(['window', 'self', 'document', 'location', 'navigator', 'performance', 'crypto', 'fetch']),
19
+ perf, crypto.randomUUID(), '', pick([8, 16, 24, 32]), Date.now() - perf,
20
+ 0, 0, 0, 0, 0, 0, 0
21
+ ];
22
+ };
23
+
24
+ exports.encode = b64;
25
+ exports.buildToken = (ua, scripts, build) => `gAAAAAC${b64(exports.browserConfig(ua, scripts, build))}`;
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const stateOf = (events, completed, conversationId, currentNode, resumeToken, malformed) => ({
4
+ events: [...events], completed, conversationId, currentNode, resumeToken, malformed
5
+ });
6
+
7
+ exports.createParser = () => {
8
+ let pending = '';
9
+ const events = [];
10
+ let completed = false;
11
+ let conversationId = null;
12
+ let currentNode = null;
13
+ let resumeToken = null;
14
+ let malformed = 0;
15
+
16
+ const acceptLine = (line) => {
17
+ if (!line.startsWith('data:')) return;
18
+ const data = line.slice(5).trim();
19
+ if (!data) return;
20
+ if (data === '[DONE]') { completed = true; return; }
21
+ let event;
22
+ try { event = JSON.parse(data); }
23
+ catch { malformed += 1; return; }
24
+ events.push(event);
25
+ const id = event?.conversation_id || event?.v?.conversation_id || event?.conversation?.id;
26
+ if (typeof id === 'string' && id) conversationId = id;
27
+ if (event?.type === 'message_marker' && typeof event.message_id === 'string' && event.message_id) {
28
+ currentNode = event.message_id;
29
+ }
30
+ if (event?.type === 'resume_conversation_token' && typeof event.token === 'string' && event.token) {
31
+ resumeToken = event.token;
32
+ }
33
+ };
34
+
35
+ return {
36
+ push(chunk) {
37
+ pending += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
38
+ const lines = pending.split(/\r?\n/);
39
+ pending = lines.pop() || '';
40
+ for (const line of lines) acceptLine(line);
41
+ return this.snapshot();
42
+ },
43
+ finish() {
44
+ if (pending) acceptLine(pending);
45
+ pending = '';
46
+ return this.snapshot();
47
+ },
48
+ snapshot() { return stateOf(events, completed, conversationId, currentNode, resumeToken, malformed); }
49
+ };
50
+ };
51
+
52
+ exports.parse = (input) => {
53
+ const parser = exports.createParser();
54
+ parser.push(input);
55
+ return parser.finish();
56
+ };
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ exports.SESSION_SOURCE = 'https://chatgpt.com/api/auth/session';
4
+ exports.normalizeResult = (result) => {
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 { status: Number(result.status || 0), body: result.body ?? result.data ?? null, source: exports.SESSION_SOURCE };
7
+ };
8
+ exports.exchangeSession = async ({ sessionMarker, profile, env = process.env, transport } = {}) => {
9
+ if (!sessionMarker || typeof sessionMarker !== 'string') throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
10
+ 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 }));
12
+ };
@@ -0,0 +1,70 @@
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 nowIso = () => new Date().toISOString();
10
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
11
+ const writeJson = (file, value) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); };
12
+
13
+ exports.validate = (result) => {
14
+ const body = result?.body ?? result;
15
+ const status = result?.status ?? 200;
16
+ const authenticated = Boolean(body && typeof body === 'object' && !Array.isArray(body) && (body.user || body.account || body.accessToken || body.sessionToken));
17
+ if (status === 200 && authenticated) return { status, body, source: exchange.SESSION_SOURCE };
18
+ throw Object.assign(new Error('ChatGPT final session response is not authenticated'), { code: status === 200 ? 'CHATGPT_SESSION_NOT_AUTHENTICATED' : 'CHATGPT_SESSION_FETCH_FAILED', status });
19
+ };
20
+
21
+ exports.resolveTransport = (env = process.env) => {
22
+ const modulePath = env.ZERO_CHATGPT_SESSION_TRANSPORT_MODULE;
23
+ if (!modulePath) throw Object.assign(new Error('ChatGPT session transport is required'), { code: 'CHATGPT_SESSION_TRANSPORT_REQUIRED' });
24
+ const resolved = require.resolve(modulePath); delete require.cache[resolved];
25
+ const loaded = require(resolved);
26
+ const transport = typeof loaded === 'function' ? loaded : loaded.transport || loaded.exchangeSession;
27
+ if (typeof transport !== 'function') throw Object.assign(new Error('Configured ChatGPT session transport is invalid'), { code: 'CHATGPT_SESSION_TRANSPORT_INVALID' });
28
+ return transport;
29
+ };
30
+
31
+ exports.status = ({ env = process.env } = {}) => {
32
+ const file = paths.sessionFile(env);
33
+ if (!fs.existsSync(file)) return { present: false, status: 'missing', redacted: true, file };
34
+ const data = readJson(file);
35
+ if (!data) return { present: true, status: 'invalid', redacted: true, file };
36
+ const headerNames = data.headers && typeof data.headers === 'object' ? Object.keys(data.headers) : [];
37
+ return { present: true, status: 'present', redacted: true, source: data.source || null, expires: data.expires || null, updatedAt: data.updatedAt || null, headerNames, file };
38
+ };
39
+
40
+ exports.acquire = async ({ env = process.env, transport, acquireMarker = localStorage.acquireLocalSessionMarker, persist = true } = {}) => {
41
+ const local = await acquireMarker({ env });
42
+ const selectedTransport = transport || exports.resolveTransport(env);
43
+ const exchanged = await exchange.exchangeSession({ sessionMarker: local.sessionMarker, profile: local.profile, env, transport: selectedTransport });
44
+ const result = exports.validate(exchanged);
45
+ if (persist) {
46
+ const file = paths.sessionFile(env);
47
+ const previous = readJson(file) || {};
48
+ const next = {
49
+ ...(previous.headers && typeof previous.headers === 'object' ? { headers: previous.headers } : {}),
50
+ source: 'api-auth-session',
51
+ updatedAt: nowIso(),
52
+ expires: result.body?.expires || previous.expires || null,
53
+ profile: local.profile || null
54
+ };
55
+ writeJson(file, next);
56
+ }
57
+ return { ...result, profile: local.profile || null };
58
+ };
59
+ exports.SESSION_STORAGE_KEY = localStorage.SESSION_STORAGE_KEY;
60
+ exports.SESSION_SOURCE = exchange.SESSION_SOURCE;
61
+ exports.resolveChromeDataRoot = (env = process.env) => localStorage.resolveChromeDataRoot(env);
62
+ exports.detectProfile = (chromeRoot, env = process.env) => localStorage.detectProfile(chromeRoot, env);
63
+ exports.copySnapshot = (source, destination) => localStorage.copySnapshot(source, destination);
64
+ exports.decodeStorageKey = (value) => localStorage.decodeStorageKey(value);
65
+ exports.decodeStorageValue = (value) => localStorage.decodeStorageValue(value);
66
+ exports.readLocalStorage = (databasePath) => localStorage.readLocalStorage(databasePath);
67
+ exports.extractSessionMarker = (storage) => localStorage.extractSessionMarker(storage);
68
+ exports.acquireLocalSessionMarker = (input = {}) => localStorage.acquireLocalSessionMarker(input);
69
+ exports.normalizeResult = (result) => exchange.normalizeResult(result);
70
+ exports.exchangeSession = (input = {}) => exchange.exchangeSession(input);
@@ -0,0 +1,75 @@
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.extractSessionMarker = (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
+ return sessions.find((item) => typeof item?.sessionToken === 'string' && item.sessionToken.length > 0)?.sessionToken || null;
61
+ };
62
+ exports.acquireLocalSessionMarker = async ({ env = process.env } = {}) => {
63
+ const chromeRoot = exports.resolveChromeDataRoot(env);
64
+ const profile = exports.detectProfile(chromeRoot, env);
65
+ const source = path.join(chromeRoot, profile, 'Local Storage', 'leveldb');
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 sessionMarker = exports.extractSessionMarker(storage);
72
+ if (!sessionMarker) throw Object.assign(new Error('No ChatGPT local session marker was found'), { code: 'CHATGPT_LOCAL_SESSION_NOT_FOUND' });
73
+ return { sessionMarker, profile };
74
+ } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); }
75
+ };