@magpie-community/opencode-kiro-auth 0.1.2 → 0.1.4

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 +57 -24
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -189,6 +189,11 @@ async function saveBack(c) {
189
189
  // ---- tokens --------------------------------------------------------------------
190
190
 
191
191
  const EXPIRED = "Kiro's sign-in has expired; sign in again"
192
+
193
+ // 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.
196
+ const gone = (msg) => Object.assign(new Error(msg), { gone: true })
192
197
  const fresh = (c) => c.method === "apikey" || !c.expires || c.expires - Date.now() > 2 * 60 * 1000
193
198
 
194
199
  async function post(url, contentType, body, headers = {}) {
@@ -201,7 +206,7 @@ async function post(url, contentType, body, headers = {}) {
201
206
  }
202
207
  const text = await res.text()
203
208
  if (!res.ok) {
204
- if ([400, 401, 403].includes(res.status)) throw new Error(EXPIRED)
209
+ if ([400, 401, 403].includes(res.status)) throw gone(EXPIRED)
205
210
  throw new Error(`refreshing Kiro's sign-in: ${res.status}`)
206
211
  }
207
212
  return JSON.parse(text)
@@ -218,7 +223,7 @@ async function refresh(c) {
218
223
  if (n && n.dbKey === c.dbKey && n.access !== c.access && fresh(n)) return n
219
224
  }
220
225
  }
221
- if (!c.refresh) throw new Error(EXPIRED)
226
+ if (!c.refresh) throw gone(EXPIRED)
222
227
  let out
223
228
  switch (c.method) {
224
229
  case "social": {
@@ -234,16 +239,16 @@ async function refresh(c) {
234
239
  break
235
240
  }
236
241
  case "external-idp": {
237
- if (!c.tokenURL) throw new Error(EXPIRED + " with `kiro-cli login`")
242
+ if (!c.tokenURL) throw gone(EXPIRED + " with `kiro-cli login`")
238
243
  const form = new URLSearchParams({ grant_type: "refresh_token", client_id: c.clientId ?? "", refresh_token: c.refresh })
239
244
  const r = await post(c.tokenURL, "application/x-www-form-urlencoded", form.toString())
240
245
  out = { access: r.access_token, refresh: r.refresh_token, expiresIn: r.expires_in }
241
246
  break
242
247
  }
243
248
  default:
244
- throw new Error(EXPIRED)
249
+ throw gone(EXPIRED)
245
250
  }
246
- if (!out.access) throw new Error(EXPIRED)
251
+ if (!out.access) throw gone(EXPIRED)
247
252
  const n = { ...c, access: out.access, expires: Date.now() + (out.expiresIn > 0 ? out.expiresIn : 3600) * 1000 }
248
253
  if (out.refresh) n.refresh = out.refresh
249
254
  await saveBack(n)
@@ -309,7 +314,10 @@ async function profileOf(c) {
309
314
  body: "{}",
310
315
  signal: AbortSignal.timeout(20_000),
311
316
  })
312
- if (!res.ok) throw new Error(`Kiro didn't take the API key: ${res.status}`)
317
+ if (!res.ok) {
318
+ const msg = `Kiro didn't take the API key: ${res.status}`
319
+ throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
320
+ }
313
321
  const arn = (await res.json().catch(() => null))?.profile?.arn
314
322
  if (!arn) throw new Error("Kiro didn't say which profile the API key is for")
315
323
  return arn
@@ -411,7 +419,7 @@ function account(client) {
411
419
  const read = await credOf(auth)
412
420
  if (!read) {
413
421
  held = null
414
- throw new Error(
422
+ throw gone(
415
423
  auth?.source
416
424
  ? "Kiro isn't signed in; add the Kiro subscription in magpie, sign in with `kiro-cli login` or the Kiro IDE, or save a Kiro API key on the provider"
417
425
  : "this Kiro account's sign-in is gone; add it again in magpie",
@@ -423,7 +431,7 @@ function account(client) {
423
431
  if (stale && c && read.access !== c.access && fresh(read)) stale = false // the owner refreshed it already
424
432
  c = read
425
433
  if (!fresh(c) || stale) {
426
- if (c.method === "apikey") throw new Error("Kiro turned down the API key")
434
+ if (c.method === "apikey") throw gone("Kiro turned down the API key")
427
435
  c = await refresh(c)
428
436
  await save(auth, c)
429
437
  }
@@ -716,7 +724,7 @@ const ERROR_TYPES = { 400: "invalid_request_error", 401: "authentication_error",
716
724
  404: "not_found_error", 413: "request_too_large", 429: "rate_limit_error", 503: "overloaded_error", 529: "overloaded_error" }
717
725
  const errorType = (status) => ERROR_TYPES[status] ?? "api_error"
718
726
  const errorResponse = (status, message) =>
719
- new Response(JSON.stringify({ type: "error", error: { type: errorType(status), message: "Kiro: " + message } }), {
727
+ new Response(JSON.stringify({ type: "error", error: { type: errorType(status), message } }), {
720
728
  status,
721
729
  headers: { "Content-Type": "application/json" },
722
730
  })
@@ -944,6 +952,19 @@ async function* events(body, model, budget) {
944
952
  yield { kind: "stop", reason, usage }
945
953
  }
946
954
 
955
+ // finished is the message's blocks with each call's arguments read.
956
+ function finished(content) {
957
+ for (const b of content)
958
+ if (b.type === "tool_use") {
959
+ try {
960
+ b.input = b.input ? JSON.parse(b.input) : {}
961
+ } catch {
962
+ b.input = {}
963
+ }
964
+ }
965
+ return content
966
+ }
967
+
947
968
  const anthropicUsage = (u) => ({
948
969
  input_tokens: u.input,
949
970
  output_tokens: u.output,
@@ -951,6 +972,16 @@ const anthropicUsage = (u) => ({
951
972
  cache_creation_input_tokens: u.cacheWrite,
952
973
  })
953
974
 
975
+ // quotaWords are magpie's (internal/gateway/fallback.go): how a vendor says
976
+ // "out of quota" or "slow down".
977
+ 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
978
+
979
+ // failedBefore is the reply to an error before any of the answer, as
980
+ // magpie's built-in relay gives it: a status, not a stream, so another
981
+ // account can take over — a 429 when it reads as a quota or rate limit,
982
+ // else a 502.
983
+ const failedBefore = (text) => errorResponse(QUOTA_WORDS.test(text) ? 429 : 502, text)
984
+
954
985
  // reply answers the Messages request from Kiro's events: a stream of
955
986
  // Messages' events, or one message.
956
987
  async function reply(it, model, stream) {
@@ -974,23 +1005,24 @@ async function reply(it, model, stream) {
974
1005
  } else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: e.name, input: "" })
975
1006
  else if (e.kind === "toolArgs") {
976
1007
  if (last?.type === "tool_use") last.input += e.text
977
- } else if (e.kind === "error") return errorResponse(502, e.text)
978
- else if (e.kind === "stop") {
979
- for (const b of content)
980
- if (b.type === "tool_use") {
981
- try {
982
- b.input = b.input ? JSON.parse(b.input) : {}
983
- } catch {
984
- b.input = {}
985
- }
986
- }
987
- return Response.json({ id, type: "message", role: "assistant", model, content, stop_reason: e.reason, stop_sequence: null,
1008
+ } else if (e.kind === "error") {
1009
+ // with none of the answer said, a status; else what was said
1010
+ if (!content.length) return failedBefore(e.text)
1011
+ return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: "end_turn",
1012
+ stop_sequence: null, usage: anthropicUsage({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }) })
1013
+ } else if (e.kind === "stop") {
1014
+ return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: e.reason, stop_sequence: null,
988
1015
  usage: anthropicUsage(e.usage) })
989
1016
  }
990
1017
  }
991
1018
  return errorResponse(502, "the reply ended before it was complete")
992
1019
  }
993
1020
 
1021
+ // an error before any of the answer is a status, as the built-in's
1022
+ const head = await it.next()
1023
+ if (head.value?.kind === "error") return failedBefore(head.value.text)
1024
+ let pending = head
1025
+
994
1026
  const enc = new TextEncoder()
995
1027
  const sse = (type, data) => enc.encode(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`)
996
1028
  let index = -1
@@ -1014,7 +1046,8 @@ async function reply(it, model, stream) {
1014
1046
  open = kind
1015
1047
  ctl.enqueue(sse("content_block_start", { index, content_block: block }))
1016
1048
  }
1017
- const r = await it.next()
1049
+ const r = pending ?? (await it.next())
1050
+ pending = null
1018
1051
  if (r.done) return ctl.close()
1019
1052
  const e = r.value
1020
1053
  switch (e.kind) {
@@ -1037,7 +1070,7 @@ async function reply(it, model, stream) {
1037
1070
  break
1038
1071
  case "error":
1039
1072
  close()
1040
- ctl.enqueue(sse("error", { error: { type: "api_error", message: "Kiro: " + e.text } }))
1073
+ ctl.enqueue(sse("error", { error: { type: "api_error", message: e.text } }))
1041
1074
  return ctl.close()
1042
1075
  case "stop":
1043
1076
  close()
@@ -1062,7 +1095,7 @@ async function generate(creds, auth, req, signal) {
1062
1095
  try {
1063
1096
  a = await creds(auth)
1064
1097
  } catch (e) {
1065
- return errorResponse(401, e.message)
1098
+ return errorResponse(e?.gone ? 401 : 502, e.message)
1066
1099
  }
1067
1100
  let res
1068
1101
  try {
@@ -1076,7 +1109,7 @@ async function generate(creds, auth, req, signal) {
1076
1109
  try {
1077
1110
  a = await creds(auth, true)
1078
1111
  } catch (e) {
1079
- return errorResponse(401, e.message)
1112
+ return errorResponse(e?.gone ? 401 : 502, e.message)
1080
1113
  }
1081
1114
  try {
1082
1115
  res = await sendKiro(a, buildKiro(req, model, a.profile, budget), signal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",