@lobehub/lobehub 2.0.0-next.191 → 2.0.0-next.193
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/.github/workflows/claude-translator.yml +1 -1
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/e2e/docs/testing-tips.md +30 -0
- package/e2e/src/steps/discover/smoke.steps.ts +11 -33
- package/locales/zh-CN/auth.json +1 -1
- package/package.json +1 -2
- package/packages/database/package.json +1 -1
- package/packages/database/src/client/db.test.ts +19 -144
- package/packages/database/src/client/db.ts +26 -234
- package/packages/database/src/models/__tests__/_util.ts +19 -3
- package/packages/database/src/models/__tests__/knowledgeBase.test.ts +30 -1
- package/packages/database/src/models/knowledgeBase.ts +9 -7
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +8 -7
- package/src/libs/next/config/define-config.ts +1 -1
- package/src/server/modules/S3/index.test.ts +58 -0
- package/src/server/modules/S3/index.ts +21 -0
- package/src/server/routers/lambda/__tests__/file.test.ts +56 -0
- package/src/server/routers/lambda/file.ts +3 -1
- package/src/server/services/file/impls/s3.test.ts +19 -0
- package/src/server/services/file/impls/s3.ts +4 -0
- package/src/server/services/file/impls/type.ts +6 -0
- package/src/server/services/file/index.ts +10 -0
- package/packages/database/src/client/pglite.ts +0 -17
- package/packages/database/src/client/pglite.worker.ts +0 -25
|
@@ -19,6 +19,7 @@ function createCallerWithCtx(partialCtx: any = {}) {
|
|
|
19
19
|
|
|
20
20
|
const fileService = {
|
|
21
21
|
getFullFileUrl: vi.fn().mockResolvedValue('full-url'),
|
|
22
|
+
getFileMetadata: vi.fn().mockResolvedValue({ contentLength: 2048, contentType: 'text/plain' }),
|
|
22
23
|
deleteFile: vi.fn().mockResolvedValue(undefined),
|
|
23
24
|
deleteFiles: vi.fn().mockResolvedValue(undefined),
|
|
24
25
|
};
|
|
@@ -114,12 +115,14 @@ vi.mock('@/database/models/file', () => ({
|
|
|
114
115
|
}));
|
|
115
116
|
|
|
116
117
|
const mockFileServiceGetFullFileUrl = vi.fn();
|
|
118
|
+
const mockFileServiceGetFileMetadata = vi.fn();
|
|
117
119
|
|
|
118
120
|
vi.mock('@/server/services/file', () => ({
|
|
119
121
|
FileService: vi.fn(() => ({
|
|
120
122
|
deleteFile: vi.fn(),
|
|
121
123
|
deleteFiles: vi.fn(),
|
|
122
124
|
getFullFileUrl: mockFileServiceGetFullFileUrl,
|
|
125
|
+
getFileMetadata: mockFileServiceGetFileMetadata,
|
|
123
126
|
})),
|
|
124
127
|
}));
|
|
125
128
|
|
|
@@ -160,6 +163,12 @@ describe('fileRouter', () => {
|
|
|
160
163
|
embeddingTaskId: null,
|
|
161
164
|
};
|
|
162
165
|
|
|
166
|
+
// Set default mock for getFileMetadata (security fix for GHSA-wrrr-8jcv-wjf5)
|
|
167
|
+
mockFileServiceGetFileMetadata.mockResolvedValue({
|
|
168
|
+
contentLength: 100,
|
|
169
|
+
contentType: 'text/plain',
|
|
170
|
+
});
|
|
171
|
+
|
|
163
172
|
// Use actual context with default mocks
|
|
164
173
|
({ ctx, caller } = createCallerWithCtx());
|
|
165
174
|
});
|
|
@@ -204,6 +213,53 @@ describe('fileRouter', () => {
|
|
|
204
213
|
url: 'https://lobehub.com/f/new-file-id',
|
|
205
214
|
});
|
|
206
215
|
});
|
|
216
|
+
|
|
217
|
+
it('should use actual file size from S3 instead of client-provided size (security fix)', async () => {
|
|
218
|
+
// Setup: S3 returns actual size of 5000 bytes
|
|
219
|
+
mockFileServiceGetFileMetadata.mockResolvedValue({
|
|
220
|
+
contentLength: 5000,
|
|
221
|
+
contentType: 'text/plain',
|
|
222
|
+
});
|
|
223
|
+
mockFileModelCheckHash.mockResolvedValue({ isExist: false });
|
|
224
|
+
mockFileModelCreate.mockResolvedValue({ id: 'new-file-id' });
|
|
225
|
+
|
|
226
|
+
// Client claims file is only 100 bytes (attempting quota bypass)
|
|
227
|
+
await caller.createFile({
|
|
228
|
+
hash: 'test-hash',
|
|
229
|
+
fileType: 'text',
|
|
230
|
+
name: 'test.txt',
|
|
231
|
+
size: 100, // Client-provided fake size
|
|
232
|
+
url: 'files/test.txt',
|
|
233
|
+
metadata: {},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Verify getFileMetadata was called to get actual size
|
|
237
|
+
expect(mockFileServiceGetFileMetadata).toHaveBeenCalledWith('files/test.txt');
|
|
238
|
+
|
|
239
|
+
// Verify create was called with actual size from S3, not client-provided size
|
|
240
|
+
expect(mockFileModelCreate).toHaveBeenCalledWith(
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
size: 5000, // Actual size from S3, not 100
|
|
243
|
+
}),
|
|
244
|
+
true,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should handle getFileMetadata errors', async () => {
|
|
249
|
+
mockFileModelCheckHash.mockResolvedValue({ isExist: false });
|
|
250
|
+
mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
|
|
251
|
+
|
|
252
|
+
await expect(
|
|
253
|
+
caller.createFile({
|
|
254
|
+
hash: 'test-hash',
|
|
255
|
+
fileType: 'text',
|
|
256
|
+
name: 'test.txt',
|
|
257
|
+
size: 100,
|
|
258
|
+
url: 'files/non-existent.txt',
|
|
259
|
+
metadata: {},
|
|
260
|
+
}),
|
|
261
|
+
).rejects.toThrow('File not found in S3');
|
|
262
|
+
});
|
|
207
263
|
});
|
|
208
264
|
|
|
209
265
|
describe('findById', () => {
|
|
@@ -64,6 +64,8 @@ export const fileRouter = router({
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
const { contentLength: actualSize } = await ctx.fileService.getFileMetadata(input.url);
|
|
68
|
+
|
|
67
69
|
const { id } = await ctx.fileModel.create(
|
|
68
70
|
{
|
|
69
71
|
fileHash: input.hash,
|
|
@@ -72,7 +74,7 @@ export const fileRouter = router({
|
|
|
72
74
|
metadata: input.metadata,
|
|
73
75
|
name: input.name,
|
|
74
76
|
parentId: resolvedParentId,
|
|
75
|
-
size:
|
|
77
|
+
size: actualSize,
|
|
76
78
|
url: input.url,
|
|
77
79
|
},
|
|
78
80
|
// if the file is not exist in global file, create a new one
|
|
@@ -24,6 +24,7 @@ vi.mock('@/server/modules/S3', () => ({
|
|
|
24
24
|
.mockResolvedValue('https://presigned.example.com/test.jpg'),
|
|
25
25
|
getFileContent: vi.fn().mockResolvedValue('file content'),
|
|
26
26
|
getFileByteArray: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
|
|
27
|
+
getFileMetadata: vi.fn().mockResolvedValue({ contentLength: 1024, contentType: 'image/png' }),
|
|
27
28
|
deleteFile: vi.fn().mockResolvedValue({}),
|
|
28
29
|
deleteFiles: vi.fn().mockResolvedValue({}),
|
|
29
30
|
createPreSignedUrl: vi.fn().mockResolvedValue('https://upload.example.com/test.jpg'),
|
|
@@ -152,6 +153,24 @@ describe('S3StaticFileImpl', () => {
|
|
|
152
153
|
});
|
|
153
154
|
});
|
|
154
155
|
|
|
156
|
+
describe('getFileMetadata', () => {
|
|
157
|
+
it('should call S3 getFileMetadata and return metadata', async () => {
|
|
158
|
+
const result = await fileService.getFileMetadata('test.png');
|
|
159
|
+
|
|
160
|
+
expect(fileService['s3'].getFileMetadata).toHaveBeenCalledWith('test.png');
|
|
161
|
+
expect(result).toEqual({ contentLength: 1024, contentType: 'image/png' });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should handle S3 errors', async () => {
|
|
165
|
+
const error = new Error('File not found');
|
|
166
|
+
fileService['s3'].getFileMetadata = vi.fn().mockRejectedValue(error);
|
|
167
|
+
|
|
168
|
+
await expect(fileService.getFileMetadata('non-existent.txt')).rejects.toThrow(
|
|
169
|
+
'File not found',
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
155
174
|
describe('uploadContent', () => {
|
|
156
175
|
it('应该调用S3的uploadContent方法', async () => {
|
|
157
176
|
await fileService.uploadContent('test.jpg', 'content');
|
|
@@ -36,6 +36,10 @@ export class S3StaticFileImpl implements FileServiceImpl {
|
|
|
36
36
|
return this.s3.createPreSignedUrl(key);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
async getFileMetadata(key: string): Promise<{ contentLength: number; contentType?: string }> {
|
|
40
|
+
return this.s3.getFileMetadata(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
async createPreSignedUrlForPreview(key: string, expiresIn?: number): Promise<string> {
|
|
40
44
|
return this.s3.createPreSignedUrlForPreview(key, expiresIn);
|
|
41
45
|
}
|
|
@@ -32,6 +32,12 @@ export interface FileServiceImpl {
|
|
|
32
32
|
*/
|
|
33
33
|
getFileContent(key: string): Promise<string>;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Get file metadata from storage
|
|
37
|
+
* Used to verify actual file size instead of trusting client-provided values
|
|
38
|
+
*/
|
|
39
|
+
getFileMetadata(key: string): Promise<{ contentLength: number; contentType?: string }>;
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* Get full file URL
|
|
37
43
|
*/
|
|
@@ -61,6 +61,16 @@ export class FileService {
|
|
|
61
61
|
return this.impl.createPreSignedUrl(key);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get file metadata from storage
|
|
66
|
+
* Used to verify actual file size instead of trusting client-provided values
|
|
67
|
+
*/
|
|
68
|
+
public async getFileMetadata(
|
|
69
|
+
key: string,
|
|
70
|
+
): Promise<{ contentLength: number; contentType?: string }> {
|
|
71
|
+
return this.impl.getFileMetadata(key);
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/**
|
|
65
75
|
* Create pre-signed preview URL
|
|
66
76
|
*/
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { PGliteWorker } from '@electric-sql/pglite/worker';
|
|
2
|
-
|
|
3
|
-
import { InitMeta } from './type';
|
|
4
|
-
|
|
5
|
-
export const initPgliteWorker = async (meta: InitMeta) => {
|
|
6
|
-
const worker = await PGliteWorker.create(
|
|
7
|
-
new Worker(new URL('pglite.worker.ts', import.meta.url)),
|
|
8
|
-
{ meta },
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
// Listen for worker status changes
|
|
12
|
-
worker.onLeaderChange(() => {
|
|
13
|
-
console.log('Worker leader changed, isLeader:', worker?.isLeader);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
return worker as PGliteWorker;
|
|
17
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { worker } from '@electric-sql/pglite/worker';
|
|
2
|
-
|
|
3
|
-
import { InitMeta } from './type';
|
|
4
|
-
|
|
5
|
-
worker({
|
|
6
|
-
async init(options) {
|
|
7
|
-
const { wasmModule, fsBundle, vectorBundlePath, dbName } = options.meta as InitMeta;
|
|
8
|
-
const { PGlite } = await import('@electric-sql/pglite');
|
|
9
|
-
|
|
10
|
-
return new PGlite({
|
|
11
|
-
dataDir: `idb://${dbName}`,
|
|
12
|
-
extensions: {
|
|
13
|
-
vector: {
|
|
14
|
-
name: 'pgvector',
|
|
15
|
-
setup: async (pglite, options) => {
|
|
16
|
-
return { bundlePath: new URL(vectorBundlePath), options };
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
fsBundle,
|
|
21
|
-
relaxedDurability: true,
|
|
22
|
-
wasmModule,
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
});
|