dsh-codex-subscription 1.12.1 → 1.13.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
@@ -21,6 +21,40 @@ import { dirname, join, resolve } from "node:path";
21
21
  const VERSION = 1;
22
22
  const DEFAULT_LABEL = "Account 1";
23
23
  const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
24
+ const EMAIL_MAX_LENGTH = 254;
25
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
26
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
27
+ function normalizeAccountEmail(value) {
28
+ if (typeof value !== "string") return void 0;
29
+ const email = value.trim();
30
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
31
+ }
32
+ function decodeJwtPayload(access) {
33
+ if (typeof access !== "string") return void 0;
34
+ const encoded = access.split(".")[1];
35
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
36
+ try {
37
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
38
+ } catch {
39
+ return;
40
+ }
41
+ }
42
+ function emailFromAccessToken(access) {
43
+ return normalizeAccountEmail(decodeJwtPayload(access)?.email);
44
+ }
45
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
46
+ function sanitizeOAuthCredential(value) {
47
+ const credential = assertOAuthCredential$1(value);
48
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
49
+ if (email === void 0) {
50
+ delete credential.email;
51
+ return credential;
52
+ }
53
+ return {
54
+ ...credential,
55
+ email
56
+ };
57
+ }
24
58
  function assertOAuthCredential$1(value) {
25
59
  if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
26
60
  return clone$1(value);
@@ -48,7 +82,7 @@ function assertVaultRecord(record) {
48
82
  return {
49
83
  id: account.id,
50
84
  label: normalizeLabel(account.label),
51
- credential: assertOAuthCredential$1(account.credential)
85
+ credential: sanitizeOAuthCredential(account.credential)
52
86
  };
53
87
  });
54
88
  if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
@@ -80,7 +114,7 @@ var PendingOAuthCredentialStore = class {
80
114
  async modify(providerId, update) {
81
115
  if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
82
116
  const next = await update(clone$1(this.#credential));
83
- if (next !== void 0) this.#credential = assertOAuthCredential$1(next);
117
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
84
118
  return clone$1(this.#credential);
85
119
  }
86
120
  async delete(providerId) {
@@ -170,7 +204,8 @@ var DshOAuthAccountVault = class {
170
204
  id: account.id,
171
205
  label: account.label,
172
206
  active: account.id === payload.activeId,
173
- expiresAt: account.credential.expires
207
+ expiresAt: account.credential.expires,
208
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
174
209
  }));
175
210
  });
176
211
  }
@@ -186,7 +221,7 @@ var DshOAuthAccountVault = class {
186
221
  add(label, credential) {
187
222
  return this.#enqueue(async () => {
188
223
  const normalizedLabel = normalizeLabel(label);
189
- const validated = assertOAuthCredential$1(credential);
224
+ const validated = sanitizeOAuthCredential(credential);
190
225
  await this.#ensurePayload();
191
226
  const id = this.createId();
192
227
  const account = (await this.#modifyPayload((current) => ({
@@ -202,7 +237,8 @@ var DshOAuthAccountVault = class {
202
237
  id,
203
238
  label: account.label,
204
239
  active: true,
205
- expiresAt: account.credential.expires
240
+ expiresAt: account.credential.expires,
241
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
206
242
  };
207
243
  });
208
244
  }
