@goodandready/dsh-subscriptions 0.7.30 โ†’ 0.7.32

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.
@@ -0,0 +1,68 @@
1
+ function text(value) {
2
+ return value == null ? '' : String(value).trim()
3
+ }
4
+
5
+ /**
6
+ * #827: decide whether a stored credential is usable without exposing it.
7
+ * Empty access/refresh tokens and non-finite expiry are treated as corrupt.
8
+ */
9
+ export function credentialState(blob) {
10
+ if (!blob || typeof blob !== 'object') return { usable: false, reason: 'invalid_blob' }
11
+ const accessToken = text(blob.accessToken)
12
+ const refreshToken = text(blob.refreshToken)
13
+ if (!accessToken && !refreshToken && !text(blob.apiKey)) {
14
+ return { usable: false, reason: 'empty_credentials' }
15
+ }
16
+ if (blob.expiresAt != null && blob.expiresAt !== '' && !Number.isFinite(Number(blob.expiresAt))) {
17
+ return { usable: false, reason: 'invalid_expiry' }
18
+ }
19
+ return { usable: true, reason: null }
20
+ }
21
+
22
+ /**
23
+ * #831: stable provider identity used for duplicate detection. Unknown
24
+ * identities return null and must never be merged automatically.
25
+ */
26
+ export function accountIdentityKey(provider, blob) {
27
+ if (!blob || typeof blob !== 'object') return null
28
+ const id = text(blob.email || blob.login || blob.accountId || blob.userId)
29
+ if (!id) return null
30
+ return `${text(provider).toLowerCase()}:${id.toLowerCase()}`
31
+ }
32
+
33
+ /**
34
+ * #818: aliases are display-only. Resolve either a stable ref or a legacy
35
+ * label within one provider back to the stable credential ref.
36
+ */
37
+ export function resolveAccountRef(slots, provider, value) {
38
+ const wanted = text(value)
39
+ if (!wanted) return null
40
+ const list = (Array.isArray(slots) ? slots : []).filter((slot) => slot && slot.provider === provider)
41
+ const byIndex = Number(wanted)
42
+ if (Number.isInteger(byIndex) && byIndex > 0) {
43
+ return list.find((slot) => Number(slot.index) === byIndex)?.ref || null
44
+ }
45
+ return list.find((slot) => slot.ref === wanted || text(slot.label) === wanted)?.ref || null
46
+ }
47
+
48
+ /**
49
+ * #806: a delayed refresh may only write when its credential lineage is
50
+ * still current. A newer save/revoke bumps the version.
51
+ */
52
+ export function refreshIsCurrent(currentVersion, expectedVersion) {
53
+ return Number(currentVersion) === Number(expectedVersion)
54
+ }
55
+
56
+ /**
57
+ * #833: deterministic host-env vs plugin-proxy precedence.
58
+ * @param {{ hostProxy?: string, pluginProxy?: string, prefer?: 'host' | 'plugin' }} [options]
59
+ */
60
+ export function proxyPrecedence(options = {}) {
61
+ const prefer = options.prefer || 'host'
62
+ const host = text(options.hostProxy)
63
+ const plugin = text(options.pluginProxy)
64
+ if (host && plugin && prefer === 'plugin') return { proxy: plugin, source: 'plugin', conflict: true }
65
+ if (host) return { proxy: host, source: 'host', conflict: Boolean(plugin) }
66
+ if (plugin) return { proxy: plugin, source: 'plugin', conflict: false }
67
+ return { proxy: '', source: 'none', conflict: false }
68
+ }
@@ -169,6 +169,8 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
169
169
  poolEnabled: slot.poolEnabled !== false,
170
170
  isDefault: !!slot.isDefault,
171
171
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
172
+ credentialError: info.credentialError || null,
173
+ duplicateOf: info.duplicateOf || null,
172
174
  capabilities,
173
175
  billing,
174
176
  renewsAt: billing.renewsAt,
package/lib/accounts.js CHANGED
@@ -46,6 +46,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials'
46
46
  import { oauthRef, isProvider } from './refs.js'
47
47
  import { parseBlob, serializeBlob } from './blob.js'
48
48
  import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
49
+ import { accountIdentityKey, credentialState } from './account-reliability.js'
49
50
  import { recordSwitch, recordExhaust, recordBroken, computeHealthScore, healthBadge } from './health.js'
