@magpie-community/opencode-kiro-auth 0.1.13 → 0.1.15

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 -6
  2. package/index.mjs +93 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,14 +17,21 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
17
17
  - Builder ID and Identity Center come back with an AWS sign-in URL and
18
18
  region. The plugin registers a client with AWS's OIDC service, sends
19
19
  the browser to AWS, and trades the code AWS returns.
20
- - The account is named by the email Kiro has for it (`Get-Usage-Limits`),
21
- and its plan is read from the same call.
20
+ - The account is named by the email Kiro has for it (`Get-Usage-Limits`)
21
+ and the way it was signed in to: `email (Google)`, `email (GitHub)`,
22
+ `email (Builder ID)`, `email (IAM Identity Center)`, `email
23
+ (Internal)`. Its plan is read from the same call, and its usage names
24
+ it the same way.
22
25
  - Kiro keeps a Google, a GitHub, a Builder ID and an Identity Center
23
26
  sign-in of one email as different accounts. The sign-in carries a `uid`
24
27
  (`google`, `github`, `builderid`, `enterprise`), so magpie keeps each one
25
28
  beside the others, and a sign-in again the same way replaces only its
26
- own account. An account signed in before 0.1.13 is given its `uid` on
27
- its first request, model listing, usage read or renewal.
29
+ own account. An account signed in before 0.1.13 is given its `uid`,
30
+ and one named by its bare email before 0.1.14 its name, on its first
31
+ request, model listing, usage read or renewal. Until then, a sign-in
32
+ again the same way goes beside it rather than replacing it: magpie
33
+ matches a sign-in by its name, and the plugin can't see the other
34
+ accounts to match it by `uid`.
28
35
  - A company's own identity provider (`external_idp`) is not supported
29
36
  here. Sign in with `kiro-cli login` and use the next method instead.
30
37
  - **Kiro CLI's or Kiro IDE's sign-in**: uses the account kiro-cli is signed
@@ -39,8 +46,12 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
39
46
  kiro-cli refreshes its own token first (`kiro-cli debug refresh-auth-token`).
40
47
  A token the plugin refreshes itself is written back, so kiro-cli and the
41
48
  IDE go on with it.
42
- - **Kiro API key (`ksk_…`)**: sent with `tokentype: API_KEY`. Its profile is
43
- found with `GetProfile`.
49
+ - **Kiro API key (`ksk_…`)**: sent with `tokentype: API_KEY`, as kiro-cli
50
+ sends it. `GetProfile` is asked in us-east-1, then eu-central-1, for the
51
+ region the key's profile is in; a key it finds none for is used in
52
+ us-east-1, as kiro-cli uses it. Its models are listed with the control
53
+ plane's `ListAvailableModels` and its credits with `GetUsageLimits`, and
54
+ no profile is named in its calls, as kiro-cli names none.
44
55
 
45
56
  A browser sign-in is kept where OpenCode keeps sign-ins (`auth.json`; in
46
57
  magpie, `plugin-auth.json`) as an `oauth` entry. The entry holds the
package/index.mjs CHANGED
@@ -56,6 +56,15 @@ const isBuilderID = (provider) => String(provider ?? "").toLowerCase() === "buil
56
56
  // keeps a sign-in of an email already signed in as that account unless
57
57
  // both have a uid and the two differ (#309). "" when it isn't known.
58
58
  const uidOf = (loginProvider) => String(loginProvider ?? "").trim().toLowerCase()
59
+ // nameOf is what an account is called: its email and the way it was signed
60
+ // in to, as Kiro's sign-in page names it, so two accounts of one email read
61
+ // apart (#309; magpie names a plugin's account by its accountId). A way not
62
+ // known, kiro-cli's or the Kiro IDE's sign-in among them, leaves the email
63
+ // as it is, and magpie tells two such apart by their slot.
64
+ const WAYS = { google: "Google", github: "GitHub", builderid: "Builder ID", enterprise: "IAM Identity Center", internal: "Internal" }
65
+ const nameOf = (email, loginProvider) => (WAYS[uidOf(loginProvider)] ? `${email} (${WAYS[uidOf(loginProvider)]})` : email)
66
+ // a bare email: what an account was called before nameOf
67
+ const bareEmail = (id) => typeof id === "string" && /^[^\s()]+@[^\s()]+$/.test(id)
59
68
 
60
69
  const env = (k) => process.env[k] ?? ""
61
70
  const home = () => env("HOME") || homedir()
@@ -324,25 +333,48 @@ async function managementCall(region, a, method, query, body) {
324
333
  return JSON.parse(text)
325
334
  }
326
335
 
