@goodandready/dsh-subscriptions 0.6.66 → 0.6.68

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
@@ -207,6 +207,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
207
207
  * **In-App Toast Alerts (`enableToasts`)**: Lightweight floating toast alerts in the DSH chat interface notifying you when accounts enter quarantine, recover from cooldowns, or exceed quota safety thresholds.
208
208
  * **Automated Webhook Integration**: Instant event notifications dispatched via `notifyQuarantineEntered` and `notifyQuarantineReleased`.
209
209
 
210
+ ### 22. ⚡ xAI Grok RFC 8628 Device Authorization Flow (v0.6.68)
211
+ * **RFC 8628 Device Code Authorization (GH #17, #532)**: Native device authorization grant support for xAI Grok. Solves the issue where xAI presents a Grok Build user-code verification page for CLI client IDs without redirecting back to loopback callbacks. Provides direct user code display and automatic token polling.
212
+
213
+ ### 21. 🛡️ Per-Account Claude Billing Attribution Isolation (v0.6.67)
214
+ * **Per-Account `cc_turn_index` & `cch` Isolation (#530)**: Isolates Claude Code billing attribution state (`cch` session hash and `cc_turn_index` counter) per subscription account (`accountId`) in a bounded module Map instead of `globalThis`, preventing cross-account telemetry mixing in multi-account pools.
215
+ * **Dynamic `cc_prompt_index` & Kill-Switch**: Automatically derives 0-based `cc_prompt_index` from multi-turn `messages` history and supports suppression via `CLAUDE_CODE_ATTRIBUTION_HEADER=0` or `disableBillingHeader: true`.
216
+
210
217
  ### 20. 🤖 Native Copilot CLI SDK Mode & Local Tool Calling (v0.6.66)
211
218
  * **Direct Copilot CLI Integration (#508)**: Native driver `copilot-cli.js` supporting execution via local `copilot` / `github-copilot-cli` binaries with zero network token emulation risk.
212
219
  * **Smart Mode Switching (`copilotMode`)**: Choose between `auto` (prefers local CLI with graceful web API fallback), `cli` (strict CLI SDK execution), and `web` (direct OAuth/API).
package/README.ru.md CHANGED
@@ -183,6 +183,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
183
183
  * **Всплывающие уведомления (Toast Alerts)**: Легковесные всплывающие тосты в веб-интерфейсе DSH при событиях карантина (с отображением причины), восстановлении доступа и критическом расходе квоты. Включение управляется параметром `enableToasts` (по умолчанию `true`).
184
184
  * **Интеграция с вебхуками**: Автоматическая отправка уведомлений через `notifyQuarantineEntered` и `notifyQuarantineReleased`.
185
185
 
186
+ ### 18. ⚡ RFC 8628 Device Code авторизация для xAI Grok (v0.6.68)
187
+ * **Авторизация по коду устройства (GH #17, #532)**: Нативная поддержка RFC 8628 device code flow для xAI Grok. Устраняет проблему, когда xAI для CLI client ID открывает страницу ввода кода для Grok Build без обратного редиректа на loopback-порт. Отображает одноразовый код на карточке и автоматически поллит токены в фоне.
188
+
189
+ ### 17. 🛡️ Поаккаунтная изоляция атрибуции биллинга Claude (v0.6.67)
190
+ * **Изоляция `cc_turn_index` и `cch` по `accountId` (#530)**: Состояние атрибуции Claude Code (`cch` и счётчик `cc_turn_index`) изолировано по каждому аккаунту в ограниченной таблице модуля вместо `globalThis`, что исключает смешивание телеметрии между подписками в пуле.
191
+ * **Динамический `cc_prompt_index` и Kill-Switch**: Индекс `cc_prompt_index` автоматически вычисляется по числу пользовательских ходов в `messages`, а отключение заголовка поддерживается через `CLAUDE_CODE_ATTRIBUTION_HEADER=0` или `disableBillingHeader: true`.
192
+
186
193
  ### 16. 🤖 Нативный режим Copilot CLI SDK и локальный Tool Calling (v0.6.66)
187
194
  * **Прямая интеграция с Copilot CLI (#508)**: Драйвер `copilot-cli.js` для работы через локальный бинарник `copilot` / `github-copilot-cli` без риска сетевых блокировок при эмуляции токенов.
188
195
  * **Гибкие режимы работы (`copilotMode`)**: Поддержка режимов `auto` (приоритет CLI с автоматическим fallback на web-API), `cli` (строгий режим CLI SDK) и `web` (прямое подключение по OAuth/API).
package/README.zh.md CHANGED
@@ -132,6 +132,13 @@ graph LR
132
132
  queueTimeoutMs: 30000 # v0.6.62: 队列最大等待超时时间(毫秒)
133
133
  * **自动化 Webhook 告警集成**:通过 `notifyQuarantineEntered` 与 `notifyQuarantineReleased` 自动向外部通知系统派发隔离生命周期事件。
134
134
 
135
+ ### xAI Grok RFC 8628 设备码授权流支持 (v0.6.68)
136
+ * **RFC 8628 设备码授权流 (GH #17, #532)**:为 xAI Grok 新增原生设备码登录授权支持。彻底解决 xAI 针对 CLI 客户端标识返回 Grok Build 验证码页面且不触发回环重定向回调导致的死锁问题。直接在槽位卡片展示用户授权码,并自动在后台轮询兑换会话令牌。
137
+
138
+ ### Claude 账户级计费归因隔离与动态轮次索引 (v0.6.67)
139
+ * **按账户隔离 `cc_turn_index` 与 `cch` (#530)**:将 Claude Code 计费归因状态(会话哈希 `cch` 与递增轮次 `cc_turn_index`)按 `accountId` 隔离在有界模块级映射表中,彻底消除 `globalThis` 跨订阅账户状态混用问题。
140
+ * **动态计算 `cc_prompt_index` 与禁用开关**:根据多轮对话消息历史自动计算从 0 开始的 `cc_prompt_index`,并支持通过环境变量 `CLAUDE_CODE_ATTRIBUTION_HEADER=0` 或 `disableBillingHeader: true` 禁用归因头注入。
141
+
135
142
  ### Antigravity 客户端密钥架构与无感 CLI 令牌刷新 (v0.6.60)
136
143
  * **配置架构密钥字段 (`antigravityClientSecret`)**:支持用户在设置中自定义配置 Google Cloud OAuth 客户端密钥(Client Secret),公共接口自动脱敏掩码 (`••••••`),彻底解决机密型 OAuth 客户端刷新令牌被拒的问题。
137
144
  * **CLI 令牌零配置平滑刷新**:内置官方 Gemini / Antigravity CLI 客户端的公共应用凭据 (`Google Gemini CLI Client ID` / `Google Gemini CLI Client Secret`),使通过 `📥 从 CLI 导入` 的会话能够持久无限刷新,不再出现 1 小时后失效问题。
package/lib/adapter.js CHANGED
@@ -329,6 +329,7 @@ export class SubscriptionAdapter extends LlmAdapter {
329
329
  if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
330
330
  for await (const chunk of vendor.streamOnce({
331
331
  blob,
332
+ accountId: account.id || account.ref,
332
333
  options: opts,
333
334
  fetchImpl: quotaFetch,
334
335
  headers: attributionHeaders(),
package/lib/client.js CHANGED
@@ -313,16 +313,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
313
313
  grok: {
314
314
  title: "xAI Grok",
315
315
  stepsEn: [
316
- "1. Run grok CLI login on the server.",
317
- "2. Click \"📥 From CLI\" (~/.grok/auth.json).",
318
- "3. Or paste your xAI API Key and click \"Save token\".",
316
+ "1. Click \"Device Login\" (or \"Connect\") on the slot card.",
317
+ "2. A one-time code will appear on the card.",
318
+ "3. Open https://accounts.x.ai/oauth2/device, enter the code, and approve access for your xAI account.",
319
+ "4. The plugin automatically captures tokens and enables Grok models.",
320
+ "5. Or click \"📥 From CLI\" (~/.grok/auth.json) / paste an xAI API Key.",
319
321
  ],
320
322
  stepsZh: [
321
- "1. 在服务器上运行 grok CLI 登录。",
322
- "2. 点击 \"📥 从 CLI 导入\" (~/.grok/auth.json)。",
323
- "3. 或将 xAI API 密钥粘贴到输入框并点击 \"保存令牌\"。",
323
+ "1. 点击卡片上的 \"设备码登录\" (Device Login) 或 \"连接\"。",
324
+ "2. 界面将展示一次性设备授权码。",
325
+ "3. 打开 https://accounts.x.ai/oauth2/device,输入代码,并为您拥有 Grok 订阅的 xAI 账户授权。",
326
+ "4. 授权成功后,插件会自动捕获会话并激活 Grok 官方模型。",
327
+ "5. 或点击 \"📥 从 CLI 导入\" (~/.grok/auth.json) / 直接粘贴 xAI API 密钥。",
324
328
  ],
325
- link: "https://x.ai",
329
+ link: "https://accounts.x.ai/oauth2/device",
326
330
  },
327
331
  kiro: {
328
332
  title: "AWS Kiro",
@@ -1362,7 +1366,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1362
1366
 
1363
1367
  const connect = async (provider, index) => {
1364
1368
  setErr('')
1365
- if (provider === 'copilot') {
1369
+ if (provider === 'copilot' || provider === 'grok') {
1366
1370
  return deviceStartLogin(provider, index).catch((e) => setErr(cleanErrorMessage(e.message || e)))
1367
1371
  }
1368
1372
  if (provider === 'antigravity' && (!draft || !draft.antigravityClientId || !String(draft.antigravityClientId).trim())) {
@@ -2253,7 +2257,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2253
2257
  type: 'button', className: 'dsub-mini',
2254
2258
  onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2255
2259
  }, on ? t('reconnect') : t('connect')),
2256
- ((row.slot.provider === 'codex' || row.slot.provider === 'copilot') ? React.createElement('button', {
2260
+ ((row.slot.provider === 'codex' || row.slot.provider === 'copilot' || row.slot.provider === 'grok') ? React.createElement('button', {
2257
2261
  type: 'button', className: 'dsub-mini',
2258
2262
  onClick: () => deviceStartLogin(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
2259
2263
  }, t('deviceLogin')) : null),
@@ -222,20 +222,65 @@ export async function usage(blob, _cfg, fetchImpl) {
222
222
  }
223
223
  }
224
224
 
225
- export function makeClaudeBillingHeader(cfg) {
225
+ const billingAccountState = new Map()
226
+ const MAX_BILLING_ACCOUNTS = 256
227
+
228
+ function resolveAccountKey(cfg, context) {
229
+ const explicit = (context && context.accountId) || (cfg && cfg.accountId)
230
+ if (typeof explicit === 'string' && explicit.trim()) return explicit.trim()
231
+ const blob = (context && context.blob) || (cfg && cfg.blob)
232
+ if (blob && typeof blob === 'object') {
233
+ const id = blob.accountId || blob.id || blob.email
234
+ if (typeof id === 'string' && id.trim()) return id.trim()
235
+ if (typeof blob.accessToken === 'string' && blob.accessToken.length >= 8) {
236
+ return `token:${blob.accessToken.slice(-12)}`
237
+ }
238
+ }
239
+ return 'default'
240
+ }
241
+
242
+ function resolvePromptIndex(cfg, context) {
243
+ const explicit = (context && context.promptIndex !== undefined)
244
+ ? context.promptIndex
245
+ : (cfg && cfg.promptIndex !== undefined ? cfg.promptIndex : undefined)
246
+ if (typeof explicit === 'number' && Number.isFinite(explicit) && explicit >= 0) {
247
+ return Math.floor(explicit)
248
+ }
249
+ const messages = (context && context.options && Array.isArray(context.options.messages))
250
+ ? context.options.messages
251
+ : (context && Array.isArray(context.messages) ? context.messages : null)
252
+ if (messages) {
253
+ const userTurns = messages.filter((m) => m && m.role === 'user').length
254
+ return Math.max(0, userTurns - 1)
255
+ }
256
+ return 0
257
+ }
258
+
259
+ export function makeClaudeBillingHeader(cfg, context) {
226
260
  const ccVersion = (cfg && cfg.claudeCcVersion) || '2.1.289.7e2'
227
- const cch = (globalThis.__DSH_CC_H__ || (() => {
228
- globalThis.__DSH_CC_H__ = Math.random().toString(16).slice(2, 6)
229
- return globalThis.__DSH_CC_H__
230
- })())
231
- const turnIndex = (globalThis.__DSH_CC_TURN__ = (globalThis.__DSH_CC_TURN__ || 0) + 1)
232
- return `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=sdk-cli; cch=${cch}; cc_turn_origin=sdk; cc_prompt_index=0; cc_turn_index=${turnIndex};`
261
+ const accountKey = resolveAccountKey(cfg, context)
262
+ let state = billingAccountState.get(accountKey)
263
+ if (!state) {
264
+ if (billingAccountState.size >= MAX_BILLING_ACCOUNTS) {
265
+ const oldestKey = billingAccountState.keys().next().value
266
+ if (oldestKey !== undefined) billingAccountState.delete(oldestKey)
267
+ }
268
+ state = {
269
+ cch: Math.random().toString(16).slice(2, 6).padEnd(4, '0'),
270
+ turnIndex: 0,
271
+ }
272
+ billingAccountState.set(accountKey, state)
273
+ }
274
+ state.turnIndex += 1
275
+ const promptIndex = resolvePromptIndex(cfg, context)
276
+ return `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=sdk-cli; cch=${state.cch}; cc_turn_origin=sdk; cc_prompt_index=${promptIndex}; cc_turn_index=${state.turnIndex};`
233
277
  }
234
278
 
235
- export function applyClaudeBillingHeader(payload, cfg) {
279
+ export function applyClaudeBillingHeader(payload, cfg, context) {
236
280
  if (process.env.CLAUDE_CODE_ATTRIBUTION_HEADER === '0') return payload
281
+ if (cfg && cfg.disableBillingHeader === true) return payload
237
282
  if (!payload) return payload
238
- const headerText = makeClaudeBillingHeader(cfg)
283
+ const headerText = makeClaudeBillingHeader(cfg, context)
239
284
  const headerBlock = { type: 'text', text: headerText }
240
285
  if (Array.isArray(payload.system)) {
241
286
  if (!payload.system.some((b) => typeof b === 'object' && b && typeof b.text === 'string' && b.text.startsWith('x-anthropic-billing-header:'))) {
@@ -249,10 +294,10 @@ export function applyClaudeBillingHeader(payload, cfg) {
249
294
  return payload
250
295
  }
251
296
 
252
- export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
297
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, accountId }) {
253
298
  const payload = anthropicPayload(options, config.systemPrefix || IDENTITY)
254
299
  applyClaudeThinking(payload, options)
255
- applyClaudeBillingHeader(payload, config)
300
+ applyClaudeBillingHeader(payload, config, { accountId, blob, options })
256
301
  const res = await fetchImpl(API, {
257
302
  method: 'POST',
258
303
  headers: {
@@ -184,7 +184,7 @@ export function isCopilotCliAvailable(options = {}) {
184
184
  * Translate DSH turn options to Copilot CLI arguments and environment.
185
185
  * @param {any} options
186
186
  * @param {any} [blob]
187
- * @param {string} [binaryPath]
187
+ * @param {string} [_binaryPath]
188
188
  * @returns {{ args: string[], env: NodeJS.ProcessEnv, inputPayload: string }}
189
189
  */
190
190
  export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'copilot') {
@@ -239,7 +239,7 @@ export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'c
239
239
  * @param {Function} [params.spawnImpl] Test seam for child_process.spawn
240
240
  * @returns {AsyncGenerator<any, void, unknown>}
241
241
  */
242
- export async function* streamCopilotCli({ binaryPath, options, blob, _config, signal, spawnImpl }) {
242
+ export async function* streamCopilotCli({ binaryPath, options, blob, config: _config, signal, spawnImpl }) {
243
243
  if (signal && signal.aborted) {
244
244
  throw new LlmError('Request aborted by client', 'ABORTED')
245
245
  }
@@ -230,6 +230,84 @@ export async function usage(blob, cfg, fetchImpl) {
230
230
  }
231
231
  }
232
232
 
233
+ const DEVICE_CODE_URL = 'https://auth.x.ai/oauth2/device/code'
234
+ const DEVICE_AUTH_URL = 'https://accounts.x.ai/oauth2/device'
235
+
236
+ export async function deviceStart(cfg, fetchImpl) {
237
+ const impl = fetchImpl || fetch
238
+ const clientId = (cfg && cfg.clientId) || defaults().clientId
239
+ const params = new URLSearchParams({
240
+ client_id: clientId,
241
+ scope: SCOPE,
242
+ })
243
+ const res = await impl(DEVICE_CODE_URL, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Content-Type': 'application/x-www-form-urlencoded',
247
+ Accept: 'application/json',
248
+ },
249
+ body: params.toString(),
250
+ })
251
+ if (!res.ok) throw httpError(res.status, await res.text().catch(() => ''), 'DEVICE')
252
+ const json = await readJson(res)
253
+ if (!json || typeof json.user_code !== 'string' || typeof json.device_code !== 'string') {
254
+ throw httpError(502, 'xAI device authorization response is incomplete', 'DEVICE')
255
+ }
256
+ return {
257
+ userCode: json.user_code,
258
+ deviceAuthId: json.device_code,
259
+ intervalMs: Math.max(parseInt(json.interval, 10) || 5, 1) * 1000,
260
+ authUrl: json.verification_uri_complete || json.verification_uri || DEVICE_AUTH_URL,
261
+ }
262
+ }
263
+
264
+ export async function devicePoll(cfg, session, fetchImpl) {
265
+ const impl = fetchImpl || fetch
266
+ const clientId = (cfg && cfg.clientId) || defaults().clientId
267
+ const params = new URLSearchParams({
268
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
269
+ client_id: clientId,
270
+ device_code: session.deviceAuthId,
271
+ })
272
+ const res = await impl(TOKEN, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/x-www-form-urlencoded',
276
+ Accept: 'application/json',
277
+ },
278
+ body: params.toString(),
279
+ })
280
+ if (res.status === 400 || res.status === 403) {
281
+ const rawText = await res.text().catch(() => '')
282
+ let errJson = {}
283
+ try {
284
+ errJson = JSON.parse(rawText)
285
+ } catch (err) {
286
+ errJson = { error: String(err && err.message) }
287
+ }
288
+ if (errJson && (errJson.error === 'authorization_pending' || errJson.error === 'slow_down')) {
289
+ return { status: 'pending' }
290
+ }
291
+ if (errJson && errJson.error === 'expired_token') {
292
+ return { status: 'expired' }
293
+ }
294
+ if (errJson && errJson.error) {
295
+ throw httpError(400, errJson.error_description || errJson.error, 'DEVICE')
296
+ }
297
+ throw httpError(res.status, rawText, 'DEVICE')
298
+ }
299
+ if (!res.ok) throw httpError(res.status, await res.text().catch(() => ''), 'DEVICE')
300
+ const json = await readJson(res)
301
+ if (!json || typeof json.access_token !== 'string') {
302
+ throw httpError(502, 'xAI device token response is incomplete', 'DEVICE')
303
+ }
304
+ const blob = tokenBlobFromOAuth(json)
305
+ return {
306
+ status: 'authorized',
307
+ blob: { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Grok' },
308
+ }
309
+ }
310
+
233
311
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
234
312
  const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
235
313
  let body = codexResponsesBody(options, '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.66",
3
+ "version": "0.6.68",
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",