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