dsh-codex-subscription 1.14.2 → 1.14.4

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/lib/index.js CHANGED
@@ -565,6 +565,35 @@ const badRequest = (message) => ({
565
565
  details: { issues: [] }
566
566
  }
567
567
  });
568
+ const accountStatusError = (message) => ({
569
+ ok: false,
570
+ error: {
571
+ code: "internal",
572
+ message,
573
+ details: { issues: [] }
574
+ }
575
+ });
576
+ const classifyAccountStatusError = (error) => {
577
+ const message = error instanceof Error ? error.message : "";
578
+ if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
579
+ if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
580
+ const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
581
+ if (error?.name === "TimeoutError" || [
582
+ "TIMEOUT",
583
+ "ETIMEDOUT",
584
+ "UND_ERR_CONNECT_TIMEOUT"
585
+ ].includes(code)) return ["transport", "Codex account status service is unavailable"];
586
+ if ([
587
+ "ECONNRESET",
588
+ "ECONNREFUSED",
589
+ "ENOTFOUND",
590
+ "EAI_AGAIN",
591
+ "NETWORK",
592
+ "NETWORK_ERROR",
593
+ "TRANSPORT"
594
+ ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
595
+ return ["unknown", "Could not read Codex account status"];
596
+ };
568
597
  const deferred = () => {
569
598
  let resolve;
570
599
  let reject;
@@ -804,7 +833,13 @@ function createCodexRpcHandler(coordinator, options = {}) {
804
833
  try {
805
834
  signal.throwIfAborted();
806
835
  const input = asObject(payload);
807
- if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
836
+ if (endpoint === "status") try {
837
+ return ok(await coordinator.accountStatus({ signal }));
838
+ } catch (error) {
839
+ if (signal.aborted) throw error;
840
+ const [, message] = classifyAccountStatusError(error);
841
+ return accountStatusError(message);
842
+ }
808
843
  if (endpoint === "login/start") {
809
844
  const started = await coordinator.start({
810
845
  method: input.method,
@@ -1310,7 +1345,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1310
1345
  }
1311
1346
  //#endregion
1312
1347
  //#region src/version.js
1313
- const PACKAGE_VERSION = "1.14.2";
1348
+ const PACKAGE_VERSION = "1.14.4";
1314
1349
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1315
1350
  //#endregion
1316
1351
  //#region src/model-catalog.js
@@ -1324,6 +1359,7 @@ const LEVELS = [
1324
1359
  "xhigh",
1325
1360
  "max"
1326
1361
  ];
1362
+ const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
1327
1363
  const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1328
1364
  const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1329
1365
  const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -1384,16 +1420,35 @@ function mergeModel(baseModels, remote) {
1384
1420
  }
1385
1421
  function createOfficialModelCatalog(options = {}) {
1386
1422
  const fetchCatalog = options.fetch ?? fetch;
1423
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
1424
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
1425
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
1387
1426
  let models;
1388
1427
  let metadata = /* @__PURE__ */ new Map();
1389
1428
  let etag;
1390
1429
  let revision = 0;
1391
1430
  let refreshing;
1392
- const refresh = async ({ signal } = {}) => {
1393
- if (refreshing !== void 0) return refreshing;
1394
- refreshing = (async () => {
1395
- const auth = await options.getAuth({ signal });
1396
- const credential = await options.readCredential({ signal });
1431
+ let generation = 0;
1432
+ let refreshStatus = "idle";
1433
+ const refresh = ({ signal } = {}) => {
1434
+ if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1435
+ if (refreshing?.generation === generation) return refreshing.promise;
1436
+ const currentGeneration = generation;
1437
+ refreshStatus = "refreshing";
1438
+ let outcome = "idle";
1439
+ const controller = new AbortController();
1440
+ const abort = () => {
1441
+ if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1442
+ };
1443
+ signal?.addEventListener("abort", abort, { once: true });
1444
+ const requestSignal = controller.signal;
1445
+ let timer;
1446
+ const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
1447
+ const work = (async () => {
1448
+ const auth = await options.getAuth({ signal: requestSignal });
1449
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1450
+ const credential = await options.readCredential({ signal: requestSignal });
1451
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1397
1452
  const access = auth?.auth?.apiKey;
1398
1453
  const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
1399
1454
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
@@ -1409,34 +1464,73 @@ function createOfficialModelCatalog(options = {}) {
1409
1464
  method: "GET",
1410
1465
  redirect: "error",
1411
1466
  headers,
1412
- signal
1467
+ signal: requestSignal
1413
1468
  });
1414
- if (response.status === 304) return false;
1469
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1470
+ if (response.status === 304) {
1471
+ outcome = "ok";
1472
+ return false;
1473
+ }
1415
1474
  if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
1416
1475
  const remote = parseOfficialModelCatalog(await response.json());
1476
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1417
1477
  if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
1418
1478
  const baseModels = options.baseModels();
1419
1479
  const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
1420
1480
  if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
1481
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
1421
1482
  models = next;
1422
1483
  metadata = new Map(remote.map((model) => [model.id, model]));
1423
1484
  etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
1424
1485
  revision += 1;
1486
+ outcome = "ok";
1425
1487
  return true;
1426
- })().finally(() => {
1427
- refreshing = void 0;
1488
+ })();
1489
+ let rejectAborted;
1490
+ const abortPromise = new Promise((_, reject) => {
1491
+ rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1492
+ if (requestSignal.aborted) rejectAborted();
1493
+ else requestSignal.addEventListener("abort", rejectAborted, { once: true });
1494
+ });
1495
+ timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
1496
+ timer.unref?.();
1497
+ const promise = Promise.race([work, abortPromise]).catch((error) => {
1498
+ outcome = "failed";
1499
+ throw error;
1500
+ }).finally(() => {
1501
+ cancelTimeout(timer);
1502
+ signal?.removeEventListener("abort", abort);
1503
+ requestSignal.removeEventListener("abort", rejectAborted);
1504
+ if (refreshing?.promise === promise) {
1505
+ refreshing = void 0;
1506
+ refreshStatus = outcome;
1507
+ }
1428
1508
  });
1429
- return refreshing;
1509
+ refreshing = {
1510
+ generation: currentGeneration,
1511
+ promise,
1512
+ cancel: () => controller.abort()
1513
+ };
1514
+ return promise;
1430
1515
  };
1431
1516
  return Object.freeze({
1432
1517
  refresh,
1433
1518
  getModels: (fallback) => models ?? fallback,
1434
1519
  metadata: (modelId) => metadata.get(modelId),
1435
1520
  revision: () => revision,
1521
+ status: () => ({
1522
+ source: models === void 0 ? "fallback" : "online",
1523
+ refresh: refreshStatus
1524
+ }),
1436
1525
  clear() {
1526
+ generation += 1;
1527
+ const flight = refreshing;
1528
+ refreshing = void 0;
1529
+ flight?.cancel();
1437
1530
  models = void 0;
1438
1531
  metadata = /* @__PURE__ */ new Map();
1439
1532
  etag = void 0;
1533
+ refreshStatus = "idle";
1440
1534
  revision += 1;
1441
1535
  }
1442
1536
  });
@@ -2192,7 +2286,8 @@ var OriginalImageStore = class {
2192
2286
  await Promise.all([assertPrivateFile(metadataFile), assertPrivateFile(originalFile)]);
2193
2287
  const metadata = parseMetadata(await readFile(metadataFile, "utf8"));
2194
2288
  if (metadata === void 0 || metadata.image.assetId !== assetId || metadata.sessionId !== sessionId && !originalImageRefsEqual(metadata.image, inherited)) return void 0;
2195
- const data = new Uint8Array(await readFile(originalFile));
2289
+ const buffer = await readFile(originalFile);
2290
+ const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
2196
2291
  const dimensions = pngDimensions(data);
2197
2292
  if (data.byteLength !== metadata.image.bytes || digest(data) !== metadata.image.sha256 || dimensions.width !== metadata.image.width || dimensions.height !== metadata.image.height) return void 0;
2198
2293
  return {
@@ -2208,10 +2303,11 @@ var OriginalImageStore = class {
2208
2303
  const stored = await this.read(sessionId, assetId, inherited);
2209
2304
  if (stored === void 0 || offset >= stored.data.byteLength || offset % 4194304 !== 0) return void 0;
2210
2305
  const end = Math.min(stored.data.byteLength, offset + ORIGINAL_IMAGE_CHUNK_BYTES);
2306
+ const chunk = Buffer.from(stored.data.buffer, stored.data.byteOffset + offset, end - offset);
2211
2307
  return {
2212
2308
  ref: stored.ref,
2213
2309
  offset,
2214
- encoded: Buffer.from(stored.data.subarray(offset, end)).toString("base64"),
2310
+ encoded: chunk.toString("base64"),
2215
2311
  done: end === stored.data.byteLength
2216
2312
  };
2217
2313
  }
@@ -2221,6 +2317,7 @@ var OriginalImageStore = class {
2221
2317
  const requestAreas = /* @__PURE__ */ new Set([
2222
2318
  "login",
2223
2319
  "model",
2320
+ "catalog",
2224
2321
  "quota",
2225
2322
  "quota-reset",
2226
2323
  "search",
@@ -2266,7 +2363,7 @@ function safeRequests(network) {
2266
2363
  return result;
2267
2364
  }
2268
2365
  /** Build a support report that deliberately excludes OAuth and account metadata. */
2269
- async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network }) {
2366
+ async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network, modelCatalog }) {
2270
2367
  let account = { status: "unknown" };
2271
2368
  const issues = [];
2272
2369
  try {
@@ -2275,6 +2372,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
2275
2372
  issues.push({ code: "account-status-unavailable" });
2276
2373
  }
2277
2374
  const preference = preferences.status();
2375
+ const catalog = modelCatalog?.status?.();
2278
2376
  return {
2279
2377
  schemaVersion: 3,
2280
2378
  package: "dsh-codex-subscription",
@@ -2287,6 +2385,15 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
2287
2385
  account,
2288
2386
  login,
2289
2387
  requests: safeRequests(network),
2388
+ ...catalog && ["fallback", "online"].includes(catalog.source) && [
2389
+ "idle",
2390
+ "refreshing",
2391
+ "ok",
2392
+ "failed"
2393
+ ].includes(catalog.refresh) ? { catalog: {
2394
+ source: catalog.source,
2395
+ refresh: catalog.refresh
2396
+ } } : {},
2290
2397
  configuration: {
2291
2398
  contextMode: preference.contextMode,
2292
2399
  quickQuotaMode: preference.quickQuotaMode,
@@ -2673,40 +2780,67 @@ function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options
2673
2780
  function createQuotaForecastReader({ reader, enabled, now = Date.now, scope = () => "default", stateStore }) {
2674
2781
  let state = { windows: {} };
2675
2782
  let loaded = false;
2783
+ let loading;
2784
+ let generation = 0;
2785
+ let historyGeneration = 0;
2786
+ let persistence = Promise.resolve();
2787
+ const persist = (operation) => {
2788
+ const pending = persistence.then(operation);
2789
+ persistence = pending.catch(() => {});
2790
+ return pending;
2791
+ };
2676
2792
  const load = async () => {
2677
2793
  if (loaded) return;
2794
+ if (loading) return loading;
2795
+ const current = historyGeneration;
2796
+ const pending = Promise.resolve().then(() => stateStore?.load?.()).then((restored) => {
2797
+ if (current !== historyGeneration) return;
2798
+ if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
2799
+ loaded = true;
2800
+ }).finally(() => {
2801
+ if (loading === pending) loading = void 0;
2802
+ });
2803
+ loading = pending;
2804
+ return pending;
2805
+ };
2806
+ const clearHistory = (clearReader = true) => {
2807
+ generation += 1;
2808
+ historyGeneration += 1;
2809
+ state = { windows: {} };
2678
2810
  loaded = true;
2679
- const restored = await stateStore?.load?.();
2680
- if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
2811
+ if (clearReader) reader.clear();
2812
+ return persist(() => stateStore?.clear?.());
2681
2813
  };
2682
2814
  return Object.freeze({
2683
2815
  async read(options) {
2816
+ const current = generation;
2817
+ const account = await scope();
2684
2818
  const usage = await reader.read(options);
2819
+ if (current !== generation) return usage;
2685
2820
  await load();
2821
+ const activeAccount = await scope();
2822
+ if (current !== generation || account !== activeAccount) return usage;
2686
2823
  if (!enabled()) {
2687
- state = { windows: {} };
2688
- await stateStore?.clear?.();
2824
+ await clearHistory(false);
2689
2825
  return usage;
2690
2826
  }
2691
- const forecast = forecastUsage(usage, state, now(), { scope: await scope() });
2827
+ const forecast = forecastUsage(usage, state, now(), { scope: account });
2692
2828
  state = forecast.state;
2693
- if (forecast.changed) await stateStore?.save?.(state);
2694
- return forecast.usage;
2695
- },
2696
- async clear() {
2697
- state = { windows: {} };
2698
- loaded = true;
2699
- reader.clear();
2700
- await stateStore?.clear?.();
2829
+ if (forecast.changed) await persist(() => stateStore?.save?.(forecast.state));
2830
+ return current === generation ? forecast.usage : usage;
2701
2831
  },
2832
+ clear: () => clearHistory(),
2702
2833
  clearCache() {
2834
+ generation += 1;
2703
2835
  reader.clear();
2704
2836
  },
2705
2837
  async clearScope(targetScope) {
2838
+ generation += 1;
2706
2839
  await load();
2707
2840
  const prefix = `[${JSON.stringify(cleanSegment(targetScope))},`;
2708
2841
  state = { windows: Object.fromEntries(Object.entries(state.windows).filter(([key]) => !key.startsWith(prefix))) };
2709
- await stateStore?.save?.(state);
2842
+ const snapshot = state;
2843
+ await persist(() => stateStore?.save?.(snapshot));
2710
2844
  }
2711
2845
  });
2712
2846
  }
@@ -3124,6 +3258,22 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3124
3258
  if (signal.aborted) throw error;
3125
3259
  return publicError("internal", "Could not create support diagnostics");
3126
3260
  }
3261
+ if (endpoint === "preferences/models") try {
3262
+ signal.throwIfAborted();
3263
+ if (typeof modelCatalog?.refresh !== "function" || typeof preferences?.status !== "function") return publicError("internal", "Could not refresh Codex model catalog");
3264
+ await modelCatalog.refresh({ signal });
3265
+ const value = preferences.status();
3266
+ return {
3267
+ ok: true,
3268
+ value: {
3269
+ contextModels: Array.isArray(value?.contextModels) ? value.contextModels : [],
3270
+ verbosityModels: Array.isArray(value?.verbosityModels) ? value.verbosityModels : []
3271
+ }
3272
+ };
3273
+ } catch (error) {
3274
+ if (signal.aborted) throw error;
3275
+ return publicError("internal", "Could not refresh Codex model catalog");
3276
+ }
3127
3277
  if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
3128
3278
  signal.throwIfAborted();
3129
3279
  if (endpoint === "preferences/update") {
@@ -3455,12 +3605,22 @@ function apply(ctx) {
3455
3605
  stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
3456
3606
  });
3457
3607
  ctx.effect(() => {
3608
+ let forecasting = false;
3458
3609
  const warmForecast = (value) => {
3459
- if (normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) !== "forecast") return;
3610
+ if (!(normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) === "forecast")) {
3611
+ if (forecasting) usageReader.clear().catch((error) => ctx.logger?.debug?.("could not clear Codex quota forecast: %s", error.message));
3612
+ forecasting = false;
3613
+ return;
3614
+ }
3615
+ forecasting = true;
3460
3616
  usageReader.read().catch((error) => ctx.logger?.debug?.("could not warm Codex quota forecast: %s", error.message));
3461
3617
  };
3462
3618
  warmForecast(settings.get());
3463
- return settings.watch(warmForecast);
3619
+ const unwatch = settings.watch(warmForecast);
3620
+ return () => {
3621
+ unwatch();
3622
+ usageReader.clearCache();
3623
+ };
3464
3624
  }, "codex-subscription: quota forecast warm-up");
3465
3625
  const resetCreditService = createCodexResetCreditService({
3466
3626
  getAuth: resolveAuth,
@@ -3477,7 +3637,8 @@ function apply(ctx) {
3477
3637
  auth,
3478
3638
  preferences,
3479
3639
  login: coordinator.supportState(),
3480
- network
3640
+ network,
3641
+ modelCatalog
3481
3642
  }),
3482
3643
  modelCatalog,
3483
3644
  originalImages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -127,8 +127,8 @@
127
127
  },
128
128
  "scripts": {
129
129
  "build": "tsdown --config tsdown.config.mjs",
130
- "test:behavior": "node --test tests/auth-security.test.mjs tests/cache-contract.test.mjs tests/codex-images.test.mjs tests/codex-search.test.mjs tests/image-edit.test.mjs tests/model-catalog.test.mjs tests/oauth-network.test.mjs tests/pi-ai-runtime.test.mjs tests/plugin-integration.test.mjs tests/reset-credits.test.mjs tests/sidebar-quota.test.mjs tests/transport-contract.test.mjs tests/usage.test.mjs",
131
- "test:delivery": "node --test tests/ci-change-plan.test.mjs tests/client-contract.test.mjs tests/prepare-compat-release.test.mjs tests/publish-idempotency.test.mjs tests/release-contract.test.mjs tests/release-notes.test.mjs tests/support-intake.test.mjs",
130
+ "test:behavior": "node scripts/run-tests.mjs behavior",
131
+ "test:delivery": "node scripts/run-tests.mjs delivery",
132
132
  "test": "node --test tests/*.test.mjs",
133
133
  "check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
134
134
  }