@magpie-community/opencode-kiro-auth 0.1.5 → 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.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/index.mjs +53 -4
  3. 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
- Tokens are refreshed 2 minutes before they expire, and also when Kiro
43
- turns one down (a 403, after which the request is tried once more):
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
@@ -197,7 +197,9 @@ const EXPIRED_CLI = EXPIRED + " with `kiro-cli login` or the Kiro IDE"
197
197
  // there. The built-in marked no Kiro account lapsed, not even then, nor
198
198
  // cleared one: every answer and usage read says the account is kept.
199
199
  const gone = (msg) => Object.assign(new Error(msg), { gone: true })
200
- const fresh = (c) => c.method === "apikey" || !c.expires || c.expires - Date.now() > 2 * 60 * 1000
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
201
203
 
202
204
  async function post(url, contentType, body, headers = {}) {
203
205
  let res
@@ -400,8 +402,23 @@ async function credOf(auth) {
400
402
  // Account holds the credentials in use, refreshing them when near their
401
403
  // end or when Kiro turned one down; one refresh at a time, as a refresh
402
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.
403
410
  function account(client) {
404
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
+ }
405
422
  let lock = Promise.resolve()
406
423
  const locked = (fn) => {
407
424
  const run = lock.then(fn, fn)
@@ -414,12 +431,12 @@ function account(client) {
414
431
  await client.auth.set({ path: { id: ID }, body: { ...rest, type: "oauth", access: c.access, refresh: c.refresh, expires: c.expires,
415
432
  profileArn: c.profile, region: c.region } }).catch(() => {})
416
433
  }
417
- return (auth, stale = false) =>
434
+ const creds = (auth, stale = false) =>
418
435
  locked(async () => {
419
436
  const id = auth?.type === "api" ? "key:" + auth.key : "oauth:" + (auth?.source ?? "own")
420
437
  // what its owner holds now: it may have refreshed it, signed out, or
421
438
  // signed in to another account since
422
- const read = await credOf(auth)
439
+ let read = await credOf(auth)
423
440
  if (!read) {
424
441
  held = null
425
442
  throw gone(
@@ -428,6 +445,7 @@ function account(client) {
428
445
  : "this Kiro account's sign-in is gone; add it again in magpie",
429
446
  )
430
447
  }
448
+ read = newest(read)
431
449
  let c = held?.id === id ? held.c : null
432
450
  if (!c || read.access !== c.access || !fresh(c) || stale) {
433
451
  if (!read.profile && c && read.access === c.access) read.profile = c.profile
@@ -435,7 +453,7 @@ function account(client) {
435
453
  c = read
436
454
  if (!fresh(c) || stale) {
437
455
  if (c.method === "apikey") throw gone("Kiro turned down the API key saved on the provider")
438
- c = await refresh(c)
456
+ c = renewed(c, await refresh(c))
439
457
  await save(auth, c)
440
458
  }
441
459
  }
@@ -446,6 +464,25 @@ function account(client) {
446
464
  held = { id, c }
447
465
  return { token: c.access, tokenType: tokenType(c), profile: c.profile, region: regionOf(c.profile, c.region) }
448
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
449
486
  }
450
487
 
451
488
  // ---- models ----------------------------------------------------------------------
@@ -1325,6 +1362,18 @@ export async function KiroAuthPlugin({ client } = {}) {
1325
1362
  return {
1326
1363
  auth: {
1327
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
+ },
1328
1377
  async loader(getAuth) {
1329
1378
  const auth = await getAuth()
1330
1379
  if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return {}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.5",
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
  }