@goodandready/dsh-subscriptions 0.6.8 → 0.6.10

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/mask.js CHANGED
@@ -30,7 +30,7 @@ export function cleanErrorMessage(raw) {
30
30
  if (parsed.error && parsed.error.message) text = parsed.error.message
31
31
  else if (parsed.message) text = parsed.message
32
32
  else if (parsed.code) text = parsed.code
33
- } catch {}
33
+ } catch (err) { /* ignore JSON parse failure */ }
34
34
  }
35
35
  const firstLine = text.split('\n')[0].trim()
36
36
  if (firstLine.length > 160) {
@@ -20,14 +20,6 @@ export const RELATIVE_UNITS = {
20
20
  hour: '{n}小时',
21
21
  day: '{n}天',
22
22
  },
23
- ru: {
24
- soon: 'только что',
25
- prefix: 'через ',
26
- suffix: '',
27
- minute: '{n} мин',
28
- hour: '{n} ч',
29
- day: '{n} дн',
30
- },
31
23
  }
32
24
 
33
25
  function fill(template, n) {
@@ -21,7 +21,7 @@ export function registerProxyRoutes(ctx, state) {
21
21
  }
22
22
  const raw = await readBody(req, 128 * 1024).catch(() => Buffer.alloc(0))
23
23
  let body = {}
24
- try { body = JSON.parse(raw.toString('utf8') || '{}') } catch {}
24
+ try { body = JSON.parse(raw.toString('utf8') || '{}') } catch (err) { body = {} }
25
25
  const events = Array.isArray(body && body.events) ? body.events : []
26
26
  const analysis = analyzeSessionEvents(events)
27
27
  writeJson(res, 200, { ok: true, analysis })
@@ -95,7 +95,7 @@ export function registerStatusRoutes(ctx, state) {
95
95
  expires[slot.provider] = Math.max(expires[slot.provider] || 0, slot.expiresAt)
96
96
  labels[slot.provider] = pmL(slot.label || info.label || slot.provider)
97
97
  }
98
- } catch {}
98
+ } catch (err) { /* ignore status inspection error */ }
99
99
  }
100
100
  // #69/#72: active subscription = last successful request (newest first).
101
101
  let active = null
@@ -118,7 +118,7 @@ export function registerStatusRoutes(ctx, state) {
118
118
  .filter((w) => w && typeof w.usedPercent === 'number')
119
119
  .map((w) => ({ id: w.id || w.en || w.ru, label: w.en || w.ru || w.id, usedPercent: w.usedPercent }))
120
120
  }
121
- } catch {}
121
+ } catch (err) { /* ignore status inspection error */ }
122
122
  active = {
123
123
  provider: lastRow.provider,
124
124
  index,
@@ -177,26 +177,27 @@ export function registerStatusRoutes(ctx, state) {
177
177
  writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
178
178
  return
179
179
  }
180
- const provider = payload.provider
180
+ const provider = payload.provider || payload.vendor
181
+ const index = payload.index || (payload.accountIndex != null ? payload.accountIndex + 1 : 1)
181
182
  if (!isProvider(provider)) {
182
183
  writeJson(res, 200, { ok: false, provider, error: { code: 'provider', message: 'unknown provider' } })
183
184
  return
184
185
  }
185
186
  const t0 = Date.now()
186
- const ref = oauthRef(provider, payload.index)
187
+ const ref = oauthRef(provider, index)
187
188
  const info = await store.describeRef(ref)
188
189
  if (!info.configured) {
189
- writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'not_connected', message: 'not connected' } })
190
+ writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'not_connected', message: 'not connected' } })
190
191
  return
191
192
  }
192
193
  let blob
193
194
  try { blob = await store.loadBlob(ref) } catch (e) {
194
- writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
195
+ writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
195
196
  return
196
197
  }
