@miruzero/zgpt 0.1.3 → 0.1.5

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,154 @@
1
+ 'use strict';
2
+
3
+ // Browser-derived MCP connector operations. The single SDK API catalog owns routes,
4
+ // authentication, transport, encoding and response handling. Never auto-create a link.
5
+ const api = require('../api');
6
+
7
+ const bad = (message) => Object.assign(new Error(message), { code: 'BAD_ARGS' });
8
+ const nonempty = (value, field) => {
9
+ if (typeof value !== 'string' || !value.trim()) throw bad(field + ' is required');
10
+ return value.trim();
11
+ };
12
+ const safeHttpUrl = (value, field) => {
13
+ const text = nonempty(value, field);
14
+ let url;
15
+ try { url = new URL(text); } catch { throw bad(field + ' must be a valid HTTPS URL'); }
16
+ if (url.protocol !== 'https:' || url.username || url.password)
17
+ throw bad(field + ' must be a valid HTTPS URL without embedded credentials');
18
+ return text;
19
+ };
20
+ const logoValue = (value) => {
21
+ const input=nonempty(value,'logoUrl');
22
+ if (input.startsWith('data:')) {
23
+ const match=/^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/.exec(input);
24
+ if (!match) throw bad('logoUrl must be a PNG data URL');
25
+ if (Buffer.from(match[1],'base64').length > 10 * 1024)
26
+ throw bad('logoUrl PNG exceeds the 10 KB maximum');
27
+ return input;
28
+ }
29
+ return safeHttpUrl(input,'logoUrl');
30
+ };
31
+ const makeTarget = ({mcpUrl, tunnelId}) => {
32
+ if (mcpUrl != null && tunnelId != null) throw bad('Choose either mcpUrl or tunnelId, not both');
33
+ if (mcpUrl != null) return {mcp_url:safeHttpUrl(mcpUrl,'mcpUrl')};
34
+ if (tunnelId != null) {
35
+ const id=nonempty(tunnelId,'tunnelId');
36
+ if (!/^tunnel_[a-z0-9]{32}$/.test(id)) throw bad('tunnelId must match tunnel_<32 lowercase letters or digits>');
37
+ return {tunnel_id:id};
38
+ }
39
+ throw bad('mcpUrl or tunnelId is required');
40
+ };
41
+ const request = (key, options, patch = {}) =>
42
+ api.invoke(key, {
43
+ ...options,...patch,
44
+ headers:{...options.headers,...patch.headers,'OAI-Product-Sku':'CONNECTOR_SETTING'}
45
+ });
46
+
47
+ const authRequestFromDiscovery = (discovery) => {
48
+ const config=discovery?.oauth_config;
49
+ const invalid=() => Object.assign(new Error('MCP OAuth discovery returned incomplete or unsupported metadata'), {
50
+ code:'MCP_OAUTH_CONFIG_INVALID'
51
+ });
52
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw invalid();
53
+ if (config.type === 'NONE') return {supported_auth:[],oauth_client_params:null};
54
+ if (config.type !== 'OAUTH' ||
55
+ typeof config.authorization_url !== 'string' || !config.authorization_url ||
56
+ typeof config.token_url !== 'string' || !config.token_url) throw invalid();
57
+ // ChatGPT Web puts the discovered OAuth config inside supported_auth.
58
+ // Registration, PKCE, scopes and resource stay exactly as advertised by the server.
59
+ return {supported_auth:[config],oauth_client_params:null};
60
+ };
61
+
62
+ exports.createCustomMcpServer = async (options = {}) => {
63
+ const name = nonempty(options.name,'name');
64
+ const target=makeTarget(options);
65
+ let authRequest;
66
+ if (options.authRequest !== undefined) {
67
+ if (options.authentication) throw bad('Use authentication or authRequest, not both');
68
+ if (!options.authRequest || typeof options.authRequest !== 'object' ||
69
+ Array.isArray(options.authRequest) || !Array.isArray(options.authRequest.supported_auth))
70
+ throw bad('authRequest must be a web-compatible object with supported_auth array');
71
+ authRequest=options.authRequest;
72
+ } else if (options.authentication === 'NONE') {
73
+ authRequest={supported_auth:[],oauth_client_params:null};
74
+ } else if (options.authentication === undefined || options.authentication === 'AUTO') {
75
+ // Discovery is safe to repeat and does not create a connector or a link.
76
+ // Never silently downgrade OAuth to NONE on a discovery error.
77
+ const discovery=await exports.getCustomMcpOAuthConfig(options);
78
+ authRequest=authRequestFromDiscovery(discovery);
79
+ } else throw bad('Unknown authentication type; use AUTO, NONE or a configured authRequest');
80
+ if (options.description !== undefined && typeof options.description !== 'string') throw bad('description must be a string');
81
+ if (options.customHeaders !== undefined && !Array.isArray(options.customHeaders)) throw bad('customHeaders must be an array');
82
+ const body = {
83
+ name, ...target,
84
+ description:(options.description || '').trim(),
85
+ logo_url:options.logoUrl == null ? null : logoValue(options.logoUrl),
86
+ auth_request:authRequest
87
+ };
88
+ if (options.customHeaders?.length) body.custom_headers=options.customHeaders;
89
+ return request('POST /backend-api/aip/connectors/mcp',options,{body});
90
+ };
91
+
92
+ exports.renameCustomMcpServer = async (options = {}) =>
93
+ request('PATCH /backend-api/aip/connectors/{id}/name',options,{
94
+ pathValues:[nonempty(options.connectorId,'connectorId')],
95
+ body:{name:nonempty(options.name,'name')}
96
+ });
97
+
98
+ exports.describeCustomMcpServer = async (options = {}) => {
99
+ if (typeof options.description !== 'string') throw bad('description must be a string');
100
+ return request('PATCH /backend-api/aip/connectors/{id}/description',options,{
101
+ pathValues:[nonempty(options.connectorId,'connectorId')],
102
+ body:{description:options.description}
103
+ });
104
+ };
105
+
106
+ exports.setCustomMcpServerLogo = async (options = {}) =>
107
+ request('PATCH /backend-api/aip/connectors/{id}/logo',options,{
108
+ pathValues:[nonempty(options.connectorId,'connectorId')],
109
+ body:{logo_url:logoValue(options.logoUrl)}
110
+ });
111
+
112
+ exports.deleteCustomMcpServer = async (options = {}) =>
113
+ request('DELETE /backend-api/aip/connectors/{id}',options,{
114
+ pathValues:[nonempty(options.connectorId,'connectorId')]
115
+ });
116
+
117
+ exports.getCustomMcpServer = async (options = {}) =>
118
+ request('GET /backend-api/aip/connectors/{id}',options,{
119
+ pathValues:[nonempty(options.connectorId,'connectorId')]
120
+ });
121
+
122
+ exports.listCustomMcpTunnels = async (options = {}) =>
123
+ request('GET /backend-api/aip/connectors/mcp/tunnels',options);
124
+
125
+ exports.getCustomMcpOAuthConfig = async (options = {}, injectedOptions = {}) => {
126
+ // A single URL argument makes `zgpt connector getCustomMcpOAuthConfig https://...` usable on PowerShell.
127
+ if (typeof options === 'string') options={...injectedOptions,mcpUrl:options};
128
+ if (options.customHeaders !== undefined && !Array.isArray(options.customHeaders))
129
+ throw bad('customHeaders must be an array');
130
+ return request('POST /backend-api/aip/connectors/mcp/oauth_config',options,{
131
+ body:{...makeTarget(options),custom_headers:options.customHeaders || []}
132
+ });
133
+ };
134
+
135
+ // Unlink an account connection without deleting the MCP connector definition.
136
+ // The user must explicitly invoke this destructive operation; never auto-unlink
137
+ // after an auth error, reconnect or action refresh.
138
+ exports.disconnectConnectorLink = async (options = {}, injectedOptions = {}) => {
139
+ if (typeof options === 'string') options = { ...injectedOptions, linkId: options };
140
+ const linkId = nonempty(options.linkId, 'linkId');
141
+ if (!/^link_[A-Za-z0-9_-]+$/.test(linkId))
142
+ throw bad('linkId must be a connector link identifier (link_...)');
143
+ return request('DELETE /backend-api/aip/connectors/links/{id}', options, {
144
+ pathValues: [linkId]
145
+ });
146
+ };
147
+
148
+ // Refresh uses the linked connection ID, not the connector ID.
149
+ exports.reloadCustomMcpServerActions = async (options = {}, injectedOptions = {}) => {
150
+ // Accept the linkId directly for `zgpt connector reloadCustomMcpServerActions <linkId>`.
151
+ // The legacy options-object form and the SDK's single refresh implementation stay intact.
152
+ if (typeof options === 'string') options = { ...injectedOptions, linkId: options };
153
+ return require('./write').refreshConnectorActions(options);
154
+ };
@@ -21,6 +21,7 @@ exports.refreshConnectorActions = async (options = {}) => {
21
21
  const target = '/backend-api/aip/connectors/mcp/refresh_actions';
22
22
  const requestJson = options.requestJson || direct.requestJson;
23
23
  return requireSuccess(await requestJson(target, options.token, options.sessionHeaders, {
24
- method: 'POST', body: { link_id: options.linkId }, route: target, retry: false
24
+ method: 'POST', body: { link_id: options.linkId }, route: target, retry: false,
25
+ headers:{...options.headers,'OAI-Product-Sku':'CONNECTOR_SETTING'}
25
26
  }), target);
26
27
  };
