@goodandready/dsh-subscriptions 0.7.31 → 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/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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.31",
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",