@goodandready/dsh-subscriptions 0.6.54 → 0.6.56
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/adapter-manager.js +8 -4
- package/lib/config-schema.js +14 -0
- package/lib/http.js +16 -3
- package/lib/index.js +7 -5
- package/lib/routes/oauth.js +4 -0
- package/lib/routes/status.js +2 -2
- package/lib/subscriptions.js +61 -15
- package/lib/vendor-factory.js +10 -0
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -114,8 +114,10 @@ export function createAdapterManager({
|
|
|
114
114
|
|
|
115
115
|
if (adapterDisposed) return
|
|
116
116
|
if (!handle && claimable.length) {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
if (ctx.llm && typeof ctx.llm.registerAdapter === 'function') {
|
|
118
|
+
handle = ctx.llm.registerAdapter(claimable, adapter)
|
|
119
|
+
registeredByUs = new Set(claimable)
|
|
120
|
+
}
|
|
119
121
|
return
|
|
120
122
|
}
|
|
121
123
|
if (!handle) {
|
|
@@ -148,8 +150,10 @@ export function createAdapterManager({
|
|
|
148
150
|
const currentGlobal = new Set((ctx.llm && typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []).map((p) => p.id))
|
|
149
151
|
const reClaimable = desired.filter((p) => !currentGlobal.has(p))
|
|
150
152
|
if (reClaimable.length) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
if (ctx.llm && typeof ctx.llm.registerAdapter === 'function') {
|
|
154
|
+
handle = ctx.llm.registerAdapter(reClaimable, adapter)
|
|
155
|
+
registeredByUs = new Set(reClaimable)
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
|
package/lib/config-schema.js
CHANGED
|
@@ -84,6 +84,20 @@ export const Config = z.object({
|
|
|
84
84
|
.description('Grok CLI identity version header. Empty uses the built-in default.'),
|
|
85
85
|
antigravityClientId: z.string().volatile().default(''),
|
|
86
86
|
antigravityRedirectUri: z.string().volatile().default(''),
|
|
87
|
+
antigravityModels: z.array(z.any()).volatile().default([])
|
|
88
|
+
.description('Custom model catalog for Antigravity.'),
|
|
89
|
+
codexOriginator: z.string().volatile().default('')
|
|
90
|
+
.description('Originator header for Codex API requests (e.g. codex_cli_rs).'),
|
|
91
|
+
codexSystemPrefix: z.string().volatile().default('')
|
|
92
|
+
.description('System prompt prefix for Codex.'),
|
|
93
|
+
codexModels: z.array(z.any()).volatile().default([])
|
|
94
|
+
.description('Custom model catalog for Codex.'),
|
|
95
|
+
claudeSystemPrefix: z.string().volatile().default('')
|
|
96
|
+
.description('System prompt prefix for Claude.'),
|
|
97
|
+
claudeModels: z.array(z.any()).volatile().default([])
|
|
98
|
+
.description('Custom model catalog for Claude.'),
|
|
99
|
+
grokModels: z.array(z.any()).volatile().default([])
|
|
100
|
+
.description('Custom model catalog for Grok.'),
|
|
87
101
|
cascadingFallback: z.boolean().volatile().default(false)
|
|
88
102
|
.description('#349 Opt-in cross-vendor fallback when all accounts of a provider are exhausted or rate limited.'),
|
|
89
103
|
cascadingChain: z.any().volatile().default({})
|
package/lib/http.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
|
+
|
|
1
3
|
export function writeJson(res, code, body) {
|
|
2
4
|
try {
|
|
3
5
|
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
@@ -12,6 +14,14 @@ export function writeHtml(res, code, html) {
|
|
|
12
14
|
} catch { /* socket closed */ }
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export function timingSafeCompare(a, b) {
|
|
18
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
19
|
+
const bufA = Buffer.from(a, 'utf8')
|
|
20
|
+
const bufB = Buffer.from(b, 'utf8')
|
|
21
|
+
if (bufA.length !== bufB.length) return false
|
|
22
|
+
return timingSafeEqual(bufA, bufB)
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export function readBody(req, maxBytes = 64 * 1024) {
|
|
16
26
|
return new Promise((resolve, reject) => {
|
|
17
27
|
const chunks = []
|
|
@@ -126,11 +136,11 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
126
136
|
// 5. Loopback request (CLI, local daemon, localhost test)
|
|
127
137
|
if (isLoopback(request)) return true
|
|
128
138
|
|
|
129
|
-
// 6. Verified server secret token with exact cookie matching (#374)
|
|
139
|
+
// 6. Verified server secret token with exact cookie matching (#374, #492)
|
|
130
140
|
const expectedToken = process.env.DSH_AUTH_TOKEN
|
|
131
141
|
if (expectedToken) {
|
|
132
142
|
const auth = headers.authorization
|
|
133
|
-
if (typeof auth === 'string' && auth.startsWith('Bearer ') && auth.slice(7).trim()
|
|
143
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ') && timingSafeCompare(auth.slice(7).trim(), expectedToken)) {
|
|
134
144
|
return true
|
|
135
145
|
}
|
|
136
146
|
const cookieHeader = headers.cookie
|
|
@@ -142,7 +152,7 @@ export function isTrustedSettingsRequest(request, targetCtx) {
|
|
|
142
152
|
cookies[part.slice(0, eq).trim()] = part.slice(eq + 1).trim()
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
|
-
if (cookies.token
|
|
155
|
+
if (timingSafeCompare(cookies.token, expectedToken) || timingSafeCompare(cookies.dsh_token, expectedToken)) {
|
|
146
156
|
return true
|
|
147
157
|
}
|
|
148
158
|
}
|
|
@@ -261,10 +271,12 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
261
271
|
clearTimeout(timer)
|
|
262
272
|
return res
|
|
263
273
|
}
|
|
274
|
+
if (typeof timer?.unref === 'function') timer.unref()
|
|
264
275
|
const origJson = typeof res.json === 'function' ? res.json.bind(res) : null
|
|
265
276
|
const origText = typeof res.text === 'function' ? res.text.bind(res) : null
|
|
266
277
|
if (origJson) {
|
|
267
278
|
res.json = () => {
|
|
279
|
+
if (typeof timer?.ref === 'function') timer.ref()
|
|
268
280
|
if (controller.signal.aborted) {
|
|
269
281
|
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
270
282
|
}
|
|
@@ -292,6 +304,7 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
292
304
|
}
|
|
293
305
|
if (origText) {
|
|
294
306
|
res.text = () => {
|
|
307
|
+
if (typeof timer?.ref === 'function') timer.ref()
|
|
295
308
|
if (controller.signal.aborted) {
|
|
296
309
|
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
297
310
|
}
|
package/lib/index.js
CHANGED
|
@@ -260,7 +260,11 @@ export function apply(ctx, rawConfig) {
|
|
|
260
260
|
const cfg = live()
|
|
261
261
|
const alive = !!cfg.ollamaFallback && await ollamaAlive(ollamaBase(cfg), fetch)
|
|
262
262
|
if (alive && !ollamaHandle) {
|
|
263
|
-
try {
|
|
263
|
+
try {
|
|
264
|
+
if (ctx.llm && typeof ctx.llm.registerAdapter === 'function') {
|
|
265
|
+
ollamaHandle = ctx.llm.registerAdapter(['ollama'], ollamaAdapter)
|
|
266
|
+
}
|
|
267
|
+
} catch { /* already registered elsewhere */ }
|
|
264
268
|
} else if (!alive && ollamaHandle) {
|
|
265
269
|
try { ollamaHandle() } catch { /* already gone */ }
|
|
266
270
|
ollamaHandle = undefined
|
|
@@ -442,10 +446,8 @@ export function apply(ctx, rawConfig) {
|
|
|
442
446
|
}
|
|
443
447
|
}
|
|
444
448
|
tick().catch(() => {})
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const id = timer(() => { tick().catch(() => {}) }, 60 * 1000)
|
|
448
|
-
return () => clear(id)
|
|
449
|
+
const id = setInterval(() => { tick().catch(() => {}) }, 60 * 1000)
|
|
450
|
+
return () => clearInterval(id)
|
|
449
451
|
}, 'dsh-subscriptions: refresh ahead')
|
|
450
452
|
|
|
451
453
|
// Background health-check probe loop
|
package/lib/routes/oauth.js
CHANGED
|
@@ -31,6 +31,10 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
31
31
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
32
32
|
return
|
|
33
33
|
}
|
|
34
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
35
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
34
38
|
const q = queryOf(req)
|
|
35
39
|
const provider = q.get('provider') || ''
|
|
36
40
|
const index = Number(q.get('index') || '1')
|
package/lib/routes/status.js
CHANGED
|
@@ -354,7 +354,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
354
354
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'localhost only' } })
|
|
355
355
|
return
|
|
356
356
|
}
|
|
357
|
-
const limit = Math.min(Number(queryOf(req).get('limit') || '10'), 100)
|
|
357
|
+
const limit = Math.max(1, Math.min(Number(queryOf(req).get('limit') || '10'), 100))
|
|
358
358
|
writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
|
|
359
359
|
}),
|
|
360
360
|
}), 'dsh-subscriptions: /history')
|
|
@@ -502,7 +502,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
502
502
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
503
503
|
return
|
|
504
504
|
}
|
|
505
|
-
const limit = Math.min(Number(queryOf(req).get('limit') || '50'), 100)
|
|
505
|
+
const limit = Math.max(1, Math.min(Number(queryOf(req).get('limit') || '50'), 100))
|
|
506
506
|
writeJson(res, 200, { ok: true, alerts: getRecentAlerts(limit) })
|
|
507
507
|
}),
|
|
508
508
|
}), 'dsh-subscriptions: /alerts')
|
package/lib/subscriptions.js
CHANGED
|
@@ -18,9 +18,16 @@ export function isAllowed(provider, path) {
|
|
|
18
18
|
let p = String(path || "")
|
|
19
19
|
// strip query and hash
|
|
20
20
|
p = p.split("?")[0].split("#")[0]
|
|
21
|
+
if (p.startsWith("//")) return false
|
|
21
22
|
try {
|
|
22
|
-
if (p.startsWith("http://") || p.startsWith("https://"))
|
|
23
|
-
|
|
23
|
+
if (p.startsWith("http://") || p.startsWith("https://")) {
|
|
24
|
+
p = new URL(p).pathname
|
|
25
|
+
} else {
|
|
26
|
+
p = new URL(p.startsWith("/") ? p : "/" + p, "http://localhost").pathname
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
24
31
|
if (!p.startsWith("/")) p = "/" + p
|
|
25
32
|
for (const allowed of list) {
|
|
26
33
|
const a = allowed.split("?")[0]
|
|
@@ -88,6 +95,11 @@ export function createSubscriptionsService(deps) {
|
|
|
88
95
|
throw err
|
|
89
96
|
}
|
|
90
97
|
const rawPath = String(path || "")
|
|
98
|
+
if (rawPath.startsWith("//")) {
|
|
99
|
+
const err = new Error("protocol-relative URL rejected: " + rawPath)
|
|
100
|
+
err.code = "FORBIDDEN"
|
|
101
|
+
throw err
|
|
102
|
+
}
|
|
91
103
|
if (rawPath.startsWith("http://") || rawPath.startsWith("https://")) {
|
|
92
104
|
const initialCfg = typeof deps.vendorConfig === "function" ? deps.vendorConfig(provider) : {}
|
|
93
105
|
const initialBase = baseFor(provider, initialCfg || {})
|
|
@@ -131,23 +143,57 @@ export function createSubscriptionsService(deps) {
|
|
|
131
143
|
const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
132
144
|
const cfg = deps.vendorConfig(provider)
|
|
133
145
|
const base = baseFor(provider, cfg)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
|
|
147
|
+
// normalize path and resolve target URL (#487)
|
|
148
|
+
let targetUrl
|
|
149
|
+
let baseOrigin = ""
|
|
150
|
+
let basePath = ""
|
|
151
|
+
try {
|
|
152
|
+
const bUrl = new URL(base)
|
|
153
|
+
baseOrigin = bUrl.origin
|
|
154
|
+
basePath = bUrl.pathname.replace(/\/$/, "")
|
|
155
|
+
} catch { /* ignore */ }
|
|
156
|
+
|
|
157
|
+
const isAbsolute = rawPath.startsWith("http://") || rawPath.startsWith("https://")
|
|
158
|
+
try {
|
|
159
|
+
if (isAbsolute) {
|
|
160
|
+
targetUrl = new URL(rawPath)
|
|
161
|
+
} else {
|
|
162
|
+
const cleanRel = rawPath.startsWith("/") ? rawPath : "/" + rawPath
|
|
163
|
+
if (basePath && (cleanRel === basePath || cleanRel.startsWith(basePath + "/"))) {
|
|
164
|
+
targetUrl = new URL(baseOrigin + cleanRel)
|
|
165
|
+
} else {
|
|
166
|
+
const cleanBase = base.replace(/\/$/, "")
|
|
167
|
+
targetUrl = new URL(cleanBase + cleanRel)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
const err = new Error("invalid URL for " + provider + ": " + rawPath)
|
|
172
|
+
err.code = "FORBIDDEN"
|
|
173
|
+
throw err
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!baseOrigin || targetUrl.origin.toLowerCase() !== baseOrigin.toLowerCase()) {
|
|
177
|
+
const err = new Error("external origin rejected for provider " + provider + ": " + rawPath)
|
|
178
|
+
err.code = "FORBIDDEN"
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!isAbsolute) {
|
|
183
|
+
if (basePath && targetUrl.pathname !== basePath && !targetUrl.pathname.startsWith(basePath + "/")) {
|
|
184
|
+
const err = new Error("path traversal rejected for provider " + provider + ": " + rawPath)
|
|
143
185
|
err.code = "FORBIDDEN"
|
|
144
186
|
throw err
|
|
145
187
|
}
|
|
146
|
-
} else {
|
|
147
|
-
if (!p.startsWith("/")) p = "/" + p
|
|
148
|
-
p = base + p
|
|
149
188
|
}
|
|
150
|
-
|
|
189
|
+
|
|
190
|
+
if (!isAllowed(provider, targetUrl.pathname)) {
|
|
191
|
+
const err = new Error("path not allowed for " + provider + ": " + targetUrl.pathname)
|
|
192
|
+
err.code = "FORBIDDEN"
|
|
193
|
+
throw err
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const url = targetUrl.toString()
|
|
151
197
|
const extraHeaders = headersFor(provider, blob, cfg)
|
|
152
198
|
const fetchImpl = pickFetch(deps, account.ref)
|
|
153
199
|
const requestTimeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 30000
|
package/lib/vendor-factory.js
CHANGED
|
@@ -139,6 +139,16 @@ export function validateProfile(raw) {
|
|
|
139
139
|
}
|
|
140
140
|
if (!/^[a-z][a-z0-9_]*$/.test(p.id)) throw new Error(`customVendors: id "${p.id}" must match [a-z][a-z0-9_]*`)
|
|
141
141
|
if (p.tokenStyle && !["form", "json"].includes(p.tokenStyle)) throw new Error(`customVendors[${p.id}]: tokenStyle must be form|json`)
|
|
142
|
+
for (const key of ["authUrl", "tokenUrl", "baseUrl"]) {
|
|
143
|
+
try {
|
|
144
|
+
const u = new URL(p[key])
|
|
145
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
146
|
+
throw new Error("protocol must be http: or https:")
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
throw new Error(`customVendors[${p.id}]: field ${key} must be a valid http(s) URL (${e.message || e})`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
142
152
|
return {
|
|
143
153
|
id: p.id,
|
|
144
154
|
displayName: p.displayName || p.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.56",
|
|
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",
|