@goodandready/dsh-subscriptions 0.6.15 → 0.6.16

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
@@ -2,7 +2,7 @@ import { fetchWithTimeout } from "./http.js"
2
2
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
3
3
  import { oauthRef, isProvider } from './refs.js'
4
4
  import { parseBlob, serializeBlob } from './blob.js'
5
- import { recordSwitch, recordExhaust, recordBroken } from './health.js'
5
+ import { recordSwitch, recordExhaust, recordBroken, computeHealthScore } from './health.js'
6
6
  import { executeWithRetry } from './backoff.js'
7
7
  import { getVendor } from './vendors/index.js'
8
8
 
@@ -177,6 +177,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
177
177
  cooldownUntil: info.cooldownUntil,
178
178
  cooldownFamilies: info.cooldownFamilies || null,
179
179
  quota: info.quota || null,
180
+ healthScore: computeHealthScore(info.health),
180
181
  quarantineUntil: inQ ? q.until : 0,
181
182
  quarantineReason: inQ ? q.reason : null,
182
183
  label: slot.label || info.label,
package/lib/adapter.js CHANGED
@@ -99,7 +99,10 @@ export class SubscriptionAdapter extends LlmAdapter {
99
99
  let yieldedAny = false
100
100
  try {
101
101
  if (typeof deps.refreshUsage === 'function') {
102
- await deps.refreshUsage(provider)
102
+ try {
103
+ const p = deps.refreshUsage(provider)
104
+ if (p && typeof p.catch === 'function') p.catch(() => {})
105
+ } catch { /* fire-and-forget background usage refresh */ }
103
106
  }
104
107
  for await (const event of streamWithRotation({
105
108
  accounts: await deps.listAccounts(provider),
@@ -108,7 +111,7 @@ export class SubscriptionAdapter extends LlmAdapter {
108
111
  switchAtRemaining: typeof deps.switchAtRemaining === 'function' ? deps.switchAtRemaining() : (deps.switchAtRemaining ?? 0),
109
112
  options,
110
113
  onCooldown: (account) => {
111
- deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
114
+ if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
112
115
  if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
113
116
  },
114
117
  streamOnce: async function* (account, opts) {
package/lib/http.js CHANGED
@@ -42,7 +42,35 @@ export function isLoopback(value) {
42
42
  * @returns {boolean}
43
43
  * Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
44
44
  export function isTrustedSettingsRequest(request) {
45
- return request.headers['sec-fetch-site'] !== 'cross-site'
45
+ const headers = request?.headers || {}
46
+ const secFetchSite = headers['sec-fetch-site']
47
+ if (secFetchSite) {
48
+ return secFetchSite !== 'cross-site'
49
+ }
50
+ const host = headers.host || headers['x-forwarded-host']
51
+ const origin = headers.origin
52
+ if (origin) {
53
+ try {
54
+ const u = new URL(origin)
55
+ if (host && u.host.toLowerCase() !== host.toLowerCase()) {
56
+ return false
57
+ }
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+ const referer = headers.referer
63
+ if (!origin && referer) {
64
+ try {
65
+ const u = new URL(referer)
66
+ if (host && u.host.toLowerCase() !== host.toLowerCase()) {
67
+ return false
68
+ }
69
+ } catch {
70
+ return false
71
+ }
72
+ }
73
+ return true
46
74
  }
47
75
 
48
76
  export function queryOf(req) {
package/lib/index.js CHANGED
@@ -26,7 +26,9 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
26
26
  import { createResetCreditService } from './reset-credits.js'
27
27
  import { maskEmail, maskLabel, maskText } from './mask.js'
28
28
  import { proxyFetch, pickFetch } from './proxy.js'
29
- import { HistoryStore } from './history.js'
29
+ import { HistoryStore, DEFAULT_HISTORY_DIR } from './history.js'
30
+ export { observeForecast, estimateForecast } from './forecast.js'
31
+ export { formatRelativeReset } from './relative-time.js'
30
32
  import {
31
33
  inspectGoogleAccount,
32
34
  antigravityMetadata,
@@ -116,7 +118,7 @@ export function apply(ctx, config) {
116
118
  bestEffort('emit limit-notice', () => ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }), undefined, logger)
117
119
  },
118
120
  })
119
- const history = new HistoryStore()
121
+ const history = new HistoryStore(DEFAULT_HISTORY_DIR, 7 * 24 * 60 * 60 * 1000, 1000)
120
122
  const recordHistory = (entry) => history.add(entry)
121
123
 
122
124
  // #85: host-only reset credit service for Codex accounts.
@@ -324,18 +326,24 @@ export function apply(ctx, config) {
324
326
  const existing = normalizeSlots(live().slots)
325
327
  const existingRefs = new Set(existing.map((s) => s.ref))
326
328
  const candidateProviders = PROVIDERS.filter((p) => isProvider(p))
327
- const additions = []
328
-
329
+ const candidates = []
329
330
  for (const provider of candidateProviders) {
330
331
  for (let index = 1; index <= 10; index++) {
331
332
  const ref = oauthRef(provider, index)
332
- if (existingRefs.has(ref)) continue
333
+ if (!existingRefs.has(ref)) {
334
+ candidates.push({ provider, index, ref })
335
+ }
336
+ }
337
+ }
338
+
339
+ const additionResults = await Promise.all(
340
+ candidates.map(async ({ provider, index, ref }) => {
333
341
  let d
334
342
  try {
335
343
  d = await creds.describe(credentialRef(ref))
336
344
  } catch (err) {
337
345
  if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] describe ${ref} error: ` + (err && err.message || err))
338
- continue
346
+ return null
339
347
  }
340
348
  if (d && d.configured) {
341
349
  let label = d.label || ''
@@ -347,10 +355,12 @@ export function apply(ctx, config) {
347
355
  } catch (loadErr) {
348
356
  if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] loadBlob ${ref} error: ` + (loadErr && loadErr.message || loadErr))
349
357
  }
350
- additions.push({ provider, index, label })
358
+ return { provider, index, label }
351
359
  }
352
- }
353
- }
360
+ return null
361
+ })
362
+ )
363
+ const additions = additionResults.filter(Boolean)
354
364
 
355
365
  if (!additions.length) return
356
366
  if (logger && logger.info) {
@@ -700,7 +710,7 @@ export function apply(ctx, config) {
700
710
  const vendor = getVendor(slot.provider)
701
711
  if (typeof vendor.check !== 'function') continue
702
712
  const cfg2 = vendorConfig(slot.provider, live())
703
- const probeFetch = async (u, i) => fetch(u, i)
713
+ const probeFetch = async (u, i) => ((typeof fetchForRef === 'function' && fetchForRef(ref)) || fetch)(u, i)
704
714
  await vendor.check(fresh, cfg2, probeFetch).catch((e) => {
705
715
  if (logger && logger.warn) logger.warn("[dsh-subscriptions] probe " + ref + ": " + String(e && e.message || e).slice(0, 200))
706
716
  })
@@ -64,6 +64,14 @@ export function parseConsumeResult(raw) {
64
64
  export function createResetCreditService({ loadBlob, fetchImpl, now = Date.now, randomId = randomUUID }) {
65
65
  const challenges = new Map()
66
66
 
67
+ function pruneChallenges(nowMs = now()) {
68
+ for (const [id, ch] of challenges.entries()) {
69
+ if (nowMs > ch.expiresAt) {
70
+ challenges.delete(id)
71
+ }
72
+ }
73
+ }
74
+
67
75
  async function readDetails(ref) {
68
76
  const blob = await loadBlob(ref)
69
77
  const access = blob && blob.accessToken
@@ -101,6 +109,7 @@ export function createResetCreditService({ loadBlob, fetchImpl, now = Date.now,
101
109
  async prepare(ref) {
102
110
  const { accountId, details } = await readDetails(ref)
103
111
  const preparedAt = now()
112
+ pruneChallenges(preparedAt)
104
113
  const readyAt = preparedAt + CONFIRM_DELAY_MS
105
114
  const expiresAt = Math.min(preparedAt + CHALLENGE_TTL_MS, details.creditExpiresAt === undefined ? Number.MAX_SAFE_INTEGER : details.creditExpiresAt)
106
115
  if (expiresAt <= readyAt) throw new Error('the available reset expires too soon')
@@ -18,6 +18,7 @@ export function registerStatusRoutes(ctx, state) {
18
18
  diagnosticsReport,
19
19
  pmL = (s) => s,
20
20
  pmE = (s) => s,
21
+ fetchForRef,
21
22
  } = state
22
23
 
23
24
  async function configResponse() {
@@ -213,7 +214,8 @@ export function registerStatusRoutes(ctx, state) {
213
214
  const probeFetch = async (url, init = {}) => {
214
215
  const timeoutSignal = AbortSignal.timeout(15_000)
215
216
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
216
- const res2 = await fetch(url, { ...init, signal })
217
+ const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
218
+ const res2 = await doFetch(url, { ...init, signal })
217
219
  try {
218
220
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
219
221
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
@@ -327,7 +329,8 @@ export function registerStatusRoutes(ctx, state) {
327
329
  const probeFetch = async (url, init = {}) => {
328
330
  const timeoutSignal = AbortSignal.timeout(15_000)
329
331
  const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
330
- const res2 = await fetch(url, { ...init, signal })
332
+ const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
333
+ const res2 = await doFetch(url, { ...init, signal })
331
334
  try {
332
335
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
333
336
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
@@ -8,8 +8,19 @@ const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
8
8
  * @param {number} [ttlMs]
9
9
  * @returns {void}
10
10
  */
11
+ export function pruneSessionPins(now = Date.now()) {
12
+ for (const [id, entry] of sessionPins.entries()) {
13
+ if (now > entry.expiresAt) {
14
+ sessionPins.delete(id)
15
+ }
16
+ }
17
+ }
18
+
11
19
  export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
12
20
  if (!sessionId || !accountRef) return
21
+ if (sessionPins.size >= 50) {
22
+ pruneSessionPins()
23
+ }
13
24
  sessionPins.set(sessionId, {
14
25
  accountRef,
15
26
  expiresAt: Date.now() + ttlMs
@@ -111,6 +111,85 @@ export async function pollDeviceToken(deviceCode, fetchImpl) {
111
111
  return readJson(res)
112
112
  }
113
113
 
114
+
115
+ export async function deviceStart(cfg, fetchImpl) {
116
+ const json = await requestDeviceCode(fetchImpl)
117
+ if (!json || !json.user_code || !json.device_code) {
118
+ throw httpError(502, "GitHub device code response is incomplete", "DEVICE")
119
+ }
120
+ return {
121
+ userCode: json.user_code,
122
+ deviceAuthId: json.device_code,
123
+ intervalMs: Math.max(parseInt(json.interval, 10) || 5, 1) * 1000,
124
+ authUrl: json.verification_uri || "https://github.com/login/device",
125
+ }
126
+ }
127
+
128
+ export async function devicePoll(cfg, session, fetchImpl) {
129
+ const impl = fetchImpl || fetch
130
+ let json
131
+ try {
132
+ json = await pollDeviceToken(session.deviceAuthId, impl)
133
+ } catch (err) {
134
+ throw httpError(502, String(err && err.message || err), "DEVICE")
135
+ }
136
+ if (json.error === "authorization_pending" || json.error === "slow_down") {
137
+ return { status: "pending" }
138
+ }
139
+ if (json.error) {
140
+ throw httpError(400, json.error_description || json.error, "DEVICE")
141
+ }
142
+ if (!json.access_token) {
143
+ return { status: "pending" }
144
+ }
145
+ const githubToken = json.access_token
146
+ const exchange = await exchangeCopilotToken(githubToken, impl)
147
+ const copilotToken = exchange && exchange.token
148
+ const expiresAt = exchange && exchange.expires_at ? exchange.expires_at * 1000 : Date.now() + 30 * 60 * 1000
149
+
150
+ let label = "copilot"
151
+ let email = ""
152
+ try {
153
+ const userRes = await impl(COPILOT_USER_URL, {
154
+ headers: {
155
+ Authorization: `Bearer ${githubToken}`,
156
+ "User-Agent": "GitHubCopilotChat/0.24.0",
157
+ },
158
+ })
159
+ if (userRes.ok) {
160
+ const user = await readJson(userRes)
161
+ if (user && user.login) {
162
+ label = user.login
163
+ if (user.email) email = user.email
164
+ }
165
+ }
166
+ } catch { /* best-effort */ }
167
+
168
+ return {
169
+ status: "authorized",
170
+ blob: {
171
+ accessToken: copilotToken || githubToken,
172
+ refreshToken: githubToken,
173
+ githubToken,
174
+ expiresAt,
175
+ label,
176
+ email,
177
+ },
178
+ }
179
+ }
180
+
181
+ export async function refresh(cfg, blob, fetchImpl) {
182
+ const impl = fetchImpl || fetch
183
+ const githubToken = blob && (blob.refreshToken || blob.githubToken)
184
+ if (!githubToken) return blob
185
+ const exchange = await exchangeCopilotToken(githubToken, impl)
186
+ return {
187
+ ...blob,
188
+ accessToken: exchange.token || blob.accessToken,
189
+ expiresAt: exchange.expires_at ? exchange.expires_at * 1000 : Date.now() + 30 * 60 * 1000,
190
+ }
191
+ }
192
+
114
193
  export async function exchangeCopilotToken(githubToken, fetchImpl) {
115
194
  const impl = fetchImpl || fetch
116
195
  const res = await impl(COPILOT_TOKEN_URL, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
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",