@magpie-community/opencode-trae-auth 0.2.15 → 0.2.17
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 -6
- package/index.mjs +69 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,9 +42,11 @@ An account of one realm does not sign in to the other: trae.cn's accounts are
|
|
|
42
42
|
not trae.ai's.
|
|
43
43
|
|
|
44
44
|
At sign-in the plugin creates the account's device and names it to the
|
|
45
|
-
authorization page: `device_id`
|
|
45
|
+
authorization page: `device_id` and `machine_id` (32 hex). Every
|
|
46
46
|
request then sends that same device. The relays saw requests dropped when the
|
|
47
|
-
device was new each time.
|
|
47
|
+
device was new each time. A Trae CN `device_id` is 16 digits since 0.2.16, the
|
|
48
|
+
shape of the ids Trae's device service gives its clients (15 or 16 digits);
|
|
49
|
+
Trae Global's stays 19.
|
|
48
50
|
|
|
49
51
|
### Enterprise (SaaS) accounts
|
|
50
52
|
|
|
@@ -279,7 +281,11 @@ an empty answer.
|
|
|
279
281
|
|
|
280
282
|
Trae CN gives credits for a daily check-in (每日签到); Trae Global has no
|
|
281
283
|
such page, so this is CN only. magpie can press it
|
|
282
|
-
once a day for each account (Settings, or the switch on the usage card)
|
|
284
|
+
once a day for each account (Settings, or the switch on the usage card),
|
|
285
|
+
through `auth.checkin`: `claimed` when Trae grants the credits, `done`
|
|
286
|
+
when the status or Trae's 9095 says this device has already claimed,
|
|
287
|
+
`inactive` when the page is disabled, and `failed` for a turned-away
|
|
288
|
+
sign-in or Trae's 9074 busy answer.
|
|
283
289
|
It asks Trae CN's own pages through
|
|
284
290
|
this plugin's fetch, which sends them as the account, as Trae CN's IDE
|
|
285
291
|
(3.3.104) sends them: its Cloud-IDE-JWT (renewed first when near its
|
|
@@ -314,11 +320,20 @@ answered it 9074 even with the client's headers.
|
|
|
314
320
|
macOS `~/Library/Application Support/Trae CN/…`, Windows
|
|
315
321
|
`%APPDATA%\Trae CN\…` (or `trae-cn`), Linux `~/.config/trae-cn/…`
|
|
316
322
|
(or `Trae CN`; not seen on a real Linux install).
|
|
323
|
+
- **Every other account's device (0.2.16):** its own device id, in the
|
|
324
|
+
client's shape. Trae CN answered a claim on the plugin's 19-digit id
|
|
325
|
+
9074, while the same account and token on a 16-digit id were taken
|
|
326
|
+
(magpie-community/plugins#194). An account signed in since 0.2.16 has a
|
|
327
|
+
16-digit id and checks in on it. One signed in before checks in on a
|
|
328
|
+
16-digit id made from its own device id and uid: the same id every day,
|
|
329
|
+
never rotated, and no other account's. Its chat requests and renewals
|
|
330
|
+
keep the 19-digit device it signed in with.
|
|
317
331
|
- **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
|
|
318
332
|
the account for a new sign-in. Code 9095 means this device has checked
|
|
319
|
-
in today. No device id is ever
|
|
320
|
-
a 9095 or a 9074, and no account is given
|
|
321
|
-
own client's
|
|
333
|
+
in today, and that holds for the day. No device id is ever changed or
|
|
334
|
+
rotated to get round a 9095 or a 9074, and no account is given another
|
|
335
|
+
account's device or its own client's device unless the client is signed
|
|
336
|
+
in as that account.
|
|
322
337
|
|
|
323
338
|
Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
|
|
324
339
|
that isn't a chat request is still refused (400). Needs 0.1.4 or later.
|
package/index.mjs
CHANGED
|
@@ -113,6 +113,10 @@ const SITES = {
|
|
|
113
113
|
// Trae CN's account info carries no region: its one deployment is the
|
|
114
114
|
// one it is asked through
|
|
115
115
|
regionOf: () => "",
|
|
116
|
+
checkin: {
|
|
117
|
+
status: { path: "/trae/api/v2/ug/checkin_credits/status" },
|
|
118
|
+
claim: { path: "/trae/api/v2/ug/checkin_credits/claim" },
|
|
119
|
+
},
|
|
116
120
|
// where usage is asked, and which page it is (CN: credits)
|
|
117
121
|
usage: {
|
|
118
122
|
kind: "credits",
|
|
@@ -296,8 +300,32 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
296
300
|
|
|
297
301
|
// device is the machine an account signs in from, made once at sign-in and
|
|
298
302
|
// named to trae.cn's authorization page, then sent with every request: a
|
|
299
|
-
// fresh one each time is what the relays saw requests dropped for.
|
|
300
|
-
|
|
303
|
+
// fresh one each time is what the relays saw requests dropped for. Trae
|
|
304
|
+
// CN's id has the shape its clients' have, 16 digits: the device service
|
|
305
|
+
// gives the IDE a 15- or 16-digit one (iCubeAuthInfo://icube-dc:<id>, see
|
|
306
|
+
// clientDevice), and Trae CN answers a check-in claim on a 19-digit id
|
|
307
|
+
// 9074 where the same account and token on a 16-digit one are taken
|
|
308
|
+
// (magpie-community/plugins#194). Trae's global site keeps the 19 digits
|
|
309
|
+
// it was signed in with: nothing says its ids are shaped otherwise.
|
|
310
|
+
const newDevice = (site) => ({ deviceId: digits(site?.id === "trae-cn" ? 16 : 19), machineId: randomBytes(16).toString("hex"), ...newDeviceKey() })
|
|
311
|
+
|
|
312
|
+
// ugDevice is the device id a Trae CN account's check-in goes on when this
|
|
313
|
+
// machine's client isn't signed in as it: its own when that already has a
|
|
314
|
+
// client's shape (15 or 16 digits), else, for an account signed in before
|
|
315
|
+
// 0.2.16 with 19 digits, a 16-digit id made from its own device id and
|
|
316
|
+
// uid. That one is the same on every check-in of the account, never
|
|
317
|
+
// rotated and never another account's: it is the account's own device in
|
|
318
|
+
// the shape Trae CN takes (#194), not a new device to get past a refusal,
|
|
319
|
+
// and a 9095 (this device is in for today) on it stays the day's answer.
|
|
320
|
+
// The chat's pages and the renewal keep the device the account signed in
|
|
321
|
+
// with.
|
|
322
|
+
function ugDevice(a) {
|
|
323
|
+
const id = String(a.deviceId ?? "")
|
|
324
|
+
if (/^\d{15,16}$/.test(id)) return id
|
|
325
|
+
if (!id && !a.uid) return id
|
|
326
|
+
const n = BigInt("0x" + createHash("sha256").update(`trae-cn-device\0${id}\0${a.uid ?? ""}`).digest("hex").slice(0, 30))
|
|
327
|
+
return String(1_000_000_000_000_000n + (n % 9_000_000_000_000_000n))
|
|
328
|
+
}
|
|
301
329
|
|
|
302
330
|
// newDeviceKey is the device's key pair, an ECDSA P-256 one as the clients
|
|
303
331
|
// keep (iCubeAuthInfo://icube-dc:<device>), whose public half goes with a
|
|
@@ -627,7 +655,7 @@ async function signedIn(q, device, site) {
|
|
|
627
655
|
// browserSignIn is the IDE's sign-in: trae.cn's authorization page, back
|
|
628
656
|
// to a callback on 127.0.0.1 (the page takes no other kind)
|
|
629
657
|
async function browserSignIn(site) {
|
|
630
|
-
const device = newDevice()
|
|
658
|
+
const device = newDevice(site)
|
|
631
659
|
const trace = randomUUID()
|
|
632
660
|
let over = false
|
|
633
661
|
let settle
|
|
@@ -2491,6 +2519,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2491
2519
|
// other account keeps its own device; none is given the client's.
|
|
2492
2520
|
const deviceId = ug && site.id === "trae-cn" ? await clientDevice(a.uid) : undefined
|
|
2493
2521
|
const own = deviceId ? { deviceId, ...thisMachine() } : undefined
|
|
2522
|
+
// every other Trae CN account checks in on its own device, in the
|
|
2523
|
+
// client's shape (ugDevice, #194)
|
|
2524
|
+
if (ug && site.id === "trae-cn" && !own) a = { ...a, deviceId: ugDevice(a) }
|
|
2494
2525
|
const res = await fetch(url, {
|
|
2495
2526
|
method,
|
|
2496
2527
|
headers: ug ? ugHeaders(a, site, { Accept: "application/json" }, own) : ideHeaders(a, site, { Accept: "application/json" }),
|
|
@@ -2503,6 +2534,39 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2503
2534
|
return new Response(text, { status: res.status, headers })
|
|
2504
2535
|
}
|
|
2505
2536
|
|
|
2537
|
+
// checkin asks Trae CN's own pages as magpie's check-in host does, with
|
|
2538
|
+
// the same account, device and renewal as any own page. Trae Global has
|
|
2539
|
+
// no such page.
|
|
2540
|
+
const checkin = async (getAuth) => {
|
|
2541
|
+
if (!site.checkin) return { outcome: "inactive", message: `${site.realm} has no daily check-in` }
|
|
2542
|
+
const a0 = fromAuth(await getAuth(), site)
|
|
2543
|
+
if (!a0) return { outcome: "failed", message: "not signed in" }
|
|
2544
|
+
const base = site.hosts.auth + site.checkin.status.path
|
|
2545
|
+
const status = await ownRequest(getAuth, a0, base, "POST", "{}")
|
|
2546
|
+
const statusText = await status.text()
|
|
2547
|
+
let body
|
|
2548
|
+
try { body = JSON.parse(statusText) } catch {}
|
|
2549
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return { outcome: "failed", message: `check-in status: unreadable reply ${statusText.trim().slice(0, 120) || "(empty)"}` }
|
|
2550
|
+
const e = errorOf(body)
|
|
2551
|
+
if (status.status === 401 || lapsedCode(e.code, site)) return { outcome: "failed", message: e.message || `${a0.name || a0.uid}: Trae's sign-in has expired` }
|
|
2552
|
+
if (status.status !== 200 || (e.code && String(e.code) !== "0")) return { outcome: "failed", message: e.message || statusLine(status.status) }
|
|
2553
|
+
if (body?.checked_in) return { outcome: "done", ...(Number(body.credits) > 0 ? { credit: Number(body.credits) } : {}) }
|
|
2554
|
+
if (!body?.enable) return { outcome: "inactive" }
|
|
2555
|
+
const claim = await ownRequest(getAuth, a0, site.hosts.auth + site.checkin.claim.path, "POST", "{}")
|
|
2556
|
+
const claimText = await claim.text()
|
|
2557
|
+
let answer
|
|
2558
|
+
try { answer = JSON.parse(claimText) } catch {}
|
|
2559
|
+
const ce = errorOf(answer)
|
|
2560
|
+
if (claim.status === 401 || lapsedCode(ce.code, site)) return { outcome: "failed", message: ce.message || `${a0.name || a0.uid}: Trae's sign-in has expired` }
|
|
2561
|
+
if (String(ce.code) === "9095") return { outcome: "ineligible", message: ce.message || "this device has checked in today" }
|
|
2562
|
+
if (String(ce.code) === "9074") return { outcome: "ineligible", message: `${ce.message} — check in in the Trae CN app` }
|
|
2563
|
+
if (claim.status !== 200 || (ce.code && String(ce.code) !== "0")) return { outcome: "failed", message: ce.message || statusLine(claim.status) }
|
|
2564
|
+
if (!answer || typeof answer !== "object" || Array.isArray(answer)) return { outcome: "failed", message: `check-in claim: unreadable reply ${claimText.trim().slice(0, 120) || "(empty)"}` }
|
|
2565
|
+
const credit = Number(answer.credits) + Number(answer.extra_credits || 0)
|
|
2566
|
+
const fallback = Number(body.credits) + Number(body.extra_credits || 0)
|
|
2567
|
+
return { outcome: "claimed", ...(credit > 0 ? { credit } : fallback > 0 ? { credit: fallback } : {}) }
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2506
2570
|
const usage = async (getAuth) => {
|
|
2507
2571
|
let a
|
|
2508
2572
|
try {
|
|
@@ -2746,6 +2810,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2746
2810
|
auth: {
|
|
2747
2811
|
provider: site.id,
|
|
2748
2812
|
usage,
|
|
2813
|
+
checkin,
|
|
2749
2814
|
// magpie renews the token LEAD_MS before its end, once, before its
|
|
2750
2815
|
// requests, models and usage ask; the check before each request
|
|
2751
2816
|
// stays for OpenCode, which doesn't call this
|
|
@@ -2861,4 +2926,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2861
2926
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2862
2927
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2863
2928
|
|
|
2864
|
-
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 }
|
|
2929
|
+
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, ugDevice, 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.17",
|
|
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",
|