@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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +62 -20
- package/lib/index.js +2 -8
- package/lib/routes/accounts.js +1 -108
- package/lib/routes/proxy.js +1 -1
- package/lib/updater.js +2 -47
- package/lib/vendor-factory.js +13 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1253
|
-
|
|
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 () => {
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () => {
|
|
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
|
-
|
|
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: () => {
|
|
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
|
-
|
|
2508
|
-
|
|
2549
|
+
copyText(headlessInfo[key].sshTunnelCommand).then((ok) => {
|
|
2550
|
+
if (!ok) return
|
|
2509
2551
|
setCopiedSsh((m) => Object.assign({}, m, { [key]: true }))
|
|
2510
|
-
|
|
2511
|
-
}
|
|
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: () => {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 {
|
package/lib/routes/accounts.js
CHANGED
|
@@ -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
|
|
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({
|
package/lib/routes/proxy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/vendor-factory.js
CHANGED
|
@@ -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.
|
|
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",
|