@magpie-community/opencode-kiro-auth 0.1.14 → 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 +6 -2
  2. package/index.mjs +70 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,8 +46,12 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
46
46
  kiro-cli refreshes its own token first (`kiro-cli debug refresh-auth-token`).
47
47
  A token the plugin refreshes itself is written back, so kiro-cli and the
48
48
  IDE go on with it.
49
- - **Kiro API key (`ksk_…`)**: sent with `tokentype: API_KEY`. Its profile is
50
- 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.
51
55
 
52
56
  A browser sign-in is kept where OpenCode keeps sign-ins (`auth.json`; in
53
57
  magpie, `plugin-auth.json`) as an `oauth` entry. The entry holds the
package/index.mjs CHANGED
@@ -333,25 +333,48 @@ async function managementCall(region, a, method, query, body) {
333
333
  return JSON.parse(text)
334
334
  }
335
335
 
336
- // profileOf finds the profile a sign-in that doesn't name one uses: an API
337
- // key's own, Builder ID's, or the first Kiro lists in either of its regions.
338
- async function profileOf(c) {
339
- if (c.method === "apikey") {
340
- const res = await fetch(management("us-east-1") + "/", {
341
- method: "POST",
342
- headers: { ...authHeaders({ token: c.access, tokenType: "API_KEY" }), "Content-Type": "application/x-amz-json-1.0",
343
- "X-Amz-Target": "AmazonCodeWhispererService.GetProfile" },
344
- body: "{}",
345
- signal: AbortSignal.timeout(20_000),
346
- })
347
- if (!res.ok) {
348
- const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
349
- throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
350
- }
351
- const arn = (await res.json().catch(() => null))?.profile?.arn
352
- if (!arn) throw new Error("Kiro didn't say which profile the API key is for")
353
- 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 {}
354
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) {
355
378
  if (c.builderID) return BUILDER_ID_PROFILE
356
379
  let last
357
380
  for (const region of ["us-east-1", "eu-central-1"]) {
@@ -370,6 +393,8 @@ async function profileOf(c) {
370
393
 
371
394
  // usageLimits is Get-Usage-Limits: the plan, the account's email, credits.
372
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" })
373
398
  return managementCall(a.region, a, "Get-Usage-Limits",
374
399
  { origin: "KIRO_CLI", profileArn: a.profile, resourceType: "CREDIT", isEmailRequired: String(email) })
375
400
  }
@@ -497,7 +522,12 @@ function account(client) {
497
522
  saved = true
498
523
  }
499
524
  }
500
- 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) {
501
531
  c.profile = await profileOf(c)
502
532
  if (!auth?.profileArn && !saved) {
503
533
  await save(auth, c)
@@ -510,7 +540,10 @@ function account(client) {
510
540
  await save(auth, c)
511
541
  }
512
542
  held = { id, c }
513
- 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) }
514
547
  })
515
548
  // renew is auth.refresh's: a sign-in kept here (not kiro-cli's or the
516
549
  // IDE's, which their owners keep) renewed under the same lock as the
@@ -584,9 +617,25 @@ function runtimeModel(m) {
584
617
  // picks a model for each request.
585
618
  const MODELS = [{ id: "auto", name: "Auto" }]
586
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
+
587
636
  // listModels is List-Available-Models, the default first.
588
637
  async function listModels(a) {
589
- 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 })
590
639
  const list = []
591
640
  for (const m of out?.models ?? []) {
592
641
  if (!m?.modelId) continue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.14",
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",