@magpie-community/opencode-kiro-auth 0.1.20 → 0.1.22

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 +26 -0
  2. package/index.mjs +218 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,23 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
6
6
 
7
7
  ## Signing in
8
8
 
9
+ Choose a sign-in method before opening the browser:
10
+
11
+ - **AWS Builder ID** opens AWS directly for a personal account.
12
+ - **IAM Identity Center (organization Start URL)** asks for the organization's
13
+ HTTPS Start URL and AWS region (blank defaults to `us-east-1`), then opens
14
+ that organization's AWS sign-in directly. Use the region of the directory.
15
+ - **Kiro sign-in page** retains the portal for Google, GitHub and other Kiro
16
+ sign-in methods. Callers without prompt inputs retain this behavior.
17
+
18
+ The two direct AWS options skip the portal's first loopback callback. When
19
+ Magpie runs remotely or in Docker, sign in and authorize, then paste the
20
+ complete final `http://127.0.0.1:<port>/oauth/callback?...` address into Magpie's
21
+ Callback URL field. The page need not load on the browser's computer. Local
22
+ browsers that can reach the plugin's listener finish automatically. Direct
23
+ AWS sign-in still uses authorization code + PKCE, not device authorization.
24
+
25
+
9
26
  - **Kiro (Google, GitHub, AWS Builder ID, IAM Identity Center)**: the Kiro
10
27
  IDE's own sign-in.
11
28
  - Kiro's page (`app.kiro.dev/signin`) sends the browser back to one of
@@ -91,6 +108,10 @@ request:
91
108
  profile ARN names.
92
109
  - It turns the request into a Kiro conversation:
93
110
  - The system prompt goes at the head of the first message.
