@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.d.ts CHANGED
@@ -2,13 +2,13 @@ export { litellmPlugin } from './plugin';
2
2
  export { litellmPlugin as default } from './plugin';
3
3
  export { createRouter } from './router';
4
4
  export * from './types';
5
- export { LiteLLMClient, normalizeRequestTags } from './client';
5
+ export { LiteLLMClient } from './client';
6
6
  export { resolveUserId, resolveUserProfile, toLiteLLMUserId, getOrProvisionUser, provisionUser, readProvisioningDefaults, readRoleConfigs, applyRoleOverrides, resolveUserRole, ProvisioningError, } from './provisioning';
7
7
  export type { BackstageUserProfile } from './provisioning';
8
8
  export { readTeamAdminConfig, isTeamManagementEnabled, assertTeamAdmin, validateTeamWriteInput, validateTeamPatchInput, } from './teamAdmin';
9
9
  export type { TeamAdminConfig, TeamAdminCheck, TeamWriteInput, TeamWriteValidation, TeamPatchValidation } from './teamAdmin';
10
10
  export { readTeamBudgetVisibility, redactTeamBudget, redactTeamUsage, teamBudgetStatusFor, } from './teamBudgetVisibility';
11
11
  export type { TeamBudgetVisibility, TeamBudgetStatus } from './teamBudgetVisibility';
12
- export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, bridgeGenerateKey, } from './bridge';
12
+ export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, } from './bridge';
13
13
  export type { BridgeClaims, TokenVerifier, BridgeConfig, KeycloakJWTVerifierOptions, } from './bridge';
14
14
  export { litellmPermissions, litellmKeyCreatePermission, litellmKeyRevokePermission, litellmKeyManagePermission, litellmKeyResetSpendPermission, litellmKeyUnblockPermission, litellmAuditReadPermission, litellmTeamCreatePermission, litellmTeamManagePermission, litellmTeamMembersManagePermission, litellmTeamKnowledgebaseManagePermission, litellmTeamMcpManagePermission, litellmTeamDeletePermission, } from '@acarmisc/backstage-plugin-litellm-common';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyUnblockPermission = exports.litellmKeyResetSpendPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.normalizeRequestTags = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
17
+ exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyUnblockPermission = exports.litellmKeyResetSpendPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
18
18
  var plugin_1 = require("./plugin");
19
19
  Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
20
20
  var plugin_2 = require("./plugin");
@@ -24,7 +24,6 @@ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function
24
24
  __exportStar(require("./types"), exports);
25
25
  var client_1 = require("./client");
26
26
  Object.defineProperty(exports, "LiteLLMClient", { enumerable: true, get: function () { return client_1.LiteLLMClient; } });
27
- Object.defineProperty(exports, "normalizeRequestTags", { enumerable: true, get: function () { return client_1.normalizeRequestTags; } });
28
27
  var provisioning_1 = require("./provisioning");
29
28
  Object.defineProperty(exports, "resolveUserId", { enumerable: true, get: function () { return provisioning_1.resolveUserId; } });
30
29
  Object.defineProperty(exports, "resolveUserProfile", { enumerable: true, get: function () { return provisioning_1.resolveUserProfile; } });
