@goodandready/dsh-subscriptions 0.6.36 → 0.6.37
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/http.js +43 -1
- package/lib/import-auth.js +1 -1
- package/lib/index.js +20 -19
- package/lib/routes/accounts.js +19 -8
- package/lib/routes/oauth.js +1 -1
- package/lib/routes/status.js +1 -0
- package/package.json +1 -1
package/lib/http.js
CHANGED
|
@@ -131,7 +131,49 @@ export function queryOf(req) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Fetch with connect/overall timeout using AbortSignal.any.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
|
|
135
|
+
export function isSameOrigin(url1, url2) {
|
|
136
|
+
try {
|
|
137
|
+
const u1 = new URL(url1)
|
|
138
|
+
const u2 = new URL(url2)
|
|
139
|
+
return u1.origin.toLowerCase() === u2.origin.toLowerCase()
|
|
140
|
+
} catch {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function sanitizeRequestHeaders(url, init = {}, options = {}) {
|
|
146
|
+
const headers = { ...(init.headers || {}) }
|
|
147
|
+
if (!url) return headers
|
|
148
|
+
try {
|
|
149
|
+
const parsed = new URL(url)
|
|
150
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
151
|
+
delete headers.authorization
|
|
152
|
+
delete headers.Authorization
|
|
153
|
+
delete headers.cookie
|
|
154
|
+
delete headers.Cookie
|
|
155
|
+
return headers
|
|
156
|
+
}
|
|
157
|
+
const allowedOrigins = options.allowedOrigins || []
|
|
158
|
+
if (allowedOrigins.length > 0) {
|
|
159
|
+
const targetOrigin = parsed.origin.toLowerCase()
|
|
160
|
+
const isAllowed = allowedOrigins.some(o => {
|
|
161
|
+
try { return new URL(o).origin.toLowerCase() === targetOrigin } catch { return false }
|
|
162
|
+
})
|
|
163
|
+
if (!isAllowed) {
|
|
164
|
+
delete headers.authorization
|
|
165
|
+
delete headers.Authorization
|
|
166
|
+
delete headers.cookie
|
|
167
|
+
delete headers.Cookie
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
171
|
+
return headers
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs = 30000, allowedOrigins } = {}) {
|
|
175
|
+
const safeHeaders = sanitizeRequestHeaders(url, init, { allowedOrigins })
|
|
176
|
+
init = { ...init, headers: safeHeaders }
|
|
135
177
|
const impl = fetchImpl || fetch
|
|
136
178
|
if (!timeoutMs || timeoutMs <= 0) return impl(url, init)
|
|
137
179
|
const controller = new AbortController()
|
package/lib/import-auth.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -198,12 +198,16 @@ export function apply(ctx, rawConfig) {
|
|
|
198
198
|
return proxyFetch(slot.proxyUrl)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
function redirectFor(ref,
|
|
201
|
+
function redirectFor(provider, ref, reqOrigin) {
|
|
202
|
+
if (typeof provider === "object" && provider !== null) {
|
|
203
|
+
const opts = provider
|
|
204
|
+
provider = opts.provider
|
|
205
|
+
ref = opts.ref
|
|
206
|
+
reqOrigin = opts.origin
|
|
207
|
+
}
|
|
202
208
|
if (live().useWebCallback) {
|
|
203
|
-
const origin = (ctx.webServer && typeof ctx.webServer.url ===
|
|
204
|
-
|
|
205
|
-
: ''
|
|
206
|
-
return webCallbackUri(origin, ref)
|
|
209
|
+
const origin = reqOrigin || ((ctx.webServer && typeof ctx.webServer.url === "string") ? ctx.webServer.url : "")
|
|
210
|
+
return webCallbackUri(origin)
|
|
207
211
|
}
|
|
208
212
|
return getVendor(provider).redirectUri(vendorConfig(provider, live()))
|
|
209
213
|
}
|
|
@@ -359,20 +363,17 @@ export function apply(ctx, rawConfig) {
|
|
|
359
363
|
}
|
|
360
364
|
|
|
361
365
|
async function completeOAuth({ provider, index, code, state }) {
|
|
362
|
-
if (!
|
|
363
|
-
const n = Number(index)
|
|
364
|
-
const ref = oauthRef(provider, n)
|
|
366
|
+
if (!state) throw new Error('missing oauth state')
|
|
365
367
|
sweepPending(Date.now())
|
|
366
|
-
|
|
367
|
-
if (!row)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const blob = await getVendor(provider).exchangeCode(cfg, {
|
|
368
|
+
const row = pending.get(state)
|
|
369
|
+
if (!row || !row.verifier) throw new Error('login session expired or invalid state; start Connect again')
|
|
370
|
+
if (provider && row.provider !== provider) throw new Error('state provider mismatch')
|
|
371
|
+
if (index !== undefined && index !== null && Number(index) !== row.index) throw new Error('state index mismatch')
|
|
372
|
+
const actualProvider = row.provider
|
|
373
|
+
const n = row.index
|
|
374
|
+
const ref = oauthRef(actualProvider, n)
|
|
375
|
+
const cfg = { ...vendorConfig(actualProvider, live()), redirectUri: row.redirectUri }
|
|
376
|
+
const blob = await getVendor(actualProvider).exchangeCode(cfg, {
|
|
376
377
|
verifier: row.verifier,
|
|
377
378
|
challenge: row.challenge,
|
|
378
379
|
state: row.state,
|
|
@@ -384,7 +385,7 @@ export function apply(ctx, rawConfig) {
|
|
|
384
385
|
pending.delete(row.state)
|
|
385
386
|
await syncAdapter()
|
|
386
387
|
refreshModels().catch(() => {})
|
|
387
|
-
return { ref, label: pmL(blob.label || blob.email) || displayName(
|
|
388
|
+
return { ref, label: pmL(blob.label || blob.email) || displayName(actualProvider) }
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
// Privacy masking
|
package/lib/routes/accounts.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Config, plainConfig, publicConfig } from '../config-schema.js'
|
|
|
4
4
|
import { decryptWithPassphrase, encryptWithPassphrase } from '../crypto.js'
|
|
5
5
|
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
6
6
|
import { discoverLocalCliSessions, loadLocalCliBlob } from '../import-auth.js'
|
|
7
|
-
import { isProvider, oauthRef } from '../refs.js'
|
|
7
|
+
import { isProvider, oauthRef, parseOauthRef } from '../refs.js'
|
|
8
8
|
|
|
9
9
|
export function registerAccountsRoutes(ctx, state) {
|
|
10
10
|
const {
|
|
@@ -143,8 +143,13 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
143
143
|
} catch {
|
|
144
144
|
body = null
|
|
145
145
|
}
|
|
146
|
-
if (!body || !body.provider) {
|
|
147
|
-
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'missing provider' } })
|
|
146
|
+
if (!body || !body.provider || !isProvider(body.provider)) {
|
|
147
|
+
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'unknown or missing provider' } })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const idxNum = Number(body.index) || 1
|
|
151
|
+
if (!Number.isInteger(idxNum) || idxNum < 1 || idxNum > 99) {
|
|
152
|
+
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'invalid account index' } })
|
|
148
153
|
return
|
|
149
154
|
}
|
|
150
155
|
try {
|
|
@@ -164,7 +169,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
164
169
|
syncCustomVendors()
|
|
165
170
|
}
|
|
166
171
|
const slot = normalizeSlots(live().slots).find((s) => s.provider === prov && s.index === idx)
|
|
167
|
-
const ref = slot ? slot.ref :
|
|
172
|
+
const ref = slot ? slot.ref : oauthRef(prov, idx)
|
|
168
173
|
await store.saveBlob(ref, blob)
|
|
169
174
|
await syncAdapter()
|
|
170
175
|
refreshModels().catch(() => {})
|
|
@@ -201,11 +206,12 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
201
206
|
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
202
207
|
return
|
|
203
208
|
}
|
|
204
|
-
const passphrase = payload.passphrase
|
|
205
|
-
if (!passphrase ||
|
|
206
|
-
writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase
|
|
209
|
+
const passphrase = String(payload.passphrase || '').trim()
|
|
210
|
+
if (!passphrase || passphrase.length < 12) {
|
|
211
|
+
writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase must be at least 12 characters' } })
|
|
207
212
|
return
|
|
208
213
|
}
|
|
214
|
+
const algorithm = payload.algorithm === 'pbkdf2' || payload.algorithm === 'scrypt-v2' || payload.algorithm === 'scrypt-hardened' ? payload.algorithm : 'scrypt'
|
|
209
215
|
try {
|
|
210
216
|
const accounts = []
|
|
211
217
|
for (const slot of normalizeSlots(live().slots)) {
|
|
@@ -219,7 +225,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
219
225
|
return
|
|
220
226
|
}
|
|
221
227
|
const bundle = JSON.stringify({ v: 1, exportedAt: Date.now(), accounts })
|
|
222
|
-
const encrypted = encryptWithPassphrase(bundle, passphrase)
|
|
228
|
+
const encrypted = encryptWithPassphrase(bundle, passphrase, { algorithm })
|
|
223
229
|
writeJson(res, 200, { ok: true, payload: encrypted, count: accounts.length })
|
|
224
230
|
} catch (e) {
|
|
225
231
|
writeJson(res, 500, { ok: false, error: { code: 'export', message: String(e && e.message || e) } })
|
|
@@ -265,6 +271,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
265
271
|
let imported = 0
|
|
266
272
|
for (const row of bundle.accounts) {
|
|
267
273
|
try {
|
|
274
|
+
if (!row || typeof row.ref !== 'string' || !parseOauthRef(row.ref) || !row.blob) continue
|
|
268
275
|
await store.saveBlob(row.ref, row.blob)
|
|
269
276
|
imported++
|
|
270
277
|
} catch { /* skip broken */ }
|
|
@@ -301,6 +308,10 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
301
308
|
writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
|
|
302
309
|
return
|
|
303
310
|
}
|
|
311
|
+
if (!Number.isInteger(index) || index < 1 || index > 99) {
|
|
312
|
+
writeJson(res, 400, { ok: false, error: { code: 'index', message: 'account index must be an integer between 1 and 99' } })
|
|
313
|
+
return
|
|
314
|
+
}
|
|
304
315
|
if (!refreshToken && !apiKey) {
|
|
305
316
|
writeJson(res, 400, { ok: false, error: { code: 'token', message: 'refreshToken or apiKey is required' } })
|
|
306
317
|
return
|
package/lib/routes/oauth.js
CHANGED
|
@@ -39,7 +39,7 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
39
39
|
return
|
|
40
40
|
}
|
|
41
41
|
const origin = requestOrigin(req)
|
|
42
|
-
const redirectUri = redirectFor(provider,
|
|
42
|
+
const redirectUri = redirectFor(provider, `${provider}:${index}`, origin)
|
|
43
43
|
const pkce = await createPkce()
|
|
44
44
|
pending.set(pkce.state, {
|
|
45
45
|
provider,
|
package/lib/routes/status.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
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",
|