dsh-codex-subscription 1.11.3 → 1.12.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.
- package/AGENTS.md +4 -4
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/compatibility.json +2 -1
- package/lib/client.js +161 -16
- package/lib/index.js +583 -59
- package/package.json +17 -17
package/lib/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as dshCredentials from "@deepseek-ai/dsh-credentials";
|
|
2
|
+
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
2
3
|
import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
7
|
import { execFile, spawn } from "node:child_process";
|
|
6
8
|
import { request } from "node:https";
|
|
7
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -10,13 +12,269 @@ import { promisify } from "node:util";
|
|
|
10
12
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
11
13
|
import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
12
14
|
import { createModels } from "@earendil-works/pi-ai";
|
|
13
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
14
15
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
15
16
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
16
17
|
import { constants } from "node:fs";
|
|
17
|
-
import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
18
|
+
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
18
19
|
import { dirname, join, resolve } from "node:path";
|
|
19
|
-
|
|
20
|
+
//#region src/account-vault.js
|
|
21
|
+
const VERSION = 1;
|
|
22
|
+
const DEFAULT_LABEL = "Account 1";
|
|
23
|
+
const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
24
|
+
function assertOAuthCredential$1(value) {
|
|
25
|
+
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 account vault received a malformed OAuth credential");
|
|
26
|
+
return clone$1(value);
|
|
27
|
+
}
|
|
28
|
+
function parseOAuthCredential$1(value) {
|
|
29
|
+
try {
|
|
30
|
+
return assertOAuthCredential$1(JSON.parse(value));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
|
|
33
|
+
throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function normalizeLabel(value) {
|
|
37
|
+
if (typeof value !== "string") throw new Error("Codex account label must be text");
|
|
38
|
+
const label = value.trim().replace(/\s+/gu, " ");
|
|
39
|
+
if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
|
|
40
|
+
return label;
|
|
41
|
+
}
|
|
42
|
+
function assertVaultRecord(record) {
|
|
43
|
+
if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
|
|
44
|
+
const ids = /* @__PURE__ */ new Set();
|
|
45
|
+
const accounts = record.payload.accounts.map((account) => {
|
|
46
|
+
if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
|
|
47
|
+
ids.add(account.id);
|
|
48
|
+
return {
|
|
49
|
+
id: account.id,
|
|
50
|
+
label: normalizeLabel(account.label),
|
|
51
|
+
credential: assertOAuthCredential$1(account.credential)
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
|
|
55
|
+
const legacyAccountId = record.payload.legacyAccountId;
|
|
56
|
+
if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
|
|
57
|
+
return {
|
|
58
|
+
version: VERSION,
|
|
59
|
+
activeId: record.payload.activeId,
|
|
60
|
+
legacyAccountId,
|
|
61
|
+
accounts
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const grant = (payload) => ({
|
|
65
|
+
kind: "grant",
|
|
66
|
+
payload
|
|
67
|
+
});
|
|
68
|
+
var PendingOAuthCredentialStore = class {
|
|
69
|
+
#credential;
|
|
70
|
+
async read(providerId) {
|
|
71
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
72
|
+
return clone$1(this.#credential);
|
|
73
|
+
}
|
|
74
|
+
async list() {
|
|
75
|
+
return this.#credential === void 0 ? [] : [{
|
|
76
|
+
providerId: "openai-codex",
|
|
77
|
+
type: "oauth"
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
async modify(providerId, update) {
|
|
81
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
82
|
+
const next = await update(clone$1(this.#credential));
|
|
83
|
+
if (next !== void 0) this.#credential = assertOAuthCredential$1(next);
|
|
84
|
+
return clone$1(this.#credential);
|
|
85
|
+
}
|
|
86
|
+
async delete(providerId) {
|
|
87
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
88
|
+
this.#credential = void 0;
|
|
89
|
+
}
|
|
90
|
+
credential() {
|
|
91
|
+
return clone$1(this.#credential);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Multi-account owner state stored in DSH's atomic plugin credential record.
|
|
96
|
+
* The old single-account reference remains as a rollback source and is kept in
|
|
97
|
+
* sync whenever that imported account rotates its refresh token.
|
|
98
|
+
*/
|
|
99
|
+
var DshOAuthAccountVault = class {
|
|
100
|
+
#tail = Promise.resolve();
|
|
101
|
+
constructor(credentials, options) {
|
|
102
|
+
if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
|
|
103
|
+
this.credentials = credentials;
|
|
104
|
+
this.key = options.key;
|
|
105
|
+
this.legacyRef = options.legacyRef;
|
|
106
|
+
this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
|
|
107
|
+
this.createId = options.createId ?? randomUUID;
|
|
108
|
+
this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
|
|
109
|
+
}
|
|
110
|
+
#enqueue(operation) {
|
|
111
|
+
const current = this.#tail.catch(() => void 0).then(operation);
|
|
112
|
+
this.#tail = current.catch(() => void 0);
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
async #legacyCredential() {
|
|
116
|
+
for (const ref of [this.legacyRef, ...this.legacyRefs]) {
|
|
117
|
+
const hit = await this.credentials.resolve(ref);
|
|
118
|
+
if (hit?.value === void 0 || hit.value === "") continue;
|
|
119
|
+
return {
|
|
120
|
+
ref,
|
|
121
|
+
credential: parseOAuthCredential$1(hit.value)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async #ensurePayload() {
|
|
126
|
+
const existing = await this.credentials.readRecord(this.key);
|
|
127
|
+
if (existing !== void 0) return assertVaultRecord(existing);
|
|
128
|
+
const legacy = await this.#legacyCredential();
|
|
129
|
+
if (legacy === void 0) return void 0;
|
|
130
|
+
const id = this.createId();
|
|
131
|
+
return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
|
|
132
|
+
if (current !== void 0) return Promise.resolve(current);
|
|
133
|
+
return Promise.resolve(grant({
|
|
134
|
+
version: VERSION,
|
|
135
|
+
activeId: id,
|
|
136
|
+
legacyAccountId: id,
|
|
137
|
+
accounts: [{
|
|
138
|
+
id,
|
|
139
|
+
label: DEFAULT_LABEL,
|
|
140
|
+
credential: legacy.credential
|
|
141
|
+
}]
|
|
142
|
+
}));
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
async #modifyPayload(update) {
|
|
146
|
+
await this.#ensurePayload();
|
|
147
|
+
let previousLegacy;
|
|
148
|
+
const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
|
|
149
|
+
if (current === void 0) throw new Error("Codex account vault is not signed in");
|
|
150
|
+
const payload = assertVaultRecord(current);
|
|
151
|
+
previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
152
|
+
const next = await update(clone$1(payload));
|
|
153
|
+
return grant(next);
|
|
154
|
+
}));
|
|
155
|
+
const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
156
|
+
try {
|
|
157
|
+
if (legacy === void 0) {
|
|
158
|
+
if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
|
|
159
|
+
} else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
|
|
160
|
+
} catch {
|
|
161
|
+
this.onLegacySyncFailure();
|
|
162
|
+
}
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
list() {
|
|
166
|
+
return this.#enqueue(async () => {
|
|
167
|
+
const payload = await this.#ensurePayload();
|
|
168
|
+
if (payload === void 0) return [];
|
|
169
|
+
return payload.accounts.map((account) => ({
|
|
170
|
+
id: account.id,
|
|
171
|
+
label: account.label,
|
|
172
|
+
active: account.id === payload.activeId,
|
|
173
|
+
expiresAt: account.credential.expires
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
readActive() {
|
|
178
|
+
return this.#enqueue(async () => {
|
|
179
|
+
const payload = await this.#ensurePayload();
|
|
180
|
+
return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
activeId() {
|
|
184
|
+
return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
|
|
185
|
+
}
|
|
186
|
+
add(label, credential) {
|
|
187
|
+
return this.#enqueue(async () => {
|
|
188
|
+
const normalizedLabel = normalizeLabel(label);
|
|
189
|
+
const validated = assertOAuthCredential$1(credential);
|
|
190
|
+
await this.#ensurePayload();
|
|
191
|
+
const id = this.createId();
|
|
192
|
+
const account = (await this.#modifyPayload((current) => ({
|
|
193
|
+
...current,
|
|
194
|
+
activeId: id,
|
|
195
|
+
accounts: [...current.accounts, {
|
|
196
|
+
id,
|
|
197
|
+
label: normalizedLabel,
|
|
198
|
+
credential: validated
|
|
199
|
+
}]
|
|
200
|
+
}))).accounts.find((candidate) => candidate.id === id);
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
label: account.label,
|
|
204
|
+
active: true,
|
|
205
|
+
expiresAt: account.credential.expires
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
select(id) {
|
|
210
|
+
return this.#enqueue(async () => {
|
|
211
|
+
await this.#modifyPayload((current) => {
|
|
212
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
213
|
+
return {
|
|
214
|
+
...current,
|
|
215
|
+
activeId: id
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
modifyActive(update) {
|
|
221
|
+
return this.#enqueue(async () => {
|
|
222
|
+
if (await this.#ensurePayload() === void 0) {
|
|
223
|
+
const initial = await update(void 0);
|
|
224
|
+
if (initial === void 0) return void 0;
|
|
225
|
+
const credential = assertOAuthCredential$1(initial);
|
|
226
|
+
await this.credentials.set(this.legacyRef, JSON.stringify(credential));
|
|
227
|
+
await this.#ensurePayload();
|
|
228
|
+
return clone$1(credential);
|
|
229
|
+
}
|
|
230
|
+
let result;
|
|
231
|
+
await this.#modifyPayload(async (current) => {
|
|
232
|
+
const index = current.accounts.findIndex((account) => account.id === current.activeId);
|
|
233
|
+
const previous = clone$1(current.accounts[index].credential);
|
|
234
|
+
const next = await update(previous);
|
|
235
|
+
if (next === void 0) {
|
|
236
|
+
result = previous;
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
const credential = assertOAuthCredential$1(next);
|
|
240
|
+
const accounts = [...current.accounts];
|
|
241
|
+
accounts[index] = {
|
|
242
|
+
...accounts[index],
|
|
243
|
+
credential
|
|
244
|
+
};
|
|
245
|
+
result = clone$1(credential);
|
|
246
|
+
return {
|
|
247
|
+
...current,
|
|
248
|
+
accounts
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
return result;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
deleteAll() {
|
|
255
|
+
return this.#enqueue(async () => {
|
|
256
|
+
await this.credentials.deleteRecord(this.key);
|
|
257
|
+
await this.credentials.unset(this.legacyRef);
|
|
258
|
+
for (const ref of this.legacyRefs) await this.credentials.unset(ref);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
remove(id) {
|
|
262
|
+
return this.#enqueue(async () => {
|
|
263
|
+
await this.#modifyPayload((current) => {
|
|
264
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
265
|
+
if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
|
|
266
|
+
const accounts = current.accounts.filter((account) => account.id !== id);
|
|
267
|
+
return {
|
|
268
|
+
...current,
|
|
269
|
+
activeId: current.activeId === id ? accounts[0].id : current.activeId,
|
|
270
|
+
legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
|
|
271
|
+
accounts
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
//#endregion
|
|
20
278
|
//#region src/credential-store.js
|
|
21
279
|
const PROVIDER$1 = "openai-codex";
|
|
22
280
|
const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
|
|
@@ -44,11 +302,15 @@ function parseOAuthCredential(value) {
|
|
|
44
302
|
*/
|
|
45
303
|
var DshOAuthCredentialStore = class {
|
|
46
304
|
#chains = /* @__PURE__ */ new Map();
|
|
47
|
-
constructor(credentials, ref, legacyRefs = []) {
|
|
305
|
+
constructor(credentials, ref, legacyRefs = [], options = {}) {
|
|
48
306
|
if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
|
|
307
|
+
const expirySkewMs = options.expirySkewMs ?? 0;
|
|
308
|
+
if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
|
|
49
309
|
this.credentials = credentials;
|
|
50
310
|
this.ref = ref;
|
|
51
311
|
this.legacyRefs = Object.freeze([...legacyRefs]);
|
|
312
|
+
this.expirySkewMs = expirySkewMs;
|
|
313
|
+
this.vault = options.vault;
|
|
52
314
|
}
|
|
53
315
|
#enqueue(providerId, operation, options) {
|
|
54
316
|
assertProvider(providerId);
|
|
@@ -66,6 +328,14 @@ var DshOAuthCredentialStore = class {
|
|
|
66
328
|
async #read(providerId, options) {
|
|
67
329
|
assertProvider(providerId);
|
|
68
330
|
abortIfNeeded(options);
|
|
331
|
+
if (this.vault !== void 0) {
|
|
332
|
+
const current = await this.vault.readActive();
|
|
333
|
+
if (current === void 0) return void 0;
|
|
334
|
+
return this.expirySkewMs === 0 ? current : {
|
|
335
|
+
...current,
|
|
336
|
+
expires: current.expires - this.expirySkewMs
|
|
337
|
+
};
|
|
338
|
+
}
|
|
69
339
|
let hit = await this.credentials.resolve(this.ref);
|
|
70
340
|
if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
|
|
71
341
|
const legacy = await this.credentials.resolve(legacyRef);
|
|
@@ -78,7 +348,11 @@ var DshOAuthCredentialStore = class {
|
|
|
78
348
|
}
|
|
79
349
|
abortIfNeeded(options);
|
|
80
350
|
if (hit?.value === void 0 || hit.value === "") return void 0;
|
|
81
|
-
|
|
351
|
+
const credential = parseOAuthCredential(hit.value);
|
|
352
|
+
return this.expirySkewMs === 0 ? credential : {
|
|
353
|
+
...credential,
|
|
354
|
+
expires: credential.expires - this.expirySkewMs
|
|
355
|
+
};
|
|
82
356
|
}
|
|
83
357
|
read(providerId, options) {
|
|
84
358
|
return this.#enqueue(providerId, () => this.#read(providerId, options), options);
|
|
@@ -92,6 +366,18 @@ var DshOAuthCredentialStore = class {
|
|
|
92
366
|
}
|
|
93
367
|
modify(providerId, update, options) {
|
|
94
368
|
return this.#enqueue(providerId, async () => {
|
|
369
|
+
if (this.vault !== void 0) {
|
|
370
|
+
const next = await this.vault.modifyActive(async (current) => {
|
|
371
|
+
const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
|
|
372
|
+
...current,
|
|
373
|
+
expires: current.expires - this.expirySkewMs
|
|
374
|
+
};
|
|
375
|
+
const updated = await update(clone(visible));
|
|
376
|
+
return updated === void 0 ? void 0 : assertOAuthCredential(updated);
|
|
377
|
+
});
|
|
378
|
+
abortIfNeeded(options);
|
|
379
|
+
return clone(next);
|
|
380
|
+
}
|
|
95
381
|
const current = await this.#read(providerId, options);
|
|
96
382
|
const next = await update(clone(current));
|
|
97
383
|
abortIfNeeded(options);
|
|
@@ -105,6 +391,11 @@ var DshOAuthCredentialStore = class {
|
|
|
105
391
|
}
|
|
106
392
|
delete(providerId, options) {
|
|
107
393
|
return this.#enqueue(providerId, async () => {
|
|
394
|
+
if (this.vault !== void 0) {
|
|
395
|
+
await this.vault.deleteAll();
|
|
396
|
+
abortIfNeeded(options);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
108
399
|
await this.credentials.unset(this.ref);
|
|
109
400
|
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
110
401
|
abortIfNeeded(options);
|
|
@@ -114,23 +405,49 @@ var DshOAuthCredentialStore = class {
|
|
|
114
405
|
/** Return only account state that is safe to expose to the browser client. */
|
|
115
406
|
function createCodexAuthService(models, store, options = {}) {
|
|
116
407
|
const runLogin = options.runLogin ?? ((run) => run());
|
|
408
|
+
const accountVault = options.accountVault;
|
|
409
|
+
const createLoginModels = options.createLoginModels;
|
|
410
|
+
const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
|
|
117
411
|
return Object.freeze({
|
|
118
412
|
async status(options) {
|
|
119
413
|
const current = await store.read(PROVIDER$1, options);
|
|
414
|
+
const accounts = await accountVault?.list();
|
|
120
415
|
if (current === void 0) return {
|
|
121
416
|
authenticated: false,
|
|
122
|
-
provider: PROVIDER$1
|
|
417
|
+
provider: PROVIDER$1,
|
|
418
|
+
...accounts === void 0 ? {} : { accounts }
|
|
123
419
|
};
|
|
124
420
|
return {
|
|
125
421
|
authenticated: true,
|
|
126
422
|
provider: PROVIDER$1,
|
|
127
423
|
type: "oauth",
|
|
128
|
-
expiresAt: current.expires
|
|
424
|
+
expiresAt: current.expires,
|
|
425
|
+
...accounts === void 0 ? {} : { accounts }
|
|
129
426
|
};
|
|
130
427
|
},
|
|
131
|
-
login(interaction) {
|
|
428
|
+
login(interaction, input = {}) {
|
|
429
|
+
if (input.label !== void 0) {
|
|
430
|
+
if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
|
|
431
|
+
return runLogin(async () => {
|
|
432
|
+
const pending = createPendingStore();
|
|
433
|
+
await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
|
|
434
|
+
const credential = pending.credential();
|
|
435
|
+
if (credential === void 0) throw new Error("Codex login did not return credentials");
|
|
436
|
+
await accountVault.add(input.label, credential);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
132
439
|
return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
|
|
133
440
|
},
|
|
441
|
+
async select(id) {
|
|
442
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
443
|
+
await accountVault.select(id);
|
|
444
|
+
return this.status();
|
|
445
|
+
},
|
|
446
|
+
async remove(id) {
|
|
447
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
448
|
+
await accountVault.remove(id);
|
|
449
|
+
return this.status();
|
|
450
|
+
},
|
|
134
451
|
logout(options) {
|
|
135
452
|
return models.logout(PROVIDER$1, options);
|
|
136
453
|
}
|
|
@@ -256,8 +573,9 @@ var CodexLoginCoordinator = class {
|
|
|
256
573
|
...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
|
|
257
574
|
};
|
|
258
575
|
}
|
|
259
|
-
async start({ method }) {
|
|
576
|
+
async start({ method, label }) {
|
|
260
577
|
if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
|
|
578
|
+
if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
|
|
261
579
|
const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
|
|
262
580
|
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
|
|
263
581
|
active.view = {
|
|
@@ -342,7 +660,7 @@ var CodexLoginCoordinator = class {
|
|
|
342
660
|
publishReady();
|
|
343
661
|
}
|
|
344
662
|
};
|
|
345
|
-
session.run = Promise.resolve().then(() => this.auth.login(interaction)).then(async () => {
|
|
663
|
+
session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
|
|
346
664
|
if (controller.signal.aborted) return;
|
|
347
665
|
const status = await this.auth.status();
|
|
348
666
|
session.view = {
|
|
@@ -365,6 +683,7 @@ var CodexLoginCoordinator = class {
|
|
|
365
683
|
return;
|
|
366
684
|
}
|
|
367
685
|
try {
|
|
686
|
+
if (label !== void 0) throw error;
|
|
368
687
|
const status = await this.auth.status();
|
|
369
688
|
if (status.authenticated === true) {
|
|
370
689
|
session.view = {
|
|
@@ -433,6 +752,12 @@ var CodexLoginCoordinator = class {
|
|
|
433
752
|
await this.auth.logout(options);
|
|
434
753
|
return this.accountStatus(options);
|
|
435
754
|
}
|
|
755
|
+
async selectAccount(id) {
|
|
756
|
+
return publicClone(await this.auth.select(id));
|
|
757
|
+
}
|
|
758
|
+
async removeAccount(id) {
|
|
759
|
+
return publicClone(await this.auth.remove(id));
|
|
760
|
+
}
|
|
436
761
|
};
|
|
437
762
|
/** Map the loopback-only DSH Connection channel onto the coordinator. */
|
|
438
763
|
function createCodexRpcHandler(coordinator, options = {}) {
|
|
@@ -443,7 +768,10 @@ function createCodexRpcHandler(coordinator, options = {}) {
|
|
|
443
768
|
const input = asObject(payload);
|
|
444
769
|
if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
|
|
445
770
|
if (endpoint === "login/start") {
|
|
446
|
-
const started = await coordinator.start({
|
|
771
|
+
const started = await coordinator.start({
|
|
772
|
+
method: input.method,
|
|
773
|
+
label: input.label
|
|
774
|
+
});
|
|
447
775
|
if (input.openExternal !== true) return ok(started);
|
|
448
776
|
const url = started.authUrl ?? started.deviceCode?.verificationUri;
|
|
449
777
|
if (typeof url !== "string" || openExternal === void 0) return ok({
|
|
@@ -470,6 +798,8 @@ function createCodexRpcHandler(coordinator, options = {}) {
|
|
|
470
798
|
}));
|
|
471
799
|
if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
|
|
472
800
|
if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
|
|
801
|
+
if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
|
|
802
|
+
if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
|
|
473
803
|
return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
|
|
474
804
|
} catch (error) {
|
|
475
805
|
if (signal.aborted) throw error;
|
|
@@ -935,7 +1265,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
935
1265
|
}
|
|
936
1266
|
//#endregion
|
|
937
1267
|
//#region src/version.js
|
|
938
|
-
const PACKAGE_VERSION = "1.
|
|
1268
|
+
const PACKAGE_VERSION = "1.12.0";
|
|
939
1269
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
940
1270
|
//#endregion
|
|
941
1271
|
//#region src/model-catalog.js
|
|
@@ -1836,6 +2166,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
1836
2166
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
1837
2167
|
const DEFAULT_TTL_MS = 6e4;
|
|
1838
2168
|
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
2169
|
+
const DEFAULT_FAILURE_TTL_MS = 5e3;
|
|
2170
|
+
const DEFAULT_MAX_RETRY_AFTER_MS = 5 * 6e4;
|
|
1839
2171
|
const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1840
2172
|
function windowOf(value) {
|
|
1841
2173
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -1951,6 +2283,15 @@ const requestSignal$1 = (signal, timeoutMs) => {
|
|
|
1951
2283
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
1952
2284
|
return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
1953
2285
|
};
|
|
2286
|
+
function retryAfterMs(response, now, maximum) {
|
|
2287
|
+
if (response.status !== 429) return void 0;
|
|
2288
|
+
const raw = response.headers?.get?.("retry-after")?.trim();
|
|
2289
|
+
if (!raw) return void 0;
|
|
2290
|
+
const seconds = Number(raw);
|
|
2291
|
+
const delay = Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : Date.parse(raw) - now();
|
|
2292
|
+
if (!Number.isFinite(delay) || delay < 0) return void 0;
|
|
2293
|
+
return Math.min(delay, maximum);
|
|
2294
|
+
}
|
|
1954
2295
|
/**
|
|
1955
2296
|
* Read quota through the same refreshable OAuth lifecycle used by model turns.
|
|
1956
2297
|
* The browser receives only a parsed quota projection; bearer and account id
|
|
@@ -1963,7 +2304,10 @@ function createCodexUsageReader(options) {
|
|
|
1963
2304
|
const now = options.now ?? Date.now;
|
|
1964
2305
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1965
2306
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
2307
|
+
const failureTtlMs = options.failureTtlMs ?? DEFAULT_FAILURE_TTL_MS;
|
|
2308
|
+
const maxRetryAfterMs = options.maxRetryAfterMs ?? DEFAULT_MAX_RETRY_AFTER_MS;
|
|
1966
2309
|
let cached;
|
|
2310
|
+
let failed;
|
|
1967
2311
|
let inFlight;
|
|
1968
2312
|
let generation = 0;
|
|
1969
2313
|
const load = async (signal) => {
|
|
@@ -1984,7 +2328,11 @@ function createCodexUsageReader(options) {
|
|
|
1984
2328
|
},
|
|
1985
2329
|
signal: requestSignal$1(signal, timeoutMs)
|
|
1986
2330
|
});
|
|
1987
|
-
if (!response.ok)
|
|
2331
|
+
if (!response.ok) {
|
|
2332
|
+
const error = /* @__PURE__ */ new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
|
|
2333
|
+
Object.defineProperty(error, "retryAfterMs", { value: retryAfterMs(response, now, maxRetryAfterMs) });
|
|
2334
|
+
throw error;
|
|
2335
|
+
}
|
|
1988
2336
|
let value;
|
|
1989
2337
|
try {
|
|
1990
2338
|
value = await response.json();
|
|
@@ -1998,12 +2346,25 @@ function createCodexUsageReader(options) {
|
|
|
1998
2346
|
};
|
|
1999
2347
|
return Object.freeze({
|
|
2000
2348
|
read({ force = false, signal } = {}) {
|
|
2349
|
+
if (failed !== void 0 && now() < failed.retryAt) return Promise.reject(new Error(failed.message));
|
|
2001
2350
|
if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return Promise.resolve(structuredClone(cached));
|
|
2002
2351
|
if (inFlight !== void 0) return inFlight.then(structuredClone);
|
|
2003
2352
|
const currentGeneration = generation;
|
|
2004
2353
|
const current = load(signal).then((value) => {
|
|
2005
|
-
if (generation === currentGeneration)
|
|
2354
|
+
if (generation === currentGeneration) {
|
|
2355
|
+
cached = structuredClone(value);
|
|
2356
|
+
failed = void 0;
|
|
2357
|
+
}
|
|
2006
2358
|
return structuredClone(value);
|
|
2359
|
+
}).catch((error) => {
|
|
2360
|
+
if (generation === currentGeneration && error?.name !== "AbortError") {
|
|
2361
|
+
const delay = Number.isFinite(error?.retryAfterMs) ? error.retryAfterMs : failureTtlMs;
|
|
2362
|
+
failed = {
|
|
2363
|
+
message: error instanceof Error ? error.message : "ChatGPT usage request failed",
|
|
2364
|
+
retryAt: now() + delay
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
throw error;
|
|
2007
2368
|
}).finally(() => {
|
|
2008
2369
|
if (inFlight === current) inFlight = void 0;
|
|
2009
2370
|
});
|
|
@@ -2013,6 +2374,7 @@ function createCodexUsageReader(options) {
|
|
|
2013
2374
|
clear() {
|
|
2014
2375
|
generation += 1;
|
|
2015
2376
|
cached = void 0;
|
|
2377
|
+
failed = void 0;
|
|
2016
2378
|
inFlight = void 0;
|
|
2017
2379
|
}
|
|
2018
2380
|
});
|
|
@@ -2021,18 +2383,33 @@ function createCodexUsageReader(options) {
|
|
|
2021
2383
|
//#region src/quota-forecast.js
|
|
2022
2384
|
const HOUR_MS = 3600 * 1e3;
|
|
2023
2385
|
const HISTORY_MS = 24 * HOUR_MS;
|
|
2024
|
-
const
|
|
2025
|
-
const MIN_CONSUMED_PERCENT = 1;
|
|
2386
|
+
const MIN_SAMPLES = 3;
|
|
2026
2387
|
const PLATEAU_SAMPLE_MS = 900 * 1e3;
|
|
2027
2388
|
const finite = (value) => Number.isFinite(Number(value));
|
|
2028
2389
|
const clampPercent = (value) => Math.max(0, Math.min(100, Number(value)));
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2390
|
+
const cleanSegment = (value) => String(value ?? "default").slice(0, 96);
|
|
2391
|
+
const keyFor = (window, context = {}) => JSON.stringify([
|
|
2392
|
+
cleanSegment(context.scope),
|
|
2393
|
+
cleanSegment(context.limitId ?? "codex"),
|
|
2394
|
+
Number(window.windowSeconds) || "limit"
|
|
2395
|
+
]);
|
|
2396
|
+
const median = (values) => {
|
|
2397
|
+
const ordered = [...values].sort((a, b) => a - b);
|
|
2398
|
+
const middle = Math.floor(ordered.length / 2);
|
|
2399
|
+
return ordered.length % 2 === 0 ? (ordered[middle - 1] + ordered[middle]) / 2 : ordered[middle];
|
|
2400
|
+
};
|
|
2401
|
+
function requiredSpanMs(consumedPercent) {
|
|
2402
|
+
if (consumedPercent >= 2) return 300 * 1e3;
|
|
2403
|
+
if (consumedPercent >= 1) return 600 * 1e3;
|
|
2404
|
+
if (consumedPercent >= .5) return 1200 * 1e3;
|
|
2405
|
+
return 1800 * 1e3;
|
|
2406
|
+
}
|
|
2407
|
+
function observeQuotaForecast(state, windows, now = Date.now(), context = {}) {
|
|
2031
2408
|
const next = { windows: { ...state?.windows ?? {} } };
|
|
2032
2409
|
let changed = false;
|
|
2033
2410
|
for (const window of windows ?? []) {
|
|
2034
2411
|
if (!finite(window?.remainingPercent)) continue;
|
|
2035
|
-
const key = keyFor(window);
|
|
2412
|
+
const key = keyFor(window, context);
|
|
2036
2413
|
const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
|
|
2037
2414
|
const remainingPercent = Math.round(clampPercent(window.remainingPercent) * 1e4) / 1e4;
|
|
2038
2415
|
const previous = next.windows[key];
|
|
@@ -2062,14 +2439,14 @@ function observeQuotaForecast(state, windows, now = Date.now()) {
|
|
|
2062
2439
|
changed
|
|
2063
2440
|
};
|
|
2064
2441
|
}
|
|
2065
|
-
function estimateQuotaForecast(state, window, now = Date.now()) {
|
|
2442
|
+
function estimateQuotaForecast(state, window, now = Date.now(), context = {}) {
|
|
2066
2443
|
if (!finite(window?.remainingPercent)) return { status: "calibrating" };
|
|
2067
|
-
const record = state?.windows?.[keyFor(window)];
|
|
2444
|
+
const record = state?.windows?.[keyFor(window, context)];
|
|
2068
2445
|
if (record === void 0) return { status: "calibrating" };
|
|
2069
2446
|
const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
|
|
2070
2447
|
if (record.resetsAt === null !== (resetsAt === null) || resetsAt !== null && Math.abs(record.resetsAt - resetsAt) > 300) return { status: "calibrating" };
|
|
2071
2448
|
const samples = record.samples.filter((sample) => sample.at >= now - HISTORY_MS && sample.at <= now + 6e4);
|
|
2072
|
-
if (samples.length <
|
|
2449
|
+
if (samples.length < MIN_SAMPLES) return {
|
|
2073
2450
|
status: "calibrating",
|
|
2074
2451
|
sampleCount: samples.length
|
|
2075
2452
|
};
|
|
@@ -2077,77 +2454,190 @@ function estimateQuotaForecast(state, window, now = Date.now()) {
|
|
|
2077
2454
|
const last = samples.at(-1);
|
|
2078
2455
|
const spanMs = last.at - first.at;
|
|
2079
2456
|
const consumedPercent = Math.max(0, first.remainingPercent - last.remainingPercent);
|
|
2080
|
-
if (spanMs <
|
|
2457
|
+
if (spanMs < requiredSpanMs(consumedPercent)) return {
|
|
2081
2458
|
status: "calibrating",
|
|
2082
2459
|
sampleCount: samples.length,
|
|
2083
2460
|
observedSpanMs: spanMs,
|
|
2084
2461
|
consumedPercent
|
|
2085
2462
|
};
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const meanY = weighted.reduce((sum, point) => sum + point.y * point.weight, 0) / totalWeight;
|
|
2095
|
-
const numerator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) * (point.y - meanY), 0);
|
|
2096
|
-
const denominator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) ** 2, 0);
|
|
2097
|
-
const pacePerHour = denominator > 0 ? numerator / denominator : 0;
|
|
2463
|
+
const slopes = [];
|
|
2464
|
+
for (let left = 0; left < samples.length - 1; left += 1) for (let right = left + 1; right < samples.length; right += 1) {
|
|
2465
|
+
const hours = (samples[right].at - samples[left].at) / HOUR_MS;
|
|
2466
|
+
if (hours <= 0) continue;
|
|
2467
|
+
slopes.push((samples[left].remainingPercent - samples[right].remainingPercent) / hours);
|
|
2468
|
+
}
|
|
2469
|
+
const positive = slopes.filter((value) => Number.isFinite(value) && value >= 0);
|
|
2470
|
+
const pacePerHour = positive.length === 0 ? 0 : median(positive);
|
|
2098
2471
|
if (!Number.isFinite(pacePerHour) || pacePerHour < .02) return {
|
|
2099
2472
|
status: "idle",
|
|
2100
|
-
pacePerHour: 0
|
|
2473
|
+
pacePerHour: 0,
|
|
2474
|
+
sampleCount: samples.length,
|
|
2475
|
+
observedSpanMs: spanMs,
|
|
2476
|
+
consumedPercent
|
|
2101
2477
|
};
|
|
2102
|
-
const
|
|
2478
|
+
const deviations = positive.map((value) => Math.abs(value - pacePerHour));
|
|
2479
|
+
const uncertaintyPerHour = deviations.length === 0 ? 0 : median(deviations) * 1.4826;
|
|
2480
|
+
const lowerPacePerHour = Math.max(.02, pacePerHour - uncertaintyPerHour);
|
|
2481
|
+
const upperPacePerHour = pacePerHour + uncertaintyPerHour;
|
|
2482
|
+
const remaining = clampPercent(window.remainingPercent);
|
|
2483
|
+
const runwaySeconds = remaining / pacePerHour * 3600;
|
|
2484
|
+
const runwayMinSeconds = remaining / upperPacePerHour * 3600;
|
|
2485
|
+
const runwayMaxSeconds = remaining / lowerPacePerHour * 3600;
|
|
2103
2486
|
const resetSeconds = resetsAt === null ? null : Math.max(0, resetsAt - now / 1e3);
|
|
2104
2487
|
return {
|
|
2105
2488
|
status: "ready",
|
|
2106
2489
|
pacePerHour,
|
|
2490
|
+
uncertaintyPerHour,
|
|
2107
2491
|
runwaySeconds,
|
|
2108
|
-
|
|
2492
|
+
runwayMinSeconds,
|
|
2493
|
+
runwayMaxSeconds,
|
|
2494
|
+
survivesReset: resetSeconds !== null && runwayMinSeconds >= resetSeconds,
|
|
2109
2495
|
sampleCount: samples.length,
|
|
2110
2496
|
observedSpanMs: spanMs,
|
|
2111
2497
|
consumedPercent
|
|
2112
2498
|
};
|
|
2113
2499
|
}
|
|
2114
|
-
function forecastUsage(usage, state = { windows: {} }, now = Date.now()) {
|
|
2115
|
-
|
|
2500
|
+
function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options = {}) {
|
|
2501
|
+
let nextState = state;
|
|
2502
|
+
let changed = false;
|
|
2503
|
+
const rateLimits = (usage?.rateLimits ?? []).map((limit) => {
|
|
2504
|
+
const context = {
|
|
2505
|
+
scope: options.scope,
|
|
2506
|
+
limitId: limit.id
|
|
2507
|
+
};
|
|
2508
|
+
const observed = observeQuotaForecast(nextState, limit.windows, now, context);
|
|
2509
|
+
nextState = observed.state;
|
|
2510
|
+
changed ||= observed.changed;
|
|
2511
|
+
return {
|
|
2512
|
+
...limit,
|
|
2513
|
+
windows: limit.windows.map((window) => ({
|
|
2514
|
+
...window,
|
|
2515
|
+
forecast: estimateQuotaForecast(nextState, window, now, context)
|
|
2516
|
+
}))
|
|
2517
|
+
};
|
|
2518
|
+
});
|
|
2116
2519
|
return {
|
|
2117
|
-
state:
|
|
2118
|
-
changed
|
|
2520
|
+
state: nextState,
|
|
2521
|
+
changed,
|
|
2119
2522
|
usage: {
|
|
2120
2523
|
...usage,
|
|
2121
|
-
rateLimits
|
|
2122
|
-
...limit,
|
|
2123
|
-
windows: limit.windows.map((window) => ({
|
|
2124
|
-
...window,
|
|
2125
|
-
forecast: estimateQuotaForecast(observed.state, window, now)
|
|
2126
|
-
}))
|
|
2127
|
-
})
|
|
2524
|
+
rateLimits
|
|
2128
2525
|
}
|
|
2129
2526
|
};
|
|
2130
2527
|
}
|
|
2131
|
-
function createQuotaForecastReader({ reader, enabled, now = Date.now }) {
|
|
2528
|
+
function createQuotaForecastReader({ reader, enabled, now = Date.now, scope = () => "default", stateStore }) {
|
|
2132
2529
|
let state = { windows: {} };
|
|
2530
|
+
let loaded = false;
|
|
2531
|
+
const load = async () => {
|
|
2532
|
+
if (loaded) return;
|
|
2533
|
+
loaded = true;
|
|
2534
|
+
const restored = await stateStore?.load?.();
|
|
2535
|
+
if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
|
|
2536
|
+
};
|
|
2133
2537
|
return Object.freeze({
|
|
2134
2538
|
async read(options) {
|
|
2135
2539
|
const usage = await reader.read(options);
|
|
2540
|
+
await load();
|
|
2136
2541
|
if (!enabled()) {
|
|
2137
2542
|
state = { windows: {} };
|
|
2543
|
+
await stateStore?.clear?.();
|
|
2138
2544
|
return usage;
|
|
2139
2545
|
}
|
|
2140
|
-
const forecast = forecastUsage(usage, state, now());
|
|
2546
|
+
const forecast = forecastUsage(usage, state, now(), { scope: await scope() });
|
|
2141
2547
|
state = forecast.state;
|
|
2548
|
+
if (forecast.changed) await stateStore?.save?.(state);
|
|
2142
2549
|
return forecast.usage;
|
|
2143
2550
|
},
|
|
2144
|
-
clear() {
|
|
2551
|
+
async clear() {
|
|
2145
2552
|
state = { windows: {} };
|
|
2553
|
+
loaded = true;
|
|
2146
2554
|
reader.clear();
|
|
2555
|
+
await stateStore?.clear?.();
|
|
2556
|
+
},
|
|
2557
|
+
clearCache() {
|
|
2558
|
+
reader.clear();
|
|
2559
|
+
},
|
|
2560
|
+
async clearScope(targetScope) {
|
|
2561
|
+
await load();
|
|
2562
|
+
const prefix = `[${JSON.stringify(cleanSegment(targetScope))},`;
|
|
2563
|
+
state = { windows: Object.fromEntries(Object.entries(state.windows).filter(([key]) => !key.startsWith(prefix))) };
|
|
2564
|
+
await stateStore?.save?.(state);
|
|
2147
2565
|
}
|
|
2148
2566
|
});
|
|
2149
2567
|
}
|
|
2150
2568
|
//#endregion
|
|
2569
|
+
//#region src/quota-forecast-store.js
|
|
2570
|
+
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
2571
|
+
const MAX_WINDOWS = 128;
|
|
2572
|
+
const MAX_SAMPLES = 192;
|
|
2573
|
+
function sanitize(value) {
|
|
2574
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.windows === null || typeof value.windows !== "object" || Array.isArray(value.windows)) return void 0;
|
|
2575
|
+
const entries = Object.entries(value.windows);
|
|
2576
|
+
if (entries.length > MAX_WINDOWS) return void 0;
|
|
2577
|
+
const windows = {};
|
|
2578
|
+
for (const [key, record] of entries) {
|
|
2579
|
+
if (key.length === 0 || key.length > 320 || record === null || typeof record !== "object" || !Array.isArray(record.samples) || record.samples.length > MAX_SAMPLES) return void 0;
|
|
2580
|
+
const resetsAt = record.resetsAt === null ? null : Number(record.resetsAt);
|
|
2581
|
+
if (resetsAt !== null && !Number.isFinite(resetsAt)) return void 0;
|
|
2582
|
+
const samples = [];
|
|
2583
|
+
for (const sample of record.samples) {
|
|
2584
|
+
const at = Number(sample?.at);
|
|
2585
|
+
const remainingPercent = Number(sample?.remainingPercent);
|
|
2586
|
+
if (!Number.isFinite(at) || !Number.isFinite(remainingPercent) || remainingPercent < 0 || remainingPercent > 100) return void 0;
|
|
2587
|
+
samples.push({
|
|
2588
|
+
at,
|
|
2589
|
+
remainingPercent
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
windows[key] = {
|
|
2593
|
+
resetsAt,
|
|
2594
|
+
samples
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
return { windows };
|
|
2598
|
+
}
|
|
2599
|
+
var QuotaForecastStateStore = class {
|
|
2600
|
+
constructor({ filename, maxBytes = DEFAULT_MAX_BYTES }) {
|
|
2601
|
+
this.filename = filename;
|
|
2602
|
+
this.maxBytes = maxBytes;
|
|
2603
|
+
}
|
|
2604
|
+
async load() {
|
|
2605
|
+
try {
|
|
2606
|
+
if ((await stat(this.filename)).size > this.maxBytes) return void 0;
|
|
2607
|
+
return sanitize(JSON.parse(await readFile(this.filename, "utf8")));
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return void 0;
|
|
2610
|
+
throw error;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
async save(state) {
|
|
2614
|
+
const safe = sanitize(state);
|
|
2615
|
+
if (safe === void 0) throw new Error("Refusing to persist malformed quota forecast state");
|
|
2616
|
+
const data = `${JSON.stringify(safe)}\n`;
|
|
2617
|
+
if (Buffer.byteLength(data) > this.maxBytes) throw new Error("Quota forecast state is too large");
|
|
2618
|
+
await mkdir(dirname(this.filename), {
|
|
2619
|
+
recursive: true,
|
|
2620
|
+
mode: 448
|
|
2621
|
+
});
|
|
2622
|
+
const temporary = `${this.filename}.${process.pid}.${randomUUID()}.tmp`;
|
|
2623
|
+
let handle;
|
|
2624
|
+
try {
|
|
2625
|
+
handle = await open(temporary, "wx", 384);
|
|
2626
|
+
await handle.writeFile(data);
|
|
2627
|
+
await handle.sync();
|
|
2628
|
+
await handle.close();
|
|
2629
|
+
handle = void 0;
|
|
2630
|
+
await rename(temporary, this.filename);
|
|
2631
|
+
} finally {
|
|
2632
|
+
await handle?.close().catch(() => void 0);
|
|
2633
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async clear() {
|
|
2637
|
+
await rm(this.filename, { force: true });
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
//#endregion
|
|
2151
2641
|
//#region src/reset-credits.js
|
|
2152
2642
|
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
2153
2643
|
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
@@ -2404,8 +2894,10 @@ const inject = [
|
|
|
2404
2894
|
"attachments"
|
|
2405
2895
|
];
|
|
2406
2896
|
const PROVIDER = "openai-codex";
|
|
2407
|
-
const
|
|
2408
|
-
const
|
|
2897
|
+
const OAUTH_EXPIRY_SKEW_MS = 6e4;
|
|
2898
|
+
const CREDENTIAL_REF = dshCredentials.credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
|
|
2899
|
+
const LEGACY_CREDENTIAL_REF = dshCredentials.credentialRef("WSL043_OPENAI_CODEX_OAUTH");
|
|
2900
|
+
const ACCOUNT_VAULT_KEY = typeof dshCredentials.credentialKey === "function" ? dshCredentials.credentialKey("codex-subscription", "accounts") : void 0;
|
|
2409
2901
|
const CHANNEL = "/codex-subscription";
|
|
2410
2902
|
const WEB_ENTRY_ID = "web";
|
|
2411
2903
|
const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
|
|
@@ -2552,10 +3044,16 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
2552
3044
|
return publicError("internal", message);
|
|
2553
3045
|
}
|
|
2554
3046
|
const result = await authHandler(endpoint, payload, signal);
|
|
3047
|
+
if (endpoint === "account/remove" && result.ok === true && typeof payload?.id === "string") await usageReader.clearScope(payload.id);
|
|
2555
3048
|
if (endpoint === "logout" && result.ok === true) {
|
|
2556
|
-
usageReader.clear();
|
|
3049
|
+
await usageReader.clear();
|
|
2557
3050
|
resetCreditService.clear();
|
|
2558
3051
|
modelCatalog?.clear();
|
|
3052
|
+
} else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
|
|
3053
|
+
usageReader.clearCache();
|
|
3054
|
+
resetCreditService.clear();
|
|
3055
|
+
modelCatalog?.clear();
|
|
3056
|
+
modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
2559
3057
|
} else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
2560
3058
|
return result;
|
|
2561
3059
|
};
|
|
@@ -2616,7 +3114,15 @@ function apply(ctx) {
|
|
|
2616
3114
|
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
2617
3115
|
const network = createCodexNetworkTransport();
|
|
2618
3116
|
const originalImages = new OriginalImageStore();
|
|
2619
|
-
const
|
|
3117
|
+
const accountVault = ACCOUNT_VAULT_KEY !== void 0 && typeof ctx.credentials.readRecord === "function" && typeof ctx.credentials.modifyRecord === "function" && typeof ctx.credentials.deleteRecord === "function" ? new DshOAuthAccountVault(ctx.credentials, {
|
|
3118
|
+
key: ACCOUNT_VAULT_KEY,
|
|
3119
|
+
legacyRef: CREDENTIAL_REF,
|
|
3120
|
+
legacyRefs: [LEGACY_CREDENTIAL_REF]
|
|
3121
|
+
}) : void 0;
|
|
3122
|
+
const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF], {
|
|
3123
|
+
expirySkewMs: OAUTH_EXPIRY_SKEW_MS,
|
|
3124
|
+
vault: accountVault
|
|
3125
|
+
});
|
|
2620
3126
|
const baseProvider = createOpenAICodexProvider();
|
|
2621
3127
|
let resolveAuth = async () => void 0;
|
|
2622
3128
|
const modelCatalog = createOfficialModelCatalog({
|
|
@@ -2738,7 +3244,15 @@ function apply(ctx) {
|
|
|
2738
3244
|
select(settings.get());
|
|
2739
3245
|
return settings.watch(select);
|
|
2740
3246
|
}, "codex-subscription: search provider selection");
|
|
2741
|
-
const auth = createCodexAuthService(authModels, store, {
|
|
3247
|
+
const auth = createCodexAuthService(authModels, store, {
|
|
3248
|
+
runLogin: (operation) => network.run("login", operation),
|
|
3249
|
+
accountVault,
|
|
3250
|
+
createLoginModels: (credentials) => {
|
|
3251
|
+
const loginModels = createModels({ credentials });
|
|
3252
|
+
loginModels.setProvider(provider);
|
|
3253
|
+
return loginModels;
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
2742
3256
|
const coordinator = new CodexLoginCoordinator(auth);
|
|
2743
3257
|
const usageReader = createQuotaForecastReader({
|
|
2744
3258
|
reader: createCodexUsageReader({
|
|
@@ -2746,8 +3260,18 @@ function apply(ctx) {
|
|
|
2746
3260
|
readCredential: (options) => store.read(PROVIDER, options),
|
|
2747
3261
|
fetch: (input, init) => network.fetch("quota", input, init)
|
|
2748
3262
|
}),
|
|
2749
|
-
enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST
|
|
3263
|
+
enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST,
|
|
3264
|
+
scope: async () => await accountVault?.activeId() ?? "legacy",
|
|
3265
|
+
stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
|
|
2750
3266
|
});
|
|
3267
|
+
ctx.effect(() => {
|
|
3268
|
+
const warmForecast = (value) => {
|
|
3269
|
+
if (normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) !== "forecast") return;
|
|
3270
|
+
usageReader.read().catch((error) => ctx.logger?.debug?.("could not warm Codex quota forecast: %s", error.message));
|
|
3271
|
+
};
|
|
3272
|
+
warmForecast(settings.get());
|
|
3273
|
+
return settings.watch(warmForecast);
|
|
3274
|
+
}, "codex-subscription: quota forecast warm-up");
|
|
2751
3275
|
const resetCreditService = createCodexResetCreditService({
|
|
2752
3276
|
getAuth: resolveAuth,
|
|
2753
3277
|
readCredential: (options) => store.read(PROVIDER, options),
|