@@ -54,7 +53,6 @@ Object.defineProperty(exports, "readBridgeConfig", { enumerable: true, get: func
54
53
  Object.defineProperty(exports, "resolveBridgeUserId", { enumerable: true, get: function () { return bridge_1.resolveBridgeUserId; } });
55
54
  Object.defineProperty(exports, "getOrProvisionUserFromClaims", { enumerable: true, get: function () { return bridge_1.getOrProvisionUserFromClaims; } });
56
55
  Object.defineProperty(exports, "bridgeListKeys", { enumerable: true, get: function () { return bridge_1.bridgeListKeys; } });
57
- Object.defineProperty(exports, "bridgeGenerateKey", { enumerable: true, get: function () { return bridge_1.bridgeGenerateKey; } });
58
56
  var backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
59
57
  Object.defineProperty(exports, "litellmPermissions", { enumerable: true, get: function () { return backstage_plugin_litellm_common_1.litellmPermissions; } });
60
58
  Object.defineProperty(exports, "litellmKeyCreatePermission", { enumerable: true, get: function () { return backstage_plugin_litellm_common_1.litellmKeyCreatePermission; } });
@@ -49,11 +49,13 @@ export declare function readProvisioningDefaults(config: Config): {
49
49
  * Extracts the authenticated Backstage user identity from the request token.
50
50
  * Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
51
51
  * the request carries no user credential (service-to-service calls).
52
+ * Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
52
53
  */
53
54
  export declare function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined>;
54
55
  /**
55
56
  * Like resolveUserId, but returns the raw BackstageCredentials object
56
57
  * (rather than just the entity ref) for passing into PermissionsService.authorize().
58
+ * Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
57
59
  */
58
60
  export declare function resolveCredentials(req: Request, auth: AuthService): Promise<BackstageCredentials | undefined>;
59
61
  /**
@@ -13,6 +13,7 @@ exports.getOrProvisionUser = getOrProvisionUser;
13
13
  exports.isUserMemberOfGroup = isUserMemberOfGroup;
14
14
  exports.resolveUserRole = resolveUserRole;
15
15
  const client_1 = require("./client");
16
+ const errors_1 = require("./errors");
16
17
  /**
17
18
  * Converts a Backstage user entity ref to a LiteLLM user_id.
18
19
  *
@@ -107,11 +108,13 @@ function readProvisioningDefaults(config) {
107
108
  * Extracts the authenticated Backstage user identity from the request token.
108
109
  * Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
109
110
  * the request carries no user credential (service-to-service calls).
111
+ * Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
110
112
  */
111
113
  async function resolveUserId(req, auth) {
112
- const rawToken = req.headers.authorization?.slice(7);
113
- if (!rawToken)
114
+ const authHeader = req.headers.authorization ?? '';
115
+ if (!authHeader.startsWith('Bearer '))
114
116
  return undefined;
117
+ const rawToken = authHeader.slice(7);
115
118
  try {
116
119
  const credentials = await auth.authenticate(rawToken);
117
120
  const principal = credentials.principal;
@@ -120,18 +123,20 @@ async function resolveUserId(req, auth) {
120
123
  }
121
124
  }
122
125
  catch {
123
- // invalid or service token — caller gets query-param fallback
126
+ // invalid or service token
124
127
  }
125
128
  return undefined;
126
129
  }
127
130
  /**
128
131
  * Like resolveUserId, but returns the raw BackstageCredentials object
129
132
  * (rather than just the entity ref) for passing into PermissionsService.authorize().
133
+ * Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
130
134
  */
131
135
  async function resolveCredentials(req, auth) {
132
- const rawToken = req.headers.authorization?.slice(7);
133
- if (!rawToken)
136
+ const authHeader = req.headers.authorization ?? '';
137
+ if (!authHeader.startsWith('Bearer '))
134
138
  return undefined;
139
+ const rawToken = authHeader.slice(7);
135
140
  try {
136
141
  return await auth.authenticate(rawToken);
137
142
  }
@@ -236,20 +241,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
236
241
  * requests for a re-deleted user can still trigger fresh provisioning.
237
242
  */
238
243
  const provisioningInFlight = new Map();
239
- /**
240
- * Strips any echoed Authorization bearer token from upstream LiteLLM error
241
- * messages before they're shipped back to the browser. LiteLLM normally does
242
- * not echo the master key, but defense in depth: never let a `Bearer …`
243
- * substring travel out in a response body.
244
- */
245
- function sanitizeUpstreamMessage(message) {
246
- if (!message)
247
- return 'unknown error';
248
- return message
249
- .replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, 'Bearer [redacted]')
250
- .replace(/sk-[A-Za-z0-9_\-]{8,}/g, 'sk-[redacted]')
251
- .slice(0, 500);
252
- }
253
244
  class ProvisioningError extends Error {
254
245
  constructor(message, hint, provisioning, status = 404) {
255
246
  super(message);
@@ -331,7 +322,7 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
331
322
  ? upstreamStatus
332
323
  : 502;
333
324
  const errMessage = err instanceof Error ? err.message : String(err);
334
- throw new ProvisioningError('LiteLLM auto-provisioning failed', `LiteLLM upstream ${upstreamStatus ?? 'error'}: ${sanitizeUpstreamMessage(errMessage)}`, true, passThrough);
325
+ throw new ProvisioningError('LiteLLM auto-provisioning failed', `LiteLLM upstream ${upstreamStatus ?? 'error'}: ${(0, errors_1.sanitizeUpstreamMessage)(errMessage)}`, true, passThrough);
335
326
  }
336
327
  })();
337
328
  provisioningInFlight.set(userId, provisionPromise);
package/dist/router.js CHANGED
@@ -148,22 +148,6 @@ async function createRouter(options) {
148
148
  }
149
149
  return { tokenEntityRef, userId, key };
150
150
  },
151
- sendOwnershipError: (err, res) => {
152
- // Handle NotAllowedError from authorizeKeyAction
153
- if (err instanceof errors_1.NotAllowedError) {
154
- res.status(403).json({ error: err.message });
155
- return true;
156
- }
157
- // Fallback for legacy errors with status and body
158
- if (err && typeof err === 'object' && 'status' in err && 'body' in err) {
159
- const errObj = err;
160
- if (typeof errObj.status === 'number' && errObj.body) {
161
- res.status(errObj.status).json(errObj.body);
162
- return true;
163
- }
164
- }
165
- return false;
166
- },
167
151
  assertPermission: async (req, permission) => {
168
152
  const credentials = await (0, provisioning_1.resolveCredentials)(req, auth);
169
153
  if (!credentials)
@@ -201,13 +185,19 @@ async function createRouter(options) {
201
185
  sendTeamError: (err, res) => {
202
186
  (0, errors_2.sendError)(res, err, logger, 'team operation');
203
187
  },
204
- authorizeTeamSubresource: async (req, res, permission) => {
188
+ authorizeTeamSubresource: async (req, res, permission, opts) => {
205
189
  if (!ctx.requireTeamMgmt(res))
206
190
  return null;
207
191
  if (!teamAdminCfg.group) {
208
192
  res.status(500).json({ error: 'Team management is misconfigured (group is missing)' });
209
193
  return null;
210
194
  }
195
+ if (teamAdminCfg.readOnly) {
196
+ res.status(403).json({
197
+ error: 'Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage',
198
+ });
199
+ return null;
200
+ }
211
201
  const check = await (0, teamAdmin_1.assertTeamAdmin)({
212
202
  req,
213
203
  auth,
@@ -217,7 +207,11 @@ async function createRouter(options) {
217
207
  permission,
218
208
  logger,
219
209
  });
220
- if (!check.ok) {
210
+ // A caller outside the admin group may still qualify through their
211
+ // LiteLLM team role (litellm.teamAdmin.memberManagerRoles), so a 403
212
+ // here is deferred until that path has been tried.
213
+ const teamRoleEligible = !!opts?.allowTeamRole && teamAdminCfg.memberManagerRoles.length > 0;
214
+ if (!check.ok && !(check.status === 403 && teamRoleEligible)) {
221
215
  res.status(check.status).json({ error: check.error });
222
216
  return null;
223
217
  }
@@ -241,25 +235,53 @@ async function createRouter(options) {
241
235
  const owningGroup = typeof existing.metadata?.owning_group === 'string'
242
236
  ? existing.metadata.owning_group
243
237
  : undefined;
244
- if (!owningGroup) {
245
- res.status(403).json({
238
+ // Group path: admin-group member whose group owns the team.
239
+ let denial;
240
+ if (!check.ok) {
241
+ denial = { status: check.status, error: check.error };
242
+ }
243
+ else if (!owningGroup) {
244
+ denial = {
245
+ status: 403,
246
246
  error: 'This team is not managed by Backstage team admins and cannot be edited here',
247
- });
248
- return null;
247
+ };
249
248
  }
250
- const owns = await (0, provisioning_1.isUserMemberOfGroup)(check.userEntityRef, owningGroup, catalogClient, auth, logger);
251
- if (!owns) {
252
- res
253
- .status(403)
254
- .json({ error: `Access denied: team is owned by ${owningGroup}` });
255
- return null;
249
+ else if (await (0, provisioning_1.isUserMemberOfGroup)(check.userEntityRef, owningGroup, catalogClient, auth, logger)) {
250
+ return {
251
+ teamId,
252
+ owningGroup,
253
+ actor: check.userEntityRef,
254
+ team: existing,
255
+ via: 'group',
256
+ };
257
+ }
258
+ else {
259
+ denial = {
260
+ status: 403,
261
+ error: `Access denied: team is owned by ${owningGroup}`,
262
+ };
263
+ }
264
+ // Team-role path: the caller holds an allowed role in this very team.
265
+ // The permission framework still has the final say.
266
+ if (teamRoleEligible) {
267
+ const actor = await (0, provisioning_1.resolveUserId)(req, auth);
268
+ const litellmUserId = actor && (0, provisioning_1.toLiteLLMUserId)(actor, userIdDomain);
269
+ const role = existing.members_with_roles?.find(m => m.user_id === litellmUserId)?.role;
270
+ if (actor &&
271
+ role &&
272
+ teamAdminCfg.memberManagerRoles.includes(role) &&
273
+ (await ctx.assertPermission(req, permission))) {
274
+ return {
275
+ teamId,
276
+ owningGroup: owningGroup ?? '',
277
+ actor,
278
+ team: existing,
279
+ via: 'teamRole',
280
+ };
281
+ }
256
282
  }
257
- return {
258
- teamId,
259
- owningGroup,
260
- actor: check.userEntityRef,
261
- team: existing,
262
- };
283
+ res.status(denial.status).json({ error: denial.error });
284
+ return null;
263
285
  },
264
286
  };
265
287
  const router = (0, express_1.Router)();
@@ -8,7 +8,7 @@ const keyService_1 = require("../services/keyService");
8
8
  const errors_1 = require("../errors");
9
9
  const backstage_plugin_litellm_common_2 = require("@acarmisc/backstage-plugin-litellm-common");
10
10
  function registerBridgeRoutes(router, ctx, bridgeOpts) {
11
- const { client, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, allowUnlimitedBudget, teamRequired, keyValidationConfig, } = ctx;
11
+ const { client, logger, userIdDomain, provisioningEnabled, provisioningDefaults, allowUnlimitedBudget, teamRequired, keyValidationConfig, } = ctx;
12
12
  const { config } = bridgeOpts;
13
13
  const generateKeyInputSchema = (0, backstage_plugin_litellm_common_2.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
14
14
  // ---------------------------------------------------------------------------
@@ -99,8 +99,8 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
99
99
  if (!allowUnlimitedBudget && input.max_budget === undefined) {
100
100
  input.max_budget = provisioningDefaults.maxBudget;
101
101
  }
102
- // ── Resolve user identity from verified claims ───────────────────────
103
- const userId = (0, bridge_1.resolveBridgeUserId)(claims, bridgeIdentity);
102
+ // ── Resolve and provision user from verified claims ──────────────────
103
+ const userInfo = await (0, bridge_1.getOrProvisionUserFromClaims)(client, claims, provisioningEnabled, provisioningDefaults, logger, bridgeIdentity);
104
104
  // ── Create key via unified service ──────────────────────────────────
105
105
  const keyCreateCtx = {
106
106
  client,
@@ -108,18 +108,10 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
108
108
  allowUnlimitedBudget,
109
109
  teamRequired,
110
110
  },
111
- // Bridge does not have Backstage catalogClient/auth
112
- // so provisioning from JWT claims only (no Backstage profile enrichment)
113
- catalogClient: undefined,
114
- auth: undefined,
115
111
  logger,
116
112
  keyValidationConfig,
117
- provisioningEnabled,
118
- provisioningDefaults,
119
- roleConfigs,
120
- userIdDomain,
121
113
  };
122
- const result = await (0, keyService_1.createKeyForUser)({ userId }, input, keyCreateCtx);
114
+ const result = await (0, keyService_1.createKeyForUser)({ userInfo, createdVia: 'abby-cli' }, input, keyCreateCtx);
123
115
  res.json(result);
124
116
  }
125
117
  catch (error) {
@@ -132,23 +124,22 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
132
124
  try {
133
125
  const claims = await requireClaims(req);
134
126
  const user = await (0, bridge_1.getOrProvisionUserFromClaims)(client, claims, provisioningEnabled, provisioningDefaults, logger, bridgeIdentity);
135
- // Enrich team list with metadata (display names, etc.)
127
+ // Enrich team list with metadata (display names, etc.) in parallel
136
128
  const teamIds = user.teams ?? [];
137
- const teamMetadata = [];
138
- for (const teamId of teamIds) {
129
+ const teamMetadata = await Promise.all(teamIds.map(async (teamId) => {
139
130
  try {
140
131
  const teamInfo = await client.getTeamInfo(teamId);
141
- teamMetadata.push({
132
+ return {
142
133
  id: teamId,
143
134
  name: teamInfo.team_alias || teamId, // Use alias as display name, fallback to ID
144
- });
135
+ };
145
136
  }
146
137
  catch (e) {
147
138
  // If we can't fetch team info, just use the ID
148
139
  logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
149
- teamMetadata.push({ id: teamId });
140
+ return { id: teamId };
150
141
  }
151
- }
142
+ }));
152
143
  res.json({ user_id: user.user_id, teams: teamIds, team_metadata: teamMetadata });
153
144
  }
154
145
  catch (error) {
@@ -26,6 +26,8 @@ function registerConfigRoutes(router, ctx) {
26
26
  maxBudgetCeiling: ctx.teamAdminCfg.maxBudgetCeiling,
27
27
  allowUnlimitedBudget: ctx.teamAdminCfg.allowUnlimitedBudget,
28
28
  objectPermissionsEnabled: ctx.objectPermsEnabled,
29
+ readOnly: ctx.teamAdminCfg.readOnly,
30
+ memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles,
29
31
  },
30
32
  display: {
31
33
  hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
@@ -40,16 +40,18 @@ export interface RouterContext {
40
40
  userId: string;
41
41
  key: VirtualKey;
42
42
  }>;
43
- sendOwnershipError(err: unknown, res: Response): boolean;
44
43
  assertPermission(req: Request, permission: BasicPermission): Promise<boolean>;
45
44
  sendPermissionDenied(res: Response, permission: BasicPermission): void;
46
45
  requireTeamMgmt(res: Response): boolean;
47
46
  requireObjectPerms(res: Response): boolean;
48
47
  sendTeamError(err: unknown, res: Response): void;
49
- authorizeTeamSubresource(req: Request, res: Response, permission: BasicPermission): Promise<{
48
+ authorizeTeamSubresource(req: Request, res: Response, permission: BasicPermission, opts?: {
49
+ allowTeamRole?: boolean;
50
+ }): Promise<{
50
51
  teamId: string;
51
52
  owningGroup: string;
52
53
  actor: string;
53
54
  team: TeamInfo;
55
+ via: 'group' | 'teamRole';
54
56
  } | null>;
55
57
  }
@@ -13,7 +13,7 @@ const withUser_1 = require("./middleware/withUser");
13
13
  * location and access to shared state (via ctx) differ.
14
14
  */
15
15
  function registerKeysRoutes(router, ctx) {
16
- const { client, catalogClient, auth, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, auditGroup, allowUnlimitedBudget, teamRequired, allowOwnerResetSpend, keyValidationConfig, authorizeKeyAction, sendOwnershipError, assertPermission, sendPermissionDenied, } = ctx;
16
+ const { client, catalogClient, auth, logger, auditGroup, allowUnlimitedBudget, teamRequired, allowOwnerResetSpend, keyValidationConfig, authorizeKeyAction, assertPermission, sendPermissionDenied, } = ctx;
17
17
  const generateKeyInputSchema = (0, backstage_plugin_litellm_common_1.createGenerateKeyInputSchema)(keyValidationConfig);
18
18
  const updateKeyInputSchema = (0, backstage_plugin_litellm_common_1.createUpdateKeyInputSchema)(keyValidationConfig);
19
19
  router.get('/user/info', async (_req, res) => {
@@ -132,7 +132,8 @@ function registerKeysRoutes(router, ctx) {
132
132
  return;
133
133
  }
134
134
  const tokenEntityRef = res.locals.tokenEntityRef;
135
- const resolvedUserId = res.locals.userId;
135
+ // ── Resolve and provision user ──────────────────────────────────────
136
+ const userInfo = await (0, withUser_1.getProvisionedUser)(ctx, res);
136
137
  // ── Create key via unified service ──────────────────────────────────
137
138
  const keyCreateCtx = {
138
139
  client,
@@ -144,12 +145,8 @@ function registerKeysRoutes(router, ctx) {
144
145
  auth,
145
146
  logger,
146
147
  keyValidationConfig,
147
- provisioningEnabled,
148
- provisioningDefaults,
149
- roleConfigs,
150
- userIdDomain,
151
148
  };
152
- const result = await (0, keyService_1.createKeyForUser)({ tokenEntityRef, userId: resolvedUserId }, input, keyCreateCtx);
149
+ const result = await (0, keyService_1.createKeyForUser)({ tokenEntityRef, userInfo }, input, keyCreateCtx);
153
150
  res.json(result);
154
151
  }
155
152
  catch (error) {
@@ -277,8 +274,6 @@ function registerKeysRoutes(router, ctx) {
277
274
  res.json(result);
278
275
  }
279
276
  catch (error) {
280
- if (sendOwnershipError(error, res))
281
- return;
282
277
  (0, errors_1.sendError)(res, error, logger, 'update key');
283
278
  }
284
279
  });
@@ -299,8 +294,6 @@ function registerKeysRoutes(router, ctx) {
299
294
  res.json({ success: true });
300
295
  }
301
296
  catch (error) {
302
- if (sendOwnershipError(error, res))
303
- return;
304
297
  (0, errors_1.sendError)(res, error, logger, 'delete key');
305
298
  }
306
299
  });
@@ -334,8 +327,6 @@ function registerKeysRoutes(router, ctx) {
334
327
  res.json({ success: true });
335
328
  }
336
329
  catch (error) {
337
- if (sendOwnershipError(error, res))
338
- return;
339
330
  (0, errors_1.sendError)(res, error, logger, 'block key');
340
331
  }
341
332
  });
@@ -371,8 +362,6 @@ function registerKeysRoutes(router, ctx) {
371
362
  res.json({ success: true });
372
363
  }
373
364
  catch (error) {
374
- if (sendOwnershipError(error, res))
375
- return;
376
365
  (0, errors_1.sendError)(res, error, logger, 'unblock key');
377
366
  }
378
367
  });
@@ -397,8 +386,6 @@ function registerKeysRoutes(router, ctx) {
397
386
  res.json({ success: true });
398
387
  }
399
388
  catch (error) {
400
- if (sendOwnershipError(error, res))
401
- return;
402
389
  (0, errors_1.sendError)(res, error, logger, 'reset key spend');
403
390
  }
404
391
  });
@@ -454,10 +441,10 @@ function registerKeysRoutes(router, ctx) {
454
441
  res.status(400).json({ error: 'start_date and end_date are required' });
455
442
  return;
456
443
  }
457
- const tokenEntityRef = res.locals.tokenEntityRef;
458
- const userId = res.locals.userId;
459
- await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
460
- const usage = await client.getUsage(start_date, end_date, userId);
444
+ await (0, withUser_1.getProvisionedUser)(ctx, res);
445
+ // Always filter by the caller's id: an empty user_id makes LiteLLM
446
+ // return org-wide activity.
447
+ const usage = await client.getUsage(start_date, end_date, res.locals.userId);
461
448
  res.json(usage);
462
449
  }
463
450
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { UserInfo } from '../../types';
2
3
  import type { RouterContext } from '../context';
3
4
  /**
4
5
  * Express middleware that resolves a Backstage user principal to a LiteLLM user ID.
@@ -12,7 +13,7 @@ export declare function createRequireUser(ctx: RouterContext): (req: Request, re
12
13
  * Returns the user info or throws ProvisioningError (which the caller must handle and respond to).
13
14
  *
14
15
  * This deduplicates the repeated pattern:
15
- * const userInfo = await getOrProvisionUser(...);
16
+ * const userInfo = await getProvisionedUser(...);
16
17
  * if (error instanceof ProvisioningError) { res.status(...).json(...); return; }
17
18
  */
18
- export declare function getProvisionedUser(ctx: RouterContext, res: Response): Promise<any>;
19
+ export declare function getProvisionedUser(ctx: RouterContext, res: Response): Promise<UserInfo>;
@@ -31,7 +31,7 @@ function createRequireUser(ctx) {
31
31
  * Returns the user info or throws ProvisioningError (which the caller must handle and respond to).
32
32
  *
33
33
  * This deduplicates the repeated pattern:
34
- * const userInfo = await getOrProvisionUser(...);
34
+ * const userInfo = await getProvisionedUser(...);
35
35
  * if (error instanceof ProvisioningError) { res.status(...).json(...); return; }
36
36
  */
37
37
  async function getProvisionedUser(ctx, res) {
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerObjectPermissionsRoutes = registerObjectPermissionsRoutes;
4
4
  const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
5
5
  const teamAdmin_1 = require("../teamAdmin");
6
- const teamBudgetVisibility_1 = require("../teamBudgetVisibility");
6
+ const respondTeam_1 = require("../http/respondTeam");
7
7
  function registerObjectPermissionsRoutes(router, ctx) {
8
- const { client, catalogClient, auth, permissions, logger, teamAdminCfg, teamBudgetVisibility, requireObjectPerms, sendTeamError, authorizeTeamSubresource, } = ctx;
8
+ const { client, catalogClient, auth, permissions, logger, teamAdminCfg, requireObjectPerms, sendTeamError, authorizeTeamSubresource, } = ctx;
9
9
  // ── Knowledge-base (vector store) management ────────────────────────────
10
10
  //
11
11
  // Attaching a vector store to a team exposes its documents to every team
@@ -91,9 +91,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
91
91
  owningGroup,
92
92
  vector_stores: requested,
93
93
  });
94
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
95
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
96
- : updated);
94
+ (0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
97
95
  }
98
96
  catch (err) {
99
97
  sendTeamError(err, res);
@@ -185,9 +183,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
185
183
  before,
186
184
  after: requested,
187
185
  });
188
- res.json(teamBudgetVisibility.hideTeamBudgetForManagers
189
- ? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
190
- : updated);
186
+ (0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
191
187
  }
192
188
  catch (err) {
193
189
  sendTeamError(err, res);
@@ -7,7 +7,7 @@ const errors_1 = require("../errors");
7
7
  const teamFetch_1 = require("../http/teamFetch");
8
8
  const withUser_1 = require("./middleware/withUser");
9
9
  function registerTeamUsageRoutes(router, ctx) {
10
- const { client, catalogClient, auth, logger, provisioningEnabled, provisioningDefaults, roleConfigs, teamBudgetVisibility, } = ctx;
10
+ const { client, catalogClient, auth, logger, teamBudgetVisibility, } = ctx;
11
11
  const requireUser = (0, withUser_1.createRequireUser)(ctx);
12
12
  router.get('/teams/:teamId/usage', requireUser, async (req, res) => {
13
13
  try {
@@ -19,8 +19,7 @@ function registerTeamUsageRoutes(router, ctx) {
19
19
  }
20
20
  // Authorize: member of the team OR a team manager
21
21
  const tokenEntityRef = res.locals.tokenEntityRef;
22
- const userId = res.locals.userId;
23
- const userInfo = await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
22
+ const userInfo = await (0, withUser_1.getProvisionedUser)(ctx, res);
24
23
  // Check membership: user must be in the team OR be a team manager
25
24
  const isMember = userInfo?.teams?.includes(teamId) ?? false;
26
25
  // A "manager" administers this specific team: team management is enabled