@goodandready/dsh-subscriptions 0.6.14 → 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 +2 -1
- package/lib/adapter.js +5 -2
- package/lib/client.js +16 -1
- package/lib/http.js +29 -1
- package/lib/index.js +20 -10
- package/lib/reset-credits.js +9 -0
- package/lib/routes/status.js +5 -2
- package/lib/session-pin.js +11 -0
- package/lib/vendors/copilot.js +79 -0
- package/package.json +1 -1
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
|
-
|
|
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/client.js
CHANGED
|
@@ -1838,7 +1838,22 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1838
1838
|
// and collapse instead of a row in the side list. The registration key must
|
|
1839
1839
|
// equal the settings namespace, otherwise the tab silently hides the slot.
|
|
1840
1840
|
try {
|
|
1841
|
-
//
|
|
1841
|
+
// Plugin-list seat (plugins.item) first: the seat the current core
|
|
1842
|
+
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration. The
|
|
1843
|
+
// label is a static string on purpose — it is resolved while the page renders,
|
|
1844
|
+
// and a locale lookup there would take the whole client batch down with it.
|
|
1845
|
+
ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
1846
|
+
{
|
|
1847
|
+
name: 'plugins.item',
|
|
1848
|
+
id: ROW_ID,
|
|
1849
|
+
order: 60,
|
|
1850
|
+
label: () => 'Subscriptions',
|
|
1851
|
+
locale: NS,
|
|
1852
|
+
inject: () => ({ ctx: ctx }),
|
|
1853
|
+
},
|
|
1854
|
+
PluginCard,
|
|
1855
|
+
))
|
|
1856
|
+
// Row seat and the legacy seat stay as fallbacks.
|
|
1842
1857
|
ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
|
|
1843
1858
|
{
|
|
1844
1859
|
name: 'plugins.row.config',
|
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
|
-
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|
package/lib/reset-credits.js
CHANGED
|
@@ -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')
|
package/lib/routes/status.js
CHANGED
|
@@ -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
|
|
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
|
|
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) }
|
package/lib/session-pin.js
CHANGED
|
@@ -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
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -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.
|
|
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",
|