@eddyskywalker/dsh-chatgpt-subscription 0.1.0-alpha.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +148 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/client.js +673 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +1753 -0
  8. package/lib/types/client/CodexSubscriptionSection.d.ts +15 -0
  9. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -0
  10. package/lib/types/client/api.d.ts +17 -0
  11. package/lib/types/client/api.d.ts.map +1 -0
  12. package/lib/types/client/index.d.ts +10 -0
  13. package/lib/types/client/index.d.ts.map +1 -0
  14. package/lib/types/client/locales.d.ts +103 -0
  15. package/lib/types/client/locales.d.ts.map +1 -0
  16. package/lib/types/client/styles.d.ts +2 -0
  17. package/lib/types/client/styles.d.ts.map +1 -0
  18. package/lib/types/compat.d.ts +26 -0
  19. package/lib/types/compat.d.ts.map +1 -0
  20. package/lib/types/host/adapter.d.ts +14 -0
  21. package/lib/types/host/adapter.d.ts.map +1 -0
  22. package/lib/types/host/callback-server.d.ts +22 -0
  23. package/lib/types/host/callback-server.d.ts.map +1 -0
  24. package/lib/types/host/model-catalog.d.ts +6 -0
  25. package/lib/types/host/model-catalog.d.ts.map +1 -0
  26. package/lib/types/host/oauth-service.d.ts +69 -0
  27. package/lib/types/host/oauth-service.d.ts.map +1 -0
  28. package/lib/types/host/responses-client.d.ts +21 -0
  29. package/lib/types/host/responses-client.d.ts.map +1 -0
  30. package/lib/types/host/responses-mapper.d.ts +11 -0
  31. package/lib/types/host/responses-mapper.d.ts.map +1 -0
  32. package/lib/types/host/routes.d.ts +5 -0
  33. package/lib/types/host/routes.d.ts.map +1 -0
  34. package/lib/types/host/token-store-windows.d.ts +10 -0
  35. package/lib/types/host/token-store-windows.d.ts.map +1 -0
  36. package/lib/types/host/token-store.d.ts +23 -0
  37. package/lib/types/host/token-store.d.ts.map +1 -0
  38. package/lib/types/host/usage-service.d.ts +33 -0
  39. package/lib/types/host/usage-service.d.ts.map +1 -0
  40. package/lib/types/host/wire-auth.d.ts +5 -0
  41. package/lib/types/host/wire-auth.d.ts.map +1 -0
  42. package/lib/types/index.d.ts +9 -0
  43. package/lib/types/index.d.ts.map +1 -0
  44. package/lib/types/shared/contracts.d.ts +77 -0
  45. package/lib/types/shared/contracts.d.ts.map +1 -0
  46. package/package.json +105 -0
