@magpie-community/opencode-trae-auth 0.2.13 → 0.2.14
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 +21 -2
- package/index.mjs +133 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -288,10 +288,29 @@ answered it 9074 even with the client's headers.
|
|
|
288
288
|
`checked_in`, `credits`.
|
|
289
289
|
- **Claim:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/claim`,
|
|
290
290
|
the same, only while it is on and today's isn't in.
|
|
291
|
+
- **The device, when Trae CN's own client is here (0.2.14):** the
|
|
292
|
+
account this machine's Trae CN desktop client is signed in as checks in
|
|
293
|
+
on the client's own device, the one Trae's device service registered:
|
|
294
|
+
`x-device-id` is the id in the client's
|
|
295
|
+
`globalStorage/storage.json` key `iCubeAuthInfo://icube-dc:<id>`,
|
|
296
|
+
`x-device-type` and `x-os-version` are this machine's, and no
|
|
297
|
+
`x-device-brand` is sent. The account is matched by the `userId` in the
|
|
298
|
+
client's sign-in (`iCubeAuthInfo://icube.cloudide`, plain JSON or the
|
|
299
|
+
client's fixed codec, as dingminhua/dsh-connect-trae reads it). The file
|
|
300
|
+
is only read: nothing in it is written, and the client's token is never
|
|
301
|
+
used. On one account the client's claim went through while the
|
|
302
|
+
plugin's, on its own device, got 9074 (yetone/magpie#808, #1920); that
|
|
303
|
+
this header is what changes it hasn't been tried on a real account
|
|
304
|
+
yet. Every other account keeps its own device, and a file that can't be
|
|
305
|
+
read or made out, or holds no single device, changes nothing. Files:
|
|
306
|
+
macOS `~/Library/Application Support/Trae CN/…`, Windows
|
|
307
|
+
`%APPDATA%\Trae CN\…` (or `trae-cn`), Linux `~/.config/trae-cn/…`
|
|
308
|
+
(or `Trae CN`; not seen on a real Linux install).
|
|
291
309
|
- **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
|
|
292
310
|
the account for a new sign-in. Code 9095 means this device has checked
|
|
293
|
-
in today
|
|
294
|
-
|
|
311
|
+
in today. No device id is ever made up, changed or rotated to get round
|
|
312
|
+
a 9095 or a 9074, and no account is given a device that isn't its
|
|
313
|
+
own client's.
|
|
295
314
|
|
|
296
315
|
Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
|
|
297
316
|
that isn't a chat request is still refused (400). Needs 0.1.4 or later.
|
package/index.mjs
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
// The account's fetch also sends Trae CN's own pages on api.trae.cn
|
|
10
10
|
// (/trae/api/…) as the account, for magpie's daily check-in (每日签到:
|
|
11
11
|
// /trae/api/v2/ug/checkin_credits/status, then /claim).
|
|
12
|
-
import { createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
12
|
+
import { createDecipheriv, createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
13
|
+
import { readFile } from "node:fs/promises"
|
|
13
14
|
import { createServer, STATUS_CODES } from "node:http"
|
|
15
|
+
import { homedir, release, version as osVersion } from "node:os"
|
|
16
|
+
import { join } from "node:path"
|
|
14
17
|
|
|
15
18
|
const SITES = {
|
|
16
19
|
// Trae CN (trae.cn): the authorization page sends the browser back to a
|
|
@@ -739,9 +742,12 @@ const ugUA = (site) =>
|
|
|
739
742
|
// ugHeaders are the headers Trae CN's IDE (3.3.104) sends a growth page
|
|
740
743
|
// with, and only those: its JWT, then its device (fb: x-device-id,
|
|
741
744
|
// x-device-brand, x-device-type, x-os-version, x-app-version), not the
|
|
742
|
-
// chat's tokens and versions.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
+
// chat's tokens and versions. own is this machine's Trae CN client, when
|
|
746
|
+
// it is signed in as this very account (ownClient): its device id and
|
|
747
|
+
// this machine's OS go instead of the account's, and no brand is named
|
|
748
|
+
// (fb leaves it out when it has no model to name).
|
|
749
|
+
function ugHeaders(a, site, extra = {}, own) {
|
|
750
|
+
const h = {
|
|
745
751
|
"Content-Type": "application/json",
|
|
746
752
|
Authorization: `Cloud-IDE-JWT ${a.token}`,
|
|
747
753
|
"User-Agent": ugUA(site),
|
|
@@ -750,8 +756,121 @@ function ugHeaders(a, site, extra = {}) {
|
|
|
750
756
|
"x-device-type": "Windows",
|
|
751
757
|
"x-os-version": "10.0.22631",
|
|
752
758
|
"x-app-version": site.clientVersion,
|
|
753
|
-
...extra,
|
|
754
759
|
}
|
|
760
|
+
if (own) {
|
|
761
|
+
h["x-device-id"] = own.deviceId
|
|
762
|
+
h["x-device-type"] = own.type
|
|
763
|
+
h["x-os-version"] = own.os
|
|
764
|
+
delete h["x-device-brand"]
|
|
765
|
+
}
|
|
766
|
+
return { ...h, ...extra }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---- this machine's own Trae CN client ----------------------------------------------
|
|
770
|
+
|
|
771
|
+
// Trae CN's desktop client keeps its state in a VS Code-style
|
|
772
|
+
// globalStorage/storage.json. The device id Trae's device service gave
|
|
773
|
+
// it is in a key's name, iCubeAuthInfo://icube-dc:<digits> (15 or 16
|
|
774
|
+
// digits seen); its sign-in is the value of iCubeAuthInfo://icube.cloudide,
|
|
775
|
+
// a JSON envelope (userId, token, …) stored as is or obfuscated with the
|
|
776
|
+
// client's fixed AES-128-CBC codec. Seen in real clients' files
|
|
777
|
+
// (yetone/magpie#808: the same id in the client's own did= query;
|
|
778
|
+
// yetone/magpie#1705) and read the same way by dingminhua/dsh-connect-trae
|
|
779
|
+
// (MIT), whose codec this is.
|
|
780
|
+
const DC_KEY = "iCubeAuthInfo://icube-dc:"
|
|
781
|
+
const AUTH_KEY = "iCubeAuthInfo://icube.cloudide"
|
|
782
|
+
|
|
783
|
+
// ownClient.paths are where storage.json may be, in order; tests replace it
|
|
784
|
+
const ownClient = {
|
|
785
|
+
paths: () => clientStoragePaths(process.platform, homedir(), process.env),
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function clientStoragePaths(platform, home, env) {
|
|
789
|
+
const file = (root, name) => join(root, name, "User", "globalStorage", "storage.json")
|
|
790
|
+
if (platform === "darwin") return [file(join(home, "Library", "Application Support"), "Trae CN")]
|
|
791
|
+
if (platform === "win32") {
|
|
792
|
+
const roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((r, i, all) => r && all.indexOf(r) === i)
|
|
793
|
+
return roots.flatMap((r) => ["Trae CN", "trae-cn"].map((n) => file(r, n)))
|
|
794
|
+
}
|
|
795
|
+
if (platform === "linux") return ["trae-cn", "Trae CN"].map((n) => file(env.XDG_CONFIG_HOME || join(home, ".config"), n))
|
|
796
|
+
return []
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// clientDevice is the device id of this machine's Trae CN client when the
|
|
800
|
+
// client is signed in as uid, else undefined. It only reads: nothing of
|
|
801
|
+
// the client's is written, kept or sent but that id, and the client's own
|
|
802
|
+
// token is read past, never used. A file it can't read or make out, or one
|
|
803
|
+
// with no single device, names no device, and the account keeps its own.
|
|
804
|
+
// Never an id made up or changed: an account the client isn't signed in
|
|
805
|
+
// as never gets the client's id.
|
|
806
|
+
async function clientDevice(uid) {
|
|
807
|
+
if (!uid) return undefined
|
|
808
|
+
for (const p of ownClient.paths()) {
|
|
809
|
+
let text
|
|
810
|
+
try {
|
|
811
|
+
text = await readFile(p, "utf8")
|
|
812
|
+
} catch {
|
|
813
|
+
continue // not installed here
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const doc = JSON.parse(text)
|
|
817
|
+
const ids = Object.keys(doc ?? {}).filter((k) => k.startsWith(DC_KEY)).map((k) => k.slice(DC_KEY.length))
|
|
818
|
+
if (ids.length !== 1 || !/^\d{8,20}$/.test(ids[0])) return undefined
|
|
819
|
+
const who = clientAuth(doc[AUTH_KEY])?.userId
|
|
820
|
+
return who != null && String(who) === String(uid) ? ids[0] : undefined
|
|
821
|
+
} catch {
|
|
822
|
+
return undefined
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return undefined
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// clientAuth is the client's sign-in envelope, as stored or decoded
|
|
829
|
+
function clientAuth(v) {
|
|
830
|
+
if (typeof v !== "string" || !v.trim()) return undefined
|
|
831
|
+
const t = v.trim()
|
|
832
|
+
return JSON.parse(t.startsWith("{") ? t : clientDecode(t))
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const xorOf = (a, b) => Buffer.from(a.map((x, i) => x ^ b[i]))
|
|
836
|
+
const CODEC = {
|
|
837
|
+
"746305100000": xorOf(
|
|
838
|
+
[82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37],
|
|
839
|
+
[31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125],
|
|
840
|
+
),
|
|
841
|
+
"123920200203": xorOf(
|
|
842
|
+
[191, 192, 216, 250, 122, 246, 220, 97, 31, 254, 98, 27, 8, 72, 71, 176, 135, 99, 96, 18, 127, 101, 203, 104, 211, 102, 191, 125, 37, 72, 150, 156, 51, 229, 121, 35, 17, 153, 141, 177, 110, 131, 150, 128, 172, 255, 254, 6, 18, 140, 55, 62, 236, 249, 135, 64, 135, 12, 117, 4, 89, 149, 168, 209],
|
|
843
|
+
[246, 204, 26, 232, 232, 70, 129, 109, 223, 146, 169, 242, 23, 241, 105, 145, 50, 196, 165, 42, 254, 120, 3, 54, 244, 207, 209, 85, 53, 6, 138, 106, 175, 148, 31, 204, 186, 186, 165, 182, 87, 142, 49, 10, 39, 110, 26, 154, 86, 56, 173, 125, 18, 64, 198, 225, 99, 99, 83, 82, 191, 134, 76, 170],
|
|
844
|
+
),
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// clientDecode undoes the client's codec: a 6-byte header naming the salt,
|
|
848
|
+
// 32 random bytes, then AES-128-CBC under sha512(sha512(random) + salt);
|
|
849
|
+
// the plaintext is led by its own sha512, which is checked
|
|
850
|
+
function clientDecode(b64) {
|
|
851
|
+
const buf = Buffer.from(b64, "base64")
|
|
852
|
+
const salt = CODEC[buf.subarray(0, 6).toString("hex")]
|
|
853
|
+
if (!salt || buf.length <= 102) throw new Error("not the client's codec")
|
|
854
|
+
const key = createHash("sha512").update(Buffer.concat([createHash("sha512").update(buf.subarray(6, 38)).digest(), salt])).digest()
|
|
855
|
+
const d = createDecipheriv("aes-128-cbc", key.subarray(0, 16), key.subarray(16, 32))
|
|
856
|
+
const out = Buffer.concat([d.update(buf.subarray(38)), d.final()])
|
|
857
|
+
const body = out.subarray(64)
|
|
858
|
+
if (out.length < 64 || !createHash("sha512").update(body).digest().equals(out.subarray(0, 64))) throw new Error("the client's value doesn't check")
|
|
859
|
+
return body.toString("utf8")
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// thisMachine is how the client names this machine's OS: its type, and
|
|
863
|
+
// the OS's own version (Windows' product name, as "Windows 11 Home China"
|
|
864
|
+
// was seen, else its build; elsewhere the kernel's release)
|
|
865
|
+
function thisMachine(platform = process.platform) {
|
|
866
|
+
if (platform === "win32") {
|
|
867
|
+
let v = ""
|
|
868
|
+
try {
|
|
869
|
+
v = osVersion()
|
|
870
|
+
} catch {}
|
|
871
|
+
return { type: "windows", os: /^Windows \d/.test(v) ? v : `Windows ${release()}` }
|
|
872
|
+
}
|
|
873
|
+
return { type: platform === "darwin" ? "mac" : platform, os: `${platform === "darwin" ? "macOS" : platform} ${release()}` }
|
|
755
874
|
}
|
|
756
875
|
|
|
757
876
|
// ugPage: url is one of Trae's growth pages (the check-in)
|
|
@@ -2328,9 +2447,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2328
2447
|
const ug = ugPage(url)
|
|
2329
2448
|
let sent = method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}"
|
|
2330
2449
|
if (ug && sent !== undefined && /^\s*(\{\s*\})?\s*$/.test(sent)) sent = JSON.stringify({ req_source: 1 })
|
|
2450
|
+
// the account this machine's own Trae CN client is signed in as checks
|
|
2451
|
+
// in on the client's device, the one Trae registered: on one account
|
|
2452
|
+
// the client's own claim went through while the plugin's, on a device
|
|
2453
|
+
// it had made up, came back 9074 (yetone/magpie#808, #1920). Every
|
|
2454
|
+
// other account keeps its own device; none is given the client's.
|
|
2455
|
+
const deviceId = ug && site.id === "trae-cn" ? await clientDevice(a.uid) : undefined
|
|
2456
|
+
const own = deviceId ? { deviceId, ...thisMachine() } : undefined
|
|
2331
2457
|
const res = await fetch(url, {
|
|
2332
2458
|
method,
|
|
2333
|
-
headers: ug ? ugHeaders(a, site, { Accept: "application/json" }) : ideHeaders(a, site, { Accept: "application/json" }),
|
|
2459
|
+
headers: ug ? ugHeaders(a, site, { Accept: "application/json" }, own) : ideHeaders(a, site, { Accept: "application/json" }),
|
|
2334
2460
|
body: sent,
|
|
2335
2461
|
signal: signal ?? AbortSignal.timeout(20_000),
|
|
2336
2462
|
})
|
|
@@ -2683,4 +2809,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2683
2809
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2684
2810
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2685
2811
|
|
|
2686
|
-
export const _internal = { traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
|
2812
|
+
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-trae-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "OpenCode provider plugins for Trae: Trae CN (trae.cn) and Trae Global (trae.ai), free tier included, with Trae's own browser sign-in",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|