@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.
- package/README.md +17 -6
- package/index.mjs +93 -26
- 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
|
|
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
|
|
27
|
-
its
|
|
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
|
|
43
|
-
|
|
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
|
-
//
|
|
328
|
-
// key
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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, ...
|
|
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 (
|
|
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 &&
|
|
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
|
-
|
|
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, ...
|
|
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
|
|
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
|
-
|
|
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
|
}
|