@goodandready/dsh-subscriptions 0.6.28 → 0.6.30
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/adapter-manager.js +2 -0
- package/lib/adapter.js +33 -2
- package/lib/alerts.js +10 -1
- package/lib/client.js +39 -8
- package/lib/crypto.js +35 -8
- package/lib/history.js +10 -4
- package/lib/import-auth.js +2 -21
- package/lib/index.js +3 -2
- package/lib/routes.js +4 -0
- package/lib/stream-rotate.js +16 -0
- package/lib/updater.js +23 -24
- package/lib/vault.js +3 -3
- package/package.json +2 -2
- package/lib/importers/coding-agents.js +0 -37
package/lib/adapter-manager.js
CHANGED
|
@@ -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")
|
|
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)
|
|
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
|
|
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
|
|
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}' +
|
|
@@ -299,12 +299,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
299
299
|
grok: {
|
|
300
300
|
title: "xAI Grok",
|
|
301
301
|
stepsEn: [
|
|
302
|
-
"1. Run grok
|
|
302
|
+
"1. Run grok CLI login on the server.",
|
|
303
303
|
"2. Click \"📥 From CLI\" (~/.grok/auth.json).",
|
|
304
304
|
"3. Or paste your xAI API Key and click \"Save token\".",
|
|
305
305
|
],
|
|
306
306
|
stepsZh: [
|
|
307
|
-
"1. 在服务器上运行 grok
|
|
307
|
+
"1. 在服务器上运行 grok CLI 登录。",
|
|
308
308
|
"2. 点击 \"📥 从 CLI 导入\" (~/.grok/auth.json)。",
|
|
309
309
|
"3. 或将 xAI API 密钥粘贴到输入框并点击 \"保存令牌\"。",
|
|
310
310
|
],
|
|
@@ -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",
|
|
@@ -528,8 +529,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
528
529
|
"backupImportTitle": "Import Encrypted Backup",
|
|
529
530
|
"backupExportAction": "Export",
|
|
530
531
|
"backupImportAction": "Import",
|
|
531
|
-
"backupPassphrase": "Vault Passphrase (min
|
|
532
|
-
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM
|
|
532
|
+
"backupPassphrase": "Vault Passphrase (min 12 chars)",
|
|
533
|
+
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM. ⚠️ Contains sensitive OAuth refresh tokens; use a strong password (min 12 chars).",
|
|
534
|
+
"vaultTokenWarning": "⚠️ Contains sensitive OAuth refresh tokens",
|
|
533
535
|
"backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
|
|
534
536
|
"backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
|
|
535
537
|
"backupExportOk": "Backup exported successfully",
|
|
@@ -674,6 +676,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
674
676
|
"save": "保存",
|
|
675
677
|
"saved": "已保存",
|
|
676
678
|
"loading": "加载中…",
|
|
679
|
+
"unavailable": "设置不可用",
|
|
677
680
|
"accountLabel": "账户",
|
|
678
681
|
"plan": "套餐方案",
|
|
679
682
|
"storedAs": "存储标识",
|
|
@@ -725,8 +728,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
725
728
|
"backupImportTitle": "导入加密备份",
|
|
726
729
|
"backupExportAction": "导出",
|
|
727
730
|
"backupImportAction": "导入",
|
|
728
|
-
"backupPassphrase": "凭据库密码(至少
|
|
729
|
-
"backupExportHint": "使用 AES-256-GCM
|
|
731
|
+
"backupPassphrase": "凭据库密码(至少 12 位)",
|
|
732
|
+
"backupExportHint": "使用 AES-256-GCM 安全加密导出所有订阅账户与配置。⚠️ 包含敏感的 OAuth 刷新令牌,请使用至少 12 位的强密码保护。",
|
|
733
|
+
"vaultTokenWarning": "⚠️ 包含敏感的 OAuth 刷新令牌",
|
|
730
734
|
"backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
|
|
731
735
|
"backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
|
|
732
736
|
"backupExportOk": "备份导出成功",
|
|
@@ -1674,7 +1678,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1674
1678
|
backupModal === 'export' ? React.createElement('button', {
|
|
1675
1679
|
type: 'button',
|
|
1676
1680
|
className: 'dsub-mini',
|
|
1677
|
-
disabled: backupLoading || !backupPass || backupPass.length <
|
|
1681
|
+
disabled: backupLoading || !backupPass || backupPass.length < 12,
|
|
1678
1682
|
onClick: doExportBackup,
|
|
1679
1683
|
}, backupLoading ? '…' : t('backupExportAction')) : React.createElement('button', {
|
|
1680
1684
|
type: 'button',
|
|
@@ -2074,6 +2078,33 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2074
2078
|
if (props && props.view === 'summary') {
|
|
2075
2079
|
return React.createElement('div', { className: 'dsub-description' }, t('cardIntro'))
|
|
2076
2080
|
}
|
|
2081
|
+
// Check host snapshot status (#408): loading or unavailable
|
|
2082
|
+
if (props && props.status === 'loading') {
|
|
2083
|
+
return React.createElement(page ? 'div' : 'li', {
|
|
2084
|
+
className: page ? 'dsub-page' : 'dsub-card',
|
|
2085
|
+
},
|
|
2086
|
+
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2087
|
+
React.createElement('div', { className: 'dsub-headText' },
|
|
2088
|
+
React.createElement('div', { className: 'dsub-name' }, t('title')),
|
|
2089
|
+
React.createElement('div', { className: 'dsub-description' }, t('loading') || t('settingsLoading') || 'Loading…'),
|
|
2090
|
+
),
|
|
2091
|
+
)
|
|
2092
|
+
)
|
|
2093
|
+
}
|
|
2094
|
+
if (props && props.status === 'unavailable') {
|
|
2095
|
+
return React.createElement(page ? 'div' : 'li', {
|
|
2096
|
+
className: page ? 'dsub-page' : 'dsub-card',
|
|
2097
|
+
},
|
|
2098
|
+
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2099
|
+
React.createElement('div', { className: 'dsub-headText' },
|
|
2100
|
+
React.createElement('div', { className: 'dsub-name' }, t('title')),
|
|
2101
|
+
React.createElement('div', { className: 'dsub-description', style: { color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
2102
|
+
t('unavailable') || t('settingsUnavailable') || 'Settings are unavailable',
|
|
2103
|
+
),
|
|
2104
|
+
),
|
|
2105
|
+
)
|
|
2106
|
+
)
|
|
2107
|
+
}
|
|
2077
2108
|
return React.createElement(page ? 'div' : 'li', {
|
|
2078
2109
|
className: page ? 'dsub-page' : 'dsub-card' + (open ? ' dsub-cardOpen' : ''),
|
|
2079
2110
|
},
|
package/lib/crypto.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { scryptSync, pbkdf2Sync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
2
2
|
|
|
3
|
-
// AES-256-GCM with PBKDF2 (
|
|
4
|
-
//
|
|
3
|
+
// AES-256-GCM with PBKDF2 (600,000 iterations, SHA-256) (DSHE2:),
|
|
4
|
+
// hardened scrypt (N=131072) (DSHE3:), or legacy scrypt (DSHE1:).
|
|
5
|
+
// Backward-compatible across all key derivation algorithms.
|
|
5
6
|
export function encryptWithPassphrase(plainText, passphrase, { algorithm = "scrypt" } = {}) {
|
|
6
7
|
const salt = randomBytes(16)
|
|
7
8
|
const isPbkdf2 = algorithm === "pbkdf2"
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const isHardenedScrypt = algorithm === "scrypt-v2" || algorithm === "scrypt-hardened"
|
|
10
|
+
let key
|
|
11
|
+
let prefix
|
|
12
|
+
|
|
13
|
+
if (isPbkdf2) {
|
|
14
|
+
key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
|
|
15
|
+
prefix = "DSHE2:"
|
|
16
|
+
} else if (isHardenedScrypt) {
|
|
17
|
+
key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
|
|
18
|
+
prefix = "DSHE3:"
|
|
19
|
+
} else {
|
|
20
|
+
key = scryptSync(String(passphrase), salt, 32)
|
|
21
|
+
prefix = "DSHE1:"
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
const iv = randomBytes(12)
|
|
12
25
|
const cipher = createCipheriv("aes-256-gcm", key, iv)
|
|
13
26
|
const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
|
|
14
27
|
const tag = cipher.getAuthTag()
|
|
15
28
|
// format: magic + salt + iv + tag + data
|
|
16
|
-
const prefix = isPbkdf2 ? "DSHE2:" : "DSHE1:"
|
|
17
29
|
return prefix + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
|
|
18
30
|
}
|
|
19
31
|
|
|
@@ -24,13 +36,28 @@ export function decryptWithPassphrase(payload, passphrase) {
|
|
|
24
36
|
let iv
|
|
25
37
|
let tag
|
|
26
38
|
let data
|
|
27
|
-
if (raw.startsWith("
|
|
39
|
+
if (raw.startsWith("DSHE3:")) {
|
|
40
|
+
const buf = Buffer.from(raw.slice(6), "base64")
|
|
41
|
+
salt = buf.subarray(0, 16)
|
|
42
|
+
iv = buf.subarray(16, 28)
|
|
43
|
+
tag = buf.subarray(28, 44)
|
|
44
|
+
data = buf.subarray(44)
|
|
45
|
+
key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
|
|
46
|
+
} else if (raw.startsWith("DSHE2:")) {
|
|
28
47
|
const buf = Buffer.from(raw.slice(6), "base64")
|
|
29
48
|
salt = buf.subarray(0, 16)
|
|
30
49
|
iv = buf.subarray(16, 28)
|
|
31
50
|
tag = buf.subarray(28, 44)
|
|
32
51
|
data = buf.subarray(44)
|
|
33
|
-
|
|
52
|
+
// Try 600,000 iterations first (#414), fallback to legacy 100,000 iterations
|
|
53
|
+
try {
|
|
54
|
+
key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
|
|
55
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv)
|
|
56
|
+
decipher.setAuthTag(tag)
|
|
57
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8")
|
|
58
|
+
} catch {
|
|
59
|
+
key = pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
|
|
60
|
+
}
|
|
34
61
|
} else if (raw.startsWith("DSHE1:")) {
|
|
35
62
|
const buf = Buffer.from(raw.slice(6), "base64")
|
|
36
63
|
salt = buf.subarray(0, 16)
|
package/lib/history.js
CHANGED
|
@@ -6,15 +6,21 @@ import { homedir } from 'node:os'
|
|
|
6
6
|
// ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
|
|
7
7
|
// All IO is best-effort: a write failure must never crash the harness.
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function resolveHistoryDir(customDir) {
|
|
10
|
+
if (customDir) return customDir
|
|
11
|
+
if (process.env.DSH_HISTORY_DIR) return process.env.DSH_HISTORY_DIR
|
|
12
|
+
if (process.env.DSH_STORAGE_DIR) return process.env.DSH_STORAGE_DIR
|
|
13
|
+
if (process.env.DSH_HOME) return join(process.env.DSH_HOME, 'storages', 'dsh-subscriptions')
|
|
14
|
+
return join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
export class HistoryStore {
|
|
12
|
-
constructor(dir =
|
|
13
|
-
this.dir = dir
|
|
18
|
+
constructor(dir = resolveHistoryDir(), ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0, maxEntries = 1000) {
|
|
19
|
+
this.dir = resolveHistoryDir(dir)
|
|
14
20
|
this.ttlMs = ttlMs
|
|
15
21
|
this.debounceMs = debounceMs
|
|
16
22
|
this.maxEntries = maxEntries
|
|
17
|
-
this.path = join(dir, 'history.json')
|
|
23
|
+
this.path = join(this.dir, 'history.json')
|
|
18
24
|
this.rows = []
|
|
19
25
|
this._persistTimer = null
|
|
20
26
|
this._load()
|
package/lib/import-auth.js
CHANGED
|
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
5
|
-
import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
|
|
6
5
|
|
|
7
6
|
// Test seam: a fixture root overrides the real home for the duration of a
|
|
8
7
|
// public call. Undefined (the default) keeps production behaviour.
|
|
@@ -46,10 +45,9 @@ async function discoverLocalCliSessionsInner() {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
// 2. Grok CLI
|
|
48
|
+
// 2. Grok CLI
|
|
50
49
|
const grokPaths = [
|
|
51
50
|
homeFile('.grok', 'auth.json'),
|
|
52
|
-
homeFile('.hermes', 'auth.json'),
|
|
53
51
|
]
|
|
54
52
|
for (const p of grokPaths) {
|
|
55
53
|
const grok = await readJson(p)
|
|
@@ -198,24 +196,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
198
196
|
}
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
const agentConfigs = await discoverCodingAgentConfigs()
|
|
203
|
-
if (agentConfigs.aider) {
|
|
204
|
-
detected.aider = {
|
|
205
|
-
provider: 'aider',
|
|
206
|
-
path: agentConfigs.aider.path,
|
|
207
|
-
email: 'Aider Configured Agent',
|
|
208
|
-
hasRefreshToken: false,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (agentConfigs.roocode) {
|
|
212
|
-
detected.roocode = {
|
|
213
|
-
provider: 'roocode',
|
|
214
|
-
path: agentConfigs.roocode.path,
|
|
215
|
-
email: 'Roo-Code Configured Agent',
|
|
216
|
-
hasRefreshToken: false,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
199
|
+
|
|
219
200
|
|
|
220
201
|
return detected
|
|
221
202
|
}
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
|
|
|
16
16
|
import { createResetCreditService } from './reset-credits.js'
|
|
17
17
|
import { maskEmail, maskLabel } from './mask.js'
|
|
18
18
|
import { proxyFetch } from './proxy.js'
|
|
19
|
-
import { HistoryStore,
|
|
19
|
+
import { HistoryStore, resolveHistoryDir } from './history.js'
|
|
20
20
|
import { createDiagnosticsReport } from './diagnostics.js'
|
|
21
21
|
import { createAccountsView } from './accounts-view.js'
|
|
22
22
|
import { createAdapterManager } from './adapter-manager.js'
|
|
@@ -185,7 +185,8 @@ export function apply(ctx, rawConfig) {
|
|
|
185
185
|
bestEffort('emit limit-notice', () => ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }), undefined, logger)
|
|
186
186
|
},
|
|
187
187
|
})
|
|
188
|
-
const
|
|
188
|
+
const historyDir = ctx?.historyDir || ctx?.storageDir || resolveHistoryDir()
|
|
189
|
+
const history = new HistoryStore(historyDir, 7 * 24 * 60 * 60 * 1000, 1000)
|
|
189
190
|
if (typeof ctx?.on === 'function') {
|
|
190
191
|
ctx.on('dispose', () => history.dispose())
|
|
191
192
|
}
|
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')
|
package/lib/stream-rotate.js
CHANGED
|
@@ -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
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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/lib/vault.js
CHANGED
|
@@ -11,8 +11,8 @@ export const VAULT_FORMAT = 'dsh-subscriptions-vault-v1'
|
|
|
11
11
|
*/
|
|
12
12
|
export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscriptions', algorithm = 'scrypt' }, passphrase) {
|
|
13
13
|
const pass = String(passphrase || '').trim()
|
|
14
|
-
if (!pass || pass.length <
|
|
15
|
-
throw new Error('passphrase must be at least
|
|
14
|
+
if (!pass || pass.length < 12) {
|
|
15
|
+
throw new Error('passphrase must be at least 12 characters')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const payload = {
|
|
@@ -43,7 +43,7 @@ export function importVault(encryptedPayload, passphrase) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const raw = String(encryptedPayload || '').trim()
|
|
46
|
-
if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:')) {
|
|
46
|
+
if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:') && !raw.startsWith('DSHE3:')) {
|
|
47
47
|
throw new Error('invalid vault format: missing DSHE1 header')
|
|
48
48
|
}
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.30",
|
|
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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "eslint lib/ test/ && node --test test/*.test.mjs",
|
|
40
|
+
"test": "eslint lib/ test/ && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
41
41
|
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
42
42
|
},
|
|
43
43
|
"dsh": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
export async function discoverCodingAgentConfigs() {
|
|
6
|
-
const configs = {}
|
|
7
|
-
|
|
8
|
-
// 1. Aider (.aider.conf.yml)
|
|
9
|
-
try {
|
|
10
|
-
const p = join(homedir(), '.aider.conf.yml')
|
|
11
|
-
const text = await readFile(p, 'utf8')
|
|
12
|
-
configs.aider = {
|
|
13
|
-
name: 'Aider Config',
|
|
14
|
-
path: p,
|
|
15
|
-
found: true,
|
|
16
|
-
raw: text
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
// not present
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 2. Roo-Code / Roo-Cline settings
|
|
23
|
-
try {
|
|
24
|
-
const p = join(homedir(), '.roo-code', 'settings.json')
|
|
25
|
-
const text = await readFile(p, 'utf8')
|
|
26
|
-
configs.roocode = {
|
|
27
|
-
name: 'Roo-Code Config',
|
|
28
|
-
path: p,
|
|
29
|
-
found: true,
|
|
30
|
-
raw: text
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// not present
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return configs
|
|
37
|
-
}
|