@goodandready/dsh-subscriptions 0.6.86 → 0.6.88

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/README.md CHANGED
@@ -311,6 +311,7 @@ dsh-subscriptions:
311
311
  - Grok `baseUrl` overrides must point to `api.x.ai`, `grok.com` / `*.grok.com`, or a loopback host; other hosts are rejected with `GROK_UNTRUSTED_HOST` so bearer tokens never leave trusted xAI infrastructure.
312
312
  - Antigravity CLI imports must configure `antigravityClientSecret`; the embedded default was removed.
313
313
  - `copilotCliPath` must point to a `copilot`, `github-copilot-cli` or `copilot-cli` binary; rename custom wrappers.
314
+ - Credential backup uses `/dsh-subscriptions/vault/export` and `/vault/import`; the legacy `/export`, `/import`, `/export-backup` and `/import-backup` routes were removed.
314
315
  - The error buffer and diagnostics reports redact bearer tokens, API keys, JWTs and `key=value` credentials before storing or returning them.
315
316
 
316
317
 
package/README.ru.md CHANGED
@@ -284,6 +284,7 @@ dsh-subscriptions:
284
284
  - Переопределение Grok `baseUrl` должно указывать на `api.x.ai`, `grok.com` / `*.grok.com` или loopback-хост; остальные хосты отклоняются с `GROK_UNTRUSTED_HOST`, чтобы bearer-токен не покидал доверенную инфраструктуру xAI.
285
285
  - Импорт Antigravity CLI требует явного `antigravityClientSecret`; встроенный default удалён.
286
286
  - `copilotCliPath` должен указывать на бинарь `copilot`, `github-copilot-cli` или `copilot-cli`; кастомные обёртки переименуйте.
287
+ - Бэкап credentials использует `/dsh-subscriptions/vault/export` и `/vault/import`; legacy-маршруты `/export`, `/import`, `/export-backup` и `/import-backup` удалены.
287
288
  - Error buffer и диагностические отчёты редактируют bearer-токены, API-ключи, JWT и `key=value`-credentials до сохранения и выдачи.
288
289
 
289
290
 
package/README.zh.md CHANGED
@@ -190,6 +190,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
190
190
  - Grok `baseUrl` 覆盖只能指向 `api.x.ai`、`grok.com` / `*.grok.com` 或回环主机;其他主机会以 `GROK_UNTRUSTED_HOST` 拒绝,避免 bearer 令牌离开可信的 xAI 基础设施。
191
191
  - Antigravity CLI 导入必须配置 `antigravityClientSecret`;内置默认值已移除。
192
192
  - `copilotCliPath` 必须指向 `copilot`、`github-copilot-cli` 或 `copilot-cli` 可执行文件;自定义包装器请重命名。
193
+ - 凭据备份使用 `/dsh-subscriptions/vault/export` 和 `/vault/import`;旧路由 `/export`、`/import`、`/export-backup`、`/import-backup` 已移除。
193
194
  - 错误缓冲区和诊断报告会在存储或返回前脱敏 bearer 令牌、API 密钥、JWT 和 `key=value` 凭据。
194
195
 
195
196
 
package/lib/accounts.js CHANGED
@@ -380,6 +380,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
380
380
  pacePerHour: info.pacePerHour || 0,
381
381
  maxConcurrency: slot.maxConcurrency !== undefined ? slot.maxConcurrency : (curConfig.accountMaxConcurrency || 1),
382
382
  maxQueueSize: slot.maxQueueSize > 0 ? Number(slot.maxQueueSize) : (curConfig.maxQueueSize || 20),
383
+ vipOnly: !!slot.vipOnly,
384
+ tags: Array.isArray(slot.tags) ? slot.tags : [],
385
+ weight: Number(slot.weight) || 0,
386
+ isPro: !!slot.isPro,
383
387
  })
384
388
  }
385
389
  return out
package/lib/client.js CHANGED
@@ -68,23 +68,28 @@ window.__ModuleLoader__.load({
68
68
  } catch { /* invalid url */ }
69
69
  return ''
70
70
  }
