@goodandready/dsh-subscriptions 0.6.72 → 0.6.82

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.
Files changed (47) hide show
  1. package/README.md +7 -0
  2. package/README.ru.md +7 -0
  3. package/README.zh.md +7 -0
  4. package/lib/accounts-view.js +97 -5
  5. package/lib/accounts.js +137 -11
  6. package/lib/adapter.js +3 -0
  7. package/lib/alerts.js +35 -0
  8. package/lib/analytics.js +31 -2
  9. package/lib/billing.js +99 -0
  10. package/lib/blob.js +18 -0
  11. package/lib/capabilities.js +208 -0
  12. package/lib/client.js +344 -17
  13. package/lib/config-schema.js +46 -4
  14. package/lib/diagnostic-wizard.js +2 -5
  15. package/lib/diagnostics.js +2 -2
  16. package/lib/error-buffer.js +2 -2
  17. package/lib/forecast.js +56 -0
  18. package/lib/headless-auth.js +379 -0
  19. package/lib/history.js +36 -0
  20. package/lib/http.js +26 -4
  21. package/lib/index.js +98 -11
  22. package/lib/jwt.js +84 -17
  23. package/lib/loopback.js +3 -2
  24. package/lib/mask.js +16 -0
  25. package/lib/oauth-pending.js +12 -0
  26. package/lib/pricing.js +89 -0
  27. package/lib/refs.js +2 -0
  28. package/lib/reset-clock.js +55 -0
  29. package/lib/rotate.js +18 -4
  30. package/lib/routes/accounts.js +131 -0
  31. package/lib/routes/oauth.js +44 -2
  32. package/lib/routes/proxy.js +14 -0
  33. package/lib/scope-inspector.js +51 -0
  34. package/lib/stream-rotate.js +31 -10
  35. package/lib/updater.js +21 -0
  36. package/lib/vault-sync.js +412 -0
  37. package/lib/vault.js +1 -1
  38. package/lib/vendor-factory.js +6 -2
  39. package/lib/vendors/antigravity.js +11 -2
  40. package/lib/vendors/claude.js +56 -8
  41. package/lib/vendors/codex.js +34 -6
  42. package/lib/vendors/copilot-cli.js +8 -1
  43. package/lib/vendors/ernie.js +9 -2
  44. package/lib/vendors/grok.js +66 -6
  45. package/lib/vendors/jetbrains.js +133 -13
  46. package/lib/vendors/perplexity.js +148 -17
  47. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -154,6 +154,24 @@ window.__ModuleLoader__.load({
154
154
  "recentErrorsDesc": "Last 20 runtime failures across model streams and providers (sanitized).",
155
155
  "recentErrorsClear": "Clear Log",
156
156
  "recentErrorsEmpty": "No recent errors recorded",
157
+ "atRestEncrypted": "Encrypted at Rest (AES-256-GCM)",
158
+ "atRestEncryptedTitle": "Credentials are protected at rest with master key encryption.",
159
+ "maxConcurrencySlotTitle": "Max parallel streams for this account slot (default: 1)",
160
+ "concurrencyQueueHint": "Active and queued parallel requests for this account",
161
+ "activeStreams": "active",
162
+ "queuedStreams": "queued",
163
+ "pacingRunwayHint": "Estimated time before quota exhausts at current pace",
164
+ "runwayPrefix": "Runway",
165
+ "billingAlertHint": "Billing status notice from provider",
166
+ "renewalHint": "Subscription billing cycle renewal date",
167
+ "valueDeliveredHint": "Equivalent official API cost of consumed tokens vs subscription price",
168
+ "capabilitiesHint": "Subscription capabilities, context window and model features",
169
+ "orPasteVaultPayload": "or paste vault payload below",
170
+ "workspaceTitle": "Organization / Workspace",
171
+ "workspaceSelect": "Select active organization/workspace",
172
+ "personalWorkspace": "Personal Default",
173
+ "windowResetsAt": "Window resets at",
174
+ "quotaRecovery": "Quota recovery",
157
175
  "toastCooldownRecovered": "is now available (cooldown expired)",
158
176
  "modelsCatalogTitle": "Curated Subscription Models",
159
177
  "modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
@@ -265,6 +283,11 @@ window.__ModuleLoader__.load({
265
283
  "instructionsTitle": "How to obtain token",
266
284
  "close": "Close",
267
285
  "reconnectRequired": "Reconnect required",
286
+ "reconnectNow": "Re-authenticate now",
287
+ "scopeWarning": "OAuth scope warning",
288
+ "reconnectScope": "Re-grant scopes",
289
+ "sessionRevokedBadge": "Revoked / Expired",
290
+ "sessionRevokedWarning": "Session expired / revoked — re-authorization required",
268
291
  "manualTitle": "If the browser did not come back",
269
292
  "updateChecking": "Checking updates…",
270
293
  "updateUpToDate": "Up to date",
@@ -309,6 +332,19 @@ window.__ModuleLoader__.load({
309
332
  "toastReleased": "Account active and healthy",
310
333
  "toastQuotaWarning": "Quota threshold exceeded",
311
334
  "enableToasts": "In-app toast alerts",
335
+ "headlessTip": "Headless / Remote Server?",
336
+ "headlessSshCommand": "SSH Port Forward Command:",
337
+ "headlessCopySsh": "Copy SSH Command",
338
+ "headlessCopied": "Copied!",
339
+ "headlessScanQr": "Scan with Mobile to Authenticate:",
340
+ "vaultSyncTitle": "Automated Remote Vault Sync",
341
+ "vaultSyncPush": "Sync Now (Push)",
342
+ "vaultSyncPull": "Sync Now (Pull)",
343
+ "vaultSyncPushOk": "Vault successfully pushed to remote storage",
344
+ "vaultSyncPullOk": "Vault successfully pulled and merged from remote storage",
345
+ "vaultSyncBackend": "Backend:",
346
+ "vaultSyncLastPush": "Last Push:",
347
+ "vaultSyncLastPull": "Last Pull:",
312
348
  "groupQueue": "Concurrency & Request Queue",
313
349
  "enableQueue": "Enable request queue",
314
350
  "enableQueueHint": "Queue excess requests when account hits concurrency limit instead of failing with 429",
@@ -450,6 +486,24 @@ window.__ModuleLoader__.load({
450
486
  "recentErrorsDesc": "最近 20 条流式传输与 API 调用失败记录(已脱敏)。",
451
487
  "recentErrorsClear": "清空记录",
452
488
  "recentErrorsEmpty": "暂无错误记录",
489
+ "atRestEncrypted": "静态存储加密 (AES-256-GCM)",
490
+ "atRestEncryptedTitle": "凭据已通过主密钥进行静态 AES-256-GCM 加密保护。",
491
+ "maxConcurrencySlotTitle": "当前账户槽位最大并发请求数(默认:1)",
492
+ "concurrencyQueueHint": "当前账户槽位进行中与排队中的并发请求",
493
+ "activeStreams": "活跃",
494
+ "queuedStreams": "排队中",
495
+ "pacingRunwayHint": "当前调用速度下的预计剩余可用时间",
496
+ "runwayPrefix": "可用时长",
497
+ "billingAlertHint": "服务商账单状态提示",
498
+ "renewalHint": "订阅账单周期续费日期",
499
+ "valueDeliveredHint": "消耗 Token 对应的官方 API 价值对比订阅费用",
500
+ "capabilitiesHint": "订阅账号模型上下文窗口与特权特性",
501
+ "orPasteVaultPayload": "或在下方粘贴保险库文本",
502
+ "workspaceTitle": "工作区与组织",
503
+ "workspaceSelect": "选择生效的工作区或组织",
504
+ "personalWorkspace": "个人默认",
505
+ "windowResetsAt": "额度重置于",
506
+ "quotaRecovery": "额度恢复",
453
507
  "toastCooldownRecovered": "已解除冷却恢复可用",
454
508
  "modelsCatalogTitle": "精选订阅模型",
455
509
  "modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
@@ -561,6 +615,11 @@ window.__ModuleLoader__.load({
561
615
  "instructionsTitle": "获取令牌指引",
562
616
  "close": "关闭",
563
617
  "reconnectRequired": "需要重新连接",
618
+ "reconnectNow": "立即重新认证",
619
+ "scopeWarning": "OAuth权限范围警告",
620
+ "reconnectScope": "重新授权权限",
621
+ "sessionRevokedBadge": "已失效 / 过期",
622
+ "sessionRevokedWarning": "会话已过期或失效 — 需要重新授权",
564
623
  "manualTitle": "若浏览器未能自动跳转回调",
565
624
  "updateChecking": "检查更新中…",
566
625
  "updateUpToDate": "已是最新版本",
@@ -602,6 +661,19 @@ window.__ModuleLoader__.load({
602
661
  "releaseQuarantineTitle": "立即解除账户隔离状态",
603
662
  "resetsIn": "重置倒计时",
604
663
  "toastQuarantined": "账户进入隔离期",
664
+ "headlessTip": "无头环境 / 远程服务器?",
665
+ "headlessSshCommand": "SSH 端口转发命令:",
666
+ "headlessCopySsh": "复制 SSH 命令",
667
+ "headlessCopied": "已复制!",
668
+ "headlessScanQr": "使用手机扫描以登录:",
669
+ "vaultSyncTitle": "远程加密保管库自动同步",
670
+ "vaultSyncPush": "立即同步 (推送)",
671
+ "vaultSyncPull": "立即同步 (拉取)",
672
+ "vaultSyncPushOk": "保管库已成功推送到远程存储",
673
+ "vaultSyncPullOk": "已从远程存储拉取并合并保管库",
674
+ "vaultSyncBackend": "存储后端:",
675
+ "vaultSyncLastPush": "上次推送:",
676
+ "vaultSyncLastPull": "上次拉取:",
605
677
  "toastReleased": "账户已解除隔离恢复使用",
606
678
  "toastQuotaWarning": "额度预警",
607
679
  "enableToasts": "应用内弹窗提醒",
@@ -678,6 +750,11 @@ window.__ModuleLoader__.load({
678
750
  else if (p.indexOf('team') >= 0) label = 'Team'
679
751
  else if (p.indexOf('plus') >= 0) label = 'Plus'
680
752
  else if (p.indexOf('enterp') >= 0) label = 'Enterprise'
753
+ } else if (provider === 'claude') {
754
+ if (p.indexOf('enterp') >= 0) label = 'Enterprise'
755
+ else if (p.indexOf('team') >= 0) label = 'Team'
756
+ else if (p.indexOf('max') >= 0) label = 'Max'
757
+ else if (p.indexOf('pro') >= 0) label = 'Pro'
681
758
  } else if (provider === 'grok') {
682
759
  if (p.indexOf('super') >= 0) label = 'SuperGrok'
683
760
  else if (p.indexOf('plus') >= 0) label = 'X Premium+'
@@ -689,6 +766,10 @@ window.__ModuleLoader__.load({
689
766
  label = 'Coding Plan'
690
767
  } else if (provider === 'glm') {
691
768
  label = '150% Boost'
769
+ } else if (provider === 'perplexity') {
770
+ label = 'Pro'
771
+ } else if (provider === 'jetbrains') {
772
+ label = 'AI Assistant'
692
773
  }
693
774
  return label ? h('span', { className: 'dsub-planBadge' }, label) : null
694
775
  }
@@ -718,22 +799,27 @@ window.__ModuleLoader__.load({
718
799
  const ms = (props.resetAt || 0) - now
719
800
  if (ms <= 0) return null
720
801
  const total = Math.floor(ms / 1000)
802
+ const d = new Date(props.resetAt)
803
+ const pad = (n) => String(n).padStart(2, '0')
804
+ const timeStr = pad(d.getHours()) + ':' + pad(d.getMinutes())
805
+ const relStr = formatRelativeReset(props.resetAt, t('lang'), now)
721
806
  if (total > 3600) {
722
807
  return h('span', { className: 'dsub-sub', style: { fontVariantNumeric: 'tabular-nums' } },
723
- t('resetLabel') + ' ' + formatRelativeReset(props.resetAt, t('lang'), now))
808
+ '⏳ ' + t('windowResetsAt') + ' ' + timeStr + ' (' + relStr + ')')
724
809
  }
725
810
  const m = Math.floor(total / 60)
726
811
  const sec = total % 60
727
- const pad = (n) => String(n).padStart(2, '0')
728
812
  return h('span', { className: 'dsub-sub', style: { fontVariantNumeric: 'tabular-nums' } },
729
- t('resetLabel') + ' ' + pad(m) + ':' + pad(sec))
813
+ '⏳ ' + t('windowResetsAt') + ' ' + timeStr + ' (' + relStr + ' · ' + pad(m) + ':' + pad(sec) + ')')
730
814
  }
731
815
 
732
816
  function badgeFor(account, t) {
733
817
  if (!account || !account.configured) return t('notConnected')
818
+ if (account.isRevoked) return t('sessionRevokedBadge')
734
819
  if (account.validationUrl) return t('verifyAccount')
735
820
  // Issue 303: a quarantined account says why it is parked.
736
821
  if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
822
+ if (account.quarantineReason === 'SESSION_REVOKED' || account.quarantineReason === 'REVOKED') return t('sessionRevokedBadge')
737
823
  return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
738
824
  }
739
825
  if (account.cooldownUntil && account.cooldownUntil > Date.now()) {
@@ -885,6 +971,53 @@ window.__ModuleLoader__.load({
885
971
  const [smoke, setSmoke] = React.useState({ testing: false, result: null })
886
972
  const [updater, setUpdater] = React.useState({ checking: false, updating: false, status: null, message: '' })
887
973
  const [backupModal, setBackupModal] = React.useState(null)
974
+ const [vaultSyncInfo, setVaultSyncInfo] = React.useState(null)
975
+ const [vaultSyncLoading, setVaultSyncLoading] = React.useState(false)
976
+ const [headlessInfo, setHeadlessInfo] = React.useState({})
977
+ const [copiedSsh, setCopiedSsh] = React.useState({})
978
+
979
+ const loadVaultSyncStatus = () => {
980
+ fetch('/dsh-subscriptions/vault/sync/status')
981
+ .then((r) => r.json())
982
+ .then((d) => { if (d && d.ok) setVaultSyncInfo(d.status) })
983
+ .catch(() => {})
984
+ }
985
+
986
+ const doVaultSyncPush = async () => {
987
+ setBackupErr(''); setBackupInfo(''); setVaultSyncLoading(true)
988
+ try {
989
+ const res = await fetch('/dsh-subscriptions/vault/sync/push', {
990
+ method: 'POST',
991
+ headers: { 'Content-Type': 'application/json' },
992
+ })
993
+ const d = await res.json().catch(() => ({}))
994
+ if (!res.ok || !d.ok) throw new Error((d.error && d.error.message) || ('HTTP ' + res.status))
995
+ setBackupInfo(t('vaultSyncPushOk'))
996
+ if (d.status) setVaultSyncInfo(d.status)
997
+ } catch (e) {
998
+ setBackupErr(cleanErrorMessage(e.message || e))
999
+ }
1000
+ setVaultSyncLoading(false)
1001
+ }
1002
+
1003
+ const doVaultSyncPull = async () => {
1004
+ setBackupErr(''); setBackupInfo(''); setVaultSyncLoading(true)
1005
+ try {
1006
+ const res = await fetch('/dsh-subscriptions/vault/sync/pull', {
1007
+ method: 'POST',
1008
+ headers: { 'Content-Type': 'application/json' },
1009
+ })
1010
+ const d = await res.json().catch(() => ({}))
1011
+ if (!res.ok || !d.ok) throw new Error((d.error && d.error.message) || ('HTTP ' + res.status))
1012
+ setBackupInfo(t('vaultSyncPullOk') + (d.result ? ' (' + d.result.slotCount + ' slots)' : ''))
1013
+ if (d.status) setVaultSyncInfo(d.status)
1014
+ if (d.accounts) setAccounts(d.accounts)
1015
+ await reload()
1016
+ } catch (e) {
1017
+ setBackupErr(cleanErrorMessage(e.message || e))
1018
+ }
1019
+ setVaultSyncLoading(false)
1020
+ }
888
1021
  const [backupPass, setBackupPass] = React.useState('')
889
1022
  const [backupData, setBackupData] = React.useState('')
890
1023
  const [backupErr, setBackupErr] = React.useState('')
@@ -959,7 +1092,7 @@ window.__ModuleLoader__.load({
959
1092
  const url = URL.createObjectURL(blob)
960
1093
  const a = document.createElement('a')
961
1094
  a.href = url
962
- a.download = 'dsh-subscriptions-backup.enc'
1095
+ a.download = 'dsh-subscriptions-vault.enc'
963
1096
  document.body.appendChild(a)
964
1097
  a.click()
965
1098
  document.body.removeChild(a)
@@ -1176,6 +1309,8 @@ window.__ModuleLoader__.load({
1176
1309
  const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
1177
1310
  const data = await res.json().catch(() => ({}))
1178
1311
  if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1312
+ const key = provider + ':' + index
1313
+ if (data.headless) setHeadlessInfo((m) => Object.assign({}, m, { [key]: data.headless }))
1179
1314
  if (data.url) window.open(data.url, '_blank', 'noopener')
1180
1315
  }
1181
1316
 
@@ -1191,6 +1326,7 @@ window.__ModuleLoader__.load({
1191
1326
  })
1192
1327
  const data = await res.json().catch(() => ({}))
1193
1328
  if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1329
+ if (data.headless) setHeadlessInfo((m) => Object.assign({}, m, { [key]: data.headless }))
1194
1330
  setDevice((m) => Object.assign({}, m, { [key]: { state: data.state, userCode: data.userCode, authUrl: data.authUrl, intervalMs: data.intervalMs || 5000, status: 'pending' } }))
1195
1331
  }
1196
1332
 
@@ -1353,6 +1489,22 @@ window.__ModuleLoader__.load({
1353
1489
  setTokenDraft((d) => Object.assign({}, d, { [provider + ':' + index]: '' }))
1354
1490
  }
1355
1491
 
1492
+ const switchOrg = async (provider, index, organizationId) => {
1493
+ setErr('')
1494
+ try {
1495
+ const res = await fetch('/dsh-subscriptions/accounts/switch-organization', {
1496
+ method: 'POST',
1497
+ headers: { 'Content-Type': 'application/json' },
1498
+ body: JSON.stringify({ provider, index, organizationId }),
1499
+ })
1500
+ const data = await res.json().catch(() => ({}))
1501
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1502
+ if (data.accounts) setAccounts(data.accounts)
1503
+ } catch (e) {
1504
+ setErr(cleanErrorMessage(e.message || e))
1505
+ }
1506
+ }
1507
+
1356
1508
  const addSlot = (provider) => {
1357
1509
  const used = slots.filter((s) => s.provider === provider).map((s) => s.index)
1358
1510
  let index = 1
@@ -1958,8 +2110,8 @@ window.__ModuleLoader__.load({
1958
2110
  h('button', { type: 'button', className: 'dsub-mini', onClick: () => runWizard(null, null, null, false) }, '🩺 ' + t('wizardAll')),
1959
2111
  h('button', { type: 'button', className: 'dsub-mini', onClick: genDiag }, t('diagGenerate')),
1960
2112
  h('button', { type: 'button', className: 'dsub-mini', onClick: runAnalyzeSession }, analyzingCache ? '…' : t('cacheAnalyze')),
1961
- h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('export'); setBackupErr(''); setBackupInfo('') } }, '🔒 ' + t('backupExport')),
1962
- h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('import'); setBackupErr(''); setBackupInfo('') } }, '📥 ' + t('backupImport')),
2113
+ h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('export'); setBackupErr(''); setBackupInfo(''); loadVaultSyncStatus() } }, '🔒 ' + t('backupExport')),
2114
+ h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('import'); setBackupErr(''); setBackupInfo(''); loadVaultSyncStatus() } }, '📥 ' + t('backupImport')),
1963
2115
  h('a', { href: 'https://github.com/GooDAnDReaDY/dsh-subscriptions/issues', target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('diagIssues')),
1964
2116
  ),
1965
2117
  h('div', { className: 'dsub-sub' }, t('diagHint')),
@@ -2086,6 +2238,24 @@ window.__ModuleLoader__.load({
2086
2238
  backupModal === 'export' ? t('backupExportHint') : t('backupImportHint')
2087
2239
  ),
2088
2240
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' } },
2241
+ backupModal === 'import' ? h('div', { className: 'dsub-row', style: { gap: '8px', alignItems: 'center' } },
2242
+ h('input', {
2243
+ type: 'file',
2244
+ accept: '.enc,.txt',
2245
+ className: 'dsub-mini',
2246
+ style: { maxWidth: '220px' },
2247
+ onChange: (e) => {
2248
+ const f = e.target.files && e.target.files[0]
2249
+ if (!f) return
2250
+ const reader = new window.FileReader()
2251
+ reader.onload = (evt) => {
2252
+ if (evt.target && evt.target.result) setBackupData(String(evt.target.result))
2253
+ }
2254
+ reader.readAsText(f)
2255
+ },
2256
+ }),
2257
+ h('span', { className: 'dsub-sub' }, t('orPasteVaultPayload'))
2258
+ ) : null,
2089
2259
  backupModal === 'import' ? h('textarea', {
2090
2260
  className: 'dsub-diag',
2091
2261
  style: { width: '100%', height: '80px', fontFamily: 'monospace', fontSize: '11px' },
@@ -2127,7 +2297,34 @@ window.__ModuleLoader__.load({
2127
2297
  onClick: downloadBackupFile,
2128
2298
  }, t('backupDownload')),
2129
2299
  h('pre', { className: 'dsub-diag', style: { maxHeight: '60px', overflow: 'hidden', textOverflow: 'ellipsis' } }, backupData)
2130
- ) : null
2300
+ ) : null,
2301
+ h('div', {
2302
+ className: 'dsub-panel-box',
2303
+ style: { marginTop: 10, padding: '10px 12px', borderTop: '1px solid var(--dsw-alias-border-l2)' }
2304
+ },
2305
+ h('div', { className: 'dsub-panel-title' },
2306
+ h('span', null, '☁️ ' + t('vaultSyncTitle')),
2307
+ h('div', { style: { display: 'flex', gap: 6 } },
2308
+ h('button', {
2309
+ type: 'button',
2310
+ className: 'dsub-mini',
2311
+ disabled: vaultSyncLoading,
2312
+ onClick: doVaultSyncPush,
2313
+ }, vaultSyncLoading ? '…' : t('vaultSyncPush')),
2314
+ h('button', {
2315
+ type: 'button',
2316
+ className: 'dsub-mini',
2317
+ disabled: vaultSyncLoading,
2318
+ onClick: doVaultSyncPull,
2319
+ }, vaultSyncLoading ? '…' : t('vaultSyncPull'))
2320
+ )
2321
+ ),
2322
+ vaultSyncInfo ? h('div', { className: 'dsub-sub', style: { marginTop: 4 } },
2323
+ t('vaultSyncBackend') + ' ' + (vaultSyncInfo.backend || 'webdav') + ' · ' +
2324
+ t('vaultSyncLastPush') + ' ' + (vaultSyncInfo.lastPushAt ? new Date(vaultSyncInfo.lastPushAt).toLocaleTimeString() : 'never') + ' · ' +
2325
+ t('vaultSyncLastPull') + ' ' + (vaultSyncInfo.lastPullAt ? new Date(vaultSyncInfo.lastPullAt).toLocaleTimeString() : 'never')
2326
+ ) : null
2327
+ )
2131
2328
  )
2132
2329
  ) : null,
2133
2330
  ),
@@ -2141,20 +2338,30 @@ window.__ModuleLoader__.load({
2141
2338
  const account = accountOf(row.slot.provider, row.slot.index)
2142
2339
  const key = row.slot.provider + ':' + row.slot.index
2143
2340
  const on = !!account.configured
2341
+ const avatarEl = (account.avatarUrl ? h('img', {
2342
+ src: account.avatarUrl,
2343
+ alt: account.email || account.label || 'Avatar',
2344
+ className: 'dsub-avatar',
2345
+ onError: (e) => { e.target.style.display = 'none' },
2346
+ }) : (account.email ? h('span', {
2347
+ className: 'dsub-avatar dsub-avatar-initials',
2348
+ title: account.email,
2349
+ }, (account.email.charAt(0) || '?').toUpperCase()) : null))
2144
2350
  return h('div', { className: 'dsub-card', key: key },
2145
- h('div', { className: 'dsub-row' },
2351
+ h('div', { className: 'dsub-row', style: { alignItems: 'center' } },
2352
+ avatarEl,
2146
2353
  h('input', {
2147
2354
  className: 'dsub-grow',
2148
2355
  value: row.slot.label || '',
2149
- placeholder: account.label || (t('accountLabel') + ' ' + row.slot.index),
2356
+ placeholder: account.email || account.label || (t('accountLabel') + ' ' + row.slot.index),
2150
2357
  onChange: (e) => {
2151
2358
  const next = slots.slice()
2152
2359
  next[row.i] = Object.assign({}, next[row.i], { label: e.target.value })
2153
2360
  setSlots(next)
2154
2361
  },
2155
2362
  }),
2156
- h('span', { className: 'dsub-badge' + (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : '')) }, badgeFor(account, t)),
2157
- normalizePlanBadge(row.slot.provider, account.plan || (account.quota && account.quota.plan)),
2363
+ h('span', { className: 'dsub-badge' + (account.isRevoked ? ' dsub-badge-bad' : (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : ''))) }, badgeFor(account, t)),
2364
+ normalizePlanBadge(row.slot.provider, account.plan || account.paidTierName || (account.quota && account.quota.plan)),
2158
2365
  h('button', {
2159
2366
  type: 'button', className: 'dsub-mini',
2160
2367
  onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
@@ -2233,8 +2440,8 @@ window.__ModuleLoader__.load({
2233
2440
  h('span', { className: 'dsub-sub', style: { color: 'var(--dsw-alias-state-error-primary)', fontVariantNumeric: 'tabular-nums' } },
2234
2441
  '⛔ ' + (account.quarantineReason || 'QUARANTINE') + ' · ' + t('resetsIn') + ' ' + formatRelativeReset(account.quarantineUntil, t('lang'), Date.now())
2235
2442
  ),
2236
- ) : (account && account.quota && account.quota.resetAt && Number(account.quota.resetAt) > Date.now()) ? h('div', { className: 'dsub-row', style: { marginTop: 4 } },
2237
- h(ResetCountdown, { resetAt: account.quota.resetAt }),
2443
+ ) : (account && (account.resetAt || (account.quota && account.quota.resetAt)) && Number(account.resetAt || account.quota.resetAt) > Date.now()) ? h('div', { className: 'dsub-row', style: { marginTop: 4 } },
2444
+ h(ResetCountdown, { resetAt: account.resetAt || account.quota.resetAt }),
2238
2445
  ) : null,
2239
2446
  h('div', { className: 'dsub-manual' },
2240
2447
  h('span', { className: 'dsub-sub' }, t('manualTitle')),
@@ -2250,6 +2457,36 @@ window.__ModuleLoader__.load({
2250
2457
  onClick: () => complete(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2251
2458
  }, t('submitCode')),
2252
2459
  ),
2460
+ (headlessInfo[key] ? h('div', {
2461
+ className: 'dsub-panel-box',
2462
+ style: { marginTop: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)' }
2463
+ },
2464
+ h('div', { style: { fontWeight: 600, fontSize: '11px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
2465
+ h('span', null, '🌐 ' + t('headlessTip')),
2466
+ h('button', {
2467
+ type: 'button',
2468
+ className: 'dsub-mini',
2469
+ onClick: () => {
2470
+ try {
2471
+ navigator.clipboard.writeText(headlessInfo[key].sshTunnelCommand)
2472
+ setCopiedSsh((m) => Object.assign({}, m, { [key]: true }))
2473
+ setTimeout(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
2474
+ } catch { /* ignore */ }
2475
+ }
2476
+ }, copiedSsh[key] ? ('✓ ' + t('headlessCopied')) : ('📋 ' + t('headlessCopySsh')))
2477
+ ),
2478
+ h('div', { style: { fontFamily: 'monospace', fontSize: '11px', margin: '4px 0', wordBreak: 'break-all' } },
2479
+ headlessInfo[key].sshTunnelCommand
2480
+ ),
2481
+ headlessInfo[key].svgDataUrl ? h('div', { style: { marginTop: 6, textAlign: 'center' } },
2482
+ h('div', { className: 'dsub-sub', style: { marginBottom: 4 } }, t('headlessScanQr')),
2483
+ h('img', {
2484
+ src: headlessInfo[key].svgDataUrl,
2485
+ alt: 'QR Code',
2486
+ style: { width: '130px', height: '130px', borderRadius: '4px', background: '#fff', padding: '4px' }
2487
+ })
2488
+ ) : null
2489
+ ) : null),
2253
2490
  h('div', { className: 'dsub-row', style: { justifyContent: 'space-between' } },
2254
2491
  h('button', {
2255
2492
  type: 'button',
@@ -2348,6 +2585,26 @@ window.__ModuleLoader__.load({
2348
2585
  disabled: checking['proxy:' + key],
2349
2586
  onClick: () => doProxyCheck(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2350
2587
  }, checking['proxy:' + key] ? '\u2026' : t('proxyCheck')),
2588
+ h('span', {
2589
+ style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'inline-flex', alignItems: 'center', gap: '3px' },
2590
+ title: t('maxConcurrencySlotTitle')
2591
+ },
2592
+ '⚡',
2593
+ h('input', {
2594
+ type: 'number',
2595
+ min: 1,
2596
+ max: 20,
2597
+ style: { width: '44px', padding: '1px 4px', fontSize: '11px', textAlign: 'center' },
2598
+ value: row.slot.maxConcurrency !== undefined ? row.slot.maxConcurrency : 1,
2599
+ title: t('maxConcurrencySlotTitle'),
2600
+ onChange: (e) => {
2601
+ const next = slots.slice()
2602
+ const val = parseInt(e.target.value, 10)
2603
+ next[row.i] = Object.assign({}, next[row.i], { maxConcurrency: isNaN(val) ? 1 : Math.max(1, val) })
2604
+ setSlots(next)
2605
+ },
2606
+ })
2607
+ ),
2351
2608
  ),
2352
2609
  (function(){
2353
2610
  var r = proxyRes[key]
@@ -2439,7 +2696,30 @@ window.__ModuleLoader__.load({
2439
2696
  )
2440
2697
  })() : null),
2441
2698
  account.accountNotice ? h('div', { className: 'dsub-verify' }, account.accountNotice) : null,
2442
- account.refreshError ? h('div', { className: 'dsub-bad' }, t('reconnectRequired') + ': ' + account.refreshError) : null,
2699
+ account.scopeWarning ? h('div', { className: 'dsub-scope-banner' },
2700
+ h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '⚠️ ' + t('scopeWarning') + ': ' + account.scopeWarning),
2701
+ h('button', {
2702
+ type: 'button',
2703
+ className: 'dsub-mini',
2704
+ onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2705
+ }, '🔄 ' + t('reconnectScope'))
2706
+ ) : null,
2707
+ (account.isRevoked || account.authWarning) ? h('div', { className: 'dsub-bad dsub-row', style: { alignItems: 'center', justifyContent: 'space-between', gap: '8px', marginBottom: '6px' } },
2708
+ h('span', null, '⛔ ' + (account.authWarning || t('sessionRevokedWarning'))),
2709
+ h('button', {
2710
+ type: 'button',
2711
+ className: 'dsub-mini dsub-btn-primary',
2712
+ onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2713
+ }, '🔄 ' + t('reconnectNow'))
2714
+ ) : null,
2715
+ account.refreshError ? h('div', { className: 'dsub-bad dsub-row', style: { alignItems: 'center', justifyContent: 'space-between', gap: '8px' } },
2716
+ h('span', null, t('reconnectRequired') + ': ' + account.refreshError),
2717
+ h('button', {
2718
+ type: 'button',
2719
+ className: 'dsub-mini dsub-btn-primary',
2720
+ onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2721
+ }, '🔄 ' + t('reconnectNow'))
2722
+ ) : null,
2443
2723
  (function(){ var r=checkRes[key]; if(!r) return null; var txt=r.ok ? ('ok ' + (r.email||'')) : ('fail ' + (r.error && r.error.message || '')); var cls=r.ok ? 'dsub-ok' : 'dsub-bad'; var q=r.quota; if(q && q.remaining!=null) txt += ' quota:'+q.remaining+(q.limit!=null?'/'+q.limit:''); return h('div', {className: cls}, txt) })(),
2444
2724
  account.validationUrl ? h('div', { className: 'dsub-verify' },
2445
2725
  t('verifyPrefix'),
@@ -2523,10 +2803,25 @@ window.__ModuleLoader__.load({
2523
2803
  h('div', { className: 'dsub-usage-grid' }, items)
2524
2804
  )
2525
2805
  })(),
2526
- ((account.paidTierName || account.ref || (telemetry && telemetry.accounts && telemetry.accounts[account.ref])) ? h('div', { className: 'dsub-slot-meta-row' },
2527
- account.paidTierName ? h('span', { className: 'dsub-sub' },
2806
+ ((account.email || account.plan || account.paidTierName || account.ref || (telemetry && telemetry.accounts && telemetry.accounts[account.ref])) ? h('div', { className: 'dsub-slot-meta-row' },
2807
+ account.email ? h('span', { className: 'dsub-sub' },
2808
+ h('strong', null, '👤 '),
2809
+ account.email
2810
+ ) : null,
2811
+ (account.plan || account.paidTierName) ? h('span', { className: 'dsub-sub' },
2528
2812
  h('strong', null, t('plan') + ': '),
2529
- account.paidTierName
2813
+ account.plan || account.paidTierName
2814
+ ) : null,
2815
+ (account.organizations && account.organizations.length > 0) ? h('span', { className: 'dsub-sub', style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
2816
+ h('strong', null, '🏢 ' + t('workspaceTitle') + ':'),
2817
+ h('select', {
2818
+ className: 'dsub-select',
2819
+ style: { padding: '1px 6px', fontSize: '12px' },
2820
+ value: account.selectedOrgId || '',
2821
+ onChange: (e) => switchOrg(account.provider, account.index, e.target.value),
2822
+ },
2823
+ account.organizations.map((org) => h('option', { key: org.id, value: org.id }, org.name + (org.role ? ' (' + org.role + ')' : '')))
2824
+ )
2530
2825
  ) : null,
2531
2826
  (function(){
2532
2827
  const accTelem = telemetry && telemetry.accounts && telemetry.accounts[account.ref]
@@ -2541,6 +2836,38 @@ window.__ModuleLoader__.load({
2541
2836
  '⚡ ' + parts.join(' · ')
2542
2837
  )
2543
2838
  })(),
2839
+ (account.activeStreams > 0 || account.queuedStreams > 0) ? h('span', { className: 'dsub-ref-tag', title: t('concurrencyQueueHint') },
2840
+ account.activeStreams > 0 ? ('⚡ ' + account.activeStreams + ' ' + t('activeStreams')) : null,
2841
+ (account.activeStreams > 0 && account.queuedStreams > 0) ? ' · ' : null,
2842
+ account.queuedStreams > 0 ? ('⏳ ' + account.queuedStreams + ' ' + t('queuedStreams')) : null
2843
+ ) : null,
2844
+ account.runwayText ? h('span', {
2845
+ className: 'dsub-ref-tag',
2846
+ style: {
2847
+ color: account.pacingStatus === 'critical' ? 'var(--dsw-alias-state-error-primary)' : (account.pacingStatus === 'elevated' ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-success-primary)'),
2848
+ },
2849
+ title: t('pacingRunwayHint')
2850
+ }, '⏱️ ' + t('runwayPrefix') + ': ' + account.runwayText) : null,
2851
+ (account.valueDelivered && account.valueDelivered.equivalentCost > 0) ? h('span', {
2852
+ className: 'dsub-speed-tag',
2853
+ title: t('valueDeliveredHint')
2854
+ }, '📊 ' + account.valueDelivered.formattedCost + ' (' + account.valueDelivered.roiLabel + ' ROI)') : null,
2855
+ (account.billing && account.billing.badgeText) ? h('span', {
2856
+ className: 'dsub-ref-tag',
2857
+ style: { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 },
2858
+ title: account.billing.warning || t('billingAlertHint')
2859
+ }, '⚠️ ' + account.billing.badgeText) : null,
2860
+ (!account.billing?.badgeText && account.renewalCountdown) ? h('span', {
2861
+ className: 'dsub-ref-tag',
2862
+ title: t('renewalHint')
2863
+ }, '📅 ' + account.renewalCountdown) : null,
2864
+ (account.capabilities && Array.isArray(account.capabilities.tags) && account.capabilities.tags.length > 0) ? h('span', {
2865
+ className: 'dsub-ref-tag',
2866
+ title: t('capabilitiesHint')
2867
+ }, '⚡ ' + account.capabilities.tags.join(' · ')) : null,
2868
+ account.isEncryptedAtRest ? h('span', { className: 'dsub-ref-tag', title: t('atRestEncryptedTitle') },
2869
+ '🔒 ' + t('atRestEncrypted')
2870
+ ) : null,
2544
2871
  account.ref ? h('span', { className: 'dsub-ref-tag' },
2545
2872
  h('span', null, t('storedAs')),
2546
2873
  h('code', null, account.ref)
@@ -20,6 +20,8 @@ export const Slot = z.object({
20
20
  .description('#67 Optional subscription expiry timestamp (ms). When set and within expiryNotifyDays, the header chip shows the account and expiry date.'),
21
21
  proxyUrl: z.string().default('')
22
22
  .description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
23
+ maxConcurrency: z.number().default(1)
24
+ .description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
23
25
  })
24
26
 
25
27
  export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
@@ -86,6 +88,10 @@ export const Config = z.object({
86
88
  antigravityClientSecret: z.string().role('secret').default('')
87
89
  .description('Google OAuth client secret for custom confidential clients or CLI tokens requiring secret refresh.'),
88
90
  antigravityRedirectUri: z.string().volatile().default(''),
91
+ perplexityClientId: z.string().volatile().default(''),
92
+ perplexityRedirectUri: z.string().volatile().default(''),
93
+ jetbrainsClientId: z.string().volatile().default(''),
94
+ jetbrainsRedirectUri: z.string().volatile().default(''),
89
95
  antigravityModels: z.array(z.any()).volatile().default([])
90
96
  .description('Custom model catalog for Antigravity.'),
91
97
  codexOriginator: z.string().volatile().default('')
@@ -130,12 +136,48 @@ export const Config = z.object({
130
136
  .description('#485 Safety margin percent: avoid draining accounts below this threshold when alternatives exist.'),
131
137
  enableQueue: z.boolean().volatile().default(true)
132
138
  .description('#518 Bounded concurrency queue to prevent bursts of 429 errors.'),
133
- accountMaxConcurrency: z.number().volatile().default(2)
134
- .description('#518 Max concurrent requests per account before queuing.'),
139
+ accountMaxConcurrency: z.number().volatile().default(1)
140
+ .description('#518 #555 Default max concurrent requests per account before queuing (1 for OAuth subscription safety).'),
135
141
  maxQueueSize: z.number().volatile().default(20)
136
142
  .description('#518 Max requests waiting in queue before reject.'),
137
143
  queueTimeoutMs: z.number().volatile().default(30000)
138
144
  .description('#518 Max queue wait time in ms before timeout.'),
145
+ masterKey: z.string().role('secret').default('')
146
+ .description('#550 Optional master passphrase for encrypting OAuth credentials at rest (defaults to DSH_SUBSCRIPTIONS_MASTER_KEY env).'),
147
+ vaultSyncEnabled: z.boolean().volatile().default(false)
148
+ .description('#562 Enable automated encrypted vault sync to remote storage.'),
149
+ vaultSyncBackend: z.string().volatile().default('webdav')
150
+ .description('#562 Remote storage backend: webdav, gist, or s3.'),
151
+ vaultSyncPassphrase: z.string().role('secret').default('')
152
+ .description('#562 Passphrase used to encrypt/decrypt the remote vault archive.'),
153
+ vaultSyncIntervalHours: z.number().volatile().default(24)
154
+ .description('#562 Automated periodic sync interval in hours (0 disables periodic pull).'),
155
+ vaultSyncAutoPush: z.boolean().volatile().default(true)
156
+ .description('#562 Automatically push encrypted vault after successful OAuth login/refresh.'),
157
+ vaultSyncWebdavUrl: z.string().volatile().default('')
158
+ .description('#562 WebDAV target URL.'),
159
+ vaultSyncWebdavUsername: z.string().volatile().default('')
160
+ .description('#562 WebDAV username.'),
161
+ vaultSyncWebdavPassword: z.string().role('secret').default('')
162
+ .description('#562 WebDAV password or app token.'),
163
+ vaultSyncGistId: z.string().volatile().default('')
164
+ .description('#562 GitHub or Gitea Gist ID.'),
165
+ vaultSyncGistToken: z.string().role('secret').default('')
166
+ .description('#562 Personal access token with gist permission.'),
167
+ vaultSyncGistBaseUrl: z.string().volatile().default('https://api.github.com')
168
+ .description('#562 Gist API base URL.'),
169
+ vaultSyncS3Endpoint: z.string().volatile().default('')
170
+ .description('#562 S3 endpoint URL.'),
171
+ vaultSyncS3Bucket: z.string().volatile().default('')
172
+ .description('#562 S3 bucket name.'),
173
+ vaultSyncS3Key: z.string().volatile().default('dsh-subscriptions-vault.enc')
174
+ .description('#562 S3 object key.'),
175
+ vaultSyncS3Region: z.string().volatile().default('us-east-1')
176
+ .description('#562 S3 region.'),
177
+ vaultSyncS3AccessKey: z.string().volatile().default('')
178
+ .description('#562 S3 Access Key ID.'),
179
+ vaultSyncS3SecretKey: z.string().role('secret').default('')
180
+ .description('#562 S3 Secret Access Key.'),
139
181
  customVendors: z.array(z.any()).volatile().default([])
140
182
  .description('Declarative OpenAI-Responses-compatible providers. See README.'),
141
183
  })
@@ -145,7 +187,7 @@ export function publicConfig(cfg) {
145
187
  const clone = { ...cfg }
146
188
  // #242, #252: redact secret fields before exposing config via GET /config.
147
189
  for (const key of Object.keys(clone)) {
148
- if (key.endsWith("ClientSecret") || key.endsWith("Secret")) {
190
+ if (key.endsWith("ClientSecret") || key.endsWith("Secret") || key.endsWith("SecretKey") || key.endsWith("Password") || key.endsWith("Token") || key.endsWith("Passphrase") || key === "masterKey") {
149
191
  clone[key] = clone[key] ? "••••••" : ""
150
192
  }
151
193
  }
@@ -199,7 +241,7 @@ export function restoreMaskedSecrets(incoming, existing) {
199
241
  const out = { ...incoming }
200
242
 
201
243
  for (const key of Object.keys(out)) {
202
- if ((key.endsWith("ClientSecret") || key.endsWith("Secret")) && typeof out[key] === "string") {
244
+ if ((key.endsWith("ClientSecret") || key.endsWith("Secret") || key.endsWith("SecretKey") || key.endsWith("Password") || key.endsWith("Token") || key.endsWith("Passphrase") || key === "masterKey") && typeof out[key] === "string") {
203
245
  if (out[key].includes("••••")) {
204
246
  out[key] = existing[key] || ""
205
247
  }