@magpie-community/opencode-cline-auth 0.1.0 → 0.1.1
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 +19 -9
- package/index.mjs +50 -11
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,15 +15,25 @@ Provider id: `cline`.
|
|
|
15
15
|
|
|
16
16
|
The sign-in is kept where OpenCode keeps auth (`auth.json` under the data
|
|
17
17
|
directory); magpie keeps plugin sign-ins in its own store
|
|
18
|
-
(`~/.config/magpie/plugin-auth.json`).
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
(`~/.config/magpie/plugin-auth.json`).
|
|
19
|
+
|
|
20
|
+
Refreshing:
|
|
21
|
+
- Access tokens are renewed through `/api/v1/auth/refresh`. Cline rotates
|
|
22
|
+
the refresh token and refuses one already spent, so the rotated pair is
|
|
23
|
+
kept in memory beside the store the moment it arrives: a save that fails
|
|
24
|
+
can't leave the next request spending the token that was already spent.
|
|
25
|
+
- magpie renews the token ten minutes before it ends, through the plugin's
|
|
26
|
+
`auth.refresh`, once for the account and before its requests, models and
|
|
27
|
+
usage need it; magpie saves the new pair. OpenCode doesn't call that hook:
|
|
28
|
+
there the token is refreshed five minutes before it ends, before a
|
|
29
|
+
request, and the new pair is saved.
|
|
30
|
+
- Refreshes are serialized, magpie's renewal included, so two at once can't
|
|
31
|
+
spend one token and lose the other; one that finds the token already
|
|
32
|
+
renewed uses that pair.
|
|
33
|
+
- A refresh that fails for a while keeps the token that is still good, as
|
|
34
|
+
Cline's own client does (magpie tries its renewal again shortly); only a
|
|
35
|
+
refresh Cline refuses, or a token that has actually expired, is treated as
|
|
36
|
+
the sign-in gone.
|
|
27
37
|
|
|
28
38
|
## Requests
|
|
29
39
|
|
package/index.mjs
CHANGED
|
@@ -21,7 +21,8 @@ const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
|
|
|
21
21
|
// signed-in one rides as "workos:<jwt>" (Cline's own clients send it so)
|
|
22
22
|
const WORKOS_PREFIX = "workos:"
|
|
23
23
|
|
|
24
|
-
const REFRESH_LEAD = 5 * 60 * 1000
|
|
24
|
+
const REFRESH_LEAD = 5 * 60 * 1000 // a token this close to its end is refreshed before a request
|
|
25
|
+
const RENEW_LEAD = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
25
26
|
// a refresh that failed transiently keeps a token with this much left, as
|
|
26
27
|
// Cline's getValidClineCredentials does (DEFAULT_RETRYABLE_TOKEN_GRACE_MS);
|
|
27
28
|
// only once it has actually expired is the sign-in the one to blame
|
|
@@ -692,6 +693,20 @@ export const ClinePlugin = async ({ client } = {}) => {
|
|
|
692
693
|
return run
|
|
693
694
|
}
|
|
694
695
|
|
|
696
|
+
// spend trades r's refresh token for a new pair and holds it, whatever a
|
|
697
|
+
// save then does, against the token it spent and the one getAuth handed
|
|
698
|
+
// back (a's): the next request offering either of them finds this chain's
|
|
699
|
+
// newest. Called under the lock only.
|
|
700
|
+
const spend = async (a, r) => {
|
|
701
|
+
const p = await refresh(r)
|
|
702
|
+
const next = { ...r, access: p.access, refresh: p.refresh, expires: p.expires, uid: p.uid || r.uid, email: p.email || r.email, name: p.name || r.name, accountId: r.accountId || p.accountId }
|
|
703
|
+
const chain = held.get(a.refresh) ?? {}
|
|
704
|
+
Object.assign(chain, { access: next.access, refresh: next.refresh, expires: next.expires })
|
|
705
|
+
held.set(a.refresh, chain)
|
|
706
|
+
held.set(r.refresh, chain)
|
|
707
|
+
return next
|
|
708
|
+
}
|
|
709
|
+
|
|
695
710
|
// fresh is the account with a token still good: one near its end is
|
|
696
711
|
// renewed, and the rotated pair saved back. A transient refresh failure
|
|
697
712
|
// keeps the token that is still good, as Cline's own client does; only a
|
|
@@ -711,23 +726,15 @@ export const ClinePlugin = async ({ client } = {}) => {
|
|
|
711
726
|
const r = h ? { ...a, access: h.access, refresh: h.refresh, expires: h.expires } : a
|
|
712
727
|
if (r.access && r.expires - Date.now() > REFRESH_LEAD) return { ...r, bearer: bearerOf(r), renewed: false }
|
|
713
728
|
if (!r.refresh) throw new Lapsed("Cline's access token has expired and there is no refresh token; sign in to Cline again")
|
|
714
|
-
let
|
|
729
|
+
let next
|
|
715
730
|
try {
|
|
716
|
-
|
|
731
|
+
next = await spend(a, r)
|
|
717
732
|
} catch (e) {
|
|
718
733
|
// a token that is still good rides on through a refresh that
|
|
719
734
|
// failed for a while; an expired one is the sign-in gone
|
|
720
735
|
if (!e?.expired && r.access && r.expires - Date.now() > RETRYABLE_GRACE) return { ...r, bearer: bearerOf(r), renewed: false }
|
|
721
736
|
throw e
|
|
722
737
|
}
|
|
723
|
-
const next = { ...r, access: p.access, refresh: p.refresh, expires: p.expires, uid: p.uid || r.uid, email: p.email || r.email, name: p.name || r.name, accountId: r.accountId || p.accountId }
|
|
724
|
-
// the pair just rotated, held whatever the save through magpie does,
|
|
725
|
-
// against the token it spent and the one getAuth handed back: the
|
|
726
|
-
// next request offering either of them finds this chain's newest
|
|
727
|
-
const chain = held.get(a.refresh) ?? {}
|
|
728
|
-
Object.assign(chain, { access: next.access, refresh: next.refresh, expires: next.expires })
|
|
729
|
-
held.set(a.refresh, chain)
|
|
730
|
-
held.set(r.refresh, chain)
|
|
731
738
|
await remember(next)
|
|
732
739
|
const cred = { ...next, bearer: bearerOf(next), renewed: true }
|
|
733
740
|
renewals.add(cred)
|
|
@@ -782,6 +789,38 @@ export const ClinePlugin = async ({ client } = {}) => {
|
|
|
782
789
|
auth: {
|
|
783
790
|
provider: PROVIDER,
|
|
784
791
|
|
|
792
|
+
// magpie renews the sign-in RENEW_LEAD before its end, once for the
|
|
793
|
+
// account, before its requests, models and usage ask for it; the
|
|
794
|
+
// check in fresh before each request stays for OpenCode, which
|
|
795
|
+
// doesn't call this. It takes the same lock and the same held pairs
|
|
796
|
+
// as fresh, so the two never spend one refresh token twice. magpie
|
|
797
|
+
// saves what this gives (it is merged over the sign-in): only what
|
|
798
|
+
// changed comes back, and remember isn't called here.
|
|
799
|
+
refreshLead: RENEW_LEAD,
|
|
800
|
+
refresh: (auth) =>
|
|
801
|
+
locked(async () => {
|
|
802
|
+
const a = authOf(auth)
|
|
803
|
+
// an API key, or a sign-in with nothing to renew with
|
|
804
|
+
if (auth?.type !== "oauth" || !a.refresh) return undefined
|
|
805
|
+
// renewed here already (a request's refresh), the store not
|
|
806
|
+
// yet saying so: that pair, not another spend
|
|
807
|
+
const h = held.get(a.refresh)
|
|
808
|
+
const r = h ? { ...a, access: h.access, refresh: h.refresh, expires: h.expires } : a
|
|
809
|
+
if (h && r.access && r.expires - Date.now() > RENEW_LEAD) return { access: r.access, refresh: r.refresh, expires: r.expires }
|
|
810
|
+
let next
|
|
811
|
+
try {
|
|
812
|
+
next = await spend(a, r)
|
|
813
|
+
} catch (e) {
|
|
814
|
+
// a refused refresh is the sign-in gone; anything else is
|
|
815
|
+
// retried by magpie, the token in hand riding on meanwhile
|
|
816
|
+
if (e?.expired) throw Object.assign(new Error(String(e.message ?? e).replace(/^Cline: /, "")), { signIn: "expired" })
|
|
817
|
+
throw e
|
|
818
|
+
}
|
|
819
|
+
const out = { access: next.access, refresh: next.refresh, expires: next.expires }
|
|
820
|
+
for (const k of ["uid", "email", "name", "accountId"]) if (next[k] && next[k] !== a[k]) out[k] = next[k]
|
|
821
|
+
return out
|
|
822
|
+
}),
|
|
823
|
+
|
|
785
824
|
async loader(getAuth) {
|
|
786
825
|
const a = await getAuth()
|
|
787
826
|
if (a?.type !== "oauth" && a?.type !== "api") return {}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cline-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cline (cline.bot) on your own account — usage billing, ClinePass and the free models — in OpenCode and magpie",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "magpie", "cline", "clinepass"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/cline" }
|
|
11
12
|
}
|