@magpie-community/opencode-kiro-auth 0.1.4 → 0.1.6
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 -2
- package/index.mjs +91 -24
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -39,8 +39,23 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
|
|
|
39
39
|
A browser sign-in is kept where OpenCode keeps sign-ins (`auth.json`; in
|
|
40
40
|
magpie, `plugin-auth.json`) as an `oauth` entry. The entry holds the
|
|
41
41
|
tokens, the profile ARN, the region and, for AWS, the registered client.
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
Refreshing such a sign-in:
|
|
43
|
+
|
|
44
|
+
- magpie renews the token 10 minutes before it expires, through the
|
|
45
|
+
plugin's `auth.refresh`, once for the account and before its requests,
|
|
46
|
+
models and usage need it. magpie saves the new token. A failed renewal,
|
|
47
|
+
even one Kiro refused, doesn't mark the account: it is tried again.
|
|
48
|
+
- OpenCode doesn't call that hook: there the token is refreshed 2 minutes
|
|
49
|
+
before it expires, before a request, and saved.
|
|
50
|
+
- Either way, a token Kiro turns down (a 403) is refreshed and the request
|
|
51
|
+
tried once more.
|
|
52
|
+
- Only one refresh runs at a time, magpie's renewal included, and a token
|
|
53
|
+
refreshed but not yet saved is used rather than spending the old refresh
|
|
54
|
+
token again.
|
|
55
|
+
- kiro-cli's and the IDE's sign-ins aren't renewed by magpie: they are
|
|
56
|
+
refreshed before a request, as above, and written back.
|
|
57
|
+
|
|
58
|
+
Tokens are refreshed through:
|
|
44
59
|
|
|
45
60
|
- Google/GitHub: through `prod.<region>.auth.desktop.kiro.dev/refreshToken`.
|
|
46
61
|
- Builder ID/Identity Center: through AWS's `oidc.<region>.amazonaws.com/token`.
|
package/index.mjs
CHANGED
|
@@ -188,13 +188,18 @@ async function saveBack(c) {
|
|
|
188
188
|
|
|
189
189
|
// ---- tokens --------------------------------------------------------------------
|
|
190
190
|
|
|
191
|
+
// EXPIRED is the built-in's words for a sign-in Kiro refused, or one with
|
|
192
|
+
// nothing to refresh it.
|
|
191
193
|
const EXPIRED = "Kiro's sign-in has expired; sign in again"
|
|
194
|
+
const EXPIRED_CLI = EXPIRED + " with `kiro-cli login` or the Kiro IDE"
|
|
192
195
|
|
|
193
196
|
// gone is an error saying the sign-in itself is gone — refused, or never
|
|
194
|
-
// there
|
|
195
|
-
//
|
|
197
|
+
// there. The built-in marked no Kiro account lapsed, not even then, nor
|
|
198
|
+
// cleared one: every answer and usage read says the account is kept.
|
|
196
199
|
const gone = (msg) => Object.assign(new Error(msg), { gone: true })
|
|
197
|
-
const
|
|
200
|
+
const EARLY_MS = 2 * 60 * 1000 // a token this close to its end is refreshed before a request
|
|
201
|
+
const LEAD_MS = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
202
|
+
const fresh = (c, early = EARLY_MS) => c.method === "apikey" || !c.expires || c.expires - Date.now() > early
|
|
198
203
|
|
|
199
204
|
async function post(url, contentType, body, headers = {}) {
|
|
200
205
|
let res
|
|
@@ -206,8 +211,8 @@ async function post(url, contentType, body, headers = {}) {
|
|
|
206
211
|
}
|
|
207
212
|
const text = await res.text()
|
|
208
213
|
if (!res.ok) {
|
|
209
|
-
if ([400, 401, 403].includes(res.status)) throw gone(
|
|
210
|
-
throw new Error(`refreshing Kiro's sign-in: ${res.status}
|
|
214
|
+
if ([400, 401, 403].includes(res.status)) throw gone(EXPIRED_CLI)
|
|
215
|
+
throw new Error(`refreshing Kiro's sign-in: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()) // Go's res.Status
|
|
211
216
|
}
|
|
212
217
|
return JSON.parse(text)
|
|
213
218
|
}
|
|
@@ -223,7 +228,7 @@ async function refresh(c) {
|
|
|
223
228
|
if (n && n.dbKey === c.dbKey && n.access !== c.access && fresh(n)) return n
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
|
-
if (!c.refresh) throw gone(
|
|
231
|
+
if (!c.refresh) throw gone(EXPIRED_CLI)
|
|
227
232
|
let out
|
|
228
233
|
switch (c.method) {
|
|
229
234
|
case "social": {
|
|
@@ -246,9 +251,9 @@ async function refresh(c) {
|
|
|
246
251
|
break
|
|
247
252
|
}
|
|
248
253
|
default:
|
|
249
|
-
throw gone(
|
|
254
|
+
throw gone("Kiro's sign-in has expired")
|
|
250
255
|
}
|
|
251
|
-
if (!out.access) throw gone(
|
|
256
|
+
if (!out.access) throw gone(EXPIRED_CLI)
|
|
252
257
|
const n = { ...c, access: out.access, expires: Date.now() + (out.expiresIn > 0 ? out.expiresIn : 3600) * 1000 }
|
|
253
258
|
if (out.refresh) n.refresh = out.refresh
|
|
254
259
|
await saveBack(n)
|
|
@@ -315,7 +320,7 @@ async function profileOf(c) {
|
|
|
315
320
|
signal: AbortSignal.timeout(20_000),
|
|
316
321
|
})
|
|
317
322
|
if (!res.ok) {
|
|
318
|
-
const msg = `Kiro didn't take the API key: ${res.status}
|
|
323
|
+
const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
|
|
319
324
|
throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
|
|
320
325
|
}
|
|
321
326
|
const arn = (await res.json().catch(() => null))?.profile?.arn
|
|
@@ -397,8 +402,23 @@ async function credOf(auth) {
|
|
|
397
402
|
// Account holds the credentials in use, refreshing them when near their
|
|
398
403
|
// end or when Kiro turned one down; one refresh at a time, as a refresh
|
|
399
404
|
// may replace the refresh token.
|
|
405
|
+
//
|
|
406
|
+
// after is what each refresh gave, by the access token it replaced: a
|
|
407
|
+
// sign-in read with that token (its save failed, or magpie hasn't yet
|
|
408
|
+
// saved what auth.refresh gave) goes on with the newer one rather than
|
|
409
|
+
// spending the replaced refresh token again.
|
|
400
410
|
function account(client) {
|
|
401
411
|
let held = null // { id, c }
|
|
412
|
+
const after = new Map()
|
|
413
|
+
const newest = (c) => {
|
|
414
|
+
for (let i = 0; i < 16 && after.has(c.access); i++) c = { ...after.get(c.access) }
|
|
415
|
+
return c
|
|
416
|
+
}
|
|
417
|
+
const renewed = (c, n) => {
|
|
418
|
+
after.set(c.access, n)
|
|
419
|
+
if (after.size > 16) after.delete(after.keys().next().value)
|
|
420
|
+
return n
|
|
421
|
+
}
|
|
402
422
|
let lock = Promise.resolve()
|
|
403
423
|
const locked = (fn) => {
|
|
404
424
|
const run = lock.then(fn, fn)
|
|
@@ -411,12 +431,12 @@ function account(client) {
|
|
|
411
431
|
await client.auth.set({ path: { id: ID }, body: { ...rest, type: "oauth", access: c.access, refresh: c.refresh, expires: c.expires,
|
|
412
432
|
profileArn: c.profile, region: c.region } }).catch(() => {})
|
|
413
433
|
}
|
|
414
|
-
|
|
434
|
+
const creds = (auth, stale = false) =>
|
|
415
435
|
locked(async () => {
|
|
416
436
|
const id = auth?.type === "api" ? "key:" + auth.key : "oauth:" + (auth?.source ?? "own")
|
|
417
437
|
// what its owner holds now: it may have refreshed it, signed out, or
|
|
418
438
|
// signed in to another account since
|
|
419
|
-
|
|
439
|
+
let read = await credOf(auth)
|
|
420
440
|
if (!read) {
|
|
421
441
|
held = null
|
|
422
442
|
throw gone(
|
|
@@ -425,14 +445,15 @@ function account(client) {
|
|
|
425
445
|
: "this Kiro account's sign-in is gone; add it again in magpie",
|
|
426
446
|
)
|
|
427
447
|
}
|
|
448
|
+
read = newest(read)
|
|
428
449
|
let c = held?.id === id ? held.c : null
|
|
429
450
|
if (!c || read.access !== c.access || !fresh(c) || stale) {
|
|
430
451
|
if (!read.profile && c && read.access === c.access) read.profile = c.profile
|
|
431
452
|
if (stale && c && read.access !== c.access && fresh(read)) stale = false // the owner refreshed it already
|
|
432
453
|
c = read
|
|
433
454
|
if (!fresh(c) || stale) {
|
|
434
|
-
if (c.method === "apikey") throw gone("Kiro turned down the API key")
|
|
435
|
-
c = await refresh(c)
|
|
455
|
+
if (c.method === "apikey") throw gone("Kiro turned down the API key saved on the provider")
|
|
456
|
+
c = renewed(c, await refresh(c))
|
|
436
457
|
await save(auth, c)
|
|
437
458
|
}
|
|
438
459
|
}
|
|
@@ -443,6 +464,25 @@ function account(client) {
|
|
|
443
464
|
held = { id, c }
|
|
444
465
|
return { token: c.access, tokenType: tokenType(c), profile: c.profile, region: regionOf(c.profile, c.region) }
|
|
445
466
|
})
|
|
467
|
+
// renew is auth.refresh's: a sign-in kept here (not kiro-cli's or the
|
|
468
|
+
// IDE's, which their owners keep) renewed under the same lock as the
|
|
469
|
+
// requests' refresh, so the two never spend one refresh token. It saves
|
|
470
|
+
// nothing — magpie saves what it gives — and gives only what changed.
|
|
471
|
+
creds.renew = (auth) =>
|
|
472
|
+
locked(async () => {
|
|
473
|
+
const read = await credOf(auth)
|
|
474
|
+
if (!read?.own || !read.refresh) return undefined
|
|
475
|
+
let c = newest(read)
|
|
476
|
+
// renewed here already (a request's refresh, or this hook's whose
|
|
477
|
+
// result magpie hasn't saved yet), the store not yet saying so
|
|
478
|
+
if (c.access === read.access || !fresh(c, LEAD_MS)) c = renewed(c, await refresh(c))
|
|
479
|
+
held = { id: "oauth:own", c }
|
|
480
|
+
const out = { access: c.access, expires: c.expires }
|
|
481
|
+
if (c.refresh !== auth.refresh) out.refresh = c.refresh
|
|
482
|
+
if (c.profile && c.profile !== auth.profileArn) out.profileArn = c.profile
|
|
483
|
+
return out
|
|
484
|
+
})
|
|
485
|
+
return creds
|
|
446
486
|
}
|
|
447
487
|
|
|
448
488
|
// ---- models ----------------------------------------------------------------------
|
|
@@ -712,7 +752,7 @@ function failure(status, body) {
|
|
|
712
752
|
const e = JSON.parse(body)
|
|
713
753
|
if (e?.message) msg = e.message + (e.reason ? ` (${e.reason})` : "")
|
|
714
754
|
} catch {}
|
|
715
|
-
msg ||=
|
|
755
|
+
msg ||= STATUS_TEXT[status] ?? ""
|
|
716
756
|
if (msg.includes("CONTENT_LENGTH_EXCEEDS_THRESHOLD") || msg.toLowerCase().includes("input is too long"))
|
|
717
757
|
return { status: 400, message: "input is too long for the model's context: " + msg }
|
|
718
758
|
if (msg.includes("INSUFFICIENT_MODEL_CAPACITY")) return { status: 503, message: msg }
|
|
@@ -723,12 +763,23 @@ function failure(status, body) {
|
|
|
723
763
|
const ERROR_TYPES = { 400: "invalid_request_error", 401: "authentication_error", 402: "billing_error", 403: "permission_error",
|
|
724
764
|
404: "not_found_error", 413: "request_too_large", 429: "rate_limit_error", 503: "overloaded_error", 529: "overloaded_error" }
|
|
725
765
|
const errorType = (status) => ERROR_TYPES[status] ?? "api_error"
|
|
766
|
+
// Each failure says X-Magpie-Sign-In: kept, as the built-in never marked
|
|
767
|
+
// an account lapsed (see gone).
|
|
726
768
|
const errorResponse = (status, message) =>
|
|
727
769
|
new Response(JSON.stringify({ type: "error", error: { type: errorType(status), message } }), {
|
|
728
770
|
status,
|
|
729
|
-
headers: { "Content-Type": "application/json" },
|
|
771
|
+
headers: { "Content-Type": "application/json", "X-Magpie-Sign-In": "kept" },
|
|
730
772
|
})
|
|
731
773
|
|
|
774
|
+
// kept is res saying the account is kept: a success too, which the
|
|
775
|
+
// built-in never took for a sign-in come back.
|
|
776
|
+
function kept(res) {
|
|
777
|
+
if (res.headers.has("X-Magpie-Sign-In")) return res
|
|
778
|
+
const headers = new Headers(res.headers)
|
|
779
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
780
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
781
|
+
}
|
|
782
|
+
|
|
732
783
|
// ---- the reply: an AWS event stream → Messages ---------------------------------------
|
|
733
784
|
|
|
734
785
|
// frames reads an AWS event stream: each message its length, its headers'
|
|
@@ -1087,7 +1138,9 @@ async function reply(it, model, stream) {
|
|
|
1087
1138
|
}
|
|
1088
1139
|
|
|
1089
1140
|
// generate answers a Messages request through Kiro's API: once more,
|
|
1090
|
-
// refreshed, when Kiro turns the token down (403).
|
|
1141
|
+
// refreshed, when Kiro turns the token down (403). As the built-in: no
|
|
1142
|
+
// credentials is a 401, a failed refresh after the 403 a 502; none marks
|
|
1143
|
+
// the account.
|
|
1091
1144
|
async function generate(creds, auth, req, signal) {
|
|
1092
1145
|
const model = String(req.model ?? "auto")
|
|
1093
1146
|
const budget = thinking(req, model)
|
|
@@ -1095,7 +1148,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1095
1148
|
try {
|
|
1096
1149
|
a = await creds(auth)
|
|
1097
1150
|
} catch (e) {
|
|
1098
|
-
return errorResponse(
|
|
1151
|
+
return errorResponse(401, e.message)
|
|
1099
1152
|
}
|
|
1100
1153
|
let res
|
|
1101
1154
|
try {
|
|
@@ -1109,7 +1162,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1109
1162
|
try {
|
|
1110
1163
|
a = await creds(auth, true)
|
|
1111
1164
|
} catch (e) {
|
|
1112
|
-
return errorResponse(
|
|
1165
|
+
return errorResponse(502, e.message)
|
|
1113
1166
|
}
|
|
1114
1167
|
try {
|
|
1115
1168
|
res = await sendKiro(a, buildKiro(req, model, a.profile, budget), signal)
|
|
@@ -1119,10 +1172,11 @@ async function generate(creds, auth, req, signal) {
|
|
|
1119
1172
|
}
|
|
1120
1173
|
}
|
|
1121
1174
|
if (!res.ok) {
|
|
1175
|
+
// Kiro's own 401 is passed on, as the built-in did, the sign-in kept
|
|
1122
1176
|
const f = failure(res.status, (await res.text()).slice(0, 1 << 20))
|
|
1123
1177
|
return errorResponse(f.status, f.message)
|
|
1124
1178
|
}
|
|
1125
|
-
return reply(events(res.body, model, budget), model, req.stream === true)
|
|
1179
|
+
return kept(await reply(events(res.body, model, budget), model, req.stream === true))
|
|
1126
1180
|
}
|
|
1127
1181
|
|
|
1128
1182
|
// ---- signing in ----------------------------------------------------------------------
|
|
@@ -1308,6 +1362,18 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1308
1362
|
return {
|
|
1309
1363
|
auth: {
|
|
1310
1364
|
provider: ID,
|
|
1365
|
+
// magpie renews a sign-in kept here LEAD_MS before its end, once for
|
|
1366
|
+
// the account, before its requests, models and usage ask for it; the
|
|
1367
|
+
// check before each request stays for OpenCode, which doesn't call
|
|
1368
|
+
// this. kiro-cli's and the IDE's sign-ins have no end magpie knows
|
|
1369
|
+
// (expires 0) and are never asked for.
|
|
1370
|
+
refreshLead: LEAD_MS,
|
|
1371
|
+
// A failure is thrown as it is, a refusal too: the built-in marked
|
|
1372
|
+
// no Kiro account lapsed (see gone).
|
|
1373
|
+
async refresh(auth) {
|
|
1374
|
+
if (auth?.type !== "oauth" || auth.source || !auth.access || !auth.refresh) return undefined
|
|
1375
|
+
return creds.renew(auth)
|
|
1376
|
+
},
|
|
1311
1377
|
async loader(getAuth) {
|
|
1312
1378
|
const auth = await getAuth()
|
|
1313
1379
|
if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return {}
|
|
@@ -1329,14 +1395,15 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1329
1395
|
},
|
|
1330
1396
|
}
|
|
1331
1397
|
},
|
|
1332
|
-
// the account's credits, as magpie's built-in showed them
|
|
1398
|
+
// the account's credits, as magpie's built-in showed them; a read,
|
|
1399
|
+
// clean or failed, marked the built-in's account nothing
|
|
1333
1400
|
async usage(getAuth) {
|
|
1334
1401
|
const auth = await getAuth()
|
|
1335
|
-
if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in" }
|
|
1402
|
+
if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in", signIn: "kept" }
|
|
1336
1403
|
try {
|
|
1337
|
-
return usageOf(await usageLimits(await creds(auth), true))
|
|
1404
|
+
return { ...usageOf(await usageLimits(await creds(auth), true)), signIn: "kept" }
|
|
1338
1405
|
} catch (e) {
|
|
1339
|
-
return { error: e.message }
|
|
1406
|
+
return { error: e.message, signIn: "kept" }
|
|
1340
1407
|
}
|
|
1341
1408
|
},
|
|
1342
1409
|
methods: [
|
|
@@ -1373,4 +1440,4 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1373
1440
|
}
|
|
1374
1441
|
|
|
1375
1442
|
// for tests
|
|
1376
|
-
export const _internal = { usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
|
|
1443
|
+
export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-kiro-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Kiro subscriptions (Free, Pro, Pro+, Power) 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", "kiro"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/kiro" }
|
|
11
12
|
}
|