327
- // profileOf finds the profile a sign-in that doesn't name one uses: an API
328
- // key's own, Builder ID's, or the first Kiro lists in either of its regions.
329
- async function profileOf(c) {
330
- if (c.method === "apikey") {
331
- const res = await fetch(management("us-east-1") + "/", {
332
- method: "POST",
333
- headers: { ...authHeaders({ token: c.access, tokenType: "API_KEY" }), "Content-Type": "application/x-amz-json-1.0",
334
- "X-Amz-Target": "AmazonCodeWhispererService.GetProfile" },
335
- body: "{}",
336
- signal: AbortSignal.timeout(20_000),
337
- })
338
- if (!res.ok) {
339
- const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
340
- throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
341
- }
342
- const arn = (await res.json().catch(() => null))?.profile?.arn
343
- if (!arn) throw new Error("Kiro didn't say which profile the API key is for")
344
- return arn
336
+ // controlCall is one of Kiro's management operations as kiro-cli sends it
337
+ // for an API key: AWS JSON 1.0, the operation named in X-Amz-Target, POSTed
338
+ // to the endpoint's root (with ?origin=KIRO_CLI where kiro-cli adds it).
339
+ async function controlCall(region, a, target, body, query) {
340
+ const res = await fetch(management(region) + "/" + (query ? "?" + new URLSearchParams(query) : ""), {
341
+ method: "POST",
342
+ headers: { ...authHeaders(a), "Content-Type": "application/x-amz-json-1.0", "X-Amz-Target": target,
343
+ "x-amzn-codewhisperer-optout": "false" },
344
+ body: JSON.stringify(body),
345
+ signal: AbortSignal.timeout(20_000),
346
+ })
347
+ const text = await res.text()
348
+ if (!res.ok) throw new KiroError(res.status, text)
349
+ return JSON.parse(text)
350
+ }
351
+
352
+ // KEY_REGIONS are where kiro-cli asks GetProfile for an API key's profile,
353
+ // in its order (the GovCloud and ISO ones after them aren't Kiro's
354
+ // subscriptions).
355
+ const KEY_REGIONS = ["us-east-1", "eu-central-1"]
356
+
357
+ // keyProfile is an API key's profile, as kiro-cli finds it: GetProfile in
358
+ // each region until one answers with a profile. kiro-cli goes on without
359
+ // one when every region turns it down (a key from an account with no
360
+ // CodeWhisperer profile answers 400), and so does this: "" then, and the
361
+ // key's calls are made in us-east-1 without a profile, as kiro-cli makes
362
+ // them (Hollowhere, on magpie's Discord: a key kiro-cli 2.33.1 served
363
+ // failed every request here with "Kiro didn't take the API key: 400").
364
+ async function keyProfile(c) {
365
+ for (const region of KEY_REGIONS) {
366
+ try {
367
+ const arn = (await controlCall(region, { token: c.access, tokenType: "API_KEY" }, "AmazonCodeWhispererService.GetProfile", {}))
368
+ ?.profile?.arn
369
+ if (arn) return arn
370
+ } catch {}
345
371
  }
372
+ return ""
373
+ }
374
+
375
+ // profileOf finds the profile a sign-in that doesn't name one uses:
376
+ // Builder ID's, or the first Kiro lists in either of its regions.
377
+ async function profileOf(c) {
346
378
  if (c.builderID) return BUILDER_ID_PROFILE
347
379
  let last
348
380
  for (const region of ["us-east-1", "eu-central-1"]) {
@@ -361,6 +393,8 @@ async function profileOf(c) {
361
393
 
362
394
  // usageLimits is Get-Usage-Limits: the plan, the account's email, credits.
363
395
  function usageLimits(a, email) {
396
+ // an API key's, as kiro-cli asks for it: no profile, no email asked
397
+ if (a.tokenType === "API_KEY") return controlCall(a.region, a, "AmazonCodeWhispererService.GetUsageLimits", { origin: "KIRO_CLI" }, { origin: "KIRO_CLI" })
364
398
  return managementCall(a.region, a, "Get-Usage-Limits",
365
399
  { origin: "KIRO_CLI", profileArn: a.profile, resourceType: "CREDIT", isEmailRequired: String(email) })
366
400
  }
@@ -448,11 +482,17 @@ function account(client) {
448
482
  // sign-in with another way to the same email no longer replaces it
449
483
  const uidSaved = new Set()
450
484
  const uidField = (auth) => (!auth?.uid && uidOf(auth?.loginProvider) ? { uid: uidOf(auth.loginProvider) } : {})
485
+ // and one called by its bare email is given its name, the way it was
486
+ // signed in to with it
487
+ const nameField = (auth) =>
488
+ bareEmail(auth?.accountId) && nameOf(auth.accountId, auth.loginProvider) !== auth.accountId
489
+ ? { accountId: nameOf(auth.accountId, auth.loginProvider) } : {}
490
+ const idFields = (auth) => ({ ...uidField(auth), ...nameField(auth) })
451
491
  const save = async (auth, c) => {
452
492
  if (!c.own || !client?.auth?.set) return
453
493
  const { type: _t, ...rest } = auth
454
494
  await client.auth.set({ path: { id: ID }, body: { ...rest, type: "oauth", access: c.access, refresh: c.refresh, expires: c.expires,
455
- profileArn: c.profile, region: c.region, ...uidField(auth) } }).catch(() => {})
495
+ profileArn: c.profile, region: c.region, ...idFields(auth) } }).catch(() => {})
456
496
  }
457
497
  const creds = (auth, stale = false) =>
458
498
  locked(async () => {
@@ -482,7 +522,12 @@ function account(client) {
482
522
  saved = true
483
523
  }
484
524
  }
485
- if (!c.profile) {
525
+ if (c.method === "apikey") {
526
+ if (!c.asked) {
527
+ c.profile = await keyProfile(c)
528
+ c.asked = true
529
+ }
530
+ } else if (!c.profile) {
486
531
  c.profile = await profileOf(c)
487
532
  if (!auth?.profileArn && !saved) {
488
533
  await save(auth, c)
@@ -490,12 +535,15 @@ function account(client) {
490
535
  }
491
536
  }
492
537
  // once per token: a save that didn't take isn't tried on every request
493
- if (!saved && c.own && uidField(auth).uid && !uidSaved.has(c.access)) {
538
+ if (!saved && c.own && Object.keys(idFields(auth)).length && !uidSaved.has(c.access)) {
494
539
  uidSaved.add(c.access)
495
540
  await save(auth, c)
496
541
  }
497
542
  held = { id, c }
498
- return { token: c.access, tokenType: tokenType(c), profile: c.profile, region: regionOf(c.profile, c.region) }
543
+ // kiro-cli names no profile in an API key's calls, even one GetProfile
544
+ // found; the profile only says the key's region
545
+ const profile = c.method === "apikey" ? "" : c.profile
546
+ return { token: c.access, tokenType: tokenType(c), profile, region: regionOf(c.profile, c.region) }
499
547
  })
500
548
  // renew is auth.refresh's: a sign-in kept here (not kiro-cli's or the
501
549
  // IDE's, which their owners keep) renewed under the same lock as the
@@ -513,7 +561,7 @@ function account(client) {
513
561
  const out = { access: c.access, expires: c.expires }
514
562
  if (c.refresh !== auth.refresh) out.refresh = c.refresh
515
563
  if (c.profile && c.profile !== auth.profileArn) out.profileArn = c.profile
516
- return { ...out, ...uidField(auth) }
564
+ return { ...out, ...idFields(auth) }
517
565
  })
518
566
  return creds
519
567
  }
@@ -569,9 +617,25 @@ function runtimeModel(m) {
569
617
  // picks a model for each request.
570
618
  const MODELS = [{ id: "auto", name: "Auto" }]
571
619
 
620
+ // keyModels is an API key's list, asked as kiro-cli asks it: Kiro's
621
+ // control plane's ListAvailableModels, with no profile, each page.
622
+ async function keyModels(a) {
623
+ const out = { models: [] }
624
+ let next
625
+ for (let i = 0; i < 20; i++) {
626
+ const page = await controlCall(a.region, a, "KiroControlPlaneBearerService.ListAvailableModels",
627
+ { origin: "KIRO_CLI", ...(next ? { nextToken: next } : {}) }, { origin: "KIRO_CLI" })
628
+ out.models.push(...(page?.models ?? []))
629
+ out.defaultModel ??= page?.defaultModel
630
+ next = page?.nextToken
631
+ if (!next) break
632
+ }
633
+ return out
634
+ }
635
+
572
636
  // listModels is List-Available-Models, the default first.
573
637
  async function listModels(a) {
574
- const out = await managementCall(a.region, a, "List-Available-Models", { origin: "KIRO_CLI", profileArn: a.profile })
638
+ const out = a.tokenType === "API_KEY" ? await keyModels(a) : await managementCall(a.region, a, "List-Available-Models", { origin: "KIRO_CLI", profileArn: a.profile })
575
639
  const list = []
576
640
  for (const m of out?.models ?? []) {
577
641
  if (!m?.modelId) continue
@@ -1423,7 +1487,7 @@ async function browserSignIn() {
1423
1487
  return fail(e.message)
1424
1488
  }
1425
1489
  const who = await whoIs(c)
1426
- const user = who.email || "Kiro account"
1490
+ const user = who.email ? nameOf(who.email, c.loginProvider) : "Kiro account"
1427
1491
  finish({ type: "success", provider: ID, refresh: c.refresh, access: c.access, expires: c.expires, accountId: user,
1428
1492
  ...(uidOf(c.loginProvider) ? { uid: uidOf(c.loginProvider) } : {}),
1429
1493
  method: c.method, loginProvider: c.loginProvider, region: c.region, profileArn: who.profile,
@@ -1515,7 +1579,10 @@ export async function KiroAuthPlugin({ client } = {}) {
1515
1579
  const auth = await getAuth()
1516
1580
  if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in", signIn: "kept" }
1517
1581
  try {
1518
- return { ...usageOf(await usageLimits(await creds(auth), true)), signIn: "kept" }
1582
+ const u = usageOf(await usageLimits(await creds(auth), true))
1583
+ // named as the account is
1584
+ if (u.user && auth.type === "oauth" && !auth.source) u.user = nameOf(u.user, auth.loginProvider)
1585
+ return { ...u, signIn: "kept" }
1519
1586
  } catch (e) {
1520
1587
  return { error: e.message, signIn: "kept" }
1521
1588
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",