71
+
72
+ async function copyText(text) {
73
+ try {
74
+ if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return false
75
+ await navigator.clipboard.writeText(String(text == null ? '' : text))
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
80
+ }
71
81
 
72
82
  const en = {
73
83
  "syncModels": "Sync Models",
74
84
  "syncModelsTitle": "Discover and synchronize available models from vendor API",
75
85
  "modelsSynced": "Models synchronized",
76
- "doctorDiagnosis": "Doctor Verdict",
77
86
  "recommendation": "Recommendation",
78
- "pingTitle": "Measure network latency to provider API",
79
87
  "notConnected": "Not connected",
80
88
  "verifyAccount": "Verify account",
81
89
  "coolingDown": "Cooling down",
82
90
  "usageFull": "Usage 100%",
83
91
  "connected": "Connected",
84
92
  "title": "Subscriptions",
85
- "cardIntro": "Subscription accounts, rotation and limits.",
86
- "show": "Show",
87
- "hide": "Hide",
88
93
  "intro": "Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.",
89
94
  "useOrigin": "Use this Web UI origin as OAuth redirect_uri",
90
95
  "useOriginHint": "Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.",
@@ -125,8 +130,6 @@ window.__ModuleLoader__.load({
125
130
  "copilotCliPathHint": "Optional path to copilot or github-copilot-cli binary.",
126
131
  "ollamaFallback": "Ollama local fallback",
127
132
  "ollamaFallbackHint": "Automatically route queries to a local Ollama instance when cloud quota is exhausted.",
128
- "ollamaBaseUrl": "Ollama Base URL",
129
- "ollamaFallbackModel": "Ollama Fallback Model",
130
133
  "advancedSettings": "Advanced Settings & Monitoring",
131
134
  "cooldownMs": "Failure cooldown (minutes)",
132
135
  "cooldownMsHint": "How long to pause a failing or rate-limited account before retrying.",
@@ -136,12 +139,10 @@ window.__ModuleLoader__.load({
136
139
  "notifyLimitsHint": "Show notification alerts when account usage reaches quota ceiling.",
137
140
  "hostOnline": "Host online",
138
141
  "hostOffline": "Host unreachable",
139
- "statsTitle": "Session Telemetry",
140
142
  "statsRequests": "Successful / Total Requests",
141
143
  "statsLatency": "Avg Response Latency",
142
144
  "statsSuccessRate": "Session Success Rate",
143
145
  "statsLastActivity": "Last Activity",
144
- "smokeTitle": "Connection Smoke Test (Ping)",
145
146
  "smokeDesc": "Test live upstream connectivity and measure round-trip latency to the subscription provider.",
146
147
  "runSmoke": "⚡ Run Smoke Test (Ping)",
147
148
  "smokeTesting": "Pinging…",
@@ -179,10 +180,7 @@ window.__ModuleLoader__.load({
179
180
  "capabilitiesHint": "Subscription capabilities, context window and model features",
180
181
  "orPasteVaultPayload": "or paste vault payload below",
181
182
  "workspaceTitle": "Organization / Workspace",
182
- "workspaceSelect": "Select active organization/workspace",
183
- "personalWorkspace": "Personal Default",
184
183
  "windowResetsAt": "Window resets at",
185
- "quotaRecovery": "Quota recovery",
186
184
  "toastCooldownRecovered": "is now available (cooldown expired)",
187
185
  "modelsCatalogTitle": "Curated Subscription Models",
188
186
  "modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
@@ -201,7 +199,6 @@ window.__ModuleLoader__.load({
201
199
  "resetCreditsHint": "ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.",
202
200
  "resetCreditsAction": "Reset ChatGPT Limit",
203
201
  "usageLimitsTitle": "Usage Limits",
204
- "slotDetailsTitle": "Slot Details",
205
202
  "resetAvailable": "Reset attempts available",
206
203
  "resetExpires": "earliest expires",
207
204
  "resetAck": "I understand one attempt will be consumed",
@@ -222,23 +219,17 @@ window.__ModuleLoader__.load({
222
219
  "tokensSaved": "Saved Tokens",
223
220
  "subsPill": "SUBS",
224
221
  "subsModalTitle": "Subscription Hub",
225
- "subsActiveHero": "Active Subscription",
226
222
  "subsPoolTitle": "Provider Accounts",
227
223
  "subsRefresh": "Refresh",
228
224
  "subsOpenSettings": "All Settings →",
229
- "subsFastMode": "Fast Mode 1.5x",
230
- "subsHealthy": "Healthy",
231
225
  "subsFree": "free",
232
226
  "subsUsed": "used",
233
- "subsModalClose": "Close",
234
227
  "subsLogged": "connected",
235
228
  "subsNotLogged": "not connected",
236
229
  "subsCooldown": "cooldown",
237
- "hudTitle": "Subscription balance",
238
230
  "settingsLoading": "Loading settings…",
239
231
  "settingsUnavailable": "Settings are unavailable",
240
232
  "settingsRetry": "Retry",
241
- "hudHint": "drag · edge docks · click refresh",
242
233
  "fcCalibrating": "calibrating…",
243
234
  "fcIdle": "no usage",
244
235
  "fcHours": "h",
@@ -266,23 +257,14 @@ window.__ModuleLoader__.load({
266
257
  "addAccount": "+ Add account",
267
258
  "save": "Save",
268
259
  "saved": "Saved",
269
- "loading": "Loading…",
270
- "unavailable": "Settings are unavailable",
271
260
  "accountLabel": "Account",
272
261
  "plan": "Plan",
273
262
  "storedAs": "Stored as",
274
- "switchLabel": "Provider",
275
- "chipActive": "Subscriptions",
276
- "expiryLabel": "expires",
277
263
  "slashLogin": "Login to a subscription provider",
278
264
  "slashLogout": "Log out a subscription provider",
279
- "slashStatus": "Subscription status",
280
- "none": "none",
281
265
  "forecast": "≈",
282
266
  "windowPrimary": "Primary window",
283
267
  "windowSecondary": "Secondary window",
284
- "resetLabel": "reset",
285
- "check": "Check",
286
268
  "checking": "Checking…",
287
269
  "importToken": "Save token",
288
270
  "importTokenPlace": "Paste token or API key",
@@ -292,7 +274,15 @@ window.__ModuleLoader__.load({
292
274
  "howToGetToken": "❓ Instructions",
293
275
  "howToGetTokenTitle": "How to get token / API key for this provider",
294
276
  "instructionsTitle": "How to obtain token",
295
- "close": "Close",
277
+ "lang": "en",
278
+ "cardIntro": "Subscription accounts, rotation and limits.",
279
+ "loading": "Loading…",
280
+ "unavailable": "Settings are unavailable",
281
+ "error": "Error",
282
+ "settingsError": "Settings error",
283
+ "cancel": "Cancel",
284
+ "quota": "Quota",
285
+ "subs5hWindow": "5h window",
296
286
  "reconnectRequired": "Reconnect required",
297
287
  "reconnectNow": "Re-authenticate now",
298
288
  "scopeWarning": "OAuth scope warning",
@@ -300,8 +290,6 @@ window.__ModuleLoader__.load({
300
290
  "sessionRevokedBadge": "Revoked / Expired",
301
291
  "sessionRevokedWarning": "Session expired / revoked — re-authorization required",
302
292
  "manualTitle": "If the browser did not come back",
303
- "updateChecking": "Checking updates…",
304
- "updateUpToDate": "Up to date",
305
293
  "updateAvailable": "Update available",
306
294
  "updateNow": "Update",
307
295
  "updating": "Updating…",
@@ -326,7 +314,6 @@ window.__ModuleLoader__.load({
326
314
  "backupImportAction": "Import",
327
315
  "backupPassphrase": "Vault Passphrase (min 12 chars)",
328
316
  "backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM. ⚠️ Contains sensitive OAuth refresh tokens; use a strong password (min 12 chars).",
329
- "vaultTokenWarning": "⚠️ Contains sensitive OAuth refresh tokens",
330
317
  "backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
331
318
  "backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
332
319
  "backupExportOk": "Backup exported successfully",
@@ -341,10 +328,7 @@ window.__ModuleLoader__.load({
341
328
  "resetsIn": "Resets in",
342
329
  "toastQuarantined": "Account placed in quarantine",
343
330
  "toastReleased": "Account active and healthy",
344
- "toastQuotaWarning": "Quota threshold exceeded",
345
- "enableToasts": "In-app toast alerts",
346
331
  "headlessTip": "Headless / Remote Server?",
347
- "headlessSshCommand": "SSH Port Forward Command:",
348
332
  "headlessCopySsh": "Copy SSH Command",
349
333
  "headlessCopied": "Copied!",
350
334
  "headlessScanQr": "Scan with Mobile to Authenticate:",
@@ -382,23 +366,14 @@ window.__ModuleLoader__.load({
382
366
  "wizardAll": "Account Wizard",
383
367
  "wizardIncludeNetwork": "Include active network checks (proxy ping & model catalog discovery)",
384
368
  "wizardRunning": "Running diagnostic checklist...",
385
- "wizardStepCreds": "Credentials & Expiry",
386
- "wizardStepProxy": "Proxy & Gateway",
387
- "wizardStepToken": "Token Validation",
388
- "wizardStepCatalog": "Model Catalog Reachability",
389
369
  "wizardHealthy": "Healthy",
390
370
  "wizardDegraded": "Degraded / Warnings",
391
371
  "wizardFailing": "Issues Detected",
392
372
  "wizardRecommendations": "Recommendations & Next Steps",
393
373
  "wizardCopy": "Copy Masked Diagnostic Report",
394
374
  "cachePinnedBadge": "Cache Pinned",
395
- "field.enableToasts": "Show popup toast notifications on quarantine or quota events",
396
375
  "reasoningEffortTitle": "Reasoning Effort Control",
397
376
  "reasoningEffortDesc": "Configure reasoning depth for models with extended thinking capabilities.",
398
- "reasoningLevelLow": "Low",
399
- "reasoningLevelMed": "Medium",
400
- "reasoningLevelHigh": "High",
401
- "reasoningLevelMax": "Max",
402
377
  "reasoningLevelOff": "Off"
403
378
  };
404
379
 
@@ -406,18 +381,13 @@ window.__ModuleLoader__.load({
406
381
  "syncModels": "同步模型",
407
382
  "syncModelsTitle": "从服务商 API 实时发现并同步可用模型列表",
408
383
  "modelsSynced": "模型已同步",
409
- "doctorDiagnosis": "诊断结论",
410
384
  "recommendation": "修复建议",
411
- "pingTitle": "测试到服务商 API 的网络往返延迟",
412
385
  "notConnected": "未连接",
413
386
  "verifyAccount": "需要验证账户",
414
387
  "coolingDown": "冷却等待中",
415
388
  "usageFull": "用量已达 100%",
416
389
  "connected": "已连接",
417
390
  "title": "个人订阅管理",
418
- "cardIntro": "AI 服务订阅账户、自动轮换与速率限制管理。",
419
- "show": "展开",
420
- "hide": "收起",
421
391
  "intro": "通过个人订阅登录。凭据安全保存在服务器端凭据库中,浏览器不回读敏感令牌。点击“连接”后,若页面跳转至 localhost 或服务商页面,请将重定向网址或授权代码粘贴到下方。",
422
392
  "useOrigin": "将当前 Web UI 地址作为 OAuth 重定向地址 (redirect_uri)",
423
393
  "useOriginHint": "除非您为当前域名申请了专属 OAuth 客户端,否则请保持关闭。官方 CLI 客户端通常需使用其公开重定向地址并配合粘贴代码流程。",
@@ -458,8 +428,6 @@ window.__ModuleLoader__.load({
458
428
  "copilotCliPathHint": "可选的 copilot 或 github-copilot-cli 二进制文件路径。",
459
429
  "ollamaFallback": "Ollama 本地模型自动兜底",
460
430
  "ollamaFallbackHint": "当云端配额耗尽或服务商故障时,自动将请求转发给本地 Ollama 实例。",
461
- "ollamaBaseUrl": "Ollama 服务基础 URL",
462
- "ollamaFallbackModel": "Ollama 兜底模型名称",
463
431
  "advancedSettings": "高级配置与监控",
464
432
  "cooldownMs": "故障与限流冷却时间(分钟)",
465
433
  "cooldownMsHint": "当账户遇到 429 或故障时暂停该账户的分钟数。",
@@ -469,12 +437,10 @@ window.__ModuleLoader__.load({
469
437
  "notifyLimitsHint": "当账户使用量达到上限时触发系统通知提醒。",
470
438
  "hostOnline": "主机在线",
471
439
  "hostOffline": "主机无法连接",
472
- "statsTitle": "会话遥测统计",
473
440
  "statsRequests": "成功 / 总请求数",
474
441
  "statsLatency": "平均响应延迟",
475
442
  "statsSuccessRate": "会话成功率",
476
443
  "statsLastActivity": "最近活跃时间",
477
- "smokeTitle": "链路连通性冒烟测试 (Ping)",
478
444
  "smokeDesc": "向当前订阅服务商发起实时探活并测量双向网络往返延迟 (RTT)。",
479
445
  "runSmoke": "⚡ 执行冒烟探活 (Ping)",
480
446
  "smokeTesting": "正在探活…",
@@ -512,10 +478,7 @@ window.__ModuleLoader__.load({
512
478
  "capabilitiesHint": "订阅账号模型上下文窗口与特权特性",
513
479
  "orPasteVaultPayload": "或在下方粘贴保险库文本",
514
480
  "workspaceTitle": "工作区与组织",
515
- "workspaceSelect": "选择生效的工作区或组织",
516
- "personalWorkspace": "个人默认",
517
481
  "windowResetsAt": "额度重置于",
518
- "quotaRecovery": "额度恢复",
519
482
  "toastCooldownRecovered": "已解除冷却恢复可用",
520
483
  "modelsCatalogTitle": "精选订阅模型",
521
484
  "modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
@@ -534,7 +497,6 @@ window.__ModuleLoader__.load({
534
497
  "resetCreditsHint": "ChatGPT 重置卡:紧急重置 5 小时额度窗口。扣除前需等待 5 秒倒计时确认。",
535
498
  "resetCreditsAction": "重置 ChatGPT 限制",
536
499
  "usageLimitsTitle": "用量限制",
537
- "slotDetailsTitle": "槽位详情",
538
500
  "resetAvailable": "可用重置次数",
539
501
  "resetExpires": "最早到期时间",
540
502
  "resetAck": "我确认将消耗一次重置机会",
@@ -555,23 +517,17 @@ window.__ModuleLoader__.load({
555
517
  "tokensSaved": "已节省 Token",
556
518
  "subsPill": "订阅",
557
519
  "subsModalTitle": "订阅管理中心",
558
- "subsActiveHero": "当前主用订阅",
559
520
  "subsPoolTitle": "服务商账户池",
560
521
  "subsRefresh": "刷新",
561
522
  "subsOpenSettings": "全部设置 →",
562
- "subsFastMode": "加速模式 1.5x",
563
- "subsHealthy": "健康运行中",
564
523
  "subsFree": "空闲可用",
565
524
  "subsUsed": "已用",
566
- "subsModalClose": "关闭",
567
525
  "subsLogged": "已连接",
568
526
  "subsNotLogged": "未连接",
569
527
  "subsCooldown": "冷却中",
570
- "hudTitle": "订阅额度余量",
571
528
  "settingsLoading": "正在加载设置…",
572
529
  "settingsUnavailable": "设置加载失败",
573
530
  "settingsRetry": "重试",
574
- "hudHint": "可拖拽 · 边缘停靠 · 点击刷新",
575
531
  "fcCalibrating": "校准中…",
576
532
  "fcIdle": "暂无消耗",
577
533
  "fcHours": "小时",
@@ -599,23 +555,14 @@ window.__ModuleLoader__.load({
599
555
  "addAccount": "+ 添加账户",
600
556
  "save": "保存",
601
557
  "saved": "已保存",
602
- "loading": "加载中…",
603
- "unavailable": "设置不可用",
604
558
  "accountLabel": "账户",
605
559
  "plan": "套餐方案",
606
560
  "storedAs": "存储标识",
607
- "switchLabel": "切换服务商",
608
- "chipActive": "已启用订阅",
609
- "expiryLabel": "到期时间",
610
561
  "slashLogin": "登录订阅服务商",
611
562
  "slashLogout": "登出订阅服务商",
612
- "slashStatus": "查看订阅状态",
613
- "none": "无",
614
563
  "forecast": "≈",
615
564
  "windowPrimary": "主配额窗口",
616
565
  "windowSecondary": "次配额窗口",
617
- "resetLabel": "重置倒计时",
618
- "check": "检测",
619
566
  "checking": "检测中…",
620
567
  "importToken": "保存令牌",
621
568
  "importTokenPlace": "粘贴令牌或 API Key",
@@ -625,7 +572,15 @@ window.__ModuleLoader__.load({
625
572
  "howToGetToken": "❓ 获取指引",
626
573
  "howToGetTokenTitle": "如何获取此服务商的令牌或 API 密钥",
627
574
  "instructionsTitle": "获取令牌指引",
628
- "close": "关闭",
575
+ "lang": "zh",
576
+ "cardIntro": "AI 服务订阅账户、自动轮换与速率限制管理。",
577
+ "loading": "加载中…",
578
+ "unavailable": "设置不可用",
579
+ "error": "错误",
580
+ "settingsError": "设置错误",
581
+ "cancel": "取消",
582
+ "quota": "配额",
583
+ "subs5hWindow": "5小时窗口",
629
584
  "reconnectRequired": "需要重新连接",
630
585
  "reconnectNow": "立即重新认证",
631
586
  "scopeWarning": "OAuth权限范围警告",
@@ -633,8 +588,6 @@ window.__ModuleLoader__.load({
633
588
  "sessionRevokedBadge": "已失效 / 过期",
634
589
  "sessionRevokedWarning": "会话已过期或失效 — 需要重新授权",
635
590
  "manualTitle": "若浏览器未能自动跳转回调",
636
- "updateChecking": "检查更新中…",
637
- "updateUpToDate": "已是最新版本",
638
591
  "updateAvailable": "有新版本可用",
639
592
  "updateNow": "立即更新",
640
593
  "updating": "正在更新…",
@@ -659,7 +612,6 @@ window.__ModuleLoader__.load({
659
612
  "backupImportAction": "导入",
660
613
  "backupPassphrase": "凭据库密码(至少 12 位)",
661
614
  "backupExportHint": "使用 AES-256-GCM 安全加密导出所有订阅账户与配置。⚠️ 包含敏感的 OAuth 刷新令牌,请使用至少 12 位的强密码保护。",
662
- "vaultTokenWarning": "⚠️ 包含敏感的 OAuth 刷新令牌",
663
615
  "backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
664
616
  "backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
665
617
  "backupExportOk": "备份导出成功",
@@ -674,7 +626,6 @@ window.__ModuleLoader__.load({
674
626
  "resetsIn": "重置倒计时",
675
627
  "toastQuarantined": "账户进入隔离期",
676
628
  "headlessTip": "无头环境 / 远程服务器?",
677
- "headlessSshCommand": "SSH 端口转发命令:",
678
629
  "headlessCopySsh": "复制 SSH 命令",
679
630
  "headlessCopied": "已复制!",
680
631
  "headlessScanQr": "使用手机扫描以登录:",
@@ -687,8 +638,6 @@ window.__ModuleLoader__.load({
687
638
  "vaultSyncLastPush": "上次推送:",
688
639
  "vaultSyncLastPull": "上次拉取:",
689
640
  "toastReleased": "账户已解除隔离恢复使用",
690
- "toastQuotaWarning": "额度预警",
691
- "enableToasts": "应用内弹窗提醒",
692
641
  "groupQueue": "并发限制与请求队列",
693
642
  "enableQueue": "启用并发请求排队",
694
643
  "enableQueueHint": "当账户达到并发上限时将多余请求排队等待,避免直接触发 429 报错",
@@ -715,23 +664,14 @@ window.__ModuleLoader__.load({
715
664
  "wizardAll": "诊断向导",
716
665
  "wizardIncludeNetwork": "包含活跃网络探测(代理连通性与模型目录探测)",
717
666
  "wizardRunning": "正在执行诊断检查清单...",
718
- "wizardStepCreds": "凭据完整性与有效期",
719
- "wizardStepProxy": "网络代理与网关",
720
- "wizardStepToken": "令牌有效性验证",
721
- "wizardStepCatalog": "模型目录可达性",
722
667
  "wizardHealthy": "健康正常",
723
668
  "wizardDegraded": "降级预警",
724
669
  "wizardFailing": "发现故障",
725
670
  "wizardRecommendations": "排查建议与修复步骤",
726
671
  "wizardCopy": "复制脱敏诊断报告",
727
672
  "cachePinnedBadge": "缓存粘性",
728
- "field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒",
729
673
  "reasoningEffortTitle": "推理强度控制",
730
674
  "reasoningEffortDesc": "为具备扩展思考能力的模型配置推理深度与思考预算。",
731
- "reasoningLevelLow": "低",
732
- "reasoningLevelMed": "中",
733
- "reasoningLevelHigh": "高",
734
- "reasoningLevelMax": "最高",
735
675
  "reasoningLevelOff": "关闭"
736
676
  };
737
677
  // #51: live countdown to the quota window reset (account.quota.resetAt).
@@ -971,7 +911,7 @@ window.__ModuleLoader__.load({
971
911
  if (d && d.ok && d.report) setDiagReport(d.report)
972
912
  const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
973
913
  setDiag(txt)
974
- try { navigator.clipboard.writeText(txt) } catch { /* clipboard write disallowed */ }
914
+ copyText(txt)
975
915
  })
976
916
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
977
917
  }
@@ -993,7 +933,7 @@ window.__ModuleLoader__.load({
993
933
  fetch('/dsh-subscriptions/vault/sync/status')
994
934
  .then((r) => r.json())
995
935
  .then((d) => { if (d && d.ok) setVaultSyncInfo(d.status) })
996
- .catch(() => {})
936
+ .catch((e) => setBackupErr(cleanErrorMessage(e && e.message ? e.message : e)))
997
937
  }
998
938
 
999
939
  const doVaultSyncPush = async () => {
@@ -1064,7 +1004,7 @@ window.__ModuleLoader__.load({
1064
1004
  const doExportBackup = async () => {
1065
1005
  setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
1066
1006
  try {
1067
- const res = await fetch('/dsh-subscriptions/export-backup', {
1007
+ const res = await fetch('/dsh-subscriptions/vault/export', {
1068
1008
  method: 'POST',
1069
1009
  headers: { 'Content-Type': 'application/json' },
1070
1010
  body: JSON.stringify({ passphrase: backupPass, algorithm: 'pbkdf2' }),
@@ -1082,7 +1022,7 @@ window.__ModuleLoader__.load({
1082
1022
  const doImportBackup = async () => {
1083
1023
  setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
1084
1024
  try {
1085
- const res = await fetch('/dsh-subscriptions/import-backup', {
1025
+ const res = await fetch('/dsh-subscriptions/vault/import', {
1086
1026
  method: 'POST',
1087
1027
  headers: { 'Content-Type': 'application/json' },
1088
1028
  body: JSON.stringify({ passphrase: backupPass, vault: backupData.trim() }),
@@ -1229,6 +1169,19 @@ window.__ModuleLoader__.load({
1229
1169
  const [err, setErr] = React.useState('')
1230
1170
  const [info, setInfo] = React.useState('')
1231
1171
  const [revision, setRevision] = React.useState(null)
1172
+ const timersRef = React.useRef(new Set())
1173
+ React.useEffect(() => () => {
1174
+ for (const id of timersRef.current) clearTimeout(id)
1175
+ timersRef.current.clear()
1176
+ }, [])
1177
+ const later = (fn, ms) => {
1178
+ const id = setTimeout(() => {
1179
+ timersRef.current.delete(id)
1180
+ fn()
1181
+ }, ms)
1182
+ timersRef.current.add(id)
1183
+ return id
1184
+ }
1232
1185
 
1233
1186
  const applyPayload = (data) => {
1234
1187
  if (data && data.revision != null) setRevision(data.revision)
@@ -1238,19 +1191,29 @@ window.__ModuleLoader__.load({
1238
1191
  setModelsCatalog((data && data.modelsCatalog) || [])
1239
1192
  }
1240
1193
 
1194
+ let reloadSeq = 0
1195
+ let reloadAbort = null
1241
1196
  const reload = () => {
1197
+ const seq = ++reloadSeq
1198
+ if (reloadAbort) reloadAbort.abort()
1199
+ const controller = new AbortController()
1200
+ reloadAbort = controller
1242
1201
  const t0 = Date.now()
1243
- return fetch('/dsh-subscriptions/config', { cache: 'no-store' })
1202
+ return fetch('/dsh-subscriptions/config', { cache: 'no-store', signal: controller.signal })
1244
1203
  .then((res) => res.json())
1245
1204
  .then((data) => {
1205
+ if (seq !== reloadSeq) return data
1246
1206
  setHostPing({ ok: true, latencyMs: Date.now() - t0 })
1247
1207
  applyPayload(data)
1248
1208
  reloadTelemetry()
1249
1209
  return data
1250
1210
  })
1251
1211
  .catch((err) => {
1252
- setHostPing({ ok: false, latencyMs: null })
1253
- throw err
1212
+ if (seq === reloadSeq && err && err.name !== 'AbortError') {
1213
+ setHostPing({ ok: false, latencyMs: null })
1214
+ throw err
1215
+ }
1216
+ return null
1254
1217
  })
1255
1218
  }
1256
1219
 
@@ -1258,7 +1221,10 @@ window.__ModuleLoader__.load({
1258
1221
  let alive = true
1259
1222
  reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
1260
1223
  checkUpdater().catch(() => {})
1261
- return () => { alive = false }
1224
+ return () => {
1225
+ alive = false
1226
+ if (reloadAbort) reloadAbort.abort()
1227
+ }
1262
1228
  }, [])
1263
1229
 
1264
1230
  // #90: poll device login while any slot is in 'pending' state.
@@ -1313,7 +1279,7 @@ window.__ModuleLoader__.load({
1313
1279
  throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1314
1280
  }
1315
1281
  applyPayload(data)
1316
- setSaved(true); setTimeout(() => setSaved(false), 2000)
1282
+ setSaved(true); later(() => setSaved(false), 2000)
1317
1283
  }
1318
1284
 
1319
1285
  const connect = async (provider, index) => {
@@ -1438,7 +1404,7 @@ window.__ModuleLoader__.load({
1438
1404
  if (!res.ok) throw new Error((data.error && data.error.message) || ("HTTP " + res.status))
1439
1405
  setAccounts(data.accounts || [])
1440
1406
  setInfo(t("toastQuarantined"))
1441
- setTimeout(() => setInfo(""), 3000)
1407
+ later(() => setInfo(""), 3000)
1442
1408
  } catch (e) { setErr(cleanErrorMessage(e.message || e)) }
1443
1409
  }
1444
1410
 
@@ -1457,7 +1423,7 @@ window.__ModuleLoader__.load({
1457
1423
  setAccounts(data.accounts || [])
1458
1424
  if (data.success || data.released) {
1459
1425
  setInfo(t("toastReleased"))
1460
- setTimeout(() => setInfo(""), 3000)
1426
+ later(() => setInfo(""), 3000)
1461
1427
  } else if (data.error) {
1462
1428
  setErr(cleanErrorMessage(data.error))
1463
1429
  }
@@ -1509,7 +1475,7 @@ window.__ModuleLoader__.load({
1509
1475
  if (data.config) applyPayload(data)
1510
1476
  else await reload()
1511
1477
  setInfo(t('importLocalSuccess'))
1512
- setTimeout(() => setInfo(''), 3000)
1478
+ later(() => setInfo(''), 3000)
1513
1479
  }
1514
1480
 
1515
1481
  const importToken = async (provider, index) => {
@@ -2160,7 +2126,7 @@ window.__ModuleLoader__.load({
2160
2126
  diag ? h('div', { className: 'dsub-row' },
2161
2127
  h('button', {
2162
2128
  type: 'button', className: 'dsub-mini',
2163
- onClick: () => { try { navigator.clipboard.writeText(diag) } catch { /* clipboard write error */ } },
2129
+ onClick: () => { copyText(diag) },
2164
2130
  }, t('diagCopy')),
2165
2131
  h('pre', { className: 'dsub-diag' }, diag),
2166
2132
  ) : null,
@@ -2255,7 +2221,7 @@ window.__ModuleLoader__.load({
2255
2221
  className: 'dsub-mini',
2256
2222
  onClick: () => {
2257
2223
  const txt = JSON.stringify(wizardModal.report, null, 2)
2258
- try { navigator.clipboard.writeText(txt) } catch { /* ignore */ }
2224
+ copyText(txt)
2259
2225
  },
2260
2226
  }, '📋 ' + t('wizardCopy')),
2261
2227
  ),
@@ -2327,7 +2293,7 @@ window.__ModuleLoader__.load({
2327
2293
  h('button', {
2328
2294
  type: 'button',
2329
2295
  className: 'dsub-mini',
2330
- onClick: () => { try { navigator.clipboard.writeText(backupData) } catch { /* ignore */ } },
2296
+ onClick: () => { copyText(backupData) },
2331
2297
  }, t('diagCopy')),
2332
2298
  h('button', {
2333
2299
  type: 'button',
@@ -2504,11 +2470,11 @@ window.__ModuleLoader__.load({
2504
2470
  type: 'button',
2505
2471
  className: 'dsub-mini',
2506
2472
  onClick: () => {
2507
- try {
2508
- navigator.clipboard.writeText(headlessInfo[key].sshTunnelCommand)
2473
+ copyText(headlessInfo[key].sshTunnelCommand).then((ok) => {
2474
+ if (!ok) return
2509
2475
  setCopiedSsh((m) => Object.assign({}, m, { [key]: true }))
2510
- setTimeout(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
2511
- } catch { /* ignore */ }
2476
+ later(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
2477
+ })
2512
2478
  }
2513
2479
  }, copiedSsh[key] ? ('✓ ' + t('headlessCopied')) : ('📋 ' + t('headlessCopySsh')))
2514
2480
  ),
@@ -2580,7 +2546,7 @@ window.__ModuleLoader__.load({
2580
2546
  stepsEn: ['Paste your token or API key into the field above and click "Save token".'],
2581
2547
  };
2582
2548
  // Determine language safely from current locale (fallback to zh if Chinese or en)
2583
- const isZh = t('instructionsTitle').indexOf('获取') >= 0 || t('close') === '关闭';
2549
+ const isZh = t('lang') === 'zh';
2584
2550
  const steps = (isZh && info.stepsZh) ? info.stepsZh : (info.stepsEn || []);
2585
2551
  return h('div', { className: 'dsub-helpBox' },
2586
2552
  h('div', { className: 'dsub-helpHead' },
@@ -2676,7 +2642,7 @@ window.__ModuleLoader__.load({
2676
2642
  h('div', { className: 'dsub-row' },
2677
2643
  h('button', {
2678
2644
  type: 'button', className: 'dsub-mini',
2679
- onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch { /* clipboard write error */ } },
2645
+ onClick: () => { copyText(d.userCode) },
2680
2646
  }, t('deviceCopy')),
2681
2647
  h('a', { href: safeExternalUrl(d.authUrl) || undefined, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
2682
2648
  ),
@@ -2846,7 +2812,7 @@ window.__ModuleLoader__.load({
2846
2812
  var agoStr = null
2847
2813
  if (at) {
2848
2814
  var m = Math.round((Date.now() - at) / 60000)
2849
- if (m >= 1) agoStr = m + 'm ' + (t('close') === '关闭' ? '前' : 'ago')
2815
+ if (m >= 1) agoStr = m + 'm ' + (t('lang') === 'zh' ? '前' : 'ago')
2850
2816
  }
2851
2817
 
2852
2818
  return h('div', { className: 'dsub-panel-box' },
@@ -3122,47 +3088,9 @@ window.__ModuleLoader__.load({
3122
3088
  expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
3123
3089
  composerQuota: (data && data.composerQuota) || 'off',
3124
3090
  active: (data && data.active) || null,
3091
+ error: null,
3125
3092
  }
3126
- } catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
3127
- }
3128
-
3129
- // #84: compact quota indicator of the active subscription in the input area.
3130
- // The runway forecast uses a rolling window of /status samples.
3131
- const fcSamples = { windows: {} }
3132
-
3133
- function fcObserve(key, remaining, now) {
3134
- const windows = fcSamples.windows
3135
- const prev = windows[key]
3136
- const samples = (prev && prev.samples) || []
3137
- const last = samples[samples.length - 1]
3138
- if (!last || (now > last.at && (Math.abs(remaining - last.pct) >= 0.1 || now - last.at >= 15 * 60 * 1000))) {
3139
- samples.push({ at: now, pct: remaining })
3140
- }
3141
- windows[key] = { samples: samples.filter((x) => x.at >= now - 24 * 60 * 60 * 1000).slice(-192) }
3142
- }
3143
-
3144
- function fcEstimate(key, remaining) {
3145
- const rec = fcSamples.windows[key]
3146
- if (!rec) return { status: 'calibrating' }
3147
- const samples = rec.samples
3148
- if (samples.length < 3) return { status: 'calibrating' }
3149
- const first = samples[0]
3150
- const last = samples[samples.length - 1]
3151
- const span = last.at - first.at
3152
- const consumed = first.pct - last.pct
3153
- if (span < 30 * 60 * 1000 || consumed < 1) return { status: 'calibrating' }
3154
- const t0 = first.at
3155
- let sw = 0, sx = 0, sy = 0, sxx = 0, sxy = 0
3156
- for (const x of samples) {
3157
- const wx = (x.at - t0) / 3600000
3158
- const wy = first.pct - x.pct
3159
- const w = Math.exp((x.at - last.at) / (6 * 3600000))
3160
- sw += w; sx += w * wx; sy += w * wy; sxx += w * wx * wx; sxy += w * wx * wy
3161
- }
3162
- const den = sw * sxx - sx * sx
3163
- const pace = den > 0 ? (sw * sxy - sx * sy) / den : 0
3164
- if (!Number.isFinite(pace) || pace < 0.02) return { status: 'idle' }
3165
- return { status: 'ready', runwaySeconds: Math.round((remaining / pace) * 3600) }
3093
+ } catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null, error: 'status unavailable' } }
3166
3094
  }
3167
3095
 
3168
3096
  function ComposerQuota(props) {
@@ -3171,15 +3099,25 @@ window.__ModuleLoader__.load({
3171
3099
  React.useEffect(() => {
3172
3100
  let alive = true
3173
3101
  const pull = () => {
3174
- refreshLoggedIn().then((r) => {
3102
+ Promise.all([
3103
+ refreshLoggedIn(),
3104
+ fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
3105
+ .then((res) => res.json())
3106
+ .catch(() => ({})),
3107
+ ]).then(([r, analyticsPayload]) => {
3175
3108
  if (!alive) return
3176
3109
  const mode = r.composerQuota || 'off'
3177
3110
  const a = r.active
3178
3111
  if (mode !== 'off' && a && a.provider && a.provider !== 'ollama' && a.usagePercent != null) {
3179
3112
  const remaining = Math.max(0, 100 - a.usagePercent)
3180
- const key = 'subs:' + a.provider
3181
- fcObserve(key, remaining, Date.now())
3182
- const est = fcEstimate(key, remaining, Date.now())
3113
+ const forecast = (analyticsPayload && analyticsPayload.analytics && analyticsPayload.analytics.forecast) || []
3114
+ const row = forecast.find((f) => f.provider === a.provider && (!f.ref || !a.ref || f.ref === a.ref))
3115
+ let est = { status: 'calibrating' }
3116
+ if (row) {
3117
+ est = row.hoursToExhaustion == null
3118
+ ? { status: 'idle' }
3119
+ : { status: 'forecast', runwaySeconds: Math.max(0, Number(row.hoursToExhaustion) * 3600) }
3120
+ }
3183
3121
  setState({ mode, active: a, remaining, est })
3184
3122
  } else {
3185
3123
  setState({ mode, active: a, remaining: null, est: null })
@@ -3287,9 +3225,15 @@ window.__ModuleLoader__.load({
3287
3225
  return false
3288
3226
  }
3289
3227
 
3228
+ let pluginSettingsGeneration = 0
3290
3229
  function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms), navCtx) {
3291
3230
  try {
3292
3231
  if (findAndClickCard(doc)) return
3232
+ const generation = ++pluginSettingsGeneration
3233
+ const runIfCurrent = (fn, ms) => schedule(() => {
3234
+ if (generation !== pluginSettingsGeneration) return
3235
+ fn()
3236
+ }, ms)
3293
3237
 
3294
3238
  const activeLabels = resolvePluginLabels(navCtx)
3295
3239
  const checkEl = (el) => matchesPluginLabel(el, activeLabels)
@@ -3300,9 +3244,9 @@ window.__ModuleLoader__.load({
3300
3244
  panel.click()
3301
3245
  const poll = (left) => {
3302
3246
  if (findAndClickCard(doc)) return
3303
- if (left > 0) schedule(() => poll(left - 1), 100)
3247
+ if (left > 0) runIfCurrent(() => poll(left - 1), 100)
3304
3248
  }
3305
- schedule(() => poll(40), 100)
3249
+ runIfCurrent(() => poll(40), 100)
3306
3250
  return
3307
3251
  }
3308
3252
 
@@ -3318,15 +3262,15 @@ window.__ModuleLoader__.load({
3318
3262
  section.click()
3319
3263
  const poll = (cLeft) => {
3320
3264
  if (findAndClickCard(doc)) return
3321
- if (cLeft > 0) schedule(() => poll(cLeft - 1), 100)
3265
+ if (cLeft > 0) runIfCurrent(() => poll(cLeft - 1), 100)
3322
3266
  }
3323
- schedule(() => poll(40), 100)
3267
+ runIfCurrent(() => poll(40), 100)
3324
3268
  return
3325
3269
  }
3326
3270
  } catch { /* bestEffort */ }
3327
- if (left > 0) schedule(() => pollSection(left - 1), 80)
3271
+ if (left > 0) runIfCurrent(() => pollSection(left - 1), 80)
3328
3272
  }
3329
- schedule(() => pollSection(20), 80)
3273
+ runIfCurrent(() => pollSection(20), 80)
3330
3274
  }
3331
3275
  }
3332
3276
  } catch { /* bestEffort */ }
@@ -3340,6 +3284,13 @@ window.__ModuleLoader__.load({
3340
3284
  const dismissToast = (id) => {
3341
3285
  setToasts((prev) => prev.filter((t) => t.id !== id))
3342
3286
  }
3287
+ const reportError = (message) => {
3288
+ const text = cleanErrorMessage(message)
3289
+ if (!text) return
3290
+ setToasts((prev) => prev.some((toast) => toast.message === text)
3291
+ ? prev
3292
+ : [{ id: 'err_' + Date.now(), message: text, cls: 'dsub-toast-bad' }, ...prev].slice(0, 3))
3293
+ }
3343
3294
 
3344
3295
  const pollAlerts = () => {
3345
3296
  fetch('/dsh-subscriptions/alerts?limit=5', { cache: 'no-store' })
@@ -3359,22 +3310,25 @@ window.__ModuleLoader__.load({
3359
3310
  }
3360
3311
  }
3361
3312
  })
3362
- .catch(() => {})
3313
+ .catch((e) => reportError(t('hostOffline') + ': ' + String(e && e.message || e)))
3363
3314
  }
3364
3315
 
3365
3316
  const pull = () => {
3366
3317
  setState((s) => Object.assign({}, s, { loading: true }))
3367
3318
  refreshLoggedIn().then((r) => {
3319
+ if (r.error) reportError(t('hostOffline') + ': ' + r.error)
3368
3320
  const logged = r.logged || []
3369
3321
  fetch('/dsh-subscriptions/config', { cache: 'no-store' })
3370
3322
  .then((res) => res.json())
3371
3323
  .then((cfg) => {
3372
3324
  setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], active: r.active, loading: false }))
3373
3325
  })
3374
- .catch(() => {
3326
+ .catch((e) => {
3327
+ reportError(t('hostOffline') + ': ' + String(e && e.message || e))
3375
3328
  setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: [], active: r.active, loading: false }))
3376
3329
  })
3377
- }).catch(() => {
3330
+ }).catch((e) => {
3331
+ reportError(t('hostOffline') + ': ' + String(e && e.message || e))
3378
3332
  setState((s) => Object.assign({}, s, { loading: false }))
3379
3333
  })
3380
3334
  }
@@ -3684,7 +3638,7 @@ window.__ModuleLoader__.load({
3684
3638
  if (target) {
3685
3639
  const popup = window.open(target, '_blank', 'noopener')
3686
3640
  if (!popup) {
3687
- try { navigator.clipboard.writeText(target) } catch { /* clipboard blocked */ }
3641
+ copyText(target)
3688
3642
  }
3689
3643
  }
3690
3644
  }).catch(() => {})
@@ -3725,7 +3679,7 @@ window.__ModuleLoader__.load({
3725
3679
  }, 'dsh-subscriptions: slash commands')
3726
3680
  }
3727
3681
 
3728
- exports.inject = ['slots', 'locale', 'sessions']
3682
+ exports.inject = ['slots', 'locale']
3729
3683
  exports.apply = function apply(ctx) {
3730
3684
  if (typeof window !== 'undefined') {
3731
3685
  window.__dshCtx = ctx
@@ -26,6 +26,14 @@ export const Slot = z.object({
26
26
  .description('#590 Optional monthly subscription cost in USD for analytics. Defaults to 20.'),
27
27
  maxQueueSize: z.number().default(0)
28
28
  .description('#592 Per-account queue depth before overflow. 0 = use the global maxQueueSize.'),
29
+ vipOnly: z.boolean().default(false)
30
+ .description('#175 Reserve this account for VIP requests only.'),
31
+ tags: z.array(z.string()).default([])
32
+ .description('#173 Tags used by tag-filtered rotation.'),
33
+ weight: z.number().default(0)
34
+ .description('#171 Weighted rotation priority. 0 = derive from isPro.'),
35
+ isPro: z.boolean().default(false)
36
+ .description('#171 Treat this account as Pro for weighted rotation.'),
29
37
  })
30
38
 
31
39
  export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
package/lib/http.js CHANGED
@@ -282,8 +282,23 @@ export function isSameOrigin(url1, url2) {
282
282
  }
283
283
  }
284
284
 
285
+ export function headerSafeValue(value) {
286
+ return String(value == null ? '' : value)
287
+ .replace(/[\r\n]+/g, ' ')
288
+ .replace(/[^\t\x20-\x7e\x80-\xff]/g, '')
289
+ }
290
+
291
+ export function sanitizeHeaderValues(headers) {
292
+ const out = {}
293
+ for (const [key, value] of Object.entries(headers || {})) {
294
+ if (value === undefined || value === null) continue
295
+ out[key] = headerSafeValue(value)
296
+ }
297
+ return out
298
+ }
299
+
285
300
  export function sanitizeRequestHeaders(url, init = {}, options = {}) {
286
- const headers = { ...(init.headers || {}) }
301
+ const headers = sanitizeHeaderValues(init.headers || {})
287
302
  if (!url) return headers
288
303
  try {
289
304
  const parsed = new URL(url)
package/lib/index.js CHANGED
@@ -11,7 +11,7 @@ import { webCallbackUri } from './oauth.js'
11
11
  import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/index.js'
12
12
  import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
13
13
  import { parseBlob } from './blob.js'
14
- import { createVendorFromProfile, validateProfile } from './vendor-factory.js'
14
+ import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
15
15
  import { registerCustomProviderIds, registerDisplayName } from './refs.js'
16
16
  import { createSubscriptionsService } from './subscriptions.js'
17
17
  import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
@@ -86,13 +86,7 @@ export function apply(ctx, rawConfig) {
86
86
  const live = () => cachedConfig
87
87
 
88
88
  function syncCustomVendors() {
89
- let profiles
90
- try {
91
- profiles = (live().customVendors || []).map(validateProfile)
92
- } catch (e) {
93
- if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors: ' + String(e && e.message || e))
94
- return
95
- }
89
+ const profiles = validateProfiles(live().customVendors)
96
90
  clearCustomVendors()
97
91
  for (const profile of profiles) {
98
92
  try {
@@ -5,11 +5,10 @@ import { normalizeSlots, vendorConfig } from '../accounts.js'
5
5
  import { getVendor } from '../vendors/index.js'
6
6
  import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
7
7
  import { Config, plainConfig, publicConfig, revisionOf } from '../config-schema.js'
8
- import { decryptWithPassphrase, encryptWithPassphrase } from '../crypto.js'
9
8
  import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
10
9
  import { discoverLocalCliSessions, loadLocalCliBlob } from '../import-auth.js'
11
10
  import { validateCredentialBlob } from '../blob.js'
12
- import { isProvider, oauthRef, parseOauthRef } from '../refs.js'
11
+ import { isProvider, oauthRef } from '../refs.js'
13
12
 
14
13
  export function registerAccountsRoutes(ctx, state) {
15
14
  const {
@@ -308,100 +307,6 @@ export function registerAccountsRoutes(ctx, state) {
308
307
  }), 'dsh-subscriptions: /import-local')
309
308
 
310
309
 
311
- // Export of the encrypted token bundle. Tokens are never logged.
312
- ctx.effect(() => ctx.webServer.register({
313
- kind: 'exact',
314
- path: '/dsh-subscriptions/export',
315
- handler: safeJsonHandler(async (req, res) => {
316
- if (req.method !== 'POST') {
317
- writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
318
- return
319
- }
320
- if (!isTrustedSettingsRequest(req, ctx)) {
321
- writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
322
- return
323
- }
324
- let payload
325
- try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
326
- writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
327
- return
328
- }
329
- const passphrase = String(payload.passphrase || '').trim()
330
- if (!passphrase || passphrase.length < 12) {
331
- writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase must be at least 12 characters' } })
332
- return
333
- }
334
- const algorithm = payload.algorithm === 'scrypt' ? 'scrypt' : (payload.algorithm === 'scrypt-v2' || payload.algorithm === 'scrypt-hardened' ? payload.algorithm : 'pbkdf2')
335
- try {
336
- const accounts = []
337
- for (const slot of normalizeSlots(live().slots)) {
338
- try {
339
- const blob = await store.loadBlob(slot.ref)
340
- accounts.push({ ref: slot.ref, provider: slot.provider, index: slot.index, label: slot.label || blob.label || '', blob })
341
- } catch { /* skip missing */ }
342
- }
343
- if (!accounts.length) {
344
- writeJson(res, 200, { ok: false, error: { code: 'empty', message: 'no connected accounts' } })
345
- return
346
- }
347
- const bundle = JSON.stringify({ v: 1, exportedAt: Date.now(), accounts })
348
- const encrypted = encryptWithPassphrase(bundle, passphrase, { algorithm })
349
- writeJson(res, 200, { ok: true, payload: encrypted, count: accounts.length })
350
- } catch (e) {
351
- writeJson(res, 500, { ok: false, error: { code: 'export', message: String(e && e.message || e) } })
352
- }
353
- }),
354
- }), 'dsh-subscriptions: /export')
355
-
356
-
357
- // Import of the encrypted bundle.
358
- ctx.effect(() => ctx.webServer.register({
359
- kind: 'exact',
360
- path: '/dsh-subscriptions/import',
361
- handler: safeJsonHandler(async (req, res) => {
362
- if (req.method !== 'POST') {
363
- writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
364
- return
365
- }
366
- if (!isTrustedSettingsRequest(req, ctx)) {
367
- writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
368
- return
369
- }
370
- let payload
371
- try { payload = JSON.parse((await readBody(req, 1024 * 1024)).toString('utf8') || '{}') } catch {
372
- writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
373
- return
374
- }
375
- const { passphrase, payload: encrypted } = payload
376
- if (!passphrase || !encrypted) {
377
- writeJson(res, 400, { ok: false, error: { code: 'params', message: 'passphrase and payload are required' } })
378
- return
379
- }
380
- let bundle
381
- try {
382
- bundle = JSON.parse(decryptWithPassphrase(encrypted, passphrase))
383
- } catch {
384
- writeJson(res, 400, { ok: false, error: { code: 'decrypt', message: 'wrong passphrase or corrupted bundle' } })
385
- return
386
- }
387
- if (!bundle || !Array.isArray(bundle.accounts)) {
388
- writeJson(res, 400, { ok: false, error: { code: 'format', message: 'invalid bundle format' } })
389
- return
390
- }
391
- let imported = 0
392
- for (const row of bundle.accounts) {
393
- try {
394
- if (!row || typeof row.ref !== 'string' || !parseOauthRef(row.ref) || !row.blob) continue
395
- await store.saveBlob(row.ref, validateCredentialBlob(row.blob))
396
- imported++
397
- } catch { /* skip broken */ }
398
- }
399
- await syncAdapter()
400
- writeJson(res, 200, { ok: true, imported, total: bundle.accounts.length, accounts: await accountsView() })
401
- }),
402
- }), 'dsh-subscriptions: /import')
403
-
404
-
405
310
  // #45: import an existing refresh token / API key without an OAuth flow.
406
311
  ctx.effect(() => ctx.webServer.register({
407
312
  kind: 'exact',
@@ -659,12 +564,6 @@ export function registerAccountsRoutes(ctx, state) {
659
564
  handler: handleVaultExport,
660
565
  }), 'dsh-subscriptions: /vault/export')
661
566
 
662
- ctx.effect(() => ctx.webServer.register({
663
- kind: 'exact',
664
- path: '/dsh-subscriptions/export-backup',
665
- handler: handleVaultExport,
666
- }), 'dsh-subscriptions: /export-backup')
667
-
668
567
  const handleVaultImport = safeJsonHandler(async (req, res) => {
669
568
  if (req.method !== 'POST') {
670
569
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
@@ -728,12 +627,6 @@ export function registerAccountsRoutes(ctx, state) {
728
627
  handler: handleVaultImport,
729
628
  }), 'dsh-subscriptions: /vault/import')
730
629
 
731
- ctx.effect(() => ctx.webServer.register({
732
- kind: 'exact',
733
- path: '/dsh-subscriptions/import-backup',
734
- handler: handleVaultImport,
735
- }), 'dsh-subscriptions: /import-backup')
736
-
737
630
 
738
631
  // #562: Remote Vault Sync Push
739
632
  ctx.effect(() => ctx.webServer.register({
@@ -40,7 +40,7 @@ export function registerProxyRoutes(ctx, state) {
40
40
  // The token never leaves the process - only the provider's response goes out.
41
41
  ctx.effect(() => ctx.webServer.register({
42
42
  kind: 'prefix',
43
- path: '/dsh-subscriptions/proxy',
43
+ path: '/dsh-subscriptions/proxy/',
44
44
  handler: safeJsonHandler(async (req, res) => {
45
45
  if (req.method !== 'POST' && req.method !== 'GET') {
46
46
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
package/lib/updater.js CHANGED
@@ -4,60 +4,15 @@ import { readFile } from 'node:fs/promises'
4
4
  import { homedir } from 'node:os'
5
5
  import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
- import { isTrustedSettingsRequest, writeJson } from './http.js'
7
+ import { isLoopback, isTrustedSettingsRequest, writeJson } from './http.js'
8
8
 
9
- const UPDATE_HEADER = 'x-dsh-plugin-update'
10
9
  const UPDATE_TIMEOUT_MS = 10 * 60_000
11
10
  const VERSION_CACHE_MS = 5 * 60_000
12
11
  let latestCache
13
12
 
14
- function header(request, name) {
15
- const value = request.headers?.[name]
16
- return Array.isArray(value) ? value[0] : value
17
- }
18
-
19
- function isLoopbackIPv4(address) {
20
- if (!address || !address.startsWith('127.')) return false
21
- const parts = address.split('.')
22
- if (parts.length !== 4) return false
23
- return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
24
- }
25
-
26
- export function isLoopback(value) {
27
- const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
28
- if (!address) return false
29
- if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
30
- const ipv4 = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address
31
- return isLoopbackIPv4(ipv4)
32
- }
13
+ export { isLoopback }
33
14
 
34
15
  export function isTrustedUpdateRequest(request, targetCtx) {
35
- const secFetchSite = String(header(request, 'sec-fetch-site') || '').toLowerCase()
36
- if (secFetchSite === 'cross-site') return false
37
-
38
- const origin = header(request, 'origin')
39
- const host = header(request, 'host')
40
- if (origin && host) {
41
- try {
42
- const u = new URL(origin)
43
- if (u.protocol === 'http:' || u.protocol === 'https:') {
44
- if (u.host.toLowerCase() !== host.toLowerCase()) return false
45
- } else if (u.protocol === 'dsh-app:') {
46
- const appHost = u.host.toLowerCase()
47
- if (appHost && appHost !== 'app' && appHost !== 'localhost') return false
48
- } else {
49
- return false
50
- }
51
- } catch {
52
- return false
53
- }
54
- }
55
-
56
- const customHeader = header(request, UPDATE_HEADER)
57
- if (customHeader === '1') {
58
- const remote = request.socket?.remoteAddress
59
- if (remote && isLoopback(remote)) return true
60
- }
61
16
  return isTrustedSettingsRequest(request, targetCtx)
62
17
  }
63
18
 
@@ -1,4 +1,4 @@
1
- import { fetchWithTimeout } from "./http.js"
1
+ import { fetchWithTimeout, sanitizeHeaderValues } from "./http.js"
2
2
  // Vendor factory from a declarative profile.
3
3
  //
4
4
  // Covers the OpenAI-Responses-compatible subscription family: PKCE auth,
@@ -12,6 +12,7 @@ import { buildAuthorizeUrl } from "./oauth.js"
12
12
  import { codexResponsesBody, modelCatalog } from "./messages.js"
13
13
  import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError , readErrorBody } from "./wire.js"
14
14
  import { emailFromToken } from "./jwt.js"
15
+ import { BUILTIN_PROVIDERS } from "./refs.js"
15
16
 
16
17
  function defaultModels(providerId, models) {
17
18
  return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
@@ -29,11 +30,11 @@ export function createVendorFromProfile(profile) {
29
30
  const tokenStyle = profile.tokenStyle === "json" ? "json" : "form"
30
31
 
31
32
  function identityHeaders(blob, extra) {
32
- return {
33
+ return sanitizeHeaderValues({
33
34
  Authorization: `Bearer ${blob.accessToken}`,
34
35
  ...(profile.headers || {}),
35
36
  ...(extra || {}),
36
- }
37
+ })
37
38
  }
38
39
 
39
40
  return {
@@ -108,12 +109,6 @@ export function createVendorFromProfile(profile) {
108
109
  return catalog
109
110
  }
110
111
  },
111
- async usage() {
112
- // Arbitrary services have no universal usage endpoint - the quota
113
- // snapshot is filled from ratelimit headers via the adapter quotaFetch.
114
- return null
115
- },
116
- check: undefined,
117
112
  async* streamOnce({ blob, options, fetchImpl, headers, signal }) {
118
113
  const body = codexResponsesBody(options, "")
119
114
  if (!body.instructions) delete body.instructions
@@ -141,6 +136,7 @@ export function validateProfile(raw) {
141
136
  if (!p[key] || typeof p[key] !== "string") throw new Error(`customVendors[${p.id || "?"}]: field ${key} is required`)
142
137
  }
143
138
  if (!/^[a-z][a-z0-9_]*$/.test(p.id)) throw new Error(`customVendors: id "${p.id}" must match [a-z][a-z0-9_]*`)
139
+ if (BUILTIN_PROVIDERS.includes(p.id)) throw new Error(`customVendors: id "${p.id}" conflicts with a built-in provider`)
144
140
  if (p.tokenStyle && !["form", "json"].includes(p.tokenStyle)) throw new Error(`customVendors[${p.id}]: tokenStyle must be form|json`)
145
141
  const localHostnames = new Set(["localhost", "127.0.0.1", "::1", "[::1]"])
146
142
  for (const key of ["authUrl", "tokenUrl", "baseUrl"]) {
@@ -170,4 +166,15 @@ export function validateProfile(raw) {
170
166
  models: Array.isArray(p.models) ? p.models : [],
171
167
  headers: p.headers && typeof p.headers === "object" ? p.headers : {},
172
168
  }
169
+ }
170
+
171
+ /** Validates a list of profiles, skipping invalid entries individually. */
172
+ export function validateProfiles(list) {
173
+ const out = []
174
+ for (const raw of Array.isArray(list) ? list : []) {
175
+ try {
176
+ out.push(validateProfile(raw))
177
+ } catch { /* skip invalid profile */ }
178
+ }
179
+ return out
173
180
  }
@@ -1,4 +1,4 @@
1
- import { isTimeoutError, fetchWithTimeout } from "../http.js"
1
+ import { isTimeoutError, fetchWithTimeout, headerSafeValue } from "../http.js"
2
2
  import { randomUUID, createHash } from 'node:crypto'
3
3
  import { buildAuthorizeUrl } from '../oauth.js'
4
4
  import { codexResponsesBody, modelCatalog } from '../messages.js'
@@ -61,10 +61,10 @@ export function authorizeUrl(cfg, pkce) {
61
61
  }
62
62
 
63
63
  function identityHeaders(blob, config, extra) {
64
- const originator = config.originator || 'codex_cli_rs'
65
- const activeOrgId = blob.selectedOrgId || blob.accountId || ''
64
+ const originator = headerSafeValue(config.originator || 'codex_cli_rs')
65
+ const activeOrgId = headerSafeValue(blob.selectedOrgId || blob.accountId || '')
66
66
  return {
67
- Authorization: `Bearer ${blob.accessToken}`,
67
+ Authorization: `Bearer ${headerSafeValue(blob.accessToken)}`,
68
68
  originator,
69
69
  'chatgpt-account-id': activeOrgId,
70
70
  'ChatGPT-Account-ID': activeOrgId,
@@ -260,7 +260,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
260
260
  Accept: 'text/event-stream',
261
261
  'session-id': sessionId,
262
262
  'x-client-request-id': sessionId,
263
- ...(isFast ? { 'x-codex-routing-hint': `model=${(options && options.model) || 'gpt-5.6-luna'};tier=priority` } : {}),
263
+ ...(isFast ? { 'x-codex-routing-hint': `model=${headerSafeValue((options && options.model) || 'gpt-5.6-luna')};tier=priority` } : {}),
264
264
  }
265
265
 
266
266
  const res = await fetchImpl(`${base}/responses`, {
@@ -39,10 +39,7 @@ export async function listModels() {
39
39
  return CODY_MODELS
40
40
  }
41
41
 
42
- export async function usage(blob) {
43
- if (blob && (blob.accessToken || blob.token)) {
44
- return { usedPercent: null, plan: 'Cody Pro', timestamp: Date.now() }
45
- }
42
+ export async function usage() {
46
43
  return null
47
44
  }
48
45
 
@@ -81,6 +81,7 @@ export async function listModels(blob, cfg, fetchImpl) {
81
81
  const impl = fetchImpl || fetch
82
82
  const fallback = modelCatalog(id, (cfg && cfg.models) || COPILOT_MODELS)
83
83
  if (!blob) return fallback
84
+ blob = { ...blob }
84
85
  const githubToken = blob.refreshToken || blob.githubToken
85
86
  let copilotToken = blob.accessToken
86
87
  try {
@@ -279,6 +280,8 @@ export async function usage(blob, config, fetchImpl) {
279
280
  const usedPercent = user && typeof user.used_percent === "number" && Number.isFinite(user.used_percent)
280
281
  ? user.used_percent
281
282
  : null
283
+
284
+ if (usedPercent == null) return null
282
285
  return {
283
286
  usedPercent,
284
287
  plan: user && user.plan ? (user.plan.name || String(user.plan)) : "Copilot",
@@ -290,6 +293,7 @@ export async function usage(blob, config, fetchImpl) {
290
293
  }
291
294
 
292
295
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, spawnImpl }) {
296
+ blob = blob ? { ...blob } : blob
293
297
  const mode = (config && config.copilotMode) || (blob && blob.copilotMode) || 'auto'
294
298
  const customPath = (config && config.copilotCliPath) || (blob && blob.copilotCliPath) || ''
295
299
 
@@ -74,7 +74,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
74
74
  const auth = await refreshErnieToken(blob.apiKey, blob.secretKey, impl)
75
75
  if (auth && auth.access_token) {
76
76
  token = auth.access_token
77
- blob.accessToken = token
78
77
  }
79
78
  }
80
79
  if (!token) throw new LlmError('Baidu ERNIE requires access token or API Key + Secret Key', 'AUTH')
@@ -171,15 +171,16 @@ export async function usage(blob, _cfg, fetchImpl) {
171
171
  })
172
172
  if (res.ok) {
173
173
  const d = await readJson(res)
174
+ if (typeof d.used_percent !== 'number') return null
174
175
  return {
175
- usedPercent: typeof d.used_percent === 'number' ? d.used_percent : null,
176
+ usedPercent: d.used_percent,
176
177
  plan: d.plan || blob.plan || 'Perplexity Pro',
177
178
  timestamp: Date.now(),
178
179
  quota: d.quota ? { used: d.quota.used, limit: d.quota.limit, resetAt: d.quota.reset_at } : undefined,
179
180
  }
180
181
  }
181
182
  } catch { /* usage endpoint is optional */ }
182
- return { usedPercent: null, plan: blob.plan || 'Perplexity Pro', timestamp: Date.now() }
183
+ return null
183
184
  }
184
185
 
185
186
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
@@ -34,10 +34,7 @@ export async function listModels() {
34
34
  return REPLIT_MODELS
35
35
  }
36
36
 
37
- export async function usage(blob) {
38
- if (blob && (blob.accessToken || blob.token)) {
39
- return { usedPercent: null, plan: 'Replit Core', timestamp: Date.now() }
40
- }
37
+ export async function usage() {
41
38
  return null
42
39
  }
43
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.86",
3
+ "version": "0.6.88",
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",