@acarmisc/backstage-plugin-litellm-backend 0.17.7 → 0.19.0

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/dist/index.cjs.js CHANGED
@@ -568,9 +568,9 @@ var require_index_cjs = __commonJS({
568
568
  }
569
569
  });
570
570
 
571
- // ../../node_modules/node-fetch/node_modules/webidl-conversions/lib/index.js
571
+ // ../../node_modules/webidl-conversions/lib/index.js
572
572
  var require_lib = __commonJS({
573
- "../../node_modules/node-fetch/node_modules/webidl-conversions/lib/index.js"(exports2, module2) {
573
+ "../../node_modules/webidl-conversions/lib/index.js"(exports2, module2) {
574
574
  "use strict";
575
575
  var conversions = {};
576
576
  module2.exports = conversions;
@@ -721,9 +721,9 @@ var require_lib = __commonJS({
721
721
  }
722
722
  });
723
723
 
724
- // ../../node_modules/node-fetch/node_modules/whatwg-url/lib/utils.js
724
+ // ../../node_modules/whatwg-url/lib/utils.js
725
725
  var require_utils = __commonJS({
726
- "../../node_modules/node-fetch/node_modules/whatwg-url/lib/utils.js"(exports2, module2) {
726
+ "../../node_modules/whatwg-url/lib/utils.js"(exports2, module2) {
727
727
  "use strict";
728
728
  module2.exports.mixin = function mixin(target, source) {
729
729
  const keys = Object.getOwnPropertyNames(source);
@@ -910,9 +910,9 @@ var require_tr46 = __commonJS({
910
910
  }
911
911
  });
912
912
 
913
- // ../../node_modules/node-fetch/node_modules/whatwg-url/lib/url-state-machine.js
913
+ // ../../node_modules/whatwg-url/lib/url-state-machine.js
914
914
  var require_url_state_machine = __commonJS({
915
- "../../node_modules/node-fetch/node_modules/whatwg-url/lib/url-state-machine.js"(exports2, module2) {
915
+ "../../node_modules/whatwg-url/lib/url-state-machine.js"(exports2, module2) {
916
916
  "use strict";
917
917
  var punycode = require("punycode");
918
918
  var tr46 = require_tr46();
@@ -1982,9 +1982,9 @@ var require_url_state_machine = __commonJS({
1982
1982
  }
1983
1983
  });
1984
1984
 
1985
- // ../../node_modules/node-fetch/node_modules/whatwg-url/lib/URL-impl.js
1985
+ // ../../node_modules/whatwg-url/lib/URL-impl.js
1986
1986
  var require_URL_impl = __commonJS({
1987
- "../../node_modules/node-fetch/node_modules/whatwg-url/lib/URL-impl.js"(exports2) {
1987
+ "../../node_modules/whatwg-url/lib/URL-impl.js"(exports2) {
1988
1988
  "use strict";
1989
1989
  var usm = require_url_state_machine();
1990
1990
  exports2.implementation = class URLImpl {
@@ -2139,9 +2139,9 @@ var require_URL_impl = __commonJS({
2139
2139
  }
2140
2140
  });
2141
2141
 
2142
- // ../../node_modules/node-fetch/node_modules/whatwg-url/lib/URL.js
2142
+ // ../../node_modules/whatwg-url/lib/URL.js
2143
2143
  var require_URL = __commonJS({
2144
- "../../node_modules/node-fetch/node_modules/whatwg-url/lib/URL.js"(exports2, module2) {
2144
+ "../../node_modules/whatwg-url/lib/URL.js"(exports2, module2) {
2145
2145
  "use strict";
2146
2146
  var conversions = require_lib();
2147
2147
  var utils = require_utils();
@@ -2321,9 +2321,9 @@ var require_URL = __commonJS({
2321
2321
  }
2322
2322
  });
2323
2323
 
2324
- // ../../node_modules/node-fetch/node_modules/whatwg-url/lib/public-api.js
2324
+ // ../../node_modules/whatwg-url/lib/public-api.js
2325
2325
  var require_public_api = __commonJS({
2326
- "../../node_modules/node-fetch/node_modules/whatwg-url/lib/public-api.js"(exports2) {
2326
+ "../../node_modules/whatwg-url/lib/public-api.js"(exports2) {
2327
2327
  "use strict";
2328
2328
  exports2.URL = require_URL().interface;
2329
2329
  exports2.serializeURL = require_url_state_machine().serializeURL;
@@ -7976,7 +7976,6 @@ __export(index_exports, {
7976
7976
  ProvisioningError: () => ProvisioningError,
7977
7977
  applyRoleOverrides: () => applyRoleOverrides,
7978
7978
  assertTeamAdmin: () => assertTeamAdmin,
7979
- bridgeGenerateKey: () => bridgeGenerateKey,
7980
7979
  bridgeListKeys: () => bridgeListKeys,
7981
7980
  createRouter: () => createRouter,
7982
7981
  default: () => litellmPlugin,
@@ -7998,7 +7997,6 @@ __export(index_exports, {
7998
7997
  litellmTeamMcpManagePermission: () => import_backstage_plugin_litellm_common11.litellmTeamMcpManagePermission,
7999
7998
  litellmTeamMembersManagePermission: () => import_backstage_plugin_litellm_common11.litellmTeamMembersManagePermission,
8000
7999
  newDefaultVerifier: () => newDefaultVerifier,
8001
- normalizeRequestTags: () => normalizeRequestTags,
8002
8000
  provisionUser: () => provisionUser,
8003
8001
  readBridgeConfig: () => readBridgeConfig,
8004
8002
  readProvisioningDefaults: () => readProvisioningDefaults,
@@ -8024,16 +8022,11 @@ var import_backend_plugin_api = require("@backstage/backend-plugin-api");
8024
8022
  // src/router.ts
8025
8023
  var import_express = __toESM(require("express"));
8026
8024
  var import_plugin_permission_common2 = __toESM(require_index_cjs2());
8027
- var import_errors20 = __toESM(require_index_cjs());
8025
+ var import_errors21 = __toESM(require_index_cjs());
8028
8026
  var import_catalog_client = require("@backstage/catalog-client");
8029
8027
 
8030
8028
  // src/client.ts
8031
8029
  var DEFAULT_TIMEOUT = 3e4;
8032
- function normalizeRequestTags(tags) {
8033
- if (!tags) return [];
8034
- if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
8035
- return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
8036
- }
8037
8030
  var LiteLLMUpstreamError = class extends Error {
8038
8031
  constructor(status, statusText, body) {
8039
8032
  let message2 = `LiteLLM API error: ${status} ${statusText}`;
@@ -8089,6 +8082,29 @@ var LiteLLMClient = class {
8089
8082
  clearTimeout(timeoutId);
8090
8083
  }
8091
8084
  }
8085
+ /**
8086
+ * Normalizes a raw LiteLLM user response to the UserInfo contract.
8087
+ * Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
8088
+ * response shapes, and converts team arrays to team_id strings.
8089
+ */
8090
+ normalizeUserInfo(raw, fallbackUserId) {
8091
+ const inner = raw?.user_info ?? {};
8092
+ const teamIds = Array.isArray(raw?.teams) ? raw.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8093
+ return {
8094
+ user_id: raw?.user_id ?? inner.user_id ?? fallbackUserId ?? "",
8095
+ user_email: inner.user_email ?? raw?.user_email,
8096
+ email: inner.email ?? raw?.email,
8097
+ teams: teamIds,
8098
+ models: inner.models ?? raw?.models,
8099
+ max_budget: inner.max_budget ?? raw?.max_budget,
8100
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
8101
+ budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
8102
+ spend: inner.spend ?? raw?.spend,
8103
+ current_spend: inner.current_spend ?? raw?.current_spend,
8104
+ soft_limit: inner.soft_limit ?? raw?.soft_limit,
8105
+ hard_limit: inner.hard_limit ?? raw?.hard_limit
8106
+ };
8107
+ }
8092
8108
  /**
8093
8109
  * Returns null when the user is not found in LiteLLM (404).
8094
8110
  * Throws on all other errors so callers know something went wrong.
@@ -8102,22 +8118,7 @@ var LiteLLMClient = class {
8102
8118
  const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
8103
8119
  try {
8104
8120
  const raw = await this.request(`/user/info${query}`);
8105
- const inner = raw?.user_info ?? {};
8106
- const teamIds = Array.isArray(raw?.teams) ? raw.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8107
- return {
8108
- user_id: raw?.user_id ?? inner.user_id ?? userId ?? "",
8109
- user_email: inner.user_email ?? raw?.user_email,
8110
- email: inner.email ?? raw?.email,
8111
- teams: teamIds,
8112
- models: inner.models ?? raw?.models,
8113
- max_budget: inner.max_budget ?? raw?.max_budget,
8114
- budget_duration: inner.budget_duration ?? raw?.budget_duration,
8115
- budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
8116
- spend: inner.spend ?? raw?.spend,
8117
- current_spend: inner.current_spend ?? raw?.current_spend,
8118
- soft_limit: inner.soft_limit ?? raw?.soft_limit,
8119
- hard_limit: inner.hard_limit ?? raw?.hard_limit
8120
- };
8121
+ return this.normalizeUserInfo(raw, userId);
8121
8122
  } catch (err) {
8122
8123
  if (err instanceof LiteLLMUpstreamError && err.status === 404) return null;
8123
8124
  throw err;
@@ -8144,22 +8145,7 @@ var LiteLLMClient = class {
8144
8145
  (u) => (u.user_email ?? u.email ?? "").toLowerCase() === email.toLowerCase()
8145
8146
  );
8146
8147
  if (!found) return null;
8147
- const inner = found.user_info ?? {};
8148
- const teamIds = Array.isArray(found.teams) ? found.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
8149
- return {
8150
- user_id: found.user_id ?? inner.user_id ?? "",
8151
- user_email: inner.user_email ?? found.user_email,
8152
- email: inner.email ?? found.email,
8153
- teams: teamIds,
8154
- models: inner.models ?? found.models,
8155
- max_budget: inner.max_budget ?? found.max_budget,
8156
- budget_duration: inner.budget_duration ?? found.budget_duration,
8157
- budget_reset_at: inner.budget_reset_at ?? found.budget_reset_at,
8158
- spend: inner.spend ?? found.spend,
8159
- current_spend: inner.current_spend ?? found.current_spend,
8160
- soft_limit: inner.soft_limit ?? found.soft_limit,
8161
- hard_limit: inner.hard_limit ?? found.hard_limit
8162
- };
8148
+ return this.normalizeUserInfo(found);
8163
8149
  } catch (err) {
8164
8150
  if (err instanceof LiteLLMUpstreamError && err.status === 404) return null;
8165
8151
  throw err;
@@ -8423,18 +8409,6 @@ var LiteLLMClient = class {
8423
8409
  body: JSON.stringify({ team_ids: [teamId] })
8424
8410
  });
8425
8411
  }
8426
- async blockTeam(teamId) {
8427
- return this.request("/team/block", {
8428
- method: "POST",
8429
- body: JSON.stringify({ team_id: teamId })
8430
- });
8431
- }
8432
- async unblockTeam(teamId) {
8433
- return this.request("/team/unblock", {
8434
- method: "POST",
8435
- body: JSON.stringify({ team_id: teamId })
8436
- });
8437
- }
8438
8412
  /**
8439
8413
  * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
8440
8414
  * under a `member` object; `role` is 'user' or 'admin' (we only ever send
@@ -8498,43 +8472,6 @@ var LiteLLMClient = class {
8498
8472
  url: r?.url ?? void 0
8499
8473
  })).filter((r) => r.id);
8500
8474
  }
8501
- /**
8502
- * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
8503
- * by date range and optionally by key/user/team. Rows are normalised so
8504
- * `request_tags` is always a `string[]` (LiteLLM returns either an array
8505
- * or an object depending on version) — callers group by tag to attribute
8506
- * spend to a conversation or caller.
8507
- *
8508
- * This is the admin/DB-backed endpoint: it can return a large number of
8509
- * rows, so callers should keep the date window tight and cap `page_size`.
8510
- */
8511
- async getSpendLogs(params) {
8512
- const query = new URLSearchParams({
8513
- start_date: params.start_date,
8514
- end_date: params.end_date
8515
- });
8516
- if (params.api_key) query.append("api_key", params.api_key);
8517
- if (params.user_id) query.append("user_id", params.user_id);
8518
- if (params.team_id) query.append("team_id", params.team_id);
8519
- if (params.page_size) query.append("page_size", String(params.page_size));
8520
- const raw = await this.request(`/spend/logs?${query.toString()}`);
8521
- const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
8522
- return rows.map((row) => ({
8523
- request_id: row?.request_id,
8524
- startTime: row?.startTime ?? row?.start_time,
8525
- endTime: row?.endTime ?? row?.end_time,
8526
- spend: row?.spend ?? 0,
8527
- total_tokens: row?.total_tokens ?? 0,
8528
- prompt_tokens: row?.prompt_tokens ?? 0,
8529
- completion_tokens: row?.completion_tokens ?? 0,
8530
- model: row?.model,
8531
- api_key: row?.api_key,
8532
- user: row?.user,
8533
- team_id: row?.team_id,
8534
- request_tags: normalizeRequestTags(row?.request_tags),
8535
- metadata: row?.metadata
8536
- }));
8537
- }
8538
8475
  emptyUsage() {
8539
8476
  return {
8540
8477
  total_spend: 0,
@@ -8698,6 +8635,114 @@ var LiteLLMClient = class {
8698
8635
  }
8699
8636
  };
8700
8637
 
8638
+ // src/errors.ts
8639
+ var HTML_TAG_PATTERN = /<[^>]*>|&[a-z]+;/gi;
8640
+ function isHttpishError(error) {
8641
+ return error !== null && typeof error === "object" && "status" in error && "body" in error && typeof error.status === "number" && // any: property access after guard check
8642
+ typeof error.body === "object";
8643
+ }
8644
+ function sanitizeUpstreamMessage(msg) {
8645
+ if (!msg || typeof msg !== "string") return "";
8646
+ return msg.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, "Bearer [redacted]").replace(/sk-[A-Za-z0-9_\-]{8,}/g, "sk-[redacted]").replace(HTML_TAG_PATTERN, "").replace(/\s+/g, " ").trim().slice(0, 500);
8647
+ }
8648
+ function toHttpError(error) {
8649
+ if (isHttpishError(error)) {
8650
+ return {
8651
+ status: error.status,
8652
+ body: error.body,
8653
+ logLevel: "warn"
8654
+ };
8655
+ }
8656
+ if (error instanceof LiteLLMUpstreamError) {
8657
+ if (error.status === 401 || error.status === 403) {
8658
+ return {
8659
+ status: 502,
8660
+ body: { error: "LiteLLM rejected the request" },
8661
+ logLevel: "warn"
8662
+ };
8663
+ }
8664
+ if (error.status >= 500 || error.status < 100) {
8665
+ return {
8666
+ status: 502,
8667
+ body: { error: "LiteLLM is unavailable" },
8668
+ logLevel: "error"
8669
+ };
8670
+ }
8671
+ return {
8672
+ status: error.status === 404 || error.status === 409 ? error.status : 400,
8673
+ body: {
8674
+ error: sanitizeUpstreamMessage(error.message),
8675
+ ...error.param ? { param: error.param } : {}
8676
+ },
8677
+ logLevel: "warn"
8678
+ };
8679
+ }
8680
+ if (error && typeof error === "object" && "name" in error && "message" in error) {
8681
+ const name = error.name;
8682
+ const message3 = String(error.message);
8683
+ if (name === "InputError") {
8684
+ return {
8685
+ status: 400,
8686
+ body: { error: message3 },
8687
+ logLevel: "warn"
8688
+ };
8689
+ }
8690
+ if (name === "NotAllowedError") {
8691
+ return {
8692
+ status: 403,
8693
+ body: { error: message3 },
8694
+ logLevel: "warn"
8695
+ };
8696
+ }
8697
+ if (name === "NotFoundError") {
8698
+ return {
8699
+ status: 404,
8700
+ body: { error: message3 },
8701
+ logLevel: "warn"
8702
+ };
8703
+ }
8704
+ if (name === "ConflictError") {
8705
+ return {
8706
+ status: 409,
8707
+ body: { error: message3 },
8708
+ logLevel: "warn"
8709
+ };
8710
+ }
8711
+ }
8712
+ if (error instanceof Error) {
8713
+ const code = error.code;
8714
+ const cause = error.cause;
8715
+ const networkCodes = ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"];
8716
+ if (error.name === "AbortError" || error.name === "TimeoutError" || error.name === "TypeError" && error.message === "fetch failed" || typeof code === "string" && networkCodes.includes(code) || typeof cause?.code === "string" && networkCodes.includes(cause.code)) {
8717
+ return {
8718
+ status: 502,
8719
+ body: { error: "LiteLLM is unavailable" },
8720
+ logLevel: "error",
8721
+ _logMessage: error.message
8722
+ };
8723
+ }
8724
+ }
8725
+ const message2 = error instanceof Error ? error.message : String(error);
8726
+ return {
8727
+ status: 500,
8728
+ body: { error: "Internal error" },
8729
+ logLevel: "error",
8730
+ _logMessage: message2
8731
+ };
8732
+ }
8733
+ function sendError(res, error, logger, context) {
8734
+ const { status, body, logLevel, _logMessage } = toHttpError(error);
8735
+ const realMessage = _logMessage || (error instanceof Error ? error.message : String(error));
8736
+ if (logLevel === "error") {
8737
+ logger.error(`Failed to ${context}`, error instanceof Error ? error : new Error(String(error)));
8738
+ } else if (logLevel === "warn") {
8739
+ logger.warn(`${context}: ${realMessage}`);
8740
+ } else {
8741
+ logger.info(`${context}: ${realMessage}`);
8742
+ }
8743
+ res.status(status).json(body);
8744
+ }
8745
+
8701
8746
  // src/provisioning.ts
8702
8747
  function toLiteLLMUserId(userEntityRef, userIdDomain) {
8703
8748
  const name = userEntityRef.split("/").pop() ?? userEntityRef;
@@ -8754,8 +8799,9 @@ function readProvisioningDefaults(config) {
8754
8799
  return { enabled, defaults };
8755
8800
  }
8756
8801
  async function resolveUserId(req, auth) {
8757
- const rawToken = req.headers.authorization?.slice(7);
8758
- if (!rawToken) return void 0;
8802
+ const authHeader = req.headers.authorization ?? "";
8803
+ if (!authHeader.startsWith("Bearer ")) return void 0;
8804
+ const rawToken = authHeader.slice(7);
8759
8805
  try {
8760
8806
  const credentials = await auth.authenticate(rawToken);
8761
8807
  const principal = credentials.principal;
@@ -8767,8 +8813,9 @@ async function resolveUserId(req, auth) {
8767
8813
  return void 0;
8768
8814
  }
8769
8815
  async function resolveCredentials(req, auth) {
8770
- const rawToken = req.headers.authorization?.slice(7);
8771
- if (!rawToken) return void 0;
8816
+ const authHeader = req.headers.authorization ?? "";
8817
+ if (!authHeader.startsWith("Bearer ")) return void 0;
8818
+ const rawToken = authHeader.slice(7);
8772
8819
  try {
8773
8820
  return await auth.authenticate(rawToken);
8774
8821
  } catch {
@@ -8847,10 +8894,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
8847
8894
  }
8848
8895
  }
8849
8896
  var provisioningInFlight = /* @__PURE__ */ new Map();
8850
- function sanitizeUpstreamMessage(message2) {
8851
- if (!message2) return "unknown error";
8852
- return message2.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, "Bearer [redacted]").replace(/sk-[A-Za-z0-9_\-]{8,}/g, "sk-[redacted]").slice(0, 500);
8853
- }
8854
8897
  var ProvisioningError = class extends Error {
8855
8898
  constructor(message2, hint, provisioning, status = 404) {
8856
8899
  super(message2);
@@ -9050,7 +9093,10 @@ function readTeamAdminConfig(config) {
9050
9093
  allowedMcpAccessGroups: config.getOptionalStringArray(
9051
9094
  "litellm.teamAdmin.allowedMcpAccessGroups"
9052
9095
  ) ?? [],
9053
- allowTeamDelete: config.getOptionalBoolean("litellm.teamAdmin.allowTeamDelete") ?? false
9096
+ allowTeamDelete: config.getOptionalBoolean("litellm.teamAdmin.allowTeamDelete") ?? false,
9097
+ readOnly: config.getOptionalBoolean("litellm.teamAdmin.readOnly") ?? false,
9098
+ createGroups: config.getOptionalStringArray("litellm.teamAdmin.createGroups") ?? [],
9099
+ memberManagerRoles: config.getOptionalStringArray("litellm.teamAdmin.memberManagerRoles") ?? []
9054
9100
  };
9055
9101
  }
9056
9102
  function validateTeamWriteInput(input, cfg) {
@@ -9251,114 +9297,6 @@ function readOpencodeConfig(config) {
9251
9297
  };
9252
9298
  }
9253
9299
 
9254
- // src/errors.ts
9255
- var HTML_TAG_PATTERN = /<[^>]*>|&[a-z]+;/gi;
9256
- function isHttpishError(error) {
9257
- return error !== null && typeof error === "object" && "status" in error && "body" in error && typeof error.status === "number" && // any: property access after guard check
9258
- typeof error.body === "object";
9259
- }
9260
- function sanitizeUpstreamMessage2(msg) {
9261
- if (!msg || typeof msg !== "string") return "";
9262
- return msg.replace(HTML_TAG_PATTERN, "").replace(/\s+/g, " ").trim().slice(0, 500);
9263
- }
9264
- function toHttpError(error) {
9265
- if (isHttpishError(error)) {
9266
- return {
9267
- status: error.status,
9268
- body: error.body,
9269
- logLevel: "warn"
9270
- };
9271
- }
9272
- if (error instanceof LiteLLMUpstreamError) {
9273
- if (error.status === 401 || error.status === 403) {
9274
- return {
9275
- status: 502,
9276
- body: { error: "LiteLLM rejected the request" },
9277
- logLevel: "warn"
9278
- };
9279
- }
9280
- if (error.status >= 500 || error.status < 100) {
9281
- return {
9282
- status: 502,
9283
- body: { error: "LiteLLM is unavailable" },
9284
- logLevel: "error"
9285
- };
9286
- }
9287
- return {
9288
- status: error.status === 404 || error.status === 409 ? error.status : 400,
9289
- body: {
9290
- error: sanitizeUpstreamMessage2(error.message),
9291
- ...error.param ? { param: error.param } : {}
9292
- },
9293
- logLevel: "warn"
9294
- };
9295
- }
9296
- if (error && typeof error === "object" && "name" in error && "message" in error) {
9297
- const name = error.name;
9298
- const message3 = String(error.message);
9299
- if (name === "InputError") {
9300
- return {
9301
- status: 400,
9302
- body: { error: message3 },
9303
- logLevel: "warn"
9304
- };
9305
- }
9306
- if (name === "NotAllowedError") {
9307
- return {
9308
- status: 403,
9309
- body: { error: message3 },
9310
- logLevel: "warn"
9311
- };
9312
- }
9313
- if (name === "NotFoundError") {
9314
- return {
9315
- status: 404,
9316
- body: { error: message3 },
9317
- logLevel: "warn"
9318
- };
9319
- }
9320
- if (name === "ConflictError") {
9321
- return {
9322
- status: 409,
9323
- body: { error: message3 },
9324
- logLevel: "warn"
9325
- };
9326
- }
9327
- }
9328
- if (error instanceof Error) {
9329
- const code = error.code;
9330
- const cause = error.cause;
9331
- const networkCodes = ["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"];
9332
- if (error.name === "AbortError" || error.name === "TimeoutError" || error.name === "TypeError" && error.message === "fetch failed" || typeof code === "string" && networkCodes.includes(code) || typeof cause?.code === "string" && networkCodes.includes(cause.code)) {
9333
- return {
9334
- status: 502,
9335
- body: { error: "LiteLLM is unavailable" },
9336
- logLevel: "error",
9337
- _logMessage: error.message
9338
- };
9339
- }
9340
- }
9341
- const message2 = error instanceof Error ? error.message : String(error);
9342
- return {
9343
- status: 500,
9344
- body: { error: "Internal error" },
9345
- logLevel: "error",
9346
- _logMessage: message2
9347
- };
9348
- }
9349
- function sendError(res, error, logger, context) {
9350
- const { status, body, logLevel, _logMessage } = toHttpError(error);
9351
- const realMessage = _logMessage || (error instanceof Error ? error.message : String(error));
9352
- if (logLevel === "error") {
9353
- logger.error(`Failed to ${context}`, error instanceof Error ? error : new Error(String(error)));
9354
- } else if (logLevel === "warn") {
9355
- logger.warn(`${context}: ${realMessage}`);
9356
- } else {
9357
- logger.info(`${context}: ${realMessage}`);
9358
- }
9359
- res.status(status).json(body);
9360
- }
9361
-
9362
9300
  // src/routes/middleware/withUser.ts
9363
9301
  function createRequireUser(ctx) {
9364
9302
  return async (req, res, next) => {
@@ -9795,7 +9733,9 @@ function registerConfigRoutes(router, ctx) {
9795
9733
  enabled: ctx.teamMgmtEnabled,
9796
9734
  maxBudgetCeiling: ctx.teamAdminCfg.maxBudgetCeiling,
9797
9735
  allowUnlimitedBudget: ctx.teamAdminCfg.allowUnlimitedBudget,
9798
- objectPermissionsEnabled: ctx.objectPermsEnabled
9736
+ objectPermissionsEnabled: ctx.objectPermsEnabled,
9737
+ readOnly: ctx.teamAdminCfg.readOnly,
9738
+ memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles
9799
9739
  },
9800
9740
  display: {
9801
9741
  hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
@@ -9892,56 +9832,9 @@ async function createKeyForUser(user, input, ctx) {
9892
9832
  config,
9893
9833
  catalogClient,
9894
9834
  auth,
9895
- logger,
9896
- provisioningEnabled,
9897
- provisioningDefaults,
9898
- roleConfigs
9835
+ logger
9899
9836
  } = ctx;
9900
- let userInfo;
9901
- if (catalogClient !== void 0 && catalogClient !== null && auth !== void 0 && auth !== null) {
9902
- userInfo = await getOrProvisionUser(
9903
- client,
9904
- user.tokenEntityRef || "",
9905
- user.userId,
9906
- provisioningEnabled,
9907
- provisioningDefaults,
9908
- roleConfigs,
9909
- catalogClient,
9910
- auth,
9911
- logger
9912
- );
9913
- } else {
9914
- userInfo = await client.getUserInfo(user.userId);
9915
- if (!userInfo) {
9916
- if (!provisioningEnabled) {
9917
- throw new KeyServiceError(
9918
- 404,
9919
- {
9920
- error: "User not found in LiteLLM",
9921
- detail: "No LiteLLM user for this identity. Log in to Backstage once to be provisioned, or enable litellm.provisioning.enabled."
9922
- }
9923
- );
9924
- }
9925
- const created = await client.createUser({
9926
- user_id: user.userId,
9927
- user_email: user.userId,
9928
- max_budget: provisioningDefaults.maxBudget,
9929
- budget_duration: provisioningDefaults.budgetDuration,
9930
- models: provisioningDefaults.models,
9931
- teams: provisioningDefaults.teams?.length ? [provisioningDefaults.teams[0]] : []
9932
- });
9933
- if (!created) {
9934
- throw new KeyServiceError(
9935
- 500,
9936
- {
9937
- error: "User provisioning failed",
9938
- detail: "Failed to create user in LiteLLM"
9939
- }
9940
- );
9941
- }
9942
- userInfo = await client.getUserInfo(user.userId);
9943
- }
9944
- }
9837
+ const userInfo = user.userInfo;
9945
9838
  if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === void 0)) {
9946
9839
  throw new KeyServiceError(
9947
9840
  400,
@@ -9987,13 +9880,15 @@ async function createKeyForUser(user, input, ctx) {
9987
9880
  }
9988
9881
  const profile = user.tokenEntityRef && catalogClient !== void 0 && catalogClient !== null && auth !== void 0 && auth !== null ? await resolveUserProfile(user.tokenEntityRef, catalogClient, auth, logger) : {};
9989
9882
  const clientMetadata = input.metadata ?? {};
9883
+ const createdVia = user.createdVia ?? "backstage";
9990
9884
  const serverMetadata = {
9991
- created_by_backstage_user: user.tokenEntityRef ?? "unknown",
9885
+ ...user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef },
9886
+ ...!user.tokenEntityRef && { created_by: userInfo.user_id },
9992
9887
  ...profile.email && { created_by_email: profile.email },
9993
9888
  ...profile.displayName && {
9994
9889
  created_by_display_name: profile.displayName
9995
9890
  },
9996
- created_via: "backstage",
9891
+ created_via: createdVia,
9997
9892
  created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
9998
9893
  };
9999
9894
  const enrichedMetadata = {
@@ -10002,7 +9897,7 @@ async function createKeyForUser(user, input, ctx) {
10002
9897
  };
10003
9898
  const upstreamRequest = {
10004
9899
  alias: input.alias,
10005
- user_id: user.userId
9900
+ user_id: userInfo.user_id
10006
9901
  };
10007
9902
  if (input.models !== void 0) {
10008
9903
  upstreamRequest.models = input.models;
@@ -10027,7 +9922,7 @@ async function createKeyForUser(user, input, ctx) {
10027
9922
  }
10028
9923
  upstreamRequest.metadata = enrichedMetadata;
10029
9924
  const result = await client.generateKey(upstreamRequest);
10030
- logger.info("key.generate", { userId: user.userId, keyAlias: input.alias });
9925
+ logger.info("key.generate", { userId: userInfo.user_id, keyAlias: input.alias });
10031
9926
  return result;
10032
9927
  }
10033
9928
 
@@ -10038,17 +9933,12 @@ function registerKeysRoutes(router, ctx) {
10038
9933
  catalogClient,
10039
9934
  auth,
10040
9935
  logger,
10041
- userIdDomain,
10042
- provisioningEnabled,
10043
- provisioningDefaults,
10044
- roleConfigs,
10045
9936
  auditGroup,
10046
9937
  allowUnlimitedBudget,
10047
9938
  teamRequired,
10048
9939
  allowOwnerResetSpend,
10049
9940
  keyValidationConfig,
10050
9941
  authorizeKeyAction,
10051
- sendOwnershipError,
10052
9942
  assertPermission,
10053
9943
  sendPermissionDenied
10054
9944
  } = ctx;
@@ -10120,7 +10010,7 @@ function registerKeysRoutes(router, ctx) {
10120
10010
  const keyId = key.token ?? key.key;
10121
10011
  let errorMsg = "Unknown error";
10122
10012
  if (err instanceof Error) {
10123
- errorMsg = sanitizeUpstreamMessage2(err.message) || "Unknown error";
10013
+ errorMsg = sanitizeUpstreamMessage(err.message) || "Unknown error";
10124
10014
  }
10125
10015
  failures.push({ keyId, error: errorMsg });
10126
10016
  logger.warn("key.prune-expired failed", {
@@ -10160,7 +10050,7 @@ function registerKeysRoutes(router, ctx) {
10160
10050
  return;
10161
10051
  }
10162
10052
  const tokenEntityRef = res.locals.tokenEntityRef;
10163
- const resolvedUserId = res.locals.userId;
10053
+ const userInfo = await getProvisionedUser(ctx, res);
10164
10054
  const keyCreateCtx = {
10165
10055
  client,
10166
10056
  config: {
@@ -10170,14 +10060,10 @@ function registerKeysRoutes(router, ctx) {
10170
10060
  catalogClient,
10171
10061
  auth,
10172
10062
  logger,
10173
- keyValidationConfig,
10174
- provisioningEnabled,
10175
- provisioningDefaults,
10176
- roleConfigs,
10177
- userIdDomain
10063
+ keyValidationConfig
10178
10064
  };
10179
10065
  const result = await createKeyForUser(
10180
- { tokenEntityRef, userId: resolvedUserId },
10066
+ { tokenEntityRef, userInfo },
10181
10067
  input,
10182
10068
  keyCreateCtx
10183
10069
  );
@@ -10277,7 +10163,6 @@ function registerKeysRoutes(router, ctx) {
10277
10163
  logger.info("key.update", { userId: tokenEntityRef ?? "unknown", keyId });
10278
10164
  res.json(result);
10279
10165
  } catch (error) {
10280
- if (sendOwnershipError(error, res)) return;
10281
10166
  sendError(res, error, logger, "update key");
10282
10167
  }
10283
10168
  });
@@ -10297,7 +10182,6 @@ function registerKeysRoutes(router, ctx) {
10297
10182
  logger.info("key.delete", { userId: tokenEntityRef ?? "unknown", keyId });
10298
10183
  res.json({ success: true });
10299
10184
  } catch (error) {
10300
- if (sendOwnershipError(error, res)) return;
10301
10185
  sendError(res, error, logger, "delete key");
10302
10186
  }
10303
10187
  });
@@ -10326,7 +10210,6 @@ function registerKeysRoutes(router, ctx) {
10326
10210
  logger.info("key.block", { userId: tokenEntityRef ?? "unknown", keyId });
10327
10211
  res.json({ success: true });
10328
10212
  } catch (error) {
10329
- if (sendOwnershipError(error, res)) return;
10330
10213
  sendError(res, error, logger, "block key");
10331
10214
  }
10332
10215
  });
@@ -10351,7 +10234,6 @@ function registerKeysRoutes(router, ctx) {
10351
10234
  logger.info("key.unblock", { userId: tokenEntityRef ?? "unknown", keyId });
10352
10235
  res.json({ success: true });
10353
10236
  } catch (error) {
10354
- if (sendOwnershipError(error, res)) return;
10355
10237
  sendError(res, error, logger, "unblock key");
10356
10238
  }
10357
10239
  });
@@ -10373,7 +10255,6 @@ function registerKeysRoutes(router, ctx) {
10373
10255
  logger.info("key.reset_spend", { userId: tokenEntityRef ?? "unknown", keyId });
10374
10256
  res.json({ success: true });
10375
10257
  } catch (error) {
10376
- if (sendOwnershipError(error, res)) return;
10377
10258
  sendError(res, error, logger, "reset key spend");
10378
10259
  }
10379
10260
  });
@@ -10433,23 +10314,11 @@ function registerKeysRoutes(router, ctx) {
10433
10314
  res.status(400).json({ error: "start_date and end_date are required" });
10434
10315
  return;
10435
10316
  }
10436
- const tokenEntityRef = res.locals.tokenEntityRef;
10437
- const userId = res.locals.userId;
10438
- await getOrProvisionUser(
10439
- client,
10440
- tokenEntityRef,
10441
- userId,
10442
- provisioningEnabled,
10443
- provisioningDefaults,
10444
- roleConfigs,
10445
- catalogClient,
10446
- auth,
10447
- logger
10448
- );
10317
+ await getProvisionedUser(ctx, res);
10449
10318
  const usage = await client.getUsage(
10450
10319
  start_date,
10451
10320
  end_date,
10452
- userId
10321
+ res.locals.userId
10453
10322
  );
10454
10323
  res.json(usage);
10455
10324
  } catch (error) {
@@ -10671,6 +10540,10 @@ function registerOpencodeRoutes(router, ctx) {
10671
10540
  var import_backstage_plugin_litellm_common5 = require("@acarmisc/backstage-plugin-litellm-common");
10672
10541
 
10673
10542
  // src/http/respondTeam.ts
10543
+ function respondTeam(res, team, ctx, caller) {
10544
+ const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
10545
+ res.json(shouldHide ? redactTeamBudget(team) : team);
10546
+ }
10674
10547
  function respondTeamList(res, teams, ctx, caller) {
10675
10548
  const shouldHide = caller === "member" ? ctx.teamBudgetVisibility.hideTeamBudgetForMembers : ctx.teamBudgetVisibility.hideTeamBudgetForManagers;
10676
10549
  res.json(shouldHide ? teams.map(redactTeamBudget) : teams);
@@ -10711,7 +10584,6 @@ function registerTeamsRoutes(router, ctx) {
10711
10584
  provisioningDefaults,
10712
10585
  roleConfigs,
10713
10586
  teamAdminCfg,
10714
- teamBudgetVisibility,
10715
10587
  requireTeamMgmt,
10716
10588
  sendTeamError,
10717
10589
  authorizeTeamSubresource
@@ -10748,6 +10620,12 @@ function registerTeamsRoutes(router, ctx) {
10748
10620
  res.status(500).json({ error: "Team management is misconfigured (group is missing)" });
10749
10621
  return;
10750
10622
  }
10623
+ if (teamAdminCfg.readOnly) {
10624
+ res.status(403).json({
10625
+ error: "Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage"
10626
+ });
10627
+ return;
10628
+ }
10751
10629
  const check = await assertTeamAdmin({
10752
10630
  req,
10753
10631
  auth,
@@ -10761,6 +10639,19 @@ function registerTeamsRoutes(router, ctx) {
10761
10639
  res.status(check.status).json({ error: check.error });
10762
10640
  return;
10763
10641
  }
10642
+ if (teamAdminCfg.createGroups.length > 0) {
10643
+ const memberships = await Promise.all(
10644
+ teamAdminCfg.createGroups.map(
10645
+ (group) => isUserMemberOfGroup(check.userEntityRef, group, catalogClient, auth, logger)
10646
+ )
10647
+ );
10648
+ if (!memberships.some(Boolean)) {
10649
+ res.status(403).json({
10650
+ error: `Access denied: team creation is limited to ${teamAdminCfg.createGroups.join(", ")}`
10651
+ });
10652
+ return;
10653
+ }
10654
+ }
10764
10655
  const v = validateTeamWriteInput(req.body ?? {}, teamAdminCfg);
10765
10656
  if (!v.ok) {
10766
10657
  res.status(400).json({ error: v.error });
@@ -10772,7 +10663,7 @@ function registerTeamsRoutes(router, ctx) {
10772
10663
  logger.info(`Team creation already in flight for ${key} \u2014 joining`);
10773
10664
  try {
10774
10665
  const result = await pending;
10775
- res.json(result);
10666
+ respondTeam(res, result, ctx, "manager");
10776
10667
  } catch (err) {
10777
10668
  sendTeamError(err, res);
10778
10669
  }
@@ -10804,9 +10695,7 @@ function registerTeamsRoutes(router, ctx) {
10804
10695
  teamCreateInFlight.set(key, createPromise);
10805
10696
  try {
10806
10697
  const result = await createPromise;
10807
- res.json(
10808
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(result) : result
10809
- );
10698
+ respondTeam(res, result, ctx, "manager");
10810
10699
  } catch (err) {
10811
10700
  sendTeamError(err, res);
10812
10701
  } finally {
@@ -10849,9 +10738,7 @@ function registerTeamsRoutes(router, ctx) {
10849
10738
  teamId,
10850
10739
  owningGroup
10851
10740
  });
10852
- res.json(
10853
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(r) : r
10854
- );
10741
+ respondTeam(res, r, ctx, "manager");
10855
10742
  } catch (err) {
10856
10743
  sendTeamError(err, res);
10857
10744
  }
@@ -10913,9 +10800,7 @@ function registerTeamsRoutes(router, ctx) {
10913
10800
  const owned = all.filter(
10914
10801
  (t) => typeof t.metadata?.owning_group === "string" && t.metadata.owning_group === teamAdminCfg.group
10915
10802
  );
10916
- res.json(
10917
- teamBudgetVisibility.hideTeamBudgetForManagers ? owned.map(redactTeamBudget) : owned
10918
- );
10803
+ respondTeamList(res, owned, ctx, "manager");
10919
10804
  } catch (err) {
10920
10805
  sendTeamError(err, res);
10921
10806
  }
@@ -10923,9 +10808,14 @@ function registerTeamsRoutes(router, ctx) {
10923
10808
  router.post(
10924
10809
  "/teams/:teamId/members",
10925
10810
  async (req, res) => {
10926
- const authz = await authorizeTeamSubresource(req, res, import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission);
10811
+ const authz = await authorizeTeamSubresource(
10812
+ req,
10813
+ res,
10814
+ import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
10815
+ { allowTeamRole: true }
10816
+ );
10927
10817
  if (!authz) return;
10928
- const { teamId, owningGroup, actor } = authz;
10818
+ const { teamId, owningGroup, actor, via } = authz;
10929
10819
  const body = req.body ?? {};
10930
10820
  const userEntityRef = (body.userEntityRef ?? "").trim();
10931
10821
  if (!userEntityRef) {
@@ -10945,6 +10835,12 @@ function registerTeamsRoutes(router, ctx) {
10945
10835
  return;
10946
10836
  }
10947
10837
  maxBudgetInTeam = body.maxBudgetInTeam;
10838
+ if (via === "teamRole") {
10839
+ res.status(403).json({
10840
+ error: "Team member managers cannot set member budgets"
10841
+ });
10842
+ return;
10843
+ }
10948
10844
  }
10949
10845
  try {
10950
10846
  const { token } = await auth.getPluginRequestToken({
@@ -11003,12 +10899,11 @@ function registerTeamsRoutes(router, ctx) {
11003
10899
  actor,
11004
10900
  teamId,
11005
10901
  member: litellmUserId,
11006
- owningGroup
10902
+ owningGroup,
10903
+ via
11007
10904
  });
11008
10905
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11009
- res.json(
11010
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11011
- );
10906
+ respondTeam(res, updated, ctx, "manager");
11012
10907
  } catch (err) {
11013
10908
  sendTeamError(err, res);
11014
10909
  }
@@ -11017,15 +10912,35 @@ function registerTeamsRoutes(router, ctx) {
11017
10912
  router.delete(
11018
10913
  "/teams/:teamId/members",
11019
10914
  async (req, res) => {
11020
- const authz = await authorizeTeamSubresource(req, res, import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission);
10915
+ const authz = await authorizeTeamSubresource(
10916
+ req,
10917
+ res,
10918
+ import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
10919
+ { allowTeamRole: true }
10920
+ );
11021
10921
  if (!authz) return;
11022
- const { teamId, owningGroup, actor } = authz;
10922
+ const { teamId, owningGroup, actor, team, via } = authz;
11023
10923
  const userEntityRef = String(req.query.userEntityRef ?? "").trim();
11024
10924
  if (!userEntityRef) {
11025
10925
  res.status(400).json({ error: "userEntityRef query parameter is required" });
11026
10926
  return;
11027
10927
  }
11028
10928
  const litellmUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
10929
+ if (via === "teamRole") {
10930
+ if (litellmUserId === toLiteLLMUserId(actor, userIdDomain)) {
10931
+ res.status(400).json({ error: "You cannot remove yourself from the team" });
10932
+ return;
10933
+ }
10934
+ const role = team.members_with_roles?.find(
10935
+ (m) => m.user_id === litellmUserId
10936
+ )?.role;
10937
+ if (role && teamAdminCfg.memberManagerRoles.includes(role)) {
10938
+ res.status(403).json({
10939
+ error: `Only platform team admins can remove a team ${role}`
10940
+ });
10941
+ return;
10942
+ }
10943
+ }
11029
10944
  try {
11030
10945
  await client.teamMemberDelete({
11031
10946
  team_id: teamId,
@@ -11035,12 +10950,11 @@ function registerTeamsRoutes(router, ctx) {
11035
10950
  actor,
11036
10951
  teamId,
11037
10952
  member: litellmUserId,
11038
- owningGroup
10953
+ owningGroup,
10954
+ via
11039
10955
  });
11040
10956
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11041
- res.json(
11042
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11043
- );
10957
+ respondTeam(res, updated, ctx, "manager");
11044
10958
  } catch (err) {
11045
10959
  sendTeamError(err, res);
11046
10960
  }
@@ -11058,7 +10972,6 @@ function registerObjectPermissionsRoutes(router, ctx) {
11058
10972
  permissions,
11059
10973
  logger,
11060
10974
  teamAdminCfg,
11061
- teamBudgetVisibility,
11062
10975
  requireObjectPerms,
11063
10976
  sendTeamError,
11064
10977
  authorizeTeamSubresource
@@ -11139,9 +11052,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11139
11052
  owningGroup,
11140
11053
  vector_stores: requested
11141
11054
  });
11142
- res.json(
11143
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11144
- );
11055
+ respondTeam(res, updated, ctx, "manager");
11145
11056
  } catch (err) {
11146
11057
  sendTeamError(err, res);
11147
11058
  }
@@ -11225,9 +11136,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
11225
11136
  before,
11226
11137
  after: requested
11227
11138
  });
11228
- res.json(
11229
- teamBudgetVisibility.hideTeamBudgetForManagers ? redactTeamBudget(updated) : updated
11230
- );
11139
+ respondTeam(res, updated, ctx, "manager");
11231
11140
  } catch (err) {
11232
11141
  sendTeamError(err, res);
11233
11142
  }
@@ -11242,9 +11151,6 @@ function registerTeamUsageRoutes(router, ctx) {
11242
11151
  catalogClient,
11243
11152
  auth,
11244
11153
  logger,
11245
- provisioningEnabled,
11246
- provisioningDefaults,
11247
- roleConfigs,
11248
11154
  teamBudgetVisibility
11249
11155
  } = ctx;
11250
11156
  const requireUser = createRequireUser(ctx);
@@ -11257,18 +11163,7 @@ function registerTeamUsageRoutes(router, ctx) {
11257
11163
  return;
11258
11164
  }
11259
11165
  const tokenEntityRef = res.locals.tokenEntityRef;
11260
- const userId = res.locals.userId;
11261
- const userInfo = await getOrProvisionUser(
11262
- client,
11263
- tokenEntityRef,
11264
- userId,
11265
- provisioningEnabled,
11266
- provisioningDefaults,
11267
- roleConfigs,
11268
- catalogClient,
11269
- auth,
11270
- logger
11271
- );
11166
+ const userInfo = await getProvisionedUser(ctx, res);
11272
11167
  const isMember = userInfo?.teams?.includes(teamId) ?? false;
11273
11168
  let isManager = false;
11274
11169
  const managerCheckNeeded = ctx.teamMgmtEnabled && (!isMember || teamBudgetVisibility.hideTeamBudgetForMembers !== teamBudgetVisibility.hideTeamBudgetForManagers);
@@ -11314,10 +11209,10 @@ function registerTeamUsageRoutes(router, ctx) {
11314
11209
  });
11315
11210
  }
11316
11211
 
11317
- // ../../node_modules/jose/dist/node/esm/runtime/base64url.js
11212
+ // node_modules/jose/dist/node/esm/runtime/base64url.js
11318
11213
  var import_node_buffer = require("node:buffer");
11319
11214
 
11320
- // ../../node_modules/jose/dist/node/esm/lib/buffer_utils.js
11215
+ // node_modules/jose/dist/node/esm/lib/buffer_utils.js
11321
11216
  var encoder = new TextEncoder();
11322
11217
  var decoder = new TextDecoder();
11323
11218
  var MAX_INT32 = 2 ** 32;
@@ -11332,7 +11227,7 @@ function concat(...buffers) {
11332
11227
  return buf;
11333
11228
  }
11334
11229
 
11335
- // ../../node_modules/jose/dist/node/esm/runtime/base64url.js
11230
+ // node_modules/jose/dist/node/esm/runtime/base64url.js
11336
11231
  function normalize(input) {
11337
11232
  let encoded = input;
11338
11233
  if (encoded instanceof Uint8Array) {
@@ -11342,7 +11237,7 @@ function normalize(input) {
11342
11237
  }
11343
11238
  var decode = (input) => new Uint8Array(import_node_buffer.Buffer.from(normalize(input), "base64url"));
11344
11239
 
11345
- // ../../node_modules/jose/dist/node/esm/util/errors.js
11240
+ // node_modules/jose/dist/node/esm/util/errors.js
11346
11241
  var JOSEError = class extends Error {
11347
11242
  static code = "ERR_JOSE_GENERIC";
11348
11243
  code = "ERR_JOSE_GENERIC";
@@ -11428,18 +11323,18 @@ var JWSSignatureVerificationFailed = class extends JOSEError {
11428
11323
  }
11429
11324
  };
11430
11325
 
11431
- // ../../node_modules/jose/dist/node/esm/runtime/is_key_object.js
11326
+ // node_modules/jose/dist/node/esm/runtime/is_key_object.js
11432
11327
  var util = __toESM(require("node:util"), 1);
11433
11328
  var is_key_object_default = (obj) => util.types.isKeyObject(obj);
11434
11329
 
11435
- // ../../node_modules/jose/dist/node/esm/runtime/webcrypto.js
11330
+ // node_modules/jose/dist/node/esm/runtime/webcrypto.js
11436
11331
  var crypto = __toESM(require("node:crypto"), 1);
11437
11332
  var util2 = __toESM(require("node:util"), 1);
11438
11333
  var webcrypto2 = crypto.webcrypto;
11439
11334
  var webcrypto_default = webcrypto2;
11440
11335
  var isCryptoKey = (key) => util2.types.isCryptoKey(key);
11441
11336
 
11442
- // ../../node_modules/jose/dist/node/esm/lib/crypto_key.js
11337
+ // node_modules/jose/dist/node/esm/lib/crypto_key.js
11443
11338
  function unusable(name, prop = "algorithm.name") {
11444
11339
  return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
11445
11340
  }
@@ -11538,7 +11433,7 @@ function checkSigCryptoKey(key, alg, ...usages) {
11538
11433
  checkUsage(key, usages);
11539
11434
  }
11540
11435
 
11541
- // ../../node_modules/jose/dist/node/esm/lib/invalid_key_input.js
11436
+ // node_modules/jose/dist/node/esm/lib/invalid_key_input.js
11542
11437
  function message(msg, actual, ...types4) {
11543
11438
  types4 = types4.filter(Boolean);
11544
11439
  if (types4.length > 2) {
@@ -11567,14 +11462,14 @@ function withAlg(alg, actual, ...types4) {
11567
11462
  return message(`Key for the ${alg} algorithm must be `, actual, ...types4);
11568
11463
  }
11569
11464
 
11570
- // ../../node_modules/jose/dist/node/esm/runtime/is_key_like.js
11465
+ // node_modules/jose/dist/node/esm/runtime/is_key_like.js
11571
11466
  var is_key_like_default = (key) => is_key_object_default(key) || isCryptoKey(key);
11572
11467
  var types3 = ["KeyObject"];
11573
11468
  if (globalThis.CryptoKey || webcrypto_default?.CryptoKey) {
11574
11469
  types3.push("CryptoKey");
11575
11470
  }
11576
11471
 
11577
- // ../../node_modules/jose/dist/node/esm/lib/is_disjoint.js
11472
+ // node_modules/jose/dist/node/esm/lib/is_disjoint.js
11578
11473
  var isDisjoint = (...headers) => {
11579
11474
  const sources = headers.filter(Boolean);
11580
11475
  if (sources.length === 0 || sources.length === 1) {
@@ -11598,7 +11493,7 @@ var isDisjoint = (...headers) => {
11598
11493
  };
11599
11494
  var is_disjoint_default = isDisjoint;
11600
11495
 
11601
- // ../../node_modules/jose/dist/node/esm/lib/is_object.js
11496
+ // node_modules/jose/dist/node/esm/lib/is_object.js
11602
11497
  function isObjectLike(value) {
11603
11498
  return typeof value === "object" && value !== null;
11604
11499
  }
@@ -11616,10 +11511,10 @@ function isObject(input) {
11616
11511
  return Object.getPrototypeOf(input) === proto;
11617
11512
  }
11618
11513
 
11619
- // ../../node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11514
+ // node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11620
11515
  var import_node_crypto = require("node:crypto");
11621
11516
 
11622
- // ../../node_modules/jose/dist/node/esm/lib/is_jwk.js
11517
+ // node_modules/jose/dist/node/esm/lib/is_jwk.js
11623
11518
  function isJWK(key) {
11624
11519
  return isObject(key) && typeof key.kty === "string";
11625
11520
  }
@@ -11633,7 +11528,7 @@ function isSecretJWK(key) {
11633
11528
  return isJWK(key) && key.kty === "oct" && typeof key.k === "string";
11634
11529
  }
11635
11530
 
11636
- // ../../node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11531
+ // node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11637
11532
  var namedCurveToJOSE = (namedCurve) => {
11638
11533
  switch (namedCurve) {
11639
11534
  case "prime256v1":
@@ -11682,7 +11577,7 @@ var getNamedCurve2 = (kee, raw) => {
11682
11577
  };
11683
11578
  var get_named_curve_default = getNamedCurve2;
11684
11579
 
11685
- // ../../node_modules/jose/dist/node/esm/runtime/check_key_length.js
11580
+ // node_modules/jose/dist/node/esm/runtime/check_key_length.js
11686
11581
  var import_node_crypto2 = require("node:crypto");
11687
11582
  var check_key_length_default = (key, alg) => {
11688
11583
  let modulusLength;
@@ -11699,7 +11594,7 @@ var check_key_length_default = (key, alg) => {
11699
11594
  }
11700
11595
  };
11701
11596
 
11702
- // ../../node_modules/jose/dist/node/esm/runtime/jwk_to_key.js
11597
+ // node_modules/jose/dist/node/esm/runtime/jwk_to_key.js
11703
11598
  var import_node_crypto3 = require("node:crypto");
11704
11599
  var parse = (key) => {
11705
11600
  if (key.d) {
@@ -11709,7 +11604,7 @@ var parse = (key) => {
11709
11604
  };
11710
11605
  var jwk_to_key_default = parse;
11711
11606
 
11712
- // ../../node_modules/jose/dist/node/esm/key/import.js
11607
+ // node_modules/jose/dist/node/esm/key/import.js
11713
11608
  async function importJWK(jwk, alg) {
11714
11609
  if (!isObject(jwk)) {
11715
11610
  throw new TypeError("JWK must be an object");
@@ -11733,7 +11628,7 @@ async function importJWK(jwk, alg) {
11733
11628
  }
11734
11629
  }
11735
11630
 
11736
- // ../../node_modules/jose/dist/node/esm/lib/check_key_type.js
11631
+ // node_modules/jose/dist/node/esm/lib/check_key_type.js
11737
11632
  var tag = (key) => key?.[Symbol.toStringTag];
11738
11633
  var jwkMatchesOp = (alg, key, usage) => {
11739
11634
  if (key.use !== void 0 && key.use !== "sig") {
@@ -11805,7 +11700,7 @@ function checkKeyType(allowJwk, alg, key, usage) {
11805
11700
  var check_key_type_default = checkKeyType.bind(void 0, false);
11806
11701
  var checkKeyTypeWithJwk = checkKeyType.bind(void 0, true);
11807
11702
 
11808
- // ../../node_modules/jose/dist/node/esm/lib/validate_crit.js
11703
+ // node_modules/jose/dist/node/esm/lib/validate_crit.js
11809
11704
  function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
11810
11705
  if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) {
11811
11706
  throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
@@ -11837,7 +11732,7 @@ function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader,
11837
11732
  }
11838
11733
  var validate_crit_default = validateCrit;
11839
11734
 
11840
- // ../../node_modules/jose/dist/node/esm/lib/validate_algorithms.js
11735
+ // node_modules/jose/dist/node/esm/lib/validate_algorithms.js
11841
11736
  var validateAlgorithms = (option, algorithms) => {
11842
11737
  if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) {
11843
11738
  throw new TypeError(`"${option}" option must be an array of strings`);
@@ -11849,11 +11744,11 @@ var validateAlgorithms = (option, algorithms) => {
11849
11744
  };
11850
11745
  var validate_algorithms_default = validateAlgorithms;
11851
11746
 
11852
- // ../../node_modules/jose/dist/node/esm/runtime/verify.js
11747
+ // node_modules/jose/dist/node/esm/runtime/verify.js
11853
11748
  var crypto3 = __toESM(require("node:crypto"), 1);
11854
11749
  var import_node_util2 = require("node:util");
11855
11750
 
11856
- // ../../node_modules/jose/dist/node/esm/runtime/dsa_digest.js
11751
+ // node_modules/jose/dist/node/esm/runtime/dsa_digest.js
11857
11752
  function dsaDigest(alg) {
11858
11753
  switch (alg) {
11859
11754
  case "PS256":
@@ -11877,7 +11772,7 @@ function dsaDigest(alg) {
11877
11772
  }
11878
11773
  }
11879
11774
 
11880
- // ../../node_modules/jose/dist/node/esm/runtime/node_key.js
11775
+ // node_modules/jose/dist/node/esm/runtime/node_key.js
11881
11776
  var import_node_crypto4 = require("node:crypto");
11882
11777
  var ecCurveAlgMap = /* @__PURE__ */ new Map([
11883
11778
  ["ES256", "P-256"],
@@ -11981,11 +11876,11 @@ function keyForCrypto(alg, key) {
11981
11876
  return options ? { ...options, key } : key;
11982
11877
  }
11983
11878
 
11984
- // ../../node_modules/jose/dist/node/esm/runtime/sign.js
11879
+ // node_modules/jose/dist/node/esm/runtime/sign.js
11985
11880
  var crypto2 = __toESM(require("node:crypto"), 1);
11986
11881
  var import_node_util = require("node:util");
11987
11882
 
11988
- // ../../node_modules/jose/dist/node/esm/runtime/hmac_digest.js
11883
+ // node_modules/jose/dist/node/esm/runtime/hmac_digest.js
11989
11884
  function hmacDigest(alg) {
11990
11885
  switch (alg) {
11991
11886
  case "HS256":
@@ -11999,7 +11894,7 @@ function hmacDigest(alg) {
11999
11894
  }
12000
11895
  }
12001
11896
 
12002
- // ../../node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js
11897
+ // node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js
12003
11898
  var import_node_crypto5 = require("node:crypto");
12004
11899
  function getSignVerifyKey(alg, key, usage) {
12005
11900
  if (key instanceof Uint8Array) {
@@ -12024,7 +11919,7 @@ function getSignVerifyKey(alg, key, usage) {
12024
11919
  throw new TypeError(invalid_key_input_default(key, ...types3, "Uint8Array", "JSON Web Key"));
12025
11920
  }
12026
11921
 
12027
- // ../../node_modules/jose/dist/node/esm/runtime/sign.js
11922
+ // node_modules/jose/dist/node/esm/runtime/sign.js
12028
11923
  var oneShotSign = (0, import_node_util.promisify)(crypto2.sign);
12029
11924
  var sign2 = async (alg, key, data) => {
12030
11925
  const k = getSignVerifyKey(alg, key, "sign");
@@ -12037,7 +11932,7 @@ var sign2 = async (alg, key, data) => {
12037
11932
  };
12038
11933
  var sign_default = sign2;
12039
11934
 
12040
- // ../../node_modules/jose/dist/node/esm/runtime/verify.js
11935
+ // node_modules/jose/dist/node/esm/runtime/verify.js
12041
11936
  var oneShotVerify = (0, import_node_util2.promisify)(crypto3.verify);
12042
11937
  var verify2 = async (alg, key, signature, data) => {
12043
11938
  const k = getSignVerifyKey(alg, key, "verify");
@@ -12060,7 +11955,7 @@ var verify2 = async (alg, key, signature, data) => {
12060
11955
  };
12061
11956
  var verify_default = verify2;
12062
11957
 
12063
- // ../../node_modules/jose/dist/node/esm/jws/flattened/verify.js
11958
+ // node_modules/jose/dist/node/esm/jws/flattened/verify.js
12064
11959
  async function flattenedVerify(jws, key, options) {
12065
11960
  if (!isObject(jws)) {
12066
11961
  throw new JWSInvalid("Flattened JWS must be an object");
@@ -12166,7 +12061,7 @@ async function flattenedVerify(jws, key, options) {
12166
12061
  return result;
12167
12062
  }
12168
12063
 
12169
- // ../../node_modules/jose/dist/node/esm/jws/compact/verify.js
12064
+ // node_modules/jose/dist/node/esm/jws/compact/verify.js
12170
12065
  async function compactVerify(jws, key, options) {
12171
12066
  if (jws instanceof Uint8Array) {
12172
12067
  jws = decoder.decode(jws);
@@ -12186,10 +12081,10 @@ async function compactVerify(jws, key, options) {
12186
12081
  return result;
12187
12082
  }
12188
12083
 
12189
- // ../../node_modules/jose/dist/node/esm/lib/epoch.js
12084
+ // node_modules/jose/dist/node/esm/lib/epoch.js
12190
12085
  var epoch_default = (date) => Math.floor(date.getTime() / 1e3);
12191
12086
 
12192
- // ../../node_modules/jose/dist/node/esm/lib/secs.js
12087
+ // node_modules/jose/dist/node/esm/lib/secs.js
12193
12088
  var minute = 60;
12194
12089
  var hour = minute * 60;
12195
12090
  var day = hour * 24;
@@ -12246,7 +12141,7 @@ var secs_default = (str) => {
12246
12141
  return numericDate;
12247
12142
  };
12248
12143
 
12249
- // ../../node_modules/jose/dist/node/esm/lib/jwt_claims_set.js
12144
+ // node_modules/jose/dist/node/esm/lib/jwt_claims_set.js
12250
12145
  var normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, "");
12251
12146
  var checkAudiencePresence = (audPayload, audOption) => {
12252
12147
  if (typeof audPayload === "string") {
@@ -12342,7 +12237,7 @@ var jwt_claims_set_default = (protectedHeader, encodedPayload, options = {}) =>
12342
12237
  return payload;
12343
12238
  };
12344
12239
 
12345
- // ../../node_modules/jose/dist/node/esm/jwt/verify.js
12240
+ // node_modules/jose/dist/node/esm/jwt/verify.js
12346
12241
  async function jwtVerify(jwt, key, options) {
12347
12242
  const verified = await compactVerify(jwt, key, options);
12348
12243
  if (verified.protectedHeader.crit?.includes("b64") && verified.protectedHeader.b64 === false) {
@@ -12356,7 +12251,7 @@ async function jwtVerify(jwt, key, options) {
12356
12251
  return result;
12357
12252
  }
12358
12253
 
12359
- // ../../node_modules/jose/dist/node/esm/jwks/local.js
12254
+ // node_modules/jose/dist/node/esm/jwks/local.js
12360
12255
  function getKtyFromAlg(alg) {
12361
12256
  switch (typeof alg === "string" && alg.slice(0, 2)) {
12362
12257
  case "RS":
@@ -12477,7 +12372,7 @@ function createLocalJWKSet(jwks) {
12477
12372
  return localJWKSet;
12478
12373
  }
12479
12374
 
12480
- // ../../node_modules/jose/dist/node/esm/runtime/fetch_jwks.js
12375
+ // node_modules/jose/dist/node/esm/runtime/fetch_jwks.js
12481
12376
  var http = __toESM(require("node:http"), 1);
12482
12377
  var https = __toESM(require("node:https"), 1);
12483
12378
  var import_node_events = require("node:events");
@@ -12519,7 +12414,7 @@ var fetchJwks = async (url, timeout, options) => {
12519
12414
  };
12520
12415
  var fetch_jwks_default = fetchJwks;
12521
12416
 
12522
- // ../../node_modules/jose/dist/node/esm/jwks/remote.js
12417
+ // node_modules/jose/dist/node/esm/jwks/remote.js
12523
12418
  function isCloudflareWorkers() {
12524
12419
  return typeof WebSocketPair !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel";
12525
12420
  }
@@ -12727,18 +12622,17 @@ function newDefaultVerifier(cfg) {
12727
12622
  return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
12728
12623
  }
12729
12624
  function normalizeIdentityOptions(opts) {
12730
- const o = typeof opts === "string" ? { userIdDomain: opts } : opts ?? {};
12731
12625
  let domains = [];
12732
- if (o.trustedEmailDomains?.length) {
12733
- domains = o.trustedEmailDomains;
12734
- } else if (o.userIdDomain) {
12735
- domains = [o.userIdDomain];
12626
+ if (opts.trustedEmailDomains?.length) {
12627
+ domains = opts.trustedEmailDomains;
12628
+ } else if (opts.userIdDomain) {
12629
+ domains = [opts.userIdDomain];
12736
12630
  }
12737
12631
  const trusted = domains.map((d) => d.toLowerCase());
12738
- return { userIdDomain: o.userIdDomain, trusted };
12632
+ return { userIdDomain: opts.userIdDomain, trusted };
12739
12633
  }
12740
12634
  function resolveBridgeUserId(claims, identity) {
12741
- const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
12635
+ const { userIdDomain, trusted } = normalizeIdentityOptions(identity ?? {});
12742
12636
  if (trusted.length === 0) {
12743
12637
  throw new BridgeIdentityError(
12744
12638
  "set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge"
@@ -12773,8 +12667,8 @@ function resolveBridgeUserId(claims, identity) {
12773
12667
  name = name || emailLocalPart;
12774
12668
  return toLiteLLMUserId(name, userIdDomain);
12775
12669
  }
12776
- async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
12777
- const userId = resolveBridgeUserId(claims, userIdDomain);
12670
+ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
12671
+ const userId = resolveBridgeUserId(claims, identity);
12778
12672
  const existing = await client.getUserInfo(userId);
12779
12673
  if (existing) return existing;
12780
12674
  if (claims.email) {
@@ -12815,38 +12709,17 @@ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled,
12815
12709
  }
12816
12710
  return created;
12817
12711
  }
12818
- async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
12712
+ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
12819
12713
  const user = await getOrProvisionUserFromClaims(
12820
12714
  client,
12821
12715
  claims,
12822
12716
  provisioningEnabled,
12823
12717
  provisioningDefaults,
12824
12718
  logger,
12825
- userIdDomain
12719
+ identity
12826
12720
  );
12827
12721
  return client.listKeys(user.user_id);
12828
12722
  }
12829
- async function bridgeGenerateKey(client, claims, provisioningEnabled, provisioningDefaults, logger, request, userIdDomain) {
12830
- const user = await getOrProvisionUserFromClaims(
12831
- client,
12832
- claims,
12833
- provisioningEnabled,
12834
- provisioningDefaults,
12835
- logger,
12836
- userIdDomain
12837
- );
12838
- const enriched = {
12839
- ...request,
12840
- user_id: user.user_id,
12841
- metadata: {
12842
- ...request.metadata ?? {},
12843
- created_via: "abby-cli",
12844
- created_by: user.user_id,
12845
- created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
12846
- }
12847
- };
12848
- return client.generateKey(enriched);
12849
- }
12850
12723
 
12851
12724
  // src/routes/bridge.ts
12852
12725
  var import_backstage_plugin_litellm_common7 = require("@acarmisc/backstage-plugin-litellm-common");
@@ -12858,7 +12731,6 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
12858
12731
  userIdDomain,
12859
12732
  provisioningEnabled,
12860
12733
  provisioningDefaults,
12861
- roleConfigs,
12862
12734
  allowUnlimitedBudget,
12863
12735
  teamRequired,
12864
12736
  keyValidationConfig
@@ -12943,26 +12815,25 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
12943
12815
  if (!allowUnlimitedBudget && input.max_budget === void 0) {
12944
12816
  input.max_budget = provisioningDefaults.maxBudget;
12945
12817
  }
12946
- const userId = resolveBridgeUserId(claims, bridgeIdentity);
12818
+ const userInfo = await getOrProvisionUserFromClaims(
12819
+ client,
12820
+ claims,
12821
+ provisioningEnabled,
12822
+ provisioningDefaults,
12823
+ logger,
12824
+ bridgeIdentity
12825
+ );
12947
12826
  const keyCreateCtx = {
12948
12827
  client,
12949
12828
  config: {
12950
12829
  allowUnlimitedBudget,
12951
12830
  teamRequired
12952
12831
  },
12953
- // Bridge does not have Backstage catalogClient/auth
12954
- // so provisioning from JWT claims only (no Backstage profile enrichment)
12955
- catalogClient: void 0,
12956
- auth: void 0,
12957
12832
  logger,
12958
- keyValidationConfig,
12959
- provisioningEnabled,
12960
- provisioningDefaults,
12961
- roleConfigs,
12962
- userIdDomain
12833
+ keyValidationConfig
12963
12834
  };
12964
12835
  const result = await createKeyForUser(
12965
- { userId },
12836
+ { userInfo, createdVia: "abby-cli" },
12966
12837
  input,
12967
12838
  keyCreateCtx
12968
12839
  );
@@ -12983,20 +12854,21 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
12983
12854
  bridgeIdentity
12984
12855
  );
12985
12856
  const teamIds = user.teams ?? [];
12986
- const teamMetadata = [];
12987
- for (const teamId of teamIds) {
12988
- try {
12989
- const teamInfo = await client.getTeamInfo(teamId);
12990
- teamMetadata.push({
12991
- id: teamId,
12992
- name: teamInfo.team_alias || teamId
12993
- // Use alias as display name, fallback to ID
12994
- });
12995
- } catch (e) {
12996
- logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
12997
- teamMetadata.push({ id: teamId });
12998
- }
12999
- }
12857
+ const teamMetadata = await Promise.all(
12858
+ teamIds.map(async (teamId) => {
12859
+ try {
12860
+ const teamInfo = await client.getTeamInfo(teamId);
12861
+ return {
12862
+ id: teamId,
12863
+ name: teamInfo.team_alias || teamId
12864
+ // Use alias as display name, fallback to ID
12865
+ };
12866
+ } catch (e) {
12867
+ logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
12868
+ return { id: teamId };
12869
+ }
12870
+ })
12871
+ );
13000
12872
  res.json({ user_id: user.user_id, teams: teamIds, team_metadata: teamMetadata });
13001
12873
  } catch (error) {
13002
12874
  handleBridgeError(error, res);
@@ -13115,31 +12987,17 @@ async function createRouter(options) {
13115
12987
  // Helper functions - defined inline below this object
13116
12988
  authorizeKeyAction: async (req, keyId) => {
13117
12989
  if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
13118
- throw new import_errors20.NotAllowedError("User identity not found in request");
12990
+ throw new import_errors21.NotAllowedError("User identity not found in request");
13119
12991
  }
13120
12992
  const tokenEntityRef = req.res.locals.tokenEntityRef;
13121
12993
  const userId = req.res.locals.userId;
13122
12994
  const ownKeys = await client.listKeys(userId);
13123
12995
  const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
13124
12996
  if (!key) {
13125
- throw new import_errors20.NotAllowedError("Access denied: key does not belong to the caller");
12997
+ throw new import_errors21.NotAllowedError("Access denied: key does not belong to the caller");
13126
12998
  }
13127
12999
  return { tokenEntityRef, userId, key };
13128
13000
  },
13129
- sendOwnershipError: (err, res) => {
13130
- if (err instanceof import_errors20.NotAllowedError) {
13131
- res.status(403).json({ error: err.message });
13132
- return true;
13133
- }
13134
- if (err && typeof err === "object" && "status" in err && "body" in err) {
13135
- const errObj = err;
13136
- if (typeof errObj.status === "number" && errObj.body) {
13137
- res.status(errObj.status).json(errObj.body);
13138
- return true;
13139
- }
13140
- }
13141
- return false;
13142
- },
13143
13001
  assertPermission: async (req, permission) => {
13144
13002
  const credentials = await resolveCredentials(req, auth);
13145
13003
  if (!credentials) return false;
@@ -13175,12 +13033,18 @@ async function createRouter(options) {
13175
13033
  sendTeamError: (err, res) => {
13176
13034
  sendError(res, err, logger, "team operation");
13177
13035
  },
13178
- authorizeTeamSubresource: async (req, res, permission) => {
13036
+ authorizeTeamSubresource: async (req, res, permission, opts) => {
13179
13037
  if (!ctx.requireTeamMgmt(res)) return null;
13180
13038
  if (!teamAdminCfg.group) {
13181
13039
  res.status(500).json({ error: "Team management is misconfigured (group is missing)" });
13182
13040
  return null;
13183
13041
  }
13042
+ if (teamAdminCfg.readOnly) {
13043
+ res.status(403).json({
13044
+ error: "Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage"
13045
+ });
13046
+ return null;
13047
+ }
13184
13048
  const check = await assertTeamAdmin({
13185
13049
  req,
13186
13050
  auth,
@@ -13190,7 +13054,8 @@ async function createRouter(options) {
13190
13054
  permission,
13191
13055
  logger
13192
13056
  });
13193
- if (!check.ok) {
13057
+ const teamRoleEligible = !!opts?.allowTeamRole && teamAdminCfg.memberManagerRoles.length > 0;
13058
+ if (!check.ok && !(check.status === 403 && teamRoleEligible)) {
13194
13059
  res.status(check.status).json({ error: check.error });
13195
13060
  return null;
13196
13061
  }
@@ -13211,29 +13076,52 @@ async function createRouter(options) {
13211
13076
  return null;
13212
13077
  }
13213
13078
  const owningGroup = typeof existing.metadata?.owning_group === "string" ? existing.metadata.owning_group : void 0;
13214
- if (!owningGroup) {
13215
- res.status(403).json({
13079
+ let denial;
13080
+ if (!check.ok) {
13081
+ denial = { status: check.status, error: check.error };
13082
+ } else if (!owningGroup) {
13083
+ denial = {
13084
+ status: 403,
13216
13085
  error: "This team is not managed by Backstage team admins and cannot be edited here"
13217
- });
13218
- return null;
13219
- }
13220
- const owns = await isUserMemberOfGroup(
13086
+ };
13087
+ } else if (await isUserMemberOfGroup(
13221
13088
  check.userEntityRef,
13222
13089
  owningGroup,
13223
13090
  catalogClient,
13224
13091
  auth,
13225
13092
  logger
13226
- );
13227
- if (!owns) {
13228
- res.status(403).json({ error: `Access denied: team is owned by ${owningGroup}` });
13229
- return null;
13093
+ )) {
13094
+ return {
13095
+ teamId,
13096
+ owningGroup,
13097
+ actor: check.userEntityRef,
13098
+ team: existing,
13099
+ via: "group"
13100
+ };
13101
+ } else {
13102
+ denial = {
13103
+ status: 403,
13104
+ error: `Access denied: team is owned by ${owningGroup}`
13105
+ };
13230
13106
  }
13231
- return {
13232
- teamId,
13233
- owningGroup,
13234
- actor: check.userEntityRef,
13235
- team: existing
13236
- };
13107
+ if (teamRoleEligible) {
13108
+ const actor = await resolveUserId(req, auth);
13109
+ const litellmUserId = actor && toLiteLLMUserId(actor, userIdDomain);
13110
+ const role = existing.members_with_roles?.find(
13111
+ (m) => m.user_id === litellmUserId
13112
+ )?.role;
13113
+ if (actor && role && teamAdminCfg.memberManagerRoles.includes(role) && await ctx.assertPermission(req, permission)) {
13114
+ return {
13115
+ teamId,
13116
+ owningGroup: owningGroup ?? "",
13117
+ actor,
13118
+ team: existing,
13119
+ via: "teamRole"
13120
+ };
13121
+ }
13122
+ }
13123
+ res.status(denial.status).json({ error: denial.error });
13124
+ return null;
13237
13125
  }
13238
13126
  };
13239
13127
  const router = (0, import_express.Router)();
@@ -13309,7 +13197,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
13309
13197
  ProvisioningError,
13310
13198
  applyRoleOverrides,
13311
13199
  assertTeamAdmin,
13312
- bridgeGenerateKey,
13313
13200
  bridgeListKeys,
13314
13201
  createRouter,
13315
13202
  getOrProvisionUser,
@@ -13330,7 +13217,6 @@ var import_backstage_plugin_litellm_common11 = require("@acarmisc/backstage-plug
13330
13217
  litellmTeamMcpManagePermission,
13331
13218
  litellmTeamMembersManagePermission,
13332
13219
  newDefaultVerifier,
13333
- normalizeRequestTags,
13334
13220
  provisionUser,
13335
13221
  readBridgeConfig,
13336
13222
  readProvisioningDefaults,