@goodandready/dsh-subscriptions 0.7.2 → 0.7.4

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.
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ export function parseArgs(argv) {
6
+ const args = Array.isArray(argv) ? argv.slice() : []
7
+ const command = args.shift() || 'status'
8
+ const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '' }
9
+ for (let i = 0; i < args.length; i += 1) {
10
+ const arg = args[i]
11
+ if (arg === '--csv') options.csv = true
12
+ else if (arg === '--json') options.json = true
13
+ else if (arg === '--help' || arg === '-h') options.help = true
14
+ else if (arg === '--type') options.type = args[++i] || 'accounts'
15
+ else if (arg === '--session') options.sessionId = args[++i] || ''
16
+ else if (command === 'set' && !options.setKey) options.setKey = arg
17
+ else if (command === 'set') options.setValue = arg
18
+ else if (command === 'fast' && !options.sessionId) options.sessionId = arg
19
+ else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
20
+ }
21
+ return options
22
+ }
23
+
24
+ export function parseConfigValue(raw) {
25
+ if (raw === 'true') return true
26
+ if (raw === 'false') return false
27
+ if (raw === 'null') return null
28
+ if (/^-?\d+(?:\.\d+)?$/.test(raw)) return Number(raw)
29
+ if (raw.startsWith('{') || raw.startsWith('[')) {
30
+ try { return JSON.parse(raw) } catch { throw new Error('value is not valid JSON: ' + raw) }
31
+ }
32
+ return raw
33
+ }
34
+
35
+ export function setNested(target, path, value) {
36
+ const parts = String(path || '').split('.').filter(Boolean)
37
+ if (!parts.length) throw new Error('set requires a dot path, for example rotationStrategy')
38
+ for (const part of parts) {
39
+ if (part === '__proto__' || part === 'constructor' || part === 'prototype') throw new Error('unsafe path: ' + path)
40
+ }
41
+ let cursor = target
42
+ for (let i = 0; i < parts.length - 1; i += 1) {
43
+ const key = parts[i]
44
+ if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) cursor[key] = {}
45
+ cursor = cursor[key]
46
+ }
47
+ cursor[parts[parts.length - 1]] = value
48
+ return target
49
+ }
50
+
51
+ export function buildRequest(options, env = process.env) {
52
+ const base = String(env.DSH_SUBS_URL || 'http://127.0.0.1:3080/dsh-subscriptions').replace(/\/$/, '')
53
+ const headers = { Accept: 'application/json' }
54
+ if (env.DSH_AUTH_TOKEN) headers.Authorization = 'Bearer ' + env.DSH_AUTH_TOKEN
55
+ if (options.command === 'status') return { url: base + '/status', init: { method: 'GET', headers } }
56
+ if (options.command === 'usage') {
57
+ const params = new URLSearchParams({ type: options.type || 'accounts', format: options.csv ? 'csv' : 'json' })
58
+ return { url: base + '/usage?' + params.toString(), init: { method: 'GET', headers } }
59
+ }
60
+ if (options.command === 'fast') {
61
+ if (!options.sessionId) throw new Error('fast requires a session id: dsh-subscriptions fast <session> [fast|standard]')
62
+ headers['Content-Type'] = 'application/json'
63
+ return {
64
+ url: base + '/speed',
65
+ init: { method: 'POST', headers, body: JSON.stringify({ sessionId: options.sessionId, mode: options.mode || 'fast' }) },
66
+ }
67
+ }
68
+ if (options.command === 'config') return { url: base + '/config', init: { method: 'GET', headers } }
69
+ if (options.command === 'sources') return { url: base + '/sources', init: { method: 'GET', headers } }
70
+ throw new Error('unknown command: ' + options.command)
71
+ }
72
+
73
+ export function formatSources(payload) {
74
+ const lines = []
75
+ const kr = payload && payload.keyRotation
76
+ lines.push('key-rotation: ' + (kr && kr.available ? (kr.healthyKeys + '/' + kr.totalKeys + ' healthy keys') : ('unavailable' + (kr && kr.reason ? ' (' + kr.reason + ')' : ''))))
77
+ for (const row of ((payload && payload.providers) || [])) {
78
+ const availability = (row.candidates || []).map((c) => c.source + '=' + (c.available ? 'yes' : 'no')).join(' ')
79
+ lines.push(row.provider + ': selected=' + (row.selected || 'none') + (availability ? ' ' + availability : ''))
80
+ }
81
+ return lines.join('\n')
82
+ }
83
+
84
+ export function buildSetRequest(options, configPayload, env = process.env) {
85
+ const base = String(env.DSH_SUBS_URL || 'http://127.0.0.1:3080/dsh-subscriptions').replace(/\/$/, '')
86
+ const headers = { Accept: 'application/json', 'Content-Type': 'application/json' }
87
+ if (env.DSH_AUTH_TOKEN) headers.Authorization = 'Bearer ' + env.DSH_AUTH_TOKEN
88
+ const config = configPayload && configPayload.config && typeof configPayload.config === 'object'
89
+ ? JSON.parse(JSON.stringify(configPayload.config))
90
+ : {}
91
+ setNested(config, options.setKey, parseConfigValue(options.setValue))
92
+ return {
93
+ url: base + '/config',
94
+ init: { method: 'PUT', headers, body: JSON.stringify({ config, revision: configPayload && configPayload.revision }) },
95
+ }
96
+ }
97
+
98
+ export async function run(argv = process.argv.slice(2), env = process.env, fetchImpl = fetch) {
99
+ const options = parseArgs(argv)
100
+ if (options.help) return { help: true }
101
+ let request = buildRequest(options.command === 'set' ? { ...options, command: 'config' } : options, env)
102
+ if (options.command === 'set') {
103
+ const current = await fetchImpl(request.url, request.init)
104
+ const text = await current.text()
105
+ if (!current.ok) throw new Error('HTTP ' + current.status + ': ' + text.slice(0, 500))
106
+ request = buildSetRequest(options, JSON.parse(text || '{}'), env)
107
+ }
108
+ const res = await fetchImpl(request.url, request.init)
109
+ const text = await res.text()
110
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
111
+ if (options.command === 'sources' && !options.json) {
112
+ try { return { text: formatSources(JSON.parse(text || '{}')), contentType: 'text/plain' } } catch { /* fall back to raw */ }
113
+ }
114
+ return { text, contentType: res.headers.get('content-type') || '' }
115
+ }
116
+
117
+ async function main() {
118
+ const result = await run()
119
+ 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')
121
+ return
122
+ }
123
+ process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
124
+ }
125
+
126
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
127
+ main().catch((err) => {
128
+ process.stderr.write(String(err && err.message || err) + '\n')
129
+ process.exitCode = 1
130
+ })
131
+ }
package/lib/alerts.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs'
2
+ import { createHmac } from 'node:crypto'
2
3
 
