@goodandready/dsh-subscriptions 0.5.26 → 0.5.31
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/lib/accounts.js +4 -3
- package/lib/client.js +32 -3
- package/lib/config-schema.js +80 -0
- package/lib/http.js +16 -0
- package/lib/index.js +28 -1080
- package/lib/rotate.js +1 -0
- package/lib/routes.js +1014 -0
- package/lib/sse.js +40 -9
- package/lib/ui-i18n.js +256 -0
- package/lib/ui-styles.js +119 -0
- package/package.json +5 -5
- package/lib/coalesce-stream.js +0 -22
- package/lib/cookie-bridge.js +0 -8
- package/lib/credit-detect.js +0 -22
- package/lib/encrypted-bundle.js +0 -46
- package/lib/latency-heatmap.js +0 -26
- package/lib/latency-score.js +0 -36
- package/lib/multi-tenant.js +0 -16
- package/lib/network-benchmark.js +0 -23
- package/lib/pkce-store.js +0 -28
- package/lib/plan.js +0 -42
- package/lib/proactive-refresh.js +0 -37
- package/lib/prompt-cache-warmer.js +0 -44
- package/lib/reconnect-stream.js +0 -21
- package/lib/reset-toast.js +0 -38
- package/lib/savings-calculator.js +0 -14
- package/lib/state-recovery.js +0 -33
- package/lib/telemetry.js +0 -28
- package/lib/token-speedometer.js +0 -26
- package/lib/zero-trace-logger.js +0 -27
package/lib/accounts.js
CHANGED
|
@@ -189,7 +189,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
189
189
|
async function ensureFresh(provider, blob, ref) {
|
|
190
190
|
if (!blob.refreshToken) return blob
|
|
191
191
|
if (blob.expiresAt && blob.expiresAt - SKEW_MS > Date.now()) return blob
|
|
192
|
-
|
|
192
|
+
const lockKey = ref || (provider + ":" + (blob.accountId || blob.email || (blob.refreshToken && blob.refreshToken.slice(-16)) || "anon"))
|
|
193
|
+
if (refreshLocks.has(lockKey)) return refreshLocks.get(lockKey)
|
|
193
194
|
const promise = (async () => {
|
|
194
195
|
try {
|
|
195
196
|
const cfg = vendorConfig(provider, getConfig())
|
|
@@ -220,10 +221,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
220
221
|
if (ref) refreshFailures.set(ref, { at: Date.now(), error: String(e && e.message || e) })
|
|
221
222
|
throw e
|
|
222
223
|
} finally {
|
|
223
|
-
|
|
224
|
+
refreshLocks.delete(lockKey)
|
|
224
225
|
}
|
|
225
226
|
})()
|
|
226
|
-
|
|
227
|
+
refreshLocks.set(lockKey, promise)
|
|
227
228
|
return promise
|
|
228
229
|
}
|
|
229
230
|
|
package/lib/client.js
CHANGED
|
@@ -12,6 +12,35 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
let t = (key) => key
|
|
13
13
|
const setT = (fn) => { t = fn }
|
|
14
14
|
|
|
15
|
+
class ErrorBoundary extends React.Component {
|
|
16
|
+
constructor(props) {
|
|
17
|
+
super(props)
|
|
18
|
+
this.state = { hasError: false, error: null }
|
|
19
|
+
}
|
|
20
|
+
static getDerivedStateFromError(error) {
|
|
21
|
+
return { hasError: true, error }
|
|
22
|
+
}
|
|
23
|
+
componentDidCatch(error, info) {
|
|
24
|
+
try { console.error("[dsh-subscriptions UI error]", error, info) } catch {}
|
|
25
|
+
}
|
|
26
|
+
render() {
|
|
27
|
+
if (this.state.hasError) {
|
|
28
|
+
return React.createElement(
|
|
29
|
+
"div",
|
|
30
|
+
{ style: { padding: "12px", border: "1px solid var(--dsw-alias-state-error-primary, #e5534b)", borderRadius: "8px", background: "var(--dsw-alias-bg-layer-2, #1f1f1f)", margin: "8px 0", fontSize: "13px" } },
|
|
31
|
+
React.createElement("div", { style: { fontWeight: 600, color: "var(--dsw-alias-state-error-primary, #e5534b)", marginBottom: "4px" } }, "Subscriptions UI error"),
|
|
32
|
+
React.createElement("div", { style: { color: "var(--dsw-alias-label-secondary, #8b949e)", fontSize: "12px", marginBottom: "8px" } }, String(this.state.error && this.state.error.message || this.state.error || "Unknown error")),
|
|
33
|
+
React.createElement("button", {
|
|
34
|
+
className: "dsub-mini",
|
|
35
|
+
onClick: () => this.setState({ hasError: false, error: null })
|
|
36
|
+
}, "Retry")
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return this.props.children
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
15
44
|
const CSS =
|
|
16
45
|
'.dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}' +
|
|
17
46
|
'.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
|
|
@@ -1322,7 +1351,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1322
1351
|
React.createElement(Chevron, { className: 'dsub-chev' + (open ? ' dsub-chevOpen' : '') }),
|
|
1323
1352
|
),
|
|
1324
1353
|
open ? React.createElement('div', { className: 'dsub-body' },
|
|
1325
|
-
React.createElement(SubsSection, props),
|
|
1354
|
+
React.createElement(ErrorBoundary, null, React.createElement(SubsSection, props)),
|
|
1326
1355
|
) : null,
|
|
1327
1356
|
)
|
|
1328
1357
|
}
|
|
@@ -1741,7 +1770,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1741
1770
|
order: 15,
|
|
1742
1771
|
locale: NS,
|
|
1743
1772
|
},
|
|
1744
|
-
(props) => React.createElement(SubsPill, { t }),
|
|
1773
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(SubsPill, { t })),
|
|
1745
1774
|
),
|
|
1746
1775
|
)
|
|
1747
1776
|
}, 'dsh-subscriptions: subs pill')
|
|
@@ -1757,7 +1786,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1757
1786
|
order: 5,
|
|
1758
1787
|
locale: NS,
|
|
1759
1788
|
},
|
|
1760
|
-
(props) => React.createElement(ComposerQuota, { t }),
|
|
1789
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(ComposerQuota, { t })),
|
|
1761
1790
|
),
|
|
1762
1791
|
)
|
|
1763
1792
|
}, 'dsh-subscriptions: composer quota')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
import { PROVIDERS } from './refs.js'
|
|
3
|
+
|
|
4
|
+
export const Slot = z.object({
|
|
5
|
+
provider: z.string().default('codex')
|
|
6
|
+
.description('One of: codex, claude, grok, antigravity.'),
|
|
7
|
+
index: z.number().default(1)
|
|
8
|
+
.description('Account slot number. Credential ref is <PROVIDER>_OAUTH_<index>.'),
|
|
9
|
+
label: z.string().default('')
|
|
10
|
+
.description('Optional display label. Empty uses the account email after login.'),
|
|
11
|
+
expiresAt: z.number().default(0)
|
|
12
|
+
.description('#67 Optional subscription expiry timestamp (ms). When set and within expiryNotifyDays, the header chip shows the account and expiry date.'),
|
|
13
|
+
proxyUrl: z.string().default('')
|
|
14
|
+
.description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
|
|
18
|
+
|
|
19
|
+
export const Config = z.object({
|
|
20
|
+
cooldownMs: z.number().default(30 * 60 * 1000)
|
|
21
|
+
.description('After RATE_LIMIT/QUOTA/429, skip that account for this many milliseconds.'),
|
|
22
|
+
switchAtRemaining: z.number().default(0.01)
|
|
23
|
+
.description('If remaining <= this (absolute or <1 fraction), treat as exhausted before request. 0 disables.'),
|
|
24
|
+
refreshAheadMs: z.number().default(5 * 60 * 1000)
|
|
25
|
+
.description('Background refresh when expiry within this many ms.'),
|
|
26
|
+
refreshRetryMs: z.number().default(10 * 60 * 1000)
|
|
27
|
+
.description('Do not retry background refresh more often than this after failure.'),
|
|
28
|
+
probeIntervalMin: z.number().default(15)
|
|
29
|
+
.description('Background health-check interval in minutes. 0 disables.'),
|
|
30
|
+
notifyLimits: z.boolean().default(true)
|
|
31
|
+
.description('Emit log notices when usage crosses 70/90/100% of a window.'),
|
|
32
|
+
expiryNotifyDays: z.number().default(7)
|
|
33
|
+
.description('#67 Warn in the header chip this many days before a subscription expiry (expiresAt). 0 disables.'),
|
|
34
|
+
privacyMask: z.boolean().default(false)
|
|
35
|
+
.description('#98 Hide personal data in the UI: emails show as j***n@example.com.'),
|
|
36
|
+
slots: z.array(Slot).default(defaultSlots)
|
|
37
|
+
.description('Account slots. Secrets are not stored here; only the credential ref names.'),
|
|
38
|
+
useWebCallback: z.boolean().default(false)
|
|
39
|
+
.description('When on, redirect_uri is this Web UI origin + /dsh-subscriptions/oauth/callback. When off, the vendor CLI registered redirect is used and you paste the redirected URL.'),
|
|
40
|
+
autoLoopback: z.boolean().default(true)
|
|
41
|
+
.description('#89 When on and the vendor redirect_uri is a loopback address (codex :1455, grok :56121), a temporary local server catches the OAuth callback automatically - no paste needed. Paste fallback stays available.'),
|
|
42
|
+
ollamaBaseUrl: z.string().default('http://127.0.0.1:11434')
|
|
43
|
+
.description('#91 Local Ollama base URL. Served as the ollama provider in the native model picker when reachable.'),
|
|
44
|
+
ollamaFallback: z.boolean().default(true)
|
|
45
|
+
.description('#91 When every account of a provider is exhausted, continue the chat on local Ollama instead of failing.'),
|
|
46
|
+
ollamaFallbackModel: z.string().default('')
|
|
47
|
+
.description('#91 Ollama model used for the fallback (for example qwen2.5-coder). Empty = first model from /api/tags.'),
|
|
48
|
+
hideDeprecatedModels: z.boolean().default(false)
|
|
49
|
+
.description('#94 Hide test/preview/beta/legacy model ids from the native model picker.'),
|
|
50
|
+
codexClientId: z.string().default(''),
|
|
51
|
+
codexVerbosity: z.string().default('')
|
|
52
|
+
.description('#93 Response verbosity for Codex reasoning models: low, medium or high. Empty = protocol default.'),
|
|
53
|
+
codexFastMode: z.boolean().default(false)
|
|
54
|
+
.description('#92 Fast Mode for Codex: sends service_tier priority (1.5x speed billing tier) with every request.'),
|
|
55
|
+
composerQuota: z.string().default('off')
|
|
56
|
+
.description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
|
|
57
|
+
codexRedirectUri: z.string().default(''),
|
|
58
|
+
codexBaseUrl: z.string().default(''),
|
|
59
|
+
claudeClientId: z.string().default(''),
|
|
60
|
+
claudeRedirectUri: z.string().default(''),
|
|
61
|
+
grokClientId: z.string().default(''),
|
|
62
|
+
grokRedirectUri: z.string().default(''),
|
|
63
|
+
grokBaseUrl: z.string().default(''),
|
|
64
|
+
grokClientVersion: z.string().default('')
|
|
65
|
+
.description('Grok CLI identity version header. Empty uses the built-in default.'),
|
|
66
|
+
antigravityClientId: z.string().default(''),
|
|
67
|
+
antigravityRedirectUri: z.string().default(''),
|
|
68
|
+
customVendors: z.array(z.any()).default([])
|
|
69
|
+
.description('Declarative OpenAI-Responses-compatible providers. See README.'),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export function publicConfig(cfg) {
|
|
73
|
+
const clone = structuredClone(cfg)
|
|
74
|
+
// #242: redact all *ClientSecret fields before exposing config via GET /config.
|
|
75
|
+
// Secrets must never leave the server; the UI does not need them.
|
|
76
|
+
for (const key of Object.keys(clone)) {
|
|
77
|
+
if (key.endsWith('ClientSecret')) clone[key] = clone[key] ? '••••••' : ''
|
|
78
|
+
}
|
|
79
|
+
return clone
|
|
80
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -64,3 +64,19 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
64
64
|
clearTimeout(timer)
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
export function safeJsonHandler(fn) {
|
|
69
|
+
return async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
await fn(req, res)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const status = Number(err && (err.status || err.statusCode) || 500)
|
|
74
|
+
const code = (err && err.code) || "INTERNAL_ERROR"
|
|
75
|
+
const message = String((err && err.message) || err || "Internal server error")
|
|
76
|
+
writeJson(res, status >= 400 && status < 600 ? status : 500, {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: { code, message },
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|