@goodandready/dsh-subscriptions 0.6.47 → 0.6.50
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 +37 -9
- package/lib/adapter-manager.js +4 -1
- package/lib/adapter.js +31 -1
- package/lib/client.js +81 -32
- package/lib/config-schema.js +1 -1
- package/lib/http.js +47 -27
- package/lib/index.js +4 -3
- package/lib/messages.js +11 -3
- package/lib/rotate.js +1 -1
- package/lib/routes/accounts.js +11 -11
- package/lib/routes/oauth.js +3 -3
- package/lib/routes/proxy.js +3 -3
- package/lib/routes/status.js +22 -11
- package/lib/tools-normalizer.js +1 -0
- package/lib/updater.js +3 -3
- package/lib/vendors/codex.js +2 -1
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { calculateBurnRatePerHour } from "./forecast.js"
|
|
1
2
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
2
3
|
import { homedir } from "node:os"
|
|
3
4
|
import { join } from "node:path"
|
|
@@ -105,10 +106,12 @@ export function vendorConfig(provider, cfg) {
|
|
|
105
106
|
export function createAccountStore({ credentials, getConfig, fetchImpl, fetchForRef, onLimitNotice }) {
|
|
106
107
|
const cooldowns = new Map()
|
|
107
108
|
const quotas = new Map()
|
|
109
|
+
const quotaSamples = new Map()
|
|
108
110
|
const quarantines = sharedQuarantines
|
|
109
111
|
const writeQueues = new Map()
|
|
110
112
|
const credentialVersions = new Map()
|
|
111
113
|
const clearedRefs = new Set()
|
|
114
|
+
const latestBlobs = new Map()
|
|
112
115
|
const refreshLocks = new Map()
|
|
113
116
|
const refreshFailures = new Map()
|
|
114
117
|
const usage = new Map()
|
|
@@ -144,6 +147,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
144
147
|
async function saveBlob(ref, blob) {
|
|
145
148
|
if (!ref || typeof ref !== "string") throw new Error("invalid ref for saveBlob")
|
|
146
149
|
clearedRefs.delete(ref)
|
|
150
|
+
latestBlobs.set(ref, blob)
|
|
147
151
|
const version = (credentialVersions.get(ref) || 0) + 1
|
|
148
152
|
credentialVersions.set(ref, version)
|
|
149
153
|
|
|
@@ -154,8 +158,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
154
158
|
}
|
|
155
159
|
try {
|
|
156
160
|
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
157
|
-
if (clearedRefs.has(ref)
|
|
161
|
+
if (clearedRefs.has(ref)) {
|
|
158
162
|
try { await credentials.unset(credentialRef(ref)) } catch { /* best-effort */ }
|
|
163
|
+
} else if (credentialVersions.get(ref) > version && latestBlobs.has(ref)) {
|
|
164
|
+
try { await credentials.set(credentialRef(ref), serializeBlob(latestBlobs.get(ref))) } catch { /* best-effort */ }
|
|
159
165
|
}
|
|
160
166
|
} catch (err) {
|
|
161
167
|
if (clearedRefs.has(ref) || credentialVersions.get(ref) !== version) {
|
|
@@ -174,6 +180,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
174
180
|
async function clearRef(ref) {
|
|
175
181
|
if (!ref || typeof ref !== "string") return
|
|
176
182
|
clearedRefs.add(ref)
|
|
183
|
+
latestBlobs.delete(ref)
|
|
177
184
|
const version = (credentialVersions.get(ref) || 0) + 1
|
|
178
185
|
credentialVersions.set(ref, version)
|
|
179
186
|
try {
|
|
@@ -181,7 +188,9 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
181
188
|
} catch { /* best-effort unset */ }
|
|
182
189
|
cooldowns.delete(ref)
|
|
183
190
|
quotas.delete(ref)
|
|
191
|
+
quotaSamples.delete(ref)
|
|
184
192
|
quarantines.delete(ref)
|
|
193
|
+
saveQuarantinesToFile(quarantines)
|
|
185
194
|
writeQueues.delete(ref)
|
|
186
195
|
for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
|
|
187
196
|
refreshFailures.delete(ref)
|
|
@@ -226,7 +235,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
226
235
|
quarantineReason: quarantines.get(ref) ? quarantines.get(ref).reason : null,
|
|
227
236
|
cooldownFamilies: cooldowns.get(ref) && cooldowns.get(ref).families ? cooldowns.get(ref).families : null,
|
|
228
237
|
quota: quotas.get(ref) || null,
|
|
229
|
-
pacePerHour: quotas.get(ref)?.pacePerHour ||
|
|
238
|
+
pacePerHour: quotas.get(ref)?.pacePerHour || calculateBurnRatePerHour(quotaSamples.get(ref) || [], Date.now()),
|
|
239
|
+
samples: quotaSamples.get(ref) || [],
|
|
230
240
|
usage: windows.get(ref) || base.usage || null,
|
|
231
241
|
health: health.get(ref) || null,
|
|
232
242
|
healthScore: computeHealthScore(health.get(ref) || null),
|
|
@@ -245,11 +255,9 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
245
255
|
const curConfig = getConfig()
|
|
246
256
|
const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
|
|
247
257
|
const out = []
|
|
248
|
-
const now = Date.now()
|
|
249
258
|
for (const slot of slots) {
|
|
250
259
|
const info = await describeRef(slot.ref)
|
|
251
260
|
const q = quarantines.get(slot.ref)
|
|
252
|
-
const inQ = q && Number(q.until) > now
|
|
253
261
|
out.push({
|
|
254
262
|
ref: slot.ref,
|
|
255
263
|
hasToken: !!info.configured,
|
|
@@ -259,8 +267,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
259
267
|
quota: info.quota || null,
|
|
260
268
|
healthScore: computeHealthScore(info.health),
|
|
261
269
|
healthBadge: healthBadge(computeHealthScore(info.health)),
|
|
262
|
-
quarantineUntil:
|
|
263
|
-
quarantineReason:
|
|
270
|
+
quarantineUntil: q ? q.until : 0,
|
|
271
|
+
quarantineReason: q ? q.reason : null,
|
|
264
272
|
label: slot.label || info.label,
|
|
265
273
|
pacePerHour: info.pacePerHour || 0,
|
|
266
274
|
})
|
|
@@ -390,6 +398,9 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
390
398
|
if (currentRaw) currentBlob = parseBlob(currentRaw)
|
|
391
399
|
} catch { /* best-effort */ }
|
|
392
400
|
if (!currentBlob || clearedRefs.has(slot.ref)) return
|
|
401
|
+
if (latestBlobs.has(slot.ref)) {
|
|
402
|
+
currentBlob = { ...currentBlob, ...latestBlobs.get(slot.ref) }
|
|
403
|
+
}
|
|
393
404
|
const prev = Array.isArray(currentBlob.usage) ? JSON.stringify(currentBlob.usage) : ''
|
|
394
405
|
if (prev !== JSON.stringify(list)) {
|
|
395
406
|
await saveBlob(slot.ref, { ...currentBlob, usage: list, usageAt: Date.now() })
|
|
@@ -439,7 +450,24 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
439
450
|
if (prev && Array.isArray(prev.families)) for (const f of prev.families) merged.add(f)
|
|
440
451
|
cooldowns.set(ref, { until: Math.max(Number(until) || 0, prev ? prev.until : 0), families: Array.from(merged) })
|
|
441
452
|
},
|
|
442
|
-
rememberQuota(ref, snap) {
|
|
453
|
+
rememberQuota(ref, snap) {
|
|
454
|
+
if (snap) {
|
|
455
|
+
quotas.set(ref, snap)
|
|
456
|
+
if (snap.usedPercent != null) {
|
|
457
|
+
const now = snap.measuredAt || Date.now()
|
|
458
|
+
const samples = quotaSamples.get(ref) || []
|
|
459
|
+
const remainingPercent = snap.remainingPercent ?? (100 - Number(snap.usedPercent))
|
|
460
|
+
samples.push({ at: now, pct: remainingPercent })
|
|
461
|
+
while (samples.length > 20) samples.shift()
|
|
462
|
+
quotaSamples.set(ref, samples)
|
|
463
|
+
snap.samples = [...samples]
|
|
464
|
+
snap.pacePerHour = calculateBurnRatePerHour(samples, now)
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
quotas.delete(ref)
|
|
468
|
+
quotaSamples.delete(ref)
|
|
469
|
+
}
|
|
470
|
+
},
|
|
443
471
|
shouldSkipRefresh(ref, now, retryMs) { const f = refreshFailures.get(ref); if (!f) return false; return (Number(f.at) + Number(retryMs)) > Number(now) },
|
|
444
472
|
getQuota(ref) { return quotas.get(ref) || null },
|
|
445
473
|
rememberUsage(ref, percent) { usage.set(ref, percent) },
|
|
@@ -452,6 +480,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
452
480
|
// successful request clears the cooldown and puts the account back in rotation
|
|
453
481
|
cooldowns.delete(ref)
|
|
454
482
|
quarantines.delete(ref)
|
|
483
|
+
saveQuarantinesToFile(quarantines)
|
|
455
484
|
if (health.has(ref)) health.set(ref, null)
|
|
456
485
|
},
|
|
457
486
|
rememberQuarantine(ref, reason, until, attempts = 1) {
|
|
@@ -467,9 +496,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
467
496
|
getQuarantine(ref, now = Date.now()) {
|
|
468
497
|
const q = quarantines.get(ref)
|
|
469
498
|
if (!q) return null
|
|
470
|
-
if (Number(q.until) <= now) {
|
|
499
|
+
if (Number(q.until) <= Number(now)) {
|
|
471
500
|
if (q.status === STATUS_PROBING) return q
|
|
472
|
-
quarantines.delete(ref)
|
|
473
501
|
return null
|
|
474
502
|
}
|
|
475
503
|
return q
|
package/lib/adapter-manager.js
CHANGED
|
@@ -41,9 +41,12 @@ export function createAdapterManager({
|
|
|
41
41
|
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
42
42
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
43
43
|
cooldownMs: () => live().cooldownMs,
|
|
44
|
+
autoPacing: () => live().autoPacing,
|
|
44
45
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
45
46
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
46
47
|
rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
|
|
48
|
+
getQuarantine: (ref) => store.getQuarantine(ref),
|
|
49
|
+
probeWarmup: (ref, fn) => store.probeWarmup(ref, fn),
|
|
47
50
|
rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
|
|
48
51
|
getQuota: (ref) => store.getQuota(ref),
|
|
49
52
|
refreshUsage: (provider) => store.refreshUsage(provider),
|
|
@@ -60,7 +63,7 @@ export function createAdapterManager({
|
|
|
60
63
|
fetchForRef,
|
|
61
64
|
hideDeprecatedModels: () => Boolean(live().hideDeprecatedModels),
|
|
62
65
|
cascadingFallback: (...args) => cascadeFallbackStream(...args),
|
|
63
|
-
ollamaFallback: typeof ollamaFallback === 'function' ? ollamaFallback : async function* ({ err }) {
|
|
66
|
+
ollamaFallback: typeof ollamaFallback === 'function' ? ollamaFallback : async function* ({ err }) { yield { type: 'finish', reason: { kind: 'error', failure: { code: err?.code || 'RATE_LIMIT', message: err?.message || 'all subscription accounts exhausted' } } } },
|
|
64
67
|
})
|
|
65
68
|
|
|
66
69
|
async function* cascadeFallbackStream({ options, provider, err, visited = new Set() }) {
|
package/lib/adapter.js
CHANGED
|
@@ -118,16 +118,23 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
118
118
|
if (p && typeof p.catch === 'function') p.catch(() => {})
|
|
119
119
|
} catch { /* fire-and-forget background usage refresh */ }
|
|
120
120
|
}
|
|
121
|
+
const effectiveAutoPacing = options.autoPacing !== undefined
|
|
122
|
+
? options.autoPacing
|
|
123
|
+
: (typeof deps.autoPacing === 'function' ? deps.autoPacing() : (deps.autoPacing ?? true))
|
|
124
|
+
const streamOptions = { ...options, autoPacing: effectiveAutoPacing }
|
|
121
125
|
for await (const event of streamWithRotation({
|
|
122
126
|
accounts: await deps.listAccounts(provider),
|
|
123
127
|
nowMs: () => Date.now(),
|
|
124
128
|
cooldownMs: deps.cooldownMs(),
|
|
125
129
|
switchAtRemaining: typeof deps.switchAtRemaining === 'function' ? deps.switchAtRemaining() : (deps.switchAtRemaining ?? 0),
|
|
126
|
-
options,
|
|
130
|
+
options: streamOptions,
|
|
127
131
|
webhookUrl: typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl,
|
|
128
132
|
fetchImpl: deps.fetchImpl || fetch,
|
|
129
133
|
onCooldown: (account) => {
|
|
130
134
|
if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
|
|
135
|
+
if (typeof deps.rememberQuarantine === 'function') {
|
|
136
|
+
deps.rememberQuarantine(account.ref, account.quarantineReason || 'RATE_LIMIT', account.quarantineUntil || account.cooldownUntil)
|
|
137
|
+
}
|
|
131
138
|
if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
|
|
132
139
|
},
|
|
133
140
|
streamOnce: async function* (account, opts) {
|
|
@@ -135,6 +142,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
135
142
|
let firstChunkTime = null
|
|
136
143
|
let outputTokens = 0
|
|
137
144
|
let inputTokens = 0
|
|
145
|
+
let reasoningTokens = 0
|
|
138
146
|
let cacheReadTokens = 0
|
|
139
147
|
let cacheWriteTokens = 0
|
|
140
148
|
let approxChars = 0
|
|
@@ -188,6 +196,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
188
196
|
tps,
|
|
189
197
|
inputTokens: inputTokens || undefined,
|
|
190
198
|
outputTokens: outputTokens || undefined,
|
|
199
|
+
reasoningTokens: reasoningTokens || undefined,
|
|
191
200
|
cacheReadTokens: cacheReadTokens || undefined,
|
|
192
201
|
cacheWriteTokens: cacheWriteTokens || undefined,
|
|
193
202
|
kind: 'stream',
|
|
@@ -254,6 +263,24 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
try {
|
|
266
|
+
// Warmup probe check for accounts returning from quarantine (#350)
|
|
267
|
+
if (typeof deps.probeWarmup === "function") {
|
|
268
|
+
const qUntil = Number(account.quarantineUntil || 0)
|
|
269
|
+
if (qUntil > 0 && qUntil <= Date.now()) {
|
|
270
|
+
const probeResult = await deps.probeWarmup(account.ref, async () => {
|
|
271
|
+
if (vendor && typeof vendor.check === "function") {
|
|
272
|
+
const probeBlob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
273
|
+
await vendor.check(probeBlob, { config: deps.vendorConfig(provider), signal: opts && opts.signal })
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
if (probeResult && !probeResult.success) {
|
|
277
|
+
const err = probeResult.error || new Error("warmup probe failed")
|
|
278
|
+
err.code = "QUOTA"
|
|
279
|
+
throw err
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
257
284
|
blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
258
285
|
if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
|
|
259
286
|
for await (const chunk of vendor.streamOnce({
|
|
@@ -280,6 +307,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
280
307
|
if (u && typeof u.inputTokens === 'number') {
|
|
281
308
|
inputTokens = u.inputTokens
|
|
282
309
|
}
|
|
310
|
+
if (u && typeof u.reasoningTokens === 'number') {
|
|
311
|
+
reasoningTokens = u.reasoningTokens
|
|
312
|
+
}
|
|
283
313
|
if (u && typeof u.cacheReadTokens === 'number') {
|
|
284
314
|
cacheReadTokens = u.cacheReadTokens
|
|
285
315
|
}
|
package/lib/client.js
CHANGED
|
@@ -2077,23 +2077,23 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2077
2077
|
// The card in the Plugins tab follows the bash/agent-loop/web-search cards:
|
|
2078
2078
|
// titled header with description and chevron, collapsible, bordered.
|
|
2079
2079
|
function PluginCard(props) {
|
|
2080
|
-
const
|
|
2080
|
+
const cardT = (props && props.t) || t
|
|
2081
2081
|
const page = !!(props && props.view === 'page')
|
|
2082
2082
|
const [open, setOpen] = React.useState(!!page)
|
|
2083
2083
|
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
2084
2084
|
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
2085
2085
|
if (props && props.view === 'summary') {
|
|
2086
|
-
return React.createElement('div', { className: 'dsub-description' },
|
|
2086
|
+
return React.createElement('div', { className: 'dsub-description' }, cardT('cardIntro'))
|
|
2087
2087
|
}
|
|
2088
|
-
// Check host snapshot status (#408): loading or
|
|
2088
|
+
// Check host snapshot status (#408, #459): loading, unavailable, or error
|
|
2089
2089
|
if (props && props.status === 'loading') {
|
|
2090
2090
|
return React.createElement(page ? 'div' : 'li', {
|
|
2091
2091
|
className: page ? 'dsub-page' : 'dsub-card',
|
|
2092
2092
|
},
|
|
2093
2093
|
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2094
2094
|
React.createElement('div', { className: 'dsub-headText' },
|
|
2095
|
-
React.createElement('div', { className: 'dsub-name' },
|
|
2096
|
-
React.createElement('div', { className: 'dsub-description' },
|
|
2095
|
+
React.createElement('div', { className: 'dsub-name' }, cardT('title')),
|
|
2096
|
+
React.createElement('div', { className: 'dsub-description' }, cardT('loading') || cardT('settingsLoading') || 'Loading…'),
|
|
2097
2097
|
),
|
|
2098
2098
|
)
|
|
2099
2099
|
)
|
|
@@ -2104,9 +2104,23 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2104
2104
|
},
|
|
2105
2105
|
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2106
2106
|
React.createElement('div', { className: 'dsub-headText' },
|
|
2107
|
-
React.createElement('div', { className: 'dsub-name' },
|
|
2107
|
+
React.createElement('div', { className: 'dsub-name' }, cardT('title')),
|
|
2108
2108
|
React.createElement('div', { className: 'dsub-description', style: { color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
2109
|
-
|
|
2109
|
+
cardT('unavailable') || cardT('settingsUnavailable') || 'Settings are unavailable',
|
|
2110
|
+
),
|
|
2111
|
+
),
|
|
2112
|
+
)
|
|
2113
|
+
)
|
|
2114
|
+
}
|
|
2115
|
+
if (props && props.status === 'error') {
|
|
2116
|
+
return React.createElement(page ? 'div' : 'li', {
|
|
2117
|
+
className: page ? 'dsub-page' : 'dsub-card',
|
|
2118
|
+
},
|
|
2119
|
+
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2120
|
+
React.createElement('div', { className: 'dsub-headText' },
|
|
2121
|
+
React.createElement('div', { className: 'dsub-name' }, cardT('title')),
|
|
2122
|
+
React.createElement('div', { className: 'dsub-description', style: { color: 'var(--dsw-alias-state-error-primary)' } },
|
|
2123
|
+
(props.error && (props.error.message || String(props.error))) || cardT('error') || cardT('settingsError') || 'Settings error',
|
|
2110
2124
|
),
|
|
2111
2125
|
),
|
|
2112
2126
|
)
|
|
@@ -2123,8 +2137,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2123
2137
|
'aria-expanded': page ? true : open,
|
|
2124
2138
|
},
|
|
2125
2139
|
React.createElement('div', { className: 'dsub-headText' },
|
|
2126
|
-
React.createElement('div', { className: 'dsub-name' },
|
|
2127
|
-
React.createElement('div', { className: 'dsub-description' },
|
|
2140
|
+
React.createElement('div', { className: 'dsub-name' }, cardT('title')),
|
|
2141
|
+
React.createElement('div', { className: 'dsub-description' }, cardT('cardIntro')),
|
|
2128
2142
|
),
|
|
2129
2143
|
React.createElement(Chevron, { className: 'dsub-chev' + (open ? ' dsub-chevOpen' : '') }),
|
|
2130
2144
|
),
|
|
@@ -2201,6 +2215,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2201
2215
|
))
|
|
2202
2216
|
if (typeof unRow === 'function') cleanups.push(unRow)
|
|
2203
2217
|
}
|
|
2218
|
+
|
|
2219
|
+
|
|
2204
2220
|
}
|
|
2205
2221
|
} catch {
|
|
2206
2222
|
// Slot system not available — expected on headless / non-web profiles.
|
|
@@ -2346,6 +2362,48 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2346
2362
|
return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
|
|
2347
2363
|
}
|
|
2348
2364
|
|
|
2365
|
+
const PLUGIN_LABELS = new Set(['Plugins', '插件'])
|
|
2366
|
+
function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms)) {
|
|
2367
|
+
try {
|
|
2368
|
+
const buttons = [...doc.querySelectorAll('button')]
|
|
2369
|
+
const panel = buttons.find((b) => PLUGIN_LABELS.has(b.getAttribute('aria-label') || ''))
|
|
2370
|
+
|| buttons.find((b) => PLUGIN_LABELS.has(b.textContent.trim()))
|
|
2371
|
+
if (panel) {
|
|
2372
|
+
panel.click()
|
|
2373
|
+
const poll = (left) => {
|
|
2374
|
+
try {
|
|
2375
|
+
for (const key of [PKG, 'dsh-subscriptions', '@goodandready/dsh-subscriptions']) {
|
|
2376
|
+
const card = doc.querySelector(`[data-plugin-package="${key}"]`) || doc.querySelector(`[data-plugin-item="${ROW_ID}"]`)
|
|
2377
|
+
if (card) {
|
|
2378
|
+
const open = [...card.querySelectorAll('button')].filter((b) => b.getAttribute('role') !== 'switch')
|
|
2379
|
+
const target = open.length >= 1 ? open[0] : card
|
|
2380
|
+
if (typeof target.click === 'function') {
|
|
2381
|
+
target.click()
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
} catch {}
|
|
2387
|
+
if (left > 0) schedule(() => poll(left - 1), 100)
|
|
2388
|
+
}
|
|
2389
|
+
schedule(() => poll(40), 150)
|
|
2390
|
+
return
|
|
2391
|
+
}
|
|
2392
|
+
const triggers = buttons.filter((b) => b.getAttribute('aria-haspopup') === 'dialog' && b.hasAttribute('aria-expanded'))
|
|
2393
|
+
if (triggers.length > 0) {
|
|
2394
|
+
const trigger = triggers.find((b) => b.getAttribute('aria-expanded') !== 'true') || triggers[0]
|
|
2395
|
+
if (trigger && typeof trigger.click === 'function') {
|
|
2396
|
+
trigger.click()
|
|
2397
|
+
schedule(() => {
|
|
2398
|
+
try {
|
|
2399
|
+
const section = [...doc.querySelectorAll('button')].find((b) => PLUGIN_LABELS.has(b.textContent.trim()))
|
|
2400
|
+
if (section && typeof section.click === 'function') section.click()
|
|
2401
|
+
} catch {}
|
|
2402
|
+
}, 80)
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
} catch {}
|
|
2406
|
+
}
|
|
2349
2407
|
function SubsPill(props) {
|
|
2350
2408
|
const t = (props && props.t) || ((k) => k)
|
|
2351
2409
|
const [state, setState] = React.useState({ logged: [], usage: 0, accounts: [], open: false, active: null, loading: false })
|
|
@@ -2556,37 +2614,23 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2556
2614
|
onClick: (e) => {
|
|
2557
2615
|
e.preventDefault()
|
|
2558
2616
|
setState((s) => Object.assign({}, s, { open: false }))
|
|
2617
|
+
const clientCtx = props.ctx || window.__dshCtx
|
|
2618
|
+
|
|
2559
2619
|
try {
|
|
2560
|
-
const nav = (
|
|
2620
|
+
const nav = (clientCtx && clientCtx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
|
|
2561
2621
|
if (nav && typeof nav.openBundle === 'function') {
|
|
2562
2622
|
nav.openBundle(PKG)
|
|
2563
|
-
return
|
|
2564
2623
|
}
|
|
2565
|
-
} catch {
|
|
2624
|
+
} catch {}
|
|
2566
2625
|
|
|
2567
2626
|
try {
|
|
2568
|
-
|
|
2569
|
-
|
|
2627
|
+
const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
|
|
2628
|
+
if (layout && typeof layout.selectPanel === 'function') {
|
|
2629
|
+
layout.selectPanel('plugins')
|
|
2570
2630
|
}
|
|
2571
|
-
} catch {
|
|
2631
|
+
} catch {}
|
|
2572
2632
|
|
|
2573
|
-
|
|
2574
|
-
const panelBtn = document.querySelector('[data-panel-id="plugins"]') ||
|
|
2575
|
-
document.querySelector('button[aria-label*="Plugin" i]') ||
|
|
2576
|
-
document.querySelector('button[title*="Plugin" i]')
|
|
2577
|
-
if (panelBtn) panelBtn.click()
|
|
2578
|
-
|
|
2579
|
-
setTimeout(() => {
|
|
2580
|
-
try {
|
|
2581
|
-
const item = document.querySelector(`[data-plugin-name="${PKG}"]`) ||
|
|
2582
|
-
document.querySelector(`[data-plugin-item="${ROW_ID}"]`) ||
|
|
2583
|
-
Array.from(document.querySelectorAll('button, a, div')).find((el) =>
|
|
2584
|
-
el.textContent && (el.textContent.includes('dsh-subscriptions') || el.textContent.includes(t('title')))
|
|
2585
|
-
)
|
|
2586
|
-
if (item) item.click()
|
|
2587
|
-
} catch { /* best-effort */ }
|
|
2588
|
-
}, 150)
|
|
2589
|
-
} catch { /* best-effort */ }
|
|
2633
|
+
openPluginSettings(document)
|
|
2590
2634
|
},
|
|
2591
2635
|
}, t('subsOpenSettings')),
|
|
2592
2636
|
),
|
|
@@ -2696,6 +2740,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2696
2740
|
ctx.pluginNavigation = navCtx.pluginNavigation
|
|
2697
2741
|
})
|
|
2698
2742
|
} catch { /* optional */ }
|
|
2743
|
+
try {
|
|
2744
|
+
ctx.inject(['layout'], (layoutCtx) => {
|
|
2745
|
+
ctx.layout = layoutCtx.layout
|
|
2746
|
+
})
|
|
2747
|
+
} catch { /* optional */ }
|
|
2699
2748
|
registerSettings(ctx)
|
|
2700
2749
|
registerSubsPill(ctx)
|
|
2701
2750
|
registerComposerQuota(ctx)
|
package/lib/config-schema.js
CHANGED
|
@@ -74,7 +74,7 @@ export const Config = z.object({
|
|
|
74
74
|
codexRedirectUri: z.string().volatile().default(''),
|
|
75
75
|
codexBaseUrl: z.string().volatile().default(''),
|
|
76
76
|
codexClientVersion: z.string().volatile().default('')
|
|
77
|
-
.description('Codex CLI identity version for /models catalog gating (e.g. 0.
|
|
77
|
+
.description('Codex CLI identity version for /models catalog gating (e.g. 0.160.0). Empty uses the built-in default.'),
|
|
78
78
|
claudeClientId: z.string().volatile().default(''),
|
|
79
79
|
claudeRedirectUri: z.string().volatile().default(''),
|
|
80
80
|
grokClientId: z.string().volatile().default(''),
|
package/lib/http.js
CHANGED
|
@@ -67,23 +67,7 @@ export function isLoopback(value) {
|
|
|
67
67
|
|| address.startsWith('::ffff:127.')
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
* @param {import('node:http').IncomingMessage} req
|
|
72
|
-
* @returns {boolean}
|
|
73
|
-
*/
|
|
74
|
-
export function hasHostCredentials(req) {
|
|
75
|
-
const headers = req?.headers || {}
|
|
76
|
-
const auth = headers.authorization
|
|
77
|
-
if (typeof auth === 'string' && auth.trim().length > 0) return true
|
|
78
|
-
const cookie = headers.cookie
|
|
79
|
-
if (typeof cookie === 'string' && cookie.trim().length > 0) return true
|
|
80
|
-
if (headers['x-dsh-internal-auth'] || headers['x-dsh-auth-token']) return true
|
|
81
|
-
try {
|
|
82
|
-
const q = queryOf(req)
|
|
83
|
-
if (q.get('token') || q.get('auth')) return true
|
|
84
|
-
} catch { /* ignore */ }
|
|
85
|
-
return false
|
|
86
|
-
}
|
|
70
|
+
|
|
87
71
|
|
|
88
72
|
/**
|
|
89
73
|
* @param {import('node:http').IncomingMessage} request
|
|
@@ -127,7 +111,12 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
127
111
|
}
|
|
128
112
|
|
|
129
113
|
// 4. Delegate to DSH connection service if available (#374)
|
|
130
|
-
|
|
114
|
+
let conn = null
|
|
115
|
+
try {
|
|
116
|
+
conn = targetCtx?.connection || request?.connection || (request && Reflect.get(request, 'connectionService'))
|
|
117
|
+
} catch {
|
|
118
|
+
conn = null
|
|
119
|
+
}
|
|
131
120
|
if (conn && typeof conn.requestRejection === 'function') {
|
|
132
121
|
const rejection = conn.requestRejection(request)
|
|
133
122
|
if (rejection === undefined) return true
|
|
@@ -137,17 +126,48 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
137
126
|
// 5. Loopback request (CLI, local daemon, localhost test)
|
|
138
127
|
if (isLoopback(request)) return true
|
|
139
128
|
|
|
140
|
-
// 6.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
129
|
+
// 6. Verified server secret token with exact cookie matching (#374)
|
|
130
|
+
const expectedToken = process.env.DSH_AUTH_TOKEN
|
|
131
|
+
if (expectedToken) {
|
|
132
|
+
const auth = headers.authorization
|
|
133
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ') && auth.slice(7).trim() === expectedToken) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
const cookieHeader = headers.cookie
|
|
137
|
+
if (typeof cookieHeader === 'string') {
|
|
138
|
+
const cookies = {}
|
|
139
|
+
for (const part of cookieHeader.split(';')) {
|
|
140
|
+
const eq = part.indexOf('=')
|
|
141
|
+
if (eq !== -1) {
|
|
142
|
+
cookies[part.slice(0, eq).trim()] = part.slice(eq + 1).trim()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (cookies.token === expectedToken || cookies.dsh_token === expectedToken) {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
145
150
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
-
|
|
151
|
+
// 7. Plain mock objects in unit tests without network socket
|
|
152
|
+
const remoteAddr = request?.socket?.remoteAddress || request?.connection?.remoteAddress
|
|
153
|
+
const hasRemoteSocket = Boolean(remoteAddr || request?.socket || request?.connection)
|
|
154
|
+
if (!hasRemoteSocket) {
|
|
155
|
+
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') return true
|
|
156
|
+
if (origin && host) {
|
|
157
|
+
try {
|
|
158
|
+
const u = new URL(origin)
|
|
159
|
+
if (u.host.toLowerCase() === host.toLowerCase()) return true
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
if (referer && host) {
|
|
163
|
+
try {
|
|
164
|
+
const u = new URL(referer)
|
|
165
|
+
if (u.host.toLowerCase() === host.toLowerCase()) return true
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
149
169
|
|
|
150
|
-
// Otherwise fail-closed
|
|
170
|
+
// Otherwise fail-closed (#374)
|
|
151
171
|
return false
|
|
152
172
|
}
|
|
153
173
|
|
package/lib/index.js
CHANGED
|
@@ -129,14 +129,14 @@ export function apply(ctx, rawConfig) {
|
|
|
129
129
|
|
|
130
130
|
const settingsApi = {
|
|
131
131
|
get: () => live(),
|
|
132
|
-
replace: async (next) => {
|
|
132
|
+
replace: async (next, expectedRevision) => {
|
|
133
133
|
const parsed = plainConfig(next)
|
|
134
134
|
const prevConfig = cachedConfig
|
|
135
135
|
try {
|
|
136
136
|
if (typeof settingsSvc?.update === 'function') {
|
|
137
|
-
await settingsSvc.update(NS, structuredClone(parsed), describeRow()?.revision)
|
|
137
|
+
await settingsSvc.update(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
|
|
138
138
|
} else if (typeof settingsSvc?.replace === 'function') {
|
|
139
|
-
await settingsSvc.replace(NS, structuredClone(parsed), describeRow()?.revision)
|
|
139
|
+
await settingsSvc.replace(NS, structuredClone(parsed), expectedRevision !== undefined ? expectedRevision : describeRow()?.revision)
|
|
140
140
|
}
|
|
141
141
|
cachedConfig = parsed
|
|
142
142
|
syncCustomVendors()
|
|
@@ -486,6 +486,7 @@ export function apply(ctx, rawConfig) {
|
|
|
486
486
|
registerRoutes(ctx, {
|
|
487
487
|
NS,
|
|
488
488
|
live,
|
|
489
|
+
describeRow,
|
|
489
490
|
accountsView,
|
|
490
491
|
getSettingsApi,
|
|
491
492
|
syncCustomVendors,
|
package/lib/messages.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import { extractImages, toOpenAiImage, toAnthropicImage, toGeminiImage, toCodexImage } from './images.js'
|
|
2
3
|
import { normalizeTools } from './tools-normalizer.js'
|
|
3
4
|
import { geminiFunctionDeclarations } from './gemini-schema.js'
|
|
@@ -21,6 +22,13 @@ function toolCalls(content) {
|
|
|
21
22
|
return content.filter((b) => b && (b.type === 'tool-call' || b.type === 'tool_use' || b.type === 'function_call'))
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export function normalizeCodexCallId(id) {
|
|
26
|
+
const str = String(id || "");
|
|
27
|
+
if (!str || str.length <= 64) return str;
|
|
28
|
+
const hash = createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
29
|
+
return str.slice(0, 47) + "_" + hash;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export function openaiMessages(options) {
|
|
25
33
|
const out = []
|
|
26
34
|
if (options.system) out.push({ role: 'system', content: options.system })
|
|
@@ -215,7 +223,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
215
223
|
flush()
|
|
216
224
|
rawInput.push({
|
|
217
225
|
type: 'function_call',
|
|
218
|
-
call_id:
|
|
226
|
+
call_id: normalizeCodexCallId(call.id),
|
|
219
227
|
name: call.name,
|
|
220
228
|
arguments: typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments || {}),
|
|
221
229
|
})
|
|
@@ -231,7 +239,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
231
239
|
if (callId) {
|
|
232
240
|
rawInput.push({
|
|
233
241
|
type: 'function_call_output',
|
|
234
|
-
call_id: callId,
|
|
242
|
+
call_id: normalizeCodexCallId(callId),
|
|
235
243
|
output: outText,
|
|
236
244
|
})
|
|
237
245
|
}
|
|
@@ -258,7 +266,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
258
266
|
if (callId) {
|
|
259
267
|
rawInput.push({
|
|
260
268
|
type: 'function_call_output',
|
|
261
|
-
call_id: callId,
|
|
269
|
+
call_id: normalizeCodexCallId(callId),
|
|
262
270
|
output: outText,
|
|
263
271
|
})
|
|
264
272
|
}
|
package/lib/rotate.js
CHANGED
|
@@ -103,7 +103,7 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
103
103
|
|
|
104
104
|
if (!opts || opts.autoPacing !== false) {
|
|
105
105
|
if (acc.pacingRisk === undefined) {
|
|
106
|
-
const rem = acc.quota?.remainingPercent ?? (acc.usagePercent != null ? (100 - Number(acc.usagePercent)) : null)
|
|
106
|
+
const rem = acc.quota?.remainingPercent ?? (acc.quota?.usedPercent != null ? (100 - Number(acc.quota.usedPercent)) : (acc.usagePercent != null ? (100 - Number(acc.usagePercent)) : null))
|
|
107
107
|
const pace = acc.pacePerHour || (Array.isArray(acc.samples) ? calculateBurnRatePerHour(acc.samples, now) : 0)
|
|
108
108
|
const resetAt = acc.quota?.resetAt || 0
|
|
109
109
|
const riskInfo = computePacingRisk(rem, pace, resetAt, now)
|
package/lib/routes/accounts.js
CHANGED
|
@@ -29,7 +29,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
29
29
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
30
30
|
return
|
|
31
31
|
}
|
|
32
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
32
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
33
33
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
34
34
|
return
|
|
35
35
|
}
|
|
@@ -55,7 +55,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
55
55
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
58
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
59
59
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
60
60
|
return
|
|
61
61
|
}
|
|
@@ -83,7 +83,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
83
83
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
84
84
|
return
|
|
85
85
|
}
|
|
86
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
86
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
87
87
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
88
88
|
return
|
|
89
89
|
}
|
|
@@ -111,7 +111,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
111
111
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
112
112
|
return
|
|
113
113
|
}
|
|
114
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
114
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
115
115
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
116
116
|
return
|
|
117
117
|
}
|
|
@@ -133,7 +133,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
133
133
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
134
134
|
return
|
|
135
135
|
}
|
|
136
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
136
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
137
137
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
138
138
|
return
|
|
139
139
|
}
|
|
@@ -198,7 +198,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
198
198
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
199
199
|
return
|
|
200
200
|
}
|
|
201
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
201
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
202
202
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
203
203
|
return
|
|
204
204
|
}
|
|
@@ -244,7 +244,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
244
244
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
245
245
|
return
|
|
246
246
|
}
|
|
247
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
247
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
248
248
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
249
249
|
return
|
|
250
250
|
}
|
|
@@ -292,7 +292,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
292
292
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
293
293
|
return
|
|
294
294
|
}
|
|
295
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
295
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
296
296
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
297
297
|
return
|
|
298
298
|
}
|
|
@@ -340,7 +340,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
340
340
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
341
341
|
return
|
|
342
342
|
}
|
|
343
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
343
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
344
344
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
345
345
|
return
|
|
346
346
|
}
|
|
@@ -366,7 +366,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
366
366
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
367
367
|
return
|
|
368
368
|
}
|
|
369
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
369
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
370
370
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
371
371
|
return
|
|
372
372
|
}
|
|
@@ -411,7 +411,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
411
411
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
412
412
|
return
|
|
413
413
|
}
|
|
414
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
414
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
415
415
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
416
416
|
return
|
|
417
417
|
}
|
package/lib/routes/oauth.js
CHANGED
|
@@ -102,7 +102,7 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
102
102
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
105
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
106
106
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
107
107
|
return
|
|
108
108
|
}
|
|
@@ -155,7 +155,7 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
155
155
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
156
156
|
return
|
|
157
157
|
}
|
|
158
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
158
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
159
159
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
160
160
|
return
|
|
161
161
|
}
|
|
@@ -231,7 +231,7 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
231
231
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
232
232
|
return
|
|
233
233
|
}
|
|
234
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
234
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
235
235
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
236
236
|
return
|
|
237
237
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -19,7 +19,7 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
19
19
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
22
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
23
23
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
24
24
|
return
|
|
25
25
|
}
|
|
@@ -46,7 +46,7 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
46
46
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
|
|
47
47
|
return
|
|
48
48
|
}
|
|
49
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
49
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
50
50
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
51
51
|
return
|
|
52
52
|
}
|
|
@@ -100,7 +100,7 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
100
100
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
101
101
|
return
|
|
102
102
|
}
|
|
103
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
103
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
104
104
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
105
105
|
return
|
|
106
106
|
}
|
package/lib/routes/status.js
CHANGED
|
@@ -65,7 +65,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
65
65
|
path: '/dsh-subscriptions/config',
|
|
66
66
|
handler: safeJsonHandler(async (req, res) => {
|
|
67
67
|
if (req.method === 'GET') {
|
|
68
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
68
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
69
69
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
70
70
|
return
|
|
71
71
|
}
|
|
@@ -76,7 +76,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
76
76
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or PUT' } })
|
|
77
77
|
return
|
|
78
78
|
}
|
|
79
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
79
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
80
80
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'settings writes are same-origin only' } })
|
|
81
81
|
return
|
|
82
82
|
}
|
|
@@ -125,7 +125,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
125
125
|
if (Array.isArray(payload.slots)) payload.slots = stripLegacySlots(payload.slots)
|
|
126
126
|
const parsed = plainConfig(Config(payload))
|
|
127
127
|
const dropped = droppedCredentialRefs(live().slots, parsed.slots)
|
|
128
|
-
await getSettingsApi().replace(parsed)
|
|
128
|
+
await getSettingsApi().replace(parsed, curRev)
|
|
129
129
|
currentRevision++
|
|
130
130
|
syncCustomVendors()
|
|
131
131
|
for (const ref of dropped) {
|
|
@@ -134,6 +134,17 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
134
134
|
await syncAdapter()
|
|
135
135
|
writeJson(res, 200, await configResponse())
|
|
136
136
|
} catch (e) {
|
|
137
|
+
if (/conflict/i.test(String(e && e.message || ''))) {
|
|
138
|
+
writeJson(res, 409, {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: {
|
|
141
|
+
code: 'conflict',
|
|
142
|
+
message: String(e && e.message || e),
|
|
143
|
+
currentRevision: getCurrentRevision(),
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
return
|
|
147
|
+
}
|
|
137
148
|
writeJson(res, 400, { ok: false, error: { code: 'save', message: String(e && e.message || e) } })
|
|
138
149
|
}
|
|
139
150
|
}),
|
|
@@ -145,7 +156,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
145
156
|
path: '/dsh-subscriptions/status',
|
|
146
157
|
handler: safeJsonHandler(async (req, res) => {
|
|
147
158
|
if (req.method === 'DELETE') {
|
|
148
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
159
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
149
160
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
150
161
|
return
|
|
151
162
|
}
|
|
@@ -157,7 +168,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
157
168
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or DELETE only' } })
|
|
158
169
|
return
|
|
159
170
|
}
|
|
160
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
171
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
161
172
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
162
173
|
return
|
|
163
174
|
}
|
|
@@ -236,7 +247,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
236
247
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
237
248
|
return
|
|
238
249
|
}
|
|
239
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
250
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
240
251
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
241
252
|
return
|
|
242
253
|
}
|
|
@@ -254,7 +265,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
254
265
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
255
266
|
return
|
|
256
267
|
}
|
|
257
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
268
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
258
269
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
259
270
|
return
|
|
260
271
|
}
|
|
@@ -334,7 +345,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
334
345
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
335
346
|
return
|
|
336
347
|
}
|
|
337
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
348
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
338
349
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
339
350
|
return
|
|
340
351
|
}
|
|
@@ -357,7 +368,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
357
368
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
358
369
|
return
|
|
359
370
|
}
|
|
360
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
371
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
361
372
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
362
373
|
return
|
|
363
374
|
}
|
|
@@ -377,7 +388,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
377
388
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
378
389
|
return
|
|
379
390
|
}
|
|
380
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
391
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
381
392
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
382
393
|
return
|
|
383
394
|
}
|
|
@@ -487,7 +498,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
487
498
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
488
499
|
return
|
|
489
500
|
}
|
|
490
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
501
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
491
502
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
492
503
|
return
|
|
493
504
|
}
|
package/lib/tools-normalizer.js
CHANGED
|
@@ -12,6 +12,7 @@ export function sanitizeDescription(desc) {
|
|
|
12
12
|
|
|
13
13
|
export function normalizeJsonSchema(schema, depth = 0) {
|
|
14
14
|
if (depth > 12) return { type: "string" }
|
|
15
|
+
if (typeof schema === "boolean") return schema
|
|
15
16
|
if (!schema || typeof schema !== "object") {
|
|
16
17
|
return { type: "object" }
|
|
17
18
|
}
|
package/lib/updater.js
CHANGED
|
@@ -23,13 +23,13 @@ export function isLoopback(value) {
|
|
|
23
23
|
|| address?.startsWith('::ffff:127.') === true
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function isTrustedUpdateRequest(request) {
|
|
26
|
+
export function isTrustedUpdateRequest(request, targetCtx) {
|
|
27
27
|
const customHeader = header(request, UPDATE_HEADER)
|
|
28
28
|
if (customHeader === '1') {
|
|
29
29
|
const remote = request.socket?.remoteAddress
|
|
30
30
|
if (remote && isLoopback(remote)) return true
|
|
31
31
|
}
|
|
32
|
-
return isTrustedSettingsRequest(request)
|
|
32
|
+
return isTrustedSettingsRequest(request, targetCtx)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function validProfileName(value) {
|
|
@@ -295,7 +295,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
295
295
|
response.end()
|
|
296
296
|
return
|
|
297
297
|
}
|
|
298
|
-
if (!isTrustedUpdateRequest(request)) {
|
|
298
|
+
if (!isTrustedUpdateRequest(request, ctx)) {
|
|
299
299
|
writeJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'Rejected non-local or cross-origin update request.' } })
|
|
300
300
|
return
|
|
301
301
|
}
|
package/lib/vendors/codex.js
CHANGED
|
@@ -31,6 +31,7 @@ export function defaults() {
|
|
|
31
31
|
baseUrl: 'https://chatgpt.com/backend-api/codex',
|
|
32
32
|
originator: 'codex_cli_rs',
|
|
33
33
|
models: [
|
|
34
|
+
{ id: 'gpt-6.1-sol', name: 'GPT-6.1 Sol' },
|
|
34
35
|
{ id: 'gpt-6-astra', name: 'GPT-6 Astra' },
|
|
35
36
|
{ id: 'gpt-6-sol', name: 'GPT-6 Sol' },
|
|
36
37
|
{ id: 'gpt-6-luna', name: 'GPT-6 Luna' },
|
|
@@ -39,7 +40,7 @@ export function defaults() {
|
|
|
39
40
|
{ id: 'gpt-5.6-luna', name: 'GPT-5.6 Luna' },
|
|
40
41
|
{ id: 'gpt-5.5', name: 'GPT-5.5' },
|
|
41
42
|
],
|
|
42
|
-
clientVersion: '0.
|
|
43
|
+
clientVersion: '0.160.0',
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.50",
|
|
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",
|