@goodandready/dsh-subscriptions 0.7.5 → 0.7.7
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/bin/dsh-subscriptions.mjs +98 -2
- package/lib/alerts.js +19 -4
- package/lib/client.js +170 -9
- package/lib/config-schema.js +6 -0
- package/lib/index.js +23 -2
- package/lib/key-rotation-bridge.js +36 -5
- package/lib/notification-scheduler.js +7 -0
- package/lib/routes/packs.js +41 -0
- package/lib/routes/webhook.js +47 -0
- package/lib/routes.js +4 -0
- package/lib/webhook-url.js +27 -0
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
4
5
|
|
|
5
6
|
export function parseArgs(argv) {
|
|
6
7
|
const args = Array.isArray(argv) ? argv.slice() : []
|
|
7
8
|
const command = args.shift() || 'status'
|
|
8
|
-
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '' }
|
|
9
|
+
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '' }
|
|
9
10
|
for (let i = 0; i < args.length; i += 1) {
|
|
10
11
|
const arg = args[i]
|
|
11
12
|
if (arg === '--csv') options.csv = true
|
|
@@ -15,6 +16,8 @@ export function parseArgs(argv) {
|
|
|
15
16
|
else if (arg === '--session') options.sessionId = args[++i] || ''
|
|
16
17
|
else if (command === 'set' && !options.setKey) options.setKey = arg
|
|
17
18
|
else if (command === 'set') options.setValue = arg
|
|
19
|
+
else if (command === 'packs' && arg === 'add') options.packAction = 'add'
|
|
20
|
+
else if (command === 'packs' && options.packAction === 'add' && !options.packFile) options.packFile = arg
|
|
18
21
|
else if (command === 'fast' && !options.sessionId) options.sessionId = arg
|
|
19
22
|
else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
|
|
20
23
|
}
|
|
@@ -67,9 +70,60 @@ export function buildRequest(options, env = process.env) {
|
|
|
67
70
|
}
|
|
68
71
|
if (options.command === 'config') return { url: base + '/config', init: { method: 'GET', headers } }
|
|
69
72
|
if (options.command === 'sources') return { url: base + '/sources', init: { method: 'GET', headers } }
|
|
73
|
+
if (options.command === 'packs') return { url: base + '/config', init: { method: 'GET', headers } }
|
|
74
|
+
if (options.command === 'diagnostics') return { url: base + '/diagnostics', init: { method: 'GET', headers } }
|
|
70
75
|
throw new Error('unknown command: ' + options.command)
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
export function doctorFailed(payload) {
|
|
79
|
+
const diag = (payload && payload.diagnostics) || {}
|
|
80
|
+
const report = diag.report || {}
|
|
81
|
+
const errors = Array.isArray(report.recentErrors) ? report.recentErrors.length : 0
|
|
82
|
+
const conflicts = Array.isArray(diag.conflicts) ? diag.conflicts.length : 0
|
|
83
|
+
return errors > 0 || conflicts > 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatDoctor(payload) {
|
|
87
|
+
const diag = (payload && payload.diagnostics) || {}
|
|
88
|
+
const report = diag.report || {}
|
|
89
|
+
const sources = (payload && payload.sources) || {}
|
|
90
|
+
const errors = Array.isArray(report.recentErrors) ? report.recentErrors.length : 0
|
|
91
|
+
const conflicts = Array.isArray(diag.conflicts) ? diag.conflicts.length : 0
|
|
92
|
+
const kr = sources.keyRotation || {}
|
|
93
|
+
const lines = []
|
|
94
|
+
lines.push('doctor: ' + (doctorFailed(payload) ? 'degraded' : 'ok'))
|
|
95
|
+
lines.push('errors: ' + errors + ' conflicts: ' + conflicts)
|
|
96
|
+
lines.push('key-rotation: ' + (kr.available ? (kr.healthyKeys + '/' + kr.totalKeys + ' healthy keys') : ('unavailable' + (kr.reason ? ' (' + kr.reason + ')' : ''))))
|
|
97
|
+
for (const row of (sources.providers || [])) lines.push(row.provider + ': ' + (row.selected || 'none'))
|
|
98
|
+
return lines.join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function sanitizePack(pack) {
|
|
102
|
+
if (!pack || typeof pack !== 'object') return pack
|
|
103
|
+
const copy = JSON.parse(JSON.stringify(pack))
|
|
104
|
+
if (copy.clientSecret) copy.clientSecret = '••••••'
|
|
105
|
+
if (copy.headers && typeof copy.headers === 'object') {
|
|
106
|
+
copy.headers = Object.fromEntries(Object.keys(copy.headers).map((key) => [key, '••••••']))
|
|
107
|
+
}
|
|
108
|
+
return copy
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatPacks(packs) {
|
|
112
|
+
return JSON.stringify((Array.isArray(packs) ? packs : []).map(sanitizePack), null, 2) + '\\n'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function mergeProviderPack(packs, pack) {
|
|
116
|
+
const list = (Array.isArray(packs) ? packs : []).filter((p) => p && p.id !== pack.id)
|
|
117
|
+
return [...list, pack]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildValidatePackRequest(pack, env = process.env) {
|
|
121
|
+
const base = String(env.DSH_SUBS_URL || 'http://127.0.0.1:3080/dsh-subscriptions').replace(/\/$/, '')
|
|
122
|
+
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' }
|
|
123
|
+
if (env.DSH_AUTH_TOKEN) headers.Authorization = 'Bearer ' + env.DSH_AUTH_TOKEN
|
|
124
|
+
return { url: base + '/provider-packs/validate', init: { method: 'POST', headers, body: JSON.stringify({ pack }) } }
|
|
125
|
+
}
|
|
126
|
+
|
|
73
127
|
export function formatSources(payload) {
|
|
74
128
|
const lines = []
|
|
75
129
|
const kr = payload && payload.keyRotation
|
|
@@ -98,6 +152,41 @@ export function buildSetRequest(options, configPayload, env = process.env) {
|
|
|
98
152
|
export async function run(argv = process.argv.slice(2), env = process.env, fetchImpl = fetch) {
|
|
99
153
|
const options = parseArgs(argv)
|
|
100
154
|
if (options.help) return { help: true }
|
|
155
|
+
if (options.command === 'doctor') {
|
|
156
|
+
const diagReq = buildRequest({ command: 'diagnostics' }, env)
|
|
157
|
+
const srcReq = buildRequest({ command: 'sources' }, env)
|
|
158
|
+
const [diagRes, srcRes] = await Promise.all([fetchImpl(diagReq.url, diagReq.init), fetchImpl(srcReq.url, srcReq.init)])
|
|
159
|
+
const diagText = await diagRes.text()
|
|
160
|
+
const srcText = await srcRes.text()
|
|
161
|
+
if (!diagRes.ok) throw new Error('HTTP ' + diagRes.status + ': ' + diagText.slice(0, 500))
|
|
162
|
+
if (!srcRes.ok) throw new Error('HTTP ' + srcRes.status + ': ' + srcText.slice(0, 500))
|
|
163
|
+
const payload = { diagnostics: JSON.parse(diagText || '{}'), sources: JSON.parse(srcText || '{}') }
|
|
164
|
+
return {
|
|
165
|
+
text: options.json ? JSON.stringify(payload, null, 2) + '\n' : formatDoctor(payload),
|
|
166
|
+
contentType: options.json ? 'application/json' : 'text/plain',
|
|
167
|
+
code: doctorFailed(payload) ? 1 : 0,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (options.command === 'packs' && options.packAction === 'add') {
|
|
171
|
+
if (!options.packFile) throw new Error('packs add requires a JSON file')
|
|
172
|
+
const pack = JSON.parse(await readFile(options.packFile, 'utf8'))
|
|
173
|
+
const validate = buildValidatePackRequest(pack.pack || pack, env)
|
|
174
|
+
const checked = await fetchImpl(validate.url, validate.init)
|
|
175
|
+
const checkedText = await checked.text()
|
|
176
|
+
if (!checked.ok) throw new Error('invalid pack: ' + checkedText.slice(0, 500))
|
|
177
|
+
const current = await fetchImpl(buildRequest({ command: 'config' }, env).url, buildRequest({ command: 'config' }, env).init)
|
|
178
|
+
const currentText = await current.text()
|
|
179
|
+
if (!current.ok) throw new Error('HTTP ' + current.status + ': ' + currentText.slice(0, 500))
|
|
180
|
+
const payload = JSON.parse(currentText || '{}')
|
|
181
|
+
const nextConfig = Object.assign({}, payload.config, {
|
|
182
|
+
providerPacks: mergeProviderPack(payload.config && payload.config.providerPacks, pack.pack || pack),
|
|
183
|
+
})
|
|
184
|
+
const put = buildSetRequest({ setKey: 'providerPacks', setValue: JSON.stringify(nextConfig.providerPacks) }, { config: nextConfig, revision: payload.revision }, env)
|
|
185
|
+
const saved = await fetchImpl(put.url, put.init)
|
|
186
|
+
const savedText = await saved.text()
|
|
187
|
+
if (!saved.ok) throw new Error('HTTP ' + saved.status + ': ' + savedText.slice(0, 500))
|
|
188
|
+
return { text: 'saved provider pack ' + ((pack.pack && pack.pack.id) || pack.id || ''), contentType: 'text/plain' }
|
|
189
|
+
}
|
|
101
190
|
let request = buildRequest(options.command === 'set' ? { ...options, command: 'config' } : options, env)
|
|
102
191
|
if (options.command === 'set') {
|
|
103
192
|
const current = await fetchImpl(request.url, request.init)
|
|
@@ -108,6 +197,12 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
108
197
|
const res = await fetchImpl(request.url, request.init)
|
|
109
198
|
const text = await res.text()
|
|
110
199
|
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
|
|
200
|
+
if (options.command === 'packs' && options.packAction !== 'add') {
|
|
201
|
+
try {
|
|
202
|
+
const payload = JSON.parse(text || '{}')
|
|
203
|
+
return { text: formatPacks((payload.config && payload.config.providerPacks) || []), contentType: 'application/json' }
|
|
204
|
+
} catch { /* fall back to raw */ }
|
|
205
|
+
}
|
|
111
206
|
if (options.command === 'sources' && !options.json) {
|
|
112
207
|
try { return { text: formatSources(JSON.parse(text || '{}')), contentType: 'text/plain' } } catch { /* fall back to raw */ }
|
|
113
208
|
}
|
|
@@ -117,10 +212,11 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
117
212
|
async function main() {
|
|
118
213
|
const result = await run()
|
|
119
214
|
if (result.help) {
|
|
120
|
-
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n')
|
|
215
|
+
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n doctor [--json]\n')
|
|
121
216
|
return
|
|
122
217
|
}
|
|
123
218
|
process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
|
|
219
|
+
if (result.code) process.exitCode = result.code
|
|
124
220
|
}
|
|
125
221
|
|
|
126
222
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/lib/alerts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { createHmac } from 'node:crypto'
|
|
3
|
+
import { validateWebhookUrl } from './webhook-url.js'
|
|
3
4
|
|
|
4
5
|
let pkgVersion = ''
|
|
5
6
|
try {
|
|
@@ -13,6 +14,16 @@ try {
|
|
|
13
14
|
// and webhook dispatch (e.g. Telegram/Discord or external monitoring).
|
|
14
15
|
|
|
15
16
|
const MAX_ALERTS = 100
|
|
17
|
+
let defaultWebhookSecret = ''
|
|
18
|
+
let defaultAllowPrivate = false
|
|
19
|
+
|
|
20
|
+
export function setWebhookSecret(secret) {
|
|
21
|
+
defaultWebhookSecret = String(secret || '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setWebhookAllowPrivate(allow) {
|
|
25
|
+
defaultAllowPrivate = Boolean(allow)
|
|
26
|
+
}
|
|
16
27
|
const DEDUP_WINDOW_MS = 10 * 60 * 1000 // 10 minutes cooldown per (ref + type)
|
|
17
28
|
|
|
18
29
|
const alertBuffer = []
|
|
@@ -62,9 +73,7 @@ export function getRecentAlerts(limit = 50) {
|
|
|
62
73
|
|
|
63
74
|
// #696: signed, retrying webhook delivery plus a digest endpoint payload.
|
|
64
75
|
export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
|
|
65
|
-
if (!alert || !webhookUrl
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
76
|
+
if (!alert || !webhookUrl) return false
|
|
68
77
|
return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -93,7 +102,13 @@ export async function dispatchNotificationDigest(alerts, webhookUrl, fetchImpl =
|
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
|
|
96
|
-
const
|
|
105
|
+
const opts = options || {}
|
|
106
|
+
const { retries = 1, timeoutMs = 5000, sleep } = opts
|
|
107
|
+
const secret = opts.secret == null ? defaultWebhookSecret : opts.secret
|
|
108
|
+
const allowPrivate = opts.allowPrivate == null ? defaultAllowPrivate : Boolean(opts.allowPrivate)
|
|
109
|
+
const checked = validateWebhookUrl(webhookUrl, { allowPrivate })
|
|
110
|
+
if (!checked.ok) return false
|
|
111
|
+
webhookUrl = checked.url
|
|
97
112
|
const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
|
|
98
113
|
const raw = JSON.stringify(payload)
|
|
99
114
|
const headers = {
|
package/lib/client.js
CHANGED
|
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
const CSS =
|
|
43
|
-
".dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.dsub-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.dsub-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.dsub-chevOpen{transform:rotate(180deg)}.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}.dsub-block{margin-top:10px}.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}.dsub-group[open]{padding-bottom:6px}.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}.dsub-groupHead::-webkit-details-marker{display:none}.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}.dsub-grow{flex:1 1 180px;min-width:140px}.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border-color:var(--dsw-alias-brand-primary)}.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-layer-1);font-size:11px;font-weight:600}.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warn-primary)}.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warn-primary)}.dsub-verify a{color:var(--dsw-alias-brand-primary)}.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px color-mix(in srgb,black 15%,transparent),inset 0 1px 0 color-mix(in srgb,white 6%,transparent);transition:all .16s ease;user-select:none}.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px color-mix(in srgb,black 25%,transparent),inset 0 1px 0 color-mix(in srgb,white 10%,transparent)}.dsub-pill:active{transform:translateY(0)}.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}.dsub-ledOk{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}.dsub-ledWarn{background:var(--dsw-alias-state-warn-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warn-primary) 70%,transparent)}.dsub-ledBad{background:var(--dsw-alias-state-error-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}.dsub-toast-container{position:fixed;top:16px;right:16px;z-index:10030;display:flex;flex-direction:column;gap:8px;pointer-events:none}.dsub-toast{pointer-events:auto;min-width:280px;max-width:400px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px color-mix(in srgb,black 40%,transparent);display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;line-height:1.4;animation:dsubFadeIn .2s ease-out}.dsub-toast-warn{border-color:var(--dsw-alias-state-warn-primary)}.dsub-toast-bad{border-color:var(--dsw-alias-state-error-primary)}.dsub-toast-ok{border-color:var(--dsw-alias-state-success-primary)}.dsub-toast-close{background:none;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:0 2px}.dsub-toast-close:hover{color:var(--dsw-alias-label-primary)}.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb,black 65%,transparent);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px color-mix(in srgb,black 60%,transparent),0 0 0 1px color-mix(in srgb,white 7%,transparent);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 color-mix(in srgb,white 5%,transparent)}.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}.dsub-select{appearance:none;height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 24px 0 8px;font-size:12px;cursor:pointer}.dsub-details{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2)}.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}.dsub-summary:hover{color:var(--dsw-alias-label-primary)}.dsub-header-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 0 12px;margin-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsub-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}.dsub-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.dsub-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;margin-top:12px}.dsub-section-title{font-size:14.5px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}.dsub-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}.dsub-chart-wrap{display:flex;align-items:flex-end;gap:3px;height:52px;padding:6px 0;margin-top:8px;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-chart-bar{flex:1;background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:3px;transition:height .2s ease;opacity:0.85;cursor:pointer}.dsub-chart-bar:hover{opacity:1;background:var(--dsw-alias-state-success-primary)}.dsub-chart-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}.dsub-btn-primary:hover:not(:disabled){opacity:0.9}.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.dsub-model-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-size:11.5px;color:var(--dsw-alias-label-primary)}.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}.dsub-model-tag{font-size:10px;padding:1px 4px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-weight:600}.dsub-trace-box{padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);margin-top:8px;font-size:12px}.dsub-trace-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-trace-row:last-child{border-bottom:none}.dsub-model-status-available{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}.dsub-model-status-degraded{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}.dsub-model-status-quarantined{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-cooldown{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent)}.dsub-model-status-exhausted{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-unconfigured{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3)}.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 5%,transparent)}.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.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}.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}.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}.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 20%,transparent)}.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}.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}.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}.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}.dsub-brandCodex{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent)}.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGrok{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.dsub-brandAgy{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent)}.dsub-brandOllama{background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 15%,transparent);color:var(--dsw-alias-label-secondary);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary) 30%,transparent)}.dsub-brandKimi{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGlm{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent);margin-left:6px}.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-accName{font-size:13px;font-weight:600}.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary)}.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border:0;cursor:pointer;transition:opacity .15s}.dsub-btnPrimary:hover{opacity:.9}.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-qw{position:fixed;z-index:10010;display:flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 4px 14px color-mix(in srgb,black 28%,transparent);cursor:grab;user-select:none;font-size:11.5px;font-weight:600;touch-action:none}.dsub-qw:active{cursor:grabbing}.dsub-qwDocked{right:16px !important;bottom:16px !important;cursor:default}.dsub-qwPct{font-variant-numeric:tabular-nums}.dsub-qwBar{width:28px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-qwBarFill{height:100%;border-radius:2px;background:var(--dsw-alias-state-success-primary)}.dsub-qwWarn .dsub-qwBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-qwFull .dsub-qwBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}";
|
|
43
|
+
".dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.dsub-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.dsub-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.dsub-chevOpen{transform:rotate(180deg)}.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}.dsub-block{margin-top:10px}.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}.dsub-group[open]{padding-bottom:6px}.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}.dsub-groupHead::-webkit-details-marker{display:none}.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}.dsub-grow{flex:1 1 180px;min-width:140px}.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border-color:var(--dsw-alias-brand-primary)}.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-layer-1);font-size:11px;font-weight:600}.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warn-primary)}.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warn-primary)}.dsub-verify a{color:var(--dsw-alias-brand-primary)}.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px color-mix(in srgb,black 15%,transparent),inset 0 1px 0 color-mix(in srgb,white 6%,transparent);transition:all .16s ease;user-select:none}.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px color-mix(in srgb,black 25%,transparent),inset 0 1px 0 color-mix(in srgb,white 10%,transparent)}.dsub-pill:active{transform:translateY(0)}.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}.dsub-ledOk{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}.dsub-ledWarn{background:var(--dsw-alias-state-warn-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warn-primary) 70%,transparent)}.dsub-ledBad{background:var(--dsw-alias-state-error-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}.dsub-toast-container{position:fixed;top:16px;right:16px;z-index:10030;display:flex;flex-direction:column;gap:8px;pointer-events:none}.dsub-toast{pointer-events:auto;min-width:280px;max-width:400px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px color-mix(in srgb,black 40%,transparent);display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;line-height:1.4;animation:dsubFadeIn .2s ease-out}.dsub-toast-warn{border-color:var(--dsw-alias-state-warn-primary)}.dsub-toast-bad{border-color:var(--dsw-alias-state-error-primary)}.dsub-toast-ok{border-color:var(--dsw-alias-state-success-primary)}.dsub-toast-close{background:none;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:0 2px}.dsub-toast-close:hover{color:var(--dsw-alias-label-primary)}.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb,black 65%,transparent);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px color-mix(in srgb,black 60%,transparent),0 0 0 1px color-mix(in srgb,white 7%,transparent);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 color-mix(in srgb,white 5%,transparent)}.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}.dsub-select{appearance:none;height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 24px 0 8px;font-size:12px;cursor:pointer}.dsub-details{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2)}.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}.dsub-summary:hover{color:var(--dsw-alias-label-primary)}.dsub-header-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 0 12px;margin-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsub-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}.dsub-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.dsub-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;margin-top:12px}.dsub-section-title{font-size:14.5px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}.dsub-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}.dsub-chart-wrap{display:flex;align-items:flex-end;gap:3px;height:52px;padding:6px 0;margin-top:8px;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-chart-bar{flex:1;background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:3px;transition:height .2s ease;opacity:0.85;cursor:pointer}.dsub-chart-bar:hover{opacity:1;background:var(--dsw-alias-state-success-primary)}.dsub-chart-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}.dsub-btn-primary:hover:not(:disabled){opacity:0.9}.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.dsub-model-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-size:11.5px;color:var(--dsw-alias-label-primary)}.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}.dsub-model-tag{font-size:10px;padding:1px 4px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-weight:600}.dsub-trace-box{padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);margin-top:8px;font-size:12px}.dsub-trace-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-trace-row:last-child{border-bottom:none}.dsub-model-status-available{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}.dsub-model-status-degraded{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}.dsub-model-status-quarantined{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-cooldown{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent)}.dsub-model-status-exhausted{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-unconfigured{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3)}.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 5%,transparent)}.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.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}.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}.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}.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 20%,transparent)}.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}.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}.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}.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}.dsub-brandCodex{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent)}.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGrok{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.dsub-brandAgy{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent)}.dsub-brandOllama{background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 15%,transparent);color:var(--dsw-alias-label-secondary);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary) 30%,transparent)}.dsub-brandKimi{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGlm{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent);margin-left:6px}.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-accName{font-size:13px;font-weight:600}.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary)}.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border:0;cursor:pointer;transition:opacity .15s}.dsub-btnPrimary:hover{opacity:.9}.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-qw{position:fixed;z-index:10010;display:flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 4px 14px color-mix(in srgb,black 28%,transparent);cursor:grab;user-select:none;font-size:11.5px;font-weight:600;touch-action:none}.dsub-qw:active{cursor:grabbing}.dsub-qwDocked{right:16px !important;bottom:16px !important;cursor:default}.dsub-qwPct{font-variant-numeric:tabular-nums}.dsub-qwBar{width:28px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-qwBarFill{height:100%;border-radius:2px;background:var(--dsw-alias-state-success-primary)}.dsub-qwPop{position:fixed;z-index:10011;width:280px;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-3);box-shadow:0 12px 32px color-mix(in srgb,black 40%,transparent);font-size:12px}.dsub-qwWarn .dsub-qwBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-qwFull .dsub-qwBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}";
|
|
44
44
|
const cssId = 'dsh-subscriptions/settings.module.css'
|
|
45
45
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + cssId + '"]')) {
|
|
46
46
|
const tag = document.createElement('style')
|
|
@@ -1842,6 +1842,7 @@ window.__ModuleLoader__.load({
|
|
|
1842
1842
|
),
|
|
1843
1843
|
h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
|
|
1844
1844
|
h(SourcesPanel, { t }),
|
|
1845
|
+
h(ProviderPacksPanel),
|
|
1845
1846
|
|
|
1846
1847
|
h('div', { className: 'dsub-row' },
|
|
1847
1848
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
@@ -1872,6 +1873,41 @@ window.__ModuleLoader__.load({
|
|
|
1872
1873
|
),
|
|
1873
1874
|
h('div', { className: 'dsub-sub' },
|
|
1874
1875
|
t('expiryNotifyDaysHint')),
|
|
1876
|
+
h('div', { className: 'dsub-row' },
|
|
1877
|
+
h('span', { className: 'dsub-h' }, 'Webhook URL'),
|
|
1878
|
+
h('input', {
|
|
1879
|
+
type: 'url',
|
|
1880
|
+
className: 'dsub-inp',
|
|
1881
|
+
placeholder: 'https://hooks.example/…',
|
|
1882
|
+
value: draft.webhookUrl || '',
|
|
1883
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { webhookUrl: e.target.value })),
|
|
1884
|
+
}),
|
|
1885
|
+
),
|
|
1886
|
+
h('div', { className: 'dsub-sub' }, 'Signed POST target for quota, session and billing alerts.'),
|
|
1887
|
+
h('div', { className: 'dsub-row' },
|
|
1888
|
+
h('span', { className: 'dsub-h' }, 'Webhook secret'),
|
|
1889
|
+
h('input', {
|
|
1890
|
+
type: 'password',
|
|
1891
|
+
className: 'dsub-inp',
|
|
1892
|
+
autoComplete: 'new-password',
|
|
1893
|
+
value: draft.webhookSecret || '',
|
|
1894
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { webhookSecret: e.target.value })),
|
|
1895
|
+
}),
|
|
1896
|
+
),
|
|
1897
|
+
h('div', { className: 'dsub-row' },
|
|
1898
|
+
h('span', { className: 'dsub-h' }, 'Digest interval (hours)'),
|
|
1899
|
+
h('input', {
|
|
1900
|
+
type: 'number',
|
|
1901
|
+
className: 'dsub-inp',
|
|
1902
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1903
|
+
min: 0,
|
|
1904
|
+
max: 168,
|
|
1905
|
+
value: draft.digestIntervalHours != null ? draft.digestIntervalHours : 0,
|
|
1906
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { digestIntervalHours: Number(e.target.value) || 0 })),
|
|
1907
|
+
}),
|
|
1908
|
+
),
|
|
1909
|
+
h('div', { className: 'dsub-sub' }, '0 disables digests. Requires a Webhook URL.'),
|
|
1910
|
+
h(WebhookTestButton),
|
|
1875
1911
|
),
|
|
1876
1912
|
h('details', { className: 'dsub-group', open: true },
|
|
1877
1913
|
h('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
|
|
@@ -3655,6 +3691,98 @@ window.__ModuleLoader__.load({
|
|
|
3655
3691
|
)
|
|
3656
3692
|
}
|
|
3657
3693
|
|
|
3694
|
+
function WebhookTestButton() {
|
|
3695
|
+
const [state, setState] = React.useState({ busy: false, message: '', error: null })
|
|
3696
|
+
const send = () => {
|
|
3697
|
+
setState({ busy: true, message: '', error: null })
|
|
3698
|
+
fetch('/dsh-subscriptions/webhook/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
3699
|
+
.then(async (res) => {
|
|
3700
|
+
const data = await res.json().catch(() => ({}))
|
|
3701
|
+
if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3702
|
+
setState({ busy: false, message: 'delivered', error: null })
|
|
3703
|
+
})
|
|
3704
|
+
.catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
|
|
3705
|
+
}
|
|
3706
|
+
return h('div', { className: 'dsub-row' },
|
|
3707
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : 'Send test'),
|
|
3708
|
+
state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
|
|
3709
|
+
state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
|
|
3710
|
+
)
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
3714
|
+
function ProviderPacksPanel() {
|
|
3715
|
+
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
3716
|
+
const publicPack = (pack) => {
|
|
3717
|
+
const copy = Object.assign({}, pack)
|
|
3718
|
+
if (copy.clientSecret) copy.clientSecret = '••••'
|
|
3719
|
+
if (copy.headers && typeof copy.headers === 'object') {
|
|
3720
|
+
copy.headers = Object.fromEntries(Object.keys(copy.headers).map((key) => [key, '••••']))
|
|
3721
|
+
}
|
|
3722
|
+
return copy
|
|
3723
|
+
}
|
|
3724
|
+
const pull = () => {
|
|
3725
|
+
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
3726
|
+
.then((res) => res.json())
|
|
3727
|
+
.then((cfg) => setState((st) => Object.assign({}, st, { packs: (cfg.config && cfg.config.providerPacks) || [] })))
|
|
3728
|
+
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
3729
|
+
}
|
|
3730
|
+
React.useEffect(() => { pull() }, [])
|
|
3731
|
+
const parse = () => {
|
|
3732
|
+
const raw = JSON.parse(state.text || '{}')
|
|
3733
|
+
return raw && raw.pack ? raw.pack : raw
|
|
3734
|
+
}
|
|
3735
|
+
const validate = (pack) => fetch('/dsh-subscriptions/provider-packs/validate', {
|
|
3736
|
+
method: 'POST',
|
|
3737
|
+
headers: { 'content-type': 'application/json' },
|
|
3738
|
+
body: JSON.stringify({ pack }),
|
|
3739
|
+
}).then(async (res) => {
|
|
3740
|
+
const data = await res.json()
|
|
3741
|
+
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3742
|
+
return data.pack
|
|
3743
|
+
})
|
|
3744
|
+
const save = () => {
|
|
3745
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3746
|
+
Promise.resolve().then(parse)
|
|
3747
|
+
.then(async (pack) => {
|
|
3748
|
+
const checked = await validate(pack)
|
|
3749
|
+
const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json())
|
|
3750
|
+
const packs = ((cfg.config && cfg.config.providerPacks) || []).filter((p) => p && p.id !== pack.id)
|
|
3751
|
+
packs.push(pack)
|
|
3752
|
+
const res = await fetch('/dsh-subscriptions/config', {
|
|
3753
|
+
method: 'PUT',
|
|
3754
|
+
headers: { 'content-type': 'application/json' },
|
|
3755
|
+
body: JSON.stringify({ config: Object.assign({}, cfg.config, { providerPacks: packs }), revision: cfg.revision }),
|
|
3756
|
+
})
|
|
3757
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
3758
|
+
setState((st) => Object.assign({}, st, { packs, busy: false, message: 'saved ' + checked.id, text: '' }))
|
|
3759
|
+
})
|
|
3760
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3761
|
+
}
|
|
3762
|
+
const exportPacks = () => setState((st) => Object.assign({}, st, { text: JSON.stringify(st.packs.map(publicPack), null, 2), message: 'exported' }))
|
|
3763
|
+
return h('div', { className: 'dsub-section-card' },
|
|
3764
|
+
h('div', { className: 'dsub-section-title' }, 'Provider packs', h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
3765
|
+
h('div', { className: 'dsub-section-desc' }, 'Add an OpenAI-Responses-compatible provider as a JSON manifest. Headers are masked in the editor.'),
|
|
3766
|
+
state.packs.length ? state.packs.map((pack) => h('div', { className: 'dsub-row', key: pack.id },
|
|
3767
|
+
h('span', { className: 'dsub-h' }, pack.id),
|
|
3768
|
+
h('span', { className: 'dsub-dim' }, (pack.displayName || '') + ' · ' + ((pack.models || []).length) + ' models'),
|
|
3769
|
+
)) : h('div', { className: 'dsub-dim' }, 'No packs yet.'),
|
|
3770
|
+
h('textarea', {
|
|
3771
|
+
className: 'dsub-inp',
|
|
3772
|
+
style: { width: '100%', minHeight: 120, fontFamily: 'ui-monospace,monospace', padding: 8 },
|
|
3773
|
+
value: state.text,
|
|
3774
|
+
placeholder: '{"id":"acme","displayName":"Acme AI","authUrl":"https://…","tokenUrl":"https://…","baseUrl":"https://…","clientId":"…","models":[{"id":"acme-1"}]}',
|
|
3775
|
+
onChange: (e) => setState((st) => Object.assign({}, st, { text: e.target.value, message: '', error: null })),
|
|
3776
|
+
}),
|
|
3777
|
+
h('div', { className: 'dsub-row' },
|
|
3778
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: save }, state.busy ? '…' : 'Validate & save'),
|
|
3779
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: exportPacks }, 'Export'),
|
|
3780
|
+
),
|
|
3781
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3782
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3783
|
+
)
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3658
3786
|
// #713: read-only source chain panel. Never renders key values.
|
|
3659
3787
|
function SourcesPanel() {
|
|
3660
3788
|
const [state, setState] = React.useState({ loading: true, data: null, error: null })
|
|
@@ -3692,8 +3820,10 @@ window.__ModuleLoader__.load({
|
|
|
3692
3820
|
// #688: floating/docked quota capsule. Draggable unless docked; opens settings.
|
|
3693
3821
|
function QuotaCapsule(props) {
|
|
3694
3822
|
const t = (props && props.t) || ((k) => k)
|
|
3695
|
-
const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null })
|
|
3823
|
+
const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null, candidates: [] })
|
|
3824
|
+
const [open, setOpen] = React.useState(false)
|
|
3696
3825
|
const [pos, setPos] = React.useState({ right: 16, bottom: 16 })
|
|
3826
|
+
const popRef = React.useRef(null)
|
|
3697
3827
|
const posRef = React.useRef({ right: 16, bottom: 16 })
|
|
3698
3828
|
const drag = React.useRef(null)
|
|
3699
3829
|
const suppressClick = React.useRef(false)
|
|
@@ -3721,7 +3851,7 @@ window.__ModuleLoader__.load({
|
|
|
3721
3851
|
refreshLoggedIn().then(async (r) => {
|
|
3722
3852
|
if (!alive) return
|
|
3723
3853
|
const mode = r.quotaWidget || 'docked'
|
|
3724
|
-
if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null }); return }
|
|
3854
|
+
if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
|
|
3725
3855
|
const sources = await fetch('/dsh-subscriptions/sources', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
|
|
3726
3856
|
let a = r.active
|
|
3727
3857
|
const current = currentModelProvider(props)
|
|
@@ -3730,9 +3860,10 @@ window.__ModuleLoader__.load({
|
|
|
3730
3860
|
const rows = (cfg && cfg.accounts) || []
|
|
3731
3861
|
a = rows.find((row) => row && row.provider === current && row.usagePercent != null) || null
|
|
3732
3862
|
}
|
|
3733
|
-
if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null }); return }
|
|
3863
|
+
if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
|
|
3734
3864
|
const sourceRow = (sources && Array.isArray(sources.providers) ? sources.providers : []).find((row) => row && row.provider === a.provider)
|
|
3735
3865
|
const source = sourceRow ? sourceRow.selected : null
|
|
3866
|
+
const candidates = sourceRow && Array.isArray(sourceRow.candidates) ? sourceRow.candidates : []
|
|
3736
3867
|
if (r.quotaWidgetPosition && typeof window !== 'undefined') {
|
|
3737
3868
|
const clamp = (v, max) => Math.max(4, Math.min(max, Number(v) || 0))
|
|
3738
3869
|
const next = {
|
|
@@ -3742,7 +3873,7 @@ window.__ModuleLoader__.load({
|
|
|
3742
3873
|
posRef.current = next
|
|
3743
3874
|
setPos(next)
|
|
3744
3875
|
}
|
|
3745
|
-
setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source })
|
|
3876
|
+
setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source, candidates })
|
|
3746
3877
|
}).catch(() => {})
|
|
3747
3878
|
}
|
|
3748
3879
|
pull()
|
|
@@ -3763,6 +3894,16 @@ window.__ModuleLoader__.load({
|
|
|
3763
3894
|
if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
|
|
3764
3895
|
} catch { /* bestEffort */ }
|
|
3765
3896
|
}
|
|
3897
|
+
React.useEffect(() => {
|
|
3898
|
+
if (!open) return undefined
|
|
3899
|
+
const onDoc = (e) => {
|
|
3900
|
+
if (popRef.current && !popRef.current.contains(e.target) && !(e.target.closest && e.target.closest('.dsub-qw'))) setOpen(false)
|
|
3901
|
+
}
|
|
3902
|
+
const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
3903
|
+
document.addEventListener('pointerdown', onDoc)
|
|
3904
|
+
document.addEventListener('keydown', onKey)
|
|
3905
|
+
return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey) }
|
|
3906
|
+
}, [open])
|
|
3766
3907
|
const onPointerDown = (e) => {
|
|
3767
3908
|
if (docked) return
|
|
3768
3909
|
drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
|
|
@@ -3786,7 +3927,27 @@ window.__ModuleLoader__.load({
|
|
|
3786
3927
|
drag.current = null
|
|
3787
3928
|
if (!docked && wasDrag && wasDrag.moved) savePosition(posRef.current)
|
|
3788
3929
|
}
|
|
3789
|
-
|
|
3930
|
+
const popover = open ? h('div', {
|
|
3931
|
+
ref: popRef,
|
|
3932
|
+
className: 'dsub-qwPop',
|
|
3933
|
+
style: { right: pos.right + 'px', bottom: (pos.bottom + 38) + 'px' },
|
|
3934
|
+
role: 'dialog',
|
|
3935
|
+
'aria-label': 'Quota sources',
|
|
3936
|
+
},
|
|
3937
|
+
h('div', { className: 'dsub-h' }, String(state.active.provider || '').toUpperCase() + ' sources'),
|
|
3938
|
+
(state.candidates || []).map((c, i) => {
|
|
3939
|
+
const label = c.source === 'api-key' ? 'API key' : (c.source === 'oauth' ? 'OAuth' : 'Ollama')
|
|
3940
|
+
const cd = c.detail && Array.isArray(c.detail.keys) ? Math.max(0, ...c.detail.keys.map((k) => Number(k.cooldownMsLeft) || 0)) : 0
|
|
3941
|
+
return h('div', { className: 'dsub-row', key: i },
|
|
3942
|
+
h('span', { className: 'dsub-h' }, label),
|
|
3943
|
+
h('span', { className: c.available ? 'dsub-ok' : 'dsub-dim' }, c.available ? 'available' : 'unavailable'),
|
|
3944
|
+
cd > 0 ? h('span', { className: 'dsub-dim' }, 'cooldown ' + Math.ceil(cd / 1000) + 's') : null,
|
|
3945
|
+
c.detail && c.detail.healthyKeys != null ? h('span', { className: 'dsub-dim' }, c.detail.healthyKeys + '/' + c.detail.totalKeys + ' keys') : null,
|
|
3946
|
+
)
|
|
3947
|
+
}),
|
|
3948
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setOpen(false); openSettings() } }, 'Open settings'),
|
|
3949
|
+
) : null
|
|
3950
|
+
return h(React.Fragment, null, h('div', {
|
|
3790
3951
|
className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
|
|
3791
3952
|
style: { right: pos.right + 'px', bottom: pos.bottom + 'px' },
|
|
3792
3953
|
role: 'button',
|
|
@@ -3797,15 +3958,15 @@ window.__ModuleLoader__.load({
|
|
|
3797
3958
|
onPointerMove,
|
|
3798
3959
|
onPointerUp,
|
|
3799
3960
|
onPointerCancel: () => { drag.current = null },
|
|
3800
|
-
onClick: () => { if (suppressClick.current) { suppressClick.current = false; return }
|
|
3801
|
-
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
3961
|
+
onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } setOpen((o) => !o) },
|
|
3962
|
+
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') setOpen((o) => !o) },
|
|
3802
3963
|
},
|
|
3803
3964
|
h('span', { className: 'dsub-led ' + ledClass(state.active.usagePercent) }),
|
|
3804
3965
|
h('span', { className: 'dsub-qwBrand' }, brandBadge(state.active.provider).icon + ' ' + String(state.active.provider || '').toUpperCase()),
|
|
3805
3966
|
state.source ? h('span', { className: 'dsub-pillTag' }, state.source === 'api-key' ? 'KEY' : (state.source === 'oauth' ? 'OAuth' : 'OLLAMA')) : null,
|
|
3806
3967
|
h('span', { className: 'dsub-qwBar' }, h('span', { className: 'dsub-qwBarFill', style: { width: state.remaining + '%', display: 'block' } })),
|
|
3807
3968
|
h('span', { className: 'dsub-qwPct' }, Math.round(state.remaining) + '%'),
|
|
3808
|
-
)
|
|
3969
|
+
), popover)
|
|
3809
3970
|
}
|
|
3810
3971
|
|
|
3811
3972
|
function registerSubsPill(ctx) {
|
package/lib/config-schema.js
CHANGED
|
@@ -157,6 +157,12 @@ export const Config = z.object({
|
|
|
157
157
|
.description('#349 Custom cross-vendor fallback chains (e.g. { claude: ["copilot", "cursor"] }).'),
|
|
158
158
|
webhookUrl: z.string().volatile().default('')
|
|
159
159
|
.description('#351 External webhook URL to receive quota and session alerts (JSON POST).'),
|
|
160
|
+
webhookSecret: z.string().role('secret').default('')
|
|
161
|
+
.description('#720 HMAC-SHA256 secret used to sign webhook payloads.'),
|
|
162
|
+
webhookAllowPrivate: z.boolean().volatile().default(false)
|
|
163
|
+
.description('#729 Allow webhook URLs that resolve to private or loopback hosts.'),
|
|
164
|
+
digestIntervalHours: z.number().volatile().default(0)
|
|
165
|
+
.description('#720 Send an alert digest every N hours (0 disables digests).'),
|
|
160
166
|
alertThresholds: z.array(z.number()).volatile().default([80, 90, 95])
|
|
161
167
|
.description('#351 Quota usage percentage thresholds for firing alerts.'),
|
|
162
168
|
enableToasts: z.boolean().volatile().default(true)
|
package/lib/index.js
CHANGED
|
@@ -28,7 +28,8 @@ import { createAccountsView } from './accounts-view.js'
|
|
|
28
28
|
import { inspectScopes } from './scope-inspector.js'
|
|
29
29
|
import { createAdapterManager } from './adapter-manager.js'
|
|
30
30
|
import { pickAccount, isAuthError } from './rotate.js'
|
|
31
|
-
import { notifySessionExpired } from './alerts.js'
|
|
31
|
+
import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret, setWebhookAllowPrivate } from './alerts.js'
|
|
32
|
+
import { isDigestDue } from './notification-scheduler.js'
|
|
32
33
|
|
|
33
34
|
export { observeForecast, estimateForecast } from './forecast.js'
|
|
34
35
|
export { formatRelativeReset } from './relative-time.js'
|
|
@@ -91,7 +92,11 @@ export function apply(ctx, rawConfig) {
|
|
|
91
92
|
return plainConfig(Config(plainConfig(rawConfig || {})))
|
|
92
93
|
}
|
|
93
94
|
let cachedConfig = resolveEntry()
|
|
94
|
-
const live = () =>
|
|
95
|
+
const live = () => {
|
|
96
|
+
setWebhookSecret(cachedConfig.webhookSecret)
|
|
97
|
+
setWebhookAllowPrivate(cachedConfig.webhookAllowPrivate)
|
|
98
|
+
return cachedConfig
|
|
99
|
+
}
|
|
95
100
|
|
|
96
101
|
function syncCustomVendors() {
|
|
97
102
|
const profiles = validateProfiles(live().customVendors)
|
|
@@ -604,6 +609,22 @@ export function apply(ctx, rawConfig) {
|
|
|
604
609
|
}
|
|
605
610
|
}, 'dsh-subscriptions: vault-sync-loop')
|
|
606
611
|
|
|
612
|
+
// #720: interval digest for busy webhook channels.
|
|
613
|
+
ctx.effect(() => {
|
|
614
|
+
let lastDigestAt = 0
|
|
615
|
+
const tick = () => {
|
|
616
|
+
const cfg = live()
|
|
617
|
+
const alerts = getRecentAlerts(100)
|
|
618
|
+
if (!isDigestDue({ lastDigestAt, nowMs: Date.now(), intervalHours: cfg.digestIntervalHours, alertCount: alerts.length })) return
|
|
619
|
+
dispatchNotificationDigest(alerts, cfg.webhookUrl, fetch, { secret: cfg.webhookSecret, allowPrivate: cfg.webhookAllowPrivate })
|
|
620
|
+
.then((ok) => { if (ok) lastDigestAt = Date.now() })
|
|
621
|
+
.catch(() => {})
|
|
622
|
+
}
|
|
623
|
+
tick()
|
|
624
|
+
const id = setInterval(tick, 60 * 60 * 1000)
|
|
625
|
+
return () => clearInterval(id)
|
|
626
|
+
}, 'dsh-subscriptions: notification digest')
|
|
627
|
+
|
|
607
628
|
registerRoutes(ctx, {
|
|
608
629
|
NS,
|
|
609
630
|
live,
|
|
@@ -41,19 +41,50 @@ export function summarizeKeyRotationStatus(raw) {
|
|
|
41
41
|
return { available: true, providers, totalKeys, healthyKeys }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// #726: a short TTL keeps the widget, settings panel and doctor from
|
|
45
|
+
// multiplying key-rotation status reads. Errors are cached for the same TTL.
|
|
46
|
+
let statusCache = { key: '', at: 0, value: null }
|
|
47
|
+
let inFlight = null
|
|
48
|
+
|
|
49
|
+
export function clearKeyRotationCache() {
|
|
50
|
+
statusCache = { key: '', at: 0, value: null }
|
|
51
|
+
inFlight = null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readKeyRotationStatus({ baseUrl, path = KEY_ROTATION_STATUS_PATH, fetchImpl = fetch, timeoutMs = 2000, cacheTtlMs = 10000, bypassCache = false } = {}) {
|
|
45
55
|
if (!baseUrl) return { available: false, reason: 'missing base url', providers: [], totalKeys: 0, healthyKeys: 0 }
|
|
56
|
+
const url = String(baseUrl).replace(/\/$/, '') + path
|
|
57
|
+
if (!bypassCache && statusCache.key === url && (Date.now() - statusCache.at) < Number(cacheTtlMs)) {
|
|
58
|
+
return statusCache.value
|
|
59
|
+
}
|
|
60
|
+
if (!bypassCache && inFlight && inFlight.key === url) return inFlight.promise
|
|
61
|
+
const promise = (async () => {
|
|
46
62
|
try {
|
|
47
63
|
const init = { method: 'GET', headers: { accept: 'application/json' } }
|
|
48
64
|
if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
49
65
|
init.signal = AbortSignal.timeout(timeoutMs)
|
|
50
66
|
}
|
|
51
|
-
const res = await fetchImpl(
|
|
52
|
-
if (!res || !res.ok)
|
|
67
|
+
const res = await fetchImpl(url, init)
|
|
68
|
+
if (!res || !res.ok) {
|
|
69
|
+
const value = { available: false, reason: 'HTTP ' + (res && res.status), providers: [], totalKeys: 0, healthyKeys: 0 }
|
|
70
|
+
statusCache = { key: url, at: Date.now(), value }
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
53
73
|
const raw = await res.json()
|
|
54
|
-
|
|
74
|
+
const value = summarizeKeyRotationStatus(raw)
|
|
75
|
+
statusCache = { key: url, at: Date.now(), value }
|
|
76
|
+
return value
|
|
55
77
|
} catch (err) {
|
|
56
|
-
|
|
78
|
+
const value = { available: false, reason: String(err && err.message || err), providers: [], totalKeys: 0, healthyKeys: 0 }
|
|
79
|
+
statusCache = { key: url, at: Date.now(), value }
|
|
80
|
+
return value
|
|
81
|
+
}
|
|
82
|
+
})()
|
|
83
|
+
inFlight = { key: url, promise }
|
|
84
|
+
try {
|
|
85
|
+
return await promise
|
|
86
|
+
} finally {
|
|
87
|
+
if (inFlight && inFlight.promise === promise) inFlight = null
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// #720: digest scheduling is a pure decision so it can be tested without timers.
|
|
2
|
+
export function isDigestDue({ lastDigestAt = 0, nowMs = Date.now(), intervalHours = 0, alertCount = 0 } = {}) {
|
|
3
|
+
if (!(Number(intervalHours) > 0)) return false
|
|
4
|
+
if (!(Number(alertCount) > 0)) return false
|
|
5
|
+
if (!lastDigestAt) return true
|
|
6
|
+
return Number(nowMs) - Number(lastDigestAt) >= Number(intervalHours) * 60 * 60 * 1000
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
2
|
+
import { normalizeVendorPack } from '../vendor-pack.js'
|
|
3
|
+
|
|
4
|
+
export function registerPacksRoutes(ctx) {
|
|
5
|
+
ctx.effect(() => ctx.webServer.register({
|
|
6
|
+
kind: 'exact',
|
|
7
|
+
path: '/dsh-subscriptions/provider-packs/validate',
|
|
8
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
9
|
+
if (req.method !== 'POST') {
|
|
10
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
14
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
let body
|
|
18
|
+
try { body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {
|
|
19
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const raw = body && body.pack && typeof body.pack === 'object' ? body.pack : body
|
|
23
|
+
try {
|
|
24
|
+
const pack = normalizeVendorPack(raw)
|
|
25
|
+
writeJson(res, 200, {
|
|
26
|
+
ok: true,
|
|
27
|
+
pack: {
|
|
28
|
+
id: pack.id,
|
|
29
|
+
displayName: pack.displayName,
|
|
30
|
+
baseUrl: pack.baseUrl,
|
|
31
|
+
models: pack.models,
|
|
32
|
+
scope: pack.scope,
|
|
33
|
+
tokenStyle: pack.tokenStyle,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
} catch (err) {
|
|
37
|
+
writeJson(res, 400, { ok: false, error: { code: 'pack', message: String(err && err.message || err) } })
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
}), 'dsh-subscriptions: /provider-packs/validate')
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { dispatchWebhookAlert } from '../alerts.js'
|
|
2
|
+
import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
|
|
3
|
+
import { validateWebhookUrl } from '../webhook-url.js'
|
|
4
|
+
|
|
5
|
+
export function registerWebhookRoutes(ctx, state) {
|
|
6
|
+
const { live } = state
|
|
7
|
+
ctx.effect(() => ctx.webServer.register({
|
|
8
|
+
kind: 'exact',
|
|
9
|
+
path: '/dsh-subscriptions/webhook/test',
|
|
10
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
11
|
+
if (req.method !== 'POST') {
|
|
12
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
16
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
const cfg = live()
|
|
20
|
+
if (!cfg.webhookUrl) {
|
|
21
|
+
writeJson(res, 400, { ok: false, delivered: false, error: { code: 'webhook', message: 'webhookUrl is not configured' } })
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
const checked = validateWebhookUrl(cfg.webhookUrl, { allowPrivate: cfg.webhookAllowPrivate })
|
|
25
|
+
if (!checked.ok) {
|
|
26
|
+
writeJson(res, 400, { ok: false, delivered: false, error: { code: 'webhook', message: checked.reason } })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const alert = {
|
|
30
|
+
id: 'wh_test_' + Date.now().toString(36),
|
|
31
|
+
type: 'webhook_test',
|
|
32
|
+
ref: null,
|
|
33
|
+
provider: 'dsh-subscriptions',
|
|
34
|
+
message: 'DSH Subscriptions webhook test',
|
|
35
|
+
severity: 'info',
|
|
36
|
+
details: null,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
}
|
|
39
|
+
const delivered = await dispatchWebhookAlert(alert, cfg.webhookUrl, fetch, {
|
|
40
|
+
secret: cfg.webhookSecret,
|
|
41
|
+
allowPrivate: cfg.webhookAllowPrivate,
|
|
42
|
+
retries: 1,
|
|
43
|
+
})
|
|
44
|
+
writeJson(res, delivered ? 200 : 502, { ok: delivered, delivered })
|
|
45
|
+
}),
|
|
46
|
+
}), 'dsh-subscriptions: /webhook/test')
|
|
47
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -6,6 +6,8 @@ import { registerOauthRoutes } from './routes/oauth.js'
|
|
|
6
6
|
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
7
7
|
import { registerProxyRoutes } from './routes/proxy.js'
|
|
8
8
|
import { registerSourcesRoutes } from './routes/sources.js'
|
|
9
|
+
import { registerPacksRoutes } from './routes/packs.js'
|
|
10
|
+
import { registerWebhookRoutes } from './routes/webhook.js'
|
|
9
11
|
import { registerPluginUpdater } from './updater.js'
|
|
10
12
|
|
|
11
13
|
export function registerRoutes(ctx, state) {
|
|
@@ -111,4 +113,6 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'
|
|
|
111
113
|
registerAccountsRoutes(ctx, state)
|
|
112
114
|
registerProxyRoutes(ctx, state)
|
|
113
115
|
registerSourcesRoutes(ctx, state)
|
|
116
|
+
registerPacksRoutes(ctx)
|
|
117
|
+
registerWebhookRoutes(ctx, state)
|
|
114
118
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// #729: webhook URLs are user-controlled and fetched by the host, so reject
|
|
2
|
+
// obvious SSRF targets unless the operator explicitly opts into private hosts.
|
|
3
|
+
export function isPrivateHostname(hostname) {
|
|
4
|
+
const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '')
|
|
5
|
+
if (!host) return true
|
|
6
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true
|
|
7
|
+
if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true
|
|
8
|
+
if (/^(fc|fd|fe8|fe9|fea|feb)/.test(host)) return true
|
|
9
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
|
10
|
+
if (!m) return false
|
|
11
|
+
const parts = m.slice(1).map(Number)
|
|
12
|
+
if (parts.some((n) => n > 255)) return true
|
|
13
|
+
const [a, b] = parts
|
|
14
|
+
return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254)
|
|
15
|
+
|| (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)
|
|
16
|
+
|| (a === 100 && b >= 64 && b <= 127)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateWebhookUrl(raw, { allowPrivate = false } = {}) {
|
|
20
|
+
if (typeof raw !== 'string' || !raw.trim()) return { ok: false, reason: 'webhook URL is empty' }
|
|
21
|
+
let url
|
|
22
|
+
try { url = new URL(raw.trim()) } catch { return { ok: false, reason: 'webhook URL is invalid' } }
|
|
23
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') return { ok: false, reason: 'webhook URL must use http(s)' }
|
|
24
|
+
if (url.username || url.password) return { ok: false, reason: 'webhook URL must not contain userinfo' }
|
|
25
|
+
if (!allowPrivate && isPrivateHostname(url.hostname)) return { ok: false, reason: 'webhook URL points to a private/loopback host' }
|
|
26
|
+
return { ok: true, url: url.toString() }
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
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",
|