@goodandready/dsh-subscriptions 0.6.4 → 0.6.5

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/blob.js CHANGED
@@ -37,11 +37,3 @@ export function parseBlob(text) {
37
37
  }
38
38
  }
39
39
 
40
- export function publicAccountView(blob, extra) {
41
- const label = (blob && (blob.label || blob.email)) || ''
42
- return {
43
- label,
44
- email: (blob && blob.email) || '',
45
- ...(extra || {}),
46
- }
47
- }
package/lib/client.js CHANGED
@@ -54,6 +54,10 @@ window.__ModuleLoader__.load({
54
54
  '.dsub-chevOpen{transform:rotate(180deg)}' +
55
55
  '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
56
56
  '.dsub-block{margin-top:10px}' +
57
+ '.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}' +
58
+ '.dsub-group[open]{padding-bottom:6px}' +
59
+ '.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}' +
60
+ '.dsub-groupHead::-webkit-details-marker{display:none}' +
57
61
  '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
58
62
  '.dsub-grow{flex:1 1 180px;min-width:140px}' +
59
63
  '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
@@ -310,6 +314,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
310
314
  'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
311
315
  'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
312
316
  'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
317
+ 'closeModal': 'Close',
318
+ 'groupAuth': 'Sign-in and redirect',
319
+ 'groupPrivacy': 'Privacy and model list',
320
+ 'groupRotation': 'Rotation and limits',
321
+ 'groupProvider': 'Provider behaviour',
322
+ 'groupFallback': 'Local fallback',
313
323
  'privacyMask': 'Mask emails and account identifiers',
314
324
  'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
315
325
  'autoLoopback': 'Auto Loopback',
@@ -539,6 +549,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
539
549
  function badgeFor(account, t) {
540
550
  if (!account || !account.configured) return t('notConnected')
541
551
  if (account.validationUrl) return t('verifyAccount')
552
+ // #303: a quarantined account says why it is parked.
553
+ if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
554
+ return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
555
+ }
542
556
  if (account.cooldownUntil && account.cooldownUntil > Date.now()) return t('coolingDown')
543
557
  if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
544
558
  return t('connected')
@@ -862,6 +876,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
862
876
  React.createElement('div', { className: 'dsub-h' }, t('title')),
863
877
  React.createElement('div', { className: 'dsub-sub' },
864
878
  t('intro')),
879
+ React.createElement('details', { className: 'dsub-group', open: true },
880
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupAuth')),
865
881
  React.createElement('label', { className: 'dsub-row' },
866
882
  React.createElement('input', {
867
883
  type: 'checkbox',
@@ -875,23 +891,26 @@ const cssId = 'dsh-subscriptions/settings.module.css'
875
891
  React.createElement('label', { className: 'dsub-row' },
876
892
  React.createElement('input', {
877
893
  type: 'checkbox',
878
- checked: !!draft.privacyMask,
879
- onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
894
+ checked: !!draft.autoLoopback,
895
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
880
896
  }),
881
- React.createElement('span', null, t('privacyMask')),
897
+ React.createElement('span', null, t('autoLoopback')),
882
898
  ),
883
899
  React.createElement('div', { className: 'dsub-sub' },
884
- t('privacyMaskHint')),
900
+ t('autoLoopbackHint')),
901
+ ),
902
+ React.createElement('details', { className: 'dsub-group', open: true },
903
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupPrivacy')),
885
904
  React.createElement('label', { className: 'dsub-row' },
886
905
  React.createElement('input', {
887
906
  type: 'checkbox',
888
- checked: !!draft.autoLoopback,
889
- onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
907
+ checked: !!draft.privacyMask,
908
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
890
909
  }),
891
- React.createElement('span', null, t('autoLoopback')),
910
+ React.createElement('span', null, t('privacyMask')),
892
911
  ),
893
912
  React.createElement('div', { className: 'dsub-sub' },
894
- t('autoLoopbackHint')),
913
+ t('privacyMaskHint')),
895
914
  React.createElement('label', { className: 'dsub-row' },
896
915
  React.createElement('input', {
897
916
  type: 'checkbox',
@@ -902,6 +921,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
902
921
  ),
903
922
  React.createElement('div', { className: 'dsub-sub' },
904
923
  t('hideDeprecatedHint')),
924
+ ),
925
+ React.createElement('details', { className: 'dsub-group', open: true },
926
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupRotation')),
905
927
  React.createElement('div', { className: 'dsub-row' },
906
928
  React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
907
929
  React.createElement('select', {
@@ -931,6 +953,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
931
953
  ),
932
954
  React.createElement('div', { className: 'dsub-sub' },
933
955
  t('expiryNotifyDaysHint')),
956
+ ),
957
+ React.createElement('details', { className: 'dsub-group', open: true },
958
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
934
959
  React.createElement('label', { className: 'dsub-row' },
935
960
  React.createElement('input', {
936
961
  type: 'checkbox',
@@ -956,6 +981,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
956
981
  ),
957
982
  React.createElement('div', { className: 'dsub-sub' },
958
983
  t('codexVerbosityHint')),
984
+ ),
985
+ React.createElement('details', { className: 'dsub-group', open: true },
986
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
959
987
  React.createElement('label', { className: 'dsub-row' },
960
988
  React.createElement('input', {
961
989
  type: 'checkbox',
@@ -982,6 +1010,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
982
1010
  onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
983
1011
  }),
984
1012
  ) : null,
1013
+ ),
985
1014
  React.createElement('details', { className: 'dsub-details' },
986
1015
  React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
987
1016
  React.createElement('div', { className: 'dsub-block' },
@@ -1691,15 +1720,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1691
1720
  const rows = state.accounts.map((a, i) => {
1692
1721
  const isLogged = state.logged.includes(a.provider)
1693
1722
  const cooled = a.cooldownUntil && a.cooldownUntil > now
1723
+ const quarantined = Boolean(a.quarantineReason) && Number(a.quarantineUntil || 0) > now
1694
1724
  const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
1695
1725
  const b = brandBadge(a.provider)
1696
1726
  const isActiveThis = state.active && state.active.provider === a.provider && state.active.index === a.index
1697
1727
 
1698
1728
  let statusClass = 'dsub-statusOff'
1699
1729
  let statusText = t('subsNotLogged')
1700
- if (cooled) {
1730
+ if (quarantined) {
1731
+ statusClass = 'dsub-statusBad'
1732
+ statusText = a.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(a.quarantineReason)
1733
+ } else if (cooled) {
1701
1734
  statusClass = 'dsub-statusWarn'
1702
- statusText = t('subsCooldown')
1735
+ // Only some model families may be cooled down - say which ones.
1736
+ statusText = t('subsCooldown') + (Array.isArray(a.cooldownFamilies) && a.cooldownFamilies.length ? ' · ' + a.cooldownFamilies.join('/') : '')
1703
1737
  } else if (isLogged) {
1704
1738
  if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
1705
1739
  else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
@@ -1715,7 +1749,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1715
1749
  isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
1716
1750
  a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1717
1751
  ),
1718
- React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
1752
+ React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass, role: 'status', 'aria-live': 'polite' }, statusText),
1719
1753
  ),
1720
1754
  (isLogged && pct != null) ? React.createElement('div', { className: 'dsub-barTrack', style: { marginTop: 4, height: 4 } },
1721
1755
  React.createElement('div', {
@@ -1746,7 +1780,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1746
1780
  className: 'dsub-modalWrap',
1747
1781
  onClick: (e) => { if (e.target === e.currentTarget) setState((s) => Object.assign({}, s, { open: false })) },
1748
1782
  },
1749
- React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
1783
+ React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('subsModalTitle') },
1750
1784
  React.createElement('div', { className: 'dsub-modalHead' },
1751
1785
  React.createElement('div', { className: 'dsub-modalTitleWrap' },
1752
1786
  React.createElement('div', { className: 'dsub-modalIcon' }, '⚡'),
@@ -1765,6 +1799,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1765
1799
  React.createElement('button', {
1766
1800
  type: 'button',
1767
1801
  className: 'dsub-mini',
1802
+ autoFocus: true,
1803
+ 'aria-label': t('closeModal'),
1768
1804
  onClick: () => setState((s) => Object.assign({}, s, { open: false })),
1769
1805
  }, '✕'),
1770
1806
  ),
package/lib/http.js CHANGED
@@ -30,7 +30,10 @@ export function readBody(req, maxBytes = 64 * 1024) {
30
30
  })
31
31
  }
32
32
 
33
- /** Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
33
+ /**
34
+ * @param {import('node:http').IncomingMessage} request
35
+ * @returns {boolean}
36
+ * Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
34
37
  export function isTrustedSettingsRequest(request) {
35
38
  return request.headers['sec-fetch-site'] !== 'cross-site'
36
39
  }
package/lib/index.js CHANGED
@@ -440,6 +440,11 @@ export function apply(ctx, config) {
440
440
  configured: info.configured,
441
441
  writable: info.writable,
442
442
  cooldownUntil: info.cooldownUntil,
443
+ // #303: expose the cooldown scope and quarantine so the card can
444
+ // explain why only some models are blocked.
445
+ cooldownFamilies: info.cooldownFamilies || null,
446
+ quarantineUntil: info.quarantineUntil || 0,
447
+ quarantineReason: info.quarantineReason || null,
443
448
  usagePercent: info.usagePercent,
444
449
  quota: info.quota || null,
445
450
  usage: info.usage || null,
package/lib/quarantine.js CHANGED
@@ -29,10 +29,3 @@ export function isQuarantined(account, nowMs = Date.now()) {
29
29
  return Number(account.quarantineUntil) > Number(nowMs)
30
30
  }
31
31
 
32
- export function releaseFromQuarantine(account) {
33
- const next = { ...account }
34
- delete next.quarantineReason
35
- delete next.quarantineUntil
36
- if (next.status === 'quarantine') next.status = 'active'
37
- return next
38
- }
@@ -1,24 +0,0 @@
1
- export function parseRateLimitHeaders(headers) {
2
- if (!headers || typeof headers !== 'object') return null
3
-
4
- const get = (key) => {
5
- const k = Object.keys(headers).find((h) => h.toLowerCase() === key.toLowerCase())
6
- return k ? headers[k] : null
7
- }
8
-
9
- const remainingTokens = get('x-ratelimit-remaining-tokens')
10
- const resetTokens = get('x-ratelimit-reset-tokens')
11
- const remainingRequests = get('x-ratelimit-remaining-requests')
12
- const resetRequests = get('x-ratelimit-reset-requests')
13
-
14
- if (!remainingTokens && !resetTokens && !remainingRequests && !resetRequests) {
15
- return null
16
- }
17
-
18
- return {
19
- remainingTokens: remainingTokens != null ? Number(remainingTokens) : null,
20
- resetTokensTime: resetTokens || null,
21
- remainingRequests: remainingRequests != null ? Number(remainingRequests) : null,
22
- resetRequestsTime: resetRequests || null
23
- }
24
- }
package/lib/ratelimit.js CHANGED
@@ -208,11 +208,3 @@ export function quotaSnapshot(provider, headers, bodyJson, nowMs = Date.now()) {
208
208
  return { remaining, limit, resetAt: resetAt ?? null, usedPercent, measuredAt: nowMs }
209
209
  }
210
210
 
211
- export function formatQuota(q) {
212
- if (!q) return ""
213
- const parts = []
214
- if (q.remaining!=null && q.limit!=null) parts.push(q.remaining+"/"+q.limit)
215
- else if (q.remaining!=null) parts.push("left "+q.remaining)
216
- if (q.resetAt) parts.push("reset "+new Date(q.resetAt).toLocaleString())
217
- return parts.join(" · ")
218
- }
package/lib/refs.js CHANGED
@@ -61,6 +61,11 @@ export function displayName(provider) {
61
61
  return DISPLAY[provider] || displayExtra[provider] || provider
62
62
  }
63
63
 
64
+ /**
65
+ * @param {string} provider
66
+ * @param {number} index
67
+ * @returns {string} provider-scoped credential ref, e.g. CODEX_OAUTH_1
68
+ */
64
69
  export function oauthRef(provider, index) {
65
70
  const n = Number(index)
66
71
  if (!Number.isInteger(n) || n < 1) throw new Error('account index must be an integer >= 1')
@@ -26,6 +26,12 @@ function fill(template, n) {
26
26
  return String(template).replace('{n}', String(n))
27
27
  }
28
28
 
29
+ /**
30
+ * @param {number} resetAt epoch ms of the quota window reset
31
+ * @param {string} [lang]
32
+ * @param {number} [now]
33
+ * @returns {string} human-readable remaining time, down to the minute
34
+ */
29
35
  export function formatRelativeReset(resetAt, lang = 'ru', now = Date.now()) {
30
36
  if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
31
37
  const delta = resetAt - now
@@ -1,6 +1,13 @@
1
1
  const sessionPins = new Map()
2
2
  const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
3
3
 
4
+ /**
5
+ * Bind a session id to an account ref for ttlMs (default 30 minutes).
6
+ * @param {string|undefined} sessionId
7
+ * @param {string|undefined} accountRef
8
+ * @param {number} [ttlMs]
9
+ * @returns {void}
10
+ */
4
11
  export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
5
12
  if (!sessionId || !accountRef) return
6
13
  sessionPins.set(sessionId, {
@@ -9,6 +16,10 @@ export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
9
16
  })
10
17
  }
11
18
 
19
+ /**
20
+ * @param {string|undefined} sessionId
21
+ * @returns {string|null} the bound account ref, or null when absent/expired
22
+ */
12
23
  export function getPinnedAccountRef(sessionId) {
13
24
  if (!sessionId) return null
14
25
  const entry = sessionPins.get(sessionId)
@@ -150,7 +150,9 @@ export async function usage(blob, _cfg, fetchImpl) {
150
150
  }
151
151
 
152
152
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, saveBlob }) {
153
- const ready = await withProject(blob, fetchImpl, saveBlob)
153
+ // Abort must cover the project pre-flight as well as the stream.
154
+ const preflight = (u, i) => fetchImpl(u, { ...(i || {}), signal })
155
+ const ready = await withProject(blob, preflight, saveBlob)
154
156
  const { envelope, sessionId } = streamEnvelope({
155
157
  projectId: ready.projectId,
156
158
  model: options.model,
@@ -236,7 +236,8 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
236
236
  if (!body.instructions) delete body.instructions
237
237
  // grok manages reasoning itself (cli catalog aware); drop the codex-level field
238
238
  delete body.reasoning
239
- const catalog = await cliCatalogCached(blob, config, fetchImpl).catch(() => new Map())
239
+ // Thread the caller AbortSignal through the catalog pre-flight too.
240
+ const catalog = await cliCatalogCached(blob, config, (u, i) => fetchImpl(u, { ...(i || {}), signal })).catch(() => new Map())
240
241
  const reasoning = grokReasoningBody(options.model, options.reasoningEffort, catalog)
241
242
  if (reasoning) body.reasoning = reasoning
242
243
  body = grokBodyForModel(options.model, body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,8 @@
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
37
  "scripts": {
38
- "test": "eslint lib/ && node --test test/*.test.mjs"
38
+ "test": "eslint lib/ && node --test test/*.test.mjs",
39
+ "typecheck": "tsc -p tsconfig.checkjs.json"
39
40
  },
40
41
  "dsh": {
41
42
  "bundle": {
@@ -62,6 +63,8 @@
62
63
  "undici": "^8.10.1"
63
64
  },
64
65
  "devDependencies": {
65
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8"
66
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
67
+ "@types/node": "^22.20.2",
68
+ "typescript": "^7.0.2"
66
69
  }
67
70
  }