@goodandready/dsh-subscriptions 0.6.82 → 0.6.83

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/README.md CHANGED
@@ -309,6 +309,7 @@ dsh-subscriptions:
309
309
 
310
310
  - `customVendors.authUrl`, `tokenUrl` and `baseUrl` must use `https://`. Plain `http://` is accepted only for loopback hosts (`localhost`, `127.0.0.1`, `::1`).
311
311
  - Grok `baseUrl` overrides must point to `api.x.ai`, `grok.com` / `*.grok.com`, or a loopback host; other hosts are rejected with `GROK_UNTRUSTED_HOST` so bearer tokens never leave trusted xAI infrastructure.
312
+ - Antigravity CLI imports must configure `antigravityClientSecret`; the embedded default was removed.
312
313
  - The error buffer and diagnostics reports redact bearer tokens, API keys, JWTs and `key=value` credentials before storing or returning them.
313
314
 
314
315
 
package/README.ru.md CHANGED
@@ -282,6 +282,7 @@ dsh-subscriptions:
282
282
 
283
283
  - `customVendors.authUrl`, `tokenUrl` и `baseUrl` обязаны использовать `https://`. Открытый `http://` допускается только для loopback-хостов (`localhost`, `127.0.0.1`, `::1`).
284
284
  - Переопределение Grok `baseUrl` должно указывать на `api.x.ai`, `grok.com` / `*.grok.com` или loopback-хост; остальные хосты отклоняются с `GROK_UNTRUSTED_HOST`, чтобы bearer-токен не покидал доверенную инфраструктуру xAI.
285
+ - Импорт Antigravity CLI требует явного `antigravityClientSecret`; встроенный default удалён.
285
286
  - Error buffer и диагностические отчёты редактируют bearer-токены, API-ключи, JWT и `key=value`-credentials до сохранения и выдачи.
286
287
 
287
288
 
package/README.zh.md CHANGED
@@ -188,6 +188,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
188
188
 
189
189
  - `customVendors.authUrl`、`tokenUrl` 和 `baseUrl` 必须使用 `https://`。明文 `http://` 仅允许用于回环主机(`localhost`、`127.0.0.1`、`::1`)。
190
190
  - Grok `baseUrl` 覆盖只能指向 `api.x.ai`、`grok.com` / `*.grok.com` 或回环主机;其他主机会以 `GROK_UNTRUSTED_HOST` 拒绝,避免 bearer 令牌离开可信的 xAI 基础设施。
191
+ - Antigravity CLI 导入必须配置 `antigravityClientSecret`;内置默认值已移除。
191
192
  - 错误缓冲区和诊断报告会在存储或返回前脱敏 bearer 令牌、API 密钥、JWT 和 `key=value` 凭据。
192
193
 
193
194
 
package/lib/blob.js CHANGED
@@ -90,3 +90,7 @@ export function parseBlob(text) {
90
90
  ...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
91
91
  }
92
92
  }
93
+
94
+ export function validateCredentialBlob(blob) {
95
+ return parseBlob(serializeBlob(blob))
96
+ }
@@ -285,3 +285,7 @@ export function restoreMaskedSecrets(incoming, existing) {
285
285
 
286
286
  return out
287
287
  }
288
+
289
+ export function revisionOf(settingsApi) {
290
+ return settingsApi && typeof settingsApi.revision === 'function' ? settingsApi.revision() : undefined
291
+ }
@@ -326,6 +326,15 @@ export function getHeadlessAuthInfo({ provider = '', redirectUri = '', authUrl =
326
326
  }
327
327
  }
328
328
 
