payload 4.0.0-internal.cd69eef → 4.0.0-internal.d1cc1d3
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/dist/admin/RichText.d.ts +1 -1
- package/dist/admin/RichText.d.ts.map +1 -1
- package/dist/admin/RichText.js.map +1 -1
- package/dist/admin/functions/index.d.ts +10 -8
- package/dist/admin/functions/index.d.ts.map +1 -1
- package/dist/admin/functions/index.js.map +1 -1
- package/dist/admin/types.d.ts +2 -2
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/admin/types.js.map +1 -1
- package/dist/auth/endpoints/login.d.ts.map +1 -1
- package/dist/auth/endpoints/login.js +2 -11
- package/dist/auth/endpoints/login.js.map +1 -1
- package/dist/auth/loginWithCookie.d.ts +33 -0
- package/dist/auth/loginWithCookie.d.ts.map +1 -0
- package/dist/auth/loginWithCookie.js +34 -0
- package/dist/auth/loginWithCookie.js.map +1 -0
- package/dist/auth/serverFunctions/cookies.d.ts +47 -0
- package/dist/auth/serverFunctions/cookies.d.ts.map +1 -0
- package/dist/auth/serverFunctions/cookies.js +52 -0
- package/dist/auth/serverFunctions/cookies.js.map +1 -0
- package/dist/auth/serverFunctions/login.d.ts +29 -0
- package/dist/auth/serverFunctions/login.d.ts.map +1 -0
- package/dist/auth/serverFunctions/login.js +63 -0
- package/dist/auth/serverFunctions/login.js.map +1 -0
- package/dist/auth/serverFunctions/logout.d.ts +17 -0
- package/dist/auth/serverFunctions/logout.d.ts.map +1 -0
- package/dist/auth/serverFunctions/logout.js +58 -0
- package/dist/auth/serverFunctions/logout.js.map +1 -0
- package/dist/auth/serverFunctions/refresh.d.ts +17 -0
- package/dist/auth/serverFunctions/refresh.d.ts.map +1 -0
- package/dist/auth/serverFunctions/refresh.js +61 -0
- package/dist/auth/serverFunctions/refresh.js.map +1 -0
- package/dist/collections/config/client.d.ts +2 -3
- package/dist/collections/config/client.d.ts.map +1 -1
- package/dist/collections/config/client.js +5 -7
- package/dist/collections/config/client.js.map +1 -1
- package/dist/collections/config/defaults.d.ts.map +1 -1
- package/dist/collections/config/defaults.js +1 -0
- package/dist/collections/config/defaults.js.map +1 -1
- package/dist/collections/config/sanitize.d.ts +3 -8
- package/dist/collections/config/sanitize.d.ts.map +1 -1
- package/dist/collections/config/sanitize.js +2 -6
- package/dist/collections/config/sanitize.js.map +1 -1
- package/dist/collections/config/types.d.ts +1 -1
- package/dist/collections/config/types.d.ts.map +1 -1
- package/dist/collections/config/types.js.map +1 -1
- package/dist/collections/config/useAsTitle.spec.js +27 -27
- package/dist/collections/config/useAsTitle.spec.js.map +1 -1
- package/dist/collections/operations/create.d.ts.map +1 -1
- package/dist/collections/operations/create.js +10 -0
- package/dist/collections/operations/create.js.map +1 -1
- package/dist/collections/operations/restoreVersion.d.ts.map +1 -1
- package/dist/collections/operations/restoreVersion.js +7 -4
- package/dist/collections/operations/restoreVersion.js.map +1 -1
- package/dist/collections/operations/updateByID.js +1 -0
- package/dist/collections/operations/updateByID.js.map +1 -1
- package/dist/config/build.js +1 -1
- package/dist/config/build.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/orderable/index.d.ts +1 -1
- package/dist/config/orderable/index.d.ts.map +1 -1
- package/dist/config/orderable/index.js +2 -2
- package/dist/config/orderable/index.js.map +1 -1
- package/dist/config/sanitize.d.ts +1 -1
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/config/sanitize.js +33 -40
- package/dist/config/sanitize.js.map +1 -1
- package/dist/config/types.d.ts +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/database/createDatabaseAdapter.d.ts +1 -1
- package/dist/database/createDatabaseAdapter.d.ts.map +1 -1
- package/dist/database/createDatabaseAdapter.js +0 -2
- package/dist/database/createDatabaseAdapter.js.map +1 -1
- package/dist/database/types.d.ts +6 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/database/types.js.map +1 -1
- package/dist/exports/auth.d.ts +5 -0
- package/dist/exports/auth.d.ts.map +1 -0
- package/dist/exports/auth.js +6 -0
- package/dist/exports/auth.js.map +1 -0
- package/dist/exports/internal.d.ts +2 -0
- package/dist/exports/internal.d.ts.map +1 -1
- package/dist/exports/internal.js +3 -1
- package/dist/exports/internal.js.map +1 -1
- package/dist/fields/baseFields/slug/duplicateContext.d.ts +11 -0
- package/dist/fields/baseFields/slug/duplicateContext.d.ts.map +1 -0
- package/dist/fields/baseFields/slug/duplicateContext.js +21 -0
- package/dist/fields/baseFields/slug/duplicateContext.js.map +1 -0
- package/dist/fields/baseFields/slug/fillEmptyLocalizedSlugs.d.ts +22 -0
- package/dist/fields/baseFields/slug/fillEmptyLocalizedSlugs.d.ts.map +1 -0
- package/dist/fields/baseFields/slug/fillEmptyLocalizedSlugs.js +63 -0
- package/dist/fields/baseFields/slug/fillEmptyLocalizedSlugs.js.map +1 -0
- package/dist/fields/baseFields/slug/generateSlug.d.ts +13 -8
- package/dist/fields/baseFields/slug/generateSlug.d.ts.map +1 -1
- package/dist/fields/baseFields/slug/generateSlug.js +98 -45
- package/dist/fields/baseFields/slug/generateSlug.js.map +1 -1
- package/dist/fields/baseFields/slug/generateSlugBeforeDuplicate.d.ts +18 -0
- package/dist/fields/baseFields/slug/generateSlugBeforeDuplicate.d.ts.map +1 -0
- package/dist/fields/baseFields/slug/generateSlugBeforeDuplicate.js +20 -0
- package/dist/fields/baseFields/slug/generateSlugBeforeDuplicate.js.map +1 -0
- package/dist/fields/baseFields/slug/getSlugFallbackValue.d.ts +22 -0
- package/dist/fields/baseFields/slug/getSlugFallbackValue.d.ts.map +1 -0
- package/dist/fields/baseFields/slug/getSlugFallbackValue.js +23 -0
- package/dist/fields/baseFields/slug/getSlugFallbackValue.js.map +1 -0
- package/dist/fields/baseFields/slug/hasValue.d.ts +3 -0
- package/dist/fields/baseFields/slug/hasValue.d.ts.map +1 -0
- package/dist/fields/baseFields/slug/hasValue.js +3 -0
- package/dist/fields/baseFields/slug/hasValue.js.map +1 -0
- package/dist/fields/config/reservedFieldNames.spec.js +18 -18
- package/dist/fields/config/reservedFieldNames.spec.js.map +1 -1
- package/dist/fields/config/sanitize.d.ts +3 -9
- package/dist/fields/config/sanitize.d.ts.map +1 -1
- package/dist/fields/config/sanitize.js +33 -34
- package/dist/fields/config/sanitize.js.map +1 -1
- package/dist/fields/config/sanitize.spec.js +128 -105
- package/dist/fields/config/sanitize.spec.js.map +1 -1
- package/dist/fields/config/types.d.ts +4 -3
- package/dist/fields/config/types.d.ts.map +1 -1
- package/dist/fields/config/types.js.map +1 -1
- package/dist/fields/hooks/beforeValidate/promise.d.ts.map +1 -1
- package/dist/fields/hooks/beforeValidate/promise.js +5 -3
- package/dist/fields/hooks/beforeValidate/promise.js.map +1 -1
- package/dist/fields/hooks/beforeValidate/stripNullRows.d.ts +10 -0
- package/dist/fields/hooks/beforeValidate/stripNullRows.d.ts.map +1 -0
- package/dist/fields/hooks/beforeValidate/stripNullRows.js +18 -0
- package/dist/fields/hooks/beforeValidate/stripNullRows.js.map +1 -0
- package/dist/fields/validations.d.ts.map +1 -1
- package/dist/fields/validations.js +7 -8
- package/dist/fields/validations.js.map +1 -1
- package/dist/fields/validations.spec.js +61 -37
- package/dist/fields/validations.spec.js.map +1 -1
- package/dist/globals/config/sanitize.d.ts +2 -7
- package/dist/globals/config/sanitize.d.ts.map +1 -1
- package/dist/globals/config/sanitize.js +2 -6
- package/dist/globals/config/sanitize.js.map +1 -1
- package/dist/globals/config/sanitize.spec.js +6 -6
- package/dist/globals/config/sanitize.spec.js.map +1 -1
- package/dist/globals/operations/countGlobalVersions.d.ts.map +1 -1
- package/dist/globals/operations/countGlobalVersions.js +1 -0
- package/dist/globals/operations/countGlobalVersions.js.map +1 -1
- package/dist/index.bundled.d.ts +247 -178
- package/dist/index.d.ts +49 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/query-presets/constraints.js +1 -1
- package/dist/query-presets/constraints.js.map +1 -1
- package/dist/queues/config/collection.d.ts.map +1 -1
- package/dist/queues/config/collection.js +37 -43
- package/dist/queues/config/collection.js.map +1 -1
- package/dist/queues/config/global.d.ts +5 -6
- package/dist/queues/config/global.d.ts.map +1 -1
- package/dist/queues/config/global.js +1 -1
- package/dist/queues/config/global.js.map +1 -1
- package/dist/queues/config/types/index.d.ts +29 -24
- package/dist/queues/config/types/index.d.ts.map +1 -1
- package/dist/queues/config/types/index.js.map +1 -1
- package/dist/queues/config/types/taskTypes.d.ts +4 -7
- package/dist/queues/config/types/taskTypes.d.ts.map +1 -1
- package/dist/queues/config/types/taskTypes.js.map +1 -1
- package/dist/queues/config/types/workflowJSONTypes.d.ts +10 -9
- package/dist/queues/config/types/workflowJSONTypes.d.ts.map +1 -1
- package/dist/queues/config/types/workflowJSONTypes.js.map +1 -1
- package/dist/queues/config/types/workflowTypes.d.ts +11 -58
- package/dist/queues/config/types/workflowTypes.d.ts.map +1 -1
- package/dist/queues/config/types/workflowTypes.js.map +1 -1
- package/dist/queues/endpoints/handleSchedules.js +1 -1
- package/dist/queues/endpoints/handleSchedules.js.map +1 -1
- package/dist/queues/errors/handleTaskError.js +4 -4
- package/dist/queues/errors/handleTaskError.js.map +1 -1
- package/dist/queues/errors/handleWorkflowError.js +1 -1
- package/dist/queues/errors/handleWorkflowError.js.map +1 -1
- package/dist/queues/errors/index.d.ts +6 -0
- package/dist/queues/errors/index.d.ts.map +1 -1
- package/dist/queues/errors/index.js +5 -0
- package/dist/queues/errors/index.js.map +1 -1
- package/dist/queues/localAPI.d.ts +4 -4
- package/dist/queues/localAPI.d.ts.map +1 -1
- package/dist/queues/localAPI.js +63 -66
- package/dist/queues/localAPI.js.map +1 -1
- package/dist/queues/operations/handleSchedules/countRunnableOrActiveJobsForQueue.d.ts +4 -4
- package/dist/queues/operations/handleSchedules/countRunnableOrActiveJobsForQueue.d.ts.map +1 -1
- package/dist/queues/operations/handleSchedules/countRunnableOrActiveJobsForQueue.js.map +1 -1
- package/dist/queues/operations/handleSchedules/index.d.ts +1 -1
- package/dist/queues/operations/handleSchedules/index.d.ts.map +1 -1
- package/dist/queues/operations/handleSchedules/index.js +1 -1
- package/dist/queues/operations/handleSchedules/index.js.map +1 -1
- package/dist/queues/operations/runJobs/heartbeat.d.ts +13 -0
- package/dist/queues/operations/runJobs/heartbeat.d.ts.map +1 -0
- package/dist/queues/operations/runJobs/heartbeat.js +66 -0
- package/dist/queues/operations/runJobs/heartbeat.js.map +1 -0
- package/dist/queues/operations/runJobs/index.d.ts.map +1 -1
- package/dist/queues/operations/runJobs/index.js +133 -84
- package/dist/queues/operations/runJobs/index.js.map +1 -1
- package/dist/queues/operations/runJobs/runJSONJob/index.d.ts.map +1 -1
- package/dist/queues/operations/runJobs/runJSONJob/index.js +6 -3
- package/dist/queues/operations/runJobs/runJSONJob/index.js.map +1 -1
- package/dist/queues/operations/runJobs/runJob/getRunTaskFunction.js +5 -5
- package/dist/queues/operations/runJobs/runJob/getRunTaskFunction.js.map +1 -1
- package/dist/queues/operations/runJobs/runJob/getUpdateJobFunction.d.ts +2 -1
- package/dist/queues/operations/runJobs/runJob/getUpdateJobFunction.d.ts.map +1 -1
- package/dist/queues/operations/runJobs/runJob/getUpdateJobFunction.js +39 -8
- package/dist/queues/operations/runJobs/runJob/getUpdateJobFunction.js.map +1 -1
- package/dist/queues/operations/runJobs/runJob/importHandlerPath.js.map +1 -1
- package/dist/queues/operations/runJobs/runJob/index.d.ts.map +1 -1
- package/dist/queues/operations/runJobs/runJob/index.js +4 -4
- package/dist/queues/operations/runJobs/runJob/index.js.map +1 -1
- package/dist/queues/utilities/updateJob.d.ts.map +1 -1
- package/dist/queues/utilities/updateJob.js +3 -0
- package/dist/queues/utilities/updateJob.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/uploads/checkFileRestrictions.d.ts +1 -1
- package/dist/uploads/checkFileRestrictions.d.ts.map +1 -1
- package/dist/uploads/checkFileRestrictions.js +15 -1
- package/dist/uploads/checkFileRestrictions.js.map +1 -1
- package/dist/uploads/endpoints/uploadInstructions.d.ts +12 -0
- package/dist/uploads/endpoints/uploadInstructions.d.ts.map +1 -1
- package/dist/uploads/endpoints/uploadInstructions.js +66 -20
- package/dist/uploads/endpoints/uploadInstructions.js.map +1 -1
- package/dist/uploads/generateFileData.js +4 -1
- package/dist/uploads/generateFileData.js.map +1 -1
- package/dist/uploads/getFileFromUploadInstructions.d.ts +8 -0
- package/dist/uploads/getFileFromUploadInstructions.d.ts.map +1 -0
- package/dist/uploads/getFileFromUploadInstructions.js +62 -0
- package/dist/uploads/getFileFromUploadInstructions.js.map +1 -0
- package/dist/uploads/getImageSize.d.ts +12 -1
- package/dist/uploads/getImageSize.d.ts.map +1 -1
- package/dist/uploads/getImageSize.js +20 -18
- package/dist/uploads/getImageSize.js.map +1 -1
- package/dist/uploads/getImageSize.spec.js +298 -0
- package/dist/uploads/getImageSize.spec.js.map +1 -0
- package/dist/uploads/probeImageSize.d.ts +39 -0
- package/dist/uploads/probeImageSize.d.ts.map +1 -0
- package/dist/uploads/probeImageSize.js +374 -0
- package/dist/uploads/probeImageSize.js.map +1 -0
- package/dist/uploads/probeImageSize.spec.js +366 -0
- package/dist/uploads/probeImageSize.spec.js.map +1 -0
- package/dist/uploads/stagedUpload.d.ts +27 -0
- package/dist/uploads/stagedUpload.d.ts.map +1 -0
- package/dist/uploads/stagedUpload.js +197 -0
- package/dist/uploads/stagedUpload.js.map +1 -0
- package/dist/uploads/types.d.ts +41 -13
- package/dist/uploads/types.d.ts.map +1 -1
- package/dist/uploads/types.js.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.d.ts.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.js +12 -46
- package/dist/utilities/addDataAndFileToRequest.js.map +1 -1
- package/dist/utilities/configToJSONSchema.spec.js +33 -33
- package/dist/utilities/configToJSONSchema.spec.js.map +1 -1
- package/dist/utilities/fieldValueExists.d.ts +29 -0
- package/dist/utilities/fieldValueExists.d.ts.map +1 -0
- package/dist/utilities/fieldValueExists.js +27 -0
- package/dist/utilities/fieldValueExists.js.map +1 -0
- package/dist/utilities/getUniqueFieldValue.d.ts +27 -0
- package/dist/utilities/getUniqueFieldValue.d.ts.map +1 -0
- package/dist/utilities/getUniqueFieldValue.js +26 -0
- package/dist/utilities/getUniqueFieldValue.js.map +1 -0
- package/dist/versions/deleteScheduledPublishJobs.js +2 -2
- package/dist/versions/deleteScheduledPublishJobs.js.map +1 -1
- package/dist/versions/payloadPackageList.d.ts.map +1 -1
- package/dist/versions/payloadPackageList.js +0 -1
- package/dist/versions/payloadPackageList.js.map +1 -1
- package/package.json +8 -3
- package/dist/database/defaultUpdateJobs.d.ts +0 -3
- package/dist/database/defaultUpdateJobs.d.ts.map +0 -1
- package/dist/database/defaultUpdateJobs.js +0 -41
- package/dist/database/defaultUpdateJobs.js.map +0 -1
- package/dist/fields/baseFields/slug/countVersions.d.ts +0 -13
- package/dist/fields/baseFields/slug/countVersions.d.ts.map +0 -1
- package/dist/fields/baseFields/slug/countVersions.js +0 -27
- package/dist/fields/baseFields/slug/countVersions.js.map +0 -1
- package/dist/uploads/tempFile.d.ts +0 -7
- package/dist/uploads/tempFile.d.ts.map +0 -1
- package/dist/uploads/tempFile.js +0 -39
- package/dist/uploads/tempFile.js.map +0 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { probeImageSize } from './probeImageSize.js';
|
|
6
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixturesDir = path.resolve(dirname, '../../../../test/uploads');
|
|
8
|
+
const readFixture = (name)=>readFileSync(path.join(fixturesDir, name));
|
|
9
|
+
// Packs `{ length, value }` fields into a little-endian, LSB-first bitstream,
|
|
10
|
+
// matching the JPEG XL `SizeHeader` bit layout.
|
|
11
|
+
const packJXLBits = (fields)=>{
|
|
12
|
+
const bits = [];
|
|
13
|
+
for (const { length, value } of fields){
|
|
14
|
+
for(let i = 0; i < length; i++){
|
|
15
|
+
bits.push(value >> i & 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const bytes = Buffer.alloc(Math.ceil(bits.length / 8));
|
|
19
|
+
bits.forEach((bit, i)=>{
|
|
20
|
+
if (bit) {
|
|
21
|
+
bytes[Math.floor(i / 8)] |= 1 << i % 8;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return bytes;
|
|
25
|
+
};
|
|
26
|
+
// Encodes a non-small `SizeHeader` dimension: a 2-bit size class selecting
|
|
27
|
+
// how many extra bits follow, then `value - 1` in that many bits.
|
|
28
|
+
const packJXLDimension = (value)=>{
|
|
29
|
+
const n = value - 1;
|
|
30
|
+
const sizeClasses = [
|
|
31
|
+
9,
|
|
32
|
+
13,
|
|
33
|
+
18,
|
|
34
|
+
30
|
|
35
|
+
];
|
|
36
|
+
const classIndex = sizeClasses.findIndex((extraBits)=>n < 2 ** extraBits);
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
length: 2,
|
|
40
|
+
value: classIndex
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
length: sizeClasses[classIndex],
|
|
44
|
+
value: n
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
};
|
|
48
|
+
const encodeJXLCodestream = (width, height)=>{
|
|
49
|
+
const body = packJXLBits([
|
|
50
|
+
{
|
|
51
|
+
length: 1,
|
|
52
|
+
value: 0
|
|
53
|
+
},
|
|
54
|
+
...packJXLDimension(height),
|
|
55
|
+
{
|
|
56
|
+
length: 3,
|
|
57
|
+
value: 0
|
|
58
|
+
},
|
|
59
|
+
...packJXLDimension(width)
|
|
60
|
+
]);
|
|
61
|
+
return Buffer.concat([
|
|
62
|
+
Buffer.from([
|
|
63
|
+
0xff,
|
|
64
|
+
0x0a
|
|
65
|
+
]),
|
|
66
|
+
body
|
|
67
|
+
]);
|
|
68
|
+
};
|
|
69
|
+
const encodeSmallJXLCodestream = (width, height)=>{
|
|
70
|
+
const body = packJXLBits([
|
|
71
|
+
{
|
|
72
|
+
length: 1,
|
|
73
|
+
value: 1
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
length: 5,
|
|
77
|
+
value: height / 8 - 1
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
length: 3,
|
|
81
|
+
value: 0
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
length: 5,
|
|
85
|
+
value: width / 8 - 1
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
return Buffer.concat([
|
|
89
|
+
Buffer.from([
|
|
90
|
+
0xff,
|
|
91
|
+
0x0a
|
|
92
|
+
]),
|
|
93
|
+
body
|
|
94
|
+
]);
|
|
95
|
+
};
|
|
96
|
+
const jxlBox = (type, payload)=>{
|
|
97
|
+
const box = Buffer.alloc(8 + payload.length);
|
|
98
|
+
box.writeUInt32BE(box.length, 0);
|
|
99
|
+
box.write(type, 4, 'ascii');
|
|
100
|
+
payload.copy(box, 8);
|
|
101
|
+
return box;
|
|
102
|
+
};
|
|
103
|
+
// The fixed 12-byte JXL container signature box
|
|
104
|
+
const jxlSignatureBox = Buffer.from([
|
|
105
|
+
0x00,
|
|
106
|
+
0x00,
|
|
107
|
+
0x00,
|
|
108
|
+
0x0c,
|
|
109
|
+
0x4a,
|
|
110
|
+
0x58,
|
|
111
|
+
0x4c,
|
|
112
|
+
0x20,
|
|
113
|
+
0x0d,
|
|
114
|
+
0x0a,
|
|
115
|
+
0x87,
|
|
116
|
+
0x0a
|
|
117
|
+
]);
|
|
118
|
+
const jxlFtypBox = jxlBox('ftyp', Buffer.concat([
|
|
119
|
+
Buffer.from('jxl '),
|
|
120
|
+
Buffer.alloc(4),
|
|
121
|
+
Buffer.from('jxl ')
|
|
122
|
+
]));
|
|
123
|
+
describe('probeImageSize', ()=>{
|
|
124
|
+
// Expected dimensions verified against the previous `image-size` implementation
|
|
125
|
+
const cases = [
|
|
126
|
+
{
|
|
127
|
+
file: 'test-image.png',
|
|
128
|
+
height: 800,
|
|
129
|
+
width: 800
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
file: 'small.png',
|
|
133
|
+
height: 80,
|
|
134
|
+
width: 320
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
file: 'image with spaces.png',
|
|
138
|
+
height: 1600,
|
|
139
|
+
width: 1600
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
file: 'test-image.jpg',
|
|
143
|
+
height: 800,
|
|
144
|
+
width: 800
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
file: 'test-image.tiff',
|
|
148
|
+
height: 100,
|
|
149
|
+
width: 200
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
file: 'image.svg',
|
|
153
|
+
height: 260,
|
|
154
|
+
width: 260
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
file: 'svgWithXml.svg',
|
|
158
|
+
height: 1,
|
|
159
|
+
width: 1
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
file: 'corrupt.svg',
|
|
163
|
+
height: 400,
|
|
164
|
+
width: 400
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
file: 'animated.webp',
|
|
168
|
+
height: 200,
|
|
169
|
+
width: 200
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
file: 'non-animated.webp',
|
|
173
|
+
height: 420,
|
|
174
|
+
width: 900
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
file: 'test-image-avif.avif',
|
|
178
|
+
height: 800,
|
|
179
|
+
width: 800
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
it.each(cases)('should read dimensions for $file', ({ file, height, width })=>{
|
|
183
|
+
expect(probeImageSize(readFixture(file))).toEqual({
|
|
184
|
+
height,
|
|
185
|
+
width
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('should return the largest image for a HEIF file embedding a thumbnail', ()=>{
|
|
189
|
+
// Apple HEIC files place a small thumbnail `ispe` before the full-resolution
|
|
190
|
+
// primary `ispe` inside `ipco`; the primary (largest) must win.
|
|
191
|
+
const box = (type, payload)=>{
|
|
192
|
+
const b = Buffer.alloc(8 + payload.length);
|
|
193
|
+
b.writeUInt32BE(b.length, 0);
|
|
194
|
+
b.write(type, 4, 'ascii');
|
|
195
|
+
payload.copy(b, 8);
|
|
196
|
+
return b;
|
|
197
|
+
};
|
|
198
|
+
const ispe = (width, height)=>{
|
|
199
|
+
const payload = Buffer.alloc(12) // 4 bytes version/flags + width + height
|
|
200
|
+
;
|
|
201
|
+
payload.writeUInt32BE(width, 4);
|
|
202
|
+
payload.writeUInt32BE(height, 8);
|
|
203
|
+
return box('ispe', payload);
|
|
204
|
+
};
|
|
205
|
+
const ipco = box('ipco', Buffer.concat([
|
|
206
|
+
ispe(80, 80),
|
|
207
|
+
ispe(400, 300)
|
|
208
|
+
]));
|
|
209
|
+
const iprp = box('iprp', ipco);
|
|
210
|
+
const meta = box('meta', Buffer.concat([
|
|
211
|
+
Buffer.alloc(4),
|
|
212
|
+
iprp
|
|
213
|
+
]));
|
|
214
|
+
const ftyp = box('ftyp', Buffer.from('avif '));
|
|
215
|
+
const heif = Buffer.concat([
|
|
216
|
+
ftyp,
|
|
217
|
+
meta
|
|
218
|
+
]);
|
|
219
|
+
expect(probeImageSize(heif)).toEqual({
|
|
220
|
+
height: 300,
|
|
221
|
+
width: 400
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
it('should throw on an unsupported file type', ()=>{
|
|
225
|
+
expect(()=>probeImageSize(Buffer.from('not an image'))).toThrow();
|
|
226
|
+
});
|
|
227
|
+
it('should read dimensions for a BMP file', ()=>{
|
|
228
|
+
const bmp = Buffer.alloc(54);
|
|
229
|
+
bmp.write('BM', 0, 'ascii');
|
|
230
|
+
bmp.writeUInt32LE(54, 10); // pixel data offset
|
|
231
|
+
bmp.writeUInt32LE(40, 14); // BITMAPINFOHEADER size
|
|
232
|
+
bmp.writeInt32LE(64, 18); // width
|
|
233
|
+
bmp.writeInt32LE(-48, 22); // height stored negative for a top-down bitmap
|
|
234
|
+
expect(probeImageSize(bmp)).toEqual({
|
|
235
|
+
height: 48,
|
|
236
|
+
width: 64
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
it('should read dimensions for an ICO file, picking the largest embedded image', ()=>{
|
|
240
|
+
const ico = Buffer.alloc(24);
|
|
241
|
+
ico.writeUInt16LE(1, 2); // type: 1 = icon
|
|
242
|
+
ico.writeUInt16LE(2, 4); // image count
|
|
243
|
+
ico[6] = 16; // entry 0: 16x16
|
|
244
|
+
ico[7] = 16;
|
|
245
|
+
ico[22] = 32; // entry 1: 32x32
|
|
246
|
+
ico[23] = 32;
|
|
247
|
+
expect(probeImageSize(ico)).toEqual({
|
|
248
|
+
height: 32,
|
|
249
|
+
width: 32
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
it('should read dimensions for a CUR file', ()=>{
|
|
253
|
+
const cur = Buffer.alloc(22);
|
|
254
|
+
cur.writeUInt16LE(2, 2); // type: 2 = cursor
|
|
255
|
+
cur.writeUInt16LE(1, 4); // image count
|
|
256
|
+
cur[6] = 24;
|
|
257
|
+
cur[7] = 24;
|
|
258
|
+
expect(probeImageSize(cur)).toEqual({
|
|
259
|
+
height: 24,
|
|
260
|
+
width: 24
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
it('should not hang on a malformed HEIF buffer with a zero-size box', ()=>{
|
|
264
|
+
// Reproduces the CVE-2025-71319 trigger: an ISO-BMFF box whose size field is
|
|
265
|
+
// zero inside a container would loop forever in the unpatched `image-size`.
|
|
266
|
+
const makeBox = (type, payload)=>{
|
|
267
|
+
const box = Buffer.alloc(8 + payload.length);
|
|
268
|
+
box.writeUInt32BE(box.length, 0);
|
|
269
|
+
box.write(type, 4, 'ascii');
|
|
270
|
+
payload.copy(box, 8);
|
|
271
|
+
return box;
|
|
272
|
+
};
|
|
273
|
+
const zeroSizeBox = Buffer.alloc(8);
|
|
274
|
+
zeroSizeBox.write('ipco', 4, 'ascii'); // size field left as 0
|
|
275
|
+
const meta = makeBox('meta', Buffer.concat([
|
|
276
|
+
Buffer.alloc(4),
|
|
277
|
+
zeroSizeBox
|
|
278
|
+
]));
|
|
279
|
+
const ftyp = makeBox('ftyp', Buffer.from('avif '));
|
|
280
|
+
const malformed = Buffer.concat([
|
|
281
|
+
ftyp,
|
|
282
|
+
meta
|
|
283
|
+
]);
|
|
284
|
+
expect(()=>probeImageSize(malformed)).toThrow();
|
|
285
|
+
});
|
|
286
|
+
it('should read dimensions for a bare JPEG XL codestream', ()=>{
|
|
287
|
+
expect(probeImageSize(encodeJXLCodestream(1920, 1080))).toEqual({
|
|
288
|
+
height: 1080,
|
|
289
|
+
width: 1920
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
it('should read dimensions for a small bare JPEG XL codestream', ()=>{
|
|
293
|
+
expect(probeImageSize(encodeSmallJXLCodestream(64, 48))).toEqual({
|
|
294
|
+
height: 48,
|
|
295
|
+
width: 64
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it('should read dimensions for a JPEG XL container with a single jxlc box', ()=>{
|
|
299
|
+
const jxlc = jxlBox('jxlc', encodeJXLCodestream(640, 480));
|
|
300
|
+
const container = Buffer.concat([
|
|
301
|
+
jxlSignatureBox,
|
|
302
|
+
jxlFtypBox,
|
|
303
|
+
jxlc
|
|
304
|
+
]);
|
|
305
|
+
expect(probeImageSize(container)).toEqual({
|
|
306
|
+
height: 480,
|
|
307
|
+
width: 640
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
it('should reassemble a JPEG XL codestream split across jxlp boxes', ()=>{
|
|
311
|
+
const codestream = encodeJXLCodestream(800, 600);
|
|
312
|
+
const splitPoint = 3;
|
|
313
|
+
// Each jxlp payload is prefixed with a 4-byte partial-codestream index
|
|
314
|
+
const jxlp0 = jxlBox('jxlp', Buffer.concat([
|
|
315
|
+
Buffer.from([
|
|
316
|
+
0,
|
|
317
|
+
0,
|
|
318
|
+
0,
|
|
319
|
+
0
|
|
320
|
+
]),
|
|
321
|
+
codestream.subarray(0, splitPoint)
|
|
322
|
+
]));
|
|
323
|
+
const jxlp1 = jxlBox('jxlp', Buffer.concat([
|
|
324
|
+
Buffer.from([
|
|
325
|
+
0,
|
|
326
|
+
0,
|
|
327
|
+
0,
|
|
328
|
+
1
|
|
329
|
+
]),
|
|
330
|
+
codestream.subarray(splitPoint)
|
|
331
|
+
]));
|
|
332
|
+
const container = Buffer.concat([
|
|
333
|
+
jxlSignatureBox,
|
|
334
|
+
jxlFtypBox,
|
|
335
|
+
jxlp0,
|
|
336
|
+
jxlp1
|
|
337
|
+
]);
|
|
338
|
+
expect(probeImageSize(container)).toEqual({
|
|
339
|
+
height: 600,
|
|
340
|
+
width: 800
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
it('should throw rather than hang on a JPEG XL container with a truncated jxlc box', ()=>{
|
|
344
|
+
const truncatedJxlc = Buffer.alloc(8);
|
|
345
|
+
truncatedJxlc.write('jxlc', 4, 'ascii'); // size field left as 0: extends to end of buffer, no payload left
|
|
346
|
+
const container = Buffer.concat([
|
|
347
|
+
jxlSignatureBox,
|
|
348
|
+
jxlFtypBox,
|
|
349
|
+
truncatedJxlc
|
|
350
|
+
]);
|
|
351
|
+
expect(()=>probeImageSize(container)).toThrow();
|
|
352
|
+
});
|
|
353
|
+
it('should not hang on a JPEG XL container with thousands of unrelated boxes', ()=>{
|
|
354
|
+
const unrelatedBoxes = Buffer.concat(Array.from({
|
|
355
|
+
length: 5000
|
|
356
|
+
}, ()=>jxlBox('free', Buffer.alloc(0))));
|
|
357
|
+
const container = Buffer.concat([
|
|
358
|
+
jxlSignatureBox,
|
|
359
|
+
jxlFtypBox,
|
|
360
|
+
unrelatedBoxes
|
|
361
|
+
]);
|
|
362
|
+
expect(()=>probeImageSize(container)).toThrow();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=probeImageSize.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/uploads/probeImageSize.spec.ts"],"sourcesContent":["import { readFileSync } from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\nimport { describe, expect, it } from 'vitest'\n\nimport { probeImageSize } from './probeImageSize.js'\n\nconst dirname = path.dirname(fileURLToPath(import.meta.url))\nconst fixturesDir = path.resolve(dirname, '../../../../test/uploads')\n\nconst readFixture = (name: string) => readFileSync(path.join(fixturesDir, name))\n\n// Packs `{ length, value }` fields into a little-endian, LSB-first bitstream,\n// matching the JPEG XL `SizeHeader` bit layout.\nconst packJXLBits = (fields: Array<{ length: number; value: number }>): Buffer => {\n const bits: number[] = []\n for (const { length, value } of fields) {\n for (let i = 0; i < length; i++) {\n bits.push((value >> i) & 1)\n }\n }\n\n const bytes = Buffer.alloc(Math.ceil(bits.length / 8))\n bits.forEach((bit, i) => {\n if (bit) {\n bytes[Math.floor(i / 8)]! |= 1 << i % 8\n }\n })\n\n return bytes\n}\n\n// Encodes a non-small `SizeHeader` dimension: a 2-bit size class selecting\n// how many extra bits follow, then `value - 1` in that many bits.\nconst packJXLDimension = (value: number): Array<{ length: number; value: number }> => {\n const n = value - 1\n const sizeClasses = [9, 13, 18, 30]\n const classIndex = sizeClasses.findIndex((extraBits) => n < 2 ** extraBits)\n\n return [\n { length: 2, value: classIndex },\n { length: sizeClasses[classIndex]!, value: n },\n ]\n}\n\nconst encodeJXLCodestream = (width: number, height: number): Buffer => {\n const body = packJXLBits([\n { length: 1, value: 0 }, // isSmallImage = false\n ...packJXLDimension(height),\n { length: 3, value: 0 }, // widthMode = 0 (explicit dimension)\n ...packJXLDimension(width),\n ])\n\n return Buffer.concat([Buffer.from([0xff, 0x0a]), body])\n}\n\nconst encodeSmallJXLCodestream = (width: number, height: number): Buffer => {\n const body = packJXLBits([\n { length: 1, value: 1 }, // isSmallImage = true\n { length: 5, value: height / 8 - 1 },\n { length: 3, value: 0 }, // widthMode = 0 (explicit dimension)\n { length: 5, value: width / 8 - 1 },\n ])\n\n return Buffer.concat([Buffer.from([0xff, 0x0a]), body])\n}\n\nconst jxlBox = (type: string, payload: Buffer): Buffer => {\n const box = Buffer.alloc(8 + payload.length)\n box.writeUInt32BE(box.length, 0)\n box.write(type, 4, 'ascii')\n payload.copy(box, 8)\n return box\n}\n\n// The fixed 12-byte JXL container signature box\nconst jxlSignatureBox = Buffer.from([\n 0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a,\n])\nconst jxlFtypBox = jxlBox(\n 'ftyp',\n Buffer.concat([Buffer.from('jxl '), Buffer.alloc(4), Buffer.from('jxl ')]),\n)\n\ndescribe('probeImageSize', () => {\n // Expected dimensions verified against the previous `image-size` implementation\n const cases: Array<{ file: string; height: number; width: number }> = [\n { file: 'test-image.png', height: 800, width: 800 },\n { file: 'small.png', height: 80, width: 320 },\n { file: 'image with spaces.png', height: 1600, width: 1600 },\n { file: 'test-image.jpg', height: 800, width: 800 },\n { file: 'test-image.tiff', height: 100, width: 200 },\n { file: 'image.svg', height: 260, width: 260 },\n { file: 'svgWithXml.svg', height: 1, width: 1 },\n { file: 'corrupt.svg', height: 400, width: 400 },\n { file: 'animated.webp', height: 200, width: 200 },\n { file: 'non-animated.webp', height: 420, width: 900 },\n { file: 'test-image-avif.avif', height: 800, width: 800 },\n ]\n\n it.each(cases)('should read dimensions for $file', ({ file, height, width }) => {\n expect(probeImageSize(readFixture(file))).toEqual({ height, width })\n })\n\n it('should return the largest image for a HEIF file embedding a thumbnail', () => {\n // Apple HEIC files place a small thumbnail `ispe` before the full-resolution\n // primary `ispe` inside `ipco`; the primary (largest) must win.\n const box = (type: string, payload: Buffer) => {\n const b = Buffer.alloc(8 + payload.length)\n b.writeUInt32BE(b.length, 0)\n b.write(type, 4, 'ascii')\n payload.copy(b, 8)\n return b\n }\n const ispe = (width: number, height: number) => {\n const payload = Buffer.alloc(12) // 4 bytes version/flags + width + height\n payload.writeUInt32BE(width, 4)\n payload.writeUInt32BE(height, 8)\n return box('ispe', payload)\n }\n const ipco = box('ipco', Buffer.concat([ispe(80, 80), ispe(400, 300)]))\n const iprp = box('iprp', ipco)\n const meta = box('meta', Buffer.concat([Buffer.alloc(4), iprp]))\n const ftyp = box('ftyp', Buffer.from('avif '))\n const heif = Buffer.concat([ftyp, meta])\n\n expect(probeImageSize(heif)).toEqual({ height: 300, width: 400 })\n })\n\n it('should throw on an unsupported file type', () => {\n expect(() => probeImageSize(Buffer.from('not an image'))).toThrow()\n })\n\n it('should read dimensions for a BMP file', () => {\n const bmp = Buffer.alloc(54)\n bmp.write('BM', 0, 'ascii')\n bmp.writeUInt32LE(54, 10) // pixel data offset\n bmp.writeUInt32LE(40, 14) // BITMAPINFOHEADER size\n bmp.writeInt32LE(64, 18) // width\n bmp.writeInt32LE(-48, 22) // height stored negative for a top-down bitmap\n\n expect(probeImageSize(bmp)).toEqual({ height: 48, width: 64 })\n })\n\n it('should read dimensions for an ICO file, picking the largest embedded image', () => {\n const ico = Buffer.alloc(24)\n ico.writeUInt16LE(1, 2) // type: 1 = icon\n ico.writeUInt16LE(2, 4) // image count\n ico[6] = 16 // entry 0: 16x16\n ico[7] = 16\n ico[22] = 32 // entry 1: 32x32\n ico[23] = 32\n\n expect(probeImageSize(ico)).toEqual({ height: 32, width: 32 })\n })\n\n it('should read dimensions for a CUR file', () => {\n const cur = Buffer.alloc(22)\n cur.writeUInt16LE(2, 2) // type: 2 = cursor\n cur.writeUInt16LE(1, 4) // image count\n cur[6] = 24\n cur[7] = 24\n\n expect(probeImageSize(cur)).toEqual({ height: 24, width: 24 })\n })\n\n it('should not hang on a malformed HEIF buffer with a zero-size box', () => {\n // Reproduces the CVE-2025-71319 trigger: an ISO-BMFF box whose size field is\n // zero inside a container would loop forever in the unpatched `image-size`.\n const makeBox = (type: string, payload: Buffer) => {\n const box = Buffer.alloc(8 + payload.length)\n box.writeUInt32BE(box.length, 0)\n box.write(type, 4, 'ascii')\n payload.copy(box, 8)\n return box\n }\n const zeroSizeBox = Buffer.alloc(8)\n zeroSizeBox.write('ipco', 4, 'ascii') // size field left as 0\n const meta = makeBox('meta', Buffer.concat([Buffer.alloc(4), zeroSizeBox]))\n const ftyp = makeBox('ftyp', Buffer.from('avif '))\n const malformed = Buffer.concat([ftyp, meta])\n\n expect(() => probeImageSize(malformed)).toThrow()\n })\n\n it('should read dimensions for a bare JPEG XL codestream', () => {\n expect(probeImageSize(encodeJXLCodestream(1920, 1080))).toEqual({ height: 1080, width: 1920 })\n })\n\n it('should read dimensions for a small bare JPEG XL codestream', () => {\n expect(probeImageSize(encodeSmallJXLCodestream(64, 48))).toEqual({ height: 48, width: 64 })\n })\n\n it('should read dimensions for a JPEG XL container with a single jxlc box', () => {\n const jxlc = jxlBox('jxlc', encodeJXLCodestream(640, 480))\n const container = Buffer.concat([jxlSignatureBox, jxlFtypBox, jxlc])\n\n expect(probeImageSize(container)).toEqual({ height: 480, width: 640 })\n })\n\n it('should reassemble a JPEG XL codestream split across jxlp boxes', () => {\n const codestream = encodeJXLCodestream(800, 600)\n const splitPoint = 3\n // Each jxlp payload is prefixed with a 4-byte partial-codestream index\n const jxlp0 = jxlBox(\n 'jxlp',\n Buffer.concat([Buffer.from([0, 0, 0, 0]), codestream.subarray(0, splitPoint)]),\n )\n const jxlp1 = jxlBox(\n 'jxlp',\n Buffer.concat([Buffer.from([0, 0, 0, 1]), codestream.subarray(splitPoint)]),\n )\n const container = Buffer.concat([jxlSignatureBox, jxlFtypBox, jxlp0, jxlp1])\n\n expect(probeImageSize(container)).toEqual({ height: 600, width: 800 })\n })\n\n it('should throw rather than hang on a JPEG XL container with a truncated jxlc box', () => {\n const truncatedJxlc = Buffer.alloc(8)\n truncatedJxlc.write('jxlc', 4, 'ascii') // size field left as 0: extends to end of buffer, no payload left\n const container = Buffer.concat([jxlSignatureBox, jxlFtypBox, truncatedJxlc])\n\n expect(() => probeImageSize(container)).toThrow()\n })\n\n it('should not hang on a JPEG XL container with thousands of unrelated boxes', () => {\n const unrelatedBoxes = Buffer.concat(\n Array.from({ length: 5000 }, () => jxlBox('free', Buffer.alloc(0))),\n )\n const container = Buffer.concat([jxlSignatureBox, jxlFtypBox, unrelatedBoxes])\n\n expect(() => probeImageSize(container)).toThrow()\n })\n})\n"],"names":["readFileSync","path","fileURLToPath","describe","expect","it","probeImageSize","dirname","url","fixturesDir","resolve","readFixture","name","join","packJXLBits","fields","bits","length","value","i","push","bytes","Buffer","alloc","Math","ceil","forEach","bit","floor","packJXLDimension","n","sizeClasses","classIndex","findIndex","extraBits","encodeJXLCodestream","width","height","body","concat","from","encodeSmallJXLCodestream","jxlBox","type","payload","box","writeUInt32BE","write","copy","jxlSignatureBox","jxlFtypBox","cases","file","each","toEqual","b","ispe","ipco","iprp","meta","ftyp","heif","toThrow","bmp","writeUInt32LE","writeInt32LE","ico","writeUInt16LE","cur","makeBox","zeroSizeBox","malformed","jxlc","container","codestream","splitPoint","jxlp0","subarray","jxlp1","truncatedJxlc","unrelatedBoxes","Array"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PayloadHandler } from '../config/types.js';
|
|
2
|
+
import type { PayloadRequest } from '../types/index.js';
|
|
3
|
+
import type { File, UploadInstructions, UploadInstructionsRequest } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a signed, one-hour URL where the client can send the file bytes. The returned file value
|
|
6
|
+
* is later passed to a document create or update request.
|
|
7
|
+
*/
|
|
8
|
+
export declare const generateStagedUploadInstructions: ({ collectionSlug, docPrefix, filename, filesize, mimeType, req, }: {
|
|
9
|
+
req: PayloadRequest;
|
|
10
|
+
} & UploadInstructionsRequest) => Promise<UploadInstructions>;
|
|
11
|
+
/**
|
|
12
|
+
* Saves the bytes sent to a signed upload URL. The file is written as a partial file first and only
|
|
13
|
+
* becomes available when its size exactly matches the size in the upload ID.
|
|
14
|
+
*/
|
|
15
|
+
export declare const uploadStagedFile: PayloadHandler;
|
|
16
|
+
/** Removes a temporary upload that the client no longer needs. */
|
|
17
|
+
export declare const deleteStagedFile: PayloadHandler;
|
|
18
|
+
/**
|
|
19
|
+
* Loads a completed staged file for a document request. Only the same user and collection can use
|
|
20
|
+
* it, and the temporary file is deleted after it is read.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getStagedFile: ({ collectionSlug, req, uploadReference, }: {
|
|
23
|
+
collectionSlug: string;
|
|
24
|
+
req: PayloadRequest;
|
|
25
|
+
uploadReference: unknown;
|
|
26
|
+
}) => Promise<File>;
|
|
27
|
+
//# sourceMappingURL=stagedUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stagedUpload.d.ts","sourceRoot":"","sources":["../../src/uploads/stagedUpload.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAOrF;;;GAGG;AACH,eAAO,MAAM,gCAAgC,sEAO1C;IAAE,GAAG,EAAE,cAAc,CAAA;CAAE,GAAG,yBAAyB,KAAG,OAAO,CAAC,kBAAkB,CAsClF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,cA0D9B,CAAA;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,cAO9B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,8CAIvB;IACD,cAAc,EAAE,MAAM,CAAA;IACtB,GAAG,EAAE,cAAc,CAAA;IACnB,eAAe,EAAE,OAAO,CAAA;CACzB,KAAG,OAAO,CAAC,IAAI,CAyCf,CAAA"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jwtVerify } from 'jose';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { jwtSign } from '../auth/jwt.js';
|
|
7
|
+
import { APIError, Forbidden } from '../errors/index.js';
|
|
8
|
+
import { formatAdminURL } from '../utilities/formatAdminURL.js';
|
|
9
|
+
import { checkFileRestrictions } from './checkFileRestrictions.js';
|
|
10
|
+
/**
|
|
11
|
+
* Creates a signed, one-hour URL where the client can send the file bytes. The returned file value
|
|
12
|
+
* is later passed to a document create or update request.
|
|
13
|
+
*/ export const generateStagedUploadInstructions = async ({ collectionSlug, docPrefix, filename, filesize, mimeType, req })=>{
|
|
14
|
+
await removeExpiredUploads(req, collectionSlug);
|
|
15
|
+
const { token: uploadId } = await jwtSign({
|
|
16
|
+
fieldsToSign: {
|
|
17
|
+
id: randomUUID(),
|
|
18
|
+
collectionSlug,
|
|
19
|
+
docPrefix,
|
|
20
|
+
filename,
|
|
21
|
+
filesize,
|
|
22
|
+
mimeType,
|
|
23
|
+
user: getUser(req)
|
|
24
|
+
},
|
|
25
|
+
secret: req.payload.secret,
|
|
26
|
+
tokenExpiration
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
type: 'http',
|
|
30
|
+
file: {
|
|
31
|
+
filename,
|
|
32
|
+
mimeType,
|
|
33
|
+
size: filesize,
|
|
34
|
+
uploadReference: {
|
|
35
|
+
uploadId
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
request: {
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Length': String(filesize),
|
|
41
|
+
'Content-Type': mimeType
|
|
42
|
+
},
|
|
43
|
+
method: 'PUT',
|
|
44
|
+
url: formatAdminURL({
|
|
45
|
+
apiRoute: req.payload.config.routes.api,
|
|
46
|
+
path: `/upload-instructions/${uploadId}`,
|
|
47
|
+
serverURL: req.payload.config.serverURL
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Saves the bytes sent to a signed upload URL. The file is written as a partial file first and only
|
|
54
|
+
* becomes available when its size exactly matches the size in the upload ID.
|
|
55
|
+
*/ export const uploadStagedFile = async (req)=>{
|
|
56
|
+
const upload = await verifyUploadID(req, req.routeParams?.uploadId);
|
|
57
|
+
const directory = await getUploadDirectory(req, upload.collectionSlug);
|
|
58
|
+
const uploadPath = path.join(directory, upload.id);
|
|
59
|
+
const temporaryPath = `${uploadPath}.${randomUUID()}.part`;
|
|
60
|
+
const file = await fs.open(temporaryPath, 'wx');
|
|
61
|
+
let uploadedSize = 0;
|
|
62
|
+
try {
|
|
63
|
+
try {
|
|
64
|
+
const reader = req.body?.getReader();
|
|
65
|
+
while(reader){
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
uploadedSize += value.byteLength;
|
|
71
|
+
if (uploadedSize > upload.filesize) {
|
|
72
|
+
throw new APIError('Uploaded file is larger than expected.', 400);
|
|
73
|
+
}
|
|
74
|
+
let offset = 0;
|
|
75
|
+
while(offset < value.byteLength){
|
|
76
|
+
const { bytesWritten } = await file.write(value, offset);
|
|
77
|
+
offset += bytesWritten;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (uploadedSize !== upload.filesize) {
|
|
81
|
+
throw new APIError('Uploaded file size does not match the expected size.', 400);
|
|
82
|
+
}
|
|
83
|
+
} finally{
|
|
84
|
+
await file.close();
|
|
85
|
+
}
|
|
86
|
+
const collection = req.payload.collections[upload.collectionSlug].config;
|
|
87
|
+
await checkFileRestrictions({
|
|
88
|
+
collection,
|
|
89
|
+
file: {
|
|
90
|
+
name: upload.filename,
|
|
91
|
+
data: Buffer.alloc(0),
|
|
92
|
+
mimetype: upload.mimeType,
|
|
93
|
+
size: upload.filesize,
|
|
94
|
+
tempFilePath: temporaryPath
|
|
95
|
+
},
|
|
96
|
+
req
|
|
97
|
+
});
|
|
98
|
+
await fs.rename(temporaryPath, uploadPath);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
await fs.rm(temporaryPath, {
|
|
101
|
+
force: true
|
|
102
|
+
});
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
return new Response(null, {
|
|
106
|
+
status: 204
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
/** Removes a temporary upload that the client no longer needs. */ export const deleteStagedFile = async (req)=>{
|
|
110
|
+
const upload = await verifyUploadID(req, req.routeParams?.uploadId);
|
|
111
|
+
const directory = await getUploadDirectory(req, upload.collectionSlug);
|
|
112
|
+
await fs.rm(path.join(directory, upload.id), {
|
|
113
|
+
force: true
|
|
114
|
+
});
|
|
115
|
+
return new Response(null, {
|
|
116
|
+
status: 204
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Loads a completed staged file for a document request. Only the same user and collection can use
|
|
121
|
+
* it, and the temporary file is deleted after it is read.
|
|
122
|
+
*/ export const getStagedFile = async ({ collectionSlug, req, uploadReference })=>{
|
|
123
|
+
if (!uploadReference || typeof uploadReference !== 'object' || Array.isArray(uploadReference) || !('uploadId' in uploadReference) || typeof uploadReference.uploadId !== 'string') {
|
|
124
|
+
throw new APIError('Invalid staged upload.', 400);
|
|
125
|
+
}
|
|
126
|
+
const { uploadId } = uploadReference;
|
|
127
|
+
const upload = await verifyUploadID(req, uploadId);
|
|
128
|
+
if (upload.collectionSlug !== collectionSlug || upload.user !== getUser(req)) {
|
|
129
|
+
throw new Forbidden(req.t);
|
|
130
|
+
}
|
|
131
|
+
const directory = await getUploadDirectory(req, upload.collectionSlug);
|
|
132
|
+
const tempFilePath = path.join(directory, upload.id);
|
|
133
|
+
let data;
|
|
134
|
+
try {
|
|
135
|
+
data = await fs.readFile(tempFilePath);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new APIError('Staged upload was not found.', 400);
|
|
138
|
+
}
|
|
139
|
+
if (data.length !== upload.filesize) {
|
|
140
|
+
await fs.rm(tempFilePath, {
|
|
141
|
+
force: true
|
|
142
|
+
});
|
|
143
|
+
throw new APIError('Staged upload is incomplete.', 400);
|
|
144
|
+
}
|
|
145
|
+
await fs.rm(tempFilePath, {
|
|
146
|
+
force: true
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
name: upload.filename,
|
|
150
|
+
data,
|
|
151
|
+
mimetype: upload.mimeType,
|
|
152
|
+
size: data.length
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Staged uploads hold a file temporarily until a document create or update uses it. The signed
|
|
157
|
+
* upload ID remembers which file and user the upload belongs to.
|
|
158
|
+
*/ const tokenExpiration = 60 * 60;
|
|
159
|
+
const getUser = (req)=>req.user ? `${req.user.collection}:${String(req.user.id)}` : null;
|
|
160
|
+
/** Returns the hidden folder where temporary uploads for this collection are stored. */ const getUploadDirectory = async (req, collectionSlug)=>{
|
|
161
|
+
const upload = req.payload.collections[collectionSlug]?.config.upload;
|
|
162
|
+
const directory = path.resolve(upload && !upload.disableLocalStorage ? upload.staticDir || collectionSlug : req.payload.config.upload.tempFileDir || os.tmpdir(), '.payload-staged-uploads');
|
|
163
|
+
await fs.mkdir(directory, {
|
|
164
|
+
recursive: true
|
|
165
|
+
});
|
|
166
|
+
return directory;
|
|
167
|
+
};
|
|
168
|
+
/** Removes temporary files older than the one-hour upload window. */ const removeExpiredUploads = async (req, collectionSlug)=>{
|
|
169
|
+
const directory = await getUploadDirectory(req, collectionSlug);
|
|
170
|
+
const expiresBefore = Date.now() - tokenExpiration * 1000;
|
|
171
|
+
for (const filename of (await fs.readdir(directory))){
|
|
172
|
+
const filePath = path.join(directory, filename);
|
|
173
|
+
const stats = await fs.stat(filePath).catch(()=>null);
|
|
174
|
+
if (stats && stats.mtimeMs < expiresBefore) {
|
|
175
|
+
await fs.rm(filePath, {
|
|
176
|
+
force: true
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/** Verifies that an upload ID was signed by Payload, has not expired, and contains valid details. */ const verifyUploadID = async (req, uploadID)=>{
|
|
182
|
+
try {
|
|
183
|
+
if (typeof uploadID !== 'string') {
|
|
184
|
+
throw new Error();
|
|
185
|
+
}
|
|
186
|
+
const secret = new TextEncoder().encode(req.payload.secret);
|
|
187
|
+
const { payload } = await jwtVerify(uploadID, secret);
|
|
188
|
+
if (typeof payload.id !== 'string' || !/^[\da-f-]{36}$/i.test(payload.id) || typeof payload.collectionSlug !== 'string' || typeof payload.filename !== 'string' || !Number.isSafeInteger(payload.filesize) || payload.filesize < 0 || typeof payload.mimeType !== 'string' || payload.user !== null && typeof payload.user !== 'string') {
|
|
189
|
+
throw new Error();
|
|
190
|
+
}
|
|
191
|
+
return payload;
|
|
192
|
+
} catch {
|
|
193
|
+
throw new APIError('Invalid or expired staged upload.', 400);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//# sourceMappingURL=stagedUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/uploads/stagedUpload.ts"],"sourcesContent":["import { jwtVerify } from 'jose'\nimport { randomUUID } from 'node:crypto'\nimport fs from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport type { PayloadHandler } from '../config/types.js'\nimport type { PayloadRequest } from '../types/index.js'\nimport type { File, UploadInstructions, UploadInstructionsRequest } from './types.js'\n\nimport { jwtSign } from '../auth/jwt.js'\nimport { APIError, Forbidden } from '../errors/index.js'\nimport { formatAdminURL } from '../utilities/formatAdminURL.js'\nimport { checkFileRestrictions } from './checkFileRestrictions.js'\n\n/**\n * Creates a signed, one-hour URL where the client can send the file bytes. The returned file value\n * is later passed to a document create or update request.\n */\nexport const generateStagedUploadInstructions = async ({\n collectionSlug,\n docPrefix,\n filename,\n filesize,\n mimeType,\n req,\n}: { req: PayloadRequest } & UploadInstructionsRequest): Promise<UploadInstructions> => {\n await removeExpiredUploads(req, collectionSlug)\n\n const { token: uploadId } = await jwtSign({\n fieldsToSign: {\n id: randomUUID(),\n collectionSlug,\n docPrefix,\n filename,\n filesize,\n mimeType,\n user: getUser(req),\n },\n secret: req.payload.secret,\n tokenExpiration,\n })\n\n return {\n type: 'http',\n file: {\n filename,\n mimeType,\n size: filesize,\n uploadReference: { uploadId },\n },\n request: {\n headers: {\n 'Content-Length': String(filesize),\n 'Content-Type': mimeType,\n },\n method: 'PUT',\n url: formatAdminURL({\n apiRoute: req.payload.config.routes.api,\n path: `/upload-instructions/${uploadId}`,\n serverURL: req.payload.config.serverURL,\n }),\n },\n }\n}\n\n/**\n * Saves the bytes sent to a signed upload URL. The file is written as a partial file first and only\n * becomes available when its size exactly matches the size in the upload ID.\n */\nexport const uploadStagedFile: PayloadHandler = async (req) => {\n const upload = await verifyUploadID(req, req.routeParams?.uploadId)\n const directory = await getUploadDirectory(req, upload.collectionSlug)\n const uploadPath = path.join(directory, upload.id)\n const temporaryPath = `${uploadPath}.${randomUUID()}.part`\n const file = await fs.open(temporaryPath, 'wx')\n let uploadedSize = 0\n\n try {\n try {\n const reader = req.body?.getReader()\n\n while (reader) {\n const { done, value } = await reader.read()\n\n if (done) {\n break\n }\n\n uploadedSize += value.byteLength\n if (uploadedSize > upload.filesize) {\n throw new APIError('Uploaded file is larger than expected.', 400)\n }\n\n let offset = 0\n while (offset < value.byteLength) {\n const { bytesWritten } = await file.write(value, offset)\n offset += bytesWritten\n }\n }\n\n if (uploadedSize !== upload.filesize) {\n throw new APIError('Uploaded file size does not match the expected size.', 400)\n }\n } finally {\n await file.close()\n }\n\n const collection = req.payload.collections[upload.collectionSlug]!.config\n await checkFileRestrictions({\n collection,\n file: {\n name: upload.filename,\n data: Buffer.alloc(0),\n mimetype: upload.mimeType,\n size: upload.filesize,\n tempFilePath: temporaryPath,\n },\n req,\n })\n\n await fs.rename(temporaryPath, uploadPath)\n } catch (error) {\n await fs.rm(temporaryPath, { force: true })\n throw error\n }\n\n return new Response(null, { status: 204 })\n}\n\n/** Removes a temporary upload that the client no longer needs. */\nexport const deleteStagedFile: PayloadHandler = async (req) => {\n const upload = await verifyUploadID(req, req.routeParams?.uploadId)\n const directory = await getUploadDirectory(req, upload.collectionSlug)\n\n await fs.rm(path.join(directory, upload.id), { force: true })\n\n return new Response(null, { status: 204 })\n}\n\n/**\n * Loads a completed staged file for a document request. Only the same user and collection can use\n * it, and the temporary file is deleted after it is read.\n */\nexport const getStagedFile = async ({\n collectionSlug,\n req,\n uploadReference,\n}: {\n collectionSlug: string\n req: PayloadRequest\n uploadReference: unknown\n}): Promise<File> => {\n if (\n !uploadReference ||\n typeof uploadReference !== 'object' ||\n Array.isArray(uploadReference) ||\n !('uploadId' in uploadReference) ||\n typeof uploadReference.uploadId !== 'string'\n ) {\n throw new APIError('Invalid staged upload.', 400)\n }\n\n const { uploadId } = uploadReference\n const upload = await verifyUploadID(req, uploadId)\n\n if (upload.collectionSlug !== collectionSlug || upload.user !== getUser(req)) {\n throw new Forbidden(req.t)\n }\n\n const directory = await getUploadDirectory(req, upload.collectionSlug)\n const tempFilePath = path.join(directory, upload.id)\n let data: Buffer\n\n try {\n data = await fs.readFile(tempFilePath)\n } catch {\n throw new APIError('Staged upload was not found.', 400)\n }\n\n if (data.length !== upload.filesize) {\n await fs.rm(tempFilePath, { force: true })\n throw new APIError('Staged upload is incomplete.', 400)\n }\n\n await fs.rm(tempFilePath, { force: true })\n\n return {\n name: upload.filename,\n data,\n mimetype: upload.mimeType,\n size: data.length,\n }\n}\n\n/**\n * Staged uploads hold a file temporarily until a document create or update uses it. The signed\n * upload ID remembers which file and user the upload belongs to.\n */\nconst tokenExpiration = 60 * 60\n\n/** The file details stored inside the signed upload ID. */\ntype StagedUploadToken = {\n id: string\n user: null | string\n} & UploadInstructionsRequest\n\nconst getUser = (req: PayloadRequest) =>\n req.user ? `${req.user.collection}:${String(req.user.id)}` : null\n\n/** Returns the hidden folder where temporary uploads for this collection are stored. */\nconst getUploadDirectory = async (req: PayloadRequest, collectionSlug: string) => {\n const upload = req.payload.collections[collectionSlug]?.config.upload\n const directory = path.resolve(\n upload && !upload.disableLocalStorage\n ? upload.staticDir || collectionSlug\n : req.payload.config.upload.tempFileDir || os.tmpdir(),\n '.payload-staged-uploads',\n )\n\n await fs.mkdir(directory, { recursive: true })\n\n return directory\n}\n\n/** Removes temporary files older than the one-hour upload window. */\nconst removeExpiredUploads = async (req: PayloadRequest, collectionSlug: string) => {\n const directory = await getUploadDirectory(req, collectionSlug)\n const expiresBefore = Date.now() - tokenExpiration * 1000\n\n for (const filename of await fs.readdir(directory)) {\n const filePath = path.join(directory, filename)\n const stats = await fs.stat(filePath).catch(() => null)\n\n if (stats && stats.mtimeMs < expiresBefore) {\n await fs.rm(filePath, { force: true })\n }\n }\n}\n\n/** Verifies that an upload ID was signed by Payload, has not expired, and contains valid details. */\nconst verifyUploadID = async (\n req: PayloadRequest,\n uploadID: unknown,\n): Promise<StagedUploadToken> => {\n try {\n if (typeof uploadID !== 'string') {\n throw new Error()\n }\n\n const secret = new TextEncoder().encode(req.payload.secret)\n const { payload } = await jwtVerify<StagedUploadToken>(uploadID, secret)\n\n if (\n typeof payload.id !== 'string' ||\n !/^[\\da-f-]{36}$/i.test(payload.id) ||\n typeof payload.collectionSlug !== 'string' ||\n typeof payload.filename !== 'string' ||\n !Number.isSafeInteger(payload.filesize) ||\n payload.filesize < 0 ||\n typeof payload.mimeType !== 'string' ||\n (payload.user !== null && typeof payload.user !== 'string')\n ) {\n throw new Error()\n }\n\n return payload as StagedUploadToken\n } catch {\n throw new APIError('Invalid or expired staged upload.', 400)\n }\n}\n"],"names":["jwtVerify","randomUUID","fs","os","path","jwtSign","APIError","Forbidden","formatAdminURL","checkFileRestrictions","generateStagedUploadInstructions","collectionSlug","docPrefix","filename","filesize","mimeType","req","removeExpiredUploads","token","uploadId","fieldsToSign","id","user","getUser","secret","payload","tokenExpiration","type","file","size","uploadReference","request","headers","String","method","url","apiRoute","config","routes","api","serverURL","uploadStagedFile","upload","verifyUploadID","routeParams","directory","getUploadDirectory","uploadPath","join","temporaryPath","open","uploadedSize","reader","body","getReader","done","value","read","byteLength","offset","bytesWritten","write","close","collection","collections","name","data","Buffer","alloc","mimetype","tempFilePath","rename","error","rm","force","Response","status","deleteStagedFile","getStagedFile","Array","isArray","t","readFile","length","resolve","disableLocalStorage","staticDir","tempFileDir","tmpdir","mkdir","recursive","expiresBefore","Date","now","readdir","filePath","stats","stat","catch","mtimeMs","uploadID","Error","TextEncoder","encode","test","Number","isSafeInteger"],"mappings":"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"}
|