@acarmisc/backstage-plugin-litellm-backend 0.17.7 → 0.18.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/config.d.ts CHANGED
@@ -324,6 +324,34 @@ export interface Config {
324
324
  */
325
325
  allowTeamDelete?: boolean;
326
326
 
327
+ /**
328
+ * When true, all team write operations return 403 even for admins:
329
+ * POST /teams, PATCH /teams/:id, DELETE /teams/:id,
330
+ * POST/DELETE /teams/:id/members, and PUT objectPermissions routes.
331
+ * Reads (GET /teams, GET /teams/managed) keep working.
332
+ * Error message: 'Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage'.
333
+ * @default false
334
+ */
335
+ readOnly?: boolean;
336
+
337
+ /**
338
+ * Backstage group refs that must include the caller for team creation.
339
+ * When non-empty, POST /teams additionally requires the caller to be
340
+ * a member of at least one of these groups, narrowing the permission.
341
+ * Error message: 'Access denied: team creation is limited to <comma-joined groups>'.
342
+ * @default []
343
+ */
344
+ createGroups?: string[];
345
+
346
+ /**
347
+ * LiteLLM team roles whose holders may add/remove members of their own team.
348
+ * When non-empty, callers with one of these roles in members_with_roles
349
+ * can POST/DELETE /teams/:teamId/members without owning the team's group.
350
+ * Allows non-admins to manage their team's membership without full admin access.
351
+ * @default []
352
+ */
353
+ memberManagerRoles?: string[];
354
+
327
355
  /**
328
356
  * Object-permission management (attaching knowledge bases / MCP servers
329
357
  * to a team). This is the highest-risk surface — attaching a vector store
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;
@@ -9050,7 +9050,10 @@ function readTeamAdminConfig(config) {
9050
9050
  allowedMcpAccessGroups: config.getOptionalStringArray(
9051
9051
  "litellm.teamAdmin.allowedMcpAccessGroups"
9052
9052
  ) ?? [],
9053
- allowTeamDelete: config.getOptionalBoolean("litellm.teamAdmin.allowTeamDelete") ?? false
9053
+ allowTeamDelete: config.getOptionalBoolean("litellm.teamAdmin.allowTeamDelete") ?? false,
9054
+ readOnly: config.getOptionalBoolean("litellm.teamAdmin.readOnly") ?? false,
9055
+ createGroups: config.getOptionalStringArray("litellm.teamAdmin.createGroups") ?? [],
9056
+ memberManagerRoles: config.getOptionalStringArray("litellm.teamAdmin.memberManagerRoles") ?? []
9054
9057
  };
9055
9058
  }
9056
9059
  function validateTeamWriteInput(input, cfg) {
@@ -9795,7 +9798,9 @@ function registerConfigRoutes(router, ctx) {
9795
9798
  enabled: ctx.teamMgmtEnabled,
9796
9799
  maxBudgetCeiling: ctx.teamAdminCfg.maxBudgetCeiling,
9797
9800
  allowUnlimitedBudget: ctx.teamAdminCfg.allowUnlimitedBudget,
9798
- objectPermissionsEnabled: ctx.objectPermsEnabled
9801
+ objectPermissionsEnabled: ctx.objectPermsEnabled,
9802
+ readOnly: ctx.teamAdminCfg.readOnly,
9803
+ memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles
9799
9804
  },
9800
9805
  display: {
9801
9806
  hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
@@ -10748,6 +10753,12 @@ function registerTeamsRoutes(router, ctx) {
10748
10753
  res.status(500).json({ error: "Team management is misconfigured (group is missing)" });
10749
10754
  return;
10750
10755
  }
10756
+ if (teamAdminCfg.readOnly) {
10757
+ res.status(403).json({
10758
+ error: "Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage"
10759
+ });
10760
+ return;
10761
+ }
10751
10762
  const check = await assertTeamAdmin({
10752
10763
  req,
10753
10764
  auth,
@@ -10761,6 +10772,19 @@ function registerTeamsRoutes(router, ctx) {
10761
10772
  res.status(check.status).json({ error: check.error });
10762
10773
  return;
10763
10774
  }
10775
+ if (teamAdminCfg.createGroups.length > 0) {
10776
+ const memberships = await Promise.all(
10777
+ teamAdminCfg.createGroups.map(
10778
+ (group) => isUserMemberOfGroup(check.userEntityRef, group, catalogClient, auth, logger)
10779
+ )
10780
+ );
10781
+ if (!memberships.some(Boolean)) {
10782
+ res.status(403).json({
10783
+ error: `Access denied: team creation is limited to ${teamAdminCfg.createGroups.join(", ")}`
10784
+ });
10785
+ return;
10786
+ }
10787
+ }
10764
10788
  const v = validateTeamWriteInput(req.body ?? {}, teamAdminCfg);
10765
10789
  if (!v.ok) {
10766
10790
  res.status(400).json({ error: v.error });
@@ -10923,9 +10947,14 @@ function registerTeamsRoutes(router, ctx) {
10923
10947
  router.post(
10924
10948
  "/teams/:teamId/members",
10925
10949
  async (req, res) => {
10926
- const authz = await authorizeTeamSubresource(req, res, import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission);
10950
+ const authz = await authorizeTeamSubresource(
10951
+ req,
10952
+ res,
10953
+ import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
10954
+ { allowTeamRole: true }
10955
+ );
10927
10956
  if (!authz) return;
10928
- const { teamId, owningGroup, actor } = authz;
10957
+ const { teamId, owningGroup, actor, via } = authz;
10929
10958
  const body = req.body ?? {};
10930
10959
  const userEntityRef = (body.userEntityRef ?? "").trim();
10931
10960
  if (!userEntityRef) {
@@ -10945,6 +10974,12 @@ function registerTeamsRoutes(router, ctx) {
10945
10974
  return;
10946
10975
  }
10947
10976
  maxBudgetInTeam = body.maxBudgetInTeam;
10977
+ if (via === "teamRole") {
10978
+ res.status(403).json({
10979
+ error: "Team member managers cannot set member budgets"
10980
+ });
10981
+ return;
10982
+ }
10948
10983
  }
10949
10984
  try {
10950
10985
  const { token } = await auth.getPluginRequestToken({
@@ -11003,7 +11038,8 @@ function registerTeamsRoutes(router, ctx) {
11003
11038
  actor,
11004
11039
  teamId,
11005
11040
  member: litellmUserId,
11006
- owningGroup
11041
+ owningGroup,
11042
+ via
11007
11043
  });
11008
11044
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11009
11045
  res.json(
@@ -11017,15 +11053,35 @@ function registerTeamsRoutes(router, ctx) {
11017
11053
  router.delete(
11018
11054
  "/teams/:teamId/members",
11019
11055
  async (req, res) => {
11020
- const authz = await authorizeTeamSubresource(req, res, import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission);
11056
+ const authz = await authorizeTeamSubresource(
11057
+ req,
11058
+ res,
11059
+ import_backstage_plugin_litellm_common5.litellmTeamMembersManagePermission,
11060
+ { allowTeamRole: true }
11061
+ );
11021
11062
  if (!authz) return;
11022
- const { teamId, owningGroup, actor } = authz;
11063
+ const { teamId, owningGroup, actor, team, via } = authz;
11023
11064
  const userEntityRef = String(req.query.userEntityRef ?? "").trim();
11024
11065
  if (!userEntityRef) {
11025
11066
  res.status(400).json({ error: "userEntityRef query parameter is required" });
11026
11067
  return;
11027
11068
  }
11028
11069
  const litellmUserId = toLiteLLMUserId(userEntityRef, userIdDomain);
11070
+ if (via === "teamRole") {
11071
+ if (litellmUserId === toLiteLLMUserId(actor, userIdDomain)) {
11072
+ res.status(400).json({ error: "You cannot remove yourself from the team" });
11073
+ return;
11074
+ }
11075
+ const role = team.members_with_roles?.find(
11076
+ (m) => m.user_id === litellmUserId
11077
+ )?.role;
11078
+ if (role && teamAdminCfg.memberManagerRoles.includes(role)) {
11079
+ res.status(403).json({
11080
+ error: `Only platform team admins can remove a team ${role}`
11081
+ });
11082
+ return;
11083
+ }
11084
+ }
11029
11085
  try {
11030
11086
  await client.teamMemberDelete({
11031
11087
  team_id: teamId,
@@ -11035,7 +11091,8 @@ function registerTeamsRoutes(router, ctx) {
11035
11091
  actor,
11036
11092
  teamId,
11037
11093
  member: litellmUserId,
11038
- owningGroup
11094
+ owningGroup,
11095
+ via
11039
11096
  });
11040
11097
  const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
11041
11098
  res.json(
@@ -11314,10 +11371,10 @@ function registerTeamUsageRoutes(router, ctx) {
11314
11371
  });
11315
11372
  }
11316
11373
 
11317
- // ../../node_modules/jose/dist/node/esm/runtime/base64url.js
11374
+ // node_modules/jose/dist/node/esm/runtime/base64url.js
11318
11375
  var import_node_buffer = require("node:buffer");
11319
11376
 
11320
- // ../../node_modules/jose/dist/node/esm/lib/buffer_utils.js
11377
+ // node_modules/jose/dist/node/esm/lib/buffer_utils.js
11321
11378
  var encoder = new TextEncoder();
11322
11379
  var decoder = new TextDecoder();
11323
11380
  var MAX_INT32 = 2 ** 32;
@@ -11332,7 +11389,7 @@ function concat(...buffers) {
11332
11389
  return buf;
11333
11390
  }
11334
11391
 
11335
- // ../../node_modules/jose/dist/node/esm/runtime/base64url.js
11392
+ // node_modules/jose/dist/node/esm/runtime/base64url.js
11336
11393
  function normalize(input) {
11337
11394
  let encoded = input;
11338
11395
  if (encoded instanceof Uint8Array) {
@@ -11342,7 +11399,7 @@ function normalize(input) {
11342
11399
  }
11343
11400
  var decode = (input) => new Uint8Array(import_node_buffer.Buffer.from(normalize(input), "base64url"));
11344
11401
 
11345
- // ../../node_modules/jose/dist/node/esm/util/errors.js
11402
+ // node_modules/jose/dist/node/esm/util/errors.js
11346
11403
  var JOSEError = class extends Error {
11347
11404
  static code = "ERR_JOSE_GENERIC";
11348
11405
  code = "ERR_JOSE_GENERIC";
@@ -11428,18 +11485,18 @@ var JWSSignatureVerificationFailed = class extends JOSEError {
11428
11485
  }
11429
11486
  };
11430
11487
 
11431
- // ../../node_modules/jose/dist/node/esm/runtime/is_key_object.js
11488
+ // node_modules/jose/dist/node/esm/runtime/is_key_object.js
11432
11489
  var util = __toESM(require("node:util"), 1);
11433
11490
  var is_key_object_default = (obj) => util.types.isKeyObject(obj);
11434
11491
 
11435
- // ../../node_modules/jose/dist/node/esm/runtime/webcrypto.js
11492
+ // node_modules/jose/dist/node/esm/runtime/webcrypto.js
11436
11493
  var crypto = __toESM(require("node:crypto"), 1);
11437
11494
  var util2 = __toESM(require("node:util"), 1);
11438
11495
  var webcrypto2 = crypto.webcrypto;
11439
11496
  var webcrypto_default = webcrypto2;
11440
11497
  var isCryptoKey = (key) => util2.types.isCryptoKey(key);
11441
11498
 
11442
- // ../../node_modules/jose/dist/node/esm/lib/crypto_key.js
11499
+ // node_modules/jose/dist/node/esm/lib/crypto_key.js
11443
11500
  function unusable(name, prop = "algorithm.name") {
11444
11501
  return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
11445
11502
  }
@@ -11538,7 +11595,7 @@ function checkSigCryptoKey(key, alg, ...usages) {
11538
11595
  checkUsage(key, usages);
11539
11596
  }
11540
11597
 
11541
- // ../../node_modules/jose/dist/node/esm/lib/invalid_key_input.js
11598
+ // node_modules/jose/dist/node/esm/lib/invalid_key_input.js
11542
11599
  function message(msg, actual, ...types4) {
11543
11600
  types4 = types4.filter(Boolean);
11544
11601
  if (types4.length > 2) {
@@ -11567,14 +11624,14 @@ function withAlg(alg, actual, ...types4) {
11567
11624
  return message(`Key for the ${alg} algorithm must be `, actual, ...types4);
11568
11625
  }
11569
11626
 
11570
- // ../../node_modules/jose/dist/node/esm/runtime/is_key_like.js
11627
+ // node_modules/jose/dist/node/esm/runtime/is_key_like.js
11571
11628
  var is_key_like_default = (key) => is_key_object_default(key) || isCryptoKey(key);
11572
11629
  var types3 = ["KeyObject"];
11573
11630
  if (globalThis.CryptoKey || webcrypto_default?.CryptoKey) {
11574
11631
  types3.push("CryptoKey");
11575
11632
  }
11576
11633
 
11577
- // ../../node_modules/jose/dist/node/esm/lib/is_disjoint.js
11634
+ // node_modules/jose/dist/node/esm/lib/is_disjoint.js
11578
11635
  var isDisjoint = (...headers) => {
11579
11636
  const sources = headers.filter(Boolean);
11580
11637
  if (sources.length === 0 || sources.length === 1) {
@@ -11598,7 +11655,7 @@ var isDisjoint = (...headers) => {
11598
11655
  };
11599
11656
  var is_disjoint_default = isDisjoint;
11600
11657
 
11601
- // ../../node_modules/jose/dist/node/esm/lib/is_object.js
11658
+ // node_modules/jose/dist/node/esm/lib/is_object.js
11602
11659
  function isObjectLike(value) {
11603
11660
  return typeof value === "object" && value !== null;
11604
11661
  }
@@ -11616,10 +11673,10 @@ function isObject(input) {
11616
11673
  return Object.getPrototypeOf(input) === proto;
11617
11674
  }
11618
11675
 
11619
- // ../../node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11676
+ // node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11620
11677
  var import_node_crypto = require("node:crypto");
11621
11678
 
11622
- // ../../node_modules/jose/dist/node/esm/lib/is_jwk.js
11679
+ // node_modules/jose/dist/node/esm/lib/is_jwk.js
11623
11680
  function isJWK(key) {
11624
11681
  return isObject(key) && typeof key.kty === "string";
11625
11682
  }
@@ -11633,7 +11690,7 @@ function isSecretJWK(key) {
11633
11690
  return isJWK(key) && key.kty === "oct" && typeof key.k === "string";
11634
11691
  }
11635
11692
 
11636
- // ../../node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11693
+ // node_modules/jose/dist/node/esm/runtime/get_named_curve.js
11637
11694
  var namedCurveToJOSE = (namedCurve) => {
11638
11695
  switch (namedCurve) {
11639
11696
  case "prime256v1":
@@ -11682,7 +11739,7 @@ var getNamedCurve2 = (kee, raw) => {
11682
11739
  };
11683
11740
  var get_named_curve_default = getNamedCurve2;
11684
11741
 
11685
- // ../../node_modules/jose/dist/node/esm/runtime/check_key_length.js
11742
+ // node_modules/jose/dist/node/esm/runtime/check_key_length.js
11686
11743
  var import_node_crypto2 = require("node:crypto");
11687
11744
  var check_key_length_default = (key, alg) => {
11688
11745
  let modulusLength;
@@ -11699,7 +11756,7 @@ var check_key_length_default = (key, alg) => {
11699
11756
  }
11700
11757
  };
11701
11758
 
11702
- // ../../node_modules/jose/dist/node/esm/runtime/jwk_to_key.js
11759
+ // node_modules/jose/dist/node/esm/runtime/jwk_to_key.js
11703
11760
  var import_node_crypto3 = require("node:crypto");
11704
11761
  var parse = (key) => {
11705
11762
  if (key.d) {
@@ -11709,7 +11766,7 @@ var parse = (key) => {
11709
11766
  };
11710
11767
  var jwk_to_key_default = parse;
11711
11768
 
11712
- // ../../node_modules/jose/dist/node/esm/key/import.js
11769
+ // node_modules/jose/dist/node/esm/key/import.js
11713
11770
  async function importJWK(jwk, alg) {
11714
11771
  if (!isObject(jwk)) {
11715
11772
  throw new TypeError("JWK must be an object");
@@ -11733,7 +11790,7 @@ async function importJWK(jwk, alg) {
11733
11790
  }
11734
11791
  }
11735
11792
 
11736
- // ../../node_modules/jose/dist/node/esm/lib/check_key_type.js
11793
+ // node_modules/jose/dist/node/esm/lib/check_key_type.js
11737
11794
  var tag = (key) => key?.[Symbol.toStringTag];
11738
11795
  var jwkMatchesOp = (alg, key, usage) => {
11739
11796
  if (key.use !== void 0 && key.use !== "sig") {
@@ -11805,7 +11862,7 @@ function checkKeyType(allowJwk, alg, key, usage) {
11805
11862
  var check_key_type_default = checkKeyType.bind(void 0, false);
11806
11863
  var checkKeyTypeWithJwk = checkKeyType.bind(void 0, true);
11807
11864
 
11808
- // ../../node_modules/jose/dist/node/esm/lib/validate_crit.js
11865
+ // node_modules/jose/dist/node/esm/lib/validate_crit.js
11809
11866
  function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
11810
11867
  if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) {
11811
11868
  throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
@@ -11837,7 +11894,7 @@ function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader,
11837
11894
  }
11838
11895
  var validate_crit_default = validateCrit;
11839
11896
 
11840
- // ../../node_modules/jose/dist/node/esm/lib/validate_algorithms.js
11897
+ // node_modules/jose/dist/node/esm/lib/validate_algorithms.js
11841
11898
  var validateAlgorithms = (option, algorithms) => {
11842
11899
  if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) {
11843
11900
  throw new TypeError(`"${option}" option must be an array of strings`);
@@ -11849,11 +11906,11 @@ var validateAlgorithms = (option, algorithms) => {
11849
11906
  };
11850
11907
  var validate_algorithms_default = validateAlgorithms;
11851
11908
 
11852
- // ../../node_modules/jose/dist/node/esm/runtime/verify.js
11909
+ // node_modules/jose/dist/node/esm/runtime/verify.js
11853
11910
  var crypto3 = __toESM(require("node:crypto"), 1);
11854
11911
  var import_node_util2 = require("node:util");
11855
11912
 
11856
- // ../../node_modules/jose/dist/node/esm/runtime/dsa_digest.js
11913
+ // node_modules/jose/dist/node/esm/runtime/dsa_digest.js
11857
11914
  function dsaDigest(alg) {
11858
11915
  switch (alg) {
11859
11916
  case "PS256":
@@ -11877,7 +11934,7 @@ function dsaDigest(alg) {
11877
11934
  }
11878
11935
  }
11879
11936
 
11880
- // ../../node_modules/jose/dist/node/esm/runtime/node_key.js
11937
+ // node_modules/jose/dist/node/esm/runtime/node_key.js
11881
11938
  var import_node_crypto4 = require("node:crypto");
11882
11939
  var ecCurveAlgMap = /* @__PURE__ */ new Map([
11883
11940
  ["ES256", "P-256"],
@@ -11981,11 +12038,11 @@ function keyForCrypto(alg, key) {
11981
12038
  return options ? { ...options, key } : key;
11982
12039
  }
11983
12040
 
11984
- // ../../node_modules/jose/dist/node/esm/runtime/sign.js
12041
+ // node_modules/jose/dist/node/esm/runtime/sign.js
11985
12042
  var crypto2 = __toESM(require("node:crypto"), 1);
11986
12043
  var import_node_util = require("node:util");
11987
12044
 
11988
- // ../../node_modules/jose/dist/node/esm/runtime/hmac_digest.js
12045
+ // node_modules/jose/dist/node/esm/runtime/hmac_digest.js
11989
12046
  function hmacDigest(alg) {
11990
12047
  switch (alg) {
11991
12048
  case "HS256":
@@ -11999,7 +12056,7 @@ function hmacDigest(alg) {
11999
12056
  }
12000
12057
  }
12001
12058
 
12002
- // ../../node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js
12059
+ // node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js
12003
12060
  var import_node_crypto5 = require("node:crypto");
12004
12061
  function getSignVerifyKey(alg, key, usage) {
12005
12062
  if (key instanceof Uint8Array) {
@@ -12024,7 +12081,7 @@ function getSignVerifyKey(alg, key, usage) {
12024
12081
  throw new TypeError(invalid_key_input_default(key, ...types3, "Uint8Array", "JSON Web Key"));
12025
12082
  }
12026
12083
 
12027
- // ../../node_modules/jose/dist/node/esm/runtime/sign.js
12084
+ // node_modules/jose/dist/node/esm/runtime/sign.js
12028
12085
  var oneShotSign = (0, import_node_util.promisify)(crypto2.sign);
12029
12086
  var sign2 = async (alg, key, data) => {
12030
12087
  const k = getSignVerifyKey(alg, key, "sign");
@@ -12037,7 +12094,7 @@ var sign2 = async (alg, key, data) => {
12037
12094
  };
12038
12095
  var sign_default = sign2;
12039
12096
 
12040
- // ../../node_modules/jose/dist/node/esm/runtime/verify.js
12097
+ // node_modules/jose/dist/node/esm/runtime/verify.js
12041
12098
  var oneShotVerify = (0, import_node_util2.promisify)(crypto3.verify);
12042
12099
  var verify2 = async (alg, key, signature, data) => {
12043
12100
  const k = getSignVerifyKey(alg, key, "verify");
@@ -12060,7 +12117,7 @@ var verify2 = async (alg, key, signature, data) => {
12060
12117
  };
12061
12118
  var verify_default = verify2;
12062
12119
 
12063
- // ../../node_modules/jose/dist/node/esm/jws/flattened/verify.js
12120
+ // node_modules/jose/dist/node/esm/jws/flattened/verify.js
12064
12121
  async function flattenedVerify(jws, key, options) {
12065
12122
  if (!isObject(jws)) {
12066
12123
  throw new JWSInvalid("Flattened JWS must be an object");
@@ -12166,7 +12223,7 @@ async function flattenedVerify(jws, key, options) {
12166
12223
  return result;
12167
12224
  }
12168
12225
 
12169
- // ../../node_modules/jose/dist/node/esm/jws/compact/verify.js
12226
+ // node_modules/jose/dist/node/esm/jws/compact/verify.js
12170
12227
  async function compactVerify(jws, key, options) {
12171
12228
  if (jws instanceof Uint8Array) {
12172
12229
  jws = decoder.decode(jws);
@@ -12186,10 +12243,10 @@ async function compactVerify(jws, key, options) {
12186
12243
  return result;
12187
12244
  }
12188
12245
 
12189
- // ../../node_modules/jose/dist/node/esm/lib/epoch.js
12246
+ // node_modules/jose/dist/node/esm/lib/epoch.js
12190
12247
  var epoch_default = (date) => Math.floor(date.getTime() / 1e3);
12191
12248
 
12192
- // ../../node_modules/jose/dist/node/esm/lib/secs.js
12249
+ // node_modules/jose/dist/node/esm/lib/secs.js
12193
12250
  var minute = 60;
12194
12251
  var hour = minute * 60;
12195
12252
  var day = hour * 24;
@@ -12246,7 +12303,7 @@ var secs_default = (str) => {
12246
12303
  return numericDate;
12247
12304
  };
12248
12305
 
12249
- // ../../node_modules/jose/dist/node/esm/lib/jwt_claims_set.js
12306
+ // node_modules/jose/dist/node/esm/lib/jwt_claims_set.js
12250
12307
  var normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, "");
12251
12308
  var checkAudiencePresence = (audPayload, audOption) => {
12252
12309
  if (typeof audPayload === "string") {
@@ -12342,7 +12399,7 @@ var jwt_claims_set_default = (protectedHeader, encodedPayload, options = {}) =>
12342
12399
  return payload;
12343
12400
  };
12344
12401
 
12345
- // ../../node_modules/jose/dist/node/esm/jwt/verify.js
12402
+ // node_modules/jose/dist/node/esm/jwt/verify.js
12346
12403
  async function jwtVerify(jwt, key, options) {
12347
12404
  const verified = await compactVerify(jwt, key, options);
12348
12405
  if (verified.protectedHeader.crit?.includes("b64") && verified.protectedHeader.b64 === false) {
@@ -12356,7 +12413,7 @@ async function jwtVerify(jwt, key, options) {
12356
12413
  return result;
12357
12414
  }
12358
12415
 
12359
- // ../../node_modules/jose/dist/node/esm/jwks/local.js
12416
+ // node_modules/jose/dist/node/esm/jwks/local.js
12360
12417
  function getKtyFromAlg(alg) {
12361
12418
  switch (typeof alg === "string" && alg.slice(0, 2)) {
12362
12419
  case "RS":
@@ -12477,7 +12534,7 @@ function createLocalJWKSet(jwks) {
12477
12534
  return localJWKSet;
12478
12535
  }
12479
12536
 
12480
- // ../../node_modules/jose/dist/node/esm/runtime/fetch_jwks.js
12537
+ // node_modules/jose/dist/node/esm/runtime/fetch_jwks.js
12481
12538
  var http = __toESM(require("node:http"), 1);
12482
12539
  var https = __toESM(require("node:https"), 1);
12483
12540
  var import_node_events = require("node:events");
@@ -12519,7 +12576,7 @@ var fetchJwks = async (url, timeout, options) => {
12519
12576
  };
12520
12577
  var fetch_jwks_default = fetchJwks;
12521
12578
 
12522
- // ../../node_modules/jose/dist/node/esm/jwks/remote.js
12579
+ // node_modules/jose/dist/node/esm/jwks/remote.js
12523
12580
  function isCloudflareWorkers() {
12524
12581
  return typeof WebSocketPair !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel";
12525
12582
  }
@@ -13175,12 +13232,18 @@ async function createRouter(options) {
13175
13232
  sendTeamError: (err, res) => {
13176
13233
  sendError(res, err, logger, "team operation");
13177
13234
  },
13178
- authorizeTeamSubresource: async (req, res, permission) => {
13235
+ authorizeTeamSubresource: async (req, res, permission, opts) => {
13179
13236
  if (!ctx.requireTeamMgmt(res)) return null;
13180
13237
  if (!teamAdminCfg.group) {
13181
13238
  res.status(500).json({ error: "Team management is misconfigured (group is missing)" });
13182
13239
  return null;
13183
13240
  }
13241
+ if (teamAdminCfg.readOnly) {
13242
+ res.status(403).json({
13243
+ error: "Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage"
13244
+ });
13245
+ return null;
13246
+ }
13184
13247
  const check = await assertTeamAdmin({
13185
13248
  req,
13186
13249
  auth,
@@ -13190,7 +13253,8 @@ async function createRouter(options) {
13190
13253
  permission,
13191
13254
  logger
13192
13255
  });
13193
- if (!check.ok) {
13256
+ const teamRoleEligible = !!opts?.allowTeamRole && teamAdminCfg.memberManagerRoles.length > 0;
13257
+ if (!check.ok && !(check.status === 403 && teamRoleEligible)) {
13194
13258
  res.status(check.status).json({ error: check.error });
13195
13259
  return null;
13196
13260
  }
@@ -13211,29 +13275,52 @@ async function createRouter(options) {
13211
13275
  return null;
13212
13276
  }
13213
13277
  const owningGroup = typeof existing.metadata?.owning_group === "string" ? existing.metadata.owning_group : void 0;
13214
- if (!owningGroup) {
13215
- res.status(403).json({
13278
+ let denial;
13279
+ if (!check.ok) {
13280
+ denial = { status: check.status, error: check.error };
13281
+ } else if (!owningGroup) {
13282
+ denial = {
13283
+ status: 403,
13216
13284
  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(
13285
+ };
13286
+ } else if (await isUserMemberOfGroup(
13221
13287
  check.userEntityRef,
13222
13288
  owningGroup,
13223
13289
  catalogClient,
13224
13290
  auth,
13225
13291
  logger
13226
- );
13227
- if (!owns) {
13228
- res.status(403).json({ error: `Access denied: team is owned by ${owningGroup}` });
13229
- return null;
13292
+ )) {
13293
+ return {
13294
+ teamId,
13295
+ owningGroup,
13296
+ actor: check.userEntityRef,
13297
+ team: existing,
13298
+ via: "group"
13299
+ };
13300
+ } else {
13301
+ denial = {
13302
+ status: 403,
13303
+ error: `Access denied: team is owned by ${owningGroup}`
13304
+ };
13230
13305
  }
13231
- return {
13232
- teamId,
13233
- owningGroup,
13234
- actor: check.userEntityRef,
13235
- team: existing
13236
- };
13306
+ if (teamRoleEligible) {
13307
+ const actor = await resolveUserId(req, auth);
13308
+ const litellmUserId = actor && toLiteLLMUserId(actor, userIdDomain);
13309
+ const role = existing.members_with_roles?.find(
13310
+ (m) => m.user_id === litellmUserId
13311
+ )?.role;
13312
+ if (actor && role && teamAdminCfg.memberManagerRoles.includes(role) && await ctx.assertPermission(req, permission)) {
13313
+ return {
13314
+ teamId,
13315
+ owningGroup: owningGroup ?? "",
13316
+ actor,
13317
+ team: existing,
13318
+ via: "teamRole"
13319
+ };
13320
+ }
13321
+ }
13322
+ res.status(denial.status).json({ error: denial.error });
13323
+ return null;
13237
13324
  }
13238
13325
  };
13239
13326
  const router = (0, import_express.Router)();