@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 ADDED
@@ -0,0 +1,58 @@
1
+ # @acarmisc/backstage-plugin-litellm-backend
2
+
3
+ Backend for the **Backstage LiteLLM Governance Plugin** (New Backend System).
4
+ It is the only component that talks to your [LiteLLM](https://github.com/BerriAI/litellm)
5
+ proxy: it holds the master key, resolves the caller's Backstage identity, and
6
+ enforces budgets, key limits, team membership, model access and permissions
7
+ before calling LiteLLM.
8
+
9
+ Pair it with the frontend, [`@acarmisc/backstage-plugin-litellm`](https://www.npmjs.com/package/@acarmisc/backstage-plugin-litellm).
10
+
11
+ ![How the backend handles a key request](https://raw.githubusercontent.com/acarmisc/backstage-plugin-litellm-govai/main/docs/images/architecture.png)
12
+
13
+ Full documentation — adoption guide, configuration reference, permissions,
14
+ security model and architecture — starts at the
15
+ [project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ yarn --cwd packages/backend add @acarmisc/backstage-plugin-litellm-backend
21
+ ```
22
+
23
+ In `packages/backend/src/index.ts`:
24
+
25
+ ```ts
26
+ backend.add(import('@acarmisc/backstage-plugin-litellm-backend'));
27
+ ```
28
+
29
+ ## Minimal configuration
30
+
31
+ ```yaml
32
+ litellm:
33
+ baseUrl: http://litellm-proxy:4000 # internal URL of the LiteLLM proxy
34
+ publicBaseUrl: https://llm.example.com # optional: shown to users in snippets
35
+ masterKey: ${LITELLM_MASTER_KEY}
36
+ userIdDomain: example.com # optional: user:default/jane → jane@example.com
37
+ provisioning:
38
+ enabled: true # optional: create LiteLLM users on first visit
39
+ ```
40
+
41
+ The configuration schema ships with the package (`config.d.ts`); see the
42
+ [configuration reference](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/configuration.md)
43
+ for every key (key ceilings, team management, budget hiding, CLI bridge,
44
+ OpenCode connect).
45
+
46
+ ## What it exposes
47
+
48
+ Routes are mounted under `/api/litellm`. A machine-readable OpenAPI 3.1
49
+ description is served at `/api/litellm/openapi.json`; the
50
+ [endpoint table](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/api.md)
51
+ summarises them.
52
+
53
+ The plugin registers the `litellm.*` permissions with the permission
54
+ framework (see [Permissions](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/permissions.md)).
55
+
56
+ ## License
57
+
58
+ Apache-2.0
package/config.d.ts CHANGED
@@ -324,6 +324,34 @@ export interface Config {
324
324
  */
325
325
  allowTeamDelete?: boolean;
326
326
 
327
+ /**
328
+ * When true, all team write operations return 403 even for admins:
329
+ * POST /teams, PATCH /teams/:id, DELETE /teams/:id,
330
+ * POST/DELETE /teams/:id/members, and PUT objectPermissions routes.
331
+ * Reads (GET /teams, GET /teams/managed) keep working.
332
+ * Error message: 'Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage'.
333
+ * @default false
334
+ */
335
+ readOnly?: boolean;
336
+
337
+ /**
338
+ * Backstage group refs that must include the caller for team creation.
339
+ * When non-empty, POST /teams additionally requires the caller to be
340
+ * a member of at least one of these groups, narrowing the permission.
341
+ * Error message: 'Access denied: team creation is limited to <comma-joined groups>'.
342
+ * @default []
343
+ */
344
+ createGroups?: string[];
345
+
346
+ /**
347
+ * LiteLLM team roles whose holders may add/remove members of their own team.
348
+ * When non-empty, callers with one of these roles in members_with_roles
349
+ * can POST/DELETE /teams/:teamId/members without owning the team's group.
350
+ * Allows non-admins to manage their team's membership without full admin access.
351
+ * @default []
352
+ */
353
+ memberManagerRoles?: string[];
354
+
327
355
  /**
328
356
  * Object-permission management (attaching knowledge bases / MCP servers
329
357
  * to a team). This is the highest-risk surface — attaching a vector store
package/dist/bridge.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Config } from '@backstage/config';
2
2
  import { LoggerService } from '@backstage/backend-plugin-api';
3
3
  import { LiteLLMClient } from './client';
4
- import { GenerateKeyRequest, GenerateKeyResponse, ProvisioningDefaults, UserInfo, VirtualKey } from './types';
4
+ import { ProvisioningDefaults, UserInfo, VirtualKey } from './types';
5
5
  /** Claims extracted from a verified Keycloak access token. */
6
6
  export interface BridgeClaims {
7
7
  sub: string;
@@ -94,7 +94,7 @@ export interface BridgeIdentityOptions {
94
94
  *
95
95
  * @throws BridgeIdentityError (403) if identity validation fails
96
96
  */
97
- export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: string | BridgeIdentityOptions): string;
97
+ export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: BridgeIdentityOptions): string;
98
98
  /**
99
99
  * Ensures a LiteLLM user exists for the verified identity. If the user is
100
100
  * missing and provisioning is enabled, creates it from the JWT claims (email +
@@ -107,8 +107,6 @@ export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: str
107
107
  * preferred_username), and helps recover from cases where the bridge's userId
108
108
  * computation and the UI's differ.
109
109
  */
110
- export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string | BridgeIdentityOptions): Promise<UserInfo>;
110
+ export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, identity?: BridgeIdentityOptions): Promise<UserInfo>;
111
111
  /** Lists the caller's virtual keys (provisioning the user first if needed). */
112
- export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string | BridgeIdentityOptions): Promise<VirtualKey[]>;
113
- /** Mints a new virtual key for the caller (provisioning the user first if needed). */
114
- export declare function bridgeGenerateKey(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, request: Partial<GenerateKeyRequest>, userIdDomain?: string | BridgeIdentityOptions): Promise<GenerateKeyResponse>;
112
+ export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, identity?: BridgeIdentityOptions): Promise<VirtualKey[]>;
package/dist/bridge.js CHANGED
@@ -6,7 +6,6 @@ exports.newDefaultVerifier = newDefaultVerifier;
6
6
  exports.resolveBridgeUserId = resolveBridgeUserId;
7
7
  exports.getOrProvisionUserFromClaims = getOrProvisionUserFromClaims;
8
8
  exports.bridgeListKeys = bridgeListKeys;
9
- exports.bridgeGenerateKey = bridgeGenerateKey;
10
9
  /**
11
10
  * Bridge — lets CLI clients (Abby) list/mint LiteLLM virtual keys without ever
12
11
  * holding the LiteLLM master key.
@@ -111,18 +110,16 @@ function newDefaultVerifier(cfg) {
111
110
  }
112
111
  return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
113
112
  }
114
- /** A bare string is the legacy form: `userIdDomain`, also the only trusted domain. */
115
113
  function normalizeIdentityOptions(opts) {
116
- const o = typeof opts === 'string' ? { userIdDomain: opts } : opts ?? {};
117
114
  let domains = [];
118
- if (o.trustedEmailDomains?.length) {
119
- domains = o.trustedEmailDomains;
115
+ if (opts.trustedEmailDomains?.length) {
116
+ domains = opts.trustedEmailDomains;
120
117
  }
121
- else if (o.userIdDomain) {
122
- domains = [o.userIdDomain];
118
+ else if (opts.userIdDomain) {
119
+ domains = [opts.userIdDomain];
123
120
  }
124
121
  const trusted = domains.map(d => d.toLowerCase());
125
- return { userIdDomain: o.userIdDomain, trusted };
122
+ return { userIdDomain: opts.userIdDomain, trusted };
126
123
  }
127
124
  /**
128
125
  * Resolves the LiteLLM user_id for a verified bridge token.
@@ -147,7 +144,7 @@ function normalizeIdentityOptions(opts) {
147
144
  * @throws BridgeIdentityError (403) if identity validation fails
148
145
  */
149
146
  function resolveBridgeUserId(claims, identity) {
150
- const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
147
+ const { userIdDomain, trusted } = normalizeIdentityOptions(identity ?? {});
151
148
  if (trusted.length === 0) {
152
149
  throw new BridgeIdentityError('set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge');
153
150
  }
@@ -193,8 +190,8 @@ function resolveBridgeUserId(claims, identity) {
193
190
  * preferred_username), and helps recover from cases where the bridge's userId
194
191
  * computation and the UI's differ.
195
192
  */
196
- async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
197
- const userId = resolveBridgeUserId(claims, userIdDomain);
193
+ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
194
+ const userId = resolveBridgeUserId(claims, identity);
198
195
  const existing = await client.getUserInfo(userId);
199
196
  if (existing)
200
197
  return existing;
@@ -222,22 +219,7 @@ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled,
222
219
  return created;
223
220
  }
224
221
  /** Lists the caller's virtual keys (provisioning the user first if needed). */
225
- async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
226
- const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain);
222
+ async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
223
+ const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity);
227
224
  return client.listKeys(user.user_id);
228
225
  }
229
- /** Mints a new virtual key for the caller (provisioning the user first if needed). */
230
- async function bridgeGenerateKey(client, claims, provisioningEnabled, provisioningDefaults, logger, request, userIdDomain) {
231
- const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain);
232
- const enriched = {
233
- ...request,
234
- user_id: user.user_id,
235
- metadata: {
236
- ...(request.metadata ?? {}),
237
- created_via: 'abby-cli',
238
- created_by: user.user_id,
239
- created_at_iso: new Date().toISOString(),
240
- },
241
- };
242
- return client.generateKey(enriched);
243
- }
package/dist/client.d.ts CHANGED
@@ -1,10 +1,4 @@
1
- import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogEntry, SpendLogsParams } from './types';
2
- /**
3
- * LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
4
- * current builds, but historically an object map. Normalise both to a flat
5
- * string array so callers can `includes('session:...')` uniformly.
6
- */
7
- export declare function normalizeRequestTags(tags: SpendLogEntry['request_tags']): string[];
1
+ import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
8
2
  /**
9
3
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
10
4
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -23,6 +17,12 @@ export declare class LiteLLMClient {
23
17
  private timeout;
24
18
  constructor(config: LiteLLMConfig, timeout?: number);
25
19
  private request;
20
+ /**
21
+ * Normalizes a raw LiteLLM user response to the UserInfo contract.
22
+ * Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
23
+ * response shapes, and converts team arrays to team_id strings.
24
+ */
25
+ private normalizeUserInfo;
26
26
  /**
27
27
  * Returns null when the user is not found in LiteLLM (404).
28
28
  * Throws on all other errors so callers know something went wrong.
@@ -126,8 +126,6 @@ export declare class LiteLLMClient {
126
126
  createTeam(payload: CreateTeamRequest): Promise<CreateTeamResponse>;
127
127
  updateTeam(payload: UpdateTeamRequest): Promise<unknown>;
128
128
  deleteTeam(teamId: string): Promise<unknown>;
129
- blockTeam(teamId: string): Promise<unknown>;
130
- unblockTeam(teamId: string): Promise<unknown>;
131
129
  /**
132
130
  * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
133
131
  * under a `member` object; `role` is 'user' or 'admin' (we only ever send
@@ -166,17 +164,6 @@ export declare class LiteLLMClient {
166
164
  name?: string;
167
165
  url?: string;
168
166
  }>>;
169
- /**
170
- * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
171
- * by date range and optionally by key/user/team. Rows are normalised so
172
- * `request_tags` is always a `string[]` (LiteLLM returns either an array
173
- * or an object depending on version) — callers group by tag to attribute
174
- * spend to a conversation or caller.
175
- *
176
- * This is the admin/DB-backed endpoint: it can return a large number of
177
- * rows, so callers should keep the date window tight and cap `page_size`.
178
- */
179
- getSpendLogs(params: SpendLogsParams): Promise<SpendLogEntry[]>;
180
167
  private emptyUsage;
181
168
  /**
182
169
  * Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
package/dist/client.js CHANGED
@@ -1,20 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LiteLLMClient = exports.LiteLLMUpstreamError = void 0;
4
- exports.normalizeRequestTags = normalizeRequestTags;
5
4
  const DEFAULT_TIMEOUT = 30000;
6
- /**
7
- * LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
8
- * current builds, but historically an object map. Normalise both to a flat
9
- * string array so callers can `includes('session:...')` uniformly.
10
- */
11
- function normalizeRequestTags(tags) {
12
- if (!tags)
13
- return [];
14
- if (Array.isArray(tags))
15
- return tags.filter((t) => typeof t === 'string');
16
- return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
17
- }
18
5
  /**
19
6
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
20
7
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -87,6 +74,33 @@ class LiteLLMClient {
87
74
  clearTimeout(timeoutId);
88
75
  }
89
76
  }
77
+ /**
78
+ * Normalizes a raw LiteLLM user response to the UserInfo contract.
79
+ * Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
80
+ * response shapes, and converts team arrays to team_id strings.
81
+ */
82
+ normalizeUserInfo(raw, fallbackUserId) {
83
+ const inner = raw?.user_info ?? {};
84
+ const teamIds = Array.isArray(raw?.teams)
85
+ ? raw.teams
86
+ .map((t) => (typeof t === 'string' ? t : t?.team_id))
87
+ .filter((t) => typeof t === 'string')
88
+ : [];
89
+ return {
90
+ user_id: raw?.user_id ?? inner.user_id ?? fallbackUserId ?? '',
91
+ user_email: inner.user_email ?? raw?.user_email,
92
+ email: inner.email ?? raw?.email,
93
+ teams: teamIds,
94
+ models: inner.models ?? raw?.models,
95
+ max_budget: inner.max_budget ?? raw?.max_budget,
96
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
97
+ budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
98
+ spend: inner.spend ?? raw?.spend,
99
+ current_spend: inner.current_spend ?? raw?.current_spend,
100
+ soft_limit: inner.soft_limit ?? raw?.soft_limit,
101
+ hard_limit: inner.hard_limit ?? raw?.hard_limit,
102
+ };
103
+ }
90
104
  /**
91
105
  * Returns null when the user is not found in LiteLLM (404).
92
106
  * Throws on all other errors so callers know something went wrong.
@@ -100,26 +114,7 @@ class LiteLLMClient {
100
114
  const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
101
115
  try {
102
116
  const raw = await this.request(`/user/info${query}`);
103
- const inner = raw?.user_info ?? {};
104
- const teamIds = Array.isArray(raw?.teams)
105
- ? raw.teams
106
- .map((t) => (typeof t === 'string' ? t : t?.team_id))
107
- .filter((t) => typeof t === 'string')
108
- : [];
109
- return {
110
- user_id: raw?.user_id ?? inner.user_id ?? userId ?? '',
111
- user_email: inner.user_email ?? raw?.user_email,
112
- email: inner.email ?? raw?.email,
113
- teams: teamIds,
114
- models: inner.models ?? raw?.models,
115
- max_budget: inner.max_budget ?? raw?.max_budget,
116
- budget_duration: inner.budget_duration ?? raw?.budget_duration,
117
- budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
118
- spend: inner.spend ?? raw?.spend,
119
- current_spend: inner.current_spend ?? raw?.current_spend,
120
- soft_limit: inner.soft_limit ?? raw?.soft_limit,
121
- hard_limit: inner.hard_limit ?? raw?.hard_limit,
122
- };
117
+ return this.normalizeUserInfo(raw, userId);
123
118
  }
124
119
  catch (err) {
125
120
  if (err instanceof LiteLLMUpstreamError && err.status === 404)
@@ -149,27 +144,7 @@ class LiteLLMClient {
149
144
  .toLowerCase() === email.toLowerCase());
150
145
  if (!found)
151
146
  return null;
152
- // Normalize to UserInfo shape (same as getUserInfo does)
153
- const inner = found.user_info ?? {};
154
- const teamIds = Array.isArray(found.teams)
155
- ? found.teams
156
- .map((t) => (typeof t === 'string' ? t : t?.team_id))
157
- .filter((t) => typeof t === 'string')
158
- : [];
159
- return {
160
- user_id: found.user_id ?? inner.user_id ?? '',
161
- user_email: inner.user_email ?? found.user_email,
162
- email: inner.email ?? found.email,
163
- teams: teamIds,
164
- models: inner.models ?? found.models,
165
- max_budget: inner.max_budget ?? found.max_budget,
166
- budget_duration: inner.budget_duration ?? found.budget_duration,
167
- budget_reset_at: inner.budget_reset_at ?? found.budget_reset_at,
168
- spend: inner.spend ?? found.spend,
169
- current_spend: inner.current_spend ?? found.current_spend,
170
- soft_limit: inner.soft_limit ?? found.soft_limit,
171
- hard_limit: inner.hard_limit ?? found.hard_limit,
172
- };
147
+ return this.normalizeUserInfo(found);
173
148
  }
174
149
  catch (err) {
175
150
  if (err instanceof LiteLLMUpstreamError && err.status === 404)
@@ -447,18 +422,6 @@ class LiteLLMClient {
447
422
  body: JSON.stringify({ team_ids: [teamId] }),
448
423
  });
449
424
  }
450
- async blockTeam(teamId) {
451
- return this.request('/team/block', {
452
- method: 'POST',
453
- body: JSON.stringify({ team_id: teamId }),
454
- });
455
- }
456
- async unblockTeam(teamId) {
457
- return this.request('/team/unblock', {
458
- method: 'POST',
459
- body: JSON.stringify({ team_id: teamId }),
460
- });
461
- }
462
425
  /**
463
426
  * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
464
427
  * under a `member` object; `role` is 'user' or 'admin' (we only ever send
@@ -532,47 +495,6 @@ class LiteLLMClient {
532
495
  }))
533
496
  .filter(r => r.id);
534
497
  }
535
- /**
536
- * Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
537
- * by date range and optionally by key/user/team. Rows are normalised so
538
- * `request_tags` is always a `string[]` (LiteLLM returns either an array
539
- * or an object depending on version) — callers group by tag to attribute
540
- * spend to a conversation or caller.
541
- *
542
- * This is the admin/DB-backed endpoint: it can return a large number of
543
- * rows, so callers should keep the date window tight and cap `page_size`.
544
- */
545
- async getSpendLogs(params) {
546
- const query = new URLSearchParams({
547
- start_date: params.start_date,
548
- end_date: params.end_date,
549
- });
550
- if (params.api_key)
551
- query.append('api_key', params.api_key);
552
- if (params.user_id)
553
- query.append('user_id', params.user_id);
554
- if (params.team_id)
555
- query.append('team_id', params.team_id);
556
- if (params.page_size)
557
- query.append('page_size', String(params.page_size));
558
- const raw = await this.request(`/spend/logs?${query.toString()}`);
559
- const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
560
- return rows.map(row => ({
561
- request_id: row?.request_id,
562
- startTime: row?.startTime ?? row?.start_time,
563
- endTime: row?.endTime ?? row?.end_time,
564
- spend: row?.spend ?? 0,
565
- total_tokens: row?.total_tokens ?? 0,
566
- prompt_tokens: row?.prompt_tokens ?? 0,
567
- completion_tokens: row?.completion_tokens ?? 0,
568
- model: row?.model,
569
- api_key: row?.api_key,
570
- user: row?.user,
571
- team_id: row?.team_id,
572
- request_tags: normalizeRequestTags(row?.request_tags),
573
- metadata: row?.metadata,
574
- }));
575
- }
576
498
  emptyUsage() {
577
499
  return {
578
500
  total_spend: 0,
package/dist/errors.d.ts CHANGED
@@ -25,11 +25,12 @@ export declare function isHttpishError(error: unknown): error is {
25
25
  export declare function errorMessage(error: unknown): string;
26
26
  /**
27
27
  * Sanitizes a message by:
28
+ * - Redacting Bearer tokens and sk- keys
28
29
  * - Stripping HTML tags
29
30
  * - Collapsing consecutive whitespace
30
31
  * - Truncating to 500 characters
31
32
  *
32
- * @param msg The raw message (may contain HTML, newlines, etc.)
33
+ * @param msg The raw message (may contain HTML, newlines, secrets, etc.)
33
34
  * @returns A safe, printable message
34
35
  */
35
36
  export declare function sanitizeUpstreamMessage(msg: string): string;
package/dist/errors.js CHANGED
@@ -49,17 +49,20 @@ function errorMessage(error) {
49
49
  }
50
50
  /**
51
51
  * Sanitizes a message by:
52
+ * - Redacting Bearer tokens and sk- keys
52
53
  * - Stripping HTML tags
53
54
  * - Collapsing consecutive whitespace
54
55
  * - Truncating to 500 characters
55
56
  *
56
- * @param msg The raw message (may contain HTML, newlines, etc.)
57
+ * @param msg The raw message (may contain HTML, newlines, secrets, etc.)
57
58
  * @returns A safe, printable message
58
59
  */
59
60
  function sanitizeUpstreamMessage(msg) {
60
61
  if (!msg || typeof msg !== 'string')
61
62
  return '';
62
63
  return msg
64
+ .replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, 'Bearer [redacted]') // Redact Bearer tokens
65
+ .replace(/sk-[A-Za-z0-9_\-]{8,}/g, 'sk-[redacted]') // Redact sk-* keys
63
66
  .replace(HTML_TAG_PATTERN, '') // Strip HTML tags and entities
64
67
  .replace(/\s+/g, ' ') // Collapse whitespace
65
68
  .trim()