197
198
  try { blob = await store.ensureFresh(provider, blob, ref) } catch (e) {
198
199
  writeJson(res, 200, {
199
- ok: false, provider, index: payload.index, ref,
200
+ ok: false, provider, index: index, ref,
200
201
  email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
201
202
  quota: info.quota || null,
202
203
  error: { code: 'refresh', message: String(e && e.message || e) },
@@ -205,25 +206,27 @@ export function registerStatusRoutes(ctx, state) {
205
206
  }
206
207
  const vendor = getVendor(provider)
207
208
  if (typeof vendor.check !== 'function') {
208
- writeJson(res, 200, { ok: true, provider, index: payload.index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
209
+ writeJson(res, 200, { ok: true, provider, index: index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
209
210
  return
210
211
  }
211
212
  let capturedQuota = null
212
- const probeFetch = async (url, init) => {
213
- const res2 = await fetch(url, init)
213
+ const probeFetch = async (url, init = {}) => {
214
+ const timeoutSignal = AbortSignal.timeout(15_000)
215
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
216
+ const res2 = await fetch(url, { ...init, signal })
214
217
  try {
215
218
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
216
219
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
217
- } catch {}
220
+ } catch (err) { /* ignore status inspection error */ }
218
221
  return res2
219
222
  }
220
223
  try {
221
224
  await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
222
225
  const latencyMs = Date.now() - t0;
223
- writeJson(res, 200, { ok: true, provider, index: payload.index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
226
+ writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
224
227
  } catch (e) {
225
228
  writeJson(res, 200, {
226
- ok: false, provider, index: payload.index, ref,
229
+ ok: false, provider, index: index, ref,
227
230
  latencyMs: Date.now() - t0,
228
231
  email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
229
232
  quota: capturedQuota || info.quota || null,
@@ -280,7 +283,7 @@ export function registerStatusRoutes(ctx, state) {
280
283
  return
281
284
  }
282
285
  let payload = {}
283
- try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {}
286
+ try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch (err) { /* ignore status inspection error */ }
284
287
 
285
288
  let provider = payload.provider
286
289
  let index = payload.index || 1
@@ -321,12 +324,14 @@ export function registerStatusRoutes(ctx, state) {
321
324
  const vendor = getVendor(provider)
322
325
  if (typeof vendor.check === 'function') {
323
326
  let capturedQuota = null
324
- const probeFetch = async (url, init) => {
325
- const res2 = await fetch(url, init)
327
+ const probeFetch = async (url, init = {}) => {
328
+ const timeoutSignal = AbortSignal.timeout(15_000)
329
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
330
+ const res2 = await fetch(url, { ...init, signal })
326
331
  try {
327
332
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
328
333
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
329
- } catch {}
334
+ } catch (err) { /* ignore status inspection error */ }
330
335
  return res2
331
336
  }
332
337
  try {
package/lib/routes.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { publicConfig } from './config-schema.js'
2
2
  import { PROVIDERS, displayName } from './refs.js'
3
- import { escapeHtml, writeHtml, writeJson } from './http.js'
3
+ import { escapeHtml, isLoopback, writeHtml, writeJson } from './http.js'
4
4
  import { registerStatusRoutes } from './routes/status.js'
5
5
  import { registerOauthRoutes } from './routes/oauth.js'
6
6
  import { registerAccountsRoutes } from './routes/accounts.js'
7
7
  import { registerProxyRoutes } from './routes/proxy.js'
8
+ import { registerPluginUpdater } from './updater.js'
8
9
 
9
10
  export function registerRoutes(ctx, state) {
10
11
  const {
@@ -22,6 +23,11 @@ export function registerRoutes(ctx, state) {
22
23
  }
23
24
  }
24
25
 
26
+ ctx.effect(() => registerPluginUpdater(ctx, {
27
+ endpoint: '/dsh-subscriptions/update',
28
+ packageName: '@goodandready/dsh-subscriptions',
29
+ manifestUrl: new URL('../package.json', import.meta.url),
30
+ }), 'dsh-subscriptions: /update')
25
31
 
26
32
  ctx.effect(() => ctx.webServer.register({
27
33
  kind: 'exact',
@@ -32,7 +38,8 @@ export function registerRoutes(ctx, state) {
32
38
  return
33
39
  }
34
40
  const host = (req.headers.host || '').split(':')[0]
35
- if (host !== 'localhost' && host !== '127.0.0.1') {
41
+ const remote = req.socket?.remoteAddress
42
+ if (!isLoopback(host) || (remote && !isLoopback(remote))) {
36
43
  writeHtml(res, 403, '<!doctype html><meta charset="utf-8"><p>Subscriptions overview is localhost-only.</p>')
37
44
  return
38
45
  }
@@ -20,7 +20,7 @@ export function isAllowed(provider, path) {
20
20
  p = p.split("?")[0].split("#")[0]
21
21
  try {
22
22
  if (p.startsWith("http://") || p.startsWith("https://")) p = new URL(p).pathname
23
- } catch {}
23
+ } catch (err) { /* ignore subscriptions error */ }
24
24
  if (!p.startsWith("/")) p = "/" + p
25
25
  for (const allowed of list) {
26
26
  const a = allowed.split("?")[0]
@@ -140,17 +140,17 @@ export function createSubscriptionsService(deps) {
140
140
  try {
141
141
  const snap = quotaSnapshot(provider, res.headers, null, Date.now())
142
142
  if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
143
- } catch {}
143
+ } catch (err) { /* ignore subscriptions error */ }
144
144
  if (!res.ok && res.clone) {
145
145
  try {
146
146
  const txt = await res.clone().text()
147
147
  let j = null
148
- try { j = JSON.parse(txt) } catch {}
148
+ try { j = JSON.parse(txt) } catch (err) { /* ignore subscriptions error */ }
149
149
  if (j) {
150
150
  const snap2 = quotaSnapshot(provider, res.headers, j, Date.now())
151
151
  if (snap2 && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap2)
152
152
  }
153
- } catch {}
153
+ } catch (err) { /* ignore subscriptions error */ }
154
154
  }
155
155
  if (!res.ok) {
156
156
  const txt = await res.text().catch(() => "")
@@ -174,7 +174,7 @@ export function createSubscriptionsService(deps) {
174
174
  ms: Date.now() - t0,
175
175
  kind: "request",
176
176
  })
177
- } catch {}
177
+ } catch (err) { /* ignore subscriptions error */ }
178
178
  }
179
179
  return res
180
180
  } catch (err) {
package/lib/updater.js ADDED
@@ -0,0 +1,260 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { writeJson } from './http.js'
8
+
9
+ const UPDATE_HEADER = 'x-dsh-plugin-update'
10
+ const UPDATE_TIMEOUT_MS = 10 * 60_000
11
+ const VERSION_CACHE_MS = 5 * 60_000
12
+ let latestCache
13
+
14
+ function header(request, name) {
15
+ const value = request.headers?.[name]
16
+ return Array.isArray(value) ? value[0] : value
17
+ }
18
+
19
+ export function isLoopback(value) {
20
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
21
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
22
+ || address?.startsWith('127.') === true
23
+ || address?.startsWith('::ffff:127.') === true
24
+ }
25
+
26
+ export function isTrustedUpdateRequest(request) {
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
32
+ }
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
46
+ }
47
+
48
+ function validProfileName(value) {
49
+ return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
50
+ && !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value)
51
+ }
52
+
53
+ function profileNameFromArgv(argv) {
54
+ for (let index = 2; index < argv.length; index += 1) {
55
+ if (argv[index] === '--profile') return argv[index + 1]
56
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
57
+ }
58
+ return argv[2] === 'web' ? 'web' : undefined
59
+ }
60
+
61
+ function findDshCliEntry() {
62
+ const value = process.argv[1]
63
+ if (value === undefined || value === '') return undefined
64
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
65
+ if (!existsSync(entry)) return undefined
66
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
67
+ const manifestPath = resolve(directory, 'package.json')
68
+ if (existsSync(manifestPath)) {
69
+ try {
70
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
71
+ const bin = typeof manifest.bin === 'string'
72
+ ? manifest.bin
73
+ : typeof manifest.bin === 'object' && manifest.bin !== null
74
+ ? manifest.bin.dsh
75
+ : undefined
76
+ if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
77
+ && !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry
78
+ } catch {
79
+ // Continue searching parent package directories.
80
+ }
81
+ }
82
+ const parent = dirname(directory)
83
+ if (parent === directory) return undefined
84
+ }
85
+ }
86
+
87
+ function getRuntime() {
88
+ const profileDir = resolve(process.env.DSH_PROFILE_DIR
89
+ ?? resolve(homedir(), '.dsh', 'profiles', 'web'))
90
+ const selected = profileNameFromArgv(process.argv)
91
+ const profileName = validProfileName(selected)
92
+ ? selected
93
+ : validProfileName(basename(profileDir)) ? basename(profileDir) : 'web'
94
+ const cliEntry = findDshCliEntry()
95
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
96
+ }
97
+
98
+ export function parseSemver(value) {
99
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
100
+ if (match === null) return undefined
101
+ return {
102
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
103
+ prerelease: match[4]?.split('.') ?? [],
104
+ }
105
+ }
106
+
107
+ function comparePrerelease(left, right) {
108
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
109
+ const length = Math.max(left.length, right.length)
110
+ for (let index = 0; index < length; index += 1) {
111
+ const a = left[index]
112
+ const b = right[index]
113
+ if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
114
+ if (a === b) continue
115
+ const aNumeric = /^\d+$/.test(a)
116
+ const bNumeric = /^\d+$/.test(b)
117
+ if (aNumeric && bNumeric) {
118
+ const aNumber = BigInt(a)
119
+ const bNumber = BigInt(b)
120
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
121
+ continue
122
+ }
123
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
124
+ return a > b ? 1 : -1
125
+ }
126
+ return 0
127
+ }
128
+
129
+ export function isNewerVersion(currentValue, candidateValue) {
130
+ const current = parseSemver(currentValue)
131
+ const candidate = parseSemver(candidateValue)
132
+ if (current === undefined || candidate === undefined) return false
133
+ for (let index = 0; index < 3; index += 1) {
134
+ if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index]
135
+ }
136
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0
137
+ }
138
+
139
+ export async function fetchLatestVersion(packageName, registry = 'https://registry.npmjs.org') {
140
+ if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
141
+ return latestCache.version
142
+ }
143
+ try {
144
+ const response = await fetch(`${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`, {
145
+ signal: AbortSignal.timeout(8_000),
146
+ })
147
+ if (!response.ok) return undefined
148
+ const value = await response.json()
149
+ if (typeof value?.version !== 'string' || value.version === '') return undefined
150
+ latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS }
151
+ return value.version
152
+ } catch {
153
+ return undefined
154
+ }
155
+ }
156
+
157
+ export async function readCurrentVersion(manifestUrl) {
158
+ const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
159
+ if (typeof value?.version !== 'string' || value.version === '') throw new Error('Cannot read current plugin version.')
160
+ return value.version
161
+ }
162
+
163
+ export async function getUpdaterStatus(options, target = getRuntime()) {
164
+ const current = await readCurrentVersion(options.manifestUrl)
165
+ const latest = await fetchLatestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
166
+ return {
167
+ ok: true,
168
+ packageName: options.packageName,
169
+ currentVersion: current,
170
+ ...(latest === undefined ? {} : { latestVersion: latest }),
171
+ latestCheckFailed: latest === undefined,
172
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
173
+ profileName: target.profileName,
174
+ canAutoUpdate: target.cliEntry !== undefined,
175
+ }
176
+ }
177
+
178
+ export async function installExactPackage(target, packageSpec, options) {
179
+ if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
180
+ await new Promise((resolvePromise, reject) => {
181
+ const child = spawn(process.execPath, [
182
+ target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
183
+ '--config.minimumReleaseAge=0', packageSpec,
184
+ `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
185
+ ], {
186
+ cwd: target.profileDir,
187
+ windowsHide: true,
188
+ stdio: ['ignore', 'pipe', 'pipe'],
189
+ env: { ...process.env, NO_COLOR: '1' },
190
+ })
191
+ let detail = ''
192
+ child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
193
+ child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
194
+ const timer = setTimeout(() => {
195
+ child.kill()
196
+ reject(new Error('Update timed out; use the normal DSH update flow.'))
197
+ }, UPDATE_TIMEOUT_MS)
198
+ child.once('error', error => { clearTimeout(timer); reject(error) })
199
+ child.once('exit', code => {
200
+ clearTimeout(timer)
201
+ if (code === 0) resolvePromise()
202
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
203
+ })
204
+ })
205
+ }
206
+
207
+ export function registerPluginUpdater(ctx, options) {
208
+ let installing = false
209
+ return ctx.webServer.register({
210
+ kind: 'exact',
211
+ path: options.endpoint,
212
+ handler: async (request, response) => {
213
+ try {
214
+ const target = getRuntime()
215
+ if (request.method === 'GET' || request.method === 'HEAD') {
216
+ const payload = await getUpdaterStatus(options, target)
217
+ writeJson(response, 200, payload)
218
+ return
219
+ }
220
+ if (request.method !== 'POST') {
221
+ response.writeHead(405, { allow: 'GET, HEAD, POST' })
222
+ response.end()
223
+ return
224
+ }
225
+ if (!isTrustedUpdateRequest(request)) {
226
+ writeJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'Rejected non-local or cross-origin update request.' } })
227
+ return
228
+ }
229
+ if (installing) {
230
+ writeJson(response, 409, { ok: false, error: { code: 'busy', message: 'This plugin is already updating.' } })
231
+ return
232
+ }
233
+ installing = true
234
+ try {
235
+ const before = await getUpdaterStatus(options, target)
236
+ if (before.latestVersion === undefined) {
237
+ writeJson(response, 503, { ok: false, error: { code: 'unavailable', message: 'The latest version is temporarily unavailable.' } })
238
+ return
239
+ }
240
+ if (!before.updateAvailable) {
241
+ writeJson(response, 200, { ok: true, ...before })
242
+ return
243
+ }
244
+ await installExactPackage(target, `${options.packageName}@${before.latestVersion}`, options)
245
+ writeJson(response, 200, {
246
+ ok: true,
247
+ ...before,
248
+ updatedVersion: before.latestVersion,
249
+ restartRequired: true,
250
+ })
251
+ } finally {
252
+ installing = false
253
+ }
254
+ } catch (error) {
255
+ ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
256
+ writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
257
+ }
258
+ },
259
+ })
260
+ }
package/lib/usage.js CHANGED
@@ -52,22 +52,22 @@ export function asUsageSnapshot(percent) {
52
52
  // Named limit windows reported by vendors (five_hour, seven_day, primary...).
53
53
  // Label map is a data table: extend it when a vendor adds a window name.
54
54
  const WINDOW_LABELS = {
55
- five_hour: { zh: "5小时", ru: "5ч", en: "5h" },
56
- seven_day: { zh: "7天", ru: "7д", en: "7d" },
57
- seven_day_oauth_apps: { zh: "7天应用", ru: "7д прил.", en: "7d apps" },
58
- weekly_limit_7_days: { zh: "7天", ru: "7д", en: "7d" },
59
- primary: { zh: "主配额", ru: "осн.", en: "primary" },
60
- secondary: { zh: "次配额", ru: "втор.", en: "secondary" },
55
+ five_hour: { zh: "5小时", en: "5h" },
56
+ seven_day: { zh: "7天", en: "7d" },
57
+ seven_day_oauth_apps: { zh: "7天应用", en: "7d apps" },
58
+ weekly_limit_7_days: { zh: "7天", en: "7d" },
59
+ primary: { zh: "主配额", en: "primary" },
60
+ secondary: { zh: "次配额", en: "secondary" },
61
61
  // codex: the vendor API returns primary_window/secondary_window - surfaced as 5h/7d in the UI
62
- primary_window: { zh: "5小时", ru: "5ч", en: "5h" },
63
- secondary_window: { zh: "7天", ru: "7д", en: "7d" },
64
- monthly: { zh: "月度", ru: "месяц", en: "month" },
65
- credits: { zh: "点数", ru: "кредиты", en: "credits" },
62
+ primary_window: { zh: "5小时", en: "5h" },
63
+ secondary_window: { zh: "7天", en: "7d" },
64
+ monthly: { zh: "月度", en: "month" },
65
+ credits: { zh: "点数", en: "credits" },
66
66
  }
67
67
 
68
68
  export function windowLabel(id) {
69
69
  const known = WINDOW_LABELS[id]
70
- return known || { zh: id, ru: id, en: id }
70
+ return known || { zh: id, en: id }
71
71
  }
72
72
 
73
73
  // Walk vendor usage JSON; every node carrying a utilization-style number
@@ -83,19 +83,19 @@ export function usageWindows(obj) {
83
83
  if (pct != null && key && !seen.has(key)) {
84
84
  seen.add(key)
85
85
  const label = windowLabel(key)
86
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: pct })
86
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: pct })
87
87
  }
88
88
  const frac = numberOrNull(value.remainingFraction)
89
89
  if (frac != null && key && !seen.has(key)) {
90
90
  seen.add(key)
91
91
  const label = windowLabel(key)
92
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: (1 - frac) * 100 })
92
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: (1 - frac) * 100 })
93
93
  }
94
94
  const ufrac = numberOrNull(value.usedFraction)
95
95
  if (ufrac != null && key && !seen.has(key)) {
96
96
  seen.add(key)
97
97
  const label = windowLabel(key)
98
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: ufrac * 100 })
98
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: ufrac * 100 })
99
99
  }
100
100
  for (const [k, child] of Object.entries(value)) {
101
101
  if (child && typeof child === "object") walk(child, k, depth + 1)
package/lib/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Executes a function safely, capturing any error and logging it at debug level
3
+ * if a logger is provided, then returning the fallback value.
4
+ *
5
+ * Used across the plugin to eliminate empty catch blocks per dsh-plugin-preflight.
6
+ */
7
+ export function bestEffort(label, fn, fallback = undefined, logger = null) {
8
+ try {
9
+ const res = fn()
10
+ if (res && typeof res.then === 'function') {
11
+ return res.catch((err) => {
12
+ if (logger && typeof logger.debug === 'function') {
13
+ logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
14
+ }
15
+ return fallback
16
+ })
17
+ }
18
+ return res
19
+ } catch (err) {
20
+ if (logger && typeof logger.debug === 'function') {
21
+ logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
22
+ }
23
+ return fallback
24
+ }
25
+ }
@@ -1,3 +1,4 @@
1
+ import { fetchWithTimeout } from "./http.js"
1
2
  // Vendor factory from a declarative profile.
2
3
  //
3
4
  // Covers the OpenAI-Responses-compatible subscription family: PKCE auth,
@@ -87,9 +88,9 @@ export function createVendorFromProfile(profile) {
87
88
  const catalog = defaultModels(cfg.models || profile.models || [])
88
89
  if (!profile.modelsPath) return catalog
89
90
  try {
90
- const res = await (fetchImpl || fetch)(`${base}${profile.modelsPath}`, {
91
+ const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
91
92
  headers: identityHeaders(blob, { Accept: "application/json" }),
92
- })
93
+ }, { timeoutMs: 15000 })
93
94
  const json = await readJson(res)
94
95
  const rows = []
95
96
  for (const entry of json.data || json.models || []) {
@@ -220,7 +220,7 @@ export async function usage(blob, cfg, fetchImpl) {
220
220
  const decoded = decodeGrokCreditsFrame(Buffer.from(buf))
221
221
  if (decoded != null) pct = decoded
222
222
  }
223
- } catch {}
223
+ } catch (err) { /* ignore grok usage capture error */ }
224
224
  }
225
225
  const snap = asUsageSnapshot(pct)
226
226
  if (snap) snap.windows = usageWindows(json)
package/lib/wire.js CHANGED
@@ -199,12 +199,14 @@ export async function formTokenRequest(url, params, fetchImpl, headers, { timeou
199
199
  return readJson(res)
200
200
  }
201
201
 
202
- export async function jsonTokenRequest(url, body, fetchImpl, headers) {
202
+ export async function jsonTokenRequest(url, body, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
203
203
  const impl = fetchImpl || fetch
204
+ const timeoutSignal = AbortSignal.timeout(timeoutMs)
204
205
  const res = await impl(url, {
205
206
  method: 'POST',
206
207
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
207
208
  body: JSON.stringify(body),
209
+ signal: timeoutSignal,
208
210
  })
209
211
  return readJson(res)
210
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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",
@@ -34,13 +34,20 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
+ "scripts": {
38
+ "test": "eslint lib/ && node --test test/*.test.mjs",
39
+ "typecheck": "tsc -p tsconfig.checkjs.json"
40
+ },
37
41
  "dsh": {
38
42
  "bundle": {
39
43
  "patch": "./cordis.patch.yml"
40
44
  },
41
45
  "client": {
42
46
  "platform": "web",
43
- "inject": []
47
+ "inject": [
48
+ "@deepseek-ai/dsh-client-locale",
49
+ "@deepseek-ai/dsh-client-ui-slots"
50
+ ]
44
51
  }
45
52
  },
46
53
  "peerDependencies": {
@@ -62,9 +69,5 @@
62
69
  "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
63
70
  "@types/node": "^22.20.2",
64
71
  "typescript": "^7.0.2"
65
- },
66
- "scripts": {
67
- "test": "eslint lib/ && node --test test/*.test.mjs",
68
- "typecheck": "tsc -p tsconfig.checkjs.json"
69
72
  }
70
- }
73
+ }