dsh-codex-subscription 0.2.8
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/AGENTS.md +150 -0
- package/LICENSE +21 -0
- package/README.en.md +180 -0
- package/README.md +166 -0
- package/SECURITY.md +32 -0
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/cordis.patch.yml +6 -0
- package/docs/assets/composer-quota-en.png +0 -0
- package/docs/assets/settings-en.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/dsh-codex.ps1 +756 -0
- package/lib/client.js +865 -0
- package/lib/index.js +925 -0
- package/package.json +117 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
+
import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
4
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
8
|
+
import { createModels } from "@earendil-works/pi-ai";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { WebError } from "@deepseek-ai/dsh-web";
|
|
11
|
+
//#region src/credential-store.js
|
|
12
|
+
const PROVIDER$1 = "openai-codex";
|
|
13
|
+
const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
|
|
14
|
+
const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
15
|
+
function assertProvider(providerId) {
|
|
16
|
+
if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
|
|
17
|
+
}
|
|
18
|
+
function assertOAuthCredential(value) {
|
|
19
|
+
if (value === void 0) return void 0;
|
|
20
|
+
if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
|
|
21
|
+
return clone(value);
|
|
22
|
+
}
|
|
23
|
+
function parseOAuthCredential(value) {
|
|
24
|
+
try {
|
|
25
|
+
return assertOAuthCredential(JSON.parse(value));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
|
|
28
|
+
throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
|
|
33
|
+
* Refresh/login/logout operations are serialized so an older refresh response
|
|
34
|
+
* cannot overwrite a newer rotated token.
|
|
35
|
+
*/
|
|
36
|
+
var DshOAuthCredentialStore = class {
|
|
37
|
+
#chains = /* @__PURE__ */ new Map();
|
|
38
|
+
constructor(credentials, ref, legacyRefs = []) {
|
|
39
|
+
if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
|
|
40
|
+
this.credentials = credentials;
|
|
41
|
+
this.ref = ref;
|
|
42
|
+
this.legacyRefs = Object.freeze([...legacyRefs]);
|
|
43
|
+
}
|
|
44
|
+
#enqueue(providerId, operation, options) {
|
|
45
|
+
assertProvider(providerId);
|
|
46
|
+
const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
47
|
+
abortIfNeeded(options);
|
|
48
|
+
return operation();
|
|
49
|
+
});
|
|
50
|
+
const tail = current.catch(() => void 0);
|
|
51
|
+
this.#chains.set(providerId, tail);
|
|
52
|
+
tail.finally(() => {
|
|
53
|
+
if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
|
|
54
|
+
});
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
async read(providerId, options) {
|
|
58
|
+
assertProvider(providerId);
|
|
59
|
+
abortIfNeeded(options);
|
|
60
|
+
let hit = await this.credentials.resolve(this.ref);
|
|
61
|
+
if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
|
|
62
|
+
const legacy = await this.credentials.resolve(legacyRef);
|
|
63
|
+
if (legacy?.value === void 0 || legacy.value === "") continue;
|
|
64
|
+
const migrated = parseOAuthCredential(legacy.value);
|
|
65
|
+
await this.credentials.set(this.ref, JSON.stringify(migrated));
|
|
66
|
+
await this.credentials.unset(legacyRef);
|
|
67
|
+
hit = { value: JSON.stringify(migrated) };
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
abortIfNeeded(options);
|
|
71
|
+
if (hit?.value === void 0 || hit.value === "") return void 0;
|
|
72
|
+
return parseOAuthCredential(hit.value);
|
|
73
|
+
}
|
|
74
|
+
async list(options) {
|
|
75
|
+
abortIfNeeded(options);
|
|
76
|
+
return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
|
|
77
|
+
providerId: PROVIDER$1,
|
|
78
|
+
type: "oauth"
|
|
79
|
+
}];
|
|
80
|
+
}
|
|
81
|
+
modify(providerId, update, options) {
|
|
82
|
+
return this.#enqueue(providerId, async () => {
|
|
83
|
+
const current = await this.read(providerId, options);
|
|
84
|
+
const next = await update(clone(current));
|
|
85
|
+
abortIfNeeded(options);
|
|
86
|
+
if (next === void 0) return current;
|
|
87
|
+
const validated = assertOAuthCredential(next);
|
|
88
|
+
await this.credentials.set(this.ref, JSON.stringify(validated));
|
|
89
|
+
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
90
|
+
abortIfNeeded(options);
|
|
91
|
+
return clone(validated);
|
|
92
|
+
}, options);
|
|
93
|
+
}
|
|
94
|
+
delete(providerId, options) {
|
|
95
|
+
return this.#enqueue(providerId, async () => {
|
|
96
|
+
await this.credentials.unset(this.ref);
|
|
97
|
+
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
98
|
+
abortIfNeeded(options);
|
|
99
|
+
}, options);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** Return only account state that is safe to expose to the browser client. */
|
|
103
|
+
function createCodexAuthService(models, store) {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
async status(options) {
|
|
106
|
+
const current = await store.read(PROVIDER$1, options);
|
|
107
|
+
if (current === void 0) return {
|
|
108
|
+
authenticated: false,
|
|
109
|
+
provider: PROVIDER$1
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
authenticated: true,
|
|
113
|
+
provider: PROVIDER$1,
|
|
114
|
+
type: "oauth",
|
|
115
|
+
expiresAt: current.expires
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
login(interaction) {
|
|
119
|
+
return models.login(PROVIDER$1, "oauth", interaction);
|
|
120
|
+
},
|
|
121
|
+
logout(options) {
|
|
122
|
+
return models.logout(PROVIDER$1, options);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/external-url.js
|
|
128
|
+
const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
|
|
129
|
+
/** Validate the only external origin this plugin may launch. */
|
|
130
|
+
function assertCodexAuthUrl(value) {
|
|
131
|
+
let url;
|
|
132
|
+
try {
|
|
133
|
+
url = new URL(value);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error("Codex auth URL is invalid");
|
|
136
|
+
}
|
|
137
|
+
if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
|
|
138
|
+
if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
|
|
139
|
+
return url.href;
|
|
140
|
+
}
|
|
141
|
+
/** Return a shell-free native opener command for the current desktop. */
|
|
142
|
+
function commandForCodexAuthUrl(value, platform = process.platform) {
|
|
143
|
+
const url = assertCodexAuthUrl(value);
|
|
144
|
+
if (platform === "win32") return {
|
|
145
|
+
file: "rundll32.exe",
|
|
146
|
+
args: ["url.dll,FileProtocolHandler", url],
|
|
147
|
+
shell: false
|
|
148
|
+
};
|
|
149
|
+
if (platform === "darwin") return {
|
|
150
|
+
file: "open",
|
|
151
|
+
args: [url],
|
|
152
|
+
shell: false
|
|
153
|
+
};
|
|
154
|
+
if (platform === "linux") return {
|
|
155
|
+
file: "xdg-open",
|
|
156
|
+
args: [url],
|
|
157
|
+
shell: false
|
|
158
|
+
};
|
|
159
|
+
throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
|
|
160
|
+
}
|
|
161
|
+
function openCodexAuthUrl(value, options = {}) {
|
|
162
|
+
const command = commandForCodexAuthUrl(value, options.platform);
|
|
163
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const child = spawnProcess(command.file, command.args, {
|
|
166
|
+
detached: true,
|
|
167
|
+
stdio: "ignore",
|
|
168
|
+
windowsHide: true,
|
|
169
|
+
shell: command.shell
|
|
170
|
+
});
|
|
171
|
+
child.once("error", reject);
|
|
172
|
+
child.once("spawn", () => {
|
|
173
|
+
child.unref();
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/login-coordinator.js
|
|
180
|
+
const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
|
|
181
|
+
const TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
182
|
+
"authenticated",
|
|
183
|
+
"failed",
|
|
184
|
+
"cancelled"
|
|
185
|
+
]);
|
|
186
|
+
const publicClone = (value) => structuredClone(value);
|
|
187
|
+
const asObject = (value) => value !== null && typeof value === "object" ? value : {};
|
|
188
|
+
const ok = (value) => ({
|
|
189
|
+
ok: true,
|
|
190
|
+
value
|
|
191
|
+
});
|
|
192
|
+
const badRequest = (message) => ({
|
|
193
|
+
ok: false,
|
|
194
|
+
error: {
|
|
195
|
+
code: "bad-request",
|
|
196
|
+
message,
|
|
197
|
+
details: { issues: [] }
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const deferred = () => {
|
|
201
|
+
let resolve;
|
|
202
|
+
let reject;
|
|
203
|
+
return {
|
|
204
|
+
promise: new Promise((onResolve, onReject) => {
|
|
205
|
+
resolve = onResolve;
|
|
206
|
+
reject = onReject;
|
|
207
|
+
}),
|
|
208
|
+
resolve,
|
|
209
|
+
reject
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
const publicPrompt = (prompt) => ({
|
|
213
|
+
type: prompt.type,
|
|
214
|
+
message: String(prompt.message ?? ""),
|
|
215
|
+
...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
|
|
216
|
+
});
|
|
217
|
+
/** Own one host-side login without exposing tokens to the browser client. */
|
|
218
|
+
var CodexLoginCoordinator = class {
|
|
219
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
220
|
+
#activeId;
|
|
221
|
+
constructor(auth, options = {}) {
|
|
222
|
+
this.auth = auth;
|
|
223
|
+
this.createId = options.createId ?? (() => crypto.randomUUID());
|
|
224
|
+
}
|
|
225
|
+
async accountStatus(options) {
|
|
226
|
+
return publicClone(await this.auth.status(options));
|
|
227
|
+
}
|
|
228
|
+
async start({ method }) {
|
|
229
|
+
if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
|
|
230
|
+
const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
|
|
231
|
+
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) throw new Error("a Codex login is already active");
|
|
232
|
+
if (active !== void 0) this.#sessions.delete(active.view.id);
|
|
233
|
+
const id = this.createId();
|
|
234
|
+
const ready = deferred();
|
|
235
|
+
const controller = new AbortController();
|
|
236
|
+
const session = {
|
|
237
|
+
controller,
|
|
238
|
+
prompt: void 0,
|
|
239
|
+
ready,
|
|
240
|
+
view: {
|
|
241
|
+
id,
|
|
242
|
+
provider: "openai-codex",
|
|
243
|
+
method,
|
|
244
|
+
phase: "starting",
|
|
245
|
+
authenticated: false
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
this.#sessions.set(id, session);
|
|
249
|
+
this.#activeId = id;
|
|
250
|
+
const publishReady = () => ready.resolve(this.read(id));
|
|
251
|
+
const interaction = {
|
|
252
|
+
signal: controller.signal,
|
|
253
|
+
prompt: async (prompt) => {
|
|
254
|
+
controller.signal.throwIfAborted();
|
|
255
|
+
if (prompt.type === "select") return method;
|
|
256
|
+
if (![
|
|
257
|
+
"manual_code",
|
|
258
|
+
"text",
|
|
259
|
+
"secret"
|
|
260
|
+
].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
|
|
261
|
+
const answer = deferred();
|
|
262
|
+
session.prompt = answer;
|
|
263
|
+
session.view = {
|
|
264
|
+
...session.view,
|
|
265
|
+
phase: "waiting_input",
|
|
266
|
+
prompt: publicPrompt(prompt)
|
|
267
|
+
};
|
|
268
|
+
const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
|
|
269
|
+
controller.signal.addEventListener("abort", abortPrompt, { once: true });
|
|
270
|
+
prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
|
|
271
|
+
publishReady();
|
|
272
|
+
try {
|
|
273
|
+
return await answer.promise;
|
|
274
|
+
} finally {
|
|
275
|
+
controller.signal.removeEventListener("abort", abortPrompt);
|
|
276
|
+
prompt.signal?.removeEventListener("abort", abortPrompt);
|
|
277
|
+
if (session.prompt === answer) session.prompt = void 0;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
notify: (event) => {
|
|
281
|
+
if (controller.signal.aborted) return;
|
|
282
|
+
if (event.type === "auth_url") session.view = {
|
|
283
|
+
...session.view,
|
|
284
|
+
phase: "waiting_browser",
|
|
285
|
+
authUrl: assertCodexAuthUrl(event.url),
|
|
286
|
+
...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
|
|
287
|
+
};
|
|
288
|
+
else if (event.type === "device_code") session.view = {
|
|
289
|
+
...session.view,
|
|
290
|
+
phase: "waiting_device",
|
|
291
|
+
deviceCode: {
|
|
292
|
+
userCode: event.userCode,
|
|
293
|
+
verificationUri: assertCodexAuthUrl(event.verificationUri),
|
|
294
|
+
...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
|
|
295
|
+
...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
else session.view = {
|
|
299
|
+
...session.view,
|
|
300
|
+
message: String(event.message ?? "")
|
|
301
|
+
};
|
|
302
|
+
publishReady();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
session.run = Promise.resolve().then(() => this.auth.login(interaction)).then(async () => {
|
|
306
|
+
if (controller.signal.aborted) return;
|
|
307
|
+
const status = await this.auth.status();
|
|
308
|
+
session.view = {
|
|
309
|
+
id,
|
|
310
|
+
provider: "openai-codex",
|
|
311
|
+
method,
|
|
312
|
+
phase: "authenticated",
|
|
313
|
+
authenticated: status.authenticated === true,
|
|
314
|
+
...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
|
|
315
|
+
};
|
|
316
|
+
}).catch((error) => {
|
|
317
|
+
if (controller.signal.aborted) {
|
|
318
|
+
session.view = {
|
|
319
|
+
id,
|
|
320
|
+
provider: "openai-codex",
|
|
321
|
+
method,
|
|
322
|
+
phase: "cancelled",
|
|
323
|
+
authenticated: false
|
|
324
|
+
};
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
session.view = {
|
|
328
|
+
id,
|
|
329
|
+
provider: "openai-codex",
|
|
330
|
+
method,
|
|
331
|
+
phase: "failed",
|
|
332
|
+
authenticated: false,
|
|
333
|
+
error: "Codex login failed"
|
|
334
|
+
};
|
|
335
|
+
session.hostError = error;
|
|
336
|
+
}).finally(publishReady);
|
|
337
|
+
return ready.promise;
|
|
338
|
+
}
|
|
339
|
+
read(id) {
|
|
340
|
+
const session = this.#sessions.get(id);
|
|
341
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
342
|
+
return publicClone(session.view);
|
|
343
|
+
}
|
|
344
|
+
async submit({ id, value }) {
|
|
345
|
+
const session = this.#sessions.get(id);
|
|
346
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
347
|
+
if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
|
|
348
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
|
|
349
|
+
const answer = session.prompt;
|
|
350
|
+
session.prompt = void 0;
|
|
351
|
+
session.view = {
|
|
352
|
+
...session.view,
|
|
353
|
+
phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
|
|
354
|
+
prompt: void 0
|
|
355
|
+
};
|
|
356
|
+
answer.resolve(value);
|
|
357
|
+
return this.read(id);
|
|
358
|
+
}
|
|
359
|
+
async cancel(id) {
|
|
360
|
+
const session = this.#sessions.get(id);
|
|
361
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
362
|
+
if (!TERMINAL_PHASES.has(session.view.phase)) {
|
|
363
|
+
session.view = {
|
|
364
|
+
id,
|
|
365
|
+
provider: "openai-codex",
|
|
366
|
+
method: session.view.method,
|
|
367
|
+
phase: "cancelled",
|
|
368
|
+
authenticated: false
|
|
369
|
+
};
|
|
370
|
+
session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
|
|
371
|
+
}
|
|
372
|
+
await Promise.resolve(session.run).catch(() => void 0);
|
|
373
|
+
return this.read(id);
|
|
374
|
+
}
|
|
375
|
+
async logout(options) {
|
|
376
|
+
if (this.#activeId !== void 0) {
|
|
377
|
+
const active = this.#sessions.get(this.#activeId);
|
|
378
|
+
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
|
|
379
|
+
}
|
|
380
|
+
await this.auth.logout(options);
|
|
381
|
+
return this.accountStatus(options);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
/** Map the loopback-only DSH Connection channel onto the coordinator. */
|
|
385
|
+
function createCodexRpcHandler(coordinator, options = {}) {
|
|
386
|
+
const openExternal = options.openExternal;
|
|
387
|
+
return async (endpoint, payload, signal) => {
|
|
388
|
+
try {
|
|
389
|
+
signal.throwIfAborted();
|
|
390
|
+
const input = asObject(payload);
|
|
391
|
+
if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
|
|
392
|
+
if (endpoint === "login/start") {
|
|
393
|
+
const started = await coordinator.start({ method: input.method });
|
|
394
|
+
if (input.openExternal !== true) return ok(started);
|
|
395
|
+
const url = started.authUrl ?? started.deviceCode?.verificationUri;
|
|
396
|
+
if (typeof url !== "string" || openExternal === void 0) return ok({
|
|
397
|
+
...started,
|
|
398
|
+
externalOpened: false
|
|
399
|
+
});
|
|
400
|
+
try {
|
|
401
|
+
await openExternal(url);
|
|
402
|
+
return ok({
|
|
403
|
+
...started,
|
|
404
|
+
externalOpened: true
|
|
405
|
+
});
|
|
406
|
+
} catch {
|
|
407
|
+
return ok({
|
|
408
|
+
...started,
|
|
409
|
+
externalOpened: false
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (endpoint === "login/status") return ok(coordinator.read(input.id));
|
|
414
|
+
if (endpoint === "login/submit") return ok(await coordinator.submit({
|
|
415
|
+
id: input.id,
|
|
416
|
+
value: input.value
|
|
417
|
+
}));
|
|
418
|
+
if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
|
|
419
|
+
if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
|
|
420
|
+
return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (signal.aborted) throw error;
|
|
423
|
+
const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
|
|
424
|
+
return badRequest(message);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/pi-ai-runtime.js
|
|
430
|
+
/**
|
|
431
|
+
* Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
|
|
432
|
+
* PiAiAdapter to pass the access token resolved by the host credential store.
|
|
433
|
+
*
|
|
434
|
+
* PiAiAdapter owns a request-local Models collection without a credential
|
|
435
|
+
* store. A pure OAuth provider ignores its `apiKey` request override and fails
|
|
436
|
+
* before dispatch with "Provider is not configured". This non-interactive
|
|
437
|
+
* bridge teaches that collection how to consume only the already-refreshed
|
|
438
|
+
* token for this request; login, refresh, persistence, headers, transport, and
|
|
439
|
+
* model behavior remain owned by the original provider.
|
|
440
|
+
*/
|
|
441
|
+
function openaiCodexSubscriptionProvider() {
|
|
442
|
+
const provider = createOpenAICodexProvider();
|
|
443
|
+
const requestToken = Object.freeze({
|
|
444
|
+
name: "DSH-managed Codex OAuth request token",
|
|
445
|
+
async resolve({ credential }) {
|
|
446
|
+
const token = credential?.type === "api_key" ? credential.key : void 0;
|
|
447
|
+
if (typeof token !== "string" || token.length === 0) return void 0;
|
|
448
|
+
return {
|
|
449
|
+
auth: { apiKey: token },
|
|
450
|
+
source: "DSH-managed OAuth request"
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return Object.freeze({
|
|
455
|
+
...provider,
|
|
456
|
+
auth: Object.freeze({
|
|
457
|
+
...provider.auth,
|
|
458
|
+
apiKey: requestToken
|
|
459
|
+
})
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/codex-search.js
|
|
464
|
+
const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
|
|
465
|
+
const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
|
|
466
|
+
const DEFAULT_MODEL = "gpt-5.6-luna";
|
|
467
|
+
const MAX_OUTPUT_TOKENS = 4096;
|
|
468
|
+
const MAX_SOURCE_DATE = 64;
|
|
469
|
+
const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
470
|
+
const nonEmpty = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
471
|
+
const displayText = (value) => {
|
|
472
|
+
const text = nonEmpty(value)?.replace(/\s+/gu, " ").trim();
|
|
473
|
+
if (text === void 0 || text.length === 0) return void 0;
|
|
474
|
+
return text;
|
|
475
|
+
};
|
|
476
|
+
const boundedDisplayText = (value, maximum) => {
|
|
477
|
+
const text = displayText(value);
|
|
478
|
+
if (text === void 0 || text.length <= maximum) return text;
|
|
479
|
+
return `${text.slice(0, maximum - 1)}…`;
|
|
480
|
+
};
|
|
481
|
+
function sourceOf(value) {
|
|
482
|
+
if (!record$1(value)) return void 0;
|
|
483
|
+
const url = nonEmpty(value.url);
|
|
484
|
+
if (url === void 0) return void 0;
|
|
485
|
+
let parsed;
|
|
486
|
+
try {
|
|
487
|
+
parsed = new URL(url);
|
|
488
|
+
} catch {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
492
|
+
const title = displayText(value.title) ?? parsed.hostname;
|
|
493
|
+
const snippet = displayText(value.snippet);
|
|
494
|
+
const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
|
|
495
|
+
return {
|
|
496
|
+
url,
|
|
497
|
+
title,
|
|
498
|
+
...snippet === void 0 ? {} : { snippet },
|
|
499
|
+
...publishedAt === void 0 ? {} : { publishedAt }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function parseSearchResponse(value) {
|
|
503
|
+
if (!record$1(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
|
|
504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
505
|
+
const sources = [];
|
|
506
|
+
for (const result of value.results ?? []) {
|
|
507
|
+
const source = sourceOf(result);
|
|
508
|
+
if (source === void 0 || seen.has(source.url)) continue;
|
|
509
|
+
seen.add(source.url);
|
|
510
|
+
sources.push(source);
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
sources,
|
|
514
|
+
truncated: false
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
|
|
518
|
+
function createCodexSearchProvider(options) {
|
|
519
|
+
const fetchSearch = options.fetch ?? fetch;
|
|
520
|
+
return Object.freeze({
|
|
521
|
+
id: CODEX_SEARCH_PROVIDER_ID,
|
|
522
|
+
available: () => true,
|
|
523
|
+
async search(request, signal) {
|
|
524
|
+
const auth = await options.getAuth({ signal });
|
|
525
|
+
const credential = await options.readCredential({ signal });
|
|
526
|
+
const access = auth?.auth?.apiKey;
|
|
527
|
+
const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
|
|
528
|
+
if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
|
|
529
|
+
const model = nonEmpty(options.resolveModel?.()) ?? DEFAULT_MODEL;
|
|
530
|
+
const id = nonEmpty(options.resolveSessionId?.()) ?? randomUUID();
|
|
531
|
+
let response;
|
|
532
|
+
try {
|
|
533
|
+
response = await fetchSearch(CODEX_SEARCH_URL, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
redirect: "error",
|
|
536
|
+
headers: {
|
|
537
|
+
authorization: `Bearer ${access}`,
|
|
538
|
+
"chatgpt-account-id": accountId,
|
|
539
|
+
accept: "application/json",
|
|
540
|
+
"content-type": "application/json",
|
|
541
|
+
originator: "pi",
|
|
542
|
+
"user-agent": "dsh-codex-subscription/0.2.8"
|
|
543
|
+
},
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
id,
|
|
546
|
+
model,
|
|
547
|
+
input: request.query,
|
|
548
|
+
commands: {
|
|
549
|
+
search_query: [{ q: request.query }],
|
|
550
|
+
response_length: "short"
|
|
551
|
+
},
|
|
552
|
+
settings: {
|
|
553
|
+
allowed_callers: ["direct"],
|
|
554
|
+
external_web_access: true
|
|
555
|
+
},
|
|
556
|
+
max_output_tokens: MAX_OUTPUT_TOKENS
|
|
557
|
+
}),
|
|
558
|
+
signal
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
|
|
562
|
+
throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
|
|
563
|
+
}
|
|
564
|
+
if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
|
|
565
|
+
let value;
|
|
566
|
+
try {
|
|
567
|
+
value = await response.json();
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
return parseSearchResponse(value);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/settings-contract.js
|
|
581
|
+
const SETTINGS_NAMESPACE = "codex-subscription";
|
|
582
|
+
const QUICK_QUOTA_FIELD = "quickQuotaVisible";
|
|
583
|
+
const SEARCH_PROVIDER_FIELD = "searchProvider";
|
|
584
|
+
const SEARCH_PROVIDER_CODEX = "codex";
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/usage.js
|
|
587
|
+
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
588
|
+
const DEFAULT_TTL_MS = 6e4;
|
|
589
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
590
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
591
|
+
function windowOf(value) {
|
|
592
|
+
if (value === void 0 || value === null) return void 0;
|
|
593
|
+
if (!record(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
594
|
+
const used = value.used_percent;
|
|
595
|
+
const seconds = value.limit_window_seconds;
|
|
596
|
+
if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
|
|
597
|
+
if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("Codex returned an invalid window duration");
|
|
598
|
+
const resetsAt = epochSeconds(value.reset_at, "rate-limit reset time");
|
|
599
|
+
return {
|
|
600
|
+
usedPercent: used,
|
|
601
|
+
remainingPercent: 100 - used,
|
|
602
|
+
windowSeconds: seconds,
|
|
603
|
+
...resetsAt === void 0 ? {} : { resetsAt }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function limitOf(id, name, value) {
|
|
607
|
+
if (value === void 0 || value === null) return void 0;
|
|
608
|
+
if (!record(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
609
|
+
const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
|
|
610
|
+
return windows.length === 0 ? void 0 : {
|
|
611
|
+
id,
|
|
612
|
+
...name ? { name } : {},
|
|
613
|
+
windows
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function decimal(value, label) {
|
|
617
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 64 || !/^-?\d+(?:\.\d+)?$/u.test(value)) throw new Error(`Codex returned an invalid ${label}`);
|
|
618
|
+
return value;
|
|
619
|
+
}
|
|
620
|
+
function epochSeconds(value, label) {
|
|
621
|
+
if (value === void 0 || value === null || value === 0) return void 0;
|
|
622
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Codex returned an invalid ${label}`);
|
|
623
|
+
return value;
|
|
624
|
+
}
|
|
625
|
+
function creditsOf(value) {
|
|
626
|
+
if (value === void 0 || value === null) return void 0;
|
|
627
|
+
if (!record(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
|
|
628
|
+
if (!value.has_credits) return void 0;
|
|
629
|
+
return {
|
|
630
|
+
unlimited: value.unlimited,
|
|
631
|
+
...value.balance === void 0 || value.balance === null ? {} : { balance: decimal(value.balance, "credit balance") }
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function individualOf(value) {
|
|
635
|
+
if (value === void 0 || value === null) return void 0;
|
|
636
|
+
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
637
|
+
const item = value.individual_limit;
|
|
638
|
+
if (item === void 0 || item === null) return void 0;
|
|
639
|
+
if (!record(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
|
|
640
|
+
const resetsAt = epochSeconds(item.reset_at, "individual-limit reset time");
|
|
641
|
+
return {
|
|
642
|
+
limit: decimal(item.limit, "individual limit"),
|
|
643
|
+
used: decimal(item.used, "individual usage"),
|
|
644
|
+
remainingPercent: item.remaining_percent,
|
|
645
|
+
...resetsAt === void 0 ? {} : { resetsAt }
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function spendControlReachedOf(value) {
|
|
649
|
+
if (value === void 0 || value === null) return void 0;
|
|
650
|
+
if (!record(value)) throw new Error("Codex returned malformed spend control");
|
|
651
|
+
if (value.reached === void 0 || value.reached === null) return void 0;
|
|
652
|
+
if (typeof value.reached !== "boolean") throw new Error("Codex returned an invalid spend-control state");
|
|
653
|
+
return value.reached;
|
|
654
|
+
}
|
|
655
|
+
function resetCreditsOf(value) {
|
|
656
|
+
if (value === void 0 || value === null) return void 0;
|
|
657
|
+
if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
|
|
658
|
+
return { availableCount: value.available_count };
|
|
659
|
+
}
|
|
660
|
+
/** Reduce the provider payload to a browser-safe quota projection. */
|
|
661
|
+
function parseCodexUsage(value) {
|
|
662
|
+
if (!record(value)) throw new Error("Codex returned a malformed usage response");
|
|
663
|
+
const rateLimits = [];
|
|
664
|
+
const seenLimitIds = /* @__PURE__ */ new Set();
|
|
665
|
+
const addLimit = (limit) => {
|
|
666
|
+
if (limit === void 0 || seenLimitIds.has(limit.id)) return;
|
|
667
|
+
seenLimitIds.add(limit.id);
|
|
668
|
+
rateLimits.push(limit);
|
|
669
|
+
};
|
|
670
|
+
addLimit(limitOf("codex", "Codex", value.rate_limit));
|
|
671
|
+
if (value.additional_rate_limits !== void 0 && value.additional_rate_limits !== null && !Array.isArray(value.additional_rate_limits)) throw new Error("Codex returned malformed additional rate limits");
|
|
672
|
+
for (const entry of value.additional_rate_limits ?? []) {
|
|
673
|
+
if (!record(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
|
|
674
|
+
if (entry.limit_name !== void 0 && entry.limit_name !== null && typeof entry.limit_name !== "string") throw new Error("Codex returned an invalid additional rate-limit name");
|
|
675
|
+
addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
|
|
676
|
+
}
|
|
677
|
+
addLimit(limitOf("code_review", "Code review", value.code_review_rate_limit));
|
|
678
|
+
const credits = creditsOf(value.credits);
|
|
679
|
+
const individualLimit = individualOf(value.spend_control);
|
|
680
|
+
const spendControlReached = spendControlReachedOf(value.spend_control);
|
|
681
|
+
const resetCredits = resetCreditsOf(value.rate_limit_reset_credits);
|
|
682
|
+
return {
|
|
683
|
+
rateLimits,
|
|
684
|
+
...credits === void 0 ? {} : { credits },
|
|
685
|
+
...individualLimit === void 0 ? {} : { individualLimit },
|
|
686
|
+
...spendControlReached === void 0 ? {} : { spendControlReached },
|
|
687
|
+
...resetCredits === void 0 ? {} : { resetCredits }
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const requestSignal = (signal, timeoutMs) => {
|
|
691
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
692
|
+
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
693
|
+
};
|
|
694
|
+
/**
|
|
695
|
+
* Read quota through the same refreshable OAuth lifecycle used by model turns.
|
|
696
|
+
* The browser receives only a parsed quota projection; bearer and account id
|
|
697
|
+
* are request-local host values. Concurrent settings polls share one request.
|
|
698
|
+
*/
|
|
699
|
+
function createCodexUsageReader(options) {
|
|
700
|
+
const getAuth = options.getAuth;
|
|
701
|
+
const readCredential = options.readCredential;
|
|
702
|
+
const fetchUsage = options.fetch ?? fetch;
|
|
703
|
+
const now = options.now ?? Date.now;
|
|
704
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
705
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
706
|
+
let cached;
|
|
707
|
+
let inFlight;
|
|
708
|
+
const load = async (signal) => {
|
|
709
|
+
const auth = await getAuth({ signal });
|
|
710
|
+
const credential = await readCredential({ signal });
|
|
711
|
+
const access = auth?.auth?.apiKey;
|
|
712
|
+
const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
|
|
713
|
+
if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
|
|
714
|
+
const response = await fetchUsage(CODEX_USAGE_URL, {
|
|
715
|
+
method: "GET",
|
|
716
|
+
redirect: "error",
|
|
717
|
+
headers: {
|
|
718
|
+
authorization: `Bearer ${access}`,
|
|
719
|
+
"chatgpt-account-id": accountId,
|
|
720
|
+
accept: "application/json",
|
|
721
|
+
"cache-control": "no-store",
|
|
722
|
+
"user-agent": "dsh-codex-subscription/0.2.8"
|
|
723
|
+
},
|
|
724
|
+
signal: requestSignal(signal, timeoutMs)
|
|
725
|
+
});
|
|
726
|
+
if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
|
|
727
|
+
let value;
|
|
728
|
+
try {
|
|
729
|
+
value = await response.json();
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error("ChatGPT returned an unreadable usage response");
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
...parseCodexUsage(value),
|
|
735
|
+
fetchedAt: now()
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
return Object.freeze({
|
|
739
|
+
read({ force = false, signal } = {}) {
|
|
740
|
+
if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return Promise.resolve(structuredClone(cached));
|
|
741
|
+
if (inFlight !== void 0) return inFlight.then(structuredClone);
|
|
742
|
+
const current = load(signal).then((value) => {
|
|
743
|
+
cached = structuredClone(value);
|
|
744
|
+
return structuredClone(value);
|
|
745
|
+
}).finally(() => {
|
|
746
|
+
if (inFlight === current) inFlight = void 0;
|
|
747
|
+
});
|
|
748
|
+
inFlight = current;
|
|
749
|
+
return current;
|
|
750
|
+
},
|
|
751
|
+
clear() {
|
|
752
|
+
cached = void 0;
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/index.js
|
|
758
|
+
const name = "codex-subscription";
|
|
759
|
+
const inject = [
|
|
760
|
+
"llm",
|
|
761
|
+
"credentials",
|
|
762
|
+
"connection",
|
|
763
|
+
"settings",
|
|
764
|
+
"web",
|
|
765
|
+
"loader"
|
|
766
|
+
];
|
|
767
|
+
const PROVIDER = "openai-codex";
|
|
768
|
+
const CREDENTIAL_REF = credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
|
|
769
|
+
const LEGACY_CREDENTIAL_REF = credentialRef("WSL043_OPENAI_CODEX_OAUTH");
|
|
770
|
+
const CHANNEL = "/codex-subscription";
|
|
771
|
+
const WEB_ENTRY_ID = "web";
|
|
772
|
+
const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
|
|
773
|
+
const publicError = (code, message) => ({
|
|
774
|
+
ok: false,
|
|
775
|
+
error: {
|
|
776
|
+
code,
|
|
777
|
+
message,
|
|
778
|
+
details: { issues: [] }
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, preferences }) {
|
|
782
|
+
return async (endpoint, payload, signal) => {
|
|
783
|
+
if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
|
|
784
|
+
signal.throwIfAborted();
|
|
785
|
+
if (endpoint === "preferences/update") {
|
|
786
|
+
const patch = {};
|
|
787
|
+
if (Object.hasOwn(payload ?? {}, "quickQuotaVisible")) {
|
|
788
|
+
if (typeof payload["quickQuotaVisible"] !== "boolean") return publicError("internal", "Invalid quick quota preference");
|
|
789
|
+
patch[QUICK_QUOTA_FIELD] = payload[QUICK_QUOTA_FIELD];
|
|
790
|
+
}
|
|
791
|
+
if (Object.hasOwn(payload ?? {}, "searchProvider")) {
|
|
792
|
+
if (!["dsh", "codex"].includes(payload["searchProvider"])) return publicError("internal", "Invalid search provider preference");
|
|
793
|
+
patch[SEARCH_PROVIDER_FIELD] = payload[SEARCH_PROVIDER_FIELD];
|
|
794
|
+
}
|
|
795
|
+
if (Object.keys(patch).length === 0) return publicError("internal", "Invalid preference update");
|
|
796
|
+
await preferences.update(patch);
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
ok: true,
|
|
800
|
+
value: preferences.status()
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (signal.aborted) throw error;
|
|
804
|
+
return publicError("internal", "Could not update preferences");
|
|
805
|
+
}
|
|
806
|
+
if (endpoint === "usage") try {
|
|
807
|
+
signal.throwIfAborted();
|
|
808
|
+
return {
|
|
809
|
+
ok: true,
|
|
810
|
+
value: await usageReader.read({
|
|
811
|
+
force: payload?.force === true,
|
|
812
|
+
signal
|
|
813
|
+
})
|
|
814
|
+
};
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (signal.aborted) throw error;
|
|
817
|
+
const known = /* @__PURE__ */ new Set(["ChatGPT subscription is not signed in", "ChatGPT sign-in needs to be renewed"]);
|
|
818
|
+
const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
|
|
819
|
+
return publicError("internal", message);
|
|
820
|
+
}
|
|
821
|
+
const result = await authHandler(endpoint, payload, signal);
|
|
822
|
+
if (endpoint === "logout" && result.ok === true) usageReader.clear();
|
|
823
|
+
return result;
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function createSearchProviderSwitcher(loader) {
|
|
827
|
+
const webEntry = () => [...loader.entries()].find((entry) => entry.options?.id === WEB_ENTRY_ID);
|
|
828
|
+
return Object.freeze({ async select(selection) {
|
|
829
|
+
const entry = webEntry();
|
|
830
|
+
const fiber = entry?.fiber;
|
|
831
|
+
if (entry === void 0 || fiber === void 0 || typeof fiber.update !== "function") throw new Error("DSH web runtime is unavailable");
|
|
832
|
+
const baseConfig = entry.options?.config ?? {};
|
|
833
|
+
const currentConfig = fiber.config ?? baseConfig;
|
|
834
|
+
const dshProvider = typeof baseConfig.searchProvider === "string" && baseConfig.searchProvider.length > 0 ? baseConfig.searchProvider : DSH_SEARCH_PROVIDER_FALLBACK;
|
|
835
|
+
const provider = selection === "codex" ? CODEX_SEARCH_PROVIDER_ID : dshProvider;
|
|
836
|
+
if (currentConfig.searchProvider === provider) return;
|
|
837
|
+
await fiber.update({
|
|
838
|
+
...currentConfig,
|
|
839
|
+
searchProvider: provider
|
|
840
|
+
}, true);
|
|
841
|
+
} });
|
|
842
|
+
}
|
|
843
|
+
function apply(ctx) {
|
|
844
|
+
const settings = ctx.settings.register(settingsNamespace(SETTINGS_NAMESPACE), z.object({
|
|
845
|
+
[QUICK_QUOTA_FIELD]: z.boolean().default(false),
|
|
846
|
+
[SEARCH_PROVIDER_FIELD]: z.union(["dsh", SEARCH_PROVIDER_CODEX]).default("dsh")
|
|
847
|
+
}));
|
|
848
|
+
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
849
|
+
const preferences = {
|
|
850
|
+
status: () => ({
|
|
851
|
+
[QUICK_QUOTA_FIELD]: settings.get()[QUICK_QUOTA_FIELD],
|
|
852
|
+
[SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
|
|
853
|
+
writable: ctx.settings.writable
|
|
854
|
+
}),
|
|
855
|
+
update: async (patch) => {
|
|
856
|
+
const previousSearchProvider = settings.get()[SEARCH_PROVIDER_FIELD];
|
|
857
|
+
await settings.update(patch);
|
|
858
|
+
if (patch["searchProvider"] === void 0) return;
|
|
859
|
+
try {
|
|
860
|
+
await searchProvider.select(patch[SEARCH_PROVIDER_FIELD]);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
await settings.update({ [SEARCH_PROVIDER_FIELD]: previousSearchProvider });
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF]);
|
|
868
|
+
const provider = openaiCodexSubscriptionProvider();
|
|
869
|
+
const authModels = createModels({ credentials: store });
|
|
870
|
+
authModels.setProvider(provider);
|
|
871
|
+
const profile = Object.freeze({
|
|
872
|
+
provider: PROVIDER,
|
|
873
|
+
displayName: "ChatGPT subscription",
|
|
874
|
+
piProvider: provider,
|
|
875
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
876
|
+
streamIdleTimeoutMs: 600 * 1e3,
|
|
877
|
+
cacheRetention: "short",
|
|
878
|
+
transport: "sse"
|
|
879
|
+
});
|
|
880
|
+
const profiles = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
|
|
881
|
+
const resolveAuth = () => authModels.getAuth(PROVIDER);
|
|
882
|
+
const adapter = new PiAiAdapter({
|
|
883
|
+
profiles: () => profiles,
|
|
884
|
+
resolveApiKey: async () => {
|
|
885
|
+
let resolved;
|
|
886
|
+
try {
|
|
887
|
+
resolved = await resolveAuth();
|
|
888
|
+
} catch {
|
|
889
|
+
throw new LlmError("ChatGPT subscription authorization failed", "AUTH_FAILED");
|
|
890
|
+
}
|
|
891
|
+
if (typeof resolved?.auth.apiKey !== "string" || resolved.auth.apiKey.length === 0) throw new LlmError("ChatGPT subscription is not signed in", "MISSING_CREDENTIAL");
|
|
892
|
+
return resolved.auth.apiKey;
|
|
893
|
+
},
|
|
894
|
+
resolveAttachments: () => ctx.get?.("attachments")
|
|
895
|
+
});
|
|
896
|
+
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
897
|
+
const currentAgent = () => ctx.get?.("agents")?.currentInitiator?.();
|
|
898
|
+
ctx.web.registerSearchProvider(createCodexSearchProvider({
|
|
899
|
+
getAuth: resolveAuth,
|
|
900
|
+
readCredential: (options) => store.read(PROVIDER, options),
|
|
901
|
+
resolveModel: () => {
|
|
902
|
+
const request = currentAgent()?.session.requestContext?.();
|
|
903
|
+
return request?.provider === PROVIDER ? request.model : void 0;
|
|
904
|
+
},
|
|
905
|
+
resolveSessionId: () => currentAgent()?.session.id
|
|
906
|
+
}));
|
|
907
|
+
ctx.effect(() => {
|
|
908
|
+
searchProvider.select(settings.get()[SEARCH_PROVIDER_FIELD]).catch((error) => {
|
|
909
|
+
ctx.logger?.warn?.("could not select the configured web search provider: %s", error.message);
|
|
910
|
+
});
|
|
911
|
+
}, "codex-subscription: search provider selection");
|
|
912
|
+
const coordinator = new CodexLoginCoordinator(createCodexAuthService(authModels, store));
|
|
913
|
+
const usageReader = createCodexUsageReader({
|
|
914
|
+
getAuth: resolveAuth,
|
|
915
|
+
readCredential: (options) => store.read(PROVIDER, options)
|
|
916
|
+
});
|
|
917
|
+
const handler = createSubscriptionRpcHandler({
|
|
918
|
+
authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
|
|
919
|
+
usageReader,
|
|
920
|
+
preferences
|
|
921
|
+
});
|
|
922
|
+
ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
export { CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionRpcHandler, inject, name, openCodexAuthUrl, parseCodexUsage };
|