@goodandready/dsh-subscriptions 0.6.28 → 0.6.29

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.
@@ -46,6 +46,8 @@ export function createAdapterManager({
46
46
  getQuota: (ref) => store.getQuota(ref),
47
47
  refreshUsage: (provider) => store.refreshUsage(provider),
48
48
  saveBlob: (ref, blob) => store.saveBlob(ref, blob),
49
+ alertThresholds: () => live().alertThresholds,
50
+ webhookUrl: () => live().webhookUrl,
49
51
  recordHistory,
50
52
  fetchImpl: fetch,
51
53
  fetchForRef,
package/lib/adapter.js CHANGED
@@ -4,6 +4,7 @@ import { displayName } from './refs.js'
4
4
  import { getVendor } from './vendors/index.js'
5
5
  import { modelCatalog } from './messages.js'
6
6
  import { streamWithRotation } from './stream-rotate.js'
7
+ import { checkQuotaThresholds } from './alerts.js'
7
8
  import { pickFetch } from './proxy.js'
8
9
  import { quotaSnapshot, parseBody } from './ratelimit.js'
9
10
  import { toTokenUsage } from './wire.js'
@@ -112,6 +113,8 @@ export class SubscriptionAdapter extends LlmAdapter {
112
113
  cooldownMs: deps.cooldownMs(),
113
114
  switchAtRemaining: typeof deps.switchAtRemaining === 'function' ? deps.switchAtRemaining() : (deps.switchAtRemaining ?? 0),
114
115
  options,
116
+ webhookUrl: typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl,
117
+ fetchImpl: deps.fetchImpl || fetch,
115
118
  onCooldown: (account) => {
116
119
  if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
117
120
  if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
@@ -129,7 +132,21 @@ export class SubscriptionAdapter extends LlmAdapter {
129
132
  const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs: 120000 })
130
133
  try {
131
134
  const snap = quotaSnapshot(provider, res.headers, null, Date.now())
132
- if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
135
+ if (snap && typeof deps.rememberQuota === "function") {
136
+ deps.rememberQuota(account.ref, snap)
137
+ if (snap.usedPercent != null) {
138
+ const th = typeof deps.alertThresholds === 'function' ? deps.alertThresholds() : deps.alertThresholds
139
+ const wh = typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl
140
+ checkQuotaThresholds({
141
+ ref: account.ref,
142
+ provider,
143
+ usedPercent: snap.usedPercent,
144
+ thresholds: th,
145
+ webhookUrl: wh,
146
+ fetchImpl: deps.fetchImpl || fetch,
147
+ })
148
+ }
149
+ }
133
150
  } catch { /* ignore quota capture failure */ }
134
151
  if (!res.ok && res.clone) {
135
152
  try {
@@ -139,7 +156,21 @@ export class SubscriptionAdapter extends LlmAdapter {
139
156
  const b = parseBody(provider, j, Date.now())
140
157
  if (b) {
141
158
  const snap2 = quotaSnapshot(provider, res.headers, j, Date.now())
142
- if (snap2) deps.rememberQuota(account.ref, snap2)
159
+ if (snap2) {
160
+ deps.rememberQuota(account.ref, snap2)
161
+ if (snap2.usedPercent != null) {
162
+ const th = typeof deps.alertThresholds === 'function' ? deps.alertThresholds() : deps.alertThresholds
163
+ const wh = typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl
164
+ checkQuotaThresholds({
165
+ ref: account.ref,
166
+ provider,
167
+ usedPercent: snap2.usedPercent,
168
+ thresholds: th,
169
+ webhookUrl: wh,
170
+ fetchImpl: deps.fetchImpl || fetch,
171
+ })
172
+ }
173
+ }
143
174
  }
144
175
  }
145
176
  } catch { /* ignore quota capture failure */ }
package/lib/alerts.js CHANGED
@@ -1,3 +1,12 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ let pkgVersion = ''
4
+ try {
5
+ pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || ''
6
+ } catch {
7
+ // ignore package read errors
8
+ }
9
+
1
10
  // #351: Proactive Quota Alerts & Webhook Dispatcher
2
11
  // Manages quota threshold warnings, session expiry notifications,
3
12
  // and webhook dispatch (e.g. Telegram/Discord or external monitoring).
@@ -70,7 +79,7 @@ export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch)
70
79
  method: 'POST',
71
80
  headers: {
72
81
  'content-type': 'application/json',
73
- 'user-agent': 'dsh-subscriptions/0.6.17',
82
+ 'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
74
83
  },
75
84
  body: JSON.stringify(payload),
76
85
  })
package/lib/client.js CHANGED
@@ -149,7 +149,7 @@ window.__ModuleLoader__.load({
149
149
  '.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
150
150
  '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
151
151
  '.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
152
- '.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-default,#3b82f6);background:color-mix(in srgb,var(--dsw-alias-brand-default,#3b82f6) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-default,#3b82f6) 20%,transparent)}' +
152
+ '.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-default);background:color-mix(in srgb,var(--dsw-alias-brand-default) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-default) 20%,transparent)}' +
153
153
  '.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
154
154
  '.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
155
155
  '.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
@@ -477,6 +477,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
477
477
  "save": "Save",
478
478
  "saved": "Saved",
479
479
  "loading": "Loading…",
480
+ "unavailable": "Settings are unavailable",
480
481
  "accountLabel": "Account",
481
482
  "plan": "Plan",
482
483
  "storedAs": "Stored as",
@@ -674,6 +675,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
674
675
  "save": "保存",
675
676
  "saved": "已保存",
676
677
  "loading": "加载中…",
678
+ "unavailable": "设置不可用",
677
679
  "accountLabel": "账户",
678
680
  "plan": "套餐方案",
679
681
  "storedAs": "存储标识",
@@ -2074,6 +2076,33 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2074
2076
  if (props && props.view === 'summary') {
2075
2077
  return React.createElement('div', { className: 'dsub-description' }, t('cardIntro'))
2076
2078
  }
2079
+ // Check host snapshot status (#408): loading or unavailable
2080
+ if (props && props.status === 'loading') {
2081
+ return React.createElement(page ? 'div' : 'li', {
2082
+ className: page ? 'dsub-page' : 'dsub-card',
2083
+ },
2084
+ React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
2085
+ React.createElement('div', { className: 'dsub-headText' },
2086
+ React.createElement('div', { className: 'dsub-name' }, t('title')),
2087
+ React.createElement('div', { className: 'dsub-description' }, t('loading') || t('settingsLoading') || 'Loading…'),
2088
+ ),
2089
+ )
2090
+ )
2091
+ }
2092
+ if (props && props.status === 'unavailable') {
2093
+ return React.createElement(page ? 'div' : 'li', {
2094
+ className: page ? 'dsub-page' : 'dsub-card',
2095
+ },
2096
+ React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
2097
+ React.createElement('div', { className: 'dsub-headText' },
2098
+ React.createElement('div', { className: 'dsub-name' }, t('title')),
2099
+ React.createElement('div', { className: 'dsub-description', style: { color: 'var(--dsw-alias-state-warning-primary)' } },
2100
+ t('unavailable') || t('settingsUnavailable') || 'Settings are unavailable',
2101
+ ),
2102
+ ),
2103
+ )
2104
+ )
2105
+ }
2077
2106
  return React.createElement(page ? 'div' : 'li', {
2078
2107
  className: page ? 'dsub-page' : 'dsub-card' + (open ? ' dsub-cardOpen' : ''),
2079
2108
  },
package/lib/routes.js CHANGED
@@ -24,6 +24,10 @@ export function registerRoutes(ctx, state) {
24
24
 
25
25
  ctx.effect(() => registerPluginUpdater(ctx, {
26
26
  endpoint: '/dsh-subscriptions/update',
27
+ aliases: [
28
+ '/api/@goodandready/dsh-subscriptions/update',
29
+ '/api/dsh-subscriptions/update',
30
+ ],
27
31
  packageName: '@goodandready/dsh-subscriptions',
28
32
  manifestUrl: new URL('../package.json', import.meta.url),
29
33
  }), 'dsh-subscriptions: /update')
@@ -1,5 +1,6 @@
1
1
  import { pickAccount, markCooldown, isSwitchableError, isRegionError, modelFamily } from './rotate.js'
2
2
  import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED } from './quarantine.js'
3
+ import { notifySessionExpired } from './alerts.js'
3
4
  import { withRunawayGuard } from './runaway-guard.js'
4
5
 
5
6
  export async function* streamWithRotation({
@@ -12,6 +13,8 @@ export async function* streamWithRotation({
12
13
  onCooldown,
13
14
  cascadeFallback, // #349: optional cross-vendor cascading fallback generator
14
15
  offlineFallback, // #174: optional fallback generator if all accounts exhausted
16
+ webhookUrl,
17
+ fetchImpl = fetch,
15
18
  }) {
16
19
  const pool = (accounts || []).map((account) => ({ ...account }))
17
20
  let lastError = null
@@ -89,6 +92,19 @@ export async function* streamWithRotation({
89
92
  account.quarantineUntil = quarantined.quarantineUntil
90
93
  account.quarantineReason = quarantined.quarantineReason
91
94
 
95
+ if (reason === REASON_REVOKED) {
96
+ try {
97
+ notifySessionExpired({
98
+ ref: account.ref,
99
+ provider: options && options.provider,
100
+ message: err && err.message,
101
+ webhookUrl: webhookUrl || (options && options.webhookUrl),
102
+ fetchImpl,
103
+ nowMs: nowMs(),
104
+ })
105
+ } catch { /* fire-and-forget alert */ }
106
+ }
107
+
92
108
  if (onCooldown) onCooldown(account)
93
109
  // Break inner retry loop to continue to next account in outer while (true) loop
94
110
  break
package/lib/updater.js CHANGED
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
4
4
  import { homedir } from 'node:os'
5
5
  import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
- import { writeJson } from './http.js'
7
+ import { isTrustedSettingsRequest, writeJson } from './http.js'
8
8
 
9
9
  const UPDATE_HEADER = 'x-dsh-plugin-update'
10
10
  const UPDATE_TIMEOUT_MS = 10 * 60_000
@@ -25,24 +25,11 @@ export function isLoopback(value) {
25
25
 
26
26
  export function isTrustedUpdateRequest(request) {
27
27
  const customHeader = header(request, UPDATE_HEADER)
28
- if (customHeader !== '1') {
29
- // In browser fetch, allow if sec-fetch-site is same-origin or same-site
30
- const site = header(request, 'sec-fetch-site')
31
- if (site === 'cross-site') return false
28
+ if (customHeader === '1') {
29
+ const remote = request.socket?.remoteAddress
30
+ if (remote && isLoopback(remote)) return true
32
31
  }
33
- const remote = request.socket?.remoteAddress
34
- if (remote && !isLoopback(remote)) return false
35
- const origin = header(request, 'origin')
36
- const host = header(request, 'host')
37
- if (origin && host) {
38
- try {
39
- const url = new URL(origin)
40
- if (url.host !== host && !isLoopback(url.hostname)) return false
41
- } catch {
42
- return false
43
- }
44
- }
45
- return true
32
+ return isTrustedSettingsRequest(request)
46
33
  }
47
34
 
48
35
  function validProfileName(value) {
@@ -206,10 +193,11 @@ export async function installExactPackage(target, packageSpec, options) {
206
193
 
207
194
  export function registerPluginUpdater(ctx, options) {
208
195
  let installing = false
209
- return ctx.webServer.register({
210
- kind: 'exact',
211
- path: options.endpoint,
212
- handler: async (request, response) => {
196
+ const endpoints = Array.isArray(options.endpoint)
197
+ ? options.endpoint
198
+ : [options.endpoint, ...(Array.isArray(options.aliases) ? options.aliases : [])]
199
+
200
+ const handler = async (request, response) => {
213
201
  try {
214
202
  const target = getRuntime()
215
203
  if (request.method === 'GET' || request.method === 'HEAD') {
@@ -255,6 +243,17 @@ export function registerPluginUpdater(ctx, options) {
255
243
  ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
256
244
  writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
257
245
  }
258
- },
259
- })
246
+ }
247
+
248
+ const unregisters = endpoints.map((ep) => ctx.webServer.register({
249
+ kind: 'exact',
250
+ path: ep,
251
+ handler,
252
+ }))
253
+
254
+ return () => {
255
+ for (const un of unregisters) {
256
+ if (typeof un === 'function') un()
257
+ }
258
+ }
260
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
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",