package/lib/index.js ADDED
@@ -0,0 +1,1753 @@
1
+ import { CallId, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import http from "node:http";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ //#region src/host/model-catalog.ts
8
+ const PROVIDER_ID = "codex-chatgpt";
9
+ const PROVIDER_NAME = "Codex(ChatGPT 订阅)";
10
+ const MODEL_IDS = [
11
+ "gpt-5.6-sol",
12
+ "gpt-5.6-terra",
13
+ "gpt-5.6-luna",
14
+ "gpt-5.5",
15
+ "gpt-5.4",
16
+ "gpt-5.4-mini",
17
+ "gpt-5.2"
18
+ ];
19
+ const STANDARD_REASONING_EFFORTS = [
20
+ "none",
21
+ "low",
22
+ "medium",
23
+ "high",
24
+ "xhigh"
25
+ ];
26
+ const GPT_56_REASONING_EFFORTS = [...STANDARD_REASONING_EFFORTS, "max"];
27
+ const MODEL_REASONING = {
28
+ "gpt-5.6-sol": {
29
+ efforts: GPT_56_REASONING_EFFORTS,
30
+ defaultEffort: "medium"
31
+ },
32
+ "gpt-5.6-terra": {
33
+ efforts: GPT_56_REASONING_EFFORTS,
34
+ defaultEffort: "medium"
35
+ },
36
+ "gpt-5.6-luna": {
37
+ efforts: GPT_56_REASONING_EFFORTS,
38
+ defaultEffort: "medium"
39
+ },
40
+ "gpt-5.5": {
41
+ efforts: STANDARD_REASONING_EFFORTS,
42
+ defaultEffort: "medium"
43
+ },
44
+ "gpt-5.4": {
45
+ efforts: STANDARD_REASONING_EFFORTS,
46
+ defaultEffort: "none"
47
+ },
48
+ "gpt-5.4-mini": {
49
+ efforts: STANDARD_REASONING_EFFORTS,
50
+ defaultEffort: "none"
51
+ },
52
+ "gpt-5.2": {
53
+ efforts: STANDARD_REASONING_EFFORTS,
54
+ defaultEffort: "none"
55
+ }
56
+ };
57
+ function listCodexModels() {
58
+ return MODEL_IDS.map((id) => ({
59
+ provider: PROVIDER_ID,
60
+ id,
61
+ name: id,
62
+ inputModalities: ["text", "image"]
63
+ }));
64
+ }
65
+ function resolveCodexModel(model) {
66
+ const reasoning = MODEL_REASONING[model] ?? MODEL_REASONING["gpt-5.6-sol"];
67
+ return {
68
+ provider: PROVIDER_ID,
69
+ id: model,
70
+ name: model,
71
+ inputModalities: ["text", "image"],
72
+ context: { contextWindow: 272e3 },
73
+ defaultMaxTokens: 32768,
74
+ reasoning: {
75
+ efforts: reasoning.efforts.map((effort) => ({
76
+ id: ReasoningEffortId(effort),
77
+ name: effort
78
+ })),
79
+ defaultEffort: ReasoningEffortId(reasoning.defaultEffort)
80
+ }
81
+ };
82
+ }
83
+ //#endregion
84
+ //#region src/host/adapter.ts
85
+ const RETRY_POLICY = resolveRetryPolicy({
86
+ mode: "normal",
87
+ maxRetries: 2,
88
+ retryableCodes: [
89
+ "RATE_LIMIT",
90
+ "SERVER_ERROR",
91
+ "NETWORK"
92
+ ],
93
+ backoff: {
94
+ initialDelayMs: 1e3,
95
+ maxDelayMs: 1e4,
96
+ jitterRatio: .15
97
+ }
98
+ }, "dsh-chatgpt-subscription.retry");
99
+ var CodexChatGptAdapter = class extends LlmAdapter {
100
+ client;
101
+ constructor(client) {
102
+ super();
103
+ this.client = client;
104
+ }
105
+ providerInfo(provider) {
106
+ return {
107
+ id: provider,
108
+ name: PROVIDER_NAME
109
+ };
110
+ }
111
+ providerRetryPolicy() {
112
+ return RETRY_POLICY;
113
+ }
114
+ async listModels() {
115
+ return listCodexModels();
116
+ }
117
+ async resolveModel(_provider, model) {
118
+ return resolveCodexModel(model);
119
+ }
120
+ stream(options) {
121
+ return this.client.stream(options);
122
+ }
123
+ };
124
+ //#endregion
125
+ //#region src/compat.ts
126
+ /**
127
+ * Compatibility constants for the ChatGPT-backed Codex flow. The backend and
128
+ * OAuth parameters are not a public third-party API contract, so every such
129
+ * value is isolated here for review and rollback.
130
+ */
131
+ const CHATGPT_OAUTH_ISSUER = "https://auth.openai.com";
132
+ const CHATGPT_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
133
+ const OAUTH_CALLBACK_HOST = "localhost";
134
+ const OAUTH_CALLBACK_PORT = 1455;
135
+ const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/auth/callback`;
136
+ const OAUTH_SCOPE = "openid profile email offline_access";
137
+ const OAUTH_ORIGINATOR = "opencode";
138
+ const ROUTE_PREFIX = "/api/dsh-chatgpt-subscription";
139
+ const PLUGIN_VERSION = "0.1.0-alpha.0";
140
+ const CODEX_RESPONSES_URL = `https://chatgpt.com/backend-api/codex/responses`;
141
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
142
+ const CODEX_ORIGINATOR = "opencode";
143
+ const QUOTA_MIN_UPSTREAM_INTERVAL_MS = 15e3;
144
+ const OAUTH_AUTHORIZE_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/authorize`;
145
+ const OAUTH_TOKEN_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/token`;
146
+ //#endregion
147
+ //#region src/host/callback-server.ts
148
+ /** One-shot localhost OAuth callback listener. */
149
+ var OAuthCallbackServer = class {
150
+ options;
151
+ abortController = new AbortController();
152
+ server = null;
153
+ settled = false;
154
+ resolveCompletion;
155
+ rejectCompletion;
156
+ completion = new Promise((resolve, reject) => {
157
+ this.resolveCompletion = resolve;
158
+ this.rejectCompletion = reject;
159
+ });
160
+ constructor(options) {
161
+ this.options = options;
162
+ }
163
+ async listen() {
164
+ if (this.server !== null) throw new Error("OAuth callback server already started");
165
+ const server = http.createServer((request, response) => {
166
+ this.handle(request, response);
167
+ });
168
+ this.server = server;
169
+ await new Promise((resolve, reject) => {
170
+ const onError = (error) => reject(error);
171
+ server.once("error", onError);
172
+ server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, () => {
173
+ server.off("error", onError);
174
+ resolve();
175
+ });
176
+ }).catch((error) => {
177
+ this.server = null;
178
+ server.close();
179
+ throw error;
180
+ });
181
+ }
182
+ cancel(reason) {
183
+ this.finish(reason);
184
+ }
185
+ dispose() {
186
+ if (!this.settled) this.finish(/* @__PURE__ */ new Error("OAuth callback listener disposed"));
187
+ else this.close();
188
+ }
189
+ async handle(request, response) {
190
+ if (this.settled) {
191
+ await writeHtml(response, 410, "This sign-in attempt is no longer active.");
192
+ return;
193
+ }
194
+ if (!isLoopback(request.socket.remoteAddress)) {
195
+ await writeHtml(response, 403, "OAuth callback rejected.");
196
+ return;
197
+ }
198
+ const url = new URL(request.url ?? "/", `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
199
+ if (url.pathname !== "/auth/callback") {
200
+ await writeHtml(response, 404, "Not found.");
201
+ return;
202
+ }
203
+ const providerError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
204
+ const code = url.searchParams.get("code");
205
+ const state = url.searchParams.get("state");
206
+ if (providerError !== null || code === null || code === "" || state !== this.options.expectedState) {
207
+ await writeHtml(response, 400, "ChatGPT returned an invalid OAuth callback.");
208
+ this.finish(/* @__PURE__ */ new Error(providerError === null ? "invalid OAuth callback" : "OAuth provider rejected sign-in"));
209
+ return;
210
+ }
211
+ try {
212
+ await this.options.exchange(code, this.abortController.signal);
213
+ await writeHtml(response, 200, "ChatGPT sign-in completed. You can close this window.");
214
+ this.finish();
215
+ } catch (error) {
216
+ await writeHtml(response, 500, "ChatGPT sign-in could not be completed. Return to DSH for details.");
217
+ this.finish(error instanceof Error ? error : /* @__PURE__ */ new Error("OAuth token exchange failed"));
218
+ }
219
+ }
220
+ finish(error) {
221
+ if (this.settled) return;
222
+ this.settled = true;
223
+ this.abortController.abort();
224
+ this.close();
225
+ if (error === void 0) this.resolveCompletion();
226
+ else this.rejectCompletion(error);
227
+ }
228
+ close() {
229
+ const server = this.server;
230
+ this.server = null;
231
+ server?.close();
232
+ server?.closeAllConnections();
233
+ }
234
+ };
235
+ function isLoopback(address) {
236
+ if (address === void 0) return false;
237
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
238
+ }
239
+ function writeHtml(response, status, message) {
240
+ const escaped = message.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
241
+ response.writeHead(status, {
242
+ "content-type": "text/html; charset=utf-8",
243
+ "cache-control": "no-store",
244
+ "x-content-type-options": "nosniff",
245
+ connection: "close"
246
+ });
247
+ return new Promise((resolve) => {
248
+ let settled = false;
249
+ const done = () => {
250
+ if (settled) return;
251
+ settled = true;
252
+ resolve();
253
+ };
254
+ response.once("finish", done);
255
+ response.once("close", done);
256
+ response.end(`<!doctype html><meta charset="utf-8"><title>DSH Codex sign-in</title><h1>${escaped}</h1>`);
257
+ });
258
+ }
259
+ //#endregion
260
+ //#region src/host/oauth-service.ts
261
+ var OAuthServiceError = class extends Error {
262
+ code;
263
+ constructor(code, message) {
264
+ super(message);
265
+ this.code = code;
266
+ this.name = "OAuthServiceError";
267
+ }
268
+ };
269
+ var OAuthService = class {
270
+ store;
271
+ fetchFn;
272
+ now;
273
+ random;
274
+ logger;
275
+ loginTimeoutMs;
276
+ loginEvents = /* @__PURE__ */ new Map();
277
+ listeners = /* @__PURE__ */ new Map();
278
+ activeLogin = null;
279
+ refreshPromise = null;
280
+ lastLoginError;
281
+ disposed = false;
282
+ constructor(store, options = {}) {
283
+ this.store = store;
284
+ this.fetchFn = options.fetchFn ?? fetch;
285
+ this.now = options.now ?? Date.now;
286
+ this.random = options.random ?? randomBytes;
287
+ this.logger = options.logger ?? console;
288
+ this.loginTimeoutMs = options.loginTimeoutMs ?? 3e5;
289
+ }
290
+ async status() {
291
+ try {
292
+ const credentials = await this.store.load();
293
+ return this.statusFromCredentials(credentials);
294
+ } catch {
295
+ return {
296
+ ...this.statusFromCredentials(null),
297
+ error: publicError(new OAuthServiceError("storage-failed", "Secure credential storage could not be read."))
298
+ };
299
+ }
300
+ }
301
+ async startLogin() {
302
+ this.assertAvailable();
303
+ if (this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
304
+ this.lastLoginError = void 0;
305
+ const loginId = this.random(24).toString("base64url");
306
+ const verifier = this.random(48).toString("base64url");
307
+ const state = this.random(32).toString("base64url");
308
+ const expiresAt = this.now() + this.loginTimeoutMs;
309
+ const server = new OAuthCallbackServer({
310
+ expectedState: state,
311
+ exchange: async (code, signal) => this.exchangeCode(code, verifier, signal)
312
+ });
313
+ try {
314
+ await server.listen();
315
+ } catch {
316
+ server.completion.catch(() => void 0);
317
+ server.dispose();
318
+ throw new OAuthServiceError("internal", "The localhost OAuth callback listener could not start on port 1455.");
319
+ }
320
+ const timeout = setTimeout(() => {
321
+ this.cancelActive(new OAuthServiceError("login-expired", "ChatGPT sign-in timed out."), "failed");
322
+ }, this.loginTimeoutMs);
323
+ timeout.unref?.();
324
+ this.activeLogin = {
325
+ id: loginId,
326
+ expiresAt,
327
+ server,
328
+ timeout
329
+ };
330
+ this.publish({
331
+ type: "pending",
332
+ loginId
333
+ });
334
+ server.completion.then(() => {
335
+ this.completeLogin(loginId);
336
+ }).catch((error) => {
337
+ this.failLogin(loginId, error);
338
+ });
339
+ this.logger.info("[dsh-chatgpt-subscription] OAuth login started");
340
+ return {
341
+ loginId,
342
+ authUrl: buildAuthorizationUrl(verifier, state),
343
+ expiresAt
344
+ };
345
+ }
346
+ cancelLogin(loginId) {
347
+ if (this.activeLogin === null || this.activeLogin.id !== loginId) throw new OAuthServiceError("bad-request", "The requested sign-in is not active.");
348
+ this.cancelActive(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."), "cancelled");
349
+ }
350
+ subscribe(loginId, listener) {
351
+ const current = this.loginEvents.get(loginId);
352
+ if (current === void 0) return null;
353
+ let set = this.listeners.get(loginId);
354
+ if (set === void 0) {
355
+ set = /* @__PURE__ */ new Set();
356
+ this.listeners.set(loginId, set);
357
+ }
358
+ set.add(listener);
359
+ listener(current);
360
+ return () => {
361
+ set?.delete(listener);
362
+ if (set?.size === 0) this.listeners.delete(loginId);
363
+ };
364
+ }
365
+ async refresh() {
366
+ this.assertAvailable();
367
+ const stored = await this.loadAuthenticated();
368
+ await this.refreshCredentials(stored);
369
+ return this.status();
370
+ }
371
+ async logout() {
372
+ if (this.activeLogin !== null) this.cancelActive(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."), "cancelled");
373
+ await this.store.clear().catch(() => {
374
+ throw new OAuthServiceError("storage-failed", "Secure credentials could not be deleted.");
375
+ });
376
+ this.lastLoginError = void 0;
377
+ this.logger.info("[dsh-chatgpt-subscription] OAuth credentials cleared");
378
+ }
379
+ async credentials(forceRefresh = false) {
380
+ const stored = await this.loadAuthenticated();
381
+ if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
382
+ return stored;
383
+ }
384
+ dispose() {
385
+ if (this.disposed) return;
386
+ this.disposed = true;
387
+ if (this.activeLogin !== null) this.cancelActive(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."), "cancelled");
388
+ this.listeners.clear();
389
+ this.loginEvents.clear();
390
+ }
391
+ async exchangeCode(code, verifier, signal) {
392
+ const response = await this.fetchFn(OAUTH_TOKEN_URL, {
393
+ method: "POST",
394
+ headers: { "content-type": "application/x-www-form-urlencoded" },
395
+ body: new URLSearchParams({
396
+ grant_type: "authorization_code",
397
+ code,
398
+ redirect_uri: OAUTH_REDIRECT_URI,
399
+ client_id: CHATGPT_OAUTH_CLIENT_ID,
400
+ code_verifier: verifier
401
+ }).toString(),
402
+ signal
403
+ }).catch(() => {
404
+ throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT token exchange could not be reached.");
405
+ });
406
+ if (!response.ok) {
407
+ const detail = await oauthErrorIdentifier(response);
408
+ throw new OAuthServiceError("oauth-token-exchange-failed", `ChatGPT token exchange failed (${response.status}${detail === null ? "" : `, ${detail}`}).`);
409
+ }
410
+ const credentials = credentialsFromTokenResponse(await response.json(), this.now());
411
+ await this.store.save(credentials).catch(() => {
412
+ throw new OAuthServiceError("storage-failed", "ChatGPT credentials could not be saved securely.");
413
+ });
414
+ }
415
+ refreshCredentials(stored) {
416
+ if (this.refreshPromise !== null) return this.refreshPromise;
417
+ this.refreshPromise = this.performRefresh(stored).finally(() => {
418
+ this.refreshPromise = null;
419
+ });
420
+ return this.refreshPromise;
421
+ }
422
+ async performRefresh(stored) {
423
+ const response = await this.fetchFn(OAUTH_TOKEN_URL, {
424
+ method: "POST",
425
+ headers: { "content-type": "application/x-www-form-urlencoded" },
426
+ body: new URLSearchParams({
427
+ grant_type: "refresh_token",
428
+ refresh_token: stored.refreshToken,
429
+ client_id: CHATGPT_OAUTH_CLIENT_ID
430
+ }).toString()
431
+ }).catch(() => {
432
+ throw new OAuthServiceError("refresh-failed", "ChatGPT token refresh could not be reached.");
433
+ });
434
+ if (!response.ok) {
435
+ const detail = await oauthErrorIdentifier(response);
436
+ if (response.status === 400 || response.status === 401) await this.store.clear().catch(() => {
437
+ throw new OAuthServiceError("storage-failed", "Expired ChatGPT credentials could not be deleted securely.");
438
+ });
439
+ throw new OAuthServiceError("refresh-failed", `ChatGPT token refresh failed (${response.status}${detail === null ? "" : `, ${detail}`}). Sign in again.`);
440
+ }
441
+ const fresh = credentialsFromTokenResponse(await response.json(), this.now(), stored);
442
+ await this.store.save(fresh).catch(() => {
443
+ throw new OAuthServiceError("storage-failed", "Refreshed credentials could not be saved securely.");
444
+ });
445
+ this.logger.info("[dsh-chatgpt-subscription] OAuth credentials refreshed");
446
+ return fresh;
447
+ }
448
+ async loadAuthenticated() {
449
+ const stored = await this.store.load().catch(() => {
450
+ throw new OAuthServiceError("storage-failed", "Secure credential storage could not be read.");
451
+ });
452
+ if (stored === null) throw new OAuthServiceError("not-authenticated", "Sign in with ChatGPT first.");
453
+ return stored;
454
+ }
455
+ statusFromCredentials(credentials) {
456
+ const active = this.activeLogin;
457
+ if (credentials === null) return {
458
+ authenticated: false,
459
+ account: null,
460
+ storage: {
461
+ kind: "windows-dpapi",
462
+ encrypted: true
463
+ },
464
+ login: {
465
+ active: active !== null,
466
+ loginId: active?.id ?? null,
467
+ expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
468
+ },
469
+ ...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
470
+ };
471
+ const identity = extractIdentity(credentials);
472
+ return {
473
+ authenticated: true,
474
+ account: {
475
+ email: maskEmail(credentials.email ?? identity.email),
476
+ planType: credentials.planType ?? identity.planType ?? null,
477
+ accountIdSuffix: maskAccountId(credentials.accountId ?? identity.accountId),
478
+ tokenExpiresAt: Math.floor(credentials.expiresAt / 1e3)
479
+ },
480
+ storage: {
481
+ kind: "windows-dpapi",
482
+ encrypted: true
483
+ },
484
+ login: {
485
+ active: active !== null,
486
+ loginId: active?.id ?? null,
487
+ expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
488
+ },
489
+ ...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
490
+ };
491
+ }
492
+ completeLogin(loginId) {
493
+ if (this.activeLogin?.id !== loginId) return;
494
+ clearTimeout(this.activeLogin.timeout);
495
+ this.activeLogin = null;
496
+ this.lastLoginError = void 0;
497
+ this.publish({
498
+ type: "completed",
499
+ loginId
500
+ });
501
+ this.logger.info("[dsh-chatgpt-subscription] OAuth login completed");
502
+ }
503
+ failLogin(loginId, error) {
504
+ if (this.activeLogin?.id !== loginId) return;
505
+ clearTimeout(this.activeLogin.timeout);
506
+ this.activeLogin = null;
507
+ const mapped = publicError(error, "oauth-callback-invalid");
508
+ this.lastLoginError = mapped;
509
+ this.publish({
510
+ type: "failed",
511
+ loginId,
512
+ error: mapped
513
+ });
514
+ this.logger.warn(`[dsh-chatgpt-subscription] OAuth login failed (${mapped.code}): ${mapped.message}`);
515
+ }
516
+ cancelActive(error, outcome) {
517
+ const active = this.activeLogin;
518
+ if (active === null) return;
519
+ clearTimeout(active.timeout);
520
+ this.activeLogin = null;
521
+ active.server.cancel(error);
522
+ this.publish(outcome === "cancelled" ? {
523
+ type: "cancelled",
524
+ loginId: active.id
525
+ } : {
526
+ type: "failed",
527
+ loginId: active.id,
528
+ error: publicError(error)
529
+ });
530
+ }
531
+ publish(event) {
532
+ this.loginEvents.set(event.loginId, event);
533
+ for (const listener of this.listeners.get(event.loginId) ?? []) listener(event);
534
+ }
535
+ assertAvailable() {
536
+ if (this.disposed) throw new OAuthServiceError("internal", "The OAuth service has been disposed.");
537
+ }
538
+ };
539
+ function buildAuthorizationUrl(verifier, state) {
540
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
541
+ return `${OAUTH_AUTHORIZE_URL}?${new URLSearchParams({
542
+ response_type: "code",
543
+ client_id: CHATGPT_OAUTH_CLIENT_ID,
544
+ redirect_uri: OAUTH_REDIRECT_URI,
545
+ scope: OAUTH_SCOPE,
546
+ code_challenge: challenge,
547
+ code_challenge_method: "S256",
548
+ id_token_add_organizations: "true",
549
+ codex_cli_simplified_flow: "true",
550
+ state,
551
+ originator: OAUTH_ORIGINATOR
552
+ }).toString()}`;
553
+ }
554
+ function parseJwtClaims(token) {
555
+ if (token === void 0) return void 0;
556
+ const parts = token.split(".");
557
+ if (parts.length !== 3) return void 0;
558
+ try {
559
+ const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
560
+ return typeof value === "object" && value !== null ? value : void 0;
561
+ } catch {
562
+ return;
563
+ }
564
+ }
565
+ function publicError(error, fallback = "internal") {
566
+ if (error instanceof OAuthServiceError) return {
567
+ code: error.code,
568
+ message: error.message
569
+ };
570
+ return {
571
+ code: fallback,
572
+ message: "The ChatGPT sign-in operation failed."
573
+ };
574
+ }
575
+ function credentialsFromTokenResponse(response, now, previous) {
576
+ if (typeof response.access_token !== "string" || response.access_token === "") throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no access token.");
577
+ const refreshToken = typeof response.refresh_token === "string" && response.refresh_token !== "" ? response.refresh_token : previous?.refreshToken;
578
+ if (refreshToken === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no refresh token.");
579
+ const seconds = Number(response.expires_in);
580
+ const expiresIn = Number.isFinite(seconds) && seconds > 0 ? seconds : 3600;
581
+ const base = {
582
+ accessToken: response.access_token,
583
+ refreshToken,
584
+ idToken: typeof response.id_token === "string" ? response.id_token : previous?.idToken,
585
+ expiresAt: now + expiresIn * 1e3
586
+ };
587
+ const identity = extractIdentity(base);
588
+ return {
589
+ ...base,
590
+ accountId: identity.accountId ?? previous?.accountId,
591
+ email: identity.email ?? previous?.email,
592
+ planType: identity.planType ?? previous?.planType
593
+ };
594
+ }
595
+ function extractIdentity(credentials) {
596
+ const result = {};
597
+ for (const token of [credentials.idToken, credentials.accessToken]) {
598
+ const claims = parseJwtClaims(token);
599
+ if (claims === void 0) continue;
600
+ const nested = claims["https://api.openai.com/auth"];
601
+ result.email ??= stringClaim(claims.email);
602
+ result.planType ??= stringClaim(claims.chatgpt_plan_type) ?? stringClaim(nested?.chatgpt_plan_type);
603
+ result.accountId ??= stringClaim(claims.chatgpt_account_id) ?? stringClaim(nested?.chatgpt_account_id) ?? stringClaim(claims.organizations?.[0]?.id) ?? stringClaim(nested?.organizations?.[0]?.id);
604
+ }
605
+ return result;
606
+ }
607
+ function stringClaim(value) {
608
+ return typeof value === "string" && value !== "" ? value : void 0;
609
+ }
610
+ function maskEmail(email) {
611
+ if (email === void 0) return null;
612
+ const at = email.indexOf("@");
613
+ if (at <= 0 || at === email.length - 1) return "***";
614
+ return `${email.slice(0, 1)}***${email.slice(at)}`;
615
+ }
616
+ function maskAccountId(accountId) {
617
+ if (accountId === void 0) return null;
618
+ return `…${accountId.slice(-4)}`;
619
+ }
620
+ async function oauthErrorIdentifier(response) {
621
+ const payload = await response.json().catch(() => null);
622
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
623
+ const error = payload.error;
624
+ const candidates = typeof error === "object" && error !== null && !Array.isArray(error) ? [error.code, error.type] : [error];
625
+ for (const candidate of candidates) if (typeof candidate === "string" && /^[a-z0-9_.-]{1,64}$/i.test(candidate)) return candidate;
626
+ return null;
627
+ }
628
+ //#endregion
629
+ //#region src/host/responses-mapper.ts
630
+ function hiddenSandboxControlToolNames(options) {
631
+ const retryTools = recentSandboxRetryToolNames(options.messages);
632
+ return new Set(options.tools?.filter((tool) => hasSandboxControls(tool.parameters) && !retryTools.has(tool.name)).map((tool) => tool.name) ?? []);
633
+ }
634
+ async function buildResponsesPayload(options, attachments) {
635
+ const sandboxRetryTools = recentSandboxRetryToolNames(options.messages);
636
+ const instructions = [
637
+ options.system?.trim(),
638
+ ...options.messages.filter((message) => message.role === "system").map((message) => blocksToText(message.content).trim()),
639
+ sandboxToolInstruction(options.tools, sandboxRetryTools),
640
+ runCodeInstruction(options.tools)
641
+ ].filter((value) => Boolean(value));
642
+ const input = [];
643
+ const knownToolCalls = /* @__PURE__ */ new Map();
644
+ for (const message of options.messages) {
645
+ if (message.role === "system") continue;
646
+ const replayItems = replayOutputItems(message);
647
+ if (message.role === "assistant" && replayItems !== null) {
648
+ input.push(...replayItems);
649
+ for (const item of replayItems) if (item.type === "function_call" && typeof item.call_id === "string") knownToolCalls.set(item.call_id, typeof item.name === "string" ? item.name : void 0);
650
+ if (!replayItems.some((item) => item.type === "message")) {
651
+ const content = await mapContent(message, attachments, options.signal);
652
+ if (content.length > 0) input.push({
653
+ role: message.role,
654
+ content
655
+ });
656
+ }
657
+ appendMissingToolCalls(input, knownToolCalls, message);
658
+ continue;
659
+ }
660
+ const toolResult = message.content.find((block) => block.type === "tool-result");
661
+ if (toolResult?.type === "tool-result") {
662
+ const callId = String(toolResult.toolCallId);
663
+ const rawOutput = blocksToText(toolResult.content);
664
+ if (knownToolCalls.has(callId)) {
665
+ const output = toolResult.isError && knownToolCalls.get(callId) === "run_code" ? runCodeErrorOutput(rawOutput) : rawOutput;
666
+ input.push({
667
+ type: "function_call_output",
668
+ call_id: callId,
669
+ output
670
+ });
671
+ } else input.push({
672
+ role: "user",
673
+ content: [{
674
+ type: "input_text",
675
+ text: `Tool result for unavailable call ${callId}${toolResult.isError ? " (error)" : ""}:\n${rawOutput}`
676
+ }]
677
+ });
678
+ continue;
679
+ }
680
+ const content = await mapContent(message, attachments, options.signal);
681
+ if (content.length > 0) input.push({
682
+ role: message.role,
683
+ content
684
+ });
685
+ if (message.role === "assistant") appendMissingToolCalls(input, knownToolCalls, message);
686
+ }
687
+ const payload = {
688
+ model: options.model,
689
+ input,
690
+ stream: true,
691
+ store: false,
692
+ include: ["reasoning.encrypted_content"]
693
+ };
694
+ if (instructions.length > 0) payload.instructions = instructions.join("\n\n");
695
+ if (options.tools?.length) {
696
+ payload.tools = options.tools.map((tool) => ({
697
+ type: "function",
698
+ name: tool.name,
699
+ description: toolDescriptionForCodex(tool.name, tool.description),
700
+ parameters: toolParametersForCodex(tool.name, tool.parameters, sandboxRetryTools.has(tool.name))
701
+ }));
702
+ payload.tool_choice = "auto";
703
+ payload.parallel_tool_calls = true;
704
+ }
705
+ if (options.reasoningEffort !== void 0) payload.reasoning = {
706
+ effort: options.reasoningEffort,
707
+ summary: "auto"
708
+ };
709
+ return payload;
710
+ }
711
+ function runCodeInstruction(tools) {
712
+ if (!tools?.some((tool) => tool.name === "run_code")) return void 0;
713
+ return "run_code compatibility rule: its code is parsed as strict JavaScript/TypeScript before execution. On Windows, do not embed PowerShell containing $, ${...}, backslashes, or here-strings in JavaScript template literals; String.raw does not disable ${...} interpolation. Prefer arrays of ordinary quoted strings joined with \"\\n\", escaping backslashes, or use a file-write tool for large scripts and then invoke pwsh.";
714
+ }
715
+ function toolDescriptionForCodex(name, description) {
716
+ if (name !== "run_code") return description;
717
+ return `${description}\n\nCompatibility: code is strict JavaScript/TypeScript. When composing PowerShell, avoid JavaScript template literals containing $, \${...}, Windows backslashes, or PowerShell here-strings. Prefer ordinary quoted string arrays joined with "\\n", or write a script file with a dedicated file tool before invoking pwsh.`;
718
+ }
719
+ function sandboxToolInstruction(tools, sandboxRetryTools) {
720
+ if (!tools?.some((tool) => hasSandboxControls(tool.parameters))) return void 0;
721
+ if (sandboxRetryTools.size === 0) return "Tool sandbox rule: this is not a sandbox-escalation retry. Omit sandbox_permissions and justification from every tool call. First run the tool with the session's current access.";
722
+ return `Tool sandbox rule: sandbox_permissions and justification may only be used to retry the exact denied call for: ${[...sandboxRetryTools].join(", ")}. Omit both fields from every other tool call, and request a strictly wider mode with a non-empty justification sentence.`;
723
+ }
724
+ function toolParametersForCodex(toolName, parameters, allowSandboxRetry) {
725
+ const hideSandboxControls = !allowSandboxRetry && hasSandboxControls(parameters);
726
+ if (!hideSandboxControls && toolName !== "run_code") return parameters;
727
+ const cloned = structuredClone(parameters);
728
+ const properties = record$2(cloned.properties);
729
+ if (properties !== null && hideSandboxControls) {
730
+ delete properties.sandbox_permissions;
731
+ delete properties.justification;
732
+ }
733
+ if (hideSandboxControls && Array.isArray(cloned.required)) cloned.required = cloned.required.filter((name) => name !== "sandbox_permissions" && name !== "justification");
734
+ if (toolName === "run_code" && properties !== null) {
735
+ const code = record$2(properties.code);
736
+ if (code !== null) {
737
+ const current = typeof code.description === "string" ? code.description.trim() : "";
738
+ const compatibility = "Strict JavaScript/TypeScript source. For PowerShell on Windows, avoid JavaScript template literals containing $, ${...}, backslashes, or here-strings; String.raw still performs ${...} interpolation. Prefer ordinary quoted string arrays joined with \"\\n\" and escape backslashes, or write a script file with a dedicated file tool.";
739
+ code.description = current ? `${current}\n\n${compatibility}` : compatibility;
740
+ }
741
+ }
742
+ return cloned;
743
+ }
744
+ function hasSandboxControls(parameters) {
745
+ const properties = record$2(parameters.properties);
746
+ return properties !== null && ("sandbox_permissions" in properties || "justification" in properties);
747
+ }
748
+ function recentSandboxRetryToolNames(messages) {
749
+ const deniedCallIds = /* @__PURE__ */ new Set();
750
+ let assistant;
751
+ for (let index = messages.length - 1; index >= 0; index--) {
752
+ const message = messages[index];
753
+ if (message.role === "assistant") {
754
+ assistant = message;
755
+ break;
756
+ }
757
+ for (const block of message.content) {
758
+ if (block.type !== "tool-result") continue;
759
+ if (isSandboxDenial(blocksToText(block.content))) deniedCallIds.add(String(block.toolCallId));
760
+ }
761
+ }
762
+ const result = /* @__PURE__ */ new Set();
763
+ if (assistant === void 0 || deniedCallIds.size === 0) return result;
764
+ for (const block of assistant.content) if (block.type === "tool-call" && deniedCallIds.has(String(block.id))) result.add(block.name);
765
+ return result;
766
+ }
767
+ function isSandboxDenial(output) {
768
+ return /\[sandbox:\s*file access denied\b/i.test(output) || /\bsandbox\b.*\b(?:access denied|denied access|EPERM)\b/i.test(output);
769
+ }
770
+ function appendMissingToolCalls(input, knownToolCalls, message) {
771
+ for (const block of message.content) {
772
+ if (block.type !== "tool-call") continue;
773
+ const callId = String(block.id);
774
+ if (knownToolCalls.has(callId)) continue;
775
+ input.push({
776
+ type: "function_call",
777
+ call_id: callId,
778
+ name: block.name,
779
+ arguments: block.arguments
780
+ });
781
+ knownToolCalls.set(callId, block.name);
782
+ }
783
+ }
784
+ function runCodeErrorOutput(output) {
785
+ if (!isRunCodeParserError(output)) return output;
786
+ return `${output}\n\nCompatibility hint: run_code failed while parsing strict JavaScript/TypeScript, before the nested tool ran. Avoid JavaScript template literals for PowerShell containing $, \${...}, Windows backslashes, or here-strings; String.raw does not prevent \${...} interpolation. Build the script from ordinary quoted strings joined with "\\n" (escaping backslashes), or write a script file with a dedicated file tool and then invoke pwsh.`;
787
+ }
788
+ function isRunCodeParserError(output) {
789
+ return /(?:Legacy octal escape is not permitted in strict mode|Unexpected token|Invalid or unexpected token|Unterminated template|Expected ['"]?\}['"]?)/i.test(output);
790
+ }
791
+ async function mapContent(message, attachments, signal) {
792
+ const result = [];
793
+ for (const block of message.content) if (block.type === "text") result.push({
794
+ type: message.role === "assistant" ? "output_text" : "input_text",
795
+ text: block.text
796
+ });
797
+ else if (block.type === "image") {
798
+ if (message.role !== "user") continue;
799
+ result.push({
800
+ type: "input_image",
801
+ image_url: await imageDataUrl(block.attachment, attachments, signal)
802
+ });
803
+ }
804
+ return result;
805
+ }
806
+ async function imageDataUrl(ref, attachments, signal) {
807
+ const stored = await attachments.readImage(ref, signal);
808
+ return `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`;
809
+ }
810
+ function blocksToText(blocks) {
811
+ return blocks.map((block) => {
812
+ if (block.type === "text" || block.type === "reasoning") return block.text;
813
+ if (block.type === "image") return `[image: ${block.attachment.name ?? block.attachment.attachmentId}]`;
814
+ if (block.type === "tool-result") return blocksToText(block.content);
815
+ return "";
816
+ }).filter(Boolean).join("\n");
817
+ }
818
+ function replayOutputItems(message) {
819
+ if (message.source.kind !== "model") return null;
820
+ const replay = message.source.replayState;
821
+ if (typeof replay !== "object" || replay === null || Array.isArray(replay)) return null;
822
+ const items = replay.outputItems;
823
+ if (!Array.isArray(items)) return null;
824
+ return structuredClone(items.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)));
825
+ }
826
+ function record$2(value) {
827
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
828
+ }
829
+ //#endregion
830
+ //#region src/host/wire-auth.ts
831
+ function codexHeaders(credentials, sessionId) {
832
+ const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
833
+ return {
834
+ authorization: `Bearer ${credentials.accessToken}`,
835
+ ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
836
+ originator: CODEX_ORIGINATOR,
837
+ "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
838
+ ...sessionId ? { "session-id": sessionId } : {}
839
+ };
840
+ }
841
+ function stableSessionId(value) {
842
+ const source = value === void 0 || value === "" ? randomUUID() : value;
843
+ return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
844
+ }
845
+ function retryAfterMs(headers) {
846
+ const raw = headers.get("retry-after");
847
+ if (raw === null) return void 0;
848
+ const seconds = Number(raw);
849
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
850
+ const timestamp = Date.parse(raw);
851
+ if (!Number.isFinite(timestamp)) return void 0;
852
+ return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
853
+ }
854
+ //#endregion
855
+ //#region src/host/responses-client.ts
856
+ var ResponsesClient = class {
857
+ oauth;
858
+ attachments;
859
+ fetchFn;
860
+ onGenerationFinished;
861
+ constructor(oauth, attachments, options = {}) {
862
+ this.oauth = oauth;
863
+ this.attachments = attachments;
864
+ this.fetchFn = options.fetchFn ?? fetch;
865
+ this.onGenerationFinished = options.onGenerationFinished ?? (() => void 0);
866
+ }
867
+ async *stream(options) {
868
+ const payload = await buildResponsesPayload(options, this.attachments);
869
+ const hiddenSandboxControls = hiddenSandboxControlToolNames(options);
870
+ const sessionId = stableSessionId(options.sessionId);
871
+ try {
872
+ yield* parseResponsesStream(await this.send(payload, sessionId, options.signal), options.signal, hiddenSandboxControls);
873
+ } finally {
874
+ this.onGenerationFinished();
875
+ }
876
+ }
877
+ async send(payload, sessionId, signal) {
878
+ let credentials = await this.oauth.credentials();
879
+ let response = await this.request(payload, credentials, sessionId, signal);
880
+ if (response.status === 401) {
881
+ await response.body?.cancel().catch(() => void 0);
882
+ credentials = await this.oauth.credentials(true);
883
+ response = await this.request(payload, credentials, sessionId, signal);
884
+ }
885
+ if (!response.ok) throw await responseError(response);
886
+ return response;
887
+ }
888
+ async request(payload, credentials, sessionId, signal) {
889
+ try {
890
+ return await this.fetchFn(CODEX_RESPONSES_URL, {
891
+ method: "POST",
892
+ headers: {
893
+ ...codexHeaders(credentials, sessionId),
894
+ "content-type": "application/json",
895
+ accept: "text/event-stream"
896
+ },
897
+ body: JSON.stringify(payload),
898
+ signal
899
+ });
900
+ } catch (cause) {
901
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
902
+ throw new LlmError("Codex could not be reached.", "NETWORK", { cause });
903
+ }
904
+ }
905
+ };
906
+ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /* @__PURE__ */ new Set()) {
907
+ if (response.body === null) throw new LlmError("Codex returned no response stream.", "PROVIDER_ERROR");
908
+ const reader = response.body.getReader();
909
+ const abortReader = () => {
910
+ reader.cancel(signal?.reason).catch(() => void 0);
911
+ };
912
+ signal?.addEventListener("abort", abortReader, { once: true });
913
+ const decoder = new TextDecoder();
914
+ let buffer = "";
915
+ let nextIndex = 0;
916
+ let textIndex = null;
917
+ let reasoningIndex = null;
918
+ let text = "";
919
+ let reasoning = "";
920
+ let terminal = null;
921
+ let usage = null;
922
+ let replayOutput = [];
923
+ const tools = /* @__PURE__ */ new Map();
924
+ const toolFor = (event, item) => {
925
+ const itemId = string(event.item_id) ?? string(item?.id);
926
+ const outputIndex = number(event.output_index);
927
+ const key = itemId ?? (outputIndex === void 0 ? `tool-${tools.size}` : `index-${outputIndex}`);
928
+ let tool = tools.get(key);
929
+ if (tool === void 0) {
930
+ tool = {
931
+ index: nextIndex++,
932
+ id: string(item?.call_id) ?? string(event.call_id) ?? `call_${key}`,
933
+ itemId,
934
+ name: string(item?.name) ?? string(event.name) ?? "",
935
+ arguments: "",
936
+ started: false
937
+ };
938
+ tools.set(key, tool);
939
+ }
940
+ return tool;
941
+ };
942
+ const consume = async function* (event) {
943
+ const type = string(event.type);
944
+ if (type === "response.output_text.delta" || type === "response.refusal.delta") {
945
+ const delta = string(event.delta) ?? "";
946
+ if (textIndex === null) {
947
+ textIndex = nextIndex++;
948
+ yield {
949
+ type: "block-start",
950
+ index: textIndex,
951
+ blockType: "text"
952
+ };
953
+ }
954
+ text += delta;
955
+ if (delta) yield {
956
+ type: "text-delta",
957
+ index: textIndex,
958
+ text: delta
959
+ };
960
+ return;
961
+ }
962
+ if (type === "response.reasoning_summary_text.delta") {
963
+ const delta = string(event.delta) ?? "";
964
+ if (reasoningIndex === null) {
965
+ reasoningIndex = nextIndex++;
966
+ yield {
967
+ type: "block-start",
968
+ index: reasoningIndex,
969
+ blockType: "reasoning"
970
+ };
971
+ }
972
+ reasoning += delta;
973
+ if (delta) yield {
974
+ type: "reasoning-delta",
975
+ index: reasoningIndex,
976
+ text: delta
977
+ };
978
+ return;
979
+ }
980
+ if (type === "response.output_item.added" || type === "response.output_item.done") {
981
+ const item = record$1(event.item);
982
+ if (item !== null && type === "response.output_item.done") replayOutput.push(structuredClone(item));
983
+ if (string(item?.type) !== "function_call") return;
984
+ const tool = toolFor(event, item ?? void 0);
985
+ tool.id = string(item?.call_id) ?? tool.id;
986
+ tool.name = string(item?.name) ?? tool.name;
987
+ const initial = string(item?.arguments) ?? "";
988
+ if (!tool.started) {
989
+ tool.started = true;
990
+ yield {
991
+ type: "block-start",
992
+ index: tool.index,
993
+ blockType: "tool-call"
994
+ };
995
+ yield {
996
+ type: "tool-call-delta",
997
+ index: tool.index,
998
+ id: CallId(tool.id),
999
+ name: tool.name || void 0,
1000
+ argumentsDelta: initial
1001
+ };
1002
+ tool.arguments = initial;
1003
+ } else if (type === "response.output_item.done" && initial !== "") tool.arguments = initial;
1004
+ return;
1005
+ }
1006
+ if (type === "response.function_call_arguments.delta") {
1007
+ const tool = toolFor(event);
1008
+ const delta = string(event.delta) ?? "";
1009
+ if (!tool.started) {
1010
+ tool.started = true;
1011
+ yield {
1012
+ type: "block-start",
1013
+ index: tool.index,
1014
+ blockType: "tool-call"
1015
+ };
1016
+ }
1017
+ tool.arguments += delta;
1018
+ yield {
1019
+ type: "tool-call-delta",
1020
+ index: tool.index,
1021
+ id: CallId(tool.id),
1022
+ name: tool.name || void 0,
1023
+ argumentsDelta: delta
1024
+ };
1025
+ return;
1026
+ }
1027
+ if (type === "response.function_call_arguments.done") {
1028
+ const tool = toolFor(event);
1029
+ const finalArguments = string(event.arguments);
1030
+ if (finalArguments !== void 0) tool.arguments = finalArguments;
1031
+ return;
1032
+ }
1033
+ if (type === "response.completed" || type === "response.incomplete") {
1034
+ const completed = record$1(event.response);
1035
+ usage = mapUsage(record$1(completed?.usage));
1036
+ const output = completed?.output;
1037
+ if (Array.isArray(output)) replayOutput = output.filter((item) => record$1(item) !== null).map((item) => structuredClone(item));
1038
+ terminal = type === "response.incomplete" ? { kind: "max-tokens" } : { kind: "stop" };
1039
+ return;
1040
+ }
1041
+ if (type === "response.failed" || type === "error") {
1042
+ const error = record$1(event.error) ?? record$1(record$1(event.response)?.error);
1043
+ throw new LlmError(string(error?.message) ?? "Codex generation failed.", string(error?.code) ?? "PROVIDER_ERROR");
1044
+ }
1045
+ };
1046
+ try {
1047
+ while (true) {
1048
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
1049
+ const { done, value } = await reader.read();
1050
+ if (done) break;
1051
+ buffer += decoder.decode(value, { stream: true });
1052
+ const frames = buffer.split(/\r?\n\r?\n/);
1053
+ buffer = frames.pop() ?? "";
1054
+ for (const frame of frames) {
1055
+ const data = frame.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
1056
+ if (data === "" || data === "[DONE]") continue;
1057
+ let event;
1058
+ try {
1059
+ event = JSON.parse(data);
1060
+ } catch {
1061
+ throw new LlmError("Codex returned malformed streaming JSON.", "PROTOCOL_ERROR");
1062
+ }
1063
+ const valueRecord = record$1(event);
1064
+ if (valueRecord !== null) yield* consume(valueRecord);
1065
+ }
1066
+ }
1067
+ } finally {
1068
+ signal?.removeEventListener("abort", abortReader);
1069
+ reader.releaseLock();
1070
+ }
1071
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
1072
+ if (terminal === null) throw new LlmError("Codex stream ended before a terminal event.", "PROTOCOL_ERROR");
1073
+ if (reasoningIndex !== null) yield {
1074
+ type: "block-end",
1075
+ index: reasoningIndex,
1076
+ block: {
1077
+ type: "reasoning",
1078
+ text: reasoning
1079
+ }
1080
+ };
1081
+ if (textIndex !== null) yield {
1082
+ type: "block-end",
1083
+ index: textIndex,
1084
+ block: {
1085
+ type: "text",
1086
+ text
1087
+ }
1088
+ };
1089
+ for (const item of replayOutput) if (item.type === "function_call" && typeof item.name === "string" && hiddenSandboxControls.has(item.name) && typeof item.arguments === "string") item.arguments = stripSandboxControls(item.arguments);
1090
+ let validToolCount = 0;
1091
+ const replayedToolCallIds = new Set(replayOutput.flatMap((item) => item.type === "function_call" && typeof item.call_id === "string" ? [item.call_id] : []));
1092
+ for (const tool of tools.values()) {
1093
+ if (!tool.started) continue;
1094
+ if (hiddenSandboxControls.has(tool.name)) tool.arguments = stripSandboxControls(tool.arguments);
1095
+ if (!isSafeJsonArguments(tool.arguments) || tool.name === "") throw new LlmError(`Codex returned invalid JSON arguments for tool ${tool.name || "(unnamed)"}.`, "INVALID_TOOL_ARGUMENTS");
1096
+ validToolCount++;
1097
+ if (!replayedToolCallIds.has(tool.id)) {
1098
+ replayOutput.push({
1099
+ type: "function_call",
1100
+ call_id: tool.id,
1101
+ name: tool.name,
1102
+ arguments: tool.arguments
1103
+ });
1104
+ replayedToolCallIds.add(tool.id);
1105
+ }
1106
+ yield {
1107
+ type: "block-end",
1108
+ index: tool.index,
1109
+ block: {
1110
+ type: "tool-call",
1111
+ id: CallId(tool.id),
1112
+ name: tool.name,
1113
+ arguments: tool.arguments
1114
+ }
1115
+ };
1116
+ }
1117
+ if (usage !== null) yield {
1118
+ type: "usage",
1119
+ usage
1120
+ };
1121
+ yield {
1122
+ type: "finish",
1123
+ reason: validToolCount > 0 ? { kind: "tool-calls" } : terminal,
1124
+ replayState: { outputItems: replayOutput }
1125
+ };
1126
+ }
1127
+ function mapUsage(value) {
1128
+ if (value === null) return null;
1129
+ const totalInput = number(value.input_tokens) ?? 0;
1130
+ const outputTokens = number(value.output_tokens) ?? 0;
1131
+ const cached = number(record$1(value.input_tokens_details)?.cached_tokens) ?? 0;
1132
+ const reasoning = number(record$1(value.output_tokens_details)?.reasoning_tokens);
1133
+ return {
1134
+ inputTokens: Math.max(0, totalInput - cached),
1135
+ outputTokens,
1136
+ ...cached > 0 ? { cacheReadTokens: cached } : {},
1137
+ ...reasoning === void 0 ? {} : { reasoningTokens: reasoning }
1138
+ };
1139
+ }
1140
+ function isSafeJsonArguments(value) {
1141
+ try {
1142
+ const parsed = JSON.parse(value);
1143
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
1144
+ } catch {
1145
+ return false;
1146
+ }
1147
+ }
1148
+ function stripSandboxControls(value) {
1149
+ try {
1150
+ const parsedRecord = record$1(JSON.parse(value));
1151
+ if (parsedRecord === null) return value;
1152
+ let changed = false;
1153
+ for (const name of ["sandbox_permissions", "justification"]) if (name in parsedRecord) {
1154
+ delete parsedRecord[name];
1155
+ changed = true;
1156
+ }
1157
+ return changed ? JSON.stringify(parsedRecord) : value;
1158
+ } catch {
1159
+ return value;
1160
+ }
1161
+ }
1162
+ async function responseError(response) {
1163
+ const requestId = response.headers.get("x-request-id");
1164
+ const detail = (await response.text().catch(() => "")).slice(0, 500);
1165
+ const options = {
1166
+ status: response.status,
1167
+ ...requestId ? { requestId: ProviderRequestId(requestId) } : {},
1168
+ ...response.status === 429 ? { providerRetryAfterMs: retryAfterMs(response.headers) } : {}
1169
+ };
1170
+ if (response.status === 401) return new LlmError("ChatGPT sign-in has expired. Sign in again.", "AUTH", options);
1171
+ if (response.status === 429) return new LlmError("Codex rate limit reached.", "RATE_LIMIT", options);
1172
+ if (response.status >= 500) return new LlmError(`Codex service error (${response.status}).`, "SERVER_ERROR", options);
1173
+ return new LlmError(`Codex request failed (${response.status})${detail ? `: ${detail}` : "."}`, "PROVIDER_ERROR", options);
1174
+ }
1175
+ function record$1(value) {
1176
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1177
+ }
1178
+ function string(value) {
1179
+ return typeof value === "string" ? value : void 0;
1180
+ }
1181
+ function number(value) {
1182
+ const parsed = Number(value);
1183
+ return Number.isFinite(parsed) ? parsed : void 0;
1184
+ }
1185
+ //#endregion
1186
+ //#region src/host/usage-service.ts
1187
+ var UsageService = class {
1188
+ oauth;
1189
+ fetchFn;
1190
+ now;
1191
+ cache = null;
1192
+ lastUpstreamAt = 0;
1193
+ blockedUntil = 0;
1194
+ invalidated = false;
1195
+ inFlight = null;
1196
+ constructor(oauth, options = {}) {
1197
+ this.oauth = oauth;
1198
+ this.fetchFn = options.fetchFn ?? fetch;
1199
+ this.now = options.now ?? Date.now;
1200
+ }
1201
+ async status(authenticated, force = false) {
1202
+ if (!authenticated) return {
1203
+ state: "signed-out",
1204
+ buckets: [],
1205
+ fetchedAt: null,
1206
+ stale: false
1207
+ };
1208
+ const now = this.now();
1209
+ let credentials;
1210
+ try {
1211
+ credentials = await this.oauth.credentials();
1212
+ } catch {
1213
+ return this.failure({
1214
+ code: "quota-failed",
1215
+ message: "ChatGPT credentials could not be refreshed."
1216
+ });
1217
+ }
1218
+ const accountKey = identityKey(credentials);
1219
+ if (this.cache !== null && this.cache.accountKey !== accountKey) this.clear();
1220
+ if (!force && !this.invalidated && this.cache !== null && now - this.cache.fetchedAt < 6e4) return this.fromCache(false);
1221
+ if (this.cache !== null && now - this.lastUpstreamAt < 15e3) return this.fromCache(this.invalidated || now - this.cache.fetchedAt >= 6e4);
1222
+ if (now < this.blockedUntil) return this.failure({
1223
+ code: "rate-limited",
1224
+ message: "Quota refresh is temporarily rate limited."
1225
+ });
1226
+ if (this.inFlight !== null) return this.inFlight;
1227
+ this.inFlight = this.refreshUpstream(credentials, accountKey).finally(() => {
1228
+ this.inFlight = null;
1229
+ });
1230
+ return this.inFlight;
1231
+ }
1232
+ invalidate() {
1233
+ this.invalidated = true;
1234
+ }
1235
+ clear() {
1236
+ this.cache = null;
1237
+ this.blockedUntil = 0;
1238
+ this.invalidated = false;
1239
+ }
1240
+ async testConnection() {
1241
+ const started = this.now();
1242
+ const result = await this.status(true, true);
1243
+ if (result.state === "error" || result.error !== void 0) throw new UsageServiceError(result.error ?? {
1244
+ code: "connection-failed",
1245
+ message: "Codex connection test failed."
1246
+ });
1247
+ return {
1248
+ connected: true,
1249
+ latencyMs: Math.max(0, this.now() - started),
1250
+ checkedAt: Math.floor(this.now() / 1e3)
1251
+ };
1252
+ }
1253
+ async refreshUpstream(initialCredentials, initialAccountKey) {
1254
+ this.lastUpstreamAt = this.now();
1255
+ try {
1256
+ let credentials = initialCredentials;
1257
+ let accountKey = initialAccountKey;
1258
+ let response = await this.fetch(credentials);
1259
+ if (response.status === 401) {
1260
+ await response.body?.cancel().catch(() => void 0);
1261
+ credentials = await this.oauth.credentials(true);
1262
+ accountKey = identityKey(credentials);
1263
+ response = await this.fetch(credentials);
1264
+ }
1265
+ if (response.status === 429) {
1266
+ const delay = retryAfterMs(response.headers) ?? 15e3;
1267
+ this.blockedUntil = this.now() + Math.max(QUOTA_MIN_UPSTREAM_INTERVAL_MS, delay);
1268
+ await response.body?.cancel().catch(() => void 0);
1269
+ return this.failure({
1270
+ code: "rate-limited",
1271
+ message: "Quota refresh was rate limited. Existing data was kept."
1272
+ });
1273
+ }
1274
+ if (!response.ok) {
1275
+ await response.body?.cancel().catch(() => void 0);
1276
+ return this.failure({
1277
+ code: "quota-failed",
1278
+ message: `Quota request failed (${response.status}).`
1279
+ });
1280
+ }
1281
+ const buckets = mapCodexUsage(await response.json());
1282
+ this.cache = {
1283
+ buckets,
1284
+ fetchedAt: this.now(),
1285
+ accountKey
1286
+ };
1287
+ this.invalidated = false;
1288
+ return this.fromCache(false);
1289
+ } catch (error) {
1290
+ const publicError = error instanceof UsageServiceError ? error.publicError : {
1291
+ code: "quota-failed",
1292
+ message: "Quota information could not be refreshed."
1293
+ };
1294
+ return this.failure(publicError);
1295
+ }
1296
+ }
1297
+ fetch(credentials) {
1298
+ return this.fetchFn(CODEX_USAGE_URL, { headers: {
1299
+ ...codexHeaders(credentials),
1300
+ accept: "application/json"
1301
+ } });
1302
+ }
1303
+ fromCache(stale, error) {
1304
+ if (this.cache === null) return {
1305
+ state: error ? "error" : "empty",
1306
+ buckets: [],
1307
+ fetchedAt: null,
1308
+ stale,
1309
+ ...error ? { error } : {}
1310
+ };
1311
+ return {
1312
+ state: error ? "stale" : this.cache.buckets.length > 0 ? "ready" : "empty",
1313
+ buckets: structuredClone(this.cache.buckets),
1314
+ fetchedAt: Math.floor(this.cache.fetchedAt / 1e3),
1315
+ stale,
1316
+ ...error ? { error } : {}
1317
+ };
1318
+ }
1319
+ failure(error) {
1320
+ return this.fromCache(this.cache !== null, error);
1321
+ }
1322
+ };
1323
+ var UsageServiceError = class extends Error {
1324
+ publicError;
1325
+ constructor(publicError) {
1326
+ super(publicError.message);
1327
+ this.publicError = publicError;
1328
+ }
1329
+ };
1330
+ function mapCodexUsage(value) {
1331
+ const data = record(value);
1332
+ if (data === null) return [];
1333
+ const planType = typeof data.plan_type === "string" ? data.plan_type : null;
1334
+ const result = [];
1335
+ addBucket(result, "codex", "Codex", planType, data.rate_limit);
1336
+ addBucket(result, "code-review", "Code review", planType, data.code_review_rate_limit);
1337
+ return result;
1338
+ }
1339
+ function addBucket(result, id, name, planType, value) {
1340
+ const source = record(value);
1341
+ if (source === null) return;
1342
+ const primary = mapWindow(source.primary_window);
1343
+ const secondary = mapWindow(source.secondary_window);
1344
+ if (primary === null && secondary === null) return;
1345
+ result.push({
1346
+ id,
1347
+ name,
1348
+ planType,
1349
+ primary,
1350
+ secondary
1351
+ });
1352
+ }
1353
+ function mapWindow(value) {
1354
+ const data = record(value);
1355
+ if (data === null) return null;
1356
+ const used = numeric(data.used_percent);
1357
+ if (used === void 0) return null;
1358
+ const seconds = numeric(data.limit_window_seconds);
1359
+ const reset = numeric(data.reset_at);
1360
+ return {
1361
+ usedPercent: Math.min(100, Math.max(0, used)),
1362
+ windowDurationMins: seconds !== void 0 && seconds > 0 ? seconds / 60 : null,
1363
+ resetsAt: reset !== void 0 && reset > 0 ? reset : null
1364
+ };
1365
+ }
1366
+ function record(value) {
1367
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1368
+ }
1369
+ function numeric(value) {
1370
+ if (typeof value !== "number" && (typeof value !== "string" || value.trim() === "")) return void 0;
1371
+ const number = Number(value);
1372
+ return Number.isFinite(number) ? number : void 0;
1373
+ }
1374
+ function identityKey(credentials) {
1375
+ return credentials.accountId ?? credentials.email ?? credentials.planType ?? "signed-in";
1376
+ }
1377
+ //#endregion
1378
+ //#region src/host/routes.ts
1379
+ const MAX_BODY_BYTES = 64 * 1024;
1380
+ function registerRoutes(ctx, oauth, usage) {
1381
+ const handler = async (request, response) => {
1382
+ const url = new URL(request.url ?? "/", "http://dsh.local");
1383
+ if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
1384
+ const oauthStatus = await oauth.status();
1385
+ json(response, {
1386
+ ok: true,
1387
+ value: {
1388
+ ...oauthStatus,
1389
+ quota: await usage.status(oauthStatus.authenticated)
1390
+ }
1391
+ });
1392
+ return;
1393
+ }
1394
+ if (request.method !== "POST") {
1395
+ jsonError(response, 405, {
1396
+ code: "bad-request",
1397
+ message: "Method not allowed."
1398
+ });
1399
+ return;
1400
+ }
1401
+ if (!isSameOriginMutation(request)) {
1402
+ jsonError(response, 403, {
1403
+ code: "csrf-rejected",
1404
+ message: "Cross-origin request rejected."
1405
+ });
1406
+ return;
1407
+ }
1408
+ const contentType = request.headers["content-type"];
1409
+ if (typeof contentType !== "string" || !contentType.toLowerCase().startsWith("application/json")) {
1410
+ jsonError(response, 415, {
1411
+ code: "bad-request",
1412
+ message: "A JSON request body is required."
1413
+ });
1414
+ return;
1415
+ }
1416
+ const body = await readJson(request);
1417
+ if (body === null) {
1418
+ jsonError(response, 400, {
1419
+ code: "bad-request",
1420
+ message: "Malformed JSON request."
1421
+ });
1422
+ return;
1423
+ }
1424
+ try {
1425
+ switch (url.pathname) {
1426
+ case `${ROUTE_PREFIX}/login/start`:
1427
+ json(response, {
1428
+ ok: true,
1429
+ value: await oauth.startLogin()
1430
+ });
1431
+ return;
1432
+ case `${ROUTE_PREFIX}/login/cancel`: {
1433
+ const loginId = field(body, "loginId");
1434
+ if (loginId === null) throw new Error("missing loginId");
1435
+ oauth.cancelLogin(loginId);
1436
+ json(response, {
1437
+ ok: true,
1438
+ value: { cancelled: true }
1439
+ });
1440
+ return;
1441
+ }
1442
+ case `${ROUTE_PREFIX}/logout`:
1443
+ await oauth.logout();
1444
+ usage.clear();
1445
+ json(response, {
1446
+ ok: true,
1447
+ value: { authenticated: false }
1448
+ });
1449
+ return;
1450
+ case `${ROUTE_PREFIX}/token/refresh`: {
1451
+ const oauthStatus = await oauth.refresh();
1452
+ json(response, {
1453
+ ok: true,
1454
+ value: {
1455
+ ...oauthStatus,
1456
+ quota: await usage.status(oauthStatus.authenticated)
1457
+ }
1458
+ });
1459
+ return;
1460
+ }
1461
+ case `${ROUTE_PREFIX}/quota/refresh`:
1462
+ if (!(await oauth.status()).authenticated) throw new Error("not authenticated");
1463
+ json(response, {
1464
+ ok: true,
1465
+ value: await usage.status(true, true)
1466
+ });
1467
+ return;
1468
+ case `${ROUTE_PREFIX}/connection/test`:
1469
+ json(response, {
1470
+ ok: true,
1471
+ value: await usage.testConnection()
1472
+ });
1473
+ return;
1474
+ default: jsonError(response, 404, {
1475
+ code: "bad-request",
1476
+ message: "Route not found."
1477
+ });
1478
+ }
1479
+ } catch (error) {
1480
+ const mapped = error instanceof UsageServiceError ? error.publicError : publicError(error, error instanceof Error && error.message === "missing loginId" ? "bad-request" : error instanceof Error && error.message === "not authenticated" ? "not-authenticated" : "internal");
1481
+ jsonError(response, statusFor(mapped), mapped);
1482
+ }
1483
+ };
1484
+ const events = (request, response) => {
1485
+ if (request.method !== "GET") {
1486
+ response.writeHead(405);
1487
+ response.end();
1488
+ return;
1489
+ }
1490
+ const loginId = new URL(request.url ?? "/", "http://dsh.local").searchParams.get("loginId");
1491
+ if (loginId === null || loginId === "") {
1492
+ jsonError(response, 400, {
1493
+ code: "bad-request",
1494
+ message: "loginId is required."
1495
+ });
1496
+ return;
1497
+ }
1498
+ response.writeHead(200, {
1499
+ "content-type": "text/event-stream; charset=utf-8",
1500
+ "cache-control": "no-store",
1501
+ connection: "keep-alive",
1502
+ "x-content-type-options": "nosniff"
1503
+ });
1504
+ response.write("retry: 1000\n\n");
1505
+ let terminal = false;
1506
+ let heartbeat;
1507
+ let unsubscribe = null;
1508
+ const cleanup = () => {
1509
+ if (heartbeat !== void 0) clearInterval(heartbeat);
1510
+ unsubscribe?.();
1511
+ unsubscribe = null;
1512
+ };
1513
+ const send = (event) => {
1514
+ response.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1515
+ if (event.type !== "pending") {
1516
+ terminal = true;
1517
+ queueMicrotask(() => {
1518
+ cleanup();
1519
+ response.end();
1520
+ });
1521
+ }
1522
+ };
1523
+ unsubscribe = oauth.subscribe(loginId, send);
1524
+ if (unsubscribe === null) {
1525
+ response.end("event: failed\ndata: {\"type\":\"failed\",\"error\":{\"code\":\"bad-request\",\"message\":\"Unknown loginId.\"}}\n\n");
1526
+ return;
1527
+ }
1528
+ if (terminal) {
1529
+ unsubscribe();
1530
+ response.end();
1531
+ return;
1532
+ }
1533
+ heartbeat = setInterval(() => response.write(": ping\n\n"), 15e3);
1534
+ request.once("close", cleanup);
1535
+ };
1536
+ const disposers = [ctx.webServer.register({
1537
+ kind: "prefix",
1538
+ path: ROUTE_PREFIX,
1539
+ handler
1540
+ }), ctx.webServer.register({
1541
+ kind: "exact",
1542
+ path: `${ROUTE_PREFIX}/login/events`,
1543
+ handler: events
1544
+ })];
1545
+ return () => {
1546
+ for (const dispose of disposers) dispose();
1547
+ };
1548
+ }
1549
+ function isSameOriginMutation(request) {
1550
+ const host = request.headers.host;
1551
+ const origin = request.headers.origin;
1552
+ if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
1553
+ try {
1554
+ const parsed = new URL(origin);
1555
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
1556
+ } catch {
1557
+ return false;
1558
+ }
1559
+ }
1560
+ async function readJson(request) {
1561
+ const chunks = [];
1562
+ let total = 0;
1563
+ for await (const chunk of request) {
1564
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1565
+ total += buffer.length;
1566
+ if (total > MAX_BODY_BYTES) return null;
1567
+ chunks.push(buffer);
1568
+ }
1569
+ try {
1570
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1571
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1572
+ } catch {
1573
+ return null;
1574
+ }
1575
+ }
1576
+ function field(value, name) {
1577
+ const candidate = value[name];
1578
+ return typeof candidate === "string" && candidate !== "" ? candidate : null;
1579
+ }
1580
+ function json(response, envelope, status = 200) {
1581
+ response.writeHead(status, {
1582
+ "content-type": "application/json; charset=utf-8",
1583
+ "cache-control": "no-store",
1584
+ "x-content-type-options": "nosniff"
1585
+ });
1586
+ response.end(JSON.stringify(envelope));
1587
+ }
1588
+ function jsonError(response, status, error) {
1589
+ json(response, {
1590
+ ok: false,
1591
+ error
1592
+ }, status);
1593
+ }
1594
+ function statusFor(error) {
1595
+ if (error.code === "csrf-rejected") return 403;
1596
+ if (error.code === "not-authenticated") return 401;
1597
+ if (error.code === "rate-limited") return 429;
1598
+ if (error.code === "login-active") return 409;
1599
+ if (error.code === "bad-request") return 400;
1600
+ return 502;
1601
+ }
1602
+ //#endregion
1603
+ //#region src/host/token-store.ts
1604
+ function parseStoredCredentials(value) {
1605
+ if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
1606
+ const record = value;
1607
+ if (typeof record.accessToken !== "string" || record.accessToken === "") throw new Error("access token is missing");
1608
+ if (typeof record.refreshToken !== "string" || record.refreshToken === "") throw new Error("refresh token is missing");
1609
+ if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new Error("expiry is invalid");
1610
+ const optional = (key) => {
1611
+ const candidate = record[key];
1612
+ if (candidate === void 0) return void 0;
1613
+ if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
1614
+ return candidate;
1615
+ };
1616
+ return {
1617
+ accessToken: record.accessToken,
1618
+ refreshToken: record.refreshToken,
1619
+ expiresAt: record.expiresAt,
1620
+ idToken: optional("idToken"),
1621
+ accountId: optional("accountId"),
1622
+ email: optional("email"),
1623
+ planType: optional("planType")
1624
+ };
1625
+ }
1626
+ //#endregion
1627
+ //#region src/host/token-store-windows.ts
1628
+ const PROTECT_SCRIPT = String.raw`
1629
+ $ErrorActionPreference = 'Stop'
1630
+ Add-Type -AssemblyName System.Security
1631
+ $path = $env:DSH_CODEX_TOKEN_PATH
1632
+ $plain = [Console]::In.ReadToEnd()
1633
+ $bytes = [Text.Encoding]::UTF8.GetBytes($plain)
1634
+ $cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1635
+ $directory = [IO.Path]::GetDirectoryName($path)
1636
+ [IO.Directory]::CreateDirectory($directory) | Out-Null
1637
+ $temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
1638
+ [IO.File]::WriteAllBytes($temporary, $cipher)
1639
+ if ([IO.File]::Exists($path)) { [IO.File]::Replace($temporary, $path, $null) } else { [IO.File]::Move($temporary, $path) }
1640
+ `;
1641
+ const UNPROTECT_SCRIPT = String.raw`
1642
+ $ErrorActionPreference = 'Stop'
1643
+ Add-Type -AssemblyName System.Security
1644
+ $path = $env:DSH_CODEX_TOKEN_PATH
1645
+ if (-not [IO.File]::Exists($path)) { exit 3 }
1646
+ $cipher = [IO.File]::ReadAllBytes($path)
1647
+ $bytes = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1648
+ [Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
1649
+ `;
1650
+ const CLEAR_SCRIPT = String.raw`
1651
+ $ErrorActionPreference = 'Stop'
1652
+ $path = $env:DSH_CODEX_TOKEN_PATH
1653
+ if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
1654
+ `;
1655
+ function defaultDpapiCredentialPath() {
1656
+ return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
1657
+ }
1658
+ var WindowsDpapiTokenStore = class {
1659
+ path;
1660
+ constructor(path = defaultDpapiCredentialPath()) {
1661
+ this.path = path;
1662
+ if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
1663
+ if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
1664
+ }
1665
+ async load() {
1666
+ const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
1667
+ if (result.code === 3) return null;
1668
+ if (result.code !== 0) throw new Error("DPAPI credential read failed");
1669
+ try {
1670
+ return parseStoredCredentials(JSON.parse(result.stdout));
1671
+ } catch {
1672
+ throw new Error("DPAPI credential payload is invalid");
1673
+ }
1674
+ }
1675
+ async save(value) {
1676
+ if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
1677
+ }
1678
+ async clear() {
1679
+ if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
1680
+ }
1681
+ };
1682
+ function runPowerShell(script, path, stdin) {
1683
+ return new Promise((resolve, reject) => {
1684
+ const child = spawn("powershell.exe", [
1685
+ "-NoLogo",
1686
+ "-NoProfile",
1687
+ "-NonInteractive",
1688
+ "-Command",
1689
+ script
1690
+ ], {
1691
+ env: {
1692
+ ...process.env,
1693
+ DSH_CODEX_TOKEN_PATH: path
1694
+ },
1695
+ stdio: [
1696
+ "pipe",
1697
+ "pipe",
1698
+ "pipe"
1699
+ ],
1700
+ windowsHide: true
1701
+ });
1702
+ let stdout = "";
1703
+ let stderrLength = 0;
1704
+ const timer = setTimeout(() => {
1705
+ child.kill();
1706
+ reject(/* @__PURE__ */ new Error("DPAPI helper timed out"));
1707
+ }, 1e4);
1708
+ child.stdout.setEncoding("utf8");
1709
+ child.stdout.on("data", (chunk) => {
1710
+ stdout += chunk;
1711
+ if (stdout.length > 1 << 20) child.kill();
1712
+ });
1713
+ child.stderr.on("data", (chunk) => {
1714
+ stderrLength += chunk.length;
1715
+ if (stderrLength > 1 << 20) child.kill();
1716
+ });
1717
+ child.once("error", (error) => {
1718
+ clearTimeout(timer);
1719
+ reject(error);
1720
+ });
1721
+ child.once("close", (code) => {
1722
+ clearTimeout(timer);
1723
+ resolve({
1724
+ code: code ?? 1,
1725
+ stdout
1726
+ });
1727
+ });
1728
+ child.stdin.end(stdin);
1729
+ });
1730
+ }
1731
+ //#endregion
1732
+ //#region src/index.ts
1733
+ const inject = [
1734
+ "webServer",
1735
+ "llm",
1736
+ "attachments"
1737
+ ];
1738
+ function apply(ctx) {
1739
+ const oauth = new OAuthService(new WindowsDpapiTokenStore(), { logger: ctx.logger });
1740
+ const usage = new UsageService(oauth);
1741
+ const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, { onGenerationFinished: () => usage.invalidate() }));
1742
+ ctx.effect(() => {
1743
+ const disposeRoutes = registerRoutes(ctx, oauth, usage);
1744
+ const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
1745
+ return () => {
1746
+ disposeAdapter();
1747
+ disposeRoutes();
1748
+ oauth.dispose();
1749
+ };
1750
+ }, "dsh-chatgpt-subscription: adapter, routes, and lifecycle");
1751
+ }
1752
+ //#endregion
1753
+ export { CodexChatGptAdapter, OAuthService, ResponsesClient, UsageService, apply, inject, mapCodexUsage, parseResponsesStream };