@goodandready/dsh-subscriptions 0.2.5 → 0.2.6

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/accounts.js CHANGED
@@ -54,6 +54,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
54
54
  const refreshLocks = new Map()
55
55
  const refreshFailures = new Map()
56
56
  const usage = new Map()
57
+ const windows = new Map()
57
58
  const usageFetched = new Map()
58
59
  const doFetch = fetchImpl || fetch
59
60
 
@@ -107,6 +108,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
107
108
  const blob = parseBlob(await resolveRaw(ref))
108
109
  base.label = blob.label || blob.email || ''
109
110
  base.email = blob.email || ''
111
+ if (Array.isArray(blob.usage)) { base.usage = blob.usage; base.usageAt = blob.usageAt || 0 }
110
112
  if (blob.validationUrl) base.validationUrl = blob.validationUrl
111
113
  if (blob.validationMessage) base.validationMessage = blob.validationMessage
112
114
  if (blob.accountNotice) base.accountNotice = blob.accountNotice
@@ -117,6 +119,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
117
119
  ...base,
118
120
  cooldownUntil: cooldowns.get(ref) || 0,
119
121
  quota: quotas.get(ref) || null,
122
+ usage: windows.get(ref) || base.usage || null,
120
123
  usagePercent: usage.has(ref) ? usage.get(ref) : null,
121
124
  refreshError: refreshFailures.get(ref)?.error || '',
122
125
  validationUrl: base.validationUrl || '',
@@ -194,9 +197,23 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
194
197
  const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
195
198
  const snap = await vendor.usage(blob, cfg, doFetch)
196
199
  usageFetched.set(slot.ref, Date.now())
197
- if (snap && Number.isFinite(Number(snap.usedPercent))) {
200
+ if (!snap) continue
201
+ if (Number.isFinite(Number(snap.usedPercent))) {
198
202
  usage.set(slot.ref, Number(snap.usedPercent))
199
203
  }
204
+ const list = Array.isArray(snap.windows) ? snap.windows : null
205
+ if (list) {
206
+ windows.set(slot.ref, list)
207
+ // persist last known windows so the card survives restarts
208
+ try {
209
+ const prev = Array.isArray(blob.usage) ? JSON.stringify(blob.usage) : ''
210
+ if (prev !== JSON.stringify(list)) {
211
+ await saveBlob(slot.ref, { ...blob, usage: list, usageAt: Date.now() })
212
+ } else if (!blob.usageAt) {
213
+ await saveBlob(slot.ref, { ...blob, usageAt: Date.now() })
214
+ }
215
+ } catch { /* persistence best-effort */ }
216
+ }
200
217
  } catch {
201
218
  usageFetched.set(slot.ref, Date.now())
202
219
  }
package/lib/blob.js CHANGED
@@ -16,6 +16,8 @@ export function serializeBlob(obj) {
16
16
  email: asString(obj && obj.email),
17
17
  accountId: asString(obj && obj.accountId),
18
18
  projectId: asString(obj && obj.projectId),
19
+ ...(Array.isArray(obj && obj.usage) ? { usage: obj.usage } : {}),
20
+ ...((obj && obj.usageAt) ? { usageAt: Number(obj.usageAt) } : {}),
19
21
  })
20
22
  }
21
23
 
@@ -30,6 +32,8 @@ export function parseBlob(text) {
30
32
  email: asString(obj.email),
31
33
  accountId: asString(obj.accountId),
32
34
  projectId: asString(obj.projectId),
35
+ ...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
36
+ ...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
33
37
  }
34
38
  }
35
39
 