3
4
  let pkgVersion = ''
4
5
  try {
@@ -59,35 +60,66 @@ export function getRecentAlerts(limit = 50) {
59
60
  return alertBuffer.slice(-Math.max(1, Math.min(limit, MAX_ALERTS))).reverse()
60
61
  }
61
62
 
62
- /**
63
- * Dispatches an alert asynchronously to an external webhookUrl if provided.
64
- */
65
- export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch) {
63
+ // #696: signed, retrying webhook delivery plus a digest endpoint payload.
64
+ export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
66
65
  if (!alert || !webhookUrl || typeof webhookUrl !== 'string' || !webhookUrl.startsWith('http')) {
67
66
  return false
68
67
  }
68
+ return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
69
+ }
69
70
 
70
- const payload = {
71
- event: 'subscription_alert',
72
- alert,
73
- source: 'dsh-subscriptions',
74
- sentAt: new Date().toISOString(),
71
+ export function buildNotificationDigest(alerts, { windowMs = 24 * 60 * 60 * 1000, nowMs = Date.now() } = {}) {
72
+ const since = nowMs - Math.max(0, Number(windowMs) || 0)
73
+ const recent = (Array.isArray(alerts) ? alerts : []).filter((a) => a && Number(a.timestamp) >= since)
74
+ const bySeverity = {}
75
+ const byType = {}
76
+ for (const a of recent) {
77
+ bySeverity[a.severity || 'warn'] = (bySeverity[a.severity || 'warn'] || 0) + 1
78
+ byType[a.type || 'unknown'] = (byType[a.type || 'unknown'] || 0) + 1
75
79
  }
80
+ return {
81
+ total: recent.length,
82
+ bySeverity,
83
+ byType,
84
+ since: new Date(since).toISOString(),
85
+ until: new Date(nowMs).toISOString(),
86
+ }
87
+ }
76
88
 
77
- try {
78
- const res = await fetchImpl(webhookUrl, {
79
- method: 'POST',
80
- headers: {
81
- 'content-type': 'application/json',
82
- 'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
83
- },
84
- body: JSON.stringify(payload),
85
- })
86
- return res.ok
87
- } catch {
88
- // Non-blocking fire-and-forget
89
- return false
89
+ export async function dispatchNotificationDigest(alerts, webhookUrl, fetchImpl = fetch, options = {}) {
90
+ if (!webhookUrl) return false
91
+ const digest = buildNotificationDigest(alerts, options)
92
+ return dispatchNotification({ event: 'subscription_digest', digest }, webhookUrl, fetchImpl, options)
93
+ }
94
+
95
+ async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
96
+ const { secret = '', retries = 1, timeoutMs = 5000, sleep } = options || {}
97
+ const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
98
+ const raw = JSON.stringify(payload)
99
+ const headers = {
100
+ 'content-type': 'application/json',
101
+ 'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
102
+ }
103
+ if (secret) headers['x-dsh-signature'] = 'sha256=' + createHmac('sha256', String(secret)).update(raw).digest('hex')
104
+ headers['x-dsh-timestamp'] = payload.sentAt
105
+ const wait = typeof sleep === 'function' ? sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms))
106
+ const attempts = Math.max(0, Number(retries) || 0) + 1
107
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
108
+ try {
109
+ const init = { method: 'POST', headers, body: raw }
110
+ if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
111
+ init.signal = AbortSignal.timeout(timeoutMs)
112
+ }
113
+ const res = await fetchImpl(webhookUrl, init)
114
+ if (res && res.ok) return true
115
+ const status = Number(res && res.status) || 0
116
+ if (status && status < 500) return false
117
+ } catch {
118
+ // retry network errors
119
+ }
120
+ if (attempt + 1 < attempts) await wait(250 * (2 ** attempt))
90
121
  }
122
+ return false
91
123
  }
92
124
 