50
51
  import { executeWithRetry } from './backoff.js'
51
52
  import { getVendor } from './vendors/index.js'
@@ -308,6 +309,26 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
308
309
  if (base.configured) {
309
310
  try {
310
311
  const blob = parseBlob(await resolveRaw(ref))
312
+ const state = credentialState(blob)
313
+ if (!state.usable) {
314
+ base.configured = false
315
+ base.credentialError = state.reason
316
+ base.label = blob.label || blob.email || ''
317
+ return {
318
+ ...base,
319
+ cooldownUntil: 0,
320
+ quarantineUntil: 0,
321
+ cooldownFamilies: null,
322
+ cooldownModels: null,
323
+ quota: null,
324
+ isRevoked: false,
325
+ authWarning: '',
326
+ validationUrl: '',
327
+ validationMessage: '',
328
+ accountNotice: '',
329
+ paidTierName: '',
330
+ }
331
+ }
311
332
  base.label = blob.label || blob.email || ''
312
333
  base.email = blob.email || ''
313
334
  base.plan = blob.plan || blob.paidTierName || ''
@@ -367,6 +388,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
367
388
  latencyGrade: pings.get(ref)?.latencyGrade ?? null,
368
389
  lastDoctorVerdict: doctorVerdicts.get(ref) || null,
369
390
  availableModels: dynamicModels.get(ref)?.models || null,
391
+ credentialError: base.credentialError || null,
370
392
  }
371
393
  }
372
394
 
@@ -374,8 +396,12 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
374
396
  const curConfig = getConfig()
375
397
  const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
376
398
  const out = []
399
+ const identities = new Map()
377
400
  for (const slot of slots) {
378
401
  const info = await describeRef(slot.ref, curConfig)
402
+ const identity = accountIdentityKey(provider, { email: info.email, accountId: info.accountId })
403
+ const duplicateOf = identity ? (identities.get(identity) || null) : null
404
+ if (identity && !duplicateOf) identities.set(identity, slot.ref)
379
405
  const q = quarantines.get(slot.ref)
380
406
  out.push({
381
407
  ref: slot.ref,
@@ -400,6 +426,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
400
426
  poolEnabled: slot.poolEnabled !== false,
401
427
  isDefault: !!slot.isDefault,
402
428
  ...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
429
+ credentialError: info.credentialError || null,
430
+ duplicateOf,
403
431
  })
404
432
  }
405
433
  return out