111
+ - System messages that end the conversation (Claude Code's environment
112
+ context) are sent as a turn before the question, answered by Kiro IDE's
113
+ "Understood.", so the question is the message answered (#538). After
114
+ tool results they stay with them.
94
115
  - Tool calls and results are paired: an unanswered call gets an error
95
116
  result, and a result longer than 250,000 characters is cut.
96
117
  - Tool ids Kiro wouldn't take are rewritten. A tool name over 64
@@ -119,6 +140,11 @@ request:
119
140
  the prompt, with a budget: low 10k, medium 20k, high 30k, xhigh and max
120
141
  50k (as magpie's built-in Kiro: #120). The
121
142
  `<thinking>` block of the reply comes back as thinking.
143
+ A model whose Kiro catalog lists effort levels with adaptive thinking
144
+ (`additionalModelRequestFieldsSchema`) is sent the level instead, as Kiro
145
+ IDE sends it: `additionalModelRequestFields` with `thinking.type:
146
+ "adaptive"` and `output_config.effort` (or `reasoning.effort`), and no
147
+ budget. So xhigh and max stay apart there (#538).
122
148
  - Anthropic's hosted web search is answered with Kiro's own web search
123
149
  (see [Web search](#web-search)).
124
150
  - Kiro's failures keep their meaning:
package/index.mjs CHANGED
@@ -574,6 +574,24 @@ function account(client) {
574
574
  // ---- models ----------------------------------------------------------------------
575
575
 
576
576
  const windows = new Map() // a model's context, as Kiro last listed it
577
+ const natives = new Map() // a model's effort levels sent as fields, as Kiro last listed them (nativeOf)
578
+
579
+ // nativeOf is what a model's additionalModelRequestFieldsSchema in Kiro's
580
+ // list offers, read as Kiro IDE reads it: the effort levels in the enum of
581
+ // output_config's (or reasoning's) effort, and whether thinking.type may be
582
+ // "adaptive". A model offering both is sent the caller's effort in
583
+ // additionalModelRequestFields, as the IDE sends it; any other keeps the
584
+ // <max_thinking_length> budget (#538).
585
+ function nativeOf(s) {
586
+ if (!s || typeof s !== "object" || Array.isArray(s)) return undefined
587
+ const types = s.properties?.thinking?.properties?.type?.enum
588
+ if (!Array.isArray(types) || !types.includes("adaptive")) return undefined
589
+ for (const path of ["output_config", "reasoning"]) {
590
+ const levels = s.properties?.[path]?.properties?.effort?.enum
591
+ if (Array.isArray(levels) && levels.length) return { path, levels }
592
+ }
593
+ return undefined
594
+ }
577
595
 
578
596
  const thinks = (id) => /claude/i.test(id) || id === "auto"
579
597
  // the levels a thinking model offers, as the built-in thinks them (xhigh as
@@ -653,6 +671,7 @@ async function listModels(a) {
653
671
  }
654
672
  if (!list.length) throw new Error("Kiro listed no models")
655
673
  for (const m of list) windows.set(m.id, m.context)
674
+ for (const m of out?.models ?? []) if (m?.modelId) natives.set(m.modelId, nativeOf(m.additionalModelRequestFieldsSchema))
656
675
  return list
657
676
  }
658
677
 
@@ -834,6 +853,17 @@ function thinking(req, model) {
834
853
  return 20000
835
854
  }
836
855
 
856
+ // nativeFields is the additionalModelRequestFields a thinking request is
857
+ // sent with when Kiro's list says the model takes its effort as a field:
858
+ // adaptive thinking at the caller's level, so xhigh and max reach the model
859
+ // as two levels rather than one 50000 budget (#538). undefined otherwise.
860
+ function nativeFields(req, model) {
861
+ const n = natives.get(model)
862
+ const effort = req.output_config?.effort
863
+ if (!n || typeof effort !== "string" || !n.levels.includes(effort) || !thinking(req, model)) return undefined
864
+ return { thinking: { type: "adaptive" }, [n.path]: { effort } }
865
+ }
866
+
837
867
  // formatText is the structured output a request asks for, said in words:
838
868
  // Anthropic's output_config.format (or the AI SDK's output_format) with a
839
869
  // JSON schema, "" when it asks for none. A JSON object with no schema
@@ -872,6 +902,34 @@ async function kiroBody(req, model, profile, budget) {
872
902
  return JSON.stringify(await fitImages(kiroRequest(req, model, profile, budget)))
873
903
  }
874
904
 
905
+ // UNDERSTOOD is the reply Kiro IDE puts in its history where a turn has
906
+ // none of its own, before the message being answered
907
+ const UNDERSTOOD = "Understood."
908
+
909
+ // contextFirst is a conversation that ends in system messages (Claude
910
+ // Code's environment context, sent after the user's question) with that
911
+ // context said first: a turn of its own, answered by Kiro IDE's
912
+ // placeholder, before the question, which stays the message being
913
+ // answered rather than the head of "question, then context" (#538). The
914
+ // instructions' own field isn't used: Kiro refuses it ("systemPrompt field
915
+ // is not enabled for this user"). A question carrying tool results, or
916
+ // following tool calls, keeps the context in it as before, so no call is
917
+ // parted from its result.
918
+ function contextFirst(messages) {
919
+ let end = messages.length
920
+ while (end > 0 && messages[end - 1]?.role === "system") end--
921
+ if (end === messages.length) return messages
922
+ let start = end
923
+ while (start > 0 && messages[start - 1]?.role === "user") start--
924
+ const parts = (m) => (typeof m?.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m?.content) ? m.content : [])
925
+ const context = messages.slice(end)
926
+ if (start === end || !context.some((m) => parts(m).some((p) => p?.type !== "text" || p.text))) return messages
927
+ const turn = messages.slice(start, end)
928
+ if (turn.some((m) => parts(m).some((p) => p?.type === "tool_result"))) return messages
929
+ if (parts(messages[start - 1]).some((p) => p?.type === "tool_use" || p?.type === "server_tool_use" || p?.type === "web_search_tool_result")) return messages
930
+ return [...messages.slice(0, start), ...context.map((m) => ({ ...m, role: "user" })), { role: "assistant", content: UNDERSTOOD }, ...turn]
931
+ }
932
+
875
933
  function kiroRequest(req, model, profile, budget) {
876
934
  const entries = []
877
935
  // the messages the caller marked for its prompt cache (cache_control),
@@ -882,7 +940,7 @@ function kiroRequest(req, model, profile, budget) {
882
940
  }
883
941
  const user = () => ({ content: "", modelId: model, origin: "KIRO_CLI" })
884
942
  const resultsOnly = new WeakSet() // the messages made to carry a reply's web search results
885
- for (const m of req.messages ?? []) {
943
+ for (const m of contextFirst(req.messages ?? [])) {
886
944
  const parts = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content ?? []
887
945
  const texts = []
888
946
  if (m.role === "assistant") {
@@ -1013,7 +1071,9 @@ function kiroRequest(req, model, profile, budget) {
1013
1071
  // end of the instructions, as magpie's built-in tells it (inSystem)
1014
1072
  const format = formatText(req)
1015
1073
  if (format) system = system ? system + "\n\n" + format : format
1016
- if (budget) system = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>` + (system ? "\n" + system : "")
1074
+ // a level Kiro takes as a field is sent as one, not also as a budget
1075
+ const native = budget ? nativeFields(req, model) : undefined
1076
+ if (budget && !native) system = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>` + (system ? "\n" + system : "")
1017
1077
  if (system) entries[0].userInputMessage.content = system + "\n\n" + entries[0].userInputMessage.content
1018
1078
  if (Array.isArray(req.system)) mark(entries[0].userInputMessage, req.system)
1019
1079
 
@@ -1063,6 +1123,7 @@ function kiroRequest(req, model, profile, budget) {
1063
1123
  if (entries.length > 1) state.history = entries.slice(0, -1)
1064
1124
  const out = { conversationState: state, agentMode: "vibe" }
1065
1125
  if (profile) out.profileArn = profile
1126
+ if (native) out.additionalModelRequestFields = native
1066
1127
  return out
1067
1128
  }
1068
1129
 
@@ -1936,6 +1997,20 @@ async function signInPost(url, body) {
1936
1997
 
1937
1998
  const expiry = (s) => Date.now() + (s > 0 ? s : 3600) * 1000
1938
1999
 
2000
+ // what Kiro's page says of where to sign in at AWS, taken only in its shape:
2001
+ // the region goes into AWS's host names
2002
+ const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d{1,2}$/
2003
+ // the ways Kiro's page comes back with a choice of
2004
+ const KIRO_WAYS = ["google", "github", "builderid", "awsidc", "internal", "external_idp"]
2005
+ function httpsURL(s) {
2006
+ try {
2007
+ const u = new URL(s)
2008
+ return u.protocol === "https:" && !u.username && !u.password
2009
+ } catch {
2010
+ return false
2011
+ }
2012
+ }
2013
+
1939
2014
  // whoIs names a new sign-in with the email Kiro has for it, and its plan.
1940
2015
  async function whoIs(c) {
1941
2016
  try {
@@ -1960,8 +2035,21 @@ async function listen() {
1960
2035
  throw new Error("the ports Kiro's sign-in comes back to are all busy; close the Kiro IDE's sign-in and try again")
1961
2036
  }
1962
2037
 
2038
+ // signingIn ends the browser sign-in under way, if any, and frees its
2039
+ // port: magpie keeps one sign-in of a provider at a time, and one left
2040
+ // behind (a tab closed, a paste never made) kept its port till it timed
2041
+ // out, so a few of them left no port for the next
2042
+ let signingIn = null
2043
+
1963
2044
  // browserSignIn is the IDE's: Kiro's sign-in page, back to a port here.
1964
- async function browserSignIn() {
2045
+ async function browserSignIn(inputs = {}) {
2046
+ const choice = inputs.loginMethod || "portal"
2047
+ if (!["portal", "builderid", "enterprise"].includes(choice)) throw new Error("unknown Kiro sign-in method")
2048
+ const issuer = choice === "enterprise" ? String(inputs.startUrl || "").trim() : BUILDER_ID_START
2049
+ const region = String(inputs.region || "us-east-1").trim()
2050
+ if (choice === "enterprise" && !httpsURL(issuer)) throw new Error("Enter the organization's HTTPS Start URL")
2051
+ if (choice !== "portal" && !AWS_REGION.test(region)) throw new Error("Enter a valid AWS region")
2052
+ signingIn?.()
1965
2053
  const { server, port } = await listen()
1966
2054
  const state = randomBytes(24).toString("base64url")
1967
2055
  const verifier = randomBytes(48).toString("base64url")
@@ -1977,7 +2065,39 @@ async function browserSignIn() {
1977
2065
  settle(result)
1978
2066
  }
1979
2067
 
2068
+ // A sign-in comes back here once (Google, GitHub), or twice (Builder ID,
2069
+ // Identity Center: Kiro's page, then AWS's). The browser may be on
2070
+ // another computer (magpie on a server or in Docker): the address its
2071
+ // tab ended on is pasted there, and magpie opens it here, on this port
2072
+ // (magpie's pluginsignin_paste.go), following a 302 to AWS as the next
2073
+ // page. So each stage answers only its own state, once, and a request
2074
+ // that isn't this sign-in's is a 404, which magpie says is the wrong
2075
+ // address at once instead of waiting.
2076
+ let stage = "portal" // then "aws", then "over"
2077
+ let busy = false // a code being traded: a second paste of it waits for the first
2078
+ let onToAWS = "" // the page the portal stage sent the browser on to, said again to a repeat
2079
+ let portalAsked = "" // what that stage was asked, so a repeat asking otherwise isn't taken
2080
+ async function startAWS(opt, issuer, region) {
2081
+ const reg = await signInPost(oidc(region) + "/client/register", { clientName: "Kiro IDE", clientType: "public", scopes: SCOPES,
2082
+ grantTypes: ["authorization_code", "refresh_token"], redirectUris: ["http://127.0.0.1/oauth/callback"], issuerUrl: issuer })
2083
+ if (stage === "over") throw new Error("this sign-in was cancelled")
2084
+ if (!reg.clientId) throw new Error("AWS registered no client")
2085
+ Object.assign(aws, { region, clientId: reg.clientId, clientSecret: reg.clientSecret,
2086
+ provider: { builderid: "BuilderId", awsidc: "Enterprise", internal: "Internal" }[opt],
2087
+ state: randomBytes(24).toString("base64url"), verifier: randomBytes(48).toString("base64url") })
2088
+ const a = new URLSearchParams({ response_type: "code", client_id: aws.clientId, redirect_uri: awsRedirect, scopes: SCOPES.join(","),
2089
+ state: aws.state, code_challenge: createHash("sha256").update(aws.verifier).digest("base64url"), code_challenge_method: "S256" })
2090
+ onToAWS = `${oidc(region)}/authorize?${a}`
2091
+ portalAsked = JSON.stringify([opt, issuer, region])
2092
+ stage = "aws"
2093
+ busy = false
2094
+ return onToAWS
2095
+ }
2096
+
1980
2097
  server.on("request", async (req, res) => {
2098
+ // each answer its own connection: none held open keeps the port once
2099
+ // the sign-in is over
2100
+ res.setHeader("Connection", "close")
1981
2101
  const url = new URL(req.url ?? "/", redirect)
1982
2102
  const html = (code, body) => {
1983
2103
  res.writeHead(code, { "Content-Type": "text/html; charset=utf-8" })
@@ -1985,57 +2105,88 @@ async function browserSignIn() {
1985
2105
  }
1986
2106
  if (url.pathname !== "/oauth/callback" && url.pathname !== "/signin/callback") return res.writeHead(404).end()
1987
2107
  const q = url.searchParams
1988
- if (!waiting) return html(200, page(false, "This sign-in is over", "Start it again."))
2108
+ const over = () => html(404, page(false, "This sign-in is over", "Start it again."))
2109
+ const notOurs = () => html(404, page(false, "This link isn't from this sign-in", "Paste the address of the tab this sign-in opened, or start it again."))
2110
+ if (stage === "over") return over()
1989
2111
  const fail = (msg) => {
2112
+ stage = "over"
1990
2113
  finish({ type: "failed", error: msg })
1991
2114
  html(200, page(false, "Sign-in didn't finish", msg))
1992
2115
  }
2116
+ const got = q.get("state") ?? ""
2117
+ const portal = got === state
2118
+ const fromAWS = stage === "aws" && !!aws.state && got === aws.state
2119
+ // a stale tab, another sign-in's, or a page that just sends a request
2120
+ // here: it says nothing about this one, and can't end it
2121
+ if (!portal && !fromAWS) return notOurs()
2122
+ if (busy) return html(409, page(false, "Already finishing", "This sign-in is finishing with this address already."))
1993
2123
  if (q.get("error")) return fail(q.get("error_description") || q.get("error"))
1994
2124
  let c
1995
2125
  try {
1996
- if (aws.state && q.get("state") === aws.state) {
1997
- // back from AWS
2126
+ if (fromAWS) {
2127
+ // back from AWS, to the address it was sent with
2128
+ if (url.pathname !== "/oauth/callback") return notOurs()
1998
2129
  if (!q.get("code")) return fail("AWS sent back no code")
2130
+ busy = true
1999
2131
  const t = await signInPost(oidc(aws.region) + "/token", { clientId: aws.clientId, clientSecret: aws.clientSecret,
2000
2132
  grantType: "authorization_code", redirectUri: awsRedirect, code: q.get("code"), codeVerifier: aws.verifier })
2001
2133
  if (!t.accessToken) throw new Error("AWS sent back no token")
2002
2134
  c = { access: t.accessToken, refresh: t.refreshToken ?? "", expires: expiry(t.expiresIn), method: "idc", loginProvider: aws.provider,
2003
2135
  region: aws.region, clientId: aws.clientId, clientSecret: aws.clientSecret, profile: "", builderID: isBuilderID(aws.provider) }
2004
- } else if (q.get("state") !== state) {
2005
- // not ours: someone else's page, or a stale tab
2006
- return html(200, page(false, "This link isn't from this sign-in", "Start it again."))
2007
2136
  } else {
2008
2137
  const opt = q.get("login_option") ?? ""
2138
+ const asked = JSON.stringify([opt, q.get("issuer_url"), q.get("idc_region")])
2139
+ if (stage === "aws") {
2140
+ // Kiro's page's address again (pasted twice, or reloaded): the
2141
+ // same AWS page, not a second client and state, which would
2142
+ // leave the first AWS tab's code refused
2143
+ if (onToAWS && asked === portalAsked) {
2144
+ res.writeHead(302, { Location: onToAWS })
2145
+ return res.end()
2146
+ }
2147
+ // chosen again on Kiro's page (Back, then another way): that
2148
+ // choice now, the AWS sign-in left behind dropped. Anything else
2149
+ // with Kiro's page's state isn't a choice, and leaves AWS's going
2150
+ if (!KIRO_WAYS.includes(opt)) return notOurs()
2151
+ stage = "portal"
2152
+ onToAWS = portalAsked = ""
2153
+ for (const k of Object.keys(aws)) delete aws[k]
2154
+ }
2009
2155
  if (opt === "google" || opt === "github") {
2156
+ if (!q.get("code")) return fail("Kiro's page sent back no code")
2157
+ busy = true
2010
2158
  const t = await signInPost(AUTH_SERVICE + "/oauth/token", { code: q.get("code"), code_verifier: verifier,
2011
2159
  redirect_uri: redirect + url.pathname + "?login_option=" + opt })
2012
2160
  if (!t.accessToken) throw new Error("Kiro sent back no token")
2013
2161
  c = { access: t.accessToken, refresh: t.refreshToken ?? "", expires: expiry(t.expiresIn), method: "social",
2014
2162
  loginProvider: opt === "google" ? "Google" : "Github", region: "us-east-1", profile: t.profileArn ?? "" }
2015
2163
  } else if (opt === "builderid" || opt === "awsidc" || opt === "internal") {
2016
- // on to AWS, with a client registered for this sign-in
2164
+ // on to AWS, with a client registered for this sign-in. The region
2165
+ // goes into AWS's host name, so it is only a region's shape
2017
2166
  const issuer = q.get("issuer_url")
2018
2167
  const region = q.get("idc_region")
2019
2168
  if (!issuer || !region) return fail("Kiro's page didn't say where to sign in at AWS")
2020
- const reg = await signInPost(oidc(region) + "/client/register", { clientName: "Kiro IDE", clientType: "public", scopes: SCOPES,
2021
- grantTypes: ["authorization_code", "refresh_token"], redirectUris: ["http://127.0.0.1/oauth/callback"], issuerUrl: issuer })
2022
- if (!reg.clientId) throw new Error("AWS registered no client")
2023
- Object.assign(aws, { region, clientId: reg.clientId, clientSecret: reg.clientSecret,
2024
- provider: { builderid: "BuilderId", awsidc: "Enterprise", internal: "Internal" }[opt],
2025
- state: randomBytes(24).toString("base64url"), verifier: randomBytes(48).toString("base64url") })
2026
- const a = new URLSearchParams({ response_type: "code", client_id: aws.clientId, redirect_uri: awsRedirect, scopes: SCOPES.join(","),
2027
- state: aws.state, code_challenge: createHash("sha256").update(aws.verifier).digest("base64url"), code_challenge_method: "S256" })
2028
- res.writeHead(302, { Location: `${oidc(region)}/authorize?${a}` })
2169
+ if (!AWS_REGION.test(region)) return fail("Kiro's page named a region AWS doesn't have")
2170
+ if (!httpsURL(issuer)) return fail("Kiro's page named an AWS sign-in that isn't an https address")
2171
+ busy = true
2172
+ await startAWS(opt, issuer, region)
2173
+ res.writeHead(302, { Location: onToAWS })
2029
2174
  return res.end()
2030
2175
  } else if (opt === "external_idp") {
2031
2176
  return fail("A company's own identity provider can't be signed in to here yet; sign in with `kiro-cli login`, and use Kiro CLI's sign-in")
2032
2177
  } else return fail(`Kiro's page came back with a sign-in this plugin doesn't know (${JSON.stringify(opt)})`)
2033
2178
  }
2034
2179
  } catch (e) {
2180
+ if (stage === "over") return over()
2181
+ busy = false
2035
2182
  return fail(e.message)
2036
2183
  }
2184
+ if (stage === "over") return over() // ended (a new sign-in, the time) while the code was traded
2185
+ // still busy while it is named: a second paste is told it is finishing
2037
2186
  const who = await whoIs(c)
2187
+ if (stage === "over") return over()
2038
2188
  const user = who.email ? nameOf(who.email, c.loginProvider) : "Kiro account"
2189
+ stage = "over"
2039
2190
  finish({ type: "success", provider: ID, refresh: c.refresh, access: c.access, expires: c.expires, accountId: user,
2040
2191
  ...(uidOf(c.loginProvider) ? { uid: uidOf(c.loginProvider) } : {}),
2041
2192
  method: c.method, loginProvider: c.loginProvider, region: c.region, profileArn: who.profile,
@@ -2043,11 +2194,43 @@ async function browserSignIn() {
2043
2194
  html(200, page(true, "You're signed in", `${user} is signed in. You can close this tab.`))
2044
2195
  })
2045
2196
 
2046
- const timer = setTimeout(() => finish({ type: "failed", error: "the sign-in timed out" }), SIGN_IN_TIMEOUT)
2197
+ const timer = setTimeout(() => {
2198
+ stage = "over"
2199
+ finish({ type: "failed", error: "the sign-in timed out" })
2200
+ }, SIGN_IN_TIMEOUT)
2201
+ const end = () => {
2202
+ if (signingIn === superseded) signingIn = null
2203
+ server.close()
2204
+ }
2205
+ // a new sign-in ends this one, or, over, frees its port at once
2206
+ const superseded = () => {
2207
+ stage = "over"
2208
+ finish({ type: "failed", error: "another sign-in to Kiro was started" })
2209
+ end()
2210
+ }
2211
+ signingIn?.() // one started while this one found its port
2212
+ signingIn = superseded
2047
2213
  done.then(() => {
2048
2214
  clearTimeout(timer)
2049
- setTimeout(() => server.close(), 10_000).unref?.()
2215
+ // a late address (pasted again, a tab reloaded) is told it is over a while
2216
+ setTimeout(end, 10_000).unref?.()
2050
2217
  })
2218
+ if (choice === "builderid" || choice === "enterprise") {
2219
+ try {
2220
+ const url = await startAWS(choice === "enterprise" ? "awsidc" : "builderid", issuer, region)
2221
+ return {
2222
+ url,
2223
+ instructions: "Complete AWS sign-in. If magpie runs on another computer, paste the complete address of the final localhost page back here.",
2224
+ method: "auto",
2225
+ callback: () => done,
2226
+ }
2227
+ } catch (error) {
2228
+ stage = "over"
2229
+ finish({ type: "failed", error: error.message })
2230
+ end()
2231
+ throw error
2232
+ }
2233
+ }
2051
2234
  const challenge = createHash("sha256").update(verifier).digest("base64url")
2052
2235
  const q = new URLSearchParams({ state, code_challenge: challenge, code_challenge_method: "S256", redirect_uri: redirect, redirect_from: "KiroIDE" })
2053
2236
  return {
@@ -2138,7 +2321,19 @@ export async function KiroAuthPlugin({ client } = {}) {
2138
2321
  }
2139
2322
  },
2140
2323
  methods: [
2141
- { type: "oauth", label: "Kiro (Google, GitHub, AWS Builder ID, IAM Identity Center)", authorize: browserSignIn },
2324
+ { type: "oauth", label: "Kiro (Google, GitHub, AWS Builder ID, IAM Identity Center)",
2325
+ prompts: [{ type: "select", key: "loginMethod", message: "Choose a sign-in method", options: [
2326
+ { label: "IAM Identity Center (organization Start URL)", value: "enterprise", hint: "Sign in to your organization with its AWS Start URL" },
2327
+ { label: "AWS Builder ID", value: "builderid", hint: "Open AWS sign-in directly; one callback to paste on a remote server" },
2328
+ { label: "Kiro sign-in page", value: "portal", hint: "Google, GitHub, IAM Identity Center or other Kiro sign-in methods" },
2329
+ ] },
2330
+ { type: "text", key: "startUrl", message: "Organization Start URL", placeholder: "https://d-xxxxxxxxxx.awsapps.com/start",
2331
+ condition: (inputs) => inputs.loginMethod === "enterprise",
2332
+ validate: (value) => httpsURL(String(value || "").trim()) ? undefined : "Enter an HTTPS Start URL" },
2333
+ { type: "text", key: "region", message: "AWS region (blank uses us-east-1)", placeholder: "us-east-1",
2334
+ condition: (inputs) => inputs.loginMethod === "enterprise",
2335
+ validate: (value) => !value || AWS_REGION.test(value.trim()) ? undefined : "Enter a valid AWS region" },
2336
+ ], authorize: browserSignIn },
2142
2337
  { type: "oauth", label: "Kiro CLI's or Kiro IDE's sign-in", authorize: ownSignIn },
2143
2338
  { type: "api", label: "Kiro API key (ksk_…)" },
2144
2339
  ],
@@ -2175,4 +2370,4 @@ export async function KiroAuthPlugin({ client } = {}) {
2175
2370
  }
2176
2371
 
2177
2372
  // for tests
2178
- export const _internal = { generate, refresh, usageOf, buildKiro, kiroBody, IMAGE_LIMIT, events, reply, failure, toolID, toolName, toolNames, thinking, configModel, runtimeModel, readCLI, readIDE, regionOf, planName, frames }
2373
+ export const _internal = { generate, refresh, usageOf, buildKiro, kiroBody, IMAGE_LIMIT, events, reply, failure, toolID, toolName, toolNames, thinking, nativeFields, listModels, contextFirst, configModel, runtimeModel, readCLI, readIDE, regionOf, planName, frames }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",