dsh-connect-qoder 0.1.1 → 0.2.0
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/credentials.js +89 -5
- package/package.json +1 -1
package/lib/credentials.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { execFileSync } from 'node:child_process'
|
|
22
22
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
23
23
|
import { tmpdir } from 'node:os'
|
|
24
|
-
import { join } from 'node:path'
|
|
24
|
+
import { join, basename } from 'node:path'
|
|
25
25
|
import { createDecipheriv } from 'node:crypto'
|
|
26
26
|
import { DatabaseSync } from 'node:sqlite'
|
|
27
27
|
|
|
@@ -49,6 +49,11 @@ export const REGIONS = [
|
|
|
49
49
|
mode: 'cn',
|
|
50
50
|
displayName: 'Qoder CN',
|
|
51
51
|
appNames: ['QoderCN', 'Qoder CN', 'QoderWork CN'],
|
|
52
|
+
// Qoder 0.3.x moved its user data to a `com.<vendor>.app.<channel>` Electron
|
|
53
|
+
// directory and replaced the VS Code `state.vscdb` store with `auth.v1.dat`.
|
|
54
|
+
// Both layouts are listed so a user who upgrades keeps working; the newer
|
|
55
|
+
// one is tried first because that is where a freshly installed app writes.
|
|
56
|
+
newAppNames: ['com.qodercn.app.stable'],
|
|
52
57
|
baseUrl: 'https://gateway.qoder.com.cn/',
|
|
53
58
|
openApiUrl: 'https://openapi.qoder.com.cn',
|
|
54
59
|
centerUrl: 'https://gateway.qoder.com.cn',
|
|
@@ -60,6 +65,7 @@ export const REGIONS = [
|
|
|
60
65
|
mode: 'global',
|
|
61
66
|
displayName: 'Qoder',
|
|
62
67
|
appNames: ['Qoder', 'QoderWork'],
|
|
68
|
+
newAppNames: ['com.qoder.app.stable'],
|
|
63
69
|
baseUrl: 'https://api3.qoder.sh/',
|
|
64
70
|
openApiUrl: 'https://openapi.qoder.sh',
|
|
65
71
|
centerUrl: 'https://center.qoder.sh',
|
|
@@ -218,11 +224,14 @@ function stateDbCandidates(appDir) {
|
|
|
218
224
|
/**
|
|
219
225
|
* The machine id Qoder binds its session to.
|
|
220
226
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
227
|
+
* The legacy layout keeps it in `machineid`; 0.3.x renamed the file to
|
|
228
|
+
* `auth.machine-id`. Both are checked because the id is sent to the gateway
|
|
229
|
+
* alongside the token, and a wrong one is indistinguishable from a hijacked
|
|
230
|
+
* session. When neither exists a stable value is derived from the app directory
|
|
231
|
+
* so repeated runs still agree with each other.
|
|
223
232
|
*/
|
|
224
233
|
function machineIdFor(appDir, fallback) {
|
|
225
|
-
for (const name of ['machineid', 'machineId']) {
|
|
234
|
+
for (const name of ['auth.machine-id', 'machineid', 'machineId']) {
|
|
226
235
|
const p = join(appDir, name)
|
|
227
236
|
if (!existsSync(p)) continue
|
|
228
237
|
const value = readFileSync(p, 'utf8').trim()
|
|
@@ -231,16 +240,91 @@ function machineIdFor(appDir, fallback) {
|
|
|
231
240
|
return fallback
|
|
232
241
|
}
|
|
233
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Read the 0.3.x credential file.
|
|
245
|
+
*
|
|
246
|
+
* From 0.3.x the apps keep their sign-in in
|
|
247
|
+
* `<userData>/auth.v1.dat` — a Chromium OSCrypt blob whose plaintext is the
|
|
248
|
+
* session JSON directly, rather than a row inside a VS Code `state.vscdb`:
|
|
249
|
+
*
|
|
250
|
+
* ```json
|
|
251
|
+
* { "schemaVersion": 1, "token": "dt-…", "refreshToken": "drt-…",
|
|
252
|
+
* "expiresAt": "2026-10-19T07:44:22Z", "refreshTokenExpiresAt": "…",
|
|
253
|
+
* "user": { "id": "…", "name": "…", "email": "…" } }
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* The envelope is unchanged (`v10` + AES-256-GCM under the DPAPI-wrapped key),
|
|
257
|
+
* so {@link oscryptKeyFor} and {@link decryptOscrypt} are reused as they are.
|
|
258
|
+
*
|
|
259
|
+
* @returns a credential record, or `undefined` when the file is absent or
|
|
260
|
+
* cannot be decoded.
|
|
261
|
+
*/
|
|
262
|
+
function loadNewCredential(region, appDir, oscryptKey) {
|
|
263
|
+
const file = join(appDir, 'auth.v1.dat')
|
|
264
|
+
if (!existsSync(file)) return undefined
|
|
265
|
+
const plain = decryptOscrypt(readFileSync(file), oscryptKey)
|
|
266
|
+
if (plain === undefined) return undefined
|
|
267
|
+
let session
|
|
268
|
+
try {
|
|
269
|
+
session = JSON.parse(plain)
|
|
270
|
+
} catch {
|
|
271
|
+
return undefined
|
|
272
|
+
}
|
|
273
|
+
if (session === null || typeof session !== 'object') return undefined
|
|
274
|
+
if (typeof session.token !== 'string' || session.token.length === 0) return undefined
|
|
275
|
+
const user = session.user !== null && typeof session.user === 'object' ? session.user : {}
|
|
276
|
+
const userID = typeof user.id === 'string' ? user.id : ''
|
|
277
|
+
if (userID.length === 0) return undefined
|
|
278
|
+
// The new file carries ISO timestamps rather than epoch millis.
|
|
279
|
+
const expiresAt = Date.parse(session.expiresAt)
|
|
280
|
+
const refreshExpiresAt = Date.parse(session.refreshTokenExpiresAt)
|
|
281
|
+
return {
|
|
282
|
+
region: region.id,
|
|
283
|
+
appName: basename(appDir),
|
|
284
|
+
userID,
|
|
285
|
+
name: typeof user.name === 'string' ? user.name : '',
|
|
286
|
+
email: typeof user.email === 'string' ? user.email : '',
|
|
287
|
+
token: session.token,
|
|
288
|
+
refreshToken: typeof session.refreshToken === 'string' ? session.refreshToken : '',
|
|
289
|
+
refreshTokenExpiresAt: Number.isFinite(refreshExpiresAt) ? refreshExpiresAt : 0,
|
|
290
|
+
expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
|
|
291
|
+
expired: Number.isFinite(expiresAt) && expiresAt > 0 ? expiresAt <= Date.now() : false,
|
|
292
|
+
// The new layout does not publish these; they were only ever used for
|
|
293
|
+
// display, and the catalog call supplies what routing actually needs.
|
|
294
|
+
userType: '',
|
|
295
|
+
userTag: '',
|
|
296
|
+
machineID: machineIdFor(appDir, `dsh-connect-qoder-${region.id}`),
|
|
297
|
+
source: 'app',
|
|
298
|
+
plan: undefined,
|
|
299
|
+
usage: undefined,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
234
303
|
/**
|
|
235
304
|
* Load one region's credential from the local Qoder apps.
|
|
236
305
|
*
|
|
306
|
+
* The 0.3.x layout is tried before the legacy one, because that is where a
|
|
307
|
+
* freshly installed app writes; a user who upgrades therefore keeps working
|
|
308
|
+
* without a reinstall, and a user who has not upgraded is unaffected.
|
|
309
|
+
*
|
|
237
310
|
* Every candidate app is tried in order; the first that yields a decryptable
|
|
238
|
-
*
|
|
311
|
+
* credential wins. A credential whose access token has expired is still
|
|
239
312
|
* returned — the caller decides whether to refresh it — but `expired` says so.
|
|
240
313
|
*
|
|
241
314
|
* @returns a credential record, or `undefined` when no app holds a usable one.
|
|
242
315
|
*/
|
|
243
316
|
export function loadCredential(region, appDataRoot) {
|
|
317
|
+
// 0.3.x: `com.<vendor>.app.stable/auth.v1.dat`.
|
|
318
|
+
for (const appName of region.newAppNames ?? []) {
|
|
319
|
+
const appDir = join(appDataRoot, appName)
|
|
320
|
+
if (!existsSync(appDir)) continue
|
|
321
|
+
const oscryptKey = oscryptKeyFor(appDir)
|
|
322
|
+
if (oscryptKey === undefined) continue
|
|
323
|
+
const credential = safeRead(() => loadNewCredential(region, appDir, oscryptKey))
|
|
324
|
+
if (credential !== undefined) return credential
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Legacy: `<AppName>/User/globalStorage/state.vscdb`.
|
|
244
328
|
for (const appName of region.appNames) {
|
|
245
329
|
const appDir = join(appDataRoot, appName)
|
|
246
330
|
if (!existsSync(appDir)) continue
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-qoder",
|
|
3
3
|
"displayName": "DSH Connect Qoder",
|
|
4
4
|
"description": "将本机已登录的 Qoder(国内版 Qoder CN / 国际版 Qoder)模型接入 DeepSeek Harness —— bring locally signed-in Qoder models into DeepSeek Harness with zero configuration.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"author": "hdhgsysh",
|
|
7
7
|
"homepage": "https://github.com/hdhgsysh/dsh-connect-qoder",
|
|
8
8
|
"repository": {
|