@@ -222,7 +258,7 @@ var DshOAuthAccountVault = class {
222
258
  if (await this.#ensurePayload() === void 0) {
223
259
  const initial = await update(void 0);
224
260
  if (initial === void 0) return void 0;
225
- const credential = assertOAuthCredential$1(initial);
261
+ const credential = sanitizeOAuthCredential(initial);
226
262
  await this.credentials.set(this.legacyRef, JSON.stringify(credential));
227
263
  await this.#ensurePayload();
228
264
  return clone$1(credential);
@@ -236,7 +272,8 @@ var DshOAuthAccountVault = class {
236
272
  result = previous;
237
273
  return current;
238
274
  }
239
- const credential = assertOAuthCredential$1(next);
275
+ const credential = sanitizeOAuthCredential(next);
276
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
240
277
  const accounts = [...current.accounts];
241
278
  accounts[index] = {
242
279
  ...accounts[index],
@@ -1265,7 +1302,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1265
1302
  }
1266
1303
  //#endregion
1267
1304
  //#region src/version.js
1268
- const PACKAGE_VERSION = "1.12.1";
1305
+ const PACKAGE_VERSION = "1.13.0";
1269
1306
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1270
1307
  //#endregion
1271
1308
  //#region src/model-catalog.js
@@ -1292,7 +1329,7 @@ function reasoningMap(levels) {
1292
1329
  function visibleModel(value) {
1293
1330
  if (!record$4(value)) return void 0;
1294
1331
  const id = nonEmpty$2(value.slug);
1295
- if (id === void 0 || value.supported_in_api !== true || value.visibility !== "list") return void 0;
1332
+ if (id === void 0 || value.visibility !== "list") return void 0;
1296
1333
  const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
1297
1334
  const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
1298
1335
  return {
@@ -2244,15 +2281,23 @@ function resetCreditsOf(value) {
2244
2281
  if (value === void 0 || value === null) return void 0;
2245
2282
  if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
2246
2283
  if (value.credits !== void 0 && value.credits !== null && !Array.isArray(value.credits)) throw new Error("Codex returned malformed reset credit details");
2247
- const expirations = (value.credits ?? []).filter((credit) => record$1(credit) && credit.status?.toLowerCase?.() === "available").map((credit) => {
2248
- if (Number.isSafeInteger(credit.expires_at) && credit.expires_at > 0) return credit.expires_at * 1e3;
2249
- if (typeof credit.expires_at === "string" && credit.expires_at.length <= 64) {
2284
+ const available = (value.credits ?? []).filter((credit) => record$1(credit) && credit.status?.toLowerCase?.() === "available").map((credit) => {
2285
+ const name = typeof credit.title === "string" && credit.title.trim().length > 0 ? credit.title.trim().slice(0, 120) : void 0;
2286
+ let expiresAt;
2287
+ if (Number.isSafeInteger(credit.expires_at) && credit.expires_at > 0) expiresAt = credit.expires_at * 1e3;
2288
+ else if (typeof credit.expires_at === "string" && credit.expires_at.length <= 64) {
2250
2289
  const parsed = Date.parse(credit.expires_at);
2251
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
2290
+ if (Number.isFinite(parsed) && parsed > 0) expiresAt = parsed;
2252
2291
  }
2253
- }).filter((expiration) => expiration !== void 0);
2292
+ return {
2293
+ ...name === void 0 ? {} : { name },
2294
+ ...expiresAt === void 0 ? {} : { expiresAt }
2295
+ };
2296
+ });
2297
+ const expirations = available.map((credit) => credit.expiresAt).filter((expiration) => expiration !== void 0);
2254
2298
  return {
2255
2299
  availableCount: value.available_count,
2300
+ ...available.length === 0 ? {} : { credits: available },
2256
2301
  ...expirations.length === 0 ? {} : { nextExpiresAt: Math.min(...expirations) }
2257
2302
  };
2258
2303
  }
@@ -2691,10 +2736,13 @@ function parseDetails(value, now) {
2691
2736
  if (available.length === 0) throw new Error("No usable quota reset is available");
2692
2737
  return {
2693
2738
  availableCount: value.available_count,
2694
- creditId: available[0].credit.id,
2695
- title: safeCopy(available[0].credit.title),
2696
- description: safeCopy(available[0].credit.description),
2697
- creditExpiresAt: available[0].expiresAt
2739
+ credits: available.map(({ credit, expiresAt }, index) => ({
2740
+ creditId: credit.id,
2741
+ title: safeCopy(credit.title),
2742
+ description: safeCopy(credit.description),
2743
+ creditExpiresAt: expiresAt,
2744
+ index
2745
+ }))
2698
2746
  };
2699
2747
  }
2700
2748
  function parseConsumeResult(value) {
@@ -2728,6 +2776,23 @@ function createCodexResetCreditService(options) {
2728
2776
  const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
2729
2777
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2730
2778
  const challenges = /* @__PURE__ */ new Map();
2779
+ const creditRefs = /* @__PURE__ */ new Map();
2780
+ let creditRefSequence = 0;
2781
+ const rememberCreditRef = (accountId, credit) => {
2782
+ const ref = `${randomUUID$1()}-${++creditRefSequence}`;
2783
+ creditRefs.set(ref, {
2784
+ accountId,
2785
+ creditId: credit.creditId,
2786
+ index: credit.index
2787
+ });
2788
+ while (creditRefs.size > 64) creditRefs.delete(creditRefs.keys().next().value);
2789
+ return ref;
2790
+ };
2791
+ const publicCredit = (accountId, credit) => ({
2792
+ ref: rememberCreditRef(accountId, credit),
2793
+ ...credit.title === void 0 ? {} : { name: credit.title },
2794
+ ...credit.creditExpiresAt === void 0 ? {} : { expiresAt: credit.creditExpiresAt }
2795
+ });
2731
2796
  const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
2732
2797
  const readDetails = async (signal, credentials) => {
2733
2798
  const { access, accountId } = credentials ?? await resolveCredentials(signal);
@@ -2757,16 +2822,27 @@ function createCodexResetCreditService(options) {
2757
2822
  };
2758
2823
  return Object.freeze({
2759
2824
  async inspect({ signal } = {}) {
2760
- const { details } = await readDetails(signal);
2825
+ const { accountId, details } = await readDetails(signal);
2761
2826
  return {
2762
2827
  availableCount: details.availableCount,
2763
- ...details.creditExpiresAt === void 0 ? {} : { nextExpiresAt: details.creditExpiresAt }
2828
+ credits: details.credits.map((credit) => publicCredit(accountId, credit)),
2829
+ ...details.credits[0]?.creditExpiresAt === void 0 ? {} : { nextExpiresAt: details.credits[0].creditExpiresAt }
2764
2830
  };
2765
2831
  },
2766
- async prepare({ signal } = {}) {
2832
+ async prepare({ creditRef, signal } = {}) {
2767
2833
  const credentials = await resolveCredentials(signal);
2768
2834
  for (const [challengeId, challenge] of challenges) {
2769
- if (challenge.accountId !== credentials.accountId || challenge.uncertain !== true) continue;
2835
+ if (challenge.accountId === credentials.accountId && challenge.uncertain === false && challenge.creditRef === creditRef) if (now() > challenge.expiresAt) challenges.delete(challengeId);
2836
+ else return {
2837
+ challengeId,
2838
+ availableCount: challenge.availableCount,
2839
+ readyAt: challenge.readyAt,
2840
+ expiresAt: challenge.expiresAt,
2841
+ ...challenge.creditExpiresAt === void 0 ? {} : { creditExpiresAt: challenge.creditExpiresAt },
2842
+ ...challenge.title === void 0 ? {} : { title: challenge.title },
2843
+ ...challenge.description === void 0 ? {} : { description: challenge.description }
2844
+ };
2845
+ if (challenge.accountId !== credentials.accountId || challenge.uncertain !== true || creditRef !== void 0 && challenge.creditRef !== creditRef) continue;
2770
2846
  if (now() > challenge.expiresAt) {
2771
2847
  challenges.delete(challengeId);
2772
2848
  continue;
@@ -2782,22 +2858,31 @@ function createCodexResetCreditService(options) {
2782
2858
  };
2783
2859
  }
2784
2860
  const { accountId, details } = await readDetails(signal, credentials);
2861
+ let selected = details.credits[0];
2862
+ if (creditRef !== void 0) {
2863
+ const reference = typeof creditRef === "string" ? creditRefs.get(creditRef) : void 0;
2864
+ if (reference === void 0 || reference.accountId !== accountId) throw new Error("This quota reset confirmation is no longer valid");
2865
+ selected = details.credits.find((credit) => credit.creditId === reference.creditId && credit.index === reference.index);
2866
+ if (selected === void 0) throw new Error("This quota reset confirmation is no longer valid");
2867
+ }
2868
+ if (selected === void 0) throw new Error("No usable quota reset is available");
2785
2869
  const preparedAt = now();
2786
2870
  const readyAt = preparedAt + confirmDelayMs;
2787
- const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
2871
+ const expiresAt = Math.min(preparedAt + challengeTtlMs, selected.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
2788
2872
  if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
2789
2873
  const challengeId = randomUUID$1();
2790
2874
  challenges.set(challengeId, {
2791
2875
  state: "prepared",
2792
2876
  accountId,
2793
- creditId: details.creditId,
2877
+ creditRef,
2878
+ creditId: selected.creditId,
2794
2879
  redeemRequestId: randomUUID$1(),
2795
2880
  readyAt,
2796
2881
  expiresAt,
2797
2882
  availableCount: details.availableCount,
2798
- creditExpiresAt: details.creditExpiresAt,
2799
- title: details.title,
2800
- description: details.description,
2883
+ creditExpiresAt: selected.creditExpiresAt,
2884
+ title: selected.title,
2885
+ description: selected.description,
2801
2886
  uncertain: false
2802
2887
  });
2803
2888
  return {
@@ -2805,9 +2890,9 @@ function createCodexResetCreditService(options) {
2805
2890
  availableCount: details.availableCount,
2806
2891
  readyAt,
2807
2892
  expiresAt,
2808
- ...details.creditExpiresAt === void 0 ? {} : { creditExpiresAt: details.creditExpiresAt },
2809
- ...details.title === void 0 ? {} : { title: details.title },
2810
- ...details.description === void 0 ? {} : { description: details.description }
2893
+ ...selected.creditExpiresAt === void 0 ? {} : { creditExpiresAt: selected.creditExpiresAt },
2894
+ ...selected.title === void 0 ? {} : { title: selected.title },
2895
+ ...selected.description === void 0 ? {} : { description: selected.description }
2811
2896
  };
2812
2897
  },
2813
2898
  async consume({ challengeId, acknowledged, signal } = {}) {
@@ -2885,6 +2970,7 @@ function createCodexResetCreditService(options) {
2885
2970
  },
2886
2971
  clear() {
2887
2972
  challenges.clear();
2973
+ creditRefs.clear();
2888
2974
  }
2889
2975
  });
2890
2976
  }
@@ -3026,7 +3112,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3026
3112
  signal.throwIfAborted();
3027
3113
  return {
3028
3114
  ok: true,
3029
- value: endpoint === "reset-credit/inspect" ? await resetCreditService.inspect({ signal }) : endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
3115
+ value: endpoint === "reset-credit/inspect" ? await resetCreditService.inspect({ signal }) : endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({
3116
+ creditRef: payload?.creditRef,
3117
+ signal
3118
+ }) : await resetCreditService.consume({
3030
3119
  challengeId: payload?.challengeId,
3031
3120
  acknowledged: payload?.acknowledged,
3032
3121
  signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
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",