@goodandready/dsh-subscriptions 0.6.86 → 0.6.87

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
@@ -311,6 +311,7 @@ dsh-subscriptions:
311
311
  - Grok `baseUrl` overrides must point to `api.x.ai`, `grok.com` / `*.grok.com`, or a loopback host; other hosts are rejected with `GROK_UNTRUSTED_HOST` so bearer tokens never leave trusted xAI infrastructure.
312
312
  - Antigravity CLI imports must configure `antigravityClientSecret`; the embedded default was removed.
313
313
  - `copilotCliPath` must point to a `copilot`, `github-copilot-cli` or `copilot-cli` binary; rename custom wrappers.
314
+ - Credential backup uses `/dsh-subscriptions/vault/export` and `/vault/import`; the legacy `/export`, `/import`, `/export-backup` and `/import-backup` routes were removed.
314
315
  - The error buffer and diagnostics reports redact bearer tokens, API keys, JWTs and `key=value` credentials before storing or returning them.
315
316
 
316
317
 
package/README.ru.md CHANGED
@@ -284,6 +284,7 @@ dsh-subscriptions:
284
284
  - Переопределение Grok `baseUrl` должно указывать на `api.x.ai`, `grok.com` / `*.grok.com` или loopback-хост; остальные хосты отклоняются с `GROK_UNTRUSTED_HOST`, чтобы bearer-токен не покидал доверенную инфраструктуру xAI.
285
285
  - Импорт Antigravity CLI требует явного `antigravityClientSecret`; встроенный default удалён.
286
286
  - `copilotCliPath` должен указывать на бинарь `copilot`, `github-copilot-cli` или `copilot-cli`; кастомные обёртки переименуйте.
287
+ - Бэкап credentials использует `/dsh-subscriptions/vault/export` и `/vault/import`; legacy-маршруты `/export`, `/import`, `/export-backup` и `/import-backup` удалены.
287
288
  - Error buffer и диагностические отчёты редактируют bearer-токены, API-ключи, JWT и `key=value`-credentials до сохранения и выдачи.
288
289
 
289
290
 
package/README.zh.md CHANGED
@@ -190,6 +190,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
190
190
  - Grok `baseUrl` 覆盖只能指向 `api.x.ai`、`grok.com` / `*.grok.com` 或回环主机;其他主机会以 `GROK_UNTRUSTED_HOST` 拒绝,避免 bearer 令牌离开可信的 xAI 基础设施。
191
191
  - Antigravity CLI 导入必须配置 `antigravityClientSecret`;内置默认值已移除。
192
192
  - `copilotCliPath` 必须指向 `copilot`、`github-copilot-cli` 或 `copilot-cli` 可执行文件;自定义包装器请重命名。
193
+ - 凭据备份使用 `/dsh-subscriptions/vault/export` 和 `/vault/import`;旧路由 `/export`、`/import`、`/export-backup`、`/import-backup` 已移除。
193
194
  - 错误缓冲区和诊断报告会在存储或返回前脱敏 bearer 令牌、API 密钥、JWT 和 `key=value` 凭据。
194
195
 
195
196
 
package/lib/client.js CHANGED
@@ -68,6 +68,16 @@ window.__ModuleLoader__.load({
68
68
  } catch { /* invalid url */ }
69
69
  return ''
70
70
  }
