@goodandready/dsh-subscriptions 0.7.17 → 0.7.19
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/README.md +20 -0
- package/README.ru.md +20 -0
- package/README.zh.md +20 -0
- package/lib/config-schema.js +2 -0
- package/lib/diagnostics.js +2 -0
- package/lib/http.js +6 -10
- package/lib/import-auth.js +5 -8
- package/lib/index.js +11 -1
- package/lib/loopback.js +8 -3
- package/lib/native-tools.js +3 -1
- package/lib/session-pin.js +2 -9
- package/lib/usage.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -372,6 +372,26 @@ Supports Anthropic's adaptive thinking (`thinking: { type: "adaptive" }`) and re
|
|
|
372
372
|
|
|
373
373
|
---
|
|
374
374
|
|
|
375
|
+
## 🔌 HTTP API
|
|
376
|
+
|
|
377
|
+
The plugin registers these exact routes on the DSH web server. Unless noted, they accept same-origin or loopback requests; mutating requests must carry `Origin` or `Referer` matching the host (#782). Responses are JSON unless noted.
|
|
378
|
+
|
|
379
|
+
| Method | Path | Purpose |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| GET | `/dsh-subscriptions/usage?type=accounts\|calls&format=json\|csv` | Quota snapshot and per-call ledger for external quota plugins (#687). |
|
|
382
|
+
| GET | `/dsh-subscriptions/instructions` | Dynamic vendor connection instructions (#540). |
|
|
383
|
+
| GET | `/dsh-subscriptions/metrics` | Prometheus text exposition (#744); loopback or same-origin only. |
|
|
384
|
+
| GET | `/dsh-subscriptions/health` | Deep health report used by monitoring (#745); public GET. |
|
|
385
|
+
| GET | `/dsh-subscriptions/models` | Enriched model catalog for active subscriptions. |
|
|
386
|
+
| POST | `/dsh-subscriptions/heartbeat/run` | Trigger one quota/health refresh; returns `{ok, refreshed}`. |
|
|
387
|
+
| GET | `/dsh-subscriptions/discover-local` | Detect locally installed CLI sessions available for import. |
|
|
388
|
+
| GET | `/dsh-subscriptions/subscriptions` | Localhost-only HTML overview of subscriptions. |
|
|
389
|
+
| GET | `/dsh-subscriptions/session-reasoning?sessionId=…&provider=…&model=…` | Read reasoning effort/capabilities for a session. |
|
|
390
|
+
| POST | `/dsh-subscriptions/session-reasoning` | Set `{sessionId, provider, model, effort, budgetTokens}`. |
|
|
391
|
+
| DELETE | `/dsh-subscriptions/session-reasoning?sessionId=…` | Clear session reasoning override. |
|
|
392
|
+
|
|
393
|
+
`/discover-local` and `/subscriptions` are retained as supported API/UI surfaces (decision recorded in `docs/design/DESIGN.md`).
|
|
394
|
+
|
|
375
395
|
## 🌐 Localization
|
|
376
396
|
|
|
377
397
|
The plugin source language is English only. Russian and other translations are provided at runtime by separate language plugins (for example the russification plugin), which translate the registered locale keys - the package itself ships no bundled translations (Changed in 0.6.1).
|
package/README.ru.md
CHANGED
|
@@ -345,6 +345,26 @@ dsh-subscriptions:
|
|
|
345
345
|
|
|
346
346
|
---
|
|
347
347
|
|
|
348
|
+
## 🔌 HTTP API
|
|
349
|
+
|
|
350
|
+
Плагин регистрирует на веб-сервере DSH следующие точные маршруты. Если не указано иное, они принимают same-origin или loopback-запросы; изменяющие запросы обязаны содержать `Origin` или `Referer`, совпадающий с хостом (#782). Ответы — JSON, если не указано иное.
|
|
351
|
+
|
|
352
|
+
| Метод | Путь | Назначение |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| GET | `/dsh-subscriptions/usage?type=accounts\|calls&format=json\|csv` | Снимок квоты и журнал вызовов для внешних плагинов квот (#687). |
|
|
355
|
+
| GET | `/dsh-subscriptions/instructions` | Динамические инструкции по подключению вендоров (#540). |
|
|
356
|
+
| GET | `/dsh-subscriptions/metrics` | Prometheus text exposition (#744); только loopback или same-origin. |
|
|
357
|
+
| GET | `/dsh-subscriptions/health` | Подробный отчёт здоровья для мониторинга (#745); публичный GET. |
|
|
358
|
+
| GET | `/dsh-subscriptions/models` | Обогащённый каталог моделей активных подписок. |
|
|
359
|
+
| POST | `/dsh-subscriptions/heartbeat/run` | Запустить одно обновление квот/здоровья; возвращает `{ok, refreshed}`. |
|
|
360
|
+
| GET | `/dsh-subscriptions/discover-local` | Найти локально установленные CLI-сессии для импорта. |
|
|
361
|
+
| GET | `/dsh-subscriptions/subscriptions` | HTML-обзор подписок только с localhost. |
|
|
362
|
+
| GET | `/dsh-subscriptions/session-reasoning?sessionId=…&provider=…&model=…` | Прочитать reasoning effort и возможности модели для сессии. |
|
|
363
|
+
| POST | `/dsh-subscriptions/session-reasoning` | Установить `{sessionId, provider, model, effort, budgetTokens}`. |
|
|
364
|
+
| DELETE | `/dsh-subscriptions/session-reasoning?sessionId=…` | Сбросить переопределение reasoning для сессии. |
|
|
365
|
+
|
|
366
|
+
`/discover-local` и `/subscriptions` сохранены как поддерживаемые API/UI-поверхности (решение записано в `docs/design/DESIGN.md`).
|
|
367
|
+
|
|
348
368
|
## 🌐 Локализация
|
|
349
369
|
|
|
350
370
|
Исходный язык плагина - только английский. Русский и другие переводы во время работы предоставляют отдельные языковые плагины (например, плагин русификации), переводящие зарегистрированные ключи локали, - сам пакет не содержит встроенных переводов (Changed in 0.6.1).
|
package/README.zh.md
CHANGED
|
@@ -251,6 +251,26 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
|
251
251
|
|
|
252
252
|
---
|
|
253
253
|
|
|
254
|
+
## 🔌 HTTP API
|
|
255
|
+
|
|
256
|
+
插件在 DSH Web 服务器上注册以下精确路由。除非另有说明,仅接受同源或 loopback 请求;写入请求必须携带与 Host 匹配的 `Origin` 或 `Referer`(#782)。除特别说明外返回 JSON。
|
|
257
|
+
|
|
258
|
+
| 方法 | 路径 | 用途 |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| GET | `/dsh-subscriptions/usage?type=accounts\|calls&format=json\|csv` | 面向外部配额插件的额度快照与调用账本(#687)。 |
|
|
261
|
+
| GET | `/dsh-subscriptions/instructions` | 动态供应商接入说明(#540)。 |
|
|
262
|
+
| GET | `/dsh-subscriptions/metrics` | Prometheus 文本指标(#744);仅 loopback 或同源。 |
|
|
263
|
+
| GET | `/dsh-subscriptions/health` | 供监控使用的深度健康报告(#745);公开 GET。 |
|
|
264
|
+
| GET | `/dsh-subscriptions/models` | 当前订阅的增强模型目录。 |
|
|
265
|
+
| POST | `/dsh-subscriptions/heartbeat/run` | 触发一次额度/健康刷新;返回 `{ok, refreshed}`。 |
|
|
266
|
+
| GET | `/dsh-subscriptions/discover-local` | 发现可导入的本地 CLI 会话。 |
|
|
267
|
+
| GET | `/dsh-subscriptions/subscriptions` | 仅限 localhost 的 HTML 订阅概览。 |
|
|
268
|
+
| GET | `/dsh-subscriptions/session-reasoning?sessionId=…&provider=…&model=…` | 读取会话的 reasoning effort 与模型能力。 |
|
|
269
|
+
| POST | `/dsh-subscriptions/session-reasoning` | 设置 `{sessionId, provider, model, effort, budgetTokens}`。 |
|
|
270
|
+
| DELETE | `/dsh-subscriptions/session-reasoning?sessionId=…` | 清除会话 reasoning 覆盖。 |
|
|
271
|
+
|
|
272
|
+
`/discover-local` 与 `/subscriptions` 保留为受支持的 API/UI 接口(决策记录于 `docs/design/DESIGN.md`)。
|
|
273
|
+
|
|
254
274
|
## 🌐 本地化
|
|
255
275
|
|
|
256
276
|
插件源语言仅为英语。俄语及其他翻译由独立的语言插件(如俄化插件)在运行时对注册的本地化键进行翻译,包本身不内置翻译(Changed in 0.6.1)。
|
package/lib/config-schema.js
CHANGED
|
@@ -231,6 +231,8 @@ export const Config = z.object({
|
|
|
231
231
|
.description('Declarative OpenAI-Responses-compatible providers. See README.'),
|
|
232
232
|
providerPacks: z.array(z.any()).volatile().default([])
|
|
233
233
|
.description('#694 Data-only vendor packs registered through the provider SDK.'),
|
|
234
|
+
nativeTools: z.boolean().volatile().default(false)
|
|
235
|
+
.description('#18 Opt-in subscription-native image/x/web/video tools. Off by default because these names shadow builtin tools; enable only when the host does not provide them.'),
|
|
234
236
|
})
|
|
235
237
|
|
|
236
238
|
export function publicConfig(cfg) {
|
package/lib/diagnostics.js
CHANGED
|
@@ -7,6 +7,7 @@ import { maskSecrets } from './mask.js'
|
|
|
7
7
|
import { normalizeSlots } from './accounts.js'
|
|
8
8
|
import { PROVIDERS } from './refs.js'
|
|
9
9
|
import { defaultErrorBuffer } from './error-buffer.js'
|
|
10
|
+
import { grokCatalogCacheSize } from './vendors/grok.js'
|
|
10
11
|
|
|
11
12
|
function scrubReport(v) {
|
|
12
13
|
if (typeof v === 'string') return maskSecrets(v)
|
|
@@ -55,6 +56,7 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
55
56
|
recentErrors: defaultErrorBuffer.getEntries(),
|
|
56
57
|
heartbeat: heartbeatStats || { lastRunAt: null, proactiveRefreshCount: 0 },
|
|
57
58
|
cache: calculateCacheMetrics(rows),
|
|
59
|
+
grokCatalogCacheEntries: grokCatalogCacheSize(),
|
|
58
60
|
queue: defaultConcurrencyQueue.stats(),
|
|
59
61
|
pins: getSessionPinStats(),
|
|
60
62
|
reasoning: getSessionReasoningStats(),
|
package/lib/http.js
CHANGED
|
@@ -113,6 +113,12 @@ function configuredTrustedHosts(targetCtx) {
|
|
|
113
113
|
export function isTrustedSettingsRequest(request, targetCtx) {
|
|
114
114
|
const headers = request?.headers || {}
|
|
115
115
|
|
|
116
|
+
// #782: every mutation must prove its browser origin. A loopback socket is
|
|
117
|
+
// not enough because the LAN bridge rewrites Host and connects over loopback.
|
|
118
|
+
const method = String(request?.method || '').toUpperCase()
|
|
119
|
+
const mutating = method !== '' && method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS'
|
|
120
|
+
if (mutating && !headers.origin && !headers.referer) return false
|
|
121
|
+
|
|
116
122
|
// 1. Explicit cross-site fetch is always rejected
|
|
117
123
|
const secFetchSite = String(headers['sec-fetch-site'] || '').toLowerCase()
|
|
118
124
|
if (secFetchSite === 'cross-site') return false
|
|
@@ -269,16 +275,6 @@ export function queryOf(req) {
|
|
|
269
275
|
* Fetch with connect/overall timeout using AbortSignal.any.
|
|
270
276
|
*/
|
|
271
277
|
|
|
272
|
-
export function isSameOrigin(url1, url2) {
|
|
273
|
-
try {
|
|
274
|
-
const u1 = new URL(url1)
|
|
275
|
-
const u2 = new URL(url2)
|
|
276
|
-
return u1.origin.toLowerCase() === u2.origin.toLowerCase()
|
|
277
|
-
} catch {
|
|
278
|
-
return false
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
278
|
export function headerSafeValue(value) {
|
|
283
279
|
return String(value == null ? '' : value)
|
|
284
280
|
.replace(/[\r\n]+/g, ' ')
|
package/lib/import-auth.js
CHANGED
|
@@ -11,15 +11,12 @@ import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from '
|
|
|
11
11
|
// Test seam: a fixture root overrides the real home for the duration of a
|
|
12
12
|
// public call. Undefined (the default) keeps production behaviour.
|
|
13
13
|
let activeHome = null
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function setWindowsCredentialReaderForTest(fn) {
|
|
17
|
-
activeWindowsCredentialReader = fn
|
|
18
|
-
}
|
|
14
|
+
// Injectable Windows Credential Manager reader used by fixture tests.
|
|
15
|
+
export const windowsCredentialReader = { current: null }
|
|
19
16
|
|
|
20
17
|
export function readWindowsAntigravityCredential() {
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
18
|
+
if (windowsCredentialReader.current) {
|
|
19
|
+
return windowsCredentialReader.current()
|
|
23
20
|
}
|
|
24
21
|
if (process.platform !== 'win32') return null
|
|
25
22
|
try {
|
|
@@ -146,7 +143,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
146
143
|
}
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
const canReadWindowsCredentialManager = Boolean(
|
|
146
|
+
const canReadWindowsCredentialManager = Boolean(windowsCredentialReader.current) || (!activeHome && process.platform === 'win32')
|
|
150
147
|
if (!detected.antigravity && canReadWindowsCredentialManager) {
|
|
151
148
|
const winCred = readWindowsAntigravityCredential()
|
|
152
149
|
const tok = winCred && winCred.token ? winCred.token : winCred
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,10 @@ import { createAdapterManager } from './adapter-manager.js'
|
|
|
30
30
|
import { pickAccount, isAuthError } from './rotate.js'
|
|
31
31
|
import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret, setWebhookAllowPrivate, setWebhookFormat } from './alerts.js'
|
|
32
32
|
import { isDigestDue } from './notification-scheduler.js'
|
|
33
|
+
import { clearSessionPins } from './session-pin.js'
|
|
34
|
+
import { clearKeyRotationCache } from './key-rotation-bridge.js'
|
|
35
|
+
import { clearOllamaHealthCache } from './ollama-health.js'
|
|
36
|
+
import { clearGrokCatalogCache } from './vendors/grok.js'
|
|
33
37
|
|
|
34
38
|
export { observeForecast, estimateForecast } from './forecast.js'
|
|
35
39
|
export { formatRelativeReset } from './relative-time.js'
|
|
@@ -100,6 +104,7 @@ export function apply(ctx, rawConfig) {
|
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function syncCustomVendors() {
|
|
107
|
+
clearGrokCatalogCache()
|
|
103
108
|
const profiles = validateProfiles(live().customVendors)
|
|
104
109
|
clearCustomVendors()
|
|
105
110
|
for (const profile of profiles) {
|
|
@@ -130,6 +135,8 @@ export function apply(ctx, rawConfig) {
|
|
|
130
135
|
try {
|
|
131
136
|
const fresh = next !== undefined ? next : resolveEntry()
|
|
132
137
|
cachedConfig = fresh
|
|
138
|
+
clearKeyRotationCache()
|
|
139
|
+
clearOllamaHealthCache()
|
|
133
140
|
syncCustomVendors()
|
|
134
141
|
syncAdapter().catch(() => {})
|
|
135
142
|
syncOllama().catch(() => {})
|
|
@@ -165,6 +172,8 @@ export function apply(ctx, rawConfig) {
|
|
|
165
172
|
await settingsSvc.replace(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
|
|
166
173
|
}
|
|
167
174
|
cachedConfig = parsed
|
|
175
|
+
clearKeyRotationCache()
|
|
176
|
+
clearOllamaHealthCache()
|
|
168
177
|
syncCustomVendors()
|
|
169
178
|
} catch (err) {
|
|
170
179
|
cachedConfig = prevConfig
|
|
@@ -405,7 +414,8 @@ export function apply(ctx, rawConfig) {
|
|
|
405
414
|
ctx.effect(() => registerSubscriptionTools(ctx, {
|
|
406
415
|
subscriptions,
|
|
407
416
|
images: () => ctx.subscriptionImages,
|
|
408
|
-
}), 'dsh-subscriptions: native tools')
|
|
417
|
+
}, { enabled: live().nativeTools || process.env.DSH_SUBSCRIPTIONS_NATIVE_TOOLS === '1' }), 'dsh-subscriptions: native tools')
|
|
418
|
+
ctx.effect(() => () => clearSessionPins(), 'dsh-subscriptions: session pins cleanup')
|
|
409
419
|
|
|
410
420
|
const pending = new Map()
|
|
411
421
|
|
package/lib/loopback.js
CHANGED
|
@@ -18,12 +18,12 @@ const ERR_HTML = '<!doctype html><meta charset="utf-8"><title>Subscriptions</tit
|
|
|
18
18
|
* Throwing means an authorization error (ERR_HTML is served).
|
|
19
19
|
* @returns {Promise<{url: string, close: () => void}>}
|
|
20
20
|
*/
|
|
21
|
-
export function startLoopback({ redirectUri, timeoutMs = 10 * 60 * 1000, onCode }) {
|
|
21
|
+
export function startLoopback({ redirectUri, timeoutMs = 10 * 60 * 1000, onCode, onListening }) {
|
|
22
22
|
const parsed = new URL(redirectUri)
|
|
23
23
|
if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
24
24
|
throw new Error(`loopback redirect requires localhost, got ${parsed.hostname}`)
|
|
25
25
|
}
|
|
26
|
-
const port = Number(parsed.port)
|
|
26
|
+
const port = parsed.port === '' ? 80 : Number(parsed.port)
|
|
27
27
|
const path = parsed.pathname
|
|
28
28
|
|
|
29
29
|
const state = { server: null, timer: null, done: false }
|
|
@@ -55,7 +55,12 @@ export function startLoopback({ redirectUri, timeoutMs = 10 * 60 * 1000, onCode
|
|
|
55
55
|
reject(err)
|
|
56
56
|
})
|
|
57
57
|
state.server = server
|
|
58
|
-
server.listen(port, parsed.hostname, () => {
|
|
58
|
+
server.listen(port, parsed.hostname, () => {
|
|
59
|
+
if (typeof onListening === 'function') {
|
|
60
|
+
const address = server.address()
|
|
61
|
+
onListening(address && typeof address === 'object' ? address.port : port)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
59
64
|
state.timer = setTimeout(() => {
|
|
60
65
|
if (state.done) return
|
|
61
66
|
state.done = true
|
package/lib/native-tools.js
CHANGED
|
@@ -57,12 +57,14 @@ async function saveImages(images, exec) {
|
|
|
57
57
|
return out
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function registerSubscriptionTools(ctx, deps) {
|
|
60
|
+
export function registerSubscriptionTools(ctx, deps, options = {}) {
|
|
61
|
+
if (options && options.enabled === false) return () => {}
|
|
61
62
|
const tools = ctx && ctx.tools
|
|
62
63
|
if (!tools || typeof tools.register !== 'function') return () => {}
|
|
63
64
|
const disposers = []
|
|
64
65
|
const register = (definition) => {
|
|
65
66
|
try {
|
|
67
|
+
if (typeof tools.get === 'function' && tools.get(definition.name)) return
|
|
66
68
|
const off = tools.register(definition)
|
|
67
69
|
if (typeof off === 'function') disposers.push(off)
|
|
68
70
|
} catch (err) {
|
package/lib/session-pin.js
CHANGED
|
@@ -124,15 +124,8 @@ export function pinSession(sessionId, accountRef, options = {}) {
|
|
|
124
124
|
* @returns {string|null}
|
|
125
125
|
*/
|
|
126
126
|
export function getPinnedAccountRef(sessionId, now = Date.now()) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!entry) return null
|
|
130
|
-
if (now > entry.expiresAt) {
|
|
131
|
-
sessionPins.delete(sessionId)
|
|
132
|
-
recordReleaseReason('pin_released_expired')
|
|
133
|
-
return null
|
|
134
|
-
}
|
|
135
|
-
return entry.accountRef
|
|
127
|
+
const details = getPinnedSessionDetails(sessionId, now)
|
|
128
|
+
return details ? details.accountRef : null
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
/**
|
package/lib/usage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
import { numberOrNull } from './ratelimit.js'
|
|
2
|
+
|
|
3
|
+
// Keep the historical import path: usage.js is the public home for these helpers.
|
|
4
|
+
export { numberOrNull }
|
|
5
5
|
|
|
6
6
|
export function windowPercent(obj, key) {
|
|
7
7
|
if (!obj || typeof obj !== 'object') return null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "eslint lib/ test/ && npm run typecheck && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
42
|
+
"test": "eslint lib/ test/ && npm run typecheck && node --import ./test/_setup.mjs --test test/*.test.mjs && node scripts/check-pack-size.mjs",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
44
44
|
},
|
|
45
45
|
"dsh": {
|