@magpie-community/opencode-kiro-auth 0.1.3 → 0.1.5

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 (2) hide show
  1. package/index.mjs +75 -32
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -188,11 +188,14 @@ async function saveBack(c) {
188
188
 
189
189
  // ---- tokens --------------------------------------------------------------------
190
190
 
191
+ // EXPIRED is the built-in's words for a sign-in Kiro refused, or one with
192
+ // nothing to refresh it.
191
193
  const EXPIRED = "Kiro's sign-in has expired; sign in again"
194
+ const EXPIRED_CLI = EXPIRED + " with `kiro-cli login` or the Kiro IDE"
192
195
 
193
196
  // gone is an error saying the sign-in itself is gone — refused, or never
194
- // there — which alone answers a request with the 401 magpie marks the
195
- // account lapsed on; a refresh that timed out or met a 5xx may pass.
197
+ // there. The built-in marked no Kiro account lapsed, not even then, nor
198
+ // cleared one: every answer and usage read says the account is kept.
196
199
  const gone = (msg) => Object.assign(new Error(msg), { gone: true })
197
200
  const fresh = (c) => c.method === "apikey" || !c.expires || c.expires - Date.now() > 2 * 60 * 1000
198
201
 
@@ -206,8 +209,8 @@ async function post(url, contentType, body, headers = {}) {
206
209
  }
207
210
  const text = await res.text()
208
211
  if (!res.ok) {
209
- if ([400, 401, 403].includes(res.status)) throw gone(EXPIRED)
210
- throw new Error(`refreshing Kiro's sign-in: ${res.status}`)
212
+ if ([400, 401, 403].includes(res.status)) throw gone(EXPIRED_CLI)
213
+ throw new Error(`refreshing Kiro's sign-in: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()) // Go's res.Status
211
214
  }
212
215
  return JSON.parse(text)
213
216
  }
@@ -223,7 +226,7 @@ async function refresh(c) {
223
226
  if (n && n.dbKey === c.dbKey && n.access !== c.access && fresh(n)) return n
224
227
  }
225
228
  }
226
- if (!c.refresh) throw gone(EXPIRED)
229
+ if (!c.refresh) throw gone(EXPIRED_CLI)
227
230
  let out
228
231
  switch (c.method) {
229
232
  case "social": {
@@ -246,9 +249,9 @@ async function refresh(c) {
246
249
  break
247
250
  }
248
251
  default:
249
- throw gone(EXPIRED)
252
+ throw gone("Kiro's sign-in has expired")
250
253
  }
251
- if (!out.access) throw gone(EXPIRED)
254
+ if (!out.access) throw gone(EXPIRED_CLI)
252
255
  const n = { ...c, access: out.access, expires: Date.now() + (out.expiresIn > 0 ? out.expiresIn : 3600) * 1000 }
253
256
  if (out.refresh) n.refresh = out.refresh
254
257
  await saveBack(n)
@@ -315,7 +318,7 @@ async function profileOf(c) {
315
318
  signal: AbortSignal.timeout(20_000),
316
319
  })
317
320
  if (!res.ok) {
318
- const msg = `Kiro didn't take the API key: ${res.status}`
321
+ const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
319
322
  throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
320
323
  }
321
324
  const arn = (await res.json().catch(() => null))?.profile?.arn
@@ -431,7 +434,7 @@ function account(client) {
431
434
  if (stale && c && read.access !== c.access && fresh(read)) stale = false // the owner refreshed it already
432
435
  c = read
433
436
  if (!fresh(c) || stale) {
434
- if (c.method === "apikey") throw gone("Kiro turned down the API key")
437
+ if (c.method === "apikey") throw gone("Kiro turned down the API key saved on the provider")
435
438
  c = await refresh(c)
436
439
  await save(auth, c)
437
440
  }
@@ -712,7 +715,7 @@ function failure(status, body) {
712
715
  const e = JSON.parse(body)
713
716
  if (e?.message) msg = e.message + (e.reason ? ` (${e.reason})` : "")
714
717
  } catch {}
715
- msg ||= `HTTP ${status}`
718
+ msg ||= STATUS_TEXT[status] ?? ""
716
719
  if (msg.includes("CONTENT_LENGTH_EXCEEDS_THRESHOLD") || msg.toLowerCase().includes("input is too long"))
717
720
  return { status: 400, message: "input is too long for the model's context: " + msg }
718
721
  if (msg.includes("INSUFFICIENT_MODEL_CAPACITY")) return { status: 503, message: msg }
@@ -723,12 +726,23 @@ function failure(status, body) {
723
726
  const ERROR_TYPES = { 400: "invalid_request_error", 401: "authentication_error", 402: "billing_error", 403: "permission_error",
724
727
  404: "not_found_error", 413: "request_too_large", 429: "rate_limit_error", 503: "overloaded_error", 529: "overloaded_error" }
725
728
  const errorType = (status) => ERROR_TYPES[status] ?? "api_error"
729
+ // Each failure says X-Magpie-Sign-In: kept, as the built-in never marked
730
+ // an account lapsed (see gone).
726
731
  const errorResponse = (status, message) =>
727
732
  new Response(JSON.stringify({ type: "error", error: { type: errorType(status), message } }), {
728
733
  status,
729
- headers: { "Content-Type": "application/json" },
734
+ headers: { "Content-Type": "application/json", "X-Magpie-Sign-In": "kept" },
730
735
  })
731
736
 
737
+ // kept is res saying the account is kept: a success too, which the
738
+ // built-in never took for a sign-in come back.
739
+ function kept(res) {
740
+ if (res.headers.has("X-Magpie-Sign-In")) return res
741
+ const headers = new Headers(res.headers)
742
+ headers.set("X-Magpie-Sign-In", "kept")
743
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
744
+ }
745
+
732
746
  // ---- the reply: an AWS event stream → Messages ---------------------------------------
733
747
 
734
748
  // frames reads an AWS event stream: each message its length, its headers'
@@ -952,6 +966,19 @@ async function* events(body, model, budget) {
952
966
  yield { kind: "stop", reason, usage }
953
967
  }
954
968
 
969
+ // finished is the message's blocks with each call's arguments read.
970
+ function finished(content) {
971
+ for (const b of content)
972
+ if (b.type === "tool_use") {
973
+ try {
974
+ b.input = b.input ? JSON.parse(b.input) : {}
975
+ } catch {
976
+ b.input = {}
977
+ }
978
+ }
979
+ return content
980
+ }
981
+
955
982
  const anthropicUsage = (u) => ({
956
983
  input_tokens: u.input,
957
984
  output_tokens: u.output,
@@ -959,6 +986,16 @@ const anthropicUsage = (u) => ({
959
986
  cache_creation_input_tokens: u.cacheWrite,
960
987
  })
961
988
 
989
+ // quotaWords are magpie's (internal/gateway/fallback.go): how a vendor says
990
+ // "out of quota" or "slow down".
991
+ const QUOTA_WORDS = /quota|insufficient|balance|credit|billing|exceeded|rate.?limit|usage.?limit|limit.?reached|hit your .*limit|limit.{0,24}resets|too many requests|overloaded|余额|额度|欠费|限流|频率|套餐|用量|上限/i
992
+
993
+ // failedBefore is the reply to an error before any of the answer, as
994
+ // magpie's built-in relay gives it: a status, not a stream, so another
995
+ // account can take over — a 429 when it reads as a quota or rate limit,
996
+ // else a 502.
997
+ const failedBefore = (text) => errorResponse(QUOTA_WORDS.test(text) ? 429 : 502, text)
998
+
962
999
  // reply answers the Messages request from Kiro's events: a stream of
963
1000
  // Messages' events, or one message.
964
1001
  async function reply(it, model, stream) {
@@ -982,23 +1019,24 @@ async function reply(it, model, stream) {
982
1019
  } else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: e.name, input: "" })
983
1020
  else if (e.kind === "toolArgs") {
984
1021
  if (last?.type === "tool_use") last.input += e.text
985
- } else if (e.kind === "error") return errorResponse(502, e.text)
986
- else if (e.kind === "stop") {
987
- for (const b of content)
988
- if (b.type === "tool_use") {
989
- try {
990
- b.input = b.input ? JSON.parse(b.input) : {}
991
- } catch {
992
- b.input = {}
993
- }
994
- }
995
- return Response.json({ id, type: "message", role: "assistant", model, content, stop_reason: e.reason, stop_sequence: null,
1022
+ } else if (e.kind === "error") {
1023
+ // with none of the answer said, a status; else what was said
1024
+ if (!content.length) return failedBefore(e.text)
1025
+ return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: "end_turn",
1026
+ stop_sequence: null, usage: anthropicUsage({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }) })
1027
+ } else if (e.kind === "stop") {
1028
+ return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: e.reason, stop_sequence: null,
996
1029
  usage: anthropicUsage(e.usage) })
997
1030
  }
998
1031
  }
999
1032
  return errorResponse(502, "the reply ended before it was complete")
1000
1033
  }
1001
1034
 
1035
+ // an error before any of the answer is a status, as the built-in's
1036
+ const head = await it.next()
1037
+ if (head.value?.kind === "error") return failedBefore(head.value.text)
1038
+ let pending = head
1039
+
1002
1040
  const enc = new TextEncoder()
1003
1041
  const sse = (type, data) => enc.encode(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`)
1004
1042
  let index = -1
@@ -1022,7 +1060,8 @@ async function reply(it, model, stream) {
1022
1060
  open = kind
1023
1061
  ctl.enqueue(sse("content_block_start", { index, content_block: block }))
1024
1062
  }
1025
- const r = await it.next()
1063
+ const r = pending ?? (await it.next())
1064
+ pending = null
1026
1065
  if (r.done) return ctl.close()
1027
1066
  const e = r.value
1028
1067
  switch (e.kind) {
@@ -1062,7 +1101,9 @@ async function reply(it, model, stream) {
1062
1101
  }
1063
1102
 
1064
1103
  // generate answers a Messages request through Kiro's API: once more,
1065
- // refreshed, when Kiro turns the token down (403).
1104
+ // refreshed, when Kiro turns the token down (403). As the built-in: no
1105
+ // credentials is a 401, a failed refresh after the 403 a 502; none marks
1106
+ // the account.
1066
1107
  async function generate(creds, auth, req, signal) {
1067
1108
  const model = String(req.model ?? "auto")
1068
1109
  const budget = thinking(req, model)
@@ -1070,7 +1111,7 @@ async function generate(creds, auth, req, signal) {
1070
1111
  try {
1071
1112
  a = await creds(auth)
1072
1113
  } catch (e) {
1073
- return errorResponse(e?.gone ? 401 : 502, e.message)
1114
+ return errorResponse(401, e.message)
1074
1115
  }
1075
1116
  let res
1076
1117
  try {
@@ -1084,7 +1125,7 @@ async function generate(creds, auth, req, signal) {
1084
1125
  try {
1085
1126
  a = await creds(auth, true)
1086
1127
  } catch (e) {
1087
- return errorResponse(e?.gone ? 401 : 502, e.message)
1128
+ return errorResponse(502, e.message)
1088
1129
  }
1089
1130
  try {
1090
1131
  res = await sendKiro(a, buildKiro(req, model, a.profile, budget), signal)
@@ -1094,10 +1135,11 @@ async function generate(creds, auth, req, signal) {
1094
1135
  }
1095
1136
  }
1096
1137
  if (!res.ok) {
1138
+ // Kiro's own 401 is passed on, as the built-in did, the sign-in kept
1097
1139
  const f = failure(res.status, (await res.text()).slice(0, 1 << 20))
1098
1140
  return errorResponse(f.status, f.message)
1099
1141
  }
1100
- return reply(events(res.body, model, budget), model, req.stream === true)
1142
+ return kept(await reply(events(res.body, model, budget), model, req.stream === true))
1101
1143
  }
1102
1144
 
1103
1145
  // ---- signing in ----------------------------------------------------------------------
@@ -1304,14 +1346,15 @@ export async function KiroAuthPlugin({ client } = {}) {
1304
1346
  },
1305
1347
  }
1306
1348
  },
1307
- // the account's credits, as magpie's built-in showed them
1349
+ // the account's credits, as magpie's built-in showed them; a read,
1350
+ // clean or failed, marked the built-in's account nothing
1308
1351
  async usage(getAuth) {
1309
1352
  const auth = await getAuth()
1310
- if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in" }
1353
+ if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in", signIn: "kept" }
1311
1354
  try {
1312
- return usageOf(await usageLimits(await creds(auth), true))
1355
+ return { ...usageOf(await usageLimits(await creds(auth), true)), signIn: "kept" }
1313
1356
  } catch (e) {
1314
- return { error: e.message }
1357
+ return { error: e.message, signIn: "kept" }
1315
1358
  }
1316
1359
  },
1317
1360
  methods: [
@@ -1348,4 +1391,4 @@ export async function KiroAuthPlugin({ client } = {}) {
1348
1391
  }
1349
1392
 
1350
1393
  // for tests
1351
- export const _internal = { usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
1394
+ export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, thinking, 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.3",
3
+ "version": "0.1.5",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",