@goodandready/dsh-subscriptions 0.6.29 → 0.6.30

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/client.js CHANGED
@@ -299,12 +299,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
299
299
  grok: {
300
300
  title: "xAI Grok",
301
301
  stepsEn: [
302
- "1. Run grok-cli or hermes login on server.",
302
+ "1. Run grok CLI login on the server.",
303
303
  "2. Click \"📥 From CLI\" (~/.grok/auth.json).",
304
304
  "3. Or paste your xAI API Key and click \"Save token\".",
305
305
  ],
306
306
  stepsZh: [
307
- "1. 在服务器上运行 grok-cli 或 hermes 登录。",
307
+ "1. 在服务器上运行 grok CLI 登录。",
308
308
  "2. 点击 \"📥 从 CLI 导入\" (~/.grok/auth.json)。",
309
309
  "3. 或将 xAI API 密钥粘贴到输入框并点击 \"保存令牌\"。",
310
310
  ],
@@ -529,8 +529,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
529
529
  "backupImportTitle": "Import Encrypted Backup",
530
530
  "backupExportAction": "Export",
531
531
  "backupImportAction": "Import",
532
- "backupPassphrase": "Vault Passphrase (min 4 chars)",
533
- "backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM and PBKDF2.",
532
+ "backupPassphrase": "Vault Passphrase (min 12 chars)",
533
+ "backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM. ⚠️ Contains sensitive OAuth refresh tokens; use a strong password (min 12 chars).",
534
+ "vaultTokenWarning": "⚠️ Contains sensitive OAuth refresh tokens",
534
535
  "backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
535
536
  "backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
536
537
  "backupExportOk": "Backup exported successfully",
@@ -727,8 +728,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
727
728
  "backupImportTitle": "导入加密备份",
728
729
  "backupExportAction": "导出",
729
730
  "backupImportAction": "导入",
730
- "backupPassphrase": "凭据库密码(至少 4 位)",
731
- "backupExportHint": "使用 AES-256-GCM 与 PBKDF2 安全加密导出所有订阅账户与配置。",
731
+ "backupPassphrase": "凭据库密码(至少 12 位)",
732
+ "backupExportHint": "使用 AES-256-GCM 安全加密导出所有订阅账户与配置。⚠️ 包含敏感的 OAuth 刷新令牌,请使用至少 12 位的强密码保护。",
733
+ "vaultTokenWarning": "⚠️ 包含敏感的 OAuth 刷新令牌",
732
734
  "backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
733
735
  "backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
734
736
  "backupExportOk": "备份导出成功",
@@ -1676,7 +1678,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1676
1678
  backupModal === 'export' ? React.createElement('button', {
1677
1679
  type: 'button',
1678
1680
  className: 'dsub-mini',
1679
- disabled: backupLoading || !backupPass || backupPass.length < 4,
1681
+ disabled: backupLoading || !backupPass || backupPass.length < 12,
1680
1682
  onClick: doExportBackup,
1681
1683
  }, backupLoading ? '…' : t('backupExportAction')) : React.createElement('button', {
1682
1684
  type: 'button',
package/lib/crypto.js CHANGED
@@ -1,19 +1,31 @@
1
1
  import { scryptSync, pbkdf2Sync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
2
2
 
3
- // AES-256-GCM with PBKDF2 (100,000 iterations, SHA-256) (DSHE2:) or scrypt (DSHE1:).
4
- // Backward-compatible across both key derivation algorithms.
3
+ // AES-256-GCM with PBKDF2 (600,000 iterations, SHA-256) (DSHE2:),
4
+ // hardened scrypt (N=131072) (DSHE3:), or legacy scrypt (DSHE1:).
5
+ // Backward-compatible across all key derivation algorithms.
5
6
  export function encryptWithPassphrase(plainText, passphrase, { algorithm = "scrypt" } = {}) {
6
7
  const salt = randomBytes(16)
7
8
  const isPbkdf2 = algorithm === "pbkdf2"
8
- const key = isPbkdf2
9
- ? pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
10
- : scryptSync(String(passphrase), salt, 32)
9
+ const isHardenedScrypt = algorithm === "scrypt-v2" || algorithm === "scrypt-hardened"
10
+ let key
11
+ let prefix
12
+
13
+ if (isPbkdf2) {
14
+ key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
15
+ prefix = "DSHE2:"
16
+ } else if (isHardenedScrypt) {
17
+ key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
18
+ prefix = "DSHE3:"
19
+ } else {
20
+ key = scryptSync(String(passphrase), salt, 32)
21
+ prefix = "DSHE1:"
22
+ }
23
+
11
24
  const iv = randomBytes(12)
12
25
  const cipher = createCipheriv("aes-256-gcm", key, iv)
13
26
  const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
14
27
  const tag = cipher.getAuthTag()
15
28
  // format: magic + salt + iv + tag + data
16
- const prefix = isPbkdf2 ? "DSHE2:" : "DSHE1:"
17
29
  return prefix + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
18
30
  }
19
31
 
@@ -24,13 +36,28 @@ export function decryptWithPassphrase(payload, passphrase) {
24
36
  let iv
25
37
  let tag
26
38
  let data
27
- if (raw.startsWith("DSHE2:")) {
39
+ if (raw.startsWith("DSHE3:")) {
40
+ const buf = Buffer.from(raw.slice(6), "base64")
41
+ salt = buf.subarray(0, 16)
42
+ iv = buf.subarray(16, 28)
43
+ tag = buf.subarray(28, 44)
44
+ data = buf.subarray(44)
45
+ key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
46
+ } else if (raw.startsWith("DSHE2:")) {
28
47
  const buf = Buffer.from(raw.slice(6), "base64")
29
48
  salt = buf.subarray(0, 16)
30
49
  iv = buf.subarray(16, 28)
31
50
  tag = buf.subarray(28, 44)
32
51
  data = buf.subarray(44)
33
- key = pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
52
+ // Try 600,000 iterations first (#414), fallback to legacy 100,000 iterations
53
+ try {
54
+ key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
55
+ const decipher = createDecipheriv("aes-256-gcm", key, iv)
56
+ decipher.setAuthTag(tag)
57
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8")
58
+ } catch {
59
+ key = pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
60
+ }
34
61
  } else if (raw.startsWith("DSHE1:")) {
35
62
  const buf = Buffer.from(raw.slice(6), "base64")
36
63
  salt = buf.subarray(0, 16)
package/lib/history.js CHANGED
@@ -6,15 +6,21 @@ import { homedir } from 'node:os'
6
6
  // ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
7
7
  // All IO is best-effort: a write failure must never crash the harness.
8
8
 
9
- export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
9
+ export function resolveHistoryDir(customDir) {
10
+ if (customDir) return customDir
11
+ if (process.env.DSH_HISTORY_DIR) return process.env.DSH_HISTORY_DIR
12
+ if (process.env.DSH_STORAGE_DIR) return process.env.DSH_STORAGE_DIR
13
+ if (process.env.DSH_HOME) return join(process.env.DSH_HOME, 'storages', 'dsh-subscriptions')
14
+ return join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
15
+ }
10
16
 
11
17
  export class HistoryStore {
12
- constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0, maxEntries = 1000) {
13
- this.dir = dir
18
+ constructor(dir = resolveHistoryDir(), ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0, maxEntries = 1000) {
19
+ this.dir = resolveHistoryDir(dir)
14
20
  this.ttlMs = ttlMs
15
21
  this.debounceMs = debounceMs
16
22
  this.maxEntries = maxEntries
17
- this.path = join(dir, 'history.json')
23
+ this.path = join(this.dir, 'history.json')
18
24
  this.rows = []
19
25
  this._persistTimer = null
20
26
  this._load()
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { readClaudeCredentials } from './vendors/claude-cli.js'
5
- import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
6
5
 
7
6
  // Test seam: a fixture root overrides the real home for the duration of a
8
7
  // public call. Undefined (the default) keeps production behaviour.
@@ -46,10 +45,9 @@ async function discoverLocalCliSessionsInner() {
46
45
  }
47
46
  }
48
47
 
49
- // 2. Grok CLI / Hermes
48
+ // 2. Grok CLI
50
49
  const grokPaths = [
51
50
  homeFile('.grok', 'auth.json'),
52
- homeFile('.hermes', 'auth.json'),
53
51
  ]
54
52
  for (const p of grokPaths) {
55
53
  const grok = await readJson(p)
@@ -198,24 +196,7 @@ async function discoverLocalCliSessionsInner() {
198
196
  }
199
197
  }
200
198
 
201
- // 10. Coding Agents (Aider / Roo-Code)
202
- const agentConfigs = await discoverCodingAgentConfigs()
203
- if (agentConfigs.aider) {
204
- detected.aider = {
205
- provider: 'aider',
206
- path: agentConfigs.aider.path,
207
- email: 'Aider Configured Agent',
208
- hasRefreshToken: false,
209
- }
210
- }
211
- if (agentConfigs.roocode) {
212
- detected.roocode = {
213
- provider: 'roocode',
214
- path: agentConfigs.roocode.path,
215
- email: 'Roo-Code Configured Agent',
216
- hasRefreshToken: false,
217
- }
218
- }
199
+
219
200
 
220
201
  return detected
221
202
  }
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
16
16
  import { createResetCreditService } from './reset-credits.js'
17
17
  import { maskEmail, maskLabel } from './mask.js'
18
18
  import { proxyFetch } from './proxy.js'
19
- import { HistoryStore, DEFAULT_HISTORY_DIR } from './history.js'
19
+ import { HistoryStore, resolveHistoryDir } from './history.js'
20
20
  import { createDiagnosticsReport } from './diagnostics.js'
21
21
  import { createAccountsView } from './accounts-view.js'
22
22
  import { createAdapterManager } from './adapter-manager.js'
@@ -185,7 +185,8 @@ export function apply(ctx, rawConfig) {
185
185
  bestEffort('emit limit-notice', () => ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }), undefined, logger)
186
186
  },
187
187
  })
188
- const history = new HistoryStore(DEFAULT_HISTORY_DIR, 7 * 24 * 60 * 60 * 1000, 1000)
188
+ const historyDir = ctx?.historyDir || ctx?.storageDir || resolveHistoryDir()
189
+ const history = new HistoryStore(historyDir, 7 * 24 * 60 * 60 * 1000, 1000)
189
190
  if (typeof ctx?.on === 'function') {
190
191
  ctx.on('dispose', () => history.dispose())
191
192
  }
package/lib/vault.js CHANGED
@@ -11,8 +11,8 @@ export const VAULT_FORMAT = 'dsh-subscriptions-vault-v1'
11
11
  */
12
12
  export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscriptions', algorithm = 'scrypt' }, passphrase) {
13
13
  const pass = String(passphrase || '').trim()
14
- if (!pass || pass.length < 4) {
15
- throw new Error('passphrase must be at least 4 characters')
14
+ if (!pass || pass.length < 12) {
15
+ throw new Error('passphrase must be at least 12 characters')
16
16
  }
17
17
 
18
18
  const payload = {
@@ -43,7 +43,7 @@ export function importVault(encryptedPayload, passphrase) {
43
43
  }
44
44
 
45
45
  const raw = String(encryptedPayload || '').trim()
46
- if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:')) {
46
+ if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:') && !raw.startsWith('DSHE3:')) {
47
47
  throw new Error('invalid vault format: missing DSHE1 header')
48
48
  }
49
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.29",
3
+ "version": "0.6.30",
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",
@@ -37,7 +37,7 @@
37
37
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
38
38
  },
39
39
  "scripts": {
40
- "test": "eslint lib/ test/ && node --test test/*.test.mjs",
40
+ "test": "eslint lib/ test/ && node --import ./test/_setup.mjs --test test/*.test.mjs",
41
41
  "typecheck": "tsc -p tsconfig.checkjs.json"
42
42
  },
43
43
  "dsh": {
@@ -1,37 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { homedir } from 'node:os'
3
- import { join } from 'node:path'
4
-
5
- export async function discoverCodingAgentConfigs() {
6
- const configs = {}
7
-
8
- // 1. Aider (.aider.conf.yml)
9
- try {
10
- const p = join(homedir(), '.aider.conf.yml')
11
- const text = await readFile(p, 'utf8')
12
- configs.aider = {
13
- name: 'Aider Config',
14
- path: p,
15
- found: true,
16
- raw: text
17
- }
18
- } catch {
19
- // not present
20
- }
21
-
22
- // 2. Roo-Code / Roo-Cline settings
23
- try {
24
- const p = join(homedir(), '.roo-code', 'settings.json')
25
- const text = await readFile(p, 'utf8')
26
- configs.roocode = {
27
- name: 'Roo-Code Config',
28
- path: p,
29
- found: true,
30
- raw: text
31
- }
32
- } catch {
33
- // not present
34
- }
35
-
36
- return configs
37
- }