package/lib/client.js CHANGED
@@ -202,6 +202,10 @@ window.__ModuleLoader__.load({
202
202
  "modelShowMore": "Show more",
203
203
  "modelShowAll": "Show all",
204
204
  "modelNoMatches": "No models match this search.",
205
+ "modelCatalogSourceHint": "Catalog source: live, cached or builtin",
206
+ "modelNamesTitle": "Model display names",
207
+ "modelNamesHint": "Presentation only: routing, reasoning and model ids stay unchanged. Reset restores the catalog name.",
208
+ "modelNameReset": "Reset",
205
209
  "rotationDecisionsTitle": "Rotation & Routing Decisions",
206
210
  "rotationDecisionsDesc": "Real-time decision trace explaining account selection, skips, and fallback reasons.",
207
211
  "rotationAttempt": "attempt",
@@ -308,7 +312,10 @@ window.__ModuleLoader__.load({
308
312
  "maxQueueSizeSlot": "Queue",
309
313
  "save": "Save",
310
314
  "saved": "Saved",
315
+ "cancel": "Cancel",
316
+ "unsavedChanges": "Unsaved changes",
311
317
  "accountLabel": "Account",
318
+ "usageRemaining": "Remaining quota",
312
319
  "plan": "Plan",
313
320
  "storedAs": "Stored as",
314
321
  "slashFast": "Toggle Codex Speed (fast/standard)",
@@ -594,6 +601,10 @@ window.__ModuleLoader__.load({
594
601
  "modelShowMore": "ๆ˜พ็คบๆ›ดๅคš",
595
602
  "modelShowAll": "ๆ˜พ็คบๅ…จ้ƒจ",
596
603
  "modelNoMatches": "ๆฒกๆœ‰ๅŒน้…็š„ๆจกๅž‹ใ€‚",
604
+ "modelCatalogSourceHint": "็›ฎๅฝ•ๆฅๆบ๏ผšๅฎžๆ—ถใ€็ผ“ๅญ˜ๆˆ–ๅ†…็ฝฎ",
605
+ "modelNamesTitle": "ๆจกๅž‹ๆ˜พ็คบๅ็งฐ",
606
+ "modelNamesHint": "ไป…ๅฝฑๅ“ๆ˜พ็คบ๏ผš่ทฏ็”ฑใ€ๆŽจ็†ๅ’Œๆจกๅž‹ ID ไธๅ˜ใ€‚้‡็ฝฎๆขๅค็›ฎๅฝ•ๅ็งฐใ€‚",
607
+ "modelNameReset": "้‡็ฝฎ",
597
608
  "rotationDecisionsTitle": "่ฝฎ่ฏข่ฐƒๅบฆไธŽ่ทฏ็”ฑๅ†ณ็ญ–",
598
609
  "rotationDecisionsDesc": "ๅฎžๆ—ถๅ†ณ็ญ–่ฟฝ่ธช๏ผŒ้€ๆ˜Žๅฑ•็คบๆฏๆฌก่ฏทๆฑ‚็š„่ดฆๅท้€‰ๆ‹ฉใ€่ทณ่ฟ‡ไธŽๅ›ž้€€ๅŽŸๅ› ใ€‚",
599
610
  "rotationAttempt": "ๆฌกๅฐ่ฏ•",
@@ -700,7 +711,10 @@ window.__ModuleLoader__.load({
700
711
  "maxQueueSizeSlot": "้˜Ÿๅˆ—",
701
712
  "save": "ไฟๅญ˜",
702
713
  "saved": "ๅทฒไฟๅญ˜",
714
+ "cancel": "ๅ–ๆถˆ",
715
+ "unsavedChanges": "ๆœ‰ๆœชไฟๅญ˜็š„ๆ›ดๆ”น",
703
716
  "accountLabel": "่ดฆๆˆท",
717
+ "usageRemaining": "ๅ‰ฉไฝ™้…้ข",
704
718
  "plan": "ๅฅ—้คๆ–นๆกˆ",
705
719
  "storedAs": "ๅญ˜ๅ‚จๆ ‡่ฏ†",
706
720
  "slashFast": "ๅˆ‡ๆข Codex ๅŠ ้€Ÿๆจกๅผ๏ผˆๅฟซ้€Ÿ/ๆ ‡ๅ‡†๏ผ‰",
@@ -1419,6 +1433,7 @@ window.__ModuleLoader__.load({
1419
1433
  }
1420
1434
  const [checking, setChecking] = React.useState({})
1421
1435
  const [saved, setSaved] = React.useState(false)
1436
+ const [savedSnapshot, setSavedSnapshot] = React.useState(null)
1422
1437
  const [tokenDraft, setTokenDraft] = React.useState({})
1423
1438
  const [openHelp, setOpenHelp] = React.useState({})
1424
1439
  const [err, setErr] = React.useState('')
@@ -1440,7 +1455,9 @@ window.__ModuleLoader__.load({
1440
1455
 
1441
1456
  const applyPayload = (data) => {
1442
1457
  if (data && data.revision != null) setRevision(data.revision)
1443
- setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
1458
+ const next = JSON.parse(JSON.stringify((data && data.config) || {}))
1459
+ setDraft(next)
1460
+ setSavedSnapshot(JSON.parse(JSON.stringify(next)))
1444
1461
  setAccounts((data && data.accounts) || [])
1445
1462
  setProviders((data && data.providers) || [])
1446
1463
  setModelsCatalog((data && data.modelsCatalog) || [])
@@ -1492,6 +1509,18 @@ window.__ModuleLoader__.load({
1492
1509
  return () => timers.forEach(clearInterval)
1493
1510
  }, [device])
1494
1511
 
1512
+ const dirty = Boolean(draft && savedSnapshot && JSON.stringify(draft) !== JSON.stringify(savedSnapshot))
1513
+ React.useEffect(() => {
1514
+ if (!dirty) return undefined
1515
+ const warn = (event) => {
1516
+ event.preventDefault()
1517
+ event.returnValue = ''
1518
+ return ''
1519
+ }
1520
+ window.addEventListener('beforeunload', warn)
1521
+ return () => window.removeEventListener('beforeunload', warn)
1522
+ }, [dirty])
1523
+
1495
1524
  // #81: settings snapshot status - never render phantom inputs before the
1496
1525
  // config snapshot arrives, and offer a retry when the store is unavailable.
1497
1526
  if (!draft) {
@@ -2378,6 +2407,7 @@ h('div', { className: 'dsub-row' },
2378
2407
  m.provider ? h('span', { className: 'dsub-brand dsub-brand-' + m.provider, style: { fontSize: '9px', padding: '1px 4px' } }, m.provider.toUpperCase()) : null,
2379
2408
  h('code', null, m.name || m.id),
2380
2409
  tags.length ? h('span', { className: 'dsub-model-tag' }, tags.join(' ยท ')) : null,
2410
+ Array.isArray(m.catalogSources) && m.catalogSources.length ? h('span', { className: 'dsub-model-tag', title: t('modelCatalogSourceHint') }, '๐Ÿ“š ' + m.catalogSources.join('/')) : null,
2381
2411
  h('span', { className: 'dsub-model-tag ' + statusClass }, poolLabel),
2382
2412
  )
2383
2413
  })
@@ -2394,6 +2424,46 @@ h('div', { className: 'dsub-row' },
2394
2424
  }, t('modelShowAll')),
2395
2425
  ) : null,
2396
2426
  ),
2427
+ h('details', { className: 'dsub-group', 'data-pane': 'models' },
2428
+ h('summary', { className: 'dsub-groupHead' }, '๐Ÿท ' + t('modelNamesTitle')),
2429
+ h('div', { className: 'dsub-sub', style: { marginTop: 4 } }, t('modelNamesHint')),
2430
+ h('div', { style: { maxHeight: 260, overflow: 'auto', marginTop: 6 } },
2431
+ catalogShown.slice(0, 50).map((model) => {
2432
+ const current = (Array.isArray(draft.modelDisplayNames) ? draft.modelDisplayNames : [])
2433
+ .find((row) => row && row.provider === model.provider && String(row.id) === String(model.id))
2434
+ return h('div', { key: 'name:' + model.provider + ':' + model.id, className: 'dsub-row', style: { gap: 6 } },
2435
+ h('code', { style: { minWidth: 120, fontSize: 11 } }, model.id),
2436
+ h('input', {
2437
+ className: 'dsub-inp dsub-grow',
2438
+ value: current ? current.name : '',
2439
+ placeholder: model.name || model.id,
2440
+ onChange: (e) => {
2441
+ const value = e.target.value
2442
+ setDraft((d) => {
2443
+ const list = Array.isArray(d.modelDisplayNames) ? d.modelDisplayNames.slice() : []
2444
+ const idx = list.findIndex((row) => row && row.provider === model.provider && String(row.id) === String(model.id))
2445
+ if (value.trim()) {
2446
+ const next = { provider: model.provider, id: String(model.id), name: value.slice(0, 80) }
2447
+ if (idx >= 0) list[idx] = next
2448
+ else list.push(next)
2449
+ } else if (idx >= 0) {
2450
+ list.splice(idx, 1)
2451
+ }
2452
+ return Object.assign({}, d, { modelDisplayNames: list })
2453
+ })
2454
+ },
2455
+ }),
2456
+ h('button', {
2457
+ type: 'button', className: 'dsub-mini',
2458
+ onClick: () => setDraft((d) => Object.assign({}, d, {
2459
+ modelDisplayNames: (Array.isArray(d.modelDisplayNames) ? d.modelDisplayNames : [])
2460
+ .filter((row) => !(row && row.provider === model.provider && String(row.id) === String(model.id))),
2461
+ })),
2462
+ }, t('modelNameReset')),
2463
+ )
2464
+ })
2465
+ ),
2466
+ ),
2397
2467
  h('details', { className: 'dsub-group', 'data-pane': 'models' },
2398
2468
  h('summary', { className: 'dsub-groupHead' }, '๐Ÿง  ' + t('reasoningEffortTitle')),
2399
2469
  h('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('reasoningEffortDesc')),
@@ -2908,6 +2978,9 @@ h('div', { className: 'dsub-row' },
2908
2978
  },
2909
2979
  }, '\u00d7'),
2910
2980
  ),
2981
+ (account && (account.quota?.remainingPercent != null || account.usagePercent != null)) ? h('div', { className: 'dsub-sub', style: { marginTop: 4 } },
2982
+ '๐Ÿ“Š ' + t('usageRemaining') + ' ' + Math.round(account.quota?.remainingPercent != null ? Number(account.quota.remainingPercent) : (100 - Number(account.usagePercent))) + '%'
2983
+ ) : null,
2911
2984
  (account && account.quarantineUntil && Number(account.quarantineUntil) > Date.now()) ? h('div', { className: 'dsub-row', style: { marginTop: 4 } },
2912
2985
  h('span', { className: 'dsub-sub', style: { color: 'var(--dsw-alias-state-error-primary)', fontVariantNumeric: 'tabular-nums' } },
2913
2986
  'โ›” ' + (account.quarantineReason || 'QUARANTINE') + ' ยท ' + t('resetsIn') + ' ' + formatRelativeReset(account.quarantineUntil, t('lang'), Date.now())
@@ -3538,6 +3611,12 @@ h('div', { className: 'dsub-row' },
3538
3611
  type: 'button', className: 'dsub-save',
3539
3612
  onClick: () => save().catch((e) => setErr(cleanErrorMessage(e.message || e))),
3540
3613
  }, t('save')),
3614
+ h('button', {
3615
+ type: 'button', className: 'dsub-mini',
3616
+ disabled: !dirty,
3617
+ onClick: () => reload().catch((e) => setErr(cleanErrorMessage(e.message || e))),
3618
+ }, t('cancel')),
3619
+ dirty ? h('span', { className: 'dsub-sub' }, t('unsavedChanges')) : null,
3541
3620
  saved ? h('span', { className: 'dsub-ok' }, t('saved')) : null,
3542
3621
  info ? h('span', { className: 'dsub-ok' }, info) : null,
3543
3622
  err ? h('span', { className: 'dsub-bad' }, err) : null,
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ import { createSubscriptionsService } from './subscriptions.js'
18
18
  import { registerSubscriptionTools } from './native-tools.js'
19
19
  import { detectConflicts, readProfilePluginNames, setActiveConflicts } from './coexistence.js'
20
20
  import { createAccountStore, defaultAccountRef, normalizeSlots, vendorConfig } from './accounts.js'
21
+ import { resolveAccountRef } from './account-reliability.js'
21
22
  import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
22
23
  import { createResetCreditService } from './reset-credits.js'
23
24
  import { maskEmail, maskLabel } from './mask.js'
@@ -276,9 +277,8 @@ export function apply(ctx, rawConfig) {
276
277
 
277
278
  // #85: host-only reset credit service for Codex accounts.
278
279
  const resetCredits = createResetCreditService({ loadBlob: (ref) => store.loadBlob(ref) })
279
- function refForSlot(provider, index) {
280
- const slot = normalizeSlots(live().slots).find((s) => s.provider === provider && s.index === index)
281
- return slot ? slot.ref : null
280
+ function refForSlot(provider, indexOrAlias) {
281
+ return resolveAccountRef(normalizeSlots(live().slots), provider, indexOrAlias)
282
282
  }
283
283
 
284
284
  function stripLegacySlots(slots) {
@@ -279,6 +279,17 @@ export function buildEnrichedCatalog({
279
279
 
280
280
  const modelName = (typeof item === 'object' && (item.name || item.display_name)) ? (item.name || item.display_name) : modelId
281
281
  const caps = inferModelCapabilities(provId, modelId)
282
+ const catalogSources = new Set()
283
+ for (const account of accounts) {
284
+ if (!account || String(account.provider || '').toLowerCase() !== String(provId).toLowerCase()) continue
285
+ const available = Array.isArray(account.availableModels) ? account.availableModels : []
286
+ const hasModel = available.some((model) => {
287
+ const id = typeof model === 'string' ? model : (model && (model.id || model.slug || model.name))
288
+ return String(id || '') === String(modelId)
289
+ })
290
+ if (hasModel) catalogSources.add(account.modelsSource || 'live')
291
+ }
292
+ if (!catalogSources.size) catalogSources.add('builtin')
282
293
 
283
294
  result.push({
284
295
  id: modelId,
@@ -288,6 +299,7 @@ export function buildEnrichedCatalog({
288
299
  contextWindow: caps.contextWindow,
289
300
  contextLabel: caps.contextLabel,
290
301
  capabilities: caps.capabilities,
302
+ catalogSources: Array.from(catalogSources),
291
303
  pool,
292
304
  })
293
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.30",
3
+ "version": "0.7.32",
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",