@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.
- package/README.md +26 -0
- package/index.mjs +218 -23
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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)",
|
|
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 }
|