@goodandready/dsh-subscriptions 0.6.53 → 0.6.55
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 +2 -0
- package/lib/adapter.js +12 -1
- package/lib/config-schema.js +4 -0
- package/lib/forecast.js +16 -0
- package/lib/http.js +16 -3
- package/lib/index.js +9 -5
- package/lib/rotate.js +41 -1
- package/lib/routes/oauth.js +4 -0
- package/lib/routes/status.js +2 -2
- package/lib/stream-rotate.js +2 -0
- package/lib/subscriptions.js +67 -16
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -42,6 +42,8 @@ export function createAdapterManager({
|
|
|
42
42
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
43
43
|
cooldownMs: () => live().cooldownMs,
|
|
44
44
|
autoPacing: () => live().autoPacing,
|
|
45
|
+
drainBeforeReset: () => live().drainBeforeReset,
|
|
46
|
+
drainSafetyPercent: () => live().drainSafetyPercent,
|
|
45
47
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
46
48
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
47
49
|
rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
|
package/lib/adapter.js
CHANGED
|
@@ -121,7 +121,18 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
121
121
|
const effectiveAutoPacing = options.autoPacing !== undefined
|
|
122
122
|
? options.autoPacing
|
|
123
123
|
: (typeof deps.autoPacing === 'function' ? deps.autoPacing() : (deps.autoPacing ?? true))
|
|
124
|
-
const
|
|
124
|
+
const effectiveDrainBeforeReset = options.drainBeforeReset !== undefined
|
|
125
|
+
? options.drainBeforeReset
|
|
126
|
+
: (typeof deps.drainBeforeReset === 'function' ? deps.drainBeforeReset() : (deps.drainBeforeReset ?? false))
|
|
127
|
+
const effectiveDrainSafetyPercent = options.drainSafetyPercent !== undefined
|
|
128
|
+
? options.drainSafetyPercent
|
|
129
|
+
: (typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : (deps.drainSafetyPercent ?? 5))
|
|
130
|
+
const streamOptions = {
|
|
131
|
+
...options,
|
|
132
|
+
autoPacing: effectiveAutoPacing,
|
|
133
|
+
drainBeforeReset: effectiveDrainBeforeReset,
|
|
134
|
+
drainSafetyPercent: effectiveDrainSafetyPercent,
|
|
135
|
+
}
|
|
125
136
|
for await (const event of streamWithRotation({
|
|
126
137
|
accounts: await deps.listAccounts(provider),
|
|
127
138
|
nowMs: () => Date.now(),
|
package/lib/config-schema.js
CHANGED
|
@@ -94,6 +94,10 @@ export const Config = z.object({
|
|
|
94
94
|
.description('#351 Quota usage percentage thresholds for firing alerts.'),
|
|
95
95
|
autoPacing: z.boolean().volatile().default(true)
|
|
96
96
|
.description('#352 Predictive burn-rate load balancing between accounts.'),
|
|
97
|
+
drainBeforeReset: z.boolean().volatile().default(false)
|
|
98
|
+
.description('#485 Prioritize accounts with closest reset window to burn expiring quota first.'),
|
|
99
|
+
drainSafetyPercent: z.number().volatile().default(5)
|
|
100
|
+
.description('#485 Safety margin percent: avoid draining accounts below this threshold when alternatives exist.'),
|
|
97
101
|
customVendors: z.array(z.any()).volatile().default([])
|
|
98
102
|
.description('Declarative OpenAI-Responses-compatible providers. See README.'),
|
|
99
103
|
})
|
package/lib/forecast.js
CHANGED
|
@@ -125,3 +125,19 @@ export function computePacingRisk(remainingPercent, pacePerHour, resetsAt, now =
|
|
|
125
125
|
pacingRisk: hoursUntilExhaustion < 1 ? 50 : 0,
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// #485: Smart Quota Drainer urgency calculation
|
|
130
|
+
export function calculateBurnUrgency(remainingPercent, resetAt, nowMs = Date.now(), safetyFloor = 5) {
|
|
131
|
+
const rem = Number(remainingPercent)
|
|
132
|
+
if (!Number.isFinite(rem) || rem <= 0) return 0
|
|
133
|
+
const floor = Number(safetyFloor) || 0
|
|
134
|
+
if (rem <= floor) return 0
|
|
135
|
+
|
|
136
|
+
const reset = Number(resetAt)
|
|
137
|
+
if (!Number.isFinite(reset) || reset <= nowMs) return 0
|
|
138
|
+
|
|
139
|
+
const hoursUntilReset = (reset - nowMs) / HOUR_MS
|
|
140
|
+
// Denominator clamped to minimum 0.05 hours (3 mins) to avoid infinity
|
|
141
|
+
const clampedHours = Math.max(hoursUntilReset, 0.05)
|
|
142
|
+
return rem / clampedHours
|
|
143
|
+
}
|
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
|
@@ -314,6 +314,8 @@ export function apply(ctx, rawConfig) {
|
|
|
314
314
|
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
315
315
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
316
316
|
cooldownMs: () => live().cooldownMs,
|
|
317
|
+
drainBeforeReset: () => live().drainBeforeReset,
|
|
318
|
+
drainSafetyPercent: () => live().drainSafetyPercent,
|
|
317
319
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
318
320
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
319
321
|
rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
|
|
@@ -348,7 +350,11 @@ export function apply(ctx, rawConfig) {
|
|
|
348
350
|
throw new Error(`unknown subscription provider: ${provider}`)
|
|
349
351
|
}
|
|
350
352
|
const accounts = await store.listAccounts(provider)
|
|
351
|
-
const eligible = pickAccount(accounts || [], Date.now(), {
|
|
353
|
+
const eligible = pickAccount(accounts || [], Date.now(), {
|
|
354
|
+
family: provider === 'codex' ? 'gpt' : 'grok',
|
|
355
|
+
drainBeforeReset: live().drainBeforeReset,
|
|
356
|
+
drainSafetyPercent: live().drainSafetyPercent,
|
|
357
|
+
})
|
|
352
358
|
const slot = eligible || (accounts || []).find((row) => row && row.ref && row.hasToken)
|
|
353
359
|
if (!slot) throw new Error(`not logged in to ${provider}: sign in in the Subscriptions section`)
|
|
354
360
|
const raw = await store.resolveRaw(slot.ref)
|
|
@@ -436,10 +442,8 @@ export function apply(ctx, rawConfig) {
|
|
|
436
442
|
}
|
|
437
443
|
}
|
|
438
444
|
tick().catch(() => {})
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
const id = timer(() => { tick().catch(() => {}) }, 60 * 1000)
|
|
442
|
-
return () => clear(id)
|
|
445
|
+
const id = setInterval(() => { tick().catch(() => {}) }, 60 * 1000)
|
|
446
|
+
return () => clearInterval(id)
|
|
443
447
|
}, 'dsh-subscriptions: refresh ahead')
|
|
444
448
|
|
|
445
449
|
// Background health-check probe loop
|
package/lib/rotate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computePacingRisk, calculateBurnRatePerHour } from "./forecast.js"
|
|
1
|
+
import { computePacingRisk, calculateBurnRatePerHour, calculateBurnUrgency } from "./forecast.js"
|
|
2
2
|
import { isQuarantined } from './quarantine.js'
|
|
3
3
|
import { getPinnedAccountRef, pinSession } from './session-pin.js'
|
|
4
4
|
|
|
@@ -41,6 +41,28 @@ export function cooldownBlocks(acc, family) {
|
|
|
41
41
|
return fams.includes(family)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
export function getAccountRemainingPercent(acc) {
|
|
46
|
+
if (!acc) return null
|
|
47
|
+
if (acc.quota?.remainingPercent != null) {
|
|
48
|
+
const val = Number(acc.quota.remainingPercent)
|
|
49
|
+
return Number.isFinite(val) ? val : null
|
|
50
|
+
}
|
|
51
|
+
if (acc.quota?.usedPercent != null) {
|
|
52
|
+
const val = 100 - Number(acc.quota.usedPercent)
|
|
53
|
+
return Number.isFinite(val) ? val : null
|
|
54
|
+
}
|
|
55
|
+
if (acc.usagePercent != null) {
|
|
56
|
+
const val = 100 - Number(acc.usagePercent)
|
|
57
|
+
return Number.isFinite(val) ? val : null
|
|
58
|
+
}
|
|
59
|
+
if (acc.quota?.remaining != null && acc.quota?.limit != null && Number(acc.quota.limit) > 0) {
|
|
60
|
+
const val = (Number(acc.quota.remaining) / Number(acc.quota.limit)) * 100
|
|
61
|
+
return Number.isFinite(val) ? val : null
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
export function pickAccount(accounts, nowMs, opts) {
|
|
45
67
|
const list = Array.isArray(accounts) ? accounts : []
|
|
46
68
|
const now = Number(nowMs) || 0
|
|
@@ -111,6 +133,15 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
|
|
136
|
+
if (opts && opts.drainBeforeReset) {
|
|
137
|
+
if (acc.burnUrgency === undefined) {
|
|
138
|
+
const rem = getAccountRemainingPercent(acc)
|
|
139
|
+
const resetAt = acc.quota?.resetAt || 0
|
|
140
|
+
const safetyFloor = opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5
|
|
141
|
+
acc.burnUrgency = calculateBurnUrgency(rem, resetAt, now, safetyFloor)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
114
145
|
// VIP Slot Reservation (#175)
|
|
115
146
|
if (acc.vipOnly && !isVipRequest) continue
|
|
116
147
|
|
|
@@ -142,6 +173,15 @@ export function pickAccount(accounts, nowMs, opts) {
|
|
|
142
173
|
for (const tier of tiers) {
|
|
143
174
|
if (tier.length) {
|
|
144
175
|
tier.sort((a, b) => {
|
|
176
|
+
// #485: Smart Quota Drainer - prioritize higher burn urgency first
|
|
177
|
+
if (opts && opts.drainBeforeReset) {
|
|
178
|
+
const aUrg = Number(a.burnUrgency) || 0
|
|
179
|
+
const bUrg = Number(b.burnUrgency) || 0
|
|
180
|
+
if (Math.abs(aUrg - bUrg) > 0.01) {
|
|
181
|
+
return bUrg - aUrg
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
// 0. Auto-Pacing: lower pacing risk preferred (#352)
|
|
146
186
|
const aRisk = Number(a.pacingRisk) || 0
|
|
147
187
|
const bRisk = Number(b.pacingRisk) || 0
|
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/stream-rotate.js
CHANGED
|
@@ -28,6 +28,8 @@ export async function* streamWithRotation({
|
|
|
28
28
|
tag: options && options.tag,
|
|
29
29
|
vip: options && options.vip,
|
|
30
30
|
autoPacing: options && (options.autoPacing !== undefined ? options.autoPacing : (options.config && options.config.autoPacing)),
|
|
31
|
+
drainBeforeReset: options && (options.drainBeforeReset !== undefined ? options.drainBeforeReset : (options.config && options.config.drainBeforeReset)),
|
|
32
|
+
drainSafetyPercent: options && (options.drainSafetyPercent !== undefined ? options.drainSafetyPercent : (options.config && options.config.drainSafetyPercent)),
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
if (!account || tried.has(account.ref || account.id)) {
|
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 || {})
|
|
@@ -108,7 +120,12 @@ export function createSubscriptionsService(deps) {
|
|
|
108
120
|
const tried = new Set()
|
|
109
121
|
let lastError = null
|
|
110
122
|
while (true) {
|
|
111
|
-
const account = pickAccount(pool, Date.now(), {
|
|
123
|
+
const account = pickAccount(pool, Date.now(), {
|
|
124
|
+
switchAtRemaining: thr,
|
|
125
|
+
family: modelFamily(provider, body && body.model),
|
|
126
|
+
drainBeforeReset: typeof deps.drainBeforeReset === 'function' ? deps.drainBeforeReset() : deps.drainBeforeReset,
|
|
127
|
+
drainSafetyPercent: typeof deps.drainSafetyPercent === 'function' ? deps.drainSafetyPercent() : deps.drainSafetyPercent,
|
|
128
|
+
})
|
|
112
129
|
if (!account) {
|
|
113
130
|
if (lastError) throw lastError
|
|
114
131
|
const err = new Error("no usable subscription account for this provider")
|
|
@@ -126,23 +143,57 @@ export function createSubscriptionsService(deps) {
|
|
|
126
143
|
const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
127
144
|
const cfg = deps.vendorConfig(provider)
|
|
128
145
|
const base = baseFor(provider, cfg)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
185
|
err.code = "FORBIDDEN"
|
|
139
186
|
throw err
|
|
140
187
|
}
|
|
141
|
-
} else {
|
|
142
|
-
if (!p.startsWith("/")) p = "/" + p
|
|
143
|
-
p = base + p
|
|
144
188
|
}
|
|
145
|
-
|
|
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()
|
|
146
197
|
const extraHeaders = headersFor(provider, blob, cfg)
|
|
147
198
|
const fetchImpl = pickFetch(deps, account.ref)
|
|
148
199
|
const requestTimeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 30000
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.55",
|
|
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",
|