@cedarjs/uploads 8.0.0-canary.3402 → 8.0.0-canary.3403
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/contentTypes.d.ts +74 -0
- package/dist/contentTypes.d.ts.map +1 -0
- package/dist/contentTypes.js +78 -0
- package/dist/detectContentType.d.ts +168 -0
- package/dist/detectContentType.d.ts.map +1 -0
- package/dist/detectContentType.js +443 -0
- package/dist/fastify/plugin.d.ts +16 -0
- package/dist/fastify/plugin.d.ts.map +1 -1
- package/dist/fastify/plugin.js +32 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +3 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +2 -1
- package/dist/profiles.d.ts +8 -2
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +5 -4
- package/dist/providers/s3.d.ts.map +1 -1
- package/dist/providers/s3.js +16 -0
- package/dist/services.d.ts +24 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +45 -10
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verifyStoredContentType.d.ts +19 -0
- package/dist/verifyStoredContentType.d.ts.map +1 -0
- package/dist/verifyStoredContentType.js +41 -0
- package/dist/webhooks/s3.d.ts +17 -8
- package/dist/webhooks/s3.d.ts.map +1 -1
- package/dist/webhooks/s3.js +22 -3
- package/package.json +7 -7
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalContentType,
|
|
3
|
+
isContentTypeListed,
|
|
4
|
+
isExplicitOnlyContentType
|
|
5
|
+
} from "./contentTypes.js";
|
|
6
|
+
import { isMimeTypeAllowed } from "./profiles.js";
|
|
7
|
+
const DETECT_CONTENT_TYPE_BYTES = 4096;
|
|
8
|
+
const DETECT_CONTENT_TYPE_TAIL_BYTES = 64 * 1024;
|
|
9
|
+
const MAX_ZIP_DIRECTORY_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
const DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
11
|
+
const XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
12
|
+
const PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
13
|
+
const DOCM = "application/vnd.ms-word.document.macroenabled.12";
|
|
14
|
+
const XLSM = "application/vnd.ms-excel.sheet.macroenabled.12";
|
|
15
|
+
const PPTM = "application/vnd.ms-powerpoint.presentation.macroenabled.12";
|
|
16
|
+
const ODT = "application/vnd.oasis.opendocument.text";
|
|
17
|
+
const ODS = "application/vnd.oasis.opendocument.spreadsheet";
|
|
18
|
+
const ODP = "application/vnd.oasis.opendocument.presentation";
|
|
19
|
+
const DETECTABLE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
20
|
+
"image/jpeg",
|
|
21
|
+
"image/png",
|
|
22
|
+
"image/gif",
|
|
23
|
+
"image/webp",
|
|
24
|
+
"image/bmp",
|
|
25
|
+
"image/tiff",
|
|
26
|
+
"image/avif",
|
|
27
|
+
"image/heic",
|
|
28
|
+
"image/heic-sequence",
|
|
29
|
+
"image/heif",
|
|
30
|
+
"image/heif-sequence",
|
|
31
|
+
"application/pdf",
|
|
32
|
+
"video/mp4",
|
|
33
|
+
"audio/mp4",
|
|
34
|
+
"video/quicktime",
|
|
35
|
+
"video/webm",
|
|
36
|
+
"video/matroska",
|
|
37
|
+
DOCX,
|
|
38
|
+
XLSX,
|
|
39
|
+
PPTX,
|
|
40
|
+
DOCM,
|
|
41
|
+
XLSM,
|
|
42
|
+
PPTM,
|
|
43
|
+
ODT,
|
|
44
|
+
ODS,
|
|
45
|
+
ODP
|
|
46
|
+
]);
|
|
47
|
+
const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
48
|
+
const EBML_SIGNATURE = [26, 69, 223, 163];
|
|
49
|
+
const BMP_INFO_HEADER_SIZES = /* @__PURE__ */ new Set([12, 40, 52, 56, 64, 108, 124]);
|
|
50
|
+
const AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
51
|
+
const HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis"]);
|
|
52
|
+
const HEIC_SEQUENCE_BRANDS = /* @__PURE__ */ new Set(["hevc", "hevx", "hevm", "hevs"]);
|
|
53
|
+
const MP4_BRANDS = /* @__PURE__ */ new Set([
|
|
54
|
+
"isom",
|
|
55
|
+
"iso2",
|
|
56
|
+
"iso3",
|
|
57
|
+
"iso4",
|
|
58
|
+
"iso5",
|
|
59
|
+
"iso6",
|
|
60
|
+
"iso7",
|
|
61
|
+
"iso8",
|
|
62
|
+
"iso9",
|
|
63
|
+
"mp41",
|
|
64
|
+
"mp42",
|
|
65
|
+
"mp71",
|
|
66
|
+
"avc1",
|
|
67
|
+
"dash",
|
|
68
|
+
"M4V "
|
|
69
|
+
]);
|
|
70
|
+
const MP4_AUDIO_BRANDS = /* @__PURE__ */ new Set(["M4A ", "M4B "]);
|
|
71
|
+
const QUICKTIME_BRAND = "qt ";
|
|
72
|
+
const MAX_FTYP_BOX_SIZE = 1024;
|
|
73
|
+
const EBML_DOC_TYPE_ID = 17026;
|
|
74
|
+
const MAX_DOC_TYPE_LENGTH = 32;
|
|
75
|
+
function hasBytesAt(bytes, offset, expected) {
|
|
76
|
+
if (bytes.length < offset + expected.length) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return expected.every((b, i) => bytes[offset + i] === b);
|
|
80
|
+
}
|
|
81
|
+
function ascii(bytes, start, end) {
|
|
82
|
+
if (end > bytes.length) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
let text = "";
|
|
86
|
+
for (let i = start; i < end; i++) {
|
|
87
|
+
text += String.fromCharCode(bytes[i]);
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
function readUint32BE(bytes, offset) {
|
|
92
|
+
if (bytes.length < offset + 4) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return bytes[offset] * 16777216 + bytes[offset + 1] * 65536 + bytes[offset + 2] * 256 + bytes[offset + 3];
|
|
96
|
+
}
|
|
97
|
+
function readUint32LE(bytes, offset) {
|
|
98
|
+
if (bytes.length < offset + 4) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return bytes[offset] + bytes[offset + 1] * 256 + bytes[offset + 2] * 65536 + bytes[offset + 3] * 16777216;
|
|
102
|
+
}
|
|
103
|
+
function detectIsoBmff(bytes) {
|
|
104
|
+
if (ascii(bytes, 4, 8) !== "ftyp") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const boxSize = readUint32BE(bytes, 0);
|
|
108
|
+
if (boxSize === null || boxSize < 16 || boxSize % 4 !== 0 || boxSize > MAX_FTYP_BOX_SIZE) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const major = ascii(bytes, 8, 12);
|
|
112
|
+
if (major === null) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const fromMajor = classifyMajorBrand(major);
|
|
116
|
+
if (fromMajor) {
|
|
117
|
+
return fromMajor;
|
|
118
|
+
}
|
|
119
|
+
if (bytes.length < boxSize) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const compatible = [];
|
|
123
|
+
for (let offset = 16; offset + 4 <= boxSize; offset += 4) {
|
|
124
|
+
const brand = ascii(bytes, offset, offset + 4);
|
|
125
|
+
if (brand !== null) {
|
|
126
|
+
compatible.push(brand);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return classifyCompatibleBrands(major, compatible);
|
|
130
|
+
}
|
|
131
|
+
function classifyMajorBrand(major) {
|
|
132
|
+
if (AVIF_BRANDS.has(major)) {
|
|
133
|
+
return "image/avif";
|
|
134
|
+
}
|
|
135
|
+
if (HEIC_BRANDS.has(major)) {
|
|
136
|
+
return "image/heic";
|
|
137
|
+
}
|
|
138
|
+
if (HEIC_SEQUENCE_BRANDS.has(major)) {
|
|
139
|
+
return "image/heic-sequence";
|
|
140
|
+
}
|
|
141
|
+
if (major === QUICKTIME_BRAND) {
|
|
142
|
+
return "video/quicktime";
|
|
143
|
+
}
|
|
144
|
+
if (MP4_AUDIO_BRANDS.has(major)) {
|
|
145
|
+
return "audio/mp4";
|
|
146
|
+
}
|
|
147
|
+
if (MP4_BRANDS.has(major)) {
|
|
148
|
+
return "video/mp4";
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function classifyCompatibleBrands(major, compatible) {
|
|
153
|
+
const has = (brands) => compatible.some((brand) => brands.has(brand));
|
|
154
|
+
if (has(AVIF_BRANDS)) {
|
|
155
|
+
return "image/avif";
|
|
156
|
+
}
|
|
157
|
+
if (has(HEIC_BRANDS)) {
|
|
158
|
+
return "image/heic";
|
|
159
|
+
}
|
|
160
|
+
if (has(HEIC_SEQUENCE_BRANDS)) {
|
|
161
|
+
return "image/heic-sequence";
|
|
162
|
+
}
|
|
163
|
+
if (major === "mif1" || major === "mif2" || compatible.includes("mif1")) {
|
|
164
|
+
return "image/heif";
|
|
165
|
+
}
|
|
166
|
+
if (major === "msf1" || compatible.includes("msf1")) {
|
|
167
|
+
return "image/heif-sequence";
|
|
168
|
+
}
|
|
169
|
+
if (compatible.includes(QUICKTIME_BRAND)) {
|
|
170
|
+
return "video/quicktime";
|
|
171
|
+
}
|
|
172
|
+
if (has(MP4_AUDIO_BRANDS)) {
|
|
173
|
+
return "audio/mp4";
|
|
174
|
+
}
|
|
175
|
+
if (has(MP4_BRANDS)) {
|
|
176
|
+
return "video/mp4";
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function readVint(bytes, offset, keepMarker) {
|
|
181
|
+
if (offset >= bytes.length) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const first = bytes[offset];
|
|
185
|
+
if (first === 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
let length = 1;
|
|
189
|
+
while (!(first & 128 >> length - 1)) {
|
|
190
|
+
length++;
|
|
191
|
+
}
|
|
192
|
+
if (offset + length > bytes.length) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const marker = 128 >> length - 1;
|
|
196
|
+
let value = keepMarker ? first : first & marker - 1;
|
|
197
|
+
let allOnes = (first & marker - 1) === marker - 1;
|
|
198
|
+
for (let i = 1; i < length; i++) {
|
|
199
|
+
const byte = bytes[offset + i];
|
|
200
|
+
value = value * 256 + byte;
|
|
201
|
+
allOnes = allOnes && byte === 255;
|
|
202
|
+
}
|
|
203
|
+
return { value, length, unknown: !keepMarker && allOnes };
|
|
204
|
+
}
|
|
205
|
+
function detectEbml(bytes) {
|
|
206
|
+
if (!hasBytesAt(bytes, 0, EBML_SIGNATURE)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const headerSize = readVint(bytes, EBML_SIGNATURE.length, false);
|
|
210
|
+
if (!headerSize || headerSize.unknown) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const headerStart = EBML_SIGNATURE.length + headerSize.length;
|
|
214
|
+
const headerEnd = headerStart + headerSize.value;
|
|
215
|
+
let offset = headerStart;
|
|
216
|
+
while (offset < Math.min(headerEnd, bytes.length)) {
|
|
217
|
+
const id = readVint(bytes, offset, true);
|
|
218
|
+
if (!id || id.length > 4 || offset + id.length > headerEnd) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const size = readVint(bytes, offset + id.length, false);
|
|
222
|
+
if (!size || size.unknown) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const dataStart = offset + id.length + size.length;
|
|
226
|
+
if (dataStart + size.value > headerEnd) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (id.value === EBML_DOC_TYPE_ID) {
|
|
230
|
+
let docTypeEnd = dataStart + size.value;
|
|
231
|
+
if (size.value > MAX_DOC_TYPE_LENGTH || docTypeEnd > bytes.length) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
while (docTypeEnd > dataStart && bytes[docTypeEnd - 1] === 0) {
|
|
235
|
+
docTypeEnd--;
|
|
236
|
+
}
|
|
237
|
+
switch (ascii(bytes, dataStart, docTypeEnd)) {
|
|
238
|
+
case "webm":
|
|
239
|
+
return "video/webm";
|
|
240
|
+
case "matroska":
|
|
241
|
+
return "video/matroska";
|
|
242
|
+
default:
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
offset = dataStart + size.value;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function detectBmp(bytes) {
|
|
251
|
+
if (ascii(bytes, 0, 2) !== "BM") {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const reserved = readUint32LE(bytes, 6);
|
|
255
|
+
const infoHeaderSize = readUint32LE(bytes, 14);
|
|
256
|
+
if (reserved === 0 && infoHeaderSize !== null && BMP_INFO_HEADER_SIZES.has(infoHeaderSize)) {
|
|
257
|
+
return "image/bmp";
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const ZIP_LOCAL_HEADER = [80, 75, 3, 4];
|
|
262
|
+
const ZIP_CENTRAL_HEADER = [80, 75, 1, 2];
|
|
263
|
+
const ZIP_END_OF_DIRECTORY = [80, 75, 5, 6];
|
|
264
|
+
const ZIP_LOCAL_HEADER_SIZE = 30;
|
|
265
|
+
const ZIP_CENTRAL_HEADER_SIZE = 46;
|
|
266
|
+
const ZIP_END_OF_DIRECTORY_SIZE = 22;
|
|
267
|
+
const ZIP_MAX_COMMENT_LENGTH = 65535;
|
|
268
|
+
const OPEN_DOCUMENT_TYPES = /* @__PURE__ */ new Set([ODT, ODS, ODP]);
|
|
269
|
+
function readUint16LE(bytes, offset) {
|
|
270
|
+
if (bytes.length < offset + 2) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return bytes[offset] + bytes[offset + 1] * 256;
|
|
274
|
+
}
|
|
275
|
+
function detectOpenDocument(bytes) {
|
|
276
|
+
const flags = readUint16LE(bytes, 6);
|
|
277
|
+
const method = readUint16LE(bytes, 8);
|
|
278
|
+
const compressedSize = readUint32LE(bytes, 18);
|
|
279
|
+
const size = readUint32LE(bytes, 22);
|
|
280
|
+
const nameLength = readUint16LE(bytes, 26);
|
|
281
|
+
const extraLength = readUint16LE(bytes, 28);
|
|
282
|
+
if (flags === null || method === null || compressedSize === null || size === null || nameLength === null || extraLength === null) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (method !== 0 || flags & 8 || compressedSize !== size || size > 64) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const nameEnd = ZIP_LOCAL_HEADER_SIZE + nameLength;
|
|
289
|
+
if (ascii(bytes, ZIP_LOCAL_HEADER_SIZE, nameEnd) !== "mimetype") {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const contentStart = nameEnd + extraLength;
|
|
293
|
+
const content = ascii(bytes, contentStart, contentStart + size);
|
|
294
|
+
return content !== null && OPEN_DOCUMENT_TYPES.has(content) ? content : null;
|
|
295
|
+
}
|
|
296
|
+
const MAX_ZIP_END_RECORD_CANDIDATES = 16;
|
|
297
|
+
function* zipEndRecords(end) {
|
|
298
|
+
const last = end.length - ZIP_END_OF_DIRECTORY_SIZE;
|
|
299
|
+
const first = Math.max(0, last - ZIP_MAX_COMMENT_LENGTH);
|
|
300
|
+
let candidates = 0;
|
|
301
|
+
for (let offset = last; offset >= first; offset--) {
|
|
302
|
+
if (!hasBytesAt(end, offset, ZIP_END_OF_DIRECTORY) || readUint16LE(end, offset + 20) !== last - offset) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (++candidates > MAX_ZIP_END_RECORD_CANDIDATES) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const disk = readUint16LE(end, offset + 4);
|
|
309
|
+
const directoryDisk = readUint16LE(end, offset + 6);
|
|
310
|
+
const entries = readUint16LE(end, offset + 10);
|
|
311
|
+
const directorySize = readUint32LE(end, offset + 12);
|
|
312
|
+
const directoryOffset = readUint32LE(end, offset + 16);
|
|
313
|
+
if (disk !== 0 || directoryDisk !== 0 || entries === null || entries === 65535 || directorySize === null || directorySize === 4294967295 || directorySize > MAX_ZIP_DIRECTORY_BYTES || directoryOffset === null || directoryOffset === 4294967295) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
yield { recordOffset: offset, entries, directorySize, directoryOffset };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function findZipCentralDirectory(tail) {
|
|
320
|
+
for (const record of zipEndRecords(tail)) {
|
|
321
|
+
if (record.entries > 0 && record.directorySize > 0) {
|
|
322
|
+
return { offset: record.directoryOffset, size: record.directorySize };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
function readZipEntryNames(end, { recordOffset, entries, directorySize }) {
|
|
328
|
+
let offset = recordOffset - directorySize;
|
|
329
|
+
if (offset < 0) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const names = [];
|
|
333
|
+
for (let i = 0; i < entries; i++) {
|
|
334
|
+
if (!hasBytesAt(end, offset, ZIP_CENTRAL_HEADER)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const nameLength = readUint16LE(end, offset + 28);
|
|
338
|
+
const extraLength = readUint16LE(end, offset + 30);
|
|
339
|
+
const commentLength = readUint16LE(end, offset + 32);
|
|
340
|
+
if (nameLength === null || extraLength === null || commentLength === null) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const nameStart = offset + ZIP_CENTRAL_HEADER_SIZE;
|
|
344
|
+
const name = ascii(end, nameStart, nameStart + nameLength);
|
|
345
|
+
offset = nameStart + nameLength + extraLength + commentLength;
|
|
346
|
+
if (name === null || offset > recordOffset) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
names.push(name);
|
|
350
|
+
}
|
|
351
|
+
return offset === recordOffset ? names : null;
|
|
352
|
+
}
|
|
353
|
+
function classifyOfficeOpenXml(entryNames) {
|
|
354
|
+
const names = entryNames.map((name) => name.toLowerCase());
|
|
355
|
+
if (!names.includes("[content_types].xml")) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const families = [
|
|
359
|
+
{ prefix: "word/", plain: DOCX, macro: DOCM },
|
|
360
|
+
{ prefix: "xl/", plain: XLSX, macro: XLSM },
|
|
361
|
+
{ prefix: "ppt/", plain: PPTX, macro: PPTM }
|
|
362
|
+
].filter(({ prefix }) => names.some((name) => name.startsWith(prefix)));
|
|
363
|
+
if (families.length !== 1) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const hasMacros = names.some(
|
|
367
|
+
(name) => name.endsWith("vbaproject.bin") || name.endsWith("vbadata.xml") || name.startsWith("xl/macrosheets/")
|
|
368
|
+
);
|
|
369
|
+
return hasMacros ? families[0].macro : families[0].plain;
|
|
370
|
+
}
|
|
371
|
+
function detectZip(bytes, tail) {
|
|
372
|
+
if (!hasBytesAt(bytes, 0, ZIP_LOCAL_HEADER)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const openDocument = detectOpenDocument(bytes);
|
|
376
|
+
if (openDocument) {
|
|
377
|
+
return openDocument;
|
|
378
|
+
}
|
|
379
|
+
const end = tail ?? bytes;
|
|
380
|
+
for (const record of zipEndRecords(end)) {
|
|
381
|
+
const names = readZipEntryNames(end, record);
|
|
382
|
+
const type = names && classifyOfficeOpenXml(names);
|
|
383
|
+
if (type) {
|
|
384
|
+
return type;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
function detectContentType(bytes, options = {}) {
|
|
390
|
+
if (hasBytesAt(bytes, 0, [255, 216, 255])) {
|
|
391
|
+
return "image/jpeg";
|
|
392
|
+
}
|
|
393
|
+
if (hasBytesAt(bytes, 0, PNG_SIGNATURE)) {
|
|
394
|
+
return "image/png";
|
|
395
|
+
}
|
|
396
|
+
const head6 = ascii(bytes, 0, 6);
|
|
397
|
+
if (head6 === "GIF87a" || head6 === "GIF89a") {
|
|
398
|
+
return "image/gif";
|
|
399
|
+
}
|
|
400
|
+
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 12) === "WEBP") {
|
|
401
|
+
return "image/webp";
|
|
402
|
+
}
|
|
403
|
+
if (ascii(bytes, 0, 5) === "%PDF-") {
|
|
404
|
+
return "application/pdf";
|
|
405
|
+
}
|
|
406
|
+
const head4 = ascii(bytes, 0, 4);
|
|
407
|
+
if (head4 === "II*\0" || head4 === "MM\0*") {
|
|
408
|
+
return "image/tiff";
|
|
409
|
+
}
|
|
410
|
+
return detectIsoBmff(bytes) ?? detectEbml(bytes) ?? detectBmp(bytes) ?? detectZip(bytes, options.tail);
|
|
411
|
+
}
|
|
412
|
+
function isClaimedContentTypeAllowed(accept, claimedType) {
|
|
413
|
+
const claimed = canonicalContentType(claimedType);
|
|
414
|
+
if (!claimed || claimed.includes("*")) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
return isExplicitOnlyContentType(claimed) ? isContentTypeListed(accept, claimed) : isMimeTypeAllowed(accept, claimed);
|
|
418
|
+
}
|
|
419
|
+
function acceptedContentType(bytes, accept, claimedType, options = {}) {
|
|
420
|
+
const detected = detectContentType(bytes, options);
|
|
421
|
+
if (detected) {
|
|
422
|
+
const allowed = isExplicitOnlyContentType(detected) ? isContentTypeListed(accept, detected) : isMimeTypeAllowed(accept, detected);
|
|
423
|
+
return allowed ? detected : null;
|
|
424
|
+
}
|
|
425
|
+
if (!claimedType) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const claimed = canonicalContentType(claimedType);
|
|
429
|
+
if (DETECTABLE_CONTENT_TYPES.has(claimed)) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return isClaimedContentTypeAllowed(accept, claimed) ? claimed : null;
|
|
433
|
+
}
|
|
434
|
+
export {
|
|
435
|
+
DETECTABLE_CONTENT_TYPES,
|
|
436
|
+
DETECT_CONTENT_TYPE_BYTES,
|
|
437
|
+
DETECT_CONTENT_TYPE_TAIL_BYTES,
|
|
438
|
+
MAX_ZIP_DIRECTORY_BYTES,
|
|
439
|
+
acceptedContentType,
|
|
440
|
+
detectContentType,
|
|
441
|
+
findZipCentralDirectory,
|
|
442
|
+
isClaimedContentTypeAllowed
|
|
443
|
+
};
|
package/dist/fastify/plugin.d.ts
CHANGED
|
@@ -47,6 +47,22 @@ export interface UploadPluginOptions {
|
|
|
47
47
|
* origin, keeping in mind that the stored MIME type is client-asserted.
|
|
48
48
|
*/
|
|
49
49
|
inlineTypes?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Checks each file's type by what its bytes are, using
|
|
52
|
+
* `acceptedContentType()`, instead of trusting the type the client
|
|
53
|
+
* claimed. On `POST {prefix}/fs`, a file the detector recognises is
|
|
54
|
+
* accepted when its detected type is in the profile's `allowedMimeTypes`;
|
|
55
|
+
* one it doesn't recognise is accepted on its claimed type, through a
|
|
56
|
+
* wildcard too, except that a claimed type the detector would have
|
|
57
|
+
* recognised is refused, and a type that can carry script (SVG, HTML,
|
|
58
|
+
* JavaScript, XML) must be listed exactly. Files are stored under the
|
|
59
|
+
* canonical spelling of the accepted type (`image/jpeg` for a claimed
|
|
60
|
+
* `image/jpg`). The S3 webhook checks objects' bytes against their
|
|
61
|
+
* declared type the same way `confirmUpload` does.
|
|
62
|
+
*
|
|
63
|
+
* Defaults to `true`. `false` checks the claimed type only.
|
|
64
|
+
*/
|
|
65
|
+
verifyContentType?: boolean;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
68
|
* Registers the upload routes on a Fastify server. Register it on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/fastify/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAA;AAE5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/fastify/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAA;AAE5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAiB9D,OAAO,KAAK,EAEV,cAAc,EACd,cAAc,EAEf,MAAM,aAAa,CAAA;AAIpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AA4B1D,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,iCAAiC;IACjC,OAAO,EAAE,cAAc,CAAA;IACvB,EAAE,EAAE,cAAc,CAAA;IAClB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;QACnC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;KACrB,CAAA;IACD,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AA+DD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA2Rf"}
|
package/dist/fastify/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fastifyMultipart from "@fastify/multipart";
|
|
2
2
|
import { deleteFile } from "../deleteFile.js";
|
|
3
|
+
import { acceptedContentType, detectContentType } from "../detectContentType.js";
|
|
3
4
|
import { UploadError } from "../errors.js";
|
|
4
5
|
import { generateStorageKey, trimTrailingSlashes } from "../keys.js";
|
|
5
6
|
import {
|
|
@@ -43,6 +44,22 @@ function headerValue(req, name) {
|
|
|
43
44
|
function normalizeMimeType(mimeType) {
|
|
44
45
|
return mimeType.split(";")[0].trim().toLowerCase();
|
|
45
46
|
}
|
|
47
|
+
function verifiedContentType(payload, data, claimedType) {
|
|
48
|
+
const mimeType = acceptedContentType(
|
|
49
|
+
data,
|
|
50
|
+
payload.allowedMimeTypes,
|
|
51
|
+
claimedType
|
|
52
|
+
);
|
|
53
|
+
if (mimeType) {
|
|
54
|
+
return mimeType;
|
|
55
|
+
}
|
|
56
|
+
const detected = detectContentType(data);
|
|
57
|
+
const what = detected ? `'${detected}'` : `of a type that couldn't be recognised (claimed '${claimedType}')`;
|
|
58
|
+
throw new UploadError(
|
|
59
|
+
"MIME_TYPE_NOT_ALLOWED",
|
|
60
|
+
`File is ${what}, which is not allowed for upload profile '${payload.profile}'.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
46
63
|
function contentDispositionHeader(disposition, filename) {
|
|
47
64
|
const ascii = filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
|
|
48
65
|
const encoded = encodeURIComponent(filename);
|
|
@@ -58,7 +75,8 @@ async function cedarUploadsPlugin(fastify, options) {
|
|
|
58
75
|
authenticate,
|
|
59
76
|
s3Webhook,
|
|
60
77
|
serveCacheControl = "private, max-age=3600",
|
|
61
|
-
inlineTypes = []
|
|
78
|
+
inlineTypes = [],
|
|
79
|
+
verifyContentType = true
|
|
62
80
|
} = options;
|
|
63
81
|
const allowedInlineTypes = new Set(inlineTypes.map(normalizeMimeType));
|
|
64
82
|
if (!tokenSecret) {
|
|
@@ -120,7 +138,12 @@ async function cedarUploadsPlugin(fastify, options) {
|
|
|
120
138
|
if (part.type !== "file") {
|
|
121
139
|
continue;
|
|
122
140
|
}
|
|
123
|
-
|
|
141
|
+
if (!verifyContentType) {
|
|
142
|
+
assertTokenAllowsFile(payload, {
|
|
143
|
+
contentType: part.mimetype,
|
|
144
|
+
size: 0n
|
|
145
|
+
});
|
|
146
|
+
}
|
|
124
147
|
let data;
|
|
125
148
|
try {
|
|
126
149
|
data = await part.toBuffer();
|
|
@@ -134,18 +157,19 @@ async function cedarUploadsPlugin(fastify, options) {
|
|
|
134
157
|
throw e;
|
|
135
158
|
}
|
|
136
159
|
const size = BigInt(data.byteLength);
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
const mimeType = verifyContentType ? verifiedContentType(payload, data, part.mimetype) : part.mimetype;
|
|
161
|
+
assertTokenAllowsFile(payload, { contentType: mimeType, size });
|
|
162
|
+
const storageKey = generateStorageKey(mimeType);
|
|
139
163
|
const upload = await createPendingUpload({
|
|
140
164
|
db,
|
|
141
165
|
payload,
|
|
142
166
|
filename: part.filename,
|
|
143
|
-
mimeType
|
|
167
|
+
mimeType,
|
|
144
168
|
size,
|
|
145
169
|
storageKey
|
|
146
170
|
});
|
|
147
171
|
stored.push({ target: payload.target, upload });
|
|
148
|
-
await target.write(storageKey, data, { contentType:
|
|
172
|
+
await target.write(storageKey, data, { contentType: mimeType });
|
|
149
173
|
const { count } = await db.upload.updateMany({
|
|
150
174
|
where: { id: upload.id, status: "pending" },
|
|
151
175
|
data: { status: "completed" }
|
|
@@ -229,7 +253,8 @@ async function cedarUploadsPlugin(fastify, options) {
|
|
|
229
253
|
targets,
|
|
230
254
|
topicArn: s3Webhook.topicArn,
|
|
231
255
|
fetchCertificate: s3Webhook.fetchCertificate,
|
|
232
|
-
fetch: s3Webhook.fetch
|
|
256
|
+
fetch: s3Webhook.fetch,
|
|
257
|
+
verifyContentType
|
|
233
258
|
});
|
|
234
259
|
return reply.code(200).send(result);
|
|
235
260
|
} catch (e) {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export type { UploadErrorCode } from './errors.js';
|
|
|
4
4
|
export { defineStorageTargets, resolveTarget } from './targets.js';
|
|
5
5
|
export { DB_MAX_FILE_SIZE, DEFAULT_UPLOAD_TOKEN_EXPIRES_IN, defineUploadProfiles, isMimeTypeAllowed, resolveProfile, } from './profiles.js';
|
|
6
6
|
export type { ResolvedUploadProfile, UploadProfile, UploadProfiles, } from './profiles.js';
|
|
7
|
+
export { acceptedContentType, DETECT_CONTENT_TYPE_BYTES, DETECT_CONTENT_TYPE_TAIL_BYTES, DETECTABLE_CONTENT_TYPES, detectContentType, findZipCentralDirectory, isClaimedContentTypeAllowed, MAX_ZIP_DIRECTORY_BYTES, } from './detectContentType.js';
|
|
8
|
+
export type { DetectContentTypeOptions } from './detectContentType.js';
|
|
9
|
+
export { canonicalContentType, CONTENT_TYPE_ALIASES, isExplicitOnlyContentType, isScriptableContentType, MACRO_CONTENT_TYPES, SCRIPTABLE_CONTENT_TYPES, } from './contentTypes.js';
|
|
7
10
|
export { createFsProvider } from './providers/fs.js';
|
|
8
11
|
export type { FsProviderOptions } from './providers/fs.js';
|
|
9
12
|
export { createDbProvider } from './providers/db.js';
|
|
@@ -28,7 +31,7 @@ export type { CreateUploadAuthenticatorOptions, UploadAuthenticator, UploadReque
|
|
|
28
31
|
export { cedarUploadsPlugin } from './fastify/plugin.js';
|
|
29
32
|
export type { UploadPluginOptions } from './fastify/plugin.js';
|
|
30
33
|
export { handleS3Webhook, processS3EventRecord } from './webhooks/s3.js';
|
|
31
|
-
export type { S3Event, S3EventOutcome, S3EventRecord, S3WebhookOptions, S3WebhookResult, } from './webhooks/s3.js';
|
|
34
|
+
export type { ProcessS3EventRecordOptions, S3Event, S3EventOutcome, S3EventRecord, S3WebhookOptions, S3WebhookResult, } from './webhooks/s3.js';
|
|
32
35
|
export { verifySnsMessage } from './webhooks/sns.js';
|
|
33
36
|
export type { SnsMessage } from './webhooks/sns.js';
|
|
34
37
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAClE,OAAO,EACL,gBAAgB,EAChB,+BAA+B,EAC/B,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,qBAAqB,EACrB,aAAa,EACb,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,4BAA4B,EAC5B,uBAAuB,EACvB,uBAAuB,EACvB,WAAW,EACX,mBAAmB,GACpB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iCAAiC,EACjC,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,kCAAkC,EAClC,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAC9D,YAAY,EACV,gCAAgC,EAChC,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EACV,OAAO,EACP,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAClE,OAAO,EACL,gBAAgB,EAChB,+BAA+B,EAC/B,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,qBAAqB,EACrB,aAAa,EACb,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,8BAA8B,EAC9B,wBAAwB,EACxB,iBAAiB,EACjB,uBAAuB,EACvB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACtE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,4BAA4B,EAC5B,uBAAuB,EACvB,uBAAuB,EACvB,WAAW,EACX,mBAAmB,GACpB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iCAAiC,EACjC,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,kCAAkC,EAClC,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAC9D,YAAY,EACV,gCAAgC,EAChC,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EACV,2BAA2B,EAC3B,OAAO,EACP,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,24 @@ import {
|
|
|
8
8
|
isMimeTypeAllowed,
|
|
9
9
|
resolveProfile
|
|
10
10
|
} from "./profiles.js";
|
|
11
|
+
import {
|
|
12
|
+
acceptedContentType,
|
|
13
|
+
DETECT_CONTENT_TYPE_BYTES,
|
|
14
|
+
DETECT_CONTENT_TYPE_TAIL_BYTES,
|
|
15
|
+
DETECTABLE_CONTENT_TYPES,
|
|
16
|
+
detectContentType,
|
|
17
|
+
findZipCentralDirectory,
|
|
18
|
+
isClaimedContentTypeAllowed,
|
|
19
|
+
MAX_ZIP_DIRECTORY_BYTES
|
|
20
|
+
} from "./detectContentType.js";
|
|
21
|
+
import {
|
|
22
|
+
canonicalContentType,
|
|
23
|
+
CONTENT_TYPE_ALIASES,
|
|
24
|
+
isExplicitOnlyContentType,
|
|
25
|
+
isScriptableContentType,
|
|
26
|
+
MACRO_CONTENT_TYPES,
|
|
27
|
+
SCRIPTABLE_CONTENT_TYPES
|
|
28
|
+
} from "./contentTypes.js";
|
|
11
29
|
import { createFsProvider } from "./providers/fs.js";
|
|
12
30
|
import { createDbProvider } from "./providers/db.js";
|
|
13
31
|
import { generateStorageKey } from "./keys.js";
|
|
@@ -46,13 +64,22 @@ import { cedarUploadsPlugin } from "./fastify/plugin.js";
|
|
|
46
64
|
import { handleS3Webhook, processS3EventRecord } from "./webhooks/s3.js";
|
|
47
65
|
import { verifySnsMessage } from "./webhooks/sns.js";
|
|
48
66
|
export {
|
|
67
|
+
CONTENT_TYPE_ALIASES,
|
|
49
68
|
DB_MAX_FILE_SIZE,
|
|
50
69
|
DEFAULT_UPLOAD_TOKEN_EXPIRES_IN,
|
|
70
|
+
DETECTABLE_CONTENT_TYPES,
|
|
71
|
+
DETECT_CONTENT_TYPE_BYTES,
|
|
72
|
+
DETECT_CONTENT_TYPE_TAIL_BYTES,
|
|
73
|
+
MACRO_CONTENT_TYPES,
|
|
74
|
+
MAX_ZIP_DIRECTORY_BYTES,
|
|
75
|
+
SCRIPTABLE_CONTENT_TYPES,
|
|
51
76
|
SERVE_TOKEN_PURPOSE,
|
|
52
77
|
UPLOAD_TOKEN_CONTEXT_KEY,
|
|
53
78
|
UPLOAD_TOKEN_HEADER,
|
|
54
79
|
UPLOAD_TOKEN_PURPOSE,
|
|
55
80
|
UploadError,
|
|
81
|
+
acceptedContentType,
|
|
82
|
+
canonicalContentType,
|
|
56
83
|
cedarUploadsPlugin,
|
|
57
84
|
cleanupStaleUploads,
|
|
58
85
|
confirmUpload,
|
|
@@ -68,10 +95,15 @@ export {
|
|
|
68
95
|
defineStorageTargets,
|
|
69
96
|
defineUploadProfiles,
|
|
70
97
|
deleteFile,
|
|
98
|
+
detectContentType,
|
|
99
|
+
findZipCentralDirectory,
|
|
71
100
|
generateStorageKey,
|
|
72
101
|
getUploadTokenPayload,
|
|
73
102
|
handleS3Webhook,
|
|
103
|
+
isClaimedContentTypeAllowed,
|
|
104
|
+
isExplicitOnlyContentType,
|
|
74
105
|
isMimeTypeAllowed,
|
|
106
|
+
isScriptableContentType,
|
|
75
107
|
issueUploadToken,
|
|
76
108
|
processS3EventRecord,
|
|
77
109
|
requireUploadTokenSchema,
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A fresh storage key: a random id plus the extension that matches
|
|
3
3
|
* `mimeType`, when one is known. Keys never derive from the client's
|
|
4
|
-
* filename.
|
|
4
|
+
* filename. Aliased types are looked up under every spelling, so the
|
|
5
|
+
* registered `video/matroska` gets the `.mkv` the database only knows for
|
|
6
|
+
* `video/x-matroska`.
|
|
5
7
|
*/
|
|
6
8
|
export declare function generateStorageKey(mimeType: string): string;
|
|
7
9
|
/**
|
package/dist/keys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM3D;AAID;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,QAIxC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQzD"}
|
package/dist/keys.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import mime from "mime-types";
|
|
3
|
+
import { canonicalContentType, contentTypeSpellings } from "./contentTypes.js";
|
|
3
4
|
import { UploadError } from "./errors.js";
|
|
4
5
|
function generateStorageKey(mimeType) {
|
|
5
|
-
const ext =
|
|
6
|
+
const ext = contentTypeSpellings(canonicalContentType(mimeType)).map((type) => mime.extension(type)).find(Boolean);
|
|
6
7
|
return ext ? `${randomUUID()}.${ext}` : randomUUID();
|
|
7
8
|
}
|
|
8
9
|
const SAFE_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|