@@ -55,6 +55,35 @@ exports.send = async (id, message, options = {}) => write.sendConversation({
55
55
  message,
56
56
  ...(await bindCanonicalContext(options))
57
57
  });
58
+ // CLI-friendly management façade matching the verified Zero_LLM command tree.
59
+ // Mutations stay owned by ./write; no copied HTTP routes or parallel auth stack.
60
+ const required = (value, name) => {
61
+ if (typeof value !== 'string' || !value.trim()) {
62
+ throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
63
+ }
64
+ return value.trim();
65
+ };
66
+ exports.create = async (message, options = {}) => write.createConversation({
67
+ ...(await bindCanonicalContext(options)),
68
+ message: required(message, 'message')
69
+ });
70
+ exports.rename = async (id, title, options = {}) => write.renameConversation({
71
+ ...(await bindCanonicalContext(options)),
72
+ conversationId: required(id, 'conversationId'),
73
+ title: required(title, 'title')
74
+ });
75
+ exports.delete = async (id, options = {}) => write.deleteConversation({
76
+ ...(await bindCanonicalContext(options)),
77
+ conversationId: required(id, 'conversationId')
78
+ });
79
+ exports.move = async (id, projectId = null, options = {}) => write.moveConversation({
80
+ ...(await bindCanonicalContext(options)),
81
+ conversationId: required(id, 'conversationId'),
82
+ projectId: projectId === 'exit' || projectId == null ? null : required(projectId, 'projectId')
83
+ });
84
+ // Compatibility with the Zero_LLM CLI: new/del map to the same owners.
85
+ exports.new = exports.create;
86
+ exports.del = exports.delete;
58
87
  exports.temp = {
59
88
  new: async (message, options = {}) => temporary.createConversation({
60
89
  message,
@@ -2,6 +2,14 @@
2
2
 
3
3
  const direct = require('../transport/direct/http-client');
4
4
 
5
+ const pageBudget = (maxPages = 1000) => {
6
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
7
+ throw Object.assign(new Error('maxPages must be a positive safe integer'), { code: 'PAGINATION_BUDGET_INVALID' });
8
+ }
9
+ return maxPages;
10
+ };
11
+ const paginationError = (code, message) => Object.assign(new Error(message), { code });
12
+
5
13
  const requireOk = (response, target) => {
6
14
  if (!response || response.status !== 200 || !response.json) {
7
15
  const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
@@ -52,25 +60,45 @@ exports.listStandalone = async ({
52
60
  token,
53
61
  sessionHeaders,
54
62
  requestJson = direct.requestJson,
55
- onProgress = () => {}
63
+ onProgress = () => {},
64
+ maxPages = 1000
56
65
  }) => {
66
+ const budget = pageBudget(maxPages);
57
67
  const items = [];
68
+ const seenItems = new Set();
58
69
  let offset = 0;
59
70
  const limit = 50;
60
71
 
61
- for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
72
+ for (let pageNumber = 0; pageNumber < budget; pageNumber += 1) {
62
73
  const target = `/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated&is_archived=false&is_starred=false`;
63
74
  const page = requireOk(await requestJson(target, token, sessionHeaders), target);
64
75
  const pageItems = Array.isArray(page.items) ? page.items : [];
65
- items.push(...pageItems);
66
- onProgress({ type: 'conversation-page', count: items.length, pageCount: pageItems.length, offset });
76
+ if (page.offset !== undefined && page.offset !== null && Number(page.offset) !== offset) {
77
+ throw paginationError('PAGINATION_STALLED', 'conversation pagination server offset did not advance');
78
+ }
79
+ let added = 0;
80
+ for (const item of pageItems) {
81
+ const id = item?.id || item?.conversation_id;
82
+ const key = id ? `id:${id}` : `row:${JSON.stringify(item)}`;
83
+ if (seenItems.has(key)) continue;
84
+ seenItems.add(key);
85
+ items.push(item);
86
+ added++;
87
+ }
88
+ if (pageItems.length > 0 && added === 0 && pageNumber > 0) {
89
+ throw paginationError('PAGINATION_STALLED', 'conversation pagination returned no new items');
90
+ }
91
+ onProgress({ type: 'conversation-page', count: items.length, pageCount: added, offset });
67
92
 
68
93
  const total = Number(page.total);
69
94
  const pageLimit = Number(page.limit) || limit;
95
+ if (!Number.isSafeInteger(pageLimit) || pageLimit <= 0) {
96
+ throw paginationError('PAGINATION_STALLED', 'conversation pagination returned an invalid page limit');
97
+ }
70
98
  offset += pageLimit;
71
99
  if (!Number.isFinite(total) || offset >= total || pageItems.length === 0) return items;
72
100
  }
73
- throw Object.assign(new Error('conversation pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
101
+ throw paginationError('PAGINATION_LIMIT', `conversation pagination exceeded ${budget} pages`);
74
102
  };
75
103
  exports.searchGlobal = async ({
76
104
  query,
@@ -186,10 +214,12 @@ exports.getConversationMessagesPage = async ({
186
214
  exports.findConversationMessage = async ({
187
215
  conversationId, messageId, numTurns = 10, includeHasVersions = true,
188
216
  token, sessionHeaders, requestJson = direct.requestJson,
189
- getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
217
+ getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage,
218
+ maxPages = 1000
190
219
  }) => {
191
220
  if (!conversationId) throw new Error('conversationId is required');
192
221
  if (!messageId) throw new Error('messageId is required');
222
+ const budget = pageBudget(maxPages);
193
223
  const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
194
224
  let pagesScanned = 1;
195
225
  let nodesScanned = Array.isArray(current?.messages) ? current.messages.length : 0;
@@ -200,6 +230,7 @@ exports.findConversationMessage = async ({
200
230
  let hasPrevious = pageInfo.has_previous_page === true;
201
231
  const seen = new Set();
202
232
  while (hasPrevious && before) {
233
+ if (pagesScanned >= budget) throw paginationError('PAGINATION_LIMIT', `conversation message search exceeded ${budget} pages`);
203
234
  if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
204
235
  seen.add(before);
205
236
  const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
@@ -220,9 +251,11 @@ exports.findConversationMessage = async ({
220
251
  exports.getConversationHistory = async ({
221
252
  conversationId, numTurns = 10, includeHasVersions = true,
222
253
  token, sessionHeaders, requestJson = direct.requestJson,
223
- getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
254
+ getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage,
255
+ maxPages = 1000
224
256
  }) => {
225
257
  if (!conversationId) throw new Error('conversationId is required');
258
+ const budget = pageBudget(maxPages);
226
259
  const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
227
260
  const pages = [{ ...current, kind: 'current_window', before: null }];
228
261
  let messageCount = Array.isArray(current?.messages) ? current.messages.length : 0;
@@ -231,6 +264,7 @@ exports.getConversationHistory = async ({
231
264
  let hasPrevious = pageInfo.has_previous_page === true;
232
265
  const seen = new Set();
233
266
  while (hasPrevious && before) {
267
+ if (pages.length >= budget) throw paginationError('PAGINATION_LIMIT', `conversation history exceeded ${budget} pages`);
234
268
  if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
235
269
  seen.add(before);
236
270
  const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
package/src/index.js CHANGED
@@ -9,6 +9,5 @@ exports.file = require('./file');
9
9
  exports.memory = require('./memory');
10
10
  exports.settings = require('./settings');
11
11
  exports.share = require('./share');
12
-
13
-
14
-
12
+ exports.profile = require('./profile');
13
+ exports.api = require('./api');
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ const context = require('../runtime-context');
3
+ const direct = require('../transport/direct/http-client');
4
+
5
+ const bodyRequired = (value) => {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0)
7
+ throw Object.assign(new Error('body must be a nonempty object'), { code: 'BAD_ARGS' });
8
+ return value;
9
+ };
10
+ const idRequired = (value, name) => {
11
+ if (typeof value !== 'string' || value.trim().length === 0)
12
+ throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
13
+ return encodeURIComponent(value);
14
+ };
15
+ const call = async (target, method, options) => {
16
+ const resolved = await context.bind(options);
17
+ const route = method === 'PATCH' ? '/backend-api/accounts/{id}/users/{id}' : target;
18
+ const args = { method, route, retry: false };
19
+ if (method !== 'GET') args.body = bodyRequired(resolved.body);
20
+ const response = await (resolved.requestJson || direct.requestJson)(target, resolved.token, resolved.sessionHeaders, args);
21
+ if (!response || response.status < 200 || response.status >= 300)
22
+ throw Object.assign(new Error('ChatGPT profile request failed (' + (response?.status || 'no-status') + '): ' + target), {
23
+ status: response?.status || null, code: response?.status === 429 ? 'RATE_LIMITED' : 'PROFILE_REQUEST_FAILED', target
24
+ });
25
+ if (method === 'GET' && (response.json === undefined || response.json === null))
26
+ throw Object.assign(new Error('Profile response missing JSON'), { code: 'PROFILE_BAD_RESPONSE' });
27
+ return response.json || { success: true };
28
+ };
29
+
30
+ // These are captured/static Web Chat routes. Callers must be entitled to the requested surface.
31
+ exports.getMe = (options = {}) => call('/backend-api/profiles/me', 'GET', options);
32
+ exports.getIdentity = (options = {}) => call('/backend-anon/me', 'GET', options);
33
+ exports.getAuraIdentity = (options = {}) => call('/backend-api/aura/identity', 'GET', options);
34
+ exports.getCreator = (options = {}) => call('/backend-api/gizmo_creator_profile', 'GET', options);
35
+ exports.updateCreator = (options = {}) => call('/backend-api/gizmo_creator_profile', 'POST', options);
36
+ exports.updateWorkspaceUser = async (options = {}) => {
37
+ const a = idRequired(options.accountId, 'accountId');
38
+ const u = idRequired(options.userId, 'userId');
39
+ bodyRequired(options.body);
40
+ return call('/backend-api/accounts/' + a + '/users/' + u, 'PATCH', options);
41
+ };
@@ -3,4 +3,38 @@
3
3
  const read = require('./read');
4
4
  const write = require('./write');
5
5
 
6
- Object.assign(exports, read, write);
6
+ Object.assign(exports, read, write);
7
+
8
+ // Mirror the proven Zero_LLM Project CLI without copying HTTP implementations.
9
+ // All actions use the same canonical Zero SDK session and project owner methods.
10
+ const bindContext = require('../runtime-context').bind;
11
+ const required = (value, name) => {
12
+ if (typeof value !== 'string' || !value.trim()) {
13
+ throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
14
+ }
15
+ return value.trim();
16
+ };
17
+ exports.list = async (options = {}) => read.listProjects(await bindContext(options));
18
+ exports.get = async (id, options = {}) => read.getProject({
19
+ ...(await bindContext(options)), projectId: required(id, 'projectId')
20
+ });
21
+ exports.conversations = async (id, options = {}) => read.listProjectConversations({
22
+ ...(await bindContext(options)), projectId: required(id, 'projectId')
23
+ });
24
+ exports.create = async (name, options = {}) => write.createProject({
25
+ ...(await bindContext(options)), name: required(name, 'name')
26
+ });
27
+ exports.rename = async (id, name, options = {}) => write.renameProject({
28
+ ...(await bindContext(options)), projectId: required(id, 'projectId'), name: required(name, 'name')
29
+ });
30
+ exports.delete = async (id, options = {}) => write.deleteProject({
31
+ ...(await bindContext(options)), projectId: required(id, 'projectId')
32
+ });
33
+ exports.save = async (projectId, conversationId, messageId, options = {}) => write.saveProjectMessage({
34
+ ...(await bindContext(options)), projectId: required(projectId, 'projectId'),
35
+ conversationId: required(conversationId, 'conversationId'),
36
+ messageId: required(messageId, 'messageId')
37
+ });
38
+ // Keep the legacy LAB's natural command names without duplicating logic.
39
+ exports.new = exports.create;
40
+ exports.del = exports.delete;
@@ -1,68 +1,60 @@
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();
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
+ sessionHeaders.Cookie = mergeSetCookies(sessionHeaders.Cookie || sessionHeaders.cookie || '', `__Secure-next-auth.session-token=${sessionMarker}`);
38
+ delete sessionHeaders.cookie;
39
+
40
+ const first = await requestJson('/api/auth/session', null, sessionHeaders, {
41
+ route: '/api/auth/session', env, sessionFile, persistSession: false
42
+ });
43
+ if (![200, 403].includes(Number(first?.status))) {
44
+ throw Object.assign(new Error('ChatGPT session bootstrap round one failed'), { code: 'CHATGPT_SESSION_BOOTSTRAP_FAILED', status: first?.status || 0 });
45
+ }
46
+
47
+ sessionHeaders.Cookie = mergeSetCookies(sessionHeaders.Cookie, first?.headers?.['set-cookie']);
48
+ const firstBody = first?.json;
49
+ const firstAuthenticated = Boolean(firstBody && typeof firstBody === 'object' && !Array.isArray(firstBody) && (firstBody.user || firstBody.account || firstBody.accessToken || firstBody.sessionToken));
50
+ if (Number(first?.status) === 200 && firstAuthenticated) {
51
+ return { status: 200, body: firstBody, sessionHeaders };
52
+ }
53
+
54
+ const second = await requestJson('/api/auth/session', null, sessionHeaders, {
55
+ route: '/api/auth/session', env, sessionFile, persistSession: false
56
+ });
57
+ return { status: Number(second?.status || 0), body: second?.json ?? null, sessionHeaders };
58
+ };
59
+
60
+ exports.transport = exports.createTransport();
@@ -15,4 +15,6 @@ exports.updateAccountUserSetting = async (options = {}) => {
15
15
  if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body)) throw Object.assign(new Error('body is required'), { code: 'BAD_ARGS' });
16
16
  const target = '/backend-api/settings/account_user_setting';
17
17
  return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'PATCH', retry: false, route: target, body: options.body }), target);
18
- };
18
+ };
19
+
20
+ Object.assign(exports, require('./web'));
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+ const context = require('../runtime-context');
3
+ const direct = require('../transport/direct/http-client');
4
+
5
+ const invoke = async (route, method, options = {}) => {
6
+ const requestOptions = { route, method, retry: false };
7
+ if (method !== 'GET') {
8
+ if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body) || Object.keys(options.body).length === 0)
9
+ throw Object.assign(new Error('body must be a nonempty object'), { code: 'BAD_ARGS' });
10
+ requestOptions.body = options.body;
11
+ }
12
+ const bound = await context.bind(options);
13
+ const response = await (bound.requestJson || direct.requestJson)(route, bound.token, bound.sessionHeaders, requestOptions);
14
+ if (!response || response.status < 200 || response.status >= 300) {
15
+ const error = new Error('ChatGPT settings request failed (' + (response?.status || 'no-status') + '): ' + route);
16
+ error.status = response?.status || null;
17
+ error.target = route;
18
+ error.code = response?.status === 429 ? 'RATE_LIMITED' : 'SETTINGS_REQUEST_FAILED';
19
+ throw error;
20
+ }
21
+ if (method === 'GET' && (response.json === undefined || response.json === null))
22
+ throw Object.assign(new Error('Settings response missing JSON'), { code: 'SETTINGS_BAD_RESPONSE' });
23
+ return response.json || { success: true };
24
+ };
25
+
26
+ exports.getVoices = (options = {}) => invoke('/backend-api/settings/voices', 'GET', options);
27
+ exports.getTrainingDataUsage = (options = {}) => invoke('/backend-api/accounts/data_usage_for_training', 'GET', options);
28
+ exports.getAgeVerificationEnabled = (options = {}) => invoke('/backend-api/settings/age_verification_enabled', 'GET', options);
29
+ exports.getAdultStatus = (options = {}) => invoke('/backend-api/settings/is_adult', 'GET', options);
30
+ exports.getConsent = (options = {}) => invoke('/backend-api/user_granular_consent', 'GET', options);
31
+ exports.updateConsent = (options = {}) => invoke('/backend-api/user_granular_consent', 'POST', options);
32
+ exports.getNotifications = (options = {}) => invoke('/backend-api/notifications/settings', 'GET', options);
33
+ exports.updateNotifications = (options = {}) => invoke('/backend-api/notifications/settings', 'PATCH', options);
34
+ exports.setDefaultModel = (options = {}) => invoke('/backend-api/settings/user_default_model_config', 'PATCH', options);
35
+ exports.setLastUsedModel = (options = {}) => invoke('/backend-api/settings/user_last_used_model_config', 'PATCH', options);
36
+ exports.setTppDefaultModel = (options = {}) => invoke('/backend-api/settings/user_tpp_default_model_config', 'PATCH', options);
37
+ exports.setTppLastUsedModel = (options = {}) => invoke('/backend-api/settings/user_tpp_last_used_model_config', 'PATCH', options);
38
+ exports.setLocale = (options = {}) => invoke('/backend-api/accounts/users/locale', 'PATCH', options);