@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 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
- if (ref && refreshLocks.has(ref)) return refreshLocks.get(ref)
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
- if (ref) refreshLocks.delete(ref)
224
+ refreshLocks.delete(lockKey)
224
225
  }
225
226
  })()
226
- if (ref) refreshLocks.set(ref, promise)
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
+ }