93
125
  /**
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-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-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')
@@ -1828,7 +1828,21 @@ window.__ModuleLoader__.load({
1828
1828
  h('option', { value: 'hidden' }, t('composerQuotaDisplayHidden')),
1829
1829
  ),
1830
1830
  ),
1831
- h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')),
1831
+ h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')), h('div', { className: 'dsub-row' },
1832
+ h('span', { className: 'dsub-h' }, 'Quota widget'),
1833
+ h('select', {
1834
+ className: 'dsub-select',
1835
+ value: draft.quotaWidget || 'docked',
1836
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { quotaWidget: e.target.value })),
1837
+ },
1838
+ h('option', { value: 'off' }, 'Off'),
1839
+ h('option', { value: 'docked' }, 'Docked'),
1840
+ h('option', { value: 'floating' }, 'Floating (draggable)'),
1841
+ ),
1842
+ ),
1843
+ h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
1844
+ h(SourcesPanel, { t }),
1845
+
1832
1846
  h('div', { className: 'dsub-row' },
1833
1847
  h('span', { className: 'dsub-h' }, t('composerQuota')),
1834
1848
  h('select', {
@@ -3225,6 +3239,8 @@ window.__ModuleLoader__.load({
3225
3239
  expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
3226
3240
  composerQuota: (data && data.composerQuota) || 'off',
3227
3241
  composerQuotaDisplay: (data && data.composerQuotaDisplay) || 'current',
3242
+ quotaWidget: (data && data.quotaWidget) || 'docked',
3243
+ quotaWidgetPosition: (data && data.quotaWidgetPosition) || { right: 16, bottom: 16 },
3228
3244
  active: (data && data.active) || null,
3229
3245
  error: null,
3230
3246
  }
@@ -3639,6 +3655,159 @@ window.__ModuleLoader__.load({
3639
3655
  )
3640
3656
  }
3641
3657
 
3658
+ // #713: read-only source chain panel. Never renders key values.
3659
+ function SourcesPanel() {
3660
+ const [state, setState] = React.useState({ loading: true, data: null, error: null })
3661
+ const pull = () => {
3662
+ fetch('/dsh-subscriptions/sources', { cache: 'no-store' })
3663
+ .then((res) => res.json())
3664
+ .then((data) => setState({ loading: false, data, error: null }))
3665
+ .catch((err) => setState({ loading: false, data: null, error: String(err && err.message || err) }))
3666
+ }
3667
+ React.useEffect(() => {
3668
+ pull()
3669
+ const id = setInterval(pull, 30 * 1000)
3670
+ return () => clearInterval(id)
3671
+ }, [])
3672
+ const sourceLabel = (source) => source === 'api-key' ? 'API key' : (source === 'oauth' ? 'OAuth' : (source === 'ollama' ? 'Ollama' : 'none'))
3673
+ return h('div', { className: 'dsub-section-card' },
3674
+ h('div', { className: 'dsub-section-title' }, 'Sources', h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, state.loading ? '…' : '↻')),
3675
+ h('div', { className: 'dsub-section-desc' }, 'Subscription → API key → Ollama. Read-only: key rotation owns its cooldowns.'),
3676
+ state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
3677
+ state.data && state.data.keyRotation && !state.data.keyRotation.available
3678
+ ? h('div', { className: 'dsub-dim' }, 'key-rotation not installed/offline' + (state.data.keyRotation.reason ? ': ' + state.data.keyRotation.reason : ''))
3679
+ : null,
3680
+ state.data && state.data.keyRotation && state.data.keyRotation.available
3681
+ ? h('div', { className: 'dsub-dim' }, 'key-rotation: ' + state.data.keyRotation.healthyKeys + '/' + state.data.keyRotation.totalKeys + ' healthy keys')
3682
+ : null,
3683
+ (state.data && state.data.providers ? state.data.providers : []).map((row) => h('div', { className: 'dsub-row', key: row.provider },
3684
+ h('span', { className: 'dsub-h' }, row.provider),
3685
+ h('span', { className: 'dsub-pillTag' }, sourceLabel(row.selected)),
3686
+ row.candidates.map((c, i) => h('span', { key: i, className: 'dsub-dim' },
3687
+ sourceLabel(c.source) + (c.available ? ' ✓' : ' ✕'))),
3688
+ )),
3689
+ )
3690
+ }
3691
+
3692
+ // #688: floating/docked quota capsule. Draggable unless docked; opens settings.
3693
+ function QuotaCapsule(props) {
3694
+ const t = (props && props.t) || ((k) => k)
3695
+ const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null })
3696
+ const [pos, setPos] = React.useState({ right: 16, bottom: 16 })
3697
+ const posRef = React.useRef({ right: 16, bottom: 16 })
3698
+ const drag = React.useRef(null)
3699
+ const suppressClick = React.useRef(false)
3700
+ const savePosition = (next) => {
3701
+ fetch('/dsh-subscriptions/config', { cache: 'no-store' })
3702
+ .then((res) => res.json())
3703
+ .then((cfg) => {
3704
+ if (!cfg || !cfg.config) return null
3705
+ return fetch('/dsh-subscriptions/config', {
3706
+ method: 'PUT',
3707
+ headers: { 'content-type': 'application/json' },
3708
+ body: JSON.stringify({
3709
+ config: Object.assign({}, cfg.config, {
3710
+ quotaWidgetPosition: { right: Math.round(next.right), bottom: Math.round(next.bottom) },
3711
+ }),
3712
+ revision: cfg.revision,
3713
+ }),
3714
+ })
3715
+ })
3716
+ .catch(() => {})
3717
+ }
3718
+ React.useEffect(() => {
3719
+ let alive = true
3720
+ const pull = () => {
3721
+ refreshLoggedIn().then(async (r) => {
3722
+ if (!alive) return
3723
+ const mode = r.quotaWidget || 'docked'
3724
+ if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null }); return }
3725
+ const sources = await fetch('/dsh-subscriptions/sources', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
3726
+ let a = r.active
3727
+ const current = currentModelProvider(props)
3728
+ if (current && (!a || a.provider !== current)) {
3729
+ const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
3730
+ const rows = (cfg && cfg.accounts) || []
3731
+ a = rows.find((row) => row && row.provider === current && row.usagePercent != null) || null
3732
+ }
3733
+ if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null }); return }
3734
+ const sourceRow = (sources && Array.isArray(sources.providers) ? sources.providers : []).find((row) => row && row.provider === a.provider)
3735
+ const source = sourceRow ? sourceRow.selected : null
3736
+ if (r.quotaWidgetPosition && typeof window !== 'undefined') {
3737
+ const clamp = (v, max) => Math.max(4, Math.min(max, Number(v) || 0))
3738
+ const next = {
3739
+ right: clamp(r.quotaWidgetPosition.right, window.innerWidth - 40),
3740
+ bottom: clamp(r.quotaWidgetPosition.bottom, window.innerHeight - 40),
3741
+ }
3742
+ posRef.current = next
3743
+ setPos(next)
3744
+ }
3745
+ setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source })
3746
+ }).catch(() => {})
3747
+ }
3748
+ pull()
3749
+ const id = setInterval(pull, 60 * 1000)
3750
+ return () => { alive = false; clearInterval(id) }
3751
+ }, [])
3752
+ if (state.mode === 'off' || !state.active || state.remaining == null) return null
3753
+ const docked = state.mode !== 'floating'
3754
+ const tone = state.remaining <= 10 ? ' dsub-qwFull' : (state.remaining <= 30 ? ' dsub-qwWarn' : '')
3755
+ const openSettings = () => {
3756
+ const clientCtx = props.ctx || window.__dshCtx
3757
+ try {
3758
+ const nav = (clientCtx && clientCtx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
3759
+ if (nav && typeof nav.openBundle === 'function') nav.openBundle(PKG)
3760
+ } catch { /* bestEffort */ }
3761
+ try {
3762
+ const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
3763
+ if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
3764
+ } catch { /* bestEffort */ }
3765
+ }
3766
+ const onPointerDown = (e) => {
3767
+ if (docked) return
3768
+ drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
3769
+ try { e.currentTarget.setPointerCapture(e.pointerId) } catch { /* optional */ }
3770
+ }
3771
+ const onPointerMove = (e) => {
3772
+ const d = drag.current
3773
+ if (!d) return
3774
+ const dx = e.clientX - d.x
3775
+ const dy = e.clientY - d.y
3776
+ if (Math.abs(dx) + Math.abs(dy) > 3) { d.moved = true; suppressClick.current = true }
3777
+ const next = {
3778
+ right: Math.max(4, Math.min(window.innerWidth - 40, d.right - dx)),
3779
+ bottom: Math.max(4, Math.min(window.innerHeight - 40, d.bottom - dy)),
3780
+ }
3781
+ posRef.current = next
3782
+ setPos(next)
3783
+ }
3784
+ const onPointerUp = () => {
3785
+ const wasDrag = drag.current
3786
+ drag.current = null
3787
+ if (!docked && wasDrag && wasDrag.moved) savePosition(posRef.current)
3788
+ }
3789
+ return h('div', {
3790
+ className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
3791
+ style: { right: pos.right + 'px', bottom: pos.bottom + 'px' },
3792
+ role: 'button',
3793
+ tabIndex: 0,
3794
+ title: (state.active.provider || 'subscription') + ' · ' + Math.round(state.remaining) + '% ' + t('subsFree'),
3795
+ 'aria-label': 'Subscription quota ' + Math.round(state.remaining) + '%',
3796
+ onPointerDown,
3797
+ onPointerMove,
3798
+ onPointerUp,
3799
+ onPointerCancel: () => { drag.current = null },
3800
+ onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } openSettings() },
3801
+ onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') openSettings() },
3802
+ },
3803
+ h('span', { className: 'dsub-led ' + ledClass(state.active.usagePercent) }),
3804
+ h('span', { className: 'dsub-qwBrand' }, brandBadge(state.active.provider).icon + ' ' + String(state.active.provider || '').toUpperCase()),
3805
+ state.source ? h('span', { className: 'dsub-pillTag' }, state.source === 'api-key' ? 'KEY' : (state.source === 'oauth' ? 'OAuth' : 'OLLAMA')) : null,
3806
+ h('span', { className: 'dsub-qwBar' }, h('span', { className: 'dsub-qwBarFill', style: { width: state.remaining + '%', display: 'block' } })),
3807
+ h('span', { className: 'dsub-qwPct' }, Math.round(state.remaining) + '%'),
3808
+ )
3809
+ }
3810
+
3642
3811
  function registerSubsPill(ctx) {
3643
3812
  ctx.effect(() => {
3644
3813
  ctx.slots.inject('conversation.session.header.actions', () =>
@@ -3671,6 +3840,22 @@ window.__ModuleLoader__.load({
3671
3840
  }, 'dsh-subscriptions: composer quota')
3672
3841
  }
3673
3842
 
3843
+ function registerQuotaCapsule(ctx) {
3844
+ ctx.effect(() => {
3845
+ ctx.slots.inject('conversation.session.header.utilities', () =>
3846
+ ctx.slots.register(
3847
+ {
3848
+ name: 'conversation.session.header.utilities',
3849
+ id: 'dsh-subscriptions-quota-capsule',
3850
+ order: 30,
3851
+ locale: NS,
3852
+ },
3853
+ () => h(ErrorBoundary, null, h(QuotaCapsule, { t, ctx })),
3854
+ ),
3855
+ )
3856
+ }, 'dsh-subscriptions: quota capsule')
3857
+ }
3858
+
3674
3859
  function registerSlashCommands(ctx) {
3675
3860
  ctx.effect(() => {
3676
3861
  const triggers = ctx.get('inputTriggers')
@@ -3769,6 +3954,7 @@ window.__ModuleLoader__.load({
3769
3954
  registerSettings(ctx)
3770
3955
  registerSubsPill(ctx)
3771
3956
  registerComposerQuota(ctx)
3957
+ registerQuotaCapsule(ctx)
3772
3958
  registerSlashCommands(ctx)
3773
3959
  }
3774
3960
  return module.exports
@@ -97,6 +97,11 @@ export const Config = z.object({
97
97
  .description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
98
98
  composerQuotaDisplay: z.string().volatile().default('current')
99
99
  .description('#682 Status-bar quota scope: current (model provider) or hidden.'),
100
+ quotaWidget: z.string().volatile().default('docked')
101
+ .description('#688 Quota widget placement: off, floating (draggable) or docked.'),
102
+ quotaWidgetPosition: z.object({ right: z.number(), bottom: z.number() }).volatile()
103
+ .default({ right: 16, bottom: 16 })
104
+ .description('#711 Last floating quota widget position in CSS pixels.'),
100
105
  rateLimitWait: z.boolean().volatile().default(true)
101
106
  .description('#684 Report the disclosed reset to dsh-llm-retry instead of parking the account on a guess.'),
102
107
  rateLimitMaxWaitMs: z.number().volatile().default(21600000)
@@ -178,6 +183,8 @@ export const Config = z.object({
178
183
  .description('#562 Remote storage backend: webdav, gist, or s3.'),
179
184
  vaultSyncPassphrase: z.string().role('secret').default('')
180
185
  .description('#562 Passphrase used to encrypt/decrypt the remote vault archive.'),
186
+ vaultSyncProviders: z.array(z.string()).volatile().default([])
187
+ .description('#691 Optional provider allowlist for selective vault sync. Empty means all providers.'),
181
188
  vaultSyncIntervalHours: z.number().volatile().default(24)
182
189
  .description('#562 Automated periodic sync interval in hours (0 disables periodic pull).'),
183
190
  vaultSyncAutoPush: z.boolean().volatile().default(true)
@@ -208,6 +215,8 @@ export const Config = z.object({
208
215
  .description('#562 S3 Secret Access Key.'),
209
216
  customVendors: z.array(z.any()).volatile().default([])
210
217
  .description('Declarative OpenAI-Responses-compatible providers. See README.'),
218
+ providerPacks: z.array(z.any()).volatile().default([])
219
+ .description('#694 Data-only vendor packs registered through the provider SDK.'),
211
220
  })
212
221
 
213
222
  export function publicConfig(cfg) {
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@ import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/i
12
12
  import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
13
13
  import { parseBlob } from './blob.js'
14
14
  import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
15
+ import { registerVendorPack } from './vendor-pack.js'
15
16
  import { registerCustomProviderIds, registerDisplayName } from './refs.js'
16
17
  import { createSubscriptionsService } from './subscriptions.js'
17
18
  import { registerSubscriptionTools } from './native-tools.js'
@@ -105,6 +106,17 @@ export function apply(ctx, rawConfig) {
105
106
  if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors[' + profile.id + ']: ' + String(e && e.message || e))
106
107
  }
107
108
  }
109
+ for (const pack of (live().providerPacks || [])) {
110
+ try {
111
+ registerVendorPack(pack, {
112
+ registerVendor: registerCustomVendor,
113
+ registerProviderId: registerCustomProviderIds,
114
+ registerDisplayName,
115
+ })
116
+ } catch (e) {
117
+ if (logger && logger.warn) logger.warn('[dsh-subscriptions] providerPacks[' + (pack && pack.id) + ']: ' + String(e && e.message || e))
118
+ }
119
+ }
108
120
  }
109
121
  syncCustomVendors()
110
122
 
@@ -0,0 +1,102 @@
1
+ // #709: read-only bridge to @goodandready/dsh-key-rotation.
2
+ // This module never writes cooldown, never rotates a key and never exposes a
3
+ // key value. It only summarises the sanitized status route so the source chain
4
+ // can show which pool would be used next.
5
+ export const KEY_ROTATION_STATUS_PATH = '/dsh-key-rotation/status'
6
+
7
+ export function summarizeKeyRotationStatus(raw) {
8
+ if (!raw || !Array.isArray(raw.providers)) return { available: false, reason: 'invalid status', providers: [], totalKeys: 0, healthyKeys: 0 }
9
+ let totalKeys = 0
10
+ let healthyKeys = 0
11
+ const providers = raw.providers.map((p) => {
12
+ const keys = (Array.isArray(p && p.keys) ? p.keys : [])
13
+ .filter((k) => k && k.present)
14
+ .map((k) => {
15
+ const cooldownMsLeft = Math.max(0, Number(k.cooldownMsLeft) || 0)
16
+ const paused = Boolean(k.paused)
17
+ const revoked = Boolean(k.revoked)
18
+ const broken = Boolean(k.broken)
19
+ const healthy = !paused && !revoked && !broken && cooldownMsLeft === 0
20
+ return {
21
+ ref: String(k.ref || ''),
22
+ active: Boolean(k.active),
23
+ cooldownMsLeft,
24
+ paused,
25
+ revoked,
26
+ broken,
27
+ healthy,
28
+ expiresAt: k.expiresAt == null ? null : Number(k.expiresAt),
29
+ }
30
+ })
31
+ totalKeys += keys.length
32
+ healthyKeys += keys.filter((k) => k.healthy).length
33
+ return {
34
+ provider: String((p && (p.providerName || p.provider)) || ''),
35
+ base: String((p && p.provider) || ''),
36
+ healthyKeys: keys.filter((k) => k.healthy).length,
37
+ totalKeys: keys.length,
38
+ keys,
39
+ }
40
+ }).filter((p) => p.provider)
41
+ return { available: true, providers, totalKeys, healthyKeys }
42
+ }
43
+
44
+ export async function readKeyRotationStatus({ baseUrl, path = KEY_ROTATION_STATUS_PATH, fetchImpl = fetch, timeoutMs = 2000 } = {}) {
45
+ if (!baseUrl) return { available: false, reason: 'missing base url', providers: [], totalKeys: 0, healthyKeys: 0 }
46
+ try {
47
+ const init = { method: 'GET', headers: { accept: 'application/json' } }
48
+ if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
49
+ init.signal = AbortSignal.timeout(timeoutMs)
50
+ }
51
+ const res = await fetchImpl(String(baseUrl).replace(/\/$/, '') + path, init)
52
+ if (!res || !res.ok) return { available: false, reason: 'HTTP ' + (res && res.status), providers: [], totalKeys: 0, healthyKeys: 0 }
53
+ const raw = await res.json()
54
+ return summarizeKeyRotationStatus(raw)
55
+ } catch (err) {
56
+ return { available: false, reason: String(err && err.message || err), providers: [], totalKeys: 0, healthyKeys: 0 }
57
+ }
58
+ }
59
+
60
+ const SOURCE_ORDER = ['oauth', 'api-key', 'ollama']
61
+
62
+ export function buildSourceChain({ subscriptions = [], keyRotation = null, ollama = { available: false } } = {}) {
63
+ const providers = new Map()
64
+ const entry = (provider) => {
65
+ const key = String(provider || '')
66
+ if (!providers.has(key)) providers.set(key, { provider: key, candidates: [] })
67
+ return providers.get(key)
68
+ }
69
+ for (const sub of (Array.isArray(subscriptions) ? subscriptions : [])) {
70
+ if (!sub || !sub.provider) continue
71
+ entry(sub.provider).candidates.push({
72
+ source: 'oauth',
73
+ available: Boolean(sub.loggedIn) && !(Number(sub.cooldownUntil) > Date.now()),
74
+ detail: {
75
+ index: sub.index == null ? null : Number(sub.index),
76
+ label: sub.label || null,
77
+ usagePercent: sub.usagePercent == null ? null : Number(sub.usagePercent),
78
+ cooldownUntil: sub.cooldownUntil == null ? null : Number(sub.cooldownUntil),
79
+ },
80
+ })
81
+ }
82
+ if (keyRotation && keyRotation.available) {
83
+ for (const pool of (keyRotation.providers || [])) {
84
+ if (!pool || !pool.provider) continue
85
+ entry(pool.provider).candidates.push({
86
+ source: 'api-key',
87
+ available: Number(pool.healthyKeys) > 0,
88
+ detail: { healthyKeys: Number(pool.healthyKeys) || 0, totalKeys: Number(pool.totalKeys) || 0, keys: pool.keys || [] },
89
+ })
90
+ }
91
+ }
92
+ if (ollama && ollama.available) {
93
+ const target = entry('ollama')
94
+ target.candidates.push({ source: 'ollama', available: true, detail: { model: ollama.model || null } })
95
+ }
96
+ const rows = [...providers.values()].map((row) => {
97
+ row.candidates.sort((a, b) => SOURCE_ORDER.indexOf(a.source) - SOURCE_ORDER.indexOf(b.source))
98
+ const selected = row.candidates.find((c) => c.available) || null
99
+ return { ...row, selected: selected ? selected.source : null }
100
+ })
101
+ return { order: SOURCE_ORDER.slice(), noDoubleRotation: true, providers: rows }
102
+ }
@@ -0,0 +1,52 @@
1
+ import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
2
+ import { buildSourceChain, readKeyRotationStatus } from '../key-rotation-bridge.js'
3
+
4
+ export function registerSourcesRoutes(ctx, state) {
5
+ const { accountsView, live } = state
6
+
7
+ ctx.effect(() => ctx.webServer.register({
8
+ kind: 'exact',
9
+ path: '/dsh-subscriptions/sources',
10
+ handler: safeJsonHandler(async (req, res) => {
11
+ if (req.method !== 'GET') {
12
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET 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
+ let subscriptions = []
20
+ try {
21
+ const rows = await accountsView()
22
+ subscriptions = (rows || []).map((row) => ({
23
+ provider: row.provider,
24
+ index: row.index,
25
+ label: row.label,
26
+ loggedIn: Boolean(row.hasToken),
27
+ usagePercent: row.usagePercent,
28
+ cooldownUntil: row.cooldownUntil,
29
+ }))
30
+ } catch { /* keep the chain available */ }
31
+ const port = req.socket && req.socket.localPort
32
+ const keyRotation = port
33
+ ? await readKeyRotationStatus({ baseUrl: 'http://127.0.0.1:' + port })
34
+ : { available: false, reason: 'no local port', providers: [], totalKeys: 0, healthyKeys: 0 }
35
+ const chain = buildSourceChain({
36
+ subscriptions,
37
+ keyRotation,
38
+ ollama: { available: live().ollamaFallback !== false },
39
+ })
40
+ writeJson(res, 200, {
41
+ ok: true,
42
+ keyRotation: {
43
+ available: keyRotation.available,
44
+ reason: keyRotation.reason || null,
45
+ totalKeys: keyRotation.totalKeys,
46
+ healthyKeys: keyRotation.healthyKeys,
47
+ },
48
+ ...chain,
49
+ })
50
+ }),
51
+ }), 'dsh-subscriptions: /sources')
52
+ }
@@ -259,6 +259,8 @@ export function registerStatusRoutes(ctx, state) {
259
259
  expiryNotifyDays: live().expiryNotifyDays,
260
260
  fastMode: !!live().codexFastMode,
261
261
  composerQuota: String(live().composerQuota || 'off'), composerQuotaDisplay: String(live().composerQuotaDisplay || 'current'),
262
+ quotaWidget: String(live().quotaWidget || 'docked'),
263
+ quotaWidgetPosition: live().quotaWidgetPosition || { right: 16, bottom: 16 },
262
264
  active,
263
265
  })
264
266
  }),
package/lib/routes.js CHANGED
@@ -5,6 +5,7 @@ import { registerStatusRoutes } from './routes/status.js'
5
5
  import { registerOauthRoutes } from './routes/oauth.js'
6
6
  import { registerAccountsRoutes } from './routes/accounts.js'
7
7
  import { registerProxyRoutes } from './routes/proxy.js'
8
+ import { registerSourcesRoutes } from './routes/sources.js'
8
9
  import { registerPluginUpdater } from './updater.js'
9
10
 
10
11
  export function registerRoutes(ctx, state) {
@@ -109,4 +110,5 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&amp;').replace(/</g,'
109
110
  registerOauthRoutes(ctx, state)
110
111
  registerAccountsRoutes(ctx, state)
111
112
  registerProxyRoutes(ctx, state)
113
+ registerSourcesRoutes(ctx, state)
112
114
  }
package/lib/vault-sync.js CHANGED
@@ -244,6 +244,40 @@ export async function pullFromS3({ endpoint, bucket, key = 'dsh-subscriptions-va
244
244
  return res.text()
245
245
  }
246
246
 
247
+ // #691: v2 sync primitives — deterministic slot merge, encrypted snapshots and
248
+ // provider-level selection. Remote wins on conflicts, but conflicting rows are
249
+ // reported so the caller can surface them instead of silently dropping local state.
250
+ export function mergeVaultSlots(localSlots, remoteSlots) {
251
+ const out = (Array.isArray(localSlots) ? localSlots : []).map((slot) => ({ ...slot }))
252
+ const keyOf = (slot) => slot && (slot.ref || (slot.provider + ':' + (slot.index == null ? 1 : slot.index)))
253
+ const indexByKey = new Map(out.map((slot, idx) => [keyOf(slot), idx]))
254
+ const conflicts = []
255
+ for (const remote of (Array.isArray(remoteSlots) ? remoteSlots : [])) {
256
+ if (!remote) continue
257
+ const key = keyOf(remote)
258
+ const idx = indexByKey.get(key)
259
+ if (idx == null) {
260
+ out.push({ ...remote })
261
+ indexByKey.set(key, out.length - 1)
262
+ continue
263
+ }
264
+ if (JSON.stringify(out[idx]) !== JSON.stringify(remote)) conflicts.push({ key, local: out[idx], remote })
265
+ out[idx] = { ...out[idx], ...remote }
266
+ }
267
+ return { slots: out, conflicts }
268
+ }
269
+
270
+ export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '') {
271
+ if (!passphrase) throw new Error('passphrase is required for a vault snapshot')
272
+ const { vault, slotCount, blobCount } = exportVault({ slots: slots || [], blobs }, passphrase)
273
+ return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
274
+ }
275
+
276
+ export function restoreVaultSnapshot(snapshot, passphrase) {
277
+ if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
278
+ return importVault(snapshot.vault, passphrase)
279
+ }
280
+
247
281
  // --- Sync Coordination: Push & Pull ---
248
282
 
249
283
  export async function syncVaultPush({ live, store, fetchFn = fetch }) {
@@ -258,7 +292,9 @@ export async function syncVaultPush({ live, store, fetchFn = fetch }) {
258
292
  syncStatus.lastPushStatus = 'running'
259
293
 
260
294
  try {
261
- const slots = cfg.slots || []
295
+ const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
296
+ const allSlots = cfg.slots || []
297
+ const slots = providerFilter.size ? allSlots.filter((slot) => providerFilter.has(String(slot && slot.provider))) : allSlots
262
298
  const blobs = {}
263
299
  for (const slot of normalizeSlots(slots)) {
264
300
  try {
@@ -366,26 +402,27 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
366
402
  }
367
403
 
368
404
  const result = importVault(encryptedData, passphrase)
369
-
370
- // Merge slots into live configuration
371
- if (result.slots.length && settingsApi) {
405
+ const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
406
+ const remoteSlots = providerFilter.size
407
+ ? result.slots.filter((slot) => providerFilter.has(String(slot && slot.provider)))
408
+ : result.slots
409
+ const allowedRefs = new Set(remoteSlots.map((slot) => slot && slot.ref))
410
+ const remoteBlobs = Object.fromEntries(Object.entries(result.blobs || {}).filter(([ref]) =>
411
+ !providerFilter.size || allowedRefs.has(ref)))
412
+
413
+ // Merge slots into live configuration; keep local-only rows and report conflicts.
414
+ let conflicts = []
415
+ if (remoteSlots.length && settingsApi) {
372
416
  const revision = revisionOf(settingsApi)
373
- const curSlots = Array.isArray(cfg.slots) ? cfg.slots.slice() : []
374
- for (const s of result.slots) {
375
- const idx = curSlots.findIndex((c) => c.provider === s.provider && Number(c.index) === Number(s.index))
376
- if (idx >= 0) {
377
- curSlots[idx] = { ...curSlots[idx], ...s }
378
- } else {
379
- curSlots.push(s)
380
- }
381
- }
382
- const parsed = plainConfig(Config({ ...cfg, slots: curSlots }))
417
+ const merged = mergeVaultSlots(Array.isArray(cfg.slots) ? cfg.slots.slice() : [], remoteSlots)
418
+ conflicts = merged.conflicts
419
+ const parsed = plainConfig(Config({ ...cfg, slots: merged.slots }))
383
420
  await settingsApi.replace(parsed, revision)
384
421
  }
385
422
 
386
423
  // Save blobs into store
387
424
  let blobCount = 0
388
- for (const [ref, blob] of Object.entries(result.blobs)) {
425
+ for (const [ref, blob] of Object.entries(remoteBlobs)) {
389
426
  await store.saveBlob(ref, validateCredentialBlob(blob))
390
427
  blobCount++
391
428
  }
@@ -403,6 +440,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
403
440
  backend,
404
441
  slotCount: result.slots.length,
405
442
  blobCount,
443
+ conflicts: conflicts.length,
406
444
  exportedAt: result.exportedAt,
407
445
  timestamp: syncStatus.lastPullAt,
408
446
  }
@@ -0,0 +1,36 @@
1
+ import { validateProfile, createVendorFromProfile } from './vendor-factory.js'
2
+
3
+ /**
4
+ * A vendor pack is a data-only provider declaration. It reuses the declarative
5
+ * profile contract but also carries the built vendor object, so a pack can be
6
+ * registered by another plugin without touching providers/vendors/cascade.
7
+ */
8
+ export function normalizeVendorPack(raw) {
9
+ const pack = raw && typeof raw === 'object' ? raw : {}
10
+ const profile = validateProfile({
11
+ id: pack.id,
12
+ displayName: pack.displayName,
13
+ authUrl: pack.authUrl,
14
+ tokenUrl: pack.tokenUrl,
15
+ baseUrl: pack.baseUrl,
16
+ scope: pack.scope,
17
+ tokenStyle: pack.tokenStyle,
18
+ clientId: pack.clientId,
19
+ redirectUri: pack.redirectUri,
20
+ modelsPath: pack.modelsPath,
21
+ models: pack.models,
22
+ headers: pack.headers,
23
+ })
24
+ return { ...profile, vendor: createVendorFromProfile(profile) }
25
+ }
26
+
27
+ export function registerVendorPack(raw, registry = {}) {
28
+ const pack = normalizeVendorPack(raw)
29
+ if (typeof registry.registerVendor !== 'function') throw new Error('vendor pack registry is unavailable')
30
+ registry.registerVendor(pack.vendor)
31
+ if (typeof registry.registerProviderId === 'function') registry.registerProviderId([pack.id])
32
+ if (typeof registry.registerDisplayName === 'function' && pack.displayName) {
33
+ registry.registerDisplayName(pack.id, pack.displayName)
34
+ }
35
+ return pack
36
+ }
@@ -0,0 +1,8 @@
1
+ # Headless/TUI overlay for a profile that has no web UI.
2
+ # Usage: dsh --profile headless --patch /absolute/path/overlay.example.yml "your task"
3
+ - id: dsh-subscriptions
4
+ name: '@goodandready/dsh-subscriptions'
5
+ config:
6
+ composerQuota: percent
7
+ composerQuotaDisplay: current
8
+ rotationStrategy: least-loaded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",
@@ -17,7 +17,9 @@
17
17
  "README.md",
18
18
  "README.ru.md",
19
19
  "README.zh.md",
20
- "LICENSE"
20
+ "LICENSE",
21
+ "bin/",
22
+ "overlay.example.yml"
21
23
  ],
22
24
  "keywords": [
23
25
  "dsh",
@@ -50,6 +52,32 @@
50
52
  "@deepseek-ai/dsh-client-locale",
51
53
  "@deepseek-ai/dsh-client-ui-slots"
52
54
  ]
55
+ },
56
+ "market": {
57
+ "name": {
58
+ "en": "Subscriptions",
59
+ "zh": "订阅管理"
60
+ },
61
+ "description": {
62
+ "en": "Use ChatGPT, Claude, Grok, Antigravity and 12 more subscriptions as DSH providers with account pools, quota tools and OAuth.",
63
+ "zh": "将 ChatGPT、Claude、Grok、Antigravity 等 16 种订阅接入 DSH,支持账户池、额度工具和 OAuth。"
64
+ },
65
+ "category": "providers",
66
+ "icon": "subscriptions",
67
+ "tags": [
68
+ "subscriptions",
69
+ "oauth",
70
+ "rotation",
71
+ "quota",
72
+ "providers",
73
+ "media"
74
+ ],
75
+ "defaultProviders": [
76
+ "codex",
77
+ "claude",
78
+ "grok",
79
+ "antigravity"
80
+ ]
53
81
  }
54
82
  },
55
83
  "peerDependencies": {
@@ -77,5 +105,8 @@
77
105
  "@types/node": "^22.20.2",
78
106
  "eslint": "^10.4.0",
79
107
  "typescript": "^7.0.2"
108
+ },
109
+ "bin": {
110
+ "dsh-subscriptions": "./bin/dsh-subscriptions.mjs"
80
111
  }
81
112
  }