@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 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/README.md +58 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +204 -405
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +0 -16
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/context.d.ts +0 -1
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +7 -20
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
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
|
|
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,
|
|
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.
|
|
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; } });
|
package/dist/provisioning.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/provisioning.js
CHANGED
|
@@ -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
|
|
113
|
-
if (!
|
|
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
|
|
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
|
|
133
|
-
if (!
|
|
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)
|
package/dist/routes/bridge.js
CHANGED
|
@@ -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,
|
|
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
|
|
103
|
-
const
|
|
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)({
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/routes/context.d.ts
CHANGED
|
@@ -40,7 +40,6 @@ 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;
|
package/dist/routes/keys.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
6
|
+
const respondTeam_1 = require("../http/respondTeam");
|
|
7
7
|
function registerObjectPermissionsRoutes(router, ctx) {
|
|
8
|
-
const { client, catalogClient, auth, permissions, logger, teamAdminCfg,
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/routes/teamUsage.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
package/dist/routes/teams.js
CHANGED
|
@@ -4,13 +4,12 @@ exports.registerTeamsRoutes = registerTeamsRoutes;
|
|
|
4
4
|
const provisioning_1 = require("../provisioning");
|
|
5
5
|
const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
6
6
|
const teamAdmin_1 = require("../teamAdmin");
|
|
7
|
-
const teamBudgetVisibility_1 = require("../teamBudgetVisibility");
|
|
8
7
|
const errors_1 = require("../errors");
|
|
9
8
|
const withUser_1 = require("./middleware/withUser");
|
|
10
9
|
const respondTeam_1 = require("../http/respondTeam");
|
|
11
10
|
const teamFetch_1 = require("../http/teamFetch");
|
|
12
11
|
function registerTeamsRoutes(router, ctx) {
|
|
13
|
-
const { client, catalogClient, auth, permissions, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, teamAdminCfg,
|
|
12
|
+
const { client, catalogClient, auth, permissions, logger, userIdDomain, provisioningEnabled, provisioningDefaults, roleConfigs, teamAdminCfg, requireTeamMgmt, sendTeamError, authorizeTeamSubresource, } = ctx;
|
|
14
13
|
const requireUser = (0, withUser_1.createRequireUser)(ctx);
|
|
15
14
|
router.get('/teams', requireUser, async (_req, res) => {
|
|
16
15
|
try {
|
|
@@ -83,7 +82,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
83
82
|
logger.info(`Team creation already in flight for ${key} — joining`);
|
|
84
83
|
try {
|
|
85
84
|
const result = await pending;
|
|
86
|
-
|
|
85
|
+
(0, respondTeam_1.respondTeam)(res, result, ctx, 'manager');
|
|
87
86
|
}
|
|
88
87
|
catch (err) {
|
|
89
88
|
sendTeamError(err, res);
|
|
@@ -116,10 +115,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
116
115
|
teamFetch_1.teamCreateInFlight.set(key, createPromise);
|
|
117
116
|
try {
|
|
118
117
|
const result = await createPromise;
|
|
119
|
-
|
|
120
|
-
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
121
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(result)
|
|
122
|
-
: result);
|
|
118
|
+
(0, respondTeam_1.respondTeam)(res, result, ctx, 'manager');
|
|
123
119
|
}
|
|
124
120
|
catch (err) {
|
|
125
121
|
sendTeamError(err, res);
|
|
@@ -166,9 +162,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
166
162
|
teamId,
|
|
167
163
|
owningGroup,
|
|
168
164
|
});
|
|
169
|
-
|
|
170
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(r)
|
|
171
|
-
: r);
|
|
165
|
+
(0, respondTeam_1.respondTeam)(res, r, ctx, 'manager');
|
|
172
166
|
}
|
|
173
167
|
catch (err) {
|
|
174
168
|
sendTeamError(err, res);
|
|
@@ -240,10 +234,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
240
234
|
// the caller's group set and filter with includes().
|
|
241
235
|
const owned = all.filter(t => typeof t.metadata?.owning_group === 'string' &&
|
|
242
236
|
t.metadata.owning_group === teamAdminCfg.group);
|
|
243
|
-
|
|
244
|
-
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
245
|
-
? owned.map(teamBudgetVisibility_1.redactTeamBudget)
|
|
246
|
-
: owned);
|
|
237
|
+
(0, respondTeam_1.respondTeamList)(res, owned, ctx, 'manager');
|
|
247
238
|
}
|
|
248
239
|
catch (err) {
|
|
249
240
|
sendTeamError(err, res);
|
|
@@ -343,9 +334,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
343
334
|
via,
|
|
344
335
|
});
|
|
345
336
|
const updated = await (0, teamFetch_1.withTeamFetchRetry)(() => client.getTeamInfo(teamId));
|
|
346
|
-
|
|
347
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
348
|
-
: updated);
|
|
337
|
+
(0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
|
|
349
338
|
}
|
|
350
339
|
catch (err) {
|
|
351
340
|
sendTeamError(err, res);
|
|
@@ -395,9 +384,7 @@ function registerTeamsRoutes(router, ctx) {
|
|
|
395
384
|
via,
|
|
396
385
|
});
|
|
397
386
|
const updated = await (0, teamFetch_1.withTeamFetchRetry)(() => client.getTeamInfo(teamId));
|
|
398
|
-
|
|
399
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
400
|
-
: updated);
|
|
387
|
+
(0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
|
|
401
388
|
}
|
|
402
389
|
catch (err) {
|
|
403
390
|
sendTeamError(err, res);
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Key generation service
|
|
2
|
+
* Key generation service.
|
|
3
3
|
*
|
|
4
4
|
* Extracts the common logic for creating API keys from both the UI (POST /keys/generate)
|
|
5
|
-
* and the bridge (POST /bridge/keys). Handles validation,
|
|
6
|
-
* and explicit upstream request construction.
|
|
5
|
+
* and the bridge (POST /bridge/keys). Handles validation, team/model checks,
|
|
6
|
+
* and explicit upstream request construction. Callers resolve the user beforehand.
|
|
7
7
|
*/
|
|
8
8
|
import { AuthService, LoggerService } from '@backstage/backend-plugin-api';
|
|
9
9
|
import { CatalogClient } from '@backstage/catalog-client';
|
|
10
10
|
import { LiteLLMClient } from '../client';
|
|
11
|
-
import { GenerateKeyResponse,
|
|
11
|
+
import { GenerateKeyResponse, UserInfo } from '../types';
|
|
12
12
|
import { type KeyValidationConfig, type GenerateKeyInput } from '@acarmisc/backstage-plugin-litellm-common';
|
|
13
|
+
/** User identity for key creation — already resolved by the caller. */
|
|
13
14
|
export interface KeyCreateUser {
|
|
14
15
|
/** Backstage user entity ref, e.g. "user:default/alice" (may be empty for bridge). */
|
|
15
16
|
tokenEntityRef?: string;
|
|
16
|
-
/** Resolved LiteLLM
|
|
17
|
-
|
|
17
|
+
/** Resolved user info from LiteLLM. Required. */
|
|
18
|
+
userInfo: UserInfo;
|
|
19
|
+
/** How the key was created (for metadata stamping). Defaults to 'backstage'. */
|
|
20
|
+
createdVia?: string;
|
|
18
21
|
}
|
|
19
22
|
/** Context needed to create a key — configuration, clients, and services. */
|
|
20
23
|
export interface KeyCreateContext {
|
|
@@ -27,10 +30,6 @@ export interface KeyCreateContext {
|
|
|
27
30
|
auth?: AuthService;
|
|
28
31
|
logger: LoggerService;
|
|
29
32
|
keyValidationConfig: KeyValidationConfig;
|
|
30
|
-
provisioningEnabled: boolean;
|
|
31
|
-
provisioningDefaults: ProvisioningDefaults;
|
|
32
|
-
roleConfigs: RoleConfig[];
|
|
33
|
-
userIdDomain?: string;
|
|
34
33
|
}
|
|
35
34
|
/** Error thrown during key creation — maps to HTTP responses. */
|
|
36
35
|
export declare class KeyServiceError extends Error {
|
|
@@ -44,18 +43,16 @@ export declare class KeyServiceError extends Error {
|
|
|
44
43
|
* Performs:
|
|
45
44
|
* - Schema validation of the input
|
|
46
45
|
* - Config flag enforcement (allowUnlimitedBudget, teamRequired)
|
|
47
|
-
* - User provisioning (single-flight, role configs)
|
|
48
46
|
* - Team membership validation
|
|
49
47
|
* - Model subset validation
|
|
50
48
|
* - Metadata stamping
|
|
51
49
|
* - Explicit upstream request construction and call
|
|
52
50
|
*
|
|
53
|
-
* @param user
|
|
51
|
+
* @param user Resolved identity: tokenEntityRef (optional) + userInfo (required, pre-provisioned)
|
|
54
52
|
* @param input Parsed GenerateKeyInput from the schema
|
|
55
53
|
* @param ctx Configuration and service clients
|
|
56
54
|
* @returns The new key response from LiteLLM
|
|
57
55
|
* @throws KeyServiceError with status and body for HTTP response
|
|
58
|
-
* @throws ProvisioningError for provisioning failures
|
|
59
56
|
* @throws LiteLLMUpstreamError for upstream LiteLLM errors
|
|
60
57
|
*/
|
|
61
58
|
export declare function createKeyForUser(user: KeyCreateUser, input: GenerateKeyInput, ctx: KeyCreateContext): Promise<GenerateKeyResponse>;
|