@goodandready/dsh-subscriptions 0.6.35 → 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.
@@ -5,7 +5,7 @@ import {
5
5
  antigravityIdentityHeaders,
6
6
  } from './code-assist.js'
7
7
 
8
- export async function enrichAntigravityAccount(slot, info, store, fetch) {
8
+ async function enrichAntigravityAccount(slot, info, store, fetch) {
9
9
  if (!info.configured || slot.provider !== 'antigravity') return info
10
10
  let blob
11
11
  try { blob = await store.loadBlob(slot.ref) } catch { return info }
package/lib/client.js CHANGED
@@ -2180,16 +2180,6 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2180
2180
  ))
2181
2181
  if (typeof un2 === 'function') cleanups.push(un2)
2182
2182
 
2183
- const un3 = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
2184
- {
2185
- name: 'settings.plugin.item',
2186
- key: NS,
2187
- locale: NS,
2188
- inject: () => ({ ctx: ctx }),
2189
- },
2190
- PluginCard,
2191
- ))
2192
- if (typeof un3 === 'function') cleanups.push(un3)
2193
2183
  }
2194
2184
  } catch {
2195
2185
  // Slot system not available — expected on headless / non-web profiles.
@@ -1,6 +1,14 @@
1
1
  import z from '@deepseek-ai/schemastery'
2
2
  import { PROVIDERS } from './refs.js'
3
3
 
4
+ // Defensive polyfill for environments / runners with schemastery < 3.18.4
5
+ const proto = z?.Schema?.prototype || (typeof z?.number === "function" ? Object.getPrototypeOf(z.number()) : null)
6
+ if (proto && typeof proto.volatile !== "function") {
7
+ proto.volatile = function volatile() {
8
+ return typeof this.extra === "function" ? this.extra("volatile", true) : this
9
+ }
10
+ }
11
+
4
12
  export const Slot = z.object({
5
13
  provider: z.string().default('codex')
6
14
  .description('One of: codex, claude, grok, antigravity.'),
@@ -2,7 +2,7 @@ import { maskText } from './mask.js'
2
2
  import { normalizeSlots } from './accounts.js'
3
3
  import { PROVIDERS } from './refs.js'
4
4
 
5
- export function scrubReport(v) {
5
+ function scrubReport(v) {
6
6
  if (typeof v === 'string') return maskText(v)
7
7
  if (Array.isArray(v)) return v.map(scrubReport)
8
8
  if (v && typeof v === 'object') {
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
- export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs = 30000 } = {}) {
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()
@@ -232,7 +232,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
232
232
  accessToken: tok,
233
233
  refreshToken: tok,
234
234
  githubToken: tok,
235
- expiresAt: Date.now() + 30 * 86400 * 1000,
235
+ expiresAt: 0,
236
236
  email,
237
237
  label: email,
238
238
  }
package/lib/index.js CHANGED
@@ -151,7 +151,7 @@ export function apply(ctx, rawConfig) {
151
151
  if (!settingsSvc) return
152
152
  const served = describeRow()
153
153
  if (served && served.value) {
154
- try { cachedConfig = plainConfig(Config(plainConfig(served.value))) } catch { /* keep current */ }
154
+ try { syncSnapshot(plainConfig(Config(plainConfig(served.value)))) } catch { /* keep current */ }
155
155
  }
156
156
  if (typeof sctx.effect === 'function') {
157
157
  sctx.effect(() => () => { settingsSvc = null })
@@ -168,7 +168,7 @@ export function apply(ctx, rawConfig) {
168
168
  const onSettingsChanged = () => {
169
169
  try {
170
170
  const row = describeRow()
171
- if (row && row.value) cachedConfig = plainConfig(Config(plainConfig(row.value)))
171
+ if (row && row.value) syncSnapshot(plainConfig(Config(plainConfig(row.value))))
172
172
  else syncSnapshot()
173
173
  reconcileSlots().catch(() => {})
174
174
  } catch { /* keep existing snapshot on error */ }
@@ -198,12 +198,16 @@ export function apply(ctx, rawConfig) {
198
198
  return proxyFetch(slot.proxyUrl)
199
199
  }
200
200
 
201
- function redirectFor(ref, provider) {
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 === 'string')
204
- ? ctx.webServer.url
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 (!isProvider(provider)) throw new Error('unknown provider')
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
- let row = state ? pending.get(state) : null
367
- if (!row) {
368
- for (const item of pending.values()) {
369
- if (item.provider === provider && item.index === n) row = item
370
- }
371
- }
372
- if (!row || !row.verifier) throw new Error('login session expired; start Connect again')
373
- if (row.provider !== provider || row.index !== n) throw new Error('state does not match this account')
374
- const cfg = { ...vendorConfig(provider, live()), redirectUri: row.redirectUri }
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(provider) }
388
+ return { ref, label: pmL(blob.label || blob.email) || displayName(actualProvider) }
388
389
  }
389
390
 
390
391
  // Privacy masking
package/lib/jwt.js CHANGED
@@ -1,4 +1,4 @@
1
- export function decodeJwtPayload(token) {
1
+ function decodeJwtPayload(token) {
2
2
  const raw = String(token || '')
3
3
  const parts = raw.split('.')
4
4
  if (parts.length < 2) return null
package/lib/messages.js CHANGED
@@ -102,7 +102,7 @@ export function openaiTools(options) {
102
102
  }))
103
103
  }
104
104
 
105
- export function reconcileResponsesInput(items) {
105
+ function reconcileResponsesInput(items) {
106
106
  const result = []
107
107
  const callsSeen = new Set()
108
108
  const outputsSeen = new Set()
@@ -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 : `${prov.toUpperCase()}_OAUTH_${idx}`
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 || typeof passphrase !== 'string') {
206
- writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase is required' } })
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
@@ -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, live(), origin)
42
+ const redirectUri = redirectFor(provider, `${provider}:${index}`, origin)
43
43
  const pkce = await createPkce()
44
44
  pending.set(pkce.state, {
45
45
  provider,
@@ -1,3 +1,4 @@
1
+ // #374: Sensitive GET routes protected by same-origin check
1
2
  import { getRecentAlerts, clearAlerts } from '../alerts.js'
2
3
  import { normalizeSlots, vendorConfig } from '../accounts.js'
3
4
  import { Config, plainConfig, publicConfig } from '../config-schema.js'
@@ -133,7 +133,7 @@ export async function listModels(blob, cfg) {
133
133
  // Advertise adaptive thinking + effort only for models whose capability table
134
134
  // declares the chosen level; anything else (unsupported family, 'off', no
135
135
  // selection) leaves the request untouched so behavior stays opt-in.
136
- export function applyClaudeThinking(payload, options) {
136
+ function applyClaudeThinking(payload, options) {
137
137
  const effort = options && String(options.reasoningEffort || '')
138
138
  if (!effort || effort === 'off') return payload
139
139
  const levels = effortLevelsForModel(payload && payload.model)
@@ -14,7 +14,7 @@ const USAGE = 'https://chatgpt.com/backend-api/wham/usage'
14
14
  // code_verifier server-side; we finish with the normal PKCE exchange.
15
15
  const DEVICE_USERCODE_URL = 'https://auth.openai.com/api/accounts/deviceauth/usercode'
16
16
  const DEVICE_TOKEN_URL = 'https://auth.openai.com/api/accounts/deviceauth/token'
17
- export const DEVICE_AUTH_URL = 'https://auth.openai.com/codex/device'
17
+ const DEVICE_AUTH_URL = 'https://auth.openai.com/codex/device'
18
18
  const DEVICE_REDIRECT_URI = 'https://auth.openai.com/deviceauth/callback'
19
19
  const SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke'
20
20
  const INSTRUCTIONS = 'You are a coding assistant using a ChatGPT Codex subscription.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.35",
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",
@@ -70,6 +70,7 @@
70
70
  "devDependencies": {
71
71
  "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
72
72
  "@types/node": "^22.20.2",
73
- "typescript": "^7.0.2"
73
+ "typescript": "^7.0.2",
74
+ "@deepseek-ai/schemastery": "^3.18.4"
74
75
  }
75
76
  }