package/lib/client.js CHANGED
@@ -301,17 +301,27 @@ window.__ModuleLoader__.load({
301
301
  t('verifySuffix'),
302
302
  ) : null,
303
303
  (function(){
304
- var q=account.quota, up=account.usagePercent, a=[];
304
+ var parts=[]
305
+ var wins=account.usage
306
+ if(Array.isArray(wins)&&wins.length){
307
+ wins.forEach(function(w){
308
+ if(w&&w.usedPercent!=null)parts.push(t('limit')+' '+(w.ru||w.id)+': '+Math.round(w.usedPercent)+'/100%')
309
+ })
310
+ }
311
+ var q=account.quota
305
312
  if(q){
306
- if(q.remaining!=null&&q.limit!=null)a.push(q.remaining+'/'+q.limit);
307
- else if(q.remaining!=null)a.push(String(q.remaining));
308
- if(q.usedPercent!=null)a.push(Math.round(q.usedPercent)+'%');
309
- if(q.resetAt){var d=new Date(q.resetAt);a.push(d.toLocaleTimeString())}
310
- if(q.measuredAt){var m=Math.round((Date.now()-q.measuredAt)/60000);a.push(m<1?'<1m':m+'m')}
313
+ var qa=[]
314
+ if(q.remaining!=null&&q.limit!=null)qa.push(q.remaining+'/'+q.limit)
315
+ else if(q.remaining!=null)qa.push(String(q.remaining))
316
+ if(q.usedPercent!=null)qa.push(Math.round(q.usedPercent)+'%')
317
+ if(q.resetAt){var d=new Date(q.resetAt);qa.push(d.toLocaleTimeString())}
318
+ if(qa.length)parts.push(t('quota')+': '+qa.join(' / '))
311
319
  }
312
- if(up!=null&&!q)a.push(Math.round(up)+'%');
313
- if(!a.length)return null;
314
- return React.createElement('span',{className:'dsub-sub'},t('quota')+' '+a.join(' \u00b7 '));
320
+ if(!wins&&!q&&account.usagePercent!=null)parts.push(Math.round(account.usagePercent)+'/100%')
321
+ var at=(q&&q.measuredAt)||(account.usageAt||0)
322
+ if(at){var m=Math.round((Date.now()-at)/60000);if(m>=1)parts.push(m+'m')}
323
+ if(!parts.length)return null
324
+ return React.createElement('span',{className:'dsub-sub'},parts.join(' \u00b7 '))
315
325
  })(),
316
326
 
317
327
  account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, t('plan') + ': ' + account.paidTierName) : null,
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { httpError, readJson } from './wire.js'
3
3
  import { validationFromLoadCodeAssist, validationFromHttpError, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
4
- import { asUsageSnapshot, deepestUsedPercent } from './usage.js'
4
+ import { asUsageSnapshot, usageWindows, deepestUsedPercent } from './usage.js'
5
5
 
6
6
  export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
7
7
  export const CODE_ASSIST_STREAM = 'https://daily-cloudcode-pa.googleapis.com/v1internal'
@@ -188,7 +188,9 @@ export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallb
188
188
  export async function retrieveQuotaPercent(fetchImpl, token, extraHeaders) {
189
189
  try {
190
190
  const json = await postAssist(fetchImpl, 'retrieveUserQuota', token, {}, extraHeaders)
191
- return asUsageSnapshot(deepestUsedPercent(json))
191
+ const snap = asUsageSnapshot(deepestUsedPercent(json))
192
+ if (snap) snap.windows = usageWindows(json)
193
+ return snap
192
194
  } catch {
193
195
  return null
194
196
  }
package/lib/index.js CHANGED
@@ -273,6 +273,7 @@ export function apply(ctx, config) {
273
273
  cooldownUntil: info.cooldownUntil,
274
274
  usagePercent: info.usagePercent,
275
275
  quota: info.quota || null,
276
+ usage: info.usage || null,
276
277
  refreshError: info.refreshError || '',
277
278
  validationUrl: info.validationUrl || '',
278
279
  validationMessage: info.validationMessage || '',
package/lib/usage.js CHANGED
@@ -48,4 +48,56 @@ export function asUsageSnapshot(percent) {
48
48
  const usedPercent = numberOrNull(percent)
49
49
  if (usedPercent == null) return null
50
50
  return { usedPercent }
51
- }
51
+ }
52
+ // Named limit windows reported by vendors (five_hour, seven_day, primary...).
53
+ // Label map is a data table: extend it when a vendor adds a window name.
54
+ const WINDOW_LABELS = {
55
+ five_hour: { ru: "5ч", en: "5h" },
56
+ seven_day: { ru: "7д", en: "7d" },
57
+ seven_day_oauth_apps: { ru: "7д прил.", en: "7d apps" },
58
+ weekly_limit_7_days: { ru: "7д", en: "7d" },
59
+ primary: { ru: "осн.", en: "primary" },
60
+ secondary: { ru: "втор.", en: "secondary" },
61
+ monthly: { ru: "месяц", en: "month" },
62
+ credits: { ru: "кредиты", en: "credits" },
63
+ }
64
+
65
+ function windowLabel(id) {
66
+ const known = WINDOW_LABELS[id]
67
+ return known || { ru: id, en: id }
68
+ }
69
+
70
+ // Walk vendor usage JSON; every node carrying a utilization-style number
71
+ // becomes a named window keyed by its JSON key.
72
+ export function usageWindows(obj) {
73
+ const out = []
74
+ const seen = new Set()
75
+ function walk(value, key, depth) {
76
+ if (!value || typeof value !== "object" || depth > 6) return
77
+ const pct = numberOrNull(
78
+ value.utilization ?? value.used_percent ?? value.usedPercent ?? value.creditUsagePercent ?? value.used_percentage,
79
+ )
80
+ if (pct != null && key && !seen.has(key)) {
81
+ seen.add(key)
82
+ const label = windowLabel(key)
83
+ out.push({ id: key, ru: label.ru, en: label.en, usedPercent: pct })
84
+ }
85
+ const frac = numberOrNull(value.remainingFraction)
86
+ if (frac != null && key && !seen.has(key)) {
87
+ seen.add(key)
88
+ const label = windowLabel(key)
89
+ out.push({ id: key, ru: label.ru, en: label.en, usedPercent: (1 - frac) * 100 })
90
+ }
91
+ const ufrac = numberOrNull(value.usedFraction)
92
+ if (ufrac != null && key && !seen.has(key)) {
93
+ seen.add(key)
94
+ const label = windowLabel(key)
95
+ out.push({ id: key, ru: label.ru, en: label.en, usedPercent: ufrac * 100 })
96
+ }
97
+ for (const [k, child] of Object.entries(value)) {
98
+ if (child && typeof child === "object") walk(child, k, depth + 1)
99
+ }
100
+ }
101
+ walk(obj, "", 0)
102
+ return out.length ? out : null
103
+ }
@@ -2,7 +2,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
2
2
  import { anthropicPayload, modelCatalog } from '../messages.js'
3
3
  import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
4
4
  import { emailFromToken } from '../jwt.js'
5
- import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
5
+ import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
6
6
 
7
7
  export const id = 'claude'
8
8
 
@@ -104,7 +104,9 @@ export async function usage(blob, _cfg, fetchImpl) {
104
104
  try {
105
105
  const res = await (fetchImpl || fetch)(USAGE, { headers: oauthHeaders(blob.accessToken) })
106
106
  const json = await readJson(res)
107
- return asUsageSnapshot(deepestUsedPercent(json))
107
+ const snap = asUsageSnapshot(deepestUsedPercent(json))
108
+ if (snap) snap.windows = usageWindows(json)
109
+ return snap
108
110
  } catch {
109
111
  return null
110
112
  }
@@ -3,7 +3,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
3
3
  import { codexResponsesBody, modelCatalog } from '../messages.js'
4
4
  import { chatgptAccountId, emailFromToken } from '../jwt.js'
5
5
  import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
6
- import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
6
+ import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
7
7
 
8
8
  export const id = 'codex'
9
9
 
@@ -150,7 +150,9 @@ export async function usage(blob, cfg, fetchImpl) {
150
150
  headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
151
151
  })
152
152
  const json = await readJson(res)
153
- return asUsageSnapshot(deepestUsedPercent(json))
153
+ const snap = asUsageSnapshot(deepestUsedPercent(json))
154
+ if (snap) snap.windows = usageWindows(json)
155
+ return snap
154
156
  } catch {
155
157
  return null
156
158
  }
@@ -2,7 +2,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
2
2
  import { modelCatalog, codexResponsesBody } from '../messages.js'
3
3
  import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
4
4
  import { emailFromToken } from '../jwt.js'
5
- import { asUsageSnapshot, grokBillingPercent } from '../usage.js'
5
+ import { asUsageSnapshot, usageWindows, grokBillingPercent } from '../usage.js'
6
6
 
7
7
  export const id = 'grok'
8
8
 
@@ -201,7 +201,9 @@ export async function usage(blob, cfg, fetchImpl) {
201
201
  headers: { ...identityHeaders(blob, cfg), Accept: 'application/json' },
202
202
  })
203
203
  const json = await readJson(res)
204
- return asUsageSnapshot(grokBillingPercent(json))
204
+ const snap = asUsageSnapshot(grokBillingPercent(json))
205
+ if (snap) snap.windows = usageWindows(json)
206
+ return snap
205
207
  } catch {
206
208
  return null
207
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",