dsh-codex-subscription 1.2.2 → 1.4.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/lib/index.js CHANGED
@@ -431,13 +431,20 @@ function createCodexRpcHandler(coordinator, options = {}) {
431
431
  //#endregion
432
432
  //#region src/settings-contract.js
433
433
  const SETTINGS_NAMESPACE = "codex-subscription";
434
- const QUICK_QUOTA_FIELD = "quickQuotaVisible";
434
+ const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
435
+ const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
436
+ const QUICK_QUOTA_MODE_PERCENT = "percent";
435
437
  const SEARCH_PROVIDER_FIELD = "searchProvider";
436
438
  const SEARCH_PROVIDER_CODEX = "codex";
437
439
  const SPEED_MODE_FIELD = "speedMode";
438
440
  const SPEED_MODE_STANDARD = "standard";
439
441
  const SPEED_MODE_FAST = "fast";
440
442
  const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
443
+ const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
444
+ "off",
445
+ "percent",
446
+ "bar"
447
+ ].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
441
448
  const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
442
449
  //#endregion
443
450
  //#region src/pi-ai-runtime.js
@@ -496,7 +503,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
496
503
  }
497
504
  //#endregion
498
505
  //#region src/version.js
499
- const PACKAGE_VERSION = "1.2.2";
506
+ const PACKAGE_VERSION = "1.4.0";
500
507
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
501
508
  //#endregion
502
509
  //#region src/codex-search.js
@@ -505,7 +512,7 @@ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
505
512
  const DEFAULT_MODEL = "gpt-5.6-luna";
506
513
  const MAX_OUTPUT_TOKENS = 4096;
507
514
  const MAX_SOURCE_DATE = 64;
508
- const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
515
+ const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
509
516
  const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
