@goodandready/dsh-subscriptions 0.7.39 → 0.7.40

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.
@@ -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
+ proxyBypass: z.array(z.string()).default([])
24
+ .description('#803 Hosts/domains that bypass the per-account proxy.'),
23
25
  maxConcurrency: z.number().default(1)
24
26
  .description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
25
27
  monthlyCost: z.number().default(20)
@@ -157,6 +159,8 @@ export const Config = z.object({
157
159
  .description('Configurable global priority sequence for cross-vendor fallback.'),
158
160
  modelAccountRules: z.array(z.object({ provider: z.string(), pattern: z.string(), account: z.string() })).volatile().default([])
159
161
  .description('#788 Per-provider model glob rules. First match wins; account may be a ref, label or index.'),
162
+ tiers: z.array(z.any()).volatile().default([])
163
+ .description('#817 Named picker tiers: { id, name, members: [accountRef] }. No cross-tier fallback.'),
160
164
  rotationStrategy: z.string().volatile().default('round-robin')
161
165
  .description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), quota-aware (urgent quota first), or priority (sequential).'),
162
166
  poolEnabled: z.boolean().volatile().default(true)
@@ -0,0 +1,107 @@
1
+ function hostOf(value) {
2
+ try { return new URL(String(value)).hostname.toLowerCase() } catch { return '' }
3
+ }
4
+
5
+ /** #803: bypass plugin proxy for listed hosts/domains. */
6
+ export function shouldBypassProxy(bypassList, targetUrl) {
7
+ const host = hostOf(targetUrl)
8
+ if (!host) return false
9
+ for (const raw of Array.isArray(bypassList) ? bypassList : []) {
10
+ const entry = String(raw || '').trim().toLowerCase().replace(/^\*\./, '').replace(/^\./, '')
11
+ if (!entry) continue
12
+ if (host === entry || host.endsWith('.' + entry)) return true
13
+ }
14
+ return false
15
+ }
16
+
17
+ /** #803: distinguish proxy transport failure from provider failure. */
18
+ export function classifyConnectivityProbe({ proxyError, providerStatus, latencyMs } = {}) {
19
+ if (proxyError) return { ok: false, kind: 'proxy', error: String(proxyError), latencyMs: null }
20
+ const status = Number(providerStatus) || 0
21
+ return {
22
+ ok: status >= 200 && status < 500,
23
+ kind: status >= 400 ? 'provider' : 'ok',
24
+ status,
25
+ latencyMs: Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : null,
26
+ }
27
+ }
28
+
29
+ /** #817: tiers are independent named pools; no cross-tier fallback. */
30
+ export function validateTiers(tiers) {
31
+ const list = Array.isArray(tiers) ? tiers : []
32
+ const ids = new Set()
33
+ const out = []
34
+ for (const tier of list) {
35
+ if (!tier || typeof tier !== 'object') continue
36
+ const id = String(tier.id || '').trim()
37
+ if (!id || ids.has(id)) continue
38
+ ids.add(id)
39
+ out.push({
40
+ id,
41
+ name: String(tier.name || id),
42
+ members: (Array.isArray(tier.members) ? tier.members : [])
43
+ .map((member) => String(member || '').trim())
44
+ .filter(Boolean),
45
+ })
46
+ }
47
+ return out
48
+ }
49
+
50
+ export function selectTierAccount(tier, accounts, nowMs = Date.now()) {
51
+ const members = new Set((tier && tier.members) || [])
52
+ if (!members.size) return null
53
+ return (Array.isArray(accounts) ? accounts : []).find((account) => account
54
+ && members.has(account.ref)
55
+ && account.hasToken
56
+ && (!account.cooldownUntil || Number(account.cooldownUntil) <= nowMs)
57
+ && (!account.quarantineUntil || Number(account.quarantineUntil) <= nowMs)
58
+ && !(account.usagePercent != null && Number(account.usagePercent) >= 100)) || null
59
+ }
60
+
61
+ /** #832: session policy snapshots are frozen at session creation. */
62
+ export function snapshotToolPolicy(policy, sessionId, at = Date.now()) {
63
+ return { sessionId: String(sessionId || ''), at: Number(at), policy: JSON.parse(JSON.stringify(policy || {})) }
64
+ }
65
+
66
+ export function toolPolicyForSession(snapshots, sessionId) {
67
+ const found = (Array.isArray(snapshots) ? snapshots : []).find((row) => row && row.sessionId === String(sessionId || ''))
68
+ return found ? found.policy : null
69
+ }
70
+
71
+ /** #834: Claude CLI-synced accounts must not fight standalone OAuth over Keychain. */
72
+ export function claudeAccountMode(slot = {}) {
73
+ const raw = String(slot.accountMode || slot.mode || '').toLowerCase()
74
+ if (raw === 'cli-synced' || raw === 'cli') return 'cli-synced'
75
+ return 'standalone'
76
+ }
77
+
78
+ export function canWriteClaudeKeychain(slot, blob, expectedAccountId) {
79
+ if (claudeAccountMode(slot) !== 'cli-synced') return false
80
+ if (!blob) return false
81
+ const owner = String(blob.accountId || blob.email || '').trim()
82
+ const expected = String(expectedAccountId || slot.accountId || slot.email || '').trim()
83
+ return Boolean(owner && expected && owner === expected)
84
+ }
85
+
86
+ /** #815: normalize Codex web-search results and bound retry behavior. */
87
+ export function normalizeWebSearchResult(payload, maxBytes = 64 * 1024) {
88
+ const body = payload && typeof payload === 'object' ? payload : {}
89
+ const text = String(body.output_text || body.answer || body.text || '').slice(0, maxBytes)
90
+ const citations = (Array.isArray(body.citations) ? body.citations : [])
91
+ .map((citation) => {
92
+ if (!citation) return null
93
+ if (typeof citation === 'string') return { url: citation }
94
+ return { url: String(citation.url || citation.link || ''), title: citation.title ? String(citation.title) : undefined }
95
+ })
96
+ .filter((citation) => citation && citation.url)
97
+ .slice(0, 50)
98
+ return { text, citations }
99
+ }
100
+
101
+ export function webSearchRetryDelay(attempt, status) {
102
+ const n = Number(attempt) || 0
103
+ const code = Number(status) || 0
104
+ if (code === 429) return null
105
+ if (code >= 500 && n < 2) return Math.min(2000, 250 * 2 ** n)
106
+ return null
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
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",