@goodandready/dsh-subscriptions 0.7.6 → 0.7.8
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 +27 -2
- package/lib/alerts.js +11 -3
- package/lib/client.js +107 -9
- package/lib/config-schema.js +2 -0
- package/lib/index.js +3 -2
- package/lib/routes/vault.js +88 -0
- package/lib/routes/webhook.js +47 -0
- package/lib/routes.js +4 -0
- package/lib/vault-snapshots.js +43 -0
- package/lib/vault-sync.js +6 -0
- package/lib/webhook-url.js +27 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
6
6
|
export function parseArgs(argv) {
|
|
7
7
|
const args = Array.isArray(argv) ? argv.slice() : []
|
|
8
8
|
const command = args.shift() || 'status'
|
|
9
|
-
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '' }
|
|
9
|
+
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '', vaultAction: '', vaultArg: '' }
|
|
10
10
|
for (let i = 0; i < args.length; i += 1) {
|
|
11
11
|
const arg = args[i]
|
|
12
12
|
if (arg === '--csv') options.csv = true
|
|
@@ -18,6 +18,8 @@ export function parseArgs(argv) {
|
|
|
18
18
|
else if (command === 'set') options.setValue = arg
|
|
19
19
|
else if (command === 'packs' && arg === 'add') options.packAction = 'add'
|
|
20
20
|
else if (command === 'packs' && options.packAction === 'add' && !options.packFile) options.packFile = arg
|
|
21
|
+
else if (command === 'vault' && !options.vaultAction) options.vaultAction = arg
|
|
22
|
+
else if (command === 'vault' && !options.vaultArg) options.vaultArg = arg
|
|
21
23
|
else if (command === 'fast' && !options.sessionId) options.sessionId = arg
|
|
22
24
|
else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
|
|
23
25
|
}
|
|
@@ -72,6 +74,15 @@ export function buildRequest(options, env = process.env) {
|
|
|
72
74
|
if (options.command === 'sources') return { url: base + '/sources', init: { method: 'GET', headers } }
|
|
73
75
|
if (options.command === 'packs') return { url: base + '/config', init: { method: 'GET', headers } }
|
|
74
76
|
if (options.command === 'diagnostics') return { url: base + '/diagnostics', init: { method: 'GET', headers } }
|
|
77
|
+
if (options.command === 'vault') {
|
|
78
|
+
if (options.vaultAction === 'restore') {
|
|
79
|
+
return { url: base + '/vault/snapshots/restore', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: options.vaultArg }) } }
|
|
80
|
+
}
|
|
81
|
+
if (options.vaultAction === 'snapshot') {
|
|
82
|
+
return { url: base + '/vault/snapshots', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ label: options.vaultArg || 'manual' }) } }
|
|
83
|
+
}
|
|
84
|
+
return { url: base + '/vault/snapshots', init: { method: 'GET', headers } }
|
|
85
|
+
}
|
|
75
86
|
throw new Error('unknown command: ' + options.command)
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -112,6 +123,12 @@ export function formatPacks(packs) {
|
|
|
112
123
|
return JSON.stringify((Array.isArray(packs) ? packs : []).map(sanitizePack), null, 2) + '\\n'
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
export function formatSnapshots(payload) {
|
|
127
|
+
const rows = (payload && payload.snapshots) || []
|
|
128
|
+
if (!rows.length) return 'no vault snapshots'
|
|
129
|
+
return rows.map((s) => s.id + ' ' + s.createdAt + ' ' + (s.label || '-') + ' ' + s.slotCount + ' slots').join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
export function mergeProviderPack(packs, pack) {
|
|
116
133
|
const list = (Array.isArray(packs) ? packs : []).filter((p) => p && p.id !== pack.id)
|
|
117
134
|
return [...list, pack]
|
|
@@ -197,6 +214,14 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
197
214
|
const res = await fetchImpl(request.url, request.init)
|
|
198
215
|
const text = await res.text()
|
|
199
216
|
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
|
|
217
|
+
if (options.command === 'vault' && !options.json) {
|
|
218
|
+
try {
|
|
219
|
+
const payload = JSON.parse(text || '{}')
|
|
220
|
+
if (options.vaultAction === 'snapshots' || !options.vaultAction) return { text: formatSnapshots(payload), contentType: 'text/plain' }
|
|
221
|
+
if (options.vaultAction === 'snapshot') return { text: 'created ' + (payload.snapshot && payload.snapshot.id), contentType: 'text/plain' }
|
|
222
|
+
if (options.vaultAction === 'restore') return { text: 'restored ' + (payload.slotCount || 0) + ' slots', contentType: 'text/plain' }
|
|
223
|
+
} catch { /* fall back to raw */ }
|
|
224
|
+
}
|
|
200
225
|
if (options.command === 'packs' && options.packAction !== 'add') {
|
|
201
226
|
try {
|
|
202
227
|
const payload = JSON.parse(text || '{}')
|
|
@@ -212,7 +237,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
212
237
|
async function main() {
|
|
213
238
|
const result = await run()
|
|
214
239
|
if (result.help) {
|
|
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')
|
|
240
|
+
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|vault|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 vault snapshots | vault snapshot [label] | vault restore <id>\n doctor [--json]\n')
|
|
216
241
|
return
|
|
217
242
|
}
|
|
218
243
|
process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
|
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 {
|
|
@@ -14,10 +15,15 @@ try {
|
|
|
14
15
|
|
|
15
16
|
const MAX_ALERTS = 100
|
|
16
17
|
let defaultWebhookSecret = ''
|
|
18
|
+
let defaultAllowPrivate = false
|
|
17
19
|
|
|
18
20
|
export function setWebhookSecret(secret) {
|
|
19
21
|
defaultWebhookSecret = String(secret || '')
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
export function setWebhookAllowPrivate(allow) {
|
|
25
|
+
defaultAllowPrivate = Boolean(allow)
|
|
26
|
+
}
|
|
21
27
|
const DEDUP_WINDOW_MS = 10 * 60 * 1000 // 10 minutes cooldown per (ref + type)
|
|
22
28
|
|
|
23
29
|
const alertBuffer = []
|
|
@@ -67,9 +73,7 @@ export function getRecentAlerts(limit = 50) {
|
|
|
67
73
|
|
|
68
74
|
// #696: signed, retrying webhook delivery plus a digest endpoint payload.
|
|
69
75
|
export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
|
|
70
|
-
if (!alert || !webhookUrl
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
76
|
+
if (!alert || !webhookUrl) return false
|
|
73
77
|
return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -101,6 +105,10 @@ async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
|
|
|
101
105
|
const opts = options || {}
|
|
102
106
|
const { retries = 1, timeoutMs = 5000, sleep } = opts
|
|
103
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
|
|
104
112
|
const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
|
|
105
113
|
const raw = JSON.stringify(payload)
|
|
106
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')
|
|
@@ -1843,6 +1843,7 @@ window.__ModuleLoader__.load({
|
|
|
1843
1843
|
h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
|
|
1844
1844
|
h(SourcesPanel, { t }),
|
|
1845
1845
|
h(ProviderPacksPanel),
|
|
1846
|
+
h(VaultSnapshotsPanel),
|
|
1846
1847
|
|
|
1847
1848
|
h('div', { className: 'dsub-row' },
|
|
1848
1849
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
@@ -1907,6 +1908,7 @@ window.__ModuleLoader__.load({
|
|
|
1907
1908
|
}),
|
|
1908
1909
|
),
|
|
1909
1910
|
h('div', { className: 'dsub-sub' }, '0 disables digests. Requires a Webhook URL.'),
|
|
1911
|
+
h(WebhookTestButton),
|
|
1910
1912
|
),
|
|
1911
1913
|
h('details', { className: 'dsub-group', open: true },
|
|
1912
1914
|
h('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
|
|
@@ -3690,6 +3692,69 @@ window.__ModuleLoader__.load({
|
|
|
3690
3692
|
)
|
|
3691
3693
|
}
|
|
3692
3694
|
|
|
3695
|
+
function WebhookTestButton() {
|
|
3696
|
+
const [state, setState] = React.useState({ busy: false, message: '', error: null })
|
|
3697
|
+
const send = () => {
|
|
3698
|
+
setState({ busy: true, message: '', error: null })
|
|
3699
|
+
fetch('/dsh-subscriptions/webhook/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
3700
|
+
.then(async (res) => {
|
|
3701
|
+
const data = await res.json().catch(() => ({}))
|
|
3702
|
+
if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3703
|
+
setState({ busy: false, message: 'delivered', error: null })
|
|
3704
|
+
})
|
|
3705
|
+
.catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
|
|
3706
|
+
}
|
|
3707
|
+
return h('div', { className: 'dsub-row' },
|
|
3708
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : 'Send test'),
|
|
3709
|
+
state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
|
|
3710
|
+
state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
|
|
3711
|
+
)
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// #736: vault snapshot list with create/restore. Payload stays server-side.
|
|
3715
|
+
function VaultSnapshotsPanel() {
|
|
3716
|
+
const [state, setState] = React.useState({ snapshots: [], message: '', error: null, busy: false })
|
|
3717
|
+
const pull = () => {
|
|
3718
|
+
fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
|
|
3719
|
+
.then((res) => res.json())
|
|
3720
|
+
.then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], error: null })))
|
|
3721
|
+
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
3722
|
+
}
|
|
3723
|
+
React.useEffect(() => { pull() }, [])
|
|
3724
|
+
const call = (url, body) => fetch(url, {
|
|
3725
|
+
method: 'POST',
|
|
3726
|
+
headers: { 'content-type': 'application/json' },
|
|
3727
|
+
body: JSON.stringify(body || {}),
|
|
3728
|
+
}).then(async (res) => {
|
|
3729
|
+
const data = await res.json().catch(() => ({}))
|
|
3730
|
+
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3731
|
+
return data
|
|
3732
|
+
})
|
|
3733
|
+
const create = () => {
|
|
3734
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3735
|
+
call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
|
|
3736
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'created ' + data.snapshot.id })); pull() })
|
|
3737
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3738
|
+
}
|
|
3739
|
+
const restore = (id) => {
|
|
3740
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3741
|
+
call('/dsh-subscriptions/vault/snapshots/restore', { id })
|
|
3742
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'restored ' + data.slotCount + ' slots' })); pull() })
|
|
3743
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3744
|
+
}
|
|
3745
|
+
return h('div', { className: 'dsub-section-card' },
|
|
3746
|
+
h('div', { className: 'dsub-section-title' }, 'Vault snapshots', h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : 'Create snapshot')),
|
|
3747
|
+
h('div', { className: 'dsub-section-desc' }, 'Encrypted slots snapshots. A pre-pull snapshot is created automatically. Payload stays on the host.'),
|
|
3748
|
+
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
3749
|
+
h('span', { className: 'dsub-h' }, snap.id),
|
|
3750
|
+
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
3751
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, 'Restore'),
|
|
3752
|
+
)) : h('div', { className: 'dsub-dim' }, 'No snapshots yet.'),
|
|
3753
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3754
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3755
|
+
)
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3693
3758
|
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
3694
3759
|
function ProviderPacksPanel() {
|
|
3695
3760
|
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
@@ -3800,8 +3865,10 @@ window.__ModuleLoader__.load({
|
|
|
3800
3865
|
// #688: floating/docked quota capsule. Draggable unless docked; opens settings.
|
|
3801
3866
|
function QuotaCapsule(props) {
|
|
3802
3867
|
const t = (props && props.t) || ((k) => k)
|
|
3803
|
-
const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null })
|
|
3868
|
+
const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null, candidates: [] })
|
|
3869
|
+
const [open, setOpen] = React.useState(false)
|
|
3804
3870
|
const [pos, setPos] = React.useState({ right: 16, bottom: 16 })
|
|
3871
|
+
const popRef = React.useRef(null)
|
|
3805
3872
|
const posRef = React.useRef({ right: 16, bottom: 16 })
|
|
3806
3873
|
const drag = React.useRef(null)
|
|
3807
3874
|
const suppressClick = React.useRef(false)
|
|
@@ -3829,7 +3896,7 @@ window.__ModuleLoader__.load({
|
|
|
3829
3896
|
refreshLoggedIn().then(async (r) => {
|
|
3830
3897
|
if (!alive) return
|
|
3831
3898
|
const mode = r.quotaWidget || 'docked'
|
|
3832
|
-
if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null }); return }
|
|
3899
|
+
if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
|
|
3833
3900
|
const sources = await fetch('/dsh-subscriptions/sources', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
|
|
3834
3901
|
let a = r.active
|
|
3835
3902
|
const current = currentModelProvider(props)
|
|
@@ -3838,9 +3905,10 @@ window.__ModuleLoader__.load({
|
|
|
3838
3905
|
const rows = (cfg && cfg.accounts) || []
|
|
3839
3906
|
a = rows.find((row) => row && row.provider === current && row.usagePercent != null) || null
|
|
3840
3907
|
}
|
|
3841
|
-
if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null }); return }
|
|
3908
|
+
if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
|
|
3842
3909
|
const sourceRow = (sources && Array.isArray(sources.providers) ? sources.providers : []).find((row) => row && row.provider === a.provider)
|
|
3843
3910
|
const source = sourceRow ? sourceRow.selected : null
|
|
3911
|
+
const candidates = sourceRow && Array.isArray(sourceRow.candidates) ? sourceRow.candidates : []
|
|
3844
3912
|
if (r.quotaWidgetPosition && typeof window !== 'undefined') {
|
|
3845
3913
|
const clamp = (v, max) => Math.max(4, Math.min(max, Number(v) || 0))
|
|
3846
3914
|
const next = {
|
|
@@ -3850,7 +3918,7 @@ window.__ModuleLoader__.load({
|
|
|
3850
3918
|
posRef.current = next
|
|
3851
3919
|
setPos(next)
|
|
3852
3920
|
}
|
|
3853
|
-
setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source })
|
|
3921
|
+
setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source, candidates })
|
|
3854
3922
|
}).catch(() => {})
|
|
3855
3923
|
}
|
|
3856
3924
|
pull()
|
|
@@ -3871,6 +3939,16 @@ window.__ModuleLoader__.load({
|
|
|
3871
3939
|
if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
|
|
3872
3940
|
} catch { /* bestEffort */ }
|
|
3873
3941
|
}
|
|
3942
|
+
React.useEffect(() => {
|
|
3943
|
+
if (!open) return undefined
|
|
3944
|
+
const onDoc = (e) => {
|
|
3945
|
+
if (popRef.current && !popRef.current.contains(e.target) && !(e.target.closest && e.target.closest('.dsub-qw'))) setOpen(false)
|
|
3946
|
+
}
|
|
3947
|
+
const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
3948
|
+
document.addEventListener('pointerdown', onDoc)
|
|
3949
|
+
document.addEventListener('keydown', onKey)
|
|
3950
|
+
return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey) }
|
|
3951
|
+
}, [open])
|
|
3874
3952
|
const onPointerDown = (e) => {
|
|
3875
3953
|
if (docked) return
|
|
3876
3954
|
drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
|
|
@@ -3894,7 +3972,27 @@ window.__ModuleLoader__.load({
|
|
|
3894
3972
|
drag.current = null
|
|
3895
3973
|
if (!docked && wasDrag && wasDrag.moved) savePosition(posRef.current)
|
|
3896
3974
|
}
|
|
3897
|
-
|
|
3975
|
+
const popover = open ? h('div', {
|
|
3976
|
+
ref: popRef,
|
|
3977
|
+
className: 'dsub-qwPop',
|
|
3978
|
+
style: { right: pos.right + 'px', bottom: (pos.bottom + 38) + 'px' },
|
|
3979
|
+
role: 'dialog',
|
|
3980
|
+
'aria-label': 'Quota sources',
|
|
3981
|
+
},
|
|
3982
|
+
h('div', { className: 'dsub-h' }, String(state.active.provider || '').toUpperCase() + ' sources'),
|
|
3983
|
+
(state.candidates || []).map((c, i) => {
|
|
3984
|
+
const label = c.source === 'api-key' ? 'API key' : (c.source === 'oauth' ? 'OAuth' : 'Ollama')
|
|
3985
|
+
const cd = c.detail && Array.isArray(c.detail.keys) ? Math.max(0, ...c.detail.keys.map((k) => Number(k.cooldownMsLeft) || 0)) : 0
|
|
3986
|
+
return h('div', { className: 'dsub-row', key: i },
|
|
3987
|
+
h('span', { className: 'dsub-h' }, label),
|
|
3988
|
+
h('span', { className: c.available ? 'dsub-ok' : 'dsub-dim' }, c.available ? 'available' : 'unavailable'),
|
|
3989
|
+
cd > 0 ? h('span', { className: 'dsub-dim' }, 'cooldown ' + Math.ceil(cd / 1000) + 's') : null,
|
|
3990
|
+
c.detail && c.detail.healthyKeys != null ? h('span', { className: 'dsub-dim' }, c.detail.healthyKeys + '/' + c.detail.totalKeys + ' keys') : null,
|
|
3991
|
+
)
|
|
3992
|
+
}),
|
|
3993
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setOpen(false); openSettings() } }, 'Open settings'),
|
|
3994
|
+
) : null
|
|
3995
|
+
return h(React.Fragment, null, h('div', {
|
|
3898
3996
|
className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
|
|
3899
3997
|
style: { right: pos.right + 'px', bottom: pos.bottom + 'px' },
|
|
3900
3998
|
role: 'button',
|
|
@@ -3905,15 +4003,15 @@ window.__ModuleLoader__.load({
|
|
|
3905
4003
|
onPointerMove,
|
|
3906
4004
|
onPointerUp,
|
|
3907
4005
|
onPointerCancel: () => { drag.current = null },
|
|
3908
|
-
onClick: () => { if (suppressClick.current) { suppressClick.current = false; return }
|
|
3909
|
-
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
4006
|
+
onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } setOpen((o) => !o) },
|
|
4007
|
+
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') setOpen((o) => !o) },
|
|
3910
4008
|
},
|
|
3911
4009
|
h('span', { className: 'dsub-led ' + ledClass(state.active.usagePercent) }),
|
|
3912
4010
|
h('span', { className: 'dsub-qwBrand' }, brandBadge(state.active.provider).icon + ' ' + String(state.active.provider || '').toUpperCase()),
|
|
3913
4011
|
state.source ? h('span', { className: 'dsub-pillTag' }, state.source === 'api-key' ? 'KEY' : (state.source === 'oauth' ? 'OAuth' : 'OLLAMA')) : null,
|
|
3914
4012
|
h('span', { className: 'dsub-qwBar' }, h('span', { className: 'dsub-qwBarFill', style: { width: state.remaining + '%', display: 'block' } })),
|
|
3915
4013
|
h('span', { className: 'dsub-qwPct' }, Math.round(state.remaining) + '%'),
|
|
3916
|
-
)
|
|
4014
|
+
), popover)
|
|
3917
4015
|
}
|
|
3918
4016
|
|
|
3919
4017
|
function registerSubsPill(ctx) {
|
package/lib/config-schema.js
CHANGED
|
@@ -159,6 +159,8 @@ export const Config = z.object({
|
|
|
159
159
|
.description('#351 External webhook URL to receive quota and session alerts (JSON POST).'),
|
|
160
160
|
webhookSecret: z.string().role('secret').default('')
|
|
161
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.'),
|
|
162
164
|
digestIntervalHours: z.number().volatile().default(0)
|
|
163
165
|
.description('#720 Send an alert digest every N hours (0 disables digests).'),
|
|
164
166
|
alertThresholds: z.array(z.number()).volatile().default([80, 90, 95])
|
package/lib/index.js
CHANGED
|
@@ -28,7 +28,7 @@ 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, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret } from './alerts.js'
|
|
31
|
+
import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret, setWebhookAllowPrivate } from './alerts.js'
|
|
32
32
|
import { isDigestDue } from './notification-scheduler.js'
|
|
33
33
|
|
|
34
34
|
export { observeForecast, estimateForecast } from './forecast.js'
|
|
@@ -94,6 +94,7 @@ export function apply(ctx, rawConfig) {
|
|
|
94
94
|
let cachedConfig = resolveEntry()
|
|
95
95
|
const live = () => {
|
|
96
96
|
setWebhookSecret(cachedConfig.webhookSecret)
|
|
97
|
+
setWebhookAllowPrivate(cachedConfig.webhookAllowPrivate)
|
|
97
98
|
return cachedConfig
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -615,7 +616,7 @@ export function apply(ctx, rawConfig) {
|
|
|
615
616
|
const cfg = live()
|
|
616
617
|
const alerts = getRecentAlerts(100)
|
|
617
618
|
if (!isDigestDue({ lastDigestAt, nowMs: Date.now(), intervalHours: cfg.digestIntervalHours, alertCount: alerts.length })) return
|
|
618
|
-
dispatchNotificationDigest(alerts, cfg.webhookUrl, fetch, { secret: cfg.webhookSecret })
|
|
619
|
+
dispatchNotificationDigest(alerts, cfg.webhookUrl, fetch, { secret: cfg.webhookSecret, allowPrivate: cfg.webhookAllowPrivate })
|
|
619
620
|
.then((ok) => { if (ok) lastDigestAt = Date.now() })
|
|
620
621
|
.catch(() => {})
|
|
621
622
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Config, plainConfig, revisionOf } from '../config-schema.js'
|
|
2
|
+
import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
3
|
+
import { capturePrePullSnapshot, restoreVaultSnapshot } from '../vault-sync.js'
|
|
4
|
+
import { defaultSnapshotStore } from '../vault-snapshots.js'
|
|
5
|
+
|
|
6
|
+
export function registerVaultRoutes(ctx, state) {
|
|
7
|
+
const { live, getSettingsApi, syncAdapter, stripLegacySlots } = state
|
|
8
|
+
|
|
9
|
+
ctx.effect(() => ctx.webServer.register({
|
|
10
|
+
kind: 'exact',
|
|
11
|
+
path: '/dsh-subscriptions/vault/snapshots',
|
|
12
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
13
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
14
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if (req.method === 'GET') {
|
|
18
|
+
writeJson(res, 200, { ok: true, snapshots: defaultSnapshotStore.list() })
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (req.method !== 'POST') {
|
|
22
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
let body = {}
|
|
26
|
+
try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
|
|
27
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const passphrase = String(live().vaultSyncPassphrase || '').trim()
|
|
31
|
+
if (!passphrase) {
|
|
32
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const snapshot = capturePrePullSnapshot(live().slots || [], passphrase, String(body.label || 'manual'))
|
|
37
|
+
writeJson(res, 200, { ok: true, snapshot })
|
|
38
|
+
} catch (err) {
|
|
39
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
}), 'dsh-subscriptions: /vault/snapshots')
|
|
43
|
+
|
|
44
|
+
ctx.effect(() => ctx.webServer.register({
|
|
45
|
+
kind: 'exact',
|
|
46
|
+
path: '/dsh-subscriptions/vault/snapshots/restore',
|
|
47
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
48
|
+
if (req.method !== 'POST') {
|
|
49
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
53
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
let body = {}
|
|
57
|
+
try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
|
|
58
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
const snapshot = defaultSnapshotStore.get(String(body.id || ''))
|
|
62
|
+
if (!snapshot) {
|
|
63
|
+
writeJson(res, 404, { ok: false, error: { code: 'snapshot', message: 'snapshot not found' } })
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const passphrase = String(live().vaultSyncPassphrase || '').trim()
|
|
67
|
+
if (!passphrase) {
|
|
68
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const api = getSettingsApi()
|
|
72
|
+
if (!api) {
|
|
73
|
+
writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const restored = restoreVaultSnapshot(snapshot, passphrase)
|
|
78
|
+
const cfg = live()
|
|
79
|
+
const slots = stripLegacySlots(restored.slots || [])
|
|
80
|
+
await api.replace(plainConfig(Config({ ...cfg, slots })), revisionOf(api))
|
|
81
|
+
if (typeof syncAdapter === 'function') await syncAdapter()
|
|
82
|
+
writeJson(res, 200, { ok: true, id: snapshot.id, slotCount: slots.length })
|
|
83
|
+
} catch (err) {
|
|
84
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
}), 'dsh-subscriptions: /vault/snapshots/restore')
|
|
88
|
+
}
|
|
@@ -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
|
@@ -7,6 +7,8 @@ import { registerAccountsRoutes } from './routes/accounts.js'
|
|
|
7
7
|
import { registerProxyRoutes } from './routes/proxy.js'
|
|
8
8
|
import { registerSourcesRoutes } from './routes/sources.js'
|
|
9
9
|
import { registerPacksRoutes } from './routes/packs.js'
|
|
10
|
+
import { registerWebhookRoutes } from './routes/webhook.js'
|
|
11
|
+
import { registerVaultRoutes } from './routes/vault.js'
|
|
10
12
|
import { registerPluginUpdater } from './updater.js'
|
|
11
13
|
|
|
12
14
|
export function registerRoutes(ctx, state) {
|
|
@@ -113,4 +115,6 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'
|
|
|
113
115
|
registerProxyRoutes(ctx, state)
|
|
114
116
|
registerSourcesRoutes(ctx, state)
|
|
115
117
|
registerPacksRoutes(ctx)
|
|
118
|
+
registerWebhookRoutes(ctx, state)
|
|
119
|
+
registerVaultRoutes(ctx, state)
|
|
116
120
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// #734: bounded in-memory snapshot ring. Only metadata leaves list(); the
|
|
2
|
+
// encrypted payload is available through get(id) for an explicit restore.
|
|
3
|
+
let seq = 0
|
|
4
|
+
|
|
5
|
+
function meta(snapshot) {
|
|
6
|
+
return {
|
|
7
|
+
id: snapshot.id,
|
|
8
|
+
label: snapshot.label || '',
|
|
9
|
+
createdAt: snapshot.createdAt,
|
|
10
|
+
version: snapshot.version || 2,
|
|
11
|
+
slotCount: Number(snapshot.slotCount) || 0,
|
|
12
|
+
blobCount: Number(snapshot.blobCount) || 0,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
|
|
17
|
+
let items = []
|
|
18
|
+
return {
|
|
19
|
+
add(snapshot, label = '') {
|
|
20
|
+
const at = now()
|
|
21
|
+
const entry = {
|
|
22
|
+
...snapshot,
|
|
23
|
+
id: 'vs_' + at.toString(36) + '_' + (++seq).toString(36),
|
|
24
|
+
label: String(label || snapshot.label || ''),
|
|
25
|
+
createdAt: snapshot.createdAt || new Date(at).toISOString(),
|
|
26
|
+
}
|
|
27
|
+
items.unshift(entry)
|
|
28
|
+
if (items.length > Math.max(1, max)) items = items.slice(0, Math.max(1, max))
|
|
29
|
+
return meta(entry)
|
|
30
|
+
},
|
|
31
|
+
list() {
|
|
32
|
+
return items.map(meta)
|
|
33
|
+
},
|
|
34
|
+
get(id) {
|
|
35
|
+
return items.find((item) => item.id === id) || null
|
|
36
|
+
},
|
|
37
|
+
clear() {
|
|
38
|
+
items = []
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const defaultSnapshotStore = createSnapshotStore({ max: 10 })
|
package/lib/vault-sync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { exportVault, importVault } from './vault.js'
|
|
|
6
6
|
import { normalizeSlots } from './accounts.js'
|
|
7
7
|
import { Config, plainConfig, revisionOf } from './config-schema.js'
|
|
8
8
|
import { validateCredentialBlob } from './blob.js'
|
|
9
|
+
import { defaultSnapshotStore } from './vault-snapshots.js'
|
|
9
10
|
|
|
10
11
|
let syncStatus = {
|
|
11
12
|
enabled: false,
|
|
@@ -273,6 +274,10 @@ export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '
|
|
|
273
274
|
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
export function capturePrePullSnapshot(slots, passphrase, label = 'pre-pull') {
|
|
278
|
+
return defaultSnapshotStore.add(createVaultSnapshot({ slots: slots || [], blobs: {} }, passphrase, label))
|
|
279
|
+
}
|
|
280
|
+
|
|
276
281
|
export function restoreVaultSnapshot(snapshot, passphrase) {
|
|
277
282
|
if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
|
|
278
283
|
return importVault(snapshot.vault, passphrase)
|
|
@@ -401,6 +406,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
401
406
|
throw new Error(`unsupported vault sync backend: ${backend}`)
|
|
402
407
|
}
|
|
403
408
|
|
|
409
|
+
try { capturePrePullSnapshot(cfg.slots || [], passphrase) } catch { /* snapshot is best-effort */ }
|
|
404
410
|
const result = importVault(encryptedData, passphrase)
|
|
405
411
|
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
406
412
|
const remoteSlots = providerFilter.size
|
|
@@ -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.8",
|
|
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",
|