71
+
72
+ async function copyText(text) {
73
+ try {
74
+ if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return false
75
+ await navigator.clipboard.writeText(String(text == null ? '' : text))
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
80
+ }
71
81
 
72
82
  const en = {
73
83
  "syncModels": "Sync Models",
@@ -293,6 +303,9 @@ window.__ModuleLoader__.load({
293
303
  "howToGetTokenTitle": "How to get token / API key for this provider",
294
304
  "instructionsTitle": "How to obtain token",
295
305
  "close": "Close",
306
+ "cancel": "Cancel",
307
+ "quota": "Quota",
308
+ "subs5hWindow": "5h window",
296
309
  "reconnectRequired": "Reconnect required",
297
310
  "reconnectNow": "Re-authenticate now",
298
311
  "scopeWarning": "OAuth scope warning",
@@ -626,6 +639,9 @@ window.__ModuleLoader__.load({
626
639
  "howToGetTokenTitle": "如何获取此服务商的令牌或 API 密钥",
627
640
  "instructionsTitle": "获取令牌指引",
628
641
  "close": "关闭",
642
+ "cancel": "取消",
643
+ "quota": "配额",
644
+ "subs5hWindow": "5小时窗口",
629
645
  "reconnectRequired": "需要重新连接",
630
646
  "reconnectNow": "立即重新认证",
631
647
  "scopeWarning": "OAuth权限范围警告",
@@ -971,7 +987,7 @@ window.__ModuleLoader__.load({
971
987
  if (d && d.ok && d.report) setDiagReport(d.report)
972
988
  const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
973
989
  setDiag(txt)
974
- try { navigator.clipboard.writeText(txt) } catch { /* clipboard write disallowed */ }
990
+ copyText(txt)
975
991
  })
976
992
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
977
993
  }
@@ -1064,7 +1080,7 @@ window.__ModuleLoader__.load({
1064
1080
  const doExportBackup = async () => {
1065
1081
  setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
1066
1082
  try {
1067
- const res = await fetch('/dsh-subscriptions/export-backup', {
1083
+ const res = await fetch('/dsh-subscriptions/vault/export', {
1068
1084
  method: 'POST',
1069
1085
  headers: { 'Content-Type': 'application/json' },
1070
1086
  body: JSON.stringify({ passphrase: backupPass, algorithm: 'pbkdf2' }),
@@ -1082,7 +1098,7 @@ window.__ModuleLoader__.load({
1082
1098
  const doImportBackup = async () => {
1083
1099
  setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
1084
1100
  try {
1085
- const res = await fetch('/dsh-subscriptions/import-backup', {
1101
+ const res = await fetch('/dsh-subscriptions/vault/import', {
1086
1102
  method: 'POST',
1087
1103
  headers: { 'Content-Type': 'application/json' },
1088
1104
  body: JSON.stringify({ passphrase: backupPass, vault: backupData.trim() }),
@@ -1229,6 +1245,19 @@ window.__ModuleLoader__.load({
1229
1245
  const [err, setErr] = React.useState('')
1230
1246
  const [info, setInfo] = React.useState('')
1231
1247
  const [revision, setRevision] = React.useState(null)
1248
+ const timersRef = React.useRef(new Set())
1249
+ React.useEffect(() => () => {
1250
+ for (const id of timersRef.current) clearTimeout(id)
1251
+ timersRef.current.clear()
1252
+ }, [])
1253
+ const later = (fn, ms) => {
1254
+ const id = setTimeout(() => {
1255
+ timersRef.current.delete(id)
1256
+ fn()
1257
+ }, ms)
1258
+ timersRef.current.add(id)
1259
+ return id
1260
+ }
1232
1261
 
1233
1262
  const applyPayload = (data) => {
1234
1263
  if (data && data.revision != null) setRevision(data.revision)
@@ -1238,19 +1267,29 @@ window.__ModuleLoader__.load({
1238
1267
  setModelsCatalog((data && data.modelsCatalog) || [])
1239
1268
  }
1240
1269
 
1270
+ let reloadSeq = 0
1271
+ let reloadAbort = null
1241
1272
  const reload = () => {
1273
+ const seq = ++reloadSeq
1274
+ if (reloadAbort) reloadAbort.abort()
1275
+ const controller = new AbortController()
1276
+ reloadAbort = controller
1242
1277
  const t0 = Date.now()
1243
- return fetch('/dsh-subscriptions/config', { cache: 'no-store' })
1278
+ return fetch('/dsh-subscriptions/config', { cache: 'no-store', signal: controller.signal })
1244
1279
  .then((res) => res.json())
1245
1280
  .then((data) => {
1281
+ if (seq !== reloadSeq) return data
1246
1282
  setHostPing({ ok: true, latencyMs: Date.now() - t0 })
1247
1283
  applyPayload(data)
1248
1284
  reloadTelemetry()
1249
1285
  return data
1250
1286
  })
1251
1287
  .catch((err) => {
1252
- setHostPing({ ok: false, latencyMs: null })
1253
- throw err
1288
+ if (seq === reloadSeq && err && err.name !== 'AbortError') {
1289
+ setHostPing({ ok: false, latencyMs: null })
1290
+ throw err
1291
+ }
1292
+ return null
1254
1293
  })
1255
1294
  }
1256
1295
 
@@ -1258,7 +1297,10 @@ window.__ModuleLoader__.load({
1258
1297
  let alive = true
1259
1298
  reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
1260
1299
  checkUpdater().catch(() => {})
1261
- return () => { alive = false }
1300
+ return () => {
1301
+ alive = false
1302
+ if (reloadAbort) reloadAbort.abort()
1303
+ }
1262
1304
  }, [])
1263
1305
 
1264
1306
  // #90: poll device login while any slot is in 'pending' state.
@@ -1313,7 +1355,7 @@ window.__ModuleLoader__.load({
1313
1355
  throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1314
1356
  }
1315
1357
  applyPayload(data)
1316
- setSaved(true); setTimeout(() => setSaved(false), 2000)
1358
+ setSaved(true); later(() => setSaved(false), 2000)
1317
1359
  }
1318
1360
 
1319
1361
  const connect = async (provider, index) => {
@@ -1438,7 +1480,7 @@ window.__ModuleLoader__.load({
1438
1480
  if (!res.ok) throw new Error((data.error && data.error.message) || ("HTTP " + res.status))
1439
1481
  setAccounts(data.accounts || [])
1440
1482
  setInfo(t("toastQuarantined"))
1441
- setTimeout(() => setInfo(""), 3000)
1483
+ later(() => setInfo(""), 3000)
1442
1484
  } catch (e) { setErr(cleanErrorMessage(e.message || e)) }
1443
1485
  }
1444
1486
 
@@ -1457,7 +1499,7 @@ window.__ModuleLoader__.load({
1457
1499
  setAccounts(data.accounts || [])
1458
1500
  if (data.success || data.released) {
1459
1501
  setInfo(t("toastReleased"))
1460
- setTimeout(() => setInfo(""), 3000)
1502
+ later(() => setInfo(""), 3000)
1461
1503
  } else if (data.error) {
1462
1504
  setErr(cleanErrorMessage(data.error))
1463
1505
  }
@@ -1509,7 +1551,7 @@ window.__ModuleLoader__.load({
1509
1551
  if (data.config) applyPayload(data)
1510
1552
  else await reload()
1511
1553
  setInfo(t('importLocalSuccess'))
1512
- setTimeout(() => setInfo(''), 3000)
1554
+ later(() => setInfo(''), 3000)
1513
1555
  }
1514
1556
 
1515
1557
  const importToken = async (provider, index) => {
@@ -2160,7 +2202,7 @@ window.__ModuleLoader__.load({
2160
2202
  diag ? h('div', { className: 'dsub-row' },
2161
2203
  h('button', {
2162
2204
  type: 'button', className: 'dsub-mini',
2163
- onClick: () => { try { navigator.clipboard.writeText(diag) } catch { /* clipboard write error */ } },
2205
+ onClick: () => { copyText(diag) },
2164
2206
  }, t('diagCopy')),
2165
2207
  h('pre', { className: 'dsub-diag' }, diag),
2166
2208
  ) : null,
@@ -2255,7 +2297,7 @@ window.__ModuleLoader__.load({
2255
2297
  className: 'dsub-mini',
2256
2298
  onClick: () => {
2257
2299
  const txt = JSON.stringify(wizardModal.report, null, 2)
2258
- try { navigator.clipboard.writeText(txt) } catch { /* ignore */ }
2300
+ copyText(txt)
2259
2301
  },
2260
2302
  }, '📋 ' + t('wizardCopy')),
2261
2303
  ),
@@ -2327,7 +2369,7 @@ window.__ModuleLoader__.load({
2327
2369
  h('button', {
2328
2370
  type: 'button',
2329
2371
  className: 'dsub-mini',
2330
- onClick: () => { try { navigator.clipboard.writeText(backupData) } catch { /* ignore */ } },
2372
+ onClick: () => { copyText(backupData) },
2331
2373
  }, t('diagCopy')),
2332
2374
  h('button', {
2333
2375
  type: 'button',
@@ -2504,11 +2546,11 @@ window.__ModuleLoader__.load({
2504
2546
  type: 'button',
2505
2547
  className: 'dsub-mini',
2506
2548
  onClick: () => {
2507
- try {
2508
- navigator.clipboard.writeText(headlessInfo[key].sshTunnelCommand)
2549
+ copyText(headlessInfo[key].sshTunnelCommand).then((ok) => {
2550
+ if (!ok) return
2509
2551
  setCopiedSsh((m) => Object.assign({}, m, { [key]: true }))
2510
- setTimeout(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
2511
- } catch { /* ignore */ }
2552
+ later(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
2553
+ })
2512
2554
  }
2513
2555
  }, copiedSsh[key] ? ('✓ ' + t('headlessCopied')) : ('📋 ' + t('headlessCopySsh')))
2514
2556
  ),
@@ -2676,7 +2718,7 @@ window.__ModuleLoader__.load({
2676
2718
  h('div', { className: 'dsub-row' },
2677
2719
  h('button', {
2678
2720
  type: 'button', className: 'dsub-mini',
2679
- onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch { /* clipboard write error */ } },
2721
+ onClick: () => { copyText(d.userCode) },
2680
2722
  }, t('deviceCopy')),
2681
2723
  h('a', { href: safeExternalUrl(d.authUrl) || undefined, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
2682
2724
  ),
@@ -3684,7 +3726,7 @@ window.__ModuleLoader__.load({
3684
3726
  if (target) {
3685
3727
  const popup = window.open(target, '_blank', 'noopener')
3686
3728
  if (!popup) {
3687
- try { navigator.clipboard.writeText(target) } catch { /* clipboard blocked */ }
3729
+ copyText(target)
3688
3730
  }
3689
3731
  }
3690
3732
  }).catch(() => {})
package/lib/index.js CHANGED
@@ -11,7 +11,7 @@ import { webCallbackUri } from './oauth.js'
11
11
  import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/index.js'
12
12
  import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
13
13
  import { parseBlob } from './blob.js'
14
- import { createVendorFromProfile, validateProfile } from './vendor-factory.js'
14
+ import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
15
15
  import { registerCustomProviderIds, registerDisplayName } from './refs.js'
16
16
  import { createSubscriptionsService } from './subscriptions.js'
17
17
  import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
@@ -86,13 +86,7 @@ export function apply(ctx, rawConfig) {
86
86
  const live = () => cachedConfig
87
87
 
88
88
  function syncCustomVendors() {
89
- let profiles
90
- try {
91
- profiles = (live().customVendors || []).map(validateProfile)
92
- } catch (e) {
93
- if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors: ' + String(e && e.message || e))
94
- return
95
- }
89
+ const profiles = validateProfiles(live().customVendors)
96
90
  clearCustomVendors()
97
91
  for (const profile of profiles) {
98
92
  try {
@@ -5,11 +5,10 @@ import { normalizeSlots, vendorConfig } from '../accounts.js'
5
5
  import { getVendor } from '../vendors/index.js'
6
6
  import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
7
7
  import { Config, plainConfig, publicConfig, revisionOf } from '../config-schema.js'
8
- import { decryptWithPassphrase, encryptWithPassphrase } from '../crypto.js'
9
8
  import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
10
9
  import { discoverLocalCliSessions, loadLocalCliBlob } from '../import-auth.js'
11
10
  import { validateCredentialBlob } from '../blob.js'
12
- import { isProvider, oauthRef, parseOauthRef } from '../refs.js'
11
+ import { isProvider, oauthRef } from '../refs.js'
13
12
 
14
13
  export function registerAccountsRoutes(ctx, state) {
15
14
  const {
@@ -308,100 +307,6 @@ export function registerAccountsRoutes(ctx, state) {
308
307
  }), 'dsh-subscriptions: /import-local')
309
308
 
310
309
 
311
- // Export of the encrypted token bundle. Tokens are never logged.
312
- ctx.effect(() => ctx.webServer.register({
313
- kind: 'exact',
314
- path: '/dsh-subscriptions/export',
315
- handler: safeJsonHandler(async (req, res) => {
316
- if (req.method !== 'POST') {
317
- writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
318
- return
319
- }
320
- if (!isTrustedSettingsRequest(req, ctx)) {
321
- writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
322
- return
323
- }
324
- let payload
325
- try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
326
- writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
327
- return
328
- }
329
- const passphrase = String(payload.passphrase || '').trim()
330
- if (!passphrase || passphrase.length < 12) {
331
- writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase must be at least 12 characters' } })
332
- return
333
- }
334
- const algorithm = payload.algorithm === 'scrypt' ? 'scrypt' : (payload.algorithm === 'scrypt-v2' || payload.algorithm === 'scrypt-hardened' ? payload.algorithm : 'pbkdf2')
335
- try {
336
- const accounts = []
337
- for (const slot of normalizeSlots(live().slots)) {
338
- try {
339
- const blob = await store.loadBlob(slot.ref)
340
- accounts.push({ ref: slot.ref, provider: slot.provider, index: slot.index, label: slot.label || blob.label || '', blob })
341
- } catch { /* skip missing */ }
342
- }
343
- if (!accounts.length) {
344
- writeJson(res, 200, { ok: false, error: { code: 'empty', message: 'no connected accounts' } })
345
- return
346
- }
347
- const bundle = JSON.stringify({ v: 1, exportedAt: Date.now(), accounts })
348
- const encrypted = encryptWithPassphrase(bundle, passphrase, { algorithm })
349
- writeJson(res, 200, { ok: true, payload: encrypted, count: accounts.length })
350
- } catch (e) {
351
- writeJson(res, 500, { ok: false, error: { code: 'export', message: String(e && e.message || e) } })
352
- }
353
- }),
354
- }), 'dsh-subscriptions: /export')
355
-
356
-
357
- // Import of the encrypted bundle.
358
- ctx.effect(() => ctx.webServer.register({
359
- kind: 'exact',
360
- path: '/dsh-subscriptions/import',
361
- handler: safeJsonHandler(async (req, res) => {
362
- if (req.method !== 'POST') {
363
- writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
364
- return
365
- }
366
- if (!isTrustedSettingsRequest(req, ctx)) {
367
- writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
368
- return
369
- }
370
- let payload
371
- try { payload = JSON.parse((await readBody(req, 1024 * 1024)).toString('utf8') || '{}') } catch {
372
- writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
373
- return
374
- }
375
- const { passphrase, payload: encrypted } = payload
376
- if (!passphrase || !encrypted) {
377
- writeJson(res, 400, { ok: false, error: { code: 'params', message: 'passphrase and payload are required' } })
378
- return
379
- }
380
- let bundle
381
- try {
382
- bundle = JSON.parse(decryptWithPassphrase(encrypted, passphrase))
383
- } catch {
384
- writeJson(res, 400, { ok: false, error: { code: 'decrypt', message: 'wrong passphrase or corrupted bundle' } })
385
- return
386
- }
387
- if (!bundle || !Array.isArray(bundle.accounts)) {
388
- writeJson(res, 400, { ok: false, error: { code: 'format', message: 'invalid bundle format' } })
389
- return
390
- }
391
- let imported = 0
392
- for (const row of bundle.accounts) {
393
- try {
394
- if (!row || typeof row.ref !== 'string' || !parseOauthRef(row.ref) || !row.blob) continue
395
- await store.saveBlob(row.ref, validateCredentialBlob(row.blob))
396
- imported++
397
- } catch { /* skip broken */ }
398
- }
399
- await syncAdapter()
400
- writeJson(res, 200, { ok: true, imported, total: bundle.accounts.length, accounts: await accountsView() })
401
- }),
402
- }), 'dsh-subscriptions: /import')
403
-
404
-
405
310
  // #45: import an existing refresh token / API key without an OAuth flow.
406
311
  ctx.effect(() => ctx.webServer.register({
407
312
  kind: 'exact',
@@ -659,12 +564,6 @@ export function registerAccountsRoutes(ctx, state) {
659
564
  handler: handleVaultExport,
660
565
  }), 'dsh-subscriptions: /vault/export')
661
566
 
662
- ctx.effect(() => ctx.webServer.register({
663
- kind: 'exact',
664
- path: '/dsh-subscriptions/export-backup',
665
- handler: handleVaultExport,
666
- }), 'dsh-subscriptions: /export-backup')
667
-
668
567
  const handleVaultImport = safeJsonHandler(async (req, res) => {
669
568
  if (req.method !== 'POST') {
670
569
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
@@ -728,12 +627,6 @@ export function registerAccountsRoutes(ctx, state) {
728
627
  handler: handleVaultImport,
729
628
  }), 'dsh-subscriptions: /vault/import')
730
629
 
731
- ctx.effect(() => ctx.webServer.register({
732
- kind: 'exact',
733
- path: '/dsh-subscriptions/import-backup',
734
- handler: handleVaultImport,
735
- }), 'dsh-subscriptions: /import-backup')
736
-
737
630
 
738
631
  // #562: Remote Vault Sync Push
739
632
  ctx.effect(() => ctx.webServer.register({
@@ -40,7 +40,7 @@ export function registerProxyRoutes(ctx, state) {
40
40
  // The token never leaves the process - only the provider's response goes out.
41
41
  ctx.effect(() => ctx.webServer.register({
42
42
  kind: 'prefix',
43
- path: '/dsh-subscriptions/proxy',
43
+ path: '/dsh-subscriptions/proxy/',
44
44
  handler: safeJsonHandler(async (req, res) => {
45
45
  if (req.method !== 'POST' && req.method !== 'GET') {
46
46
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
package/lib/updater.js CHANGED
@@ -4,60 +4,15 @@ 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 { isTrustedSettingsRequest, writeJson } from './http.js'
7
+ import { isLoopback, isTrustedSettingsRequest, writeJson } from './http.js'
8
8
 
9
- const UPDATE_HEADER = 'x-dsh-plugin-update'
10
9
  const UPDATE_TIMEOUT_MS = 10 * 60_000
11
10
  const VERSION_CACHE_MS = 5 * 60_000
12
11
  let latestCache
13
12
 
14
- function header(request, name) {
15
- const value = request.headers?.[name]
16
- return Array.isArray(value) ? value[0] : value
17
- }
18
-
19
- function isLoopbackIPv4(address) {
20
- if (!address || !address.startsWith('127.')) return false
21
- const parts = address.split('.')
22
- if (parts.length !== 4) return false
23
- return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
24
- }
25
-
26
- export function isLoopback(value) {
27
- const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
28
- if (!address) return false
29
- if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
30
- const ipv4 = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address
31
- return isLoopbackIPv4(ipv4)
32
- }
13
+ export { isLoopback }
33
14
 
34
15
  export function isTrustedUpdateRequest(request, targetCtx) {
35
- const secFetchSite = String(header(request, 'sec-fetch-site') || '').toLowerCase()
36
- if (secFetchSite === 'cross-site') return false
37
-
38
- const origin = header(request, 'origin')
39
- const host = header(request, 'host')
40
- if (origin && host) {
41
- try {
42
- const u = new URL(origin)
43
- if (u.protocol === 'http:' || u.protocol === 'https:') {
44
- if (u.host.toLowerCase() !== host.toLowerCase()) return false
45
- } else if (u.protocol === 'dsh-app:') {
46
- const appHost = u.host.toLowerCase()
47
- if (appHost && appHost !== 'app' && appHost !== 'localhost') return false
48
- } else {
49
- return false
50
- }
51
- } catch {
52
- return false
53
- }
54
- }
55
-
56
- const customHeader = header(request, UPDATE_HEADER)
57
- if (customHeader === '1') {
58
- const remote = request.socket?.remoteAddress
59
- if (remote && isLoopback(remote)) return true
60
- }
61
16
  return isTrustedSettingsRequest(request, targetCtx)
62
17
  }
63
18
 
@@ -12,6 +12,7 @@ import { buildAuthorizeUrl } from "./oauth.js"
12
12
  import { codexResponsesBody, modelCatalog } from "./messages.js"
13
13
  import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError , readErrorBody } from "./wire.js"
14
14
  import { emailFromToken } from "./jwt.js"
15
+ import { BUILTIN_PROVIDERS } from "./refs.js"
15
16
 
16
17
  function defaultModels(providerId, models) {
17
18
  return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
@@ -108,12 +109,6 @@ export function createVendorFromProfile(profile) {
108
109
  return catalog
109
110
  }
110
111
  },
111
- async usage() {
112
- // Arbitrary services have no universal usage endpoint - the quota
113
- // snapshot is filled from ratelimit headers via the adapter quotaFetch.
114
- return null
115
- },
116
- check: undefined,
117
112
  async* streamOnce({ blob, options, fetchImpl, headers, signal }) {
118
113
  const body = codexResponsesBody(options, "")
119
114
  if (!body.instructions) delete body.instructions
@@ -141,6 +136,7 @@ export function validateProfile(raw) {
141
136
  if (!p[key] || typeof p[key] !== "string") throw new Error(`customVendors[${p.id || "?"}]: field ${key} is required`)
142
137
  }
143
138
  if (!/^[a-z][a-z0-9_]*$/.test(p.id)) throw new Error(`customVendors: id "${p.id}" must match [a-z][a-z0-9_]*`)
139
+ if (BUILTIN_PROVIDERS.includes(p.id)) throw new Error(`customVendors: id "${p.id}" conflicts with a built-in provider`)
144
140
  if (p.tokenStyle && !["form", "json"].includes(p.tokenStyle)) throw new Error(`customVendors[${p.id}]: tokenStyle must be form|json`)
145
141
  const localHostnames = new Set(["localhost", "127.0.0.1", "::1", "[::1]"])
146
142
  for (const key of ["authUrl", "tokenUrl", "baseUrl"]) {
@@ -170,4 +166,15 @@ export function validateProfile(raw) {
170
166
  models: Array.isArray(p.models) ? p.models : [],
171
167
  headers: p.headers && typeof p.headers === "object" ? p.headers : {},
172
168
  }
169
+ }
170
+
171
+ /** Validates a list of profiles, skipping invalid entries individually. */
172
+ export function validateProfiles(list) {
173
+ const out = []
174
+ for (const raw of Array.isArray(list) ? list : []) {
175
+ try {
176
+ out.push(validateProfile(raw))
177
+ } catch { /* skip invalid profile */ }
178
+ }
179
+ return out
173
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.86",
3
+ "version": "0.6.87",
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",