@magpie-community/opencode-trae-auth 0.2.9 → 0.2.10
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 +17 -0
- package/index.mjs +89 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,23 @@ authorization page: `device_id` (19 digits) and `machine_id` (32 hex). Every
|
|
|
46
46
|
request then sends that same device. The relays saw requests dropped when the
|
|
47
47
|
device was new each time.
|
|
48
48
|
|
|
49
|
+
### Enterprise (SaaS) accounts
|
|
50
|
+
|
|
51
|
+
Since 0.2.10 the authorization page also offers Trae's enterprise (SaaS / SSO)
|
|
52
|
+
sign-in; before, the plugin asked the page to hide it (`hide_saas_login`), so
|
|
53
|
+
an enterprise subscription could only sign in as a personal account. A host
|
|
54
|
+
the sign-in names for the account is kept when it is an https host on Trae's
|
|
55
|
+
own domains (`trae.cn`, `trae.ai`, `mchost.guru`): a model host other than the
|
|
56
|
+
consumer site's own (`www`, `api`, `grow…`) takes the account's model
|
|
57
|
+
requests, and a token host the callback names (`authHost`) takes its
|
|
58
|
+
renewals. Any other host is ignored and the consumer hosts are used, as for a
|
|
59
|
+
personal account, which is unchanged. The account's tenant and login-type
|
|
60
|
+
fields are kept with the sign-in; nothing sends them.
|
|
61
|
+
|
|
62
|
+
**Not tried with a real enterprise account.** Which hosts and fields an
|
|
63
|
+
enterprise sign-in sends back is not known yet; if yours doesn't work, open an
|
|
64
|
+
issue with what the sign-in page showed.
|
|
65
|
+
|
|
49
66
|
### The international realm's region
|
|
50
67
|
|
|
51
68
|
Trae Global has two deployments. The sign-in's `AIRegion` (or a US auth host
|
package/index.mjs
CHANGED
|
@@ -79,11 +79,19 @@ const SITES = {
|
|
|
79
79
|
// the model host for an account: Trae CN's own hosts serve no models,
|
|
80
80
|
// so only a host that is one is taken, else the one gateway
|
|
81
81
|
apiOf: (a) => {
|
|
82
|
-
const h =
|
|
83
|
-
return
|
|
82
|
+
const h = traeHost(a?.api)
|
|
83
|
+
return h && SITES["trae-cn"].modelHost(h) ? h : SITES["trae-cn"].hosts.api
|
|
84
|
+
},
|
|
85
|
+
// whether a host named by a sign-in is one of the model hosts: an https
|
|
86
|
+
// host on mchost.guru or named trae-api-…, as the consumer gateway is,
|
|
87
|
+
// or another trae.cn host than the consumer site's own (www, api,
|
|
88
|
+
// grow…), which an enterprise (SaaS) sign-in may name for its tenant
|
|
89
|
+
modelHost: (h) => {
|
|
90
|
+
const t = traeHost(h)
|
|
91
|
+
if (!t) return false
|
|
92
|
+
const n = new URL(t).hostname
|
|
93
|
+
return /(^|\.)mchost\.guru$|^trae-api-/i.test(n) || (/\.trae\.cn$/i.test(n) && !/^(www|api|grow[^.]*)\./i.test(n))
|
|
84
94
|
},
|
|
85
|
-
// whether a host named by a sign-in is one of the model hosts
|
|
86
|
-
modelHost: (h) => /mchost\.guru|trae-api-/i.test(String(h ?? "")),
|
|
87
95
|
// ownPage: url is one of Trae CN's own JSON pages on api.trae.cn
|
|
88
96
|
// (/trae/api/…: the daily check-in, credits), which the account's fetch
|
|
89
97
|
// sends as the account rather than as a chat
|
|
@@ -166,15 +174,22 @@ const SITES = {
|
|
|
166
174
|
// the model host: the account's region (or a US host the sign-in named)
|
|
167
175
|
// selects the US deployment, else the SG one
|
|
168
176
|
apiOf: (a) => {
|
|
169
|
-
const h =
|
|
177
|
+
const h = traeHost(a?.api)
|
|
178
|
+
const n = h ? new URL(h).hostname : ""
|
|
170
179
|
const region = String(a?.region ?? "").toUpperCase()
|
|
171
|
-
if (/coreva/i.test(
|
|
172
|
-
if (/coresg/i.test(
|
|
180
|
+
if (/coreva/i.test(n) || /api-us(?:[-.]|$)/i.test(n) || /^US(?:[-_ ]|$)/.test(region)) return SITES["trae-global"].hosts.us
|
|
181
|
+
if (/coresg/i.test(n)) return h
|
|
182
|
+
// another trae.ai host than the site's own (www, api-…, grow…): one an
|
|
183
|
+
// enterprise (SaaS) sign-in named for its tenant
|
|
184
|
+
if (/\.trae\.ai$/i.test(n) && !/^(www\.|api[-.]|grow)/i.test(n)) return h
|
|
173
185
|
return SITES["trae-global"].hosts.api
|
|
174
186
|
},
|
|
175
187
|
// a sign-in names a host of Trae's, which can be an auth host
|
|
176
188
|
// (api-us-east), not the chat host
|
|
177
|
-
modelHost: (h) =>
|
|
189
|
+
modelHost: (h) => {
|
|
190
|
+
const t = traeHost(h)
|
|
191
|
+
return !!t && /(^|\.)trae\.ai$/i.test(new URL(t).hostname)
|
|
192
|
+
},
|
|
178
193
|
// Trae Global serves no own JSON pages through the account's fetch
|
|
179
194
|
ownPage: () => false,
|
|
180
195
|
// the account's region, from the sign-in's user info
|
|
@@ -213,6 +228,46 @@ const SITES = {
|
|
|
213
228
|
},
|
|
214
229
|
}
|
|
215
230
|
|
|
231
|
+
// traeHost is a host a sign-in named, when it is a well-formed https one
|
|
232
|
+
// on Trae's own domains (no port, no credentials), as origin plus any path;
|
|
233
|
+
// "" for anything else, which is then not taken
|
|
234
|
+
const TRAE_DOMAINS = ["trae.cn", "trae.ai", "mchost.guru"]
|
|
235
|
+
function traeHost(h) {
|
|
236
|
+
try {
|
|
237
|
+
const u = new URL(String(h ?? "").trim())
|
|
238
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || u.search || u.hash) return ""
|
|
239
|
+
const n = u.hostname.toLowerCase()
|
|
240
|
+
if (!TRAE_DOMAINS.some((d) => n === d || n.endsWith("." + d))) return ""
|
|
241
|
+
return (u.origin + u.pathname).replace(/\/+$/, "")
|
|
242
|
+
} catch {
|
|
243
|
+
return ""
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// authHostOf is the host a sign-in named for its tokens, when it named one
|
|
248
|
+
// on the site's own domain (an enterprise's may differ from the consumer
|
|
249
|
+
// one); "" when it named none, and the site's own is used
|
|
250
|
+
function authHostOf(h, site) {
|
|
251
|
+
const t = traeHost(h)
|
|
252
|
+
if (!t) return ""
|
|
253
|
+
const n = new URL(t).hostname
|
|
254
|
+
return n === site.webHost || n.endsWith("." + site.webHost) ? t : ""
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// accountOf keeps the account fields of the sign-in's user info that say
|
|
258
|
+
// what kind of account it is (tenant, enterprise, login type), plain values
|
|
259
|
+
// only and never anything that looks like a secret, for later use; nothing
|
|
260
|
+
// sends them
|
|
261
|
+
function accountOf(info) {
|
|
262
|
+
const out = {}
|
|
263
|
+
for (const [k, v] of Object.entries(info ?? {})) {
|
|
264
|
+
if (!/tenant|enterprise|saas|login|org|team|sso|^ent|usertype|accounttype/i.test(k)) continue
|
|
265
|
+
if (/token|secret|key|password|cookie|session|jwt/i.test(k)) continue
|
|
266
|
+
if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v
|
|
267
|
+
}
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
|
|
216
271
|
const SIGN_IN_TIMEOUT = 10 * 60 * 1000
|
|
217
272
|
const EARLY_MS = 2 * 60 * 1000 // a token this close to its end is renewed before a request
|
|
218
273
|
const LEAD_MS = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
@@ -301,6 +356,11 @@ function toAuth(s, site) {
|
|
|
301
356
|
// the model host the sign-in named, if it named one
|
|
302
357
|
...(site.modelHost(s.api) ? { api: s.api } : {}),
|
|
303
358
|
...(s.region ? { region: s.region } : {}),
|
|
359
|
+
// the host the sign-in named, its own token host if it named one, and
|
|
360
|
+
// what kind of account it is (an enterprise one's tenant), kept as given
|
|
361
|
+
...(traeHost(s.host) ? { host: traeHost(s.host) } : {}),
|
|
362
|
+
...(authHostOf(s.authHost, site) ? { authHost: authHostOf(s.authHost, site) } : {}),
|
|
363
|
+
...(s.account && Object.keys(s.account).length ? { account: s.account } : {}),
|
|
304
364
|
}
|
|
305
365
|
}
|
|
306
366
|
|
|
@@ -319,6 +379,9 @@ function fromAuth(auth, site) {
|
|
|
319
379
|
devicePrivateKey: auth.devicePrivateKey || "",
|
|
320
380
|
api: auth.api || "",
|
|
321
381
|
region: site.regionOf(auth),
|
|
382
|
+
host: auth.host || "",
|
|
383
|
+
authHost: authHostOf(auth.authHost, site),
|
|
384
|
+
account: auth.account && typeof auth.account === "object" ? auth.account : {},
|
|
322
385
|
}
|
|
323
386
|
}
|
|
324
387
|
|
|
@@ -405,6 +468,7 @@ async function exchange(refresh, clientId, site, host = site.hosts.auth) {
|
|
|
405
468
|
name: String(info.ScreenName ?? info.screenName ?? ""),
|
|
406
469
|
api: info.Host ?? "",
|
|
407
470
|
region: site.regionOf(info),
|
|
471
|
+
account: accountOf(info),
|
|
408
472
|
}
|
|
409
473
|
}
|
|
410
474
|
const e = errorOf(text)
|
|
@@ -495,12 +559,14 @@ async function boundExchange(a, site, host = site.hosts.auth) {
|
|
|
495
559
|
// before 0.2.3 (exchange). A device key made for the account goes with
|
|
496
560
|
// either, so the next renewal signs with the same one.
|
|
497
561
|
async function renewal(a, site) {
|
|
498
|
-
|
|
562
|
+
// the token host the sign-in named, else the site's
|
|
563
|
+
const host = authHostOf(a.authHost, site) || site.hosts.auth
|
|
564
|
+
if (!site.deviceExchange) return exchange(a.refresh, a.clientId, site, host)
|
|
499
565
|
const keys = a.devicePrivateKey && a.devicePublicKey ? {} : newDeviceKey()
|
|
500
566
|
try {
|
|
501
|
-
return await boundExchange({ ...a, ...keys }, site)
|
|
567
|
+
return await boundExchange({ ...a, ...keys }, site, host)
|
|
502
568
|
} catch {}
|
|
503
|
-
return { ...(await exchange(a.refresh, a.clientId, site)), ...keys }
|
|
569
|
+
return { ...(await exchange(a.refresh, a.clientId, site, host)), ...keys }
|
|
504
570
|
}
|
|
505
571
|
|
|
506
572
|
// ---- the browser sign-in --------------------------------------------------------
|
|
@@ -527,10 +593,15 @@ async function signedIn(q, device, site) {
|
|
|
527
593
|
name: String(info.ScreenName ?? info.screenName ?? ""),
|
|
528
594
|
api: host,
|
|
529
595
|
region: site.regionOf(info),
|
|
596
|
+
host,
|
|
597
|
+
// a token host the callback named, if it named one
|
|
598
|
+
authHost: authHostOf(q.get("authHost") || q.get("auth_host") || info.AuthHost || info.authHost, site),
|
|
599
|
+
account: accountOf(info),
|
|
530
600
|
}
|
|
601
|
+
const authHost = s.authHost || site.hosts.auth
|
|
531
602
|
if (!s.token && s.refresh) {
|
|
532
|
-
const x = await exchange(s.refresh, s.clientId, site)
|
|
533
|
-
s = { ...s, ...x, uid: x.uid || s.uid, name: x.name || s.name, api: x.api || s.api, region: x.region || s.region }
|
|
603
|
+
const x = await exchange(s.refresh, s.clientId, site, authHost)
|
|
604
|
+
s = { ...s, ...x, uid: x.uid || s.uid, name: x.name || s.name, api: x.api || s.api, region: x.region || s.region, host: s.host || x.api, account: { ...x.account, ...s.account } }
|
|
534
605
|
}
|
|
535
606
|
if (!s.token) throw new Error(`${site.webHost} sent back no token`)
|
|
536
607
|
if (!s.refresh) throw new Error(`${site.webHost} sent back no refresh token`)
|
|
@@ -539,7 +610,7 @@ async function signedIn(q, device, site) {
|
|
|
539
610
|
// one at once, so a check-in takes it; when Trae won't, it does as is
|
|
540
611
|
if (site.deviceExchange) {
|
|
541
612
|
try {
|
|
542
|
-
const x = await boundExchange(s, site)
|
|
613
|
+
const x = await boundExchange(s, site, authHost)
|
|
543
614
|
s = { ...s, token: x.token, refresh: x.refresh, expires: x.expires || s.expires }
|
|
544
615
|
} catch {}
|
|
545
616
|
}
|
|
@@ -616,7 +687,9 @@ async function browserSignIn(site) {
|
|
|
616
687
|
x_env: "",
|
|
617
688
|
x_app_version: site.ideVersion,
|
|
618
689
|
x_app_type: "stable",
|
|
619
|
-
|
|
690
|
+
// the enterprise (SaaS) sign-in is offered beside the personal one: an
|
|
691
|
+
// enterprise subscription signs in there (@shad70w0 on X)
|
|
692
|
+
hide_saas_login: "false",
|
|
620
693
|
})
|
|
621
694
|
return {
|
|
622
695
|
url: `${site.hosts.web}/authorization?${q}`,
|
|
@@ -2347,4 +2420,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2347
2420
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2348
2421
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2349
2422
|
|
|
2350
|
-
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
|
2423
|
+
export const _internal = { traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, 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.10",
|
|
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",
|