@goodandready/dsh-subscriptions 0.7.18 → 0.7.20

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 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/client.js CHANGED
@@ -4118,6 +4118,16 @@ h('div', { className: 'dsub-row' },
4118
4118
  const id = setInterval(pull, 60 * 1000)
4119
4119
  return () => { alive = false; clearInterval(id) }
4120
4120
  }, [])
4121
+ React.useEffect(() => {
4122
+ if (!open) return undefined
4123
+ const onDoc = (e) => {
4124
+ if (popRef.current && !popRef.current.contains(e.target) && !(e.target.closest && e.target.closest('.dsub-qw'))) setOpen(false)
4125
+ }
4126
+ const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
4127
+ document.addEventListener('pointerdown', onDoc)
4128
+ document.addEventListener('keydown', onKey)
4129
+ return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey) }
4130
+ }, [open])
4121
4131
  if (state.mode === 'off' || !state.active || state.remaining == null) return null
4122
4132
  const docked = state.mode !== 'floating'
4123
4133
  const tone = state.remaining <= 10 ? ' dsub-qwFull' : (state.remaining <= 30 ? ' dsub-qwWarn' : '')
@@ -4132,16 +4142,6 @@ h('div', { className: 'dsub-row' },
4132
4142
  if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
4133
4143
  } catch { /* bestEffort */ }
4134
4144
  }
4135
- React.useEffect(() => {
4136
- if (!open) return undefined
4137
- const onDoc = (e) => {
4138
- if (popRef.current && !popRef.current.contains(e.target) && !(e.target.closest && e.target.closest('.dsub-qw'))) setOpen(false)
4139
- }
4140
- const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
4141
- document.addEventListener('pointerdown', onDoc)
4142
- document.addEventListener('keydown', onKey)
4143
- return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey) }
4144
- }, [open])
4145
4145
  const onPointerDown = (e) => {
4146
4146
  if (docked) return
4147
4147
  drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
@@ -4358,4 +4358,4 @@ h('div', { className: 'dsub-row' },
4358
4358
  }
4359
4359
  return module.exports
4360
4360
  },
4361
- })
4361
+ })
@@ -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, ' ')
@@ -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
- let activeWindowsCredentialReader = null
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 (activeWindowsCredentialReader) {
22
- return activeWindowsCredentialReader()
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(activeWindowsCredentialReader) || (!activeHome && process.platform === 'win32')
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
@@ -406,6 +415,7 @@ export function apply(ctx, rawConfig) {
406
415
  subscriptions,
407
416
  images: () => ctx.subscriptionImages,
408
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) || 80
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
@@ -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
- if (!sessionId) return null
128
- const entry = sessionPins.get(sessionId)
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
- export function numberOrNull(value) {
2
- const n = Number(value)
3
- return Number.isFinite(n) ? n : null
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.18",
3
+ "version": "0.7.20",
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": {