329
+ export function safePostMessageOrigin(originHeader) {
330
+ try {
331
+ const u = new URL(String(originHeader || ''))
332
+ if (u.protocol === 'http:' || u.protocol === 'https:') return u.origin
333
+ if (u.protocol === 'dsh-app:') return 'dsh-app://' + u.host
334
+ } catch { /* invalid origin */ }
335
+ return ''
336
+ }
337
+
329
338
  export async function handleHeadlessRedirect(req, res, { pending, completeOAuth, accountsView }) {
330
339
  const rawUrl = new URL(req.url || '/', 'http://127.0.0.1')
331
340
  const code = rawUrl.searchParams.get('code') || ''
@@ -353,11 +362,15 @@ export async function handleHeadlessRedirect(req, res, { pending, completeOAuth,
353
362
  try {
354
363
  await completeOAuth({ provider: row.provider, index: row.index, code, state })
355
364
  if (typeof accountsView === 'function') await accountsView()
365
+ const targetOrigin = safePostMessageOrigin(req && req.headers && req.headers.origin)
366
+ const relay = targetOrigin
367
+ ? `<script>if (window.opener) { window.opener.postMessage({ type: 'dsh-oauth-success', state: ${JSON.stringify(String(state))} }, ${JSON.stringify(targetOrigin)}); }</script>`
368
+ : ''
356
369
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
357
370
  res.end(`<!doctype html><meta charset="utf-8"><title>Subscriptions</title><body style="font-family:sans-serif;padding:2rem;">` +
358
371
  `<h2 style="color:#2e7d32;">Signed In Successfully</h2>` +
359
372
  `<p>Your account has been authorized. You can close this window and return to Settings.</p>` +
360
- `<script>if (window.opener) { window.opener.postMessage({ type: 'dsh-oauth-success', state: '${escapeHtml(state)}' }, '*'); }</script>` +
373
+ relay +
361
374
  `</body>`)
362
375
  return
363
376
  } catch (e) {
package/lib/http.js CHANGED
@@ -79,7 +79,34 @@ export function isLoopback(value) {
79
79
 
80
80
 
81
81
 
82
+ function hostNameOf(hostHeader) {
83
+ const raw = String(Array.isArray(hostHeader) ? (hostHeader[0] || '') : (hostHeader || '')).trim().toLowerCase()
84
+ if (!raw) return ''
85
+ if (raw === '::1') return '::1'
86
+ if (raw.startsWith('[')) {
87
+ const end = raw.indexOf(']')
88
+ return end > 0 ? raw.slice(0, end + 1) : raw
89
+ }
90
+ return raw.split(':')[0]
91
+ }
92
+
93
+ function configuredTrustedHosts(targetCtx) {
94
+ const out = new Set()
95
+ for (const part of String(process.env.DSH_TRUSTED_HOSTS || '').split(',')) {
96
+ const host = hostNameOf(part)
97
+ if (host) out.add(host)
98
+ }
99
+ try {
100
+ const url = targetCtx && targetCtx.webServer && targetCtx.webServer.url
101
+ if (url) out.add(hostNameOf(new URL(url).host))
102
+ } catch { /* ignore malformed web server url */ }
103
+ return out
104
+ }
105
+
82
106
  /**
107
+ * @param {import('node:http').IncomingMessage} request
108
+ * @param {any} [targetCtx]
109
+ * @returns {boolean}/**
83
110
  * @param {import('node:http').IncomingMessage} request
84
111
  * @param {any} [targetCtx]
85
112
  * @returns {boolean}
@@ -148,6 +175,20 @@ export function isTrustedSettingsRequest(request, targetCtx) {
148
175
  }
149
176
  }
150
177
 
178
+ // 4a. Loopback peers must present a loopback or configured Host.
179
+ // A loopback socket with a foreign Host is a DNS-rebinding attempt.
180
+ const peerAddress = request?.socket?.remoteAddress || request?.connection?.remoteAddress
181
+ if (peerAddress && isLoopback(peerAddress)) {
182
+ const host = hostNameOf(headers.host)
183
+ const loopbackHost = host === ''
184
+ || host === 'localhost'
185
+ || host === 'localhost.'
186
+ || host === '127.0.0.1'
187
+ || host === '::1'
188
+ || host === '[::1]'
189
+ if (!loopbackHost && !configuredTrustedHosts(targetCtx).has(host)) return false
190
+ }
191
+
151
192
  // 4. Delegate to DSH connection service if available (#374)
152
193
  let conn = null
153
194
  try {
@@ -6,7 +6,7 @@ import { readClaudeCredentials } from './vendors/claude-cli.js'
6
6
  import { readCopilotCredentials } from './vendors/copilot-cli.js'
7
7
  import { normalizeExpiresAt } from './blob.js'
8
8
  import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
9
- import { DEFAULT_CLI_CLIENT_ID, DEFAULT_CLI_CLIENT_SECRET } from './vendors/antigravity.js'
9
+
10
10
 
11
11
  // Test seam: a fixture root overrides the real home for the duration of a
12
12
  // public call. Undefined (the default) keeps production behaviour.
@@ -146,7 +146,8 @@ async function discoverLocalCliSessionsInner() {
146
146
  }
147
147
  }
148
148
 
149
- if (!detected.antigravity && (process.platform === 'win32' || activeWindowsCredentialReader)) {
149
+ const canReadWindowsCredentialManager = Boolean(activeWindowsCredentialReader) || (!activeHome && process.platform === 'win32')
150
+ if (!detected.antigravity && canReadWindowsCredentialManager) {
150
151
  const winCred = readWindowsAntigravityCredential()
151
152
  const tok = winCred && winCred.token ? winCred.token : winCred
152
153
  if (tok && (tok.access_token || tok.accessToken)) {
@@ -453,10 +454,6 @@ export async function loadLocalCliBlob(provider, options = {}) {
453
454
  raw.installed?.client_secret || raw.web?.client_secret || ''
454
455
  let clientSecret = typeof directSecret === 'string' ? directSecret.trim() : ''
455
456
 
456
- if (!clientSecret && clientId === DEFAULT_CLI_CLIENT_ID) {
457
- clientSecret = DEFAULT_CLI_CLIENT_SECRET
458
- }
459
-
460
457
  const rawExpiry = tok.expiry ?? tok.expires_at ?? tok.expiresAt ?? tok.expiry_date ?? raw.expiry_date ?? raw.expiry ?? (tok.expires_in ? Date.now() + Number(tok.expires_in) * 1000 : 0)
461
458
  const expiresAt = normalizeExpiresAt(rawExpiry)
462
459
 
package/lib/index.js CHANGED
@@ -135,6 +135,7 @@ export function apply(ctx, rawConfig) {
135
135
 
136
136
  const settingsApi = {
137
137
  get: () => live(),
138
+ revision: () => describeRow()?.revision,
138
139
  replace: async (next, expectedRevision) => {
139
140
  const parsed = plainConfig(next)
140
141
  const prevConfig = cachedConfig
@@ -4,10 +4,11 @@ import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from '../vault-sync.
4
4
  import { normalizeSlots, vendorConfig } from '../accounts.js'
5
5
  import { getVendor } from '../vendors/index.js'
6
6
  import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
7
- import { Config, plainConfig, publicConfig } from '../config-schema.js'
7
+ import { Config, plainConfig, publicConfig, revisionOf } from '../config-schema.js'
8
8
  import { decryptWithPassphrase, encryptWithPassphrase } from '../crypto.js'
9
9
  import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
10
10
  import { discoverLocalCliSessions, loadLocalCliBlob } from '../import-auth.js'
11
+ import { validateCredentialBlob } from '../blob.js'
11
12
  import { isProvider, oauthRef, parseOauthRef } from '../refs.js'
12
13
 
13
14
  export function registerAccountsRoutes(ctx, state) {
@@ -276,18 +277,20 @@ export function registerAccountsRoutes(ctx, state) {
276
277
  const curSlots = Array.isArray(live().slots) ? live().slots.slice() : []
277
278
  const exists = curSlots.some((s) => s && s.provider === prov && Number(s.index) === idx)
278
279
  if (!exists && getSettingsApi()) {
280
+ const settings = getSettingsApi()
281
+ const revision = revisionOf(settings)
279
282
  const nextSlots = curSlots.concat([{
280
283
  provider: prov,
281
284
  index: idx,
282
285
  label: blob.email || '',
283
286
  }])
284
287
  const parsed = plainConfig(Config({ ...live(), slots: stripLegacySlots(nextSlots) }))
285
- await getSettingsApi().replace(parsed)
288
+ await settings.replace(parsed, revision)
286
289
  syncCustomVendors()
287
290
  }
288
291
  const slot = normalizeSlots(live().slots).find((s) => s.provider === prov && s.index === idx)
289
292
  const ref = slot ? slot.ref : oauthRef(prov, idx)
290
- await store.saveBlob(ref, blob)
293
+ await store.saveBlob(ref, validateCredentialBlob(blob))
291
294
  await syncAdapter()
292
295
  refreshModels().catch(() => {})
293
296
  writeJson(res, 200, {
@@ -389,7 +392,7 @@ export function registerAccountsRoutes(ctx, state) {
389
392
  for (const row of bundle.accounts) {
390
393
  try {
391
394
  if (!row || typeof row.ref !== 'string' || !parseOauthRef(row.ref) || !row.blob) continue
392
- await store.saveBlob(row.ref, row.blob)
395
+ await store.saveBlob(row.ref, validateCredentialBlob(row.blob))
393
396
  imported++
394
397
  } catch { /* skip broken */ }
395
398
  }
@@ -437,7 +440,7 @@ export function registerAccountsRoutes(ctx, state) {
437
440
  const ref = oauthRef(provider, index)
438
441
  const blob = { accessToken: apiKey || refreshToken, refreshToken: refreshToken || apiKey }
439
442
  if (apiKey) { blob.apiKey = apiKey; blob.apiKeyOnly = true }
440
- await store.saveBlob(ref, blob)
443
+ await store.saveBlob(ref, validateCredentialBlob(blob))
441
444
  await syncAdapter()
442
445
  refreshModels().catch(() => {})
443
446
  writeJson(res, 200, { ok: true, ref, accounts: await accountsView() })
@@ -674,6 +677,7 @@ export function registerAccountsRoutes(ctx, state) {
674
677
  try {
675
678
  const result = importVault(vault, passphrase)
676
679
  const settings = getSettingsApi()
680
+ const revision = revisionOf(settings)
677
681
  let parsed
678
682
  if (result.slots.length) {
679
683
  if (!settings) throw new Error('settings are unavailable for vault import')
@@ -690,11 +694,11 @@ export function registerAccountsRoutes(ctx, state) {
690
694
  }
691
695
  let blobCount = 0
692
696
  for (const [ref, blob] of Object.entries(result.blobs)) {
693
- await store.saveBlob(ref, blob)
697
+ await store.saveBlob(ref, validateCredentialBlob(blob))
694
698
  blobCount++
695
699
  }
696
700
  if (parsed) {
697
- await settings.replace(parsed)
701
+ await settings.replace(parsed, revision)
698
702
  syncCustomVendors()
699
703
  }
700
704
  await syncAdapter()
@@ -301,6 +301,13 @@ export function registerOauthRoutes(ctx, state) {
301
301
  ctx.effect(() => ctx.webServer.register({
302
302
  kind: 'exact',
303
303
  path: '/dsh-subscriptions/oauth/headless-redirect',
304
- handler: (req, res) => handleHeadlessRedirect(req, res, { pending, completeOAuth, accountsView }),
304
+ handler: (req, res) => {
305
+ if (!isTrustedSettingsRequest(req, ctx)) {
306
+ res.writeHead(403, { 'Content-Type': 'text/html; charset=utf-8' })
307
+ res.end('<!doctype html><meta charset="utf-8"><p>Forbidden: same-origin or loopback requests only.</p>')
308
+ return
309
+ }
310
+ return handleHeadlessRedirect(req, res, { pending, completeOAuth, accountsView })
311
+ },
305
312
  }), 'dsh-subscriptions: /oauth/headless-redirect')
306
313
  }
@@ -5,16 +5,39 @@ import { pickFetch } from "./proxy.js"
5
5
  import { fetchWithTimeout } from "./http.js"
6
6
 
7
7
  // ponytail: allowlist per provider — add path to extend without touching request logic
8
+ const OPENAI_COMPAT = [
9
+ "/models",
10
+ "/v1/models",
11
+ "/chat/completions",
12
+ "/v1/chat/completions",
13
+ "/responses",
14
+ "/v1/responses",
15
+ "/images/generations",
16
+ "/v1/images/generations",
17
+ ]
18
+
8
19
  export const ALLOWLIST = {
9
20
  codex: ["/responses", "/models", "/images/generations", "/backend-api/codex/responses", "/backend-api/codex/models", "/backend-api/codex/images/generations"],
10
21
  claude: ["/v1/messages", "/api/oauth/profile", "/api/oauth/usage", "/v1/models"],
11
22
  grok: ["/v1/models", "/responses", "/v1/responses", "/v1/billing", "/v1/chat/completions", "/images/generations", "/v1/images/generations"],
12
23
  antigravity: ["/v1/models", "/v1/generateContent", "/v1/streamGenerateContent", "/v1/loadCodeAssist", "/v1/fetchAvailableModels", "/v1/countTokens", "/v1/internal:loadCodeAssist"],
24
+ kimi: OPENAI_COMPAT,
25
+ glm: [...OPENAI_COMPAT, "/api/paas/v4/chat/completions", "/api/paas/v4/models"],
26
+ cursor: OPENAI_COMPAT,
27
+ kiro: OPENAI_COMPAT,
28
+ copilot: OPENAI_COMPAT,
29
+ qwen: [...OPENAI_COMPAT, "/compatible-mode/v1/chat/completions", "/compatible-mode/v1/models"],
30
+ ernie: [...OPENAI_COMPAT, "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat", "/oauth/2.0/token"],
31
+ spark: OPENAI_COMPAT,
32
+ jetbrains: OPENAI_COMPAT,
33
+ perplexity: [...OPENAI_COMPAT, "/api/v1/user/profile", "/api/v1/user/usage"],
34
+ replit: [...OPENAI_COMPAT, "/api/v1/ai"],
35
+ cody: OPENAI_COMPAT,
13
36
  }
14
37
 
15
38
  export function isAllowed(provider, path) {
16
39
  if (!isProvider(provider)) return false
17
- const list = ALLOWLIST[provider] || []
40
+ const list = ALLOWLIST[provider] || OPENAI_COMPAT
18
41
  let p = String(path || "")
19
42
  // strip query and hash
20
43
  p = p.split("?")[0].split("#")[0]
package/lib/vault-sync.js CHANGED
@@ -4,7 +4,8 @@
4
4
  import { createHmac, createHash } from 'node:crypto'
5
5
  import { exportVault, importVault } from './vault.js'
6
6
  import { normalizeSlots } from './accounts.js'
7
- import { Config, plainConfig } from './config-schema.js'
7
+ import { Config, plainConfig, revisionOf } from './config-schema.js'
8
+ import { validateCredentialBlob } from './blob.js'
8
9
 
9
10
  let syncStatus = {
10
11
  enabled: false,
@@ -368,6 +369,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
368
369
 
369
370
  // Merge slots into live configuration
370
371
  if (result.slots.length && settingsApi) {
372
+ const revision = revisionOf(settingsApi)
371
373
  const curSlots = Array.isArray(cfg.slots) ? cfg.slots.slice() : []
372
374
  for (const s of result.slots) {
373
375
  const idx = curSlots.findIndex((c) => c.provider === s.provider && Number(c.index) === Number(s.index))
@@ -378,13 +380,13 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
378
380
  }
379
381
  }
380
382
  const parsed = plainConfig(Config({ ...cfg, slots: curSlots }))
381
- await settingsApi.replace(parsed)
383
+ await settingsApi.replace(parsed, revision)
382
384
  }
383
385
 
384
386
  // Save blobs into store
385
387
  let blobCount = 0
386
388
  for (const [ref, blob] of Object.entries(result.blobs)) {
387
- await store.saveBlob(ref, blob)
389
+ await store.saveBlob(ref, validateCredentialBlob(blob))
388
390
  blobCount++
389
391
  }
390
392
 
@@ -18,7 +18,7 @@ import {
18
18
 
19
19
  export const id = 'antigravity'
20
20
  export const DEFAULT_CLI_CLIENT_ID = ['681255809395', 'oo8ft2oprdrnp9e3aqf6av3hmdib135j'].join('-') + '.' + 'apps.googleusercontent.com'
21
- export const DEFAULT_CLI_CLIENT_SECRET = ['GOCSPX', '4uHgMPm', '1o7Sk', 'geV6Cu5clXFsxl'].join('-')
21
+
22
22
 
23
23
  const AUTH = 'https://accounts.google.com/o/oauth2/v2/auth'
24
24
  const TOKEN = 'https://oauth2.googleapis.com/token'
@@ -143,7 +143,7 @@ export async function refresh(cfg, blob, fetchImpl) {
143
143
  let clientSecret = (cfg?.clientSecret && String(cfg.clientSecret).trim()) || (blob?.clientSecret && String(blob.clientSecret).trim()) || ''
144
144
 
145
145
  if (!clientSecret && clientId === DEFAULT_CLI_CLIENT_ID) {
146
- clientSecret = DEFAULT_CLI_CLIENT_SECRET
146
+ throw new Error('Google OAuth refresh requires a client secret for the default Gemini CLI client. Configure antigravityClientSecret in plugin settings.')
147
147
  }
148
148
 
149
149
  if (!clientId) {
@@ -8,6 +8,13 @@ import { join, delimiter } from 'node:path'
8
8
  import { spawn } from 'node:child_process'
9
9
  import { LlmError } from '@deepseek-ai/dsh-llm'
10
10
 
11
+ const MAX_STDERR_CHARS = 2048
12
+ const GITHUB_TOKEN_PATTERN = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g
13
+
14
+ export function redactCopilotStderr(text) {
15
+ return String(text || '').replace(GITHUB_TOKEN_PATTERN, '[REDACTED]')
16
+ }
17
+
11
18
  /**
12
19
  * Standard candidate binary names for GitHub Copilot CLI.
13
20
  */
@@ -144,8 +151,11 @@ export function findCopilotCli(customPath, env = process.env, platform = process
144
151
  )
145
152
  }
146
153
 
154
+ const candidates = platform === 'win32'
155
+ ? COPILOT_CLI_BINARIES.filter((bin) => /\.(cmd|exe|bat)$/i.test(bin))
156
+ : COPILOT_CLI_BINARIES.filter((bin) => !/\.(cmd|exe|bat)$/i.test(bin))
147
157
  for (const dir of pathDirs) {
148
- for (const bin of COPILOT_CLI_BINARIES) {
158
+ for (const bin of candidates) {
149
159
  const fullPath = join(dir, bin)
150
160
  try {
151
161
  if (existsSync(fullPath)) {
@@ -232,6 +242,16 @@ export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'c
232
242
  return { args, env, inputPayload }
233
243
  }
234
244
 
245
+ export function resolveSpawnCommand(binaryPath, args, platform = process.platform) {
246
+ if (platform === 'win32' && /\.(cmd|bat)$/i.test(String(binaryPath))) {
247
+ return {
248
+ command: process.env.ComSpec || 'cmd.exe',
249
+ args: ['/d', '/s', '/c', binaryPath, ...args],
250
+ }
251
+ }
252
+ return { command: binaryPath, args }
253
+ }
254
+
235
255
  /**
236
256
  * Stream execution from local Copilot CLI process.
237
257
  * Yields canonical DSH events: block-start, text-delta, tool-call-delta, finish, usage.
@@ -251,10 +271,11 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
251
271
 
252
272
  const { args, env, inputPayload } = prepareCopilotCliInvocation(options, blob, binaryPath)
253
273
  const spawner = spawnImpl || spawn
274
+ const invocation = resolveSpawnCommand(binaryPath, args)
254
275
 
255
276
  let child
256
277
  try {
257
- child = spawner(binaryPath, args, {
278
+ child = spawner(invocation.command, invocation.args, {
258
279
  env,
259
280
  stdio: ['pipe', 'pipe', 'pipe'],
260
281
  })
@@ -285,7 +306,10 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
285
306
  let stderrBuffer = ''
286
307
 
287
308
  if (child.stderr) {
288
- child.stderr.on('data', (d) => { stderrBuffer += d.toString() })
309
+ child.stderr.on('data', (d) => {
310
+ if (stderrBuffer.length >= MAX_STDERR_CHARS) return
311
+ stderrBuffer += String(d).slice(0, MAX_STDERR_CHARS - stderrBuffer.length)
312
+ })
289
313
  }
290
314
 
291
315
  const chunkQueue = []
@@ -326,7 +350,7 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
326
350
  stdoutBuffer = ''
327
351
  }
328
352
  if (code !== 0 && !chunkQueue.length) {
329
- processError = new LlmError(`Copilot CLI exited with code ${code}: ${stderrBuffer.trim() || 'unknown error'}`, 'VENDOR')
353
+ processError = new LlmError(`Copilot CLI exited with code ${code}: ${redactCopilotStderr(stderrBuffer.trim()) || 'unknown error'}`, 'VENDOR')
330
354
  }
331
355
  if (resolveNext) {
332
356
  const r = resolveNext
@@ -83,12 +83,15 @@ export function defaults() {
83
83
  export function authorizeUrl(cfg = {}, pkce = {}) {
84
84
  const c = cfg || {}
85
85
  const p = pkce || {}
86
+ if (!p.challenge || !p.state) {
87
+ throw new Error('JetBrains OAuth requires PKCE challenge and state')
88
+ }
86
89
  return buildAuthorizeUrl({
87
90
  authUrl: AUTH,
88
91
  clientId: c.clientId || defaults().clientId,
89
92
  redirectUri: c.redirectUri || defaults().redirectUri,
90
- challenge: p.challenge || 'challenge',
91
- state: p.state || 'state',
93
+ challenge: p.challenge,
94
+ state: p.state,
92
95
  scope: SCOPE,
93
96
  })
94
97
  }
@@ -77,12 +77,15 @@ export function defaults() {
77
77
  export function authorizeUrl(cfg = {}, pkce = {}) {
78
78
  const c = cfg || {}
79
79
  const p = pkce || {}
80
+ if (!p.challenge || !p.state) {
81
+ throw new Error('Perplexity OAuth requires PKCE challenge and state')
82
+ }
80
83
  return buildAuthorizeUrl({
81
84
  authUrl: AUTH,
82
85
  clientId: c.clientId || defaults().clientId,
83
86
  redirectUri: c.redirectUri || defaults().redirectUri,
84
- challenge: p.challenge || 'challenge',
85
- state: p.state || 'state',
87
+ challenge: p.challenge,
88
+ state: p.state,
86
89
  scope: SCOPE,
87
90
  })
88
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.82",
3
+ "version": "0.6.83",
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",