510
517
  const displayText = (value) => {
511
518
  const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
@@ -518,7 +525,7 @@ const boundedDisplayText = (value, maximum) => {
518
525
  return `${text.slice(0, maximum - 1)}…`;
519
526
  };
520
527
  function sourceOf(value) {
521
- if (!record$2(value)) return void 0;
528
+ if (!record$3(value)) return void 0;
522
529
  const url = nonEmpty$1(value.url);
523
530
  if (url === void 0) return void 0;
524
531
  let parsed;
@@ -539,7 +546,7 @@ function sourceOf(value) {
539
546
  };
540
547
  }
541
548
  function parseSearchResponse(value) {
542
- if (!record$2(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
549
+ if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
543
550
  const seen = /* @__PURE__ */ new Set();
544
551
  const sources = [];
545
552
  for (const result of value.results ?? []) {
@@ -631,7 +638,7 @@ const PNG_SIGNATURE = Buffer.from([
631
638
  26,
632
639
  10
633
640
  ]);
634
- const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
641
+ const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
635
642
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
636
643
  function encodedLimit(decodedBytes) {
637
644
  return Math.ceil(decodedBytes / 3) * 4;
@@ -732,7 +739,7 @@ function imageOutputSchema() {
732
739
  }
733
740
  function responseMetadata(value) {
734
741
  const data = Array.isArray(value?.data) ? value.data[0] : void 0;
735
- const encoded = record$1(data) ? data.b64_json : void 0;
742
+ const encoded = record$2(data) ? data.b64_json : void 0;
736
743
  if (typeof encoded !== "string") throw new Error("Codex returned no image data");
737
744
  return {
738
745
  encoded,
@@ -865,11 +872,11 @@ async function createSubscriptionDiagnostics({ auth, preferences }) {
865
872
  //#region src/usage.js
866
873
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
867
874
  const DEFAULT_TTL_MS = 6e4;
868
- const DEFAULT_TIMEOUT_MS = 15e3;
869
- const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
875
+ const DEFAULT_TIMEOUT_MS$1 = 15e3;
876
+ const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
870
877
  function windowOf(value) {
871
878
  if (value === void 0 || value === null) return void 0;
872
- if (!record(value)) throw new Error("Codex returned a malformed rate-limit window");
879
+ if (!record$1(value)) throw new Error("Codex returned a malformed rate-limit window");
873
880
  const used = value.used_percent;
874
881
  const seconds = value.limit_window_seconds;
875
882
  if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
@@ -884,7 +891,7 @@ function windowOf(value) {
884
891
  }
885
892
  function limitOf(id, name, value) {
886
893
  if (value === void 0 || value === null) return void 0;
887
- if (!record(value)) throw new Error("Codex returned malformed rate-limit details");
894
+ if (!record$1(value)) throw new Error("Codex returned malformed rate-limit details");
888
895
  const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
889
896
  return windows.length === 0 ? void 0 : {
890
897
  id,
@@ -903,7 +910,7 @@ function epochSeconds(value, label) {
903
910
  }
904
911
  function creditsOf(value) {
905
912
  if (value === void 0 || value === null) return void 0;
906
- if (!record(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
913
+ if (!record$1(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
907
914
  if (!value.has_credits) return void 0;
908
915
  return {
909
916
  unlimited: value.unlimited,
@@ -912,10 +919,10 @@ function creditsOf(value) {
912
919
  }
913
920
  function individualOf(value) {
914
921
  if (value === void 0 || value === null) return void 0;
915
- if (!record(value)) throw new Error("Codex returned malformed spend control");
922
+ if (!record$1(value)) throw new Error("Codex returned malformed spend control");
916
923
  const item = value.individual_limit;
917
924
  if (item === void 0 || item === null) return void 0;
918
- if (!record(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
925
+ if (!record$1(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
919
926
  const resetsAt = epochSeconds(item.reset_at, "individual-limit reset time");
920
927
  return {
921
928
  limit: decimal(item.limit, "individual limit"),
@@ -926,19 +933,19 @@ function individualOf(value) {
926
933
  }
927
934
  function spendControlReachedOf(value) {
928
935
  if (value === void 0 || value === null) return void 0;
929
- if (!record(value)) throw new Error("Codex returned malformed spend control");
936
+ if (!record$1(value)) throw new Error("Codex returned malformed spend control");
930
937
  if (value.reached === void 0 || value.reached === null) return void 0;
931
938
  if (typeof value.reached !== "boolean") throw new Error("Codex returned an invalid spend-control state");
932
939
  return value.reached;
933
940
  }
934
941
  function resetCreditsOf(value) {
935
942
  if (value === void 0 || value === null) return void 0;
936
- if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
943
+ if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
937
944
  return { availableCount: value.available_count };
938
945
  }
939
946
  /** Reduce the provider payload to a browser-safe quota projection. */
940
947
  function parseCodexUsage(value) {
941
- if (!record(value)) throw new Error("Codex returned a malformed usage response");
948
+ if (!record$1(value)) throw new Error("Codex returned a malformed usage response");
942
949
  const rateLimits = [];
943
950
  const seenLimitIds = /* @__PURE__ */ new Set();
944
951
  const addLimit = (limit) => {
@@ -949,7 +956,7 @@ function parseCodexUsage(value) {
949
956
  addLimit(limitOf("codex", "Codex", value.rate_limit));
950
957
  if (value.additional_rate_limits !== void 0 && value.additional_rate_limits !== null && !Array.isArray(value.additional_rate_limits)) throw new Error("Codex returned malformed additional rate limits");
951
958
  for (const entry of value.additional_rate_limits ?? []) {
952
- if (!record(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
959
+ if (!record$1(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
953
960
  if (entry.limit_name !== void 0 && entry.limit_name !== null && typeof entry.limit_name !== "string") throw new Error("Codex returned an invalid additional rate-limit name");
954
961
  addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
955
962
  }
@@ -966,7 +973,7 @@ function parseCodexUsage(value) {
966
973
  ...resetCredits === void 0 ? {} : { resetCredits }
967
974
  };
968
975
  }
969
- const requestSignal = (signal, timeoutMs) => {
976
+ const requestSignal$1 = (signal, timeoutMs) => {
970
977
  const timeout = AbortSignal.timeout(timeoutMs);
971
978
  return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
972
979
  };
@@ -981,7 +988,7 @@ function createCodexUsageReader(options) {
981
988
  const fetchUsage = options.fetch ?? fetch;
982
989
  const now = options.now ?? Date.now;
983
990
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
984
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
991
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
985
992
  let cached;
986
993
  let inFlight;
987
994
  let generation = 0;
@@ -1001,7 +1008,7 @@ function createCodexUsageReader(options) {
1001
1008
  "cache-control": "no-store",
1002
1009
  "user-agent": USER_AGENT
1003
1010
  },
1004
- signal: requestSignal(signal, timeoutMs)
1011
+ signal: requestSignal$1(signal, timeoutMs)
1005
1012
  });
1006
1013
  if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
1007
1014
  let value;
@@ -1037,6 +1044,185 @@ function createCodexUsageReader(options) {
1037
1044
  });
1038
1045
  }
1039
1046
  //#endregion
1047
+ //#region src/reset-credits.js
1048
+ const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1049
+ const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
1050
+ const RESET_CONFIRM_PHRASE = "USE RESET";
1051
+ const DEFAULT_CONFIRM_DELAY_MS = 5e3;
1052
+ const DEFAULT_CHALLENGE_TTL_MS = 6e4;
1053
+ const DEFAULT_TIMEOUT_MS = 15e3;
1054
+ const MAX_COPY_LENGTH = 240;
1055
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1056
+ const requestSignal = (signal, timeoutMs) => {
1057
+ const timeout = AbortSignal.timeout(timeoutMs);
1058
+ return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
1059
+ };
1060
+ function safeCopy(value) {
1061
+ return typeof value === "string" && value.length > 0 ? value.slice(0, MAX_COPY_LENGTH) : void 0;
1062
+ }
1063
+ function credentialsOf(auth, credential) {
1064
+ const access = auth?.auth?.apiKey;
1065
+ const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
1066
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
1067
+ return {
1068
+ access,
1069
+ accountId
1070
+ };
1071
+ }
1072
+ function parseDetails(value, now) {
1073
+ if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0 || !Array.isArray(value.credits)) throw new Error("ChatGPT returned malformed quota reset details");
1074
+ if (value.available_count === 0) throw new Error("No quota reset is available");
1075
+ const available = value.credits.filter((credit) => record(credit) && typeof credit.id === "string" && credit.id.length > 0 && credit.id.length <= 256 && typeof credit.status === "string" && credit.status.toLowerCase() === "available").map((credit) => {
1076
+ const expiresAt = credit.expires_at === void 0 || credit.expires_at === null ? void 0 : credit.expires_at * 1e3;
1077
+ if (expiresAt !== void 0 && (!Number.isSafeInteger(credit.expires_at) || credit.expires_at <= 0)) throw new Error("ChatGPT returned malformed quota reset details");
1078
+ return {
1079
+ credit,
1080
+ expiresAt
1081
+ };
1082
+ }).filter(({ expiresAt }) => expiresAt === void 0 || expiresAt > now).sort((a, b) => (a.expiresAt ?? Number.MAX_SAFE_INTEGER) - (b.expiresAt ?? Number.MAX_SAFE_INTEGER));
1083
+ if (available.length === 0) throw new Error("No usable quota reset is available");
1084
+ return {
1085
+ availableCount: value.available_count,
1086
+ creditId: available[0].credit.id,
1087
+ title: safeCopy(available[0].credit.title),
1088
+ description: safeCopy(available[0].credit.description),
1089
+ creditExpiresAt: available[0].expiresAt
1090
+ };
1091
+ }
1092
+ function parseConsumeResult(value) {
1093
+ if (!record(value) || ![
1094
+ "reset",
1095
+ "nothing_to_reset",
1096
+ "no_credit",
1097
+ "already_redeemed"
1098
+ ].includes(value.code)) throw new Error("ChatGPT returned an unreadable quota reset response");
1099
+ const windowsReset = Array.isArray(value.windows_reset) ? value.windows_reset.filter((item) => typeof item === "string").slice(0, 16) : [];
1100
+ return {
1101
+ code: value.code,
1102
+ windowsReset
1103
+ };
1104
+ }
1105
+ function quotaIsExhausted(usage) {
1106
+ return Array.isArray(usage?.rateLimits) && usage.rateLimits.some((limit) => limit?.id !== "code_review" && Array.isArray(limit?.windows) && limit.windows.some((window) => Number.isFinite(window?.usedPercent) && window.usedPercent >= 100));
1107
+ }
1108
+ /**
1109
+ * Host-only reset redemption. The browser receives an opaque, short-lived
1110
+ * challenge; account ids, bearer tokens, credit ids, and idempotency keys stay
1111
+ * in memory on the host.
1112
+ */
1113
+ function createCodexResetCreditService(options) {
1114
+ const getAuth = options.getAuth;
1115
+ const readCredential = options.readCredential;
1116
+ const usageReader = options.usageReader;
1117
+ const fetchReset = options.fetch ?? fetch;
1118
+ const now = options.now ?? Date.now;
1119
+ const randomUUID$1 = options.randomUUID ?? randomUUID;
1120
+ const confirmDelayMs = options.confirmDelayMs ?? DEFAULT_CONFIRM_DELAY_MS;
1121
+ const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
1122
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1123
+ const challenges = /* @__PURE__ */ new Map();
1124
+ const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
1125
+ return Object.freeze({
1126
+ async prepare({ signal } = {}) {
1127
+ const { access, accountId } = await resolveCredentials(signal);
1128
+ const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1129
+ method: "GET",
1130
+ redirect: "error",
1131
+ headers: {
1132
+ authorization: `Bearer ${access}`,
1133
+ "chatgpt-account-id": accountId,
1134
+ accept: "application/json",
1135
+ "cache-control": "no-store",
1136
+ "user-agent": USER_AGENT
1137
+ },
1138
+ signal: requestSignal(signal, timeoutMs)
1139
+ });
1140
+ if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
1141
+ let raw;
1142
+ try {
1143
+ raw = await response.json();
1144
+ } catch {
1145
+ throw new Error("ChatGPT returned unreadable quota reset details");
1146
+ }
1147
+ const details = parseDetails(raw, now());
1148
+ const preparedAt = now();
1149
+ const readyAt = preparedAt + confirmDelayMs;
1150
+ const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
1151
+ if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
1152
+ const challengeId = randomUUID$1();
1153
+ challenges.set(challengeId, {
1154
+ state: "prepared",
1155
+ accountId,
1156
+ creditId: details.creditId,
1157
+ redeemRequestId: randomUUID$1(),
1158
+ readyAt,
1159
+ expiresAt
1160
+ });
1161
+ return {
1162
+ challengeId,
1163
+ availableCount: details.availableCount,
1164
+ confirmPhrase: RESET_CONFIRM_PHRASE,
1165
+ readyAt,
1166
+ expiresAt,
1167
+ ...details.title === void 0 ? {} : { title: details.title },
1168
+ ...details.description === void 0 ? {} : { description: details.description }
1169
+ };
1170
+ },
1171
+ async consume({ challengeId, phrase, signal } = {}) {
1172
+ const challenge = typeof challengeId === "string" ? challenges.get(challengeId) : void 0;
1173
+ if (challenge === void 0) throw new Error("This quota reset confirmation is no longer valid");
1174
+ if (challenge.state === "pending") throw new Error("This quota reset is already in progress");
1175
+ if (now() < challenge.readyAt) throw new Error("Wait before confirming this quota reset");
1176
+ if (now() > challenge.expiresAt) {
1177
+ challenges.delete(challengeId);
1178
+ throw new Error("This quota reset confirmation is no longer valid");
1179
+ }
1180
+ if (phrase !== "USE RESET") throw new Error("The quota reset confirmation phrase does not match");
1181
+ challenge.state = "pending";
1182
+ try {
1183
+ if (!quotaIsExhausted(await usageReader.read({
1184
+ force: true,
1185
+ signal
1186
+ }))) throw new Error("The current Codex quota is not exhausted");
1187
+ const { access, accountId } = await resolveCredentials(signal);
1188
+ if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
1189
+ const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
1190
+ method: "POST",
1191
+ redirect: "error",
1192
+ headers: {
1193
+ authorization: `Bearer ${access}`,
1194
+ "chatgpt-account-id": accountId,
1195
+ accept: "application/json",
1196
+ "content-type": "application/json",
1197
+ "cache-control": "no-store",
1198
+ "user-agent": USER_AGENT
1199
+ },
1200
+ body: JSON.stringify({
1201
+ redeem_request_id: challenge.redeemRequestId,
1202
+ credit_id: challenge.creditId
1203
+ }),
1204
+ signal: requestSignal(signal, timeoutMs)
1205
+ });
1206
+ if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
1207
+ let raw;
1208
+ try {
1209
+ raw = await response.json();
1210
+ } catch {
1211
+ throw new Error("ChatGPT returned an unreadable quota reset response");
1212
+ }
1213
+ const result = parseConsumeResult(raw);
1214
+ usageReader.clear();
1215
+ return result;
1216
+ } finally {
1217
+ challenges.delete(challengeId);
1218
+ }
1219
+ },
1220
+ clear() {
1221
+ challenges.clear();
1222
+ }
1223
+ });
1224
+ }
1225
+ //#endregion
1040
1226
  //#region src/index.js
1041
1227
  const name = "codex-subscription";
1042
1228
  const inject = [
@@ -1066,7 +1252,7 @@ const publicError = (code, message) => ({
1066
1252
  details: { issues: [] }
1067
1253
  }
1068
1254
  });
1069
- function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, diagnosticsReader }) {
1255
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader }) {
1070
1256
  return async (endpoint, payload, signal) => {
1071
1257
  if (endpoint === "diagnostics") try {
1072
1258
  signal.throwIfAborted();
@@ -1082,9 +1268,13 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
1082
1268
  signal.throwIfAborted();
1083
1269
  if (endpoint === "preferences/update") {
1084
1270
  const patch = {};
1085
- if (Object.hasOwn(payload ?? {}, "quickQuotaVisible")) {
1086
- if (typeof payload["quickQuotaVisible"] !== "boolean") return publicError("internal", "Invalid quick quota preference");
1087
- patch[QUICK_QUOTA_FIELD] = payload[QUICK_QUOTA_FIELD];
1271
+ if (Object.hasOwn(payload ?? {}, "quickQuotaMode")) {
1272
+ if (![
1273
+ "off",
1274
+ "percent",
1275
+ "bar"
1276
+ ].includes(payload["quickQuotaMode"])) return publicError("internal", "Invalid quick quota preference");
1277
+ patch[QUICK_QUOTA_MODE_FIELD] = payload[QUICK_QUOTA_MODE_FIELD];
1088
1278
  }
1089
1279
  if (Object.hasOwn(payload ?? {}, "searchProvider")) {
1090
1280
  if (!["dsh", "codex"].includes(payload["searchProvider"])) return publicError("internal", "Invalid search provider preference");
@@ -1120,8 +1310,40 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
1120
1310
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
1121
1311
  return publicError("internal", message);
1122
1312
  }
1313
+ if (endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
1314
+ signal.throwIfAborted();
1315
+ return {
1316
+ ok: true,
1317
+ value: endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
1318
+ challengeId: payload?.challengeId,
1319
+ phrase: payload?.phrase,
1320
+ signal
1321
+ })
1322
+ };
1323
+ } catch (error) {
1324
+ if (signal.aborted) throw error;
1325
+ const known = /* @__PURE__ */ new Set([
1326
+ "ChatGPT subscription is not signed in",
1327
+ "ChatGPT sign-in needs to be renewed",
1328
+ "No quota reset is available",
1329
+ "No usable quota reset is available",
1330
+ "The available quota reset expires too soon",
1331
+ "This quota reset confirmation is no longer valid",
1332
+ "This quota reset is already in progress",
1333
+ "Wait before confirming this quota reset",
1334
+ "The quota reset confirmation phrase does not match",
1335
+ "The current Codex quota is not exhausted",
1336
+ "The signed-in ChatGPT account changed"
1337
+ ]);
1338
+ const fallback = endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
1339
+ const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
1340
+ return publicError("internal", message);
1341
+ }
1123
1342
  const result = await authHandler(endpoint, payload, signal);
1124
- if (endpoint === "logout" && result.ok === true) usageReader.clear();
1343
+ if (endpoint === "logout" && result.ok === true) {
1344
+ usageReader.clear();
1345
+ resetCreditService.clear();
1346
+ }
1125
1347
  return result;
1126
1348
  };
1127
1349
  }
@@ -1144,14 +1366,19 @@ function createSearchProviderSwitcher(loader) {
1144
1366
  }
1145
1367
  function apply(ctx) {
1146
1368
  const settings = ctx.settings.register(settingsNamespace(SETTINGS_NAMESPACE), z.object({
1147
- [QUICK_QUOTA_FIELD]: z.boolean().default(false),
1369
+ [QUICK_QUOTA_MODE_FIELD]: z.union([
1370
+ "off",
1371
+ QUICK_QUOTA_MODE_PERCENT,
1372
+ "bar"
1373
+ ]),
1374
+ [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
1148
1375
  [SEARCH_PROVIDER_FIELD]: z.union(["dsh", SEARCH_PROVIDER_CODEX]).default("dsh"),
1149
1376
  [SPEED_MODE_FIELD]: z.union([SPEED_MODE_STANDARD, SPEED_MODE_FAST]).default(DEFAULT_SPEED_MODE)
1150
1377
  }));
1151
1378
  const searchProvider = createSearchProviderSwitcher(ctx.loader);
1152
1379
  const preferences = {
1153
1380
  status: () => ({
1154
- [QUICK_QUOTA_FIELD]: settings.get()[QUICK_QUOTA_FIELD],
1381
+ [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
1155
1382
  [SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
1156
1383
  [SPEED_MODE_FIELD]: settings.get()[SPEED_MODE_FIELD],
1157
1384
  writable: ctx.settings.writable
@@ -1231,9 +1458,15 @@ function apply(ctx) {
1231
1458
  getAuth: resolveAuth,
1232
1459
  readCredential: (options) => store.read(PROVIDER, options)
1233
1460
  });
1461
+ const resetCreditService = createCodexResetCreditService({
1462
+ getAuth: resolveAuth,
1463
+ readCredential: (options) => store.read(PROVIDER, options),
1464
+ usageReader
1465
+ });
1234
1466
  const handler = createSubscriptionRpcHandler({
1235
1467
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
1236
1468
  usageReader,
1469
+ resetCreditService,
1237
1470
  preferences,
1238
1471
  diagnosticsReader: () => createSubscriptionDiagnostics({
1239
1472
  auth,
@@ -1243,4 +1476,4 @@ function apply(ctx) {
1243
1476
  ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
1244
1477
  }
1245
1478
  //#endregion
1246
- export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
1479
+ export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, RESET_CONFIRM_PHRASE, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.2.2",
4
- "description": "ChatGPT and Codex subscriptions for DeepSeek Harness with OAuth login, image generation, quota, and subscription web search",
3
+ "version": "1.4.0",
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",
7
7
  "exports": {
@@ -14,7 +14,7 @@
14
14
  "lib/*.js",
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
- "README.en.md",
17
+ "README.zh-CN.md",
18
18
  "DIRECTORY.md",
19
19
  "AGENTS.md",
20
20
  "compatibility.json",
@@ -52,6 +52,8 @@
52
52
  "chatgpt-plus",
53
53
  "subscription",
54
54
  "quota",
55
+ "quota-reset",
56
+ "fast-mode",
55
57
  "usage",
56
58
  "oauth",
57
59
  "web-search",