@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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/blob.js +4 -0
- package/lib/config-schema.js +4 -0
- package/lib/headless-auth.js +14 -1
- package/lib/http.js +41 -0
- package/lib/import-auth.js +3 -6
- package/lib/index.js +1 -0
- package/lib/routes/accounts.js +11 -7
- package/lib/routes/oauth.js +8 -1
- package/lib/subscriptions.js +24 -1
- package/lib/vault-sync.js +5 -3
- package/lib/vendors/antigravity.js +2 -2
- package/lib/vendors/copilot-cli.js +28 -4
- package/lib/vendors/jetbrains.js +5 -2
- package/lib/vendors/perplexity.js +5 -2
- package/package.json +1 -1
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
package/lib/config-schema.js
CHANGED
package/lib/headless-auth.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/import-auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/lib/routes/accounts.js
CHANGED
|
@@ -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
|
|
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()
|
package/lib/routes/oauth.js
CHANGED
|
@@ -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) =>
|
|
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
|
}
|
package/lib/subscriptions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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) => {
|
|
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
|
package/lib/vendors/jetbrains.js
CHANGED
|
@@ -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
|
|
91
|
-
state: p.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
|
|
85
|
-
state: p.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.
|
|
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",
|