@schafevormfenster/rest-commons 0.1.1
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/CONTRIBUTING.md +1190 -0
- package/README.md +275 -0
- package/bin/setup.js +10 -0
- package/dist/api-schemas/error.schema.d.ts +20 -0
- package/dist/api-schemas/error.schema.d.ts.map +1 -0
- package/dist/api-schemas/error.schema.js +17 -0
- package/dist/api-schemas/health.schema.d.ts +497 -0
- package/dist/api-schemas/health.schema.d.ts.map +1 -0
- package/dist/api-schemas/health.schema.js +33 -0
- package/dist/api-schemas/okay.schema.d.ts +13 -0
- package/dist/api-schemas/okay.schema.d.ts.map +1 -0
- package/dist/api-schemas/okay.schema.js +5 -0
- package/dist/api-schemas/paginated-results.schema.d.ts +59 -0
- package/dist/api-schemas/paginated-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/paginated-results.schema.js +10 -0
- package/dist/api-schemas/partial-results.schema.d.ts +30 -0
- package/dist/api-schemas/partial-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/partial-results.schema.js +10 -0
- package/dist/api-schemas/result.schema.d.ts +17 -0
- package/dist/api-schemas/result.schema.d.ts.map +1 -0
- package/dist/api-schemas/result.schema.js +5 -0
- package/dist/api-schemas/results.schema.d.ts +21 -0
- package/dist/api-schemas/results.schema.d.ts.map +1 -0
- package/dist/api-schemas/results.schema.js +5 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts +7 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts.map +1 -0
- package/dist/helpers/correlation/get-correlation-id.js +16 -0
- package/dist/helpers/correlation/get-header.d.ts +7 -0
- package/dist/helpers/correlation/get-header.d.ts.map +1 -0
- package/dist/helpers/correlation/get-header.js +11 -0
- package/dist/helpers/detect-mime-type.d.ts +11 -0
- package/dist/helpers/detect-mime-type.d.ts.map +1 -0
- package/dist/helpers/detect-mime-type.js +40 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts +8 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts.map +1 -0
- package/dist/helpers/detect-suspicious-patterns.js +55 -0
- package/dist/helpers/eventify-constants.types.d.ts +32 -0
- package/dist/helpers/eventify-constants.types.d.ts.map +1 -0
- package/dist/helpers/eventify-constants.types.js +40 -0
- package/dist/helpers/hash-binary.d.ts +21 -0
- package/dist/helpers/hash-binary.d.ts.map +1 -0
- package/dist/helpers/hash-binary.js +28 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-image-mime-type.js +41 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.js +34 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.js +13 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.js +23 -0
- package/dist/helpers/parameter-validation.d.ts +6 -0
- package/dist/helpers/parameter-validation.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.js +19 -0
- package/dist/helpers/parameter-validation.types.d.ts +16 -0
- package/dist/helpers/parameter-validation.types.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.types.js +38 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts +6 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts.map +1 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.js +23 -0
- package/dist/helpers/response-headers/environment.types.d.ts +2 -0
- package/dist/helpers/response-headers/environment.types.d.ts.map +1 -0
- package/dist/helpers/response-headers/environment.types.js +1 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts +8 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts.map +1 -0
- package/dist/helpers/response-headers/resolve-environment.js +18 -0
- package/dist/helpers/slugify.d.ts +15 -0
- package/dist/helpers/slugify.d.ts.map +1 -0
- package/dist/helpers/slugify.js +32 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/normalization/normalize-list.d.ts +11 -0
- package/dist/normalization/normalize-list.d.ts.map +1 -0
- package/dist/normalization/normalize-list.js +19 -0
- package/dist/normalization/normalize-location.d.ts +16 -0
- package/dist/normalization/normalize-location.d.ts.map +1 -0
- package/dist/normalization/normalize-location.js +26 -0
- package/dist/primitives/coordinate-precision.d.ts +10 -0
- package/dist/primitives/coordinate-precision.d.ts.map +1 -0
- package/dist/primitives/coordinate-precision.js +27 -0
- package/dist/primitives/geo-point.schema.d.ts +8 -0
- package/dist/primitives/geo-point.schema.d.ts.map +1 -0
- package/dist/primitives/geo-point.schema.js +10 -0
- package/dist/primitives/geoname-id.schema.d.ts +8 -0
- package/dist/primitives/geoname-id.schema.d.ts.map +1 -0
- package/dist/primitives/geoname-id.schema.js +9 -0
- package/dist/primitives/international-zip.schema.d.ts +76 -0
- package/dist/primitives/international-zip.schema.d.ts.map +1 -0
- package/dist/primitives/international-zip.schema.js +81 -0
- package/dist/primitives/latitude.schema.d.ts +9 -0
- package/dist/primitives/latitude.schema.d.ts.map +1 -0
- package/dist/primitives/latitude.schema.js +13 -0
- package/dist/primitives/location.schema.d.ts +8 -0
- package/dist/primitives/location.schema.d.ts.map +1 -0
- package/dist/primitives/location.schema.js +15 -0
- package/dist/primitives/longitude.schema.d.ts +9 -0
- package/dist/primitives/longitude.schema.d.ts.map +1 -0
- package/dist/primitives/longitude.schema.js +13 -0
- package/dist/primitives/numeric-id.schema.d.ts +8 -0
- package/dist/primitives/numeric-id.schema.d.ts.map +1 -0
- package/dist/primitives/numeric-id.schema.js +10 -0
- package/dist/primitives/slug.schema.d.ts +17 -0
- package/dist/primitives/slug.schema.d.ts.map +1 -0
- package/dist/primitives/slug.schema.js +30 -0
- package/dist/primitives/uuid.schema.d.ts +8 -0
- package/dist/primitives/uuid.schema.d.ts.map +1 -0
- package/dist/primitives/uuid.schema.js +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts.map +1 -0
- package/dist/primitives/wikidata-id.schema.js +10 -0
- package/dist/time/boundary-enforcement.d.ts +11 -0
- package/dist/time/boundary-enforcement.d.ts.map +1 -0
- package/dist/time/boundary-enforcement.js +43 -0
- package/dist/time/bounded-time.schema.d.ts +31 -0
- package/dist/time/bounded-time.schema.d.ts.map +1 -0
- package/dist/time/bounded-time.schema.js +77 -0
- package/dist/time/flexible-time-parser.d.ts +12 -0
- package/dist/time/flexible-time-parser.d.ts.map +1 -0
- package/dist/time/flexible-time-parser.js +94 -0
- package/dist/time/flexible-time.schema.d.ts +31 -0
- package/dist/time/flexible-time.schema.d.ts.map +1 -0
- package/dist/time/flexible-time.schema.js +31 -0
- package/dist/time/get-week-end.d.ts +10 -0
- package/dist/time/get-week-end.d.ts.map +1 -0
- package/dist/time/get-week-end.js +25 -0
- package/dist/time/get-week-start.d.ts +10 -0
- package/dist/time/get-week-start.d.ts.map +1 -0
- package/dist/time/get-week-start.js +25 -0
- package/dist/time/is-relative-time.d.ts +8 -0
- package/dist/time/is-relative-time.d.ts.map +1 -0
- package/dist/time/is-relative-time.js +9 -0
- package/dist/time/iso8601.schema.d.ts +14 -0
- package/dist/time/iso8601.schema.d.ts.map +1 -0
- package/dist/time/iso8601.schema.js +17 -0
- package/dist/time/iso8601.types.d.ts +6 -0
- package/dist/time/iso8601.types.d.ts.map +1 -0
- package/dist/time/iso8601.types.js +11 -0
- package/dist/time/parse-relative-time.d.ts +9 -0
- package/dist/time/parse-relative-time.d.ts.map +1 -0
- package/dist/time/parse-relative-time.js +36 -0
- package/dist/time/relative-time.schema.d.ts +23 -0
- package/dist/time/relative-time.schema.d.ts.map +1 -0
- package/dist/time/relative-time.schema.js +25 -0
- package/dist/time/since-parameter.schema.d.ts +8 -0
- package/dist/time/since-parameter.schema.d.ts.map +1 -0
- package/dist/time/since-parameter.schema.js +56 -0
- package/dist/time/time-helpers.d.ts +19 -0
- package/dist/time/time-helpers.d.ts.map +1 -0
- package/dist/time/time-helpers.js +56 -0
- package/dist/time/time-schemas.d.ts +20 -0
- package/dist/time/time-schemas.d.ts.map +1 -0
- package/dist/time/time-schemas.js +25 -0
- package/dist/time/timezone.types.d.ts +17 -0
- package/dist/time/timezone.types.d.ts.map +1 -0
- package/dist/time/timezone.types.js +15 -0
- package/dist/validation/zod-error-handler.d.ts +3 -0
- package/dist/validation/zod-error-handler.d.ts.map +1 -0
- package/dist/validation/zod-error-handler.js +189 -0
- package/dist/validation/zod-utils.d.ts +9 -0
- package/dist/validation/zod-utils.d.ts.map +1 -0
- package/dist/validation/zod-utils.js +23 -0
- package/eslint.config.mjs +16 -0
- package/package.json +44 -0
- package/src/api-schemas/error.schema.test.ts +27 -0
- package/src/api-schemas/error.schema.ts +23 -0
- package/src/api-schemas/health.schema.test.ts +104 -0
- package/src/api-schemas/health.schema.ts +63 -0
- package/src/api-schemas/okay.schema.test.ts +15 -0
- package/src/api-schemas/okay.schema.ts +8 -0
- package/src/api-schemas/paginated-results.schema.ts +17 -0
- package/src/api-schemas/partial-results.schema.ts +13 -0
- package/src/api-schemas/result.schema.test.ts +19 -0
- package/src/api-schemas/result.schema.ts +9 -0
- package/src/api-schemas/results.schema.test.ts +15 -0
- package/src/api-schemas/results.schema.ts +9 -0
- package/src/helpers/correlation/get-correlation-id.test.ts +126 -0
- package/src/helpers/correlation/get-correlation-id.ts +22 -0
- package/src/helpers/correlation/get-header.test.ts +179 -0
- package/src/helpers/correlation/get-header.ts +21 -0
- package/src/helpers/detect-mime-type.test.ts +100 -0
- package/src/helpers/detect-mime-type.ts +46 -0
- package/src/helpers/detect-suspicious-patterns.test.ts +45 -0
- package/src/helpers/detect-suspicious-patterns.ts +57 -0
- package/src/helpers/eventify-constants.test.ts +52 -0
- package/src/helpers/eventify-constants.types.test.ts +52 -0
- package/src/helpers/eventify-constants.types.ts +51 -0
- package/src/helpers/hash-binary.test.ts +60 -0
- package/src/helpers/hash-binary.ts +30 -0
- package/src/helpers/mime-types/detect-image-mime-type.test.ts +73 -0
- package/src/helpers/mime-types/detect-image-mime-type.ts +50 -0
- package/src/helpers/mime-types/detect-ole-mime-type.test.ts +86 -0
- package/src/helpers/mime-types/detect-ole-mime-type.ts +44 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.test.ts +39 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.ts +15 -0
- package/src/helpers/mime-types/detect-zip-mime-type.test.ts +88 -0
- package/src/helpers/mime-types/detect-zip-mime-type.ts +28 -0
- package/src/helpers/parameter-validation.test.ts +35 -0
- package/src/helpers/parameter-validation.ts +32 -0
- package/src/helpers/process-eventify-request.ts +146 -0
- package/src/helpers/response-headers/build-api-unauthorized-headers.ts +30 -0
- package/src/helpers/response-headers/environment.types.ts +1 -0
- package/src/helpers/response-headers/resolve-environment.ts +17 -0
- package/src/helpers/slugify.test.ts +77 -0
- package/src/helpers/slugify.ts +34 -0
- package/src/index.ts +46 -0
- package/src/normalization/normalize-list.test.ts +43 -0
- package/src/normalization/normalize-list.ts +21 -0
- package/src/normalization/normalize-location.test.ts +91 -0
- package/src/normalization/normalize-location.ts +29 -0
- package/src/primitives/coordinate-precision.test.ts +46 -0
- package/src/primitives/coordinate-precision.ts +30 -0
- package/src/primitives/geo-point.schema.test.ts +70 -0
- package/src/primitives/geo-point.schema.ts +14 -0
- package/src/primitives/geoname-id.schema.test.ts +60 -0
- package/src/primitives/geoname-id.schema.ts +12 -0
- package/src/primitives/international-zip.schema.test.ts +212 -0
- package/src/primitives/international-zip.schema.ts +103 -0
- package/src/primitives/latitude.schema.test.ts +77 -0
- package/src/primitives/latitude.schema.ts +20 -0
- package/src/primitives/location.schema.test.ts +21 -0
- package/src/primitives/location.schema.ts +22 -0
- package/src/primitives/longitude.schema.test.ts +77 -0
- package/src/primitives/longitude.schema.ts +20 -0
- package/src/primitives/numeric-id.schema.test.ts +32 -0
- package/src/primitives/numeric-id.schema.ts +13 -0
- package/src/primitives/slug.schema.test.ts +101 -0
- package/src/primitives/slug.schema.ts +41 -0
- package/src/primitives/uuid.schema.test.ts +45 -0
- package/src/primitives/uuid.schema.ts +12 -0
- package/src/primitives/wikidata-id.schema.test.ts +51 -0
- package/src/primitives/wikidata-id.schema.ts +16 -0
- package/src/time/README.md +220 -0
- package/src/time/boundary-enforcement.test.ts +130 -0
- package/src/time/boundary-enforcement.ts +59 -0
- package/src/time/bounded-time.schema.test.ts +294 -0
- package/src/time/bounded-time.schema.ts +111 -0
- package/src/time/flexible-time-parser.test.ts +586 -0
- package/src/time/flexible-time-parser.ts +122 -0
- package/src/time/flexible-time.schema.test.ts +243 -0
- package/src/time/flexible-time.schema.ts +43 -0
- package/src/time/is-relative-time.test.ts +23 -0
- package/src/time/is-relative-time.ts +9 -0
- package/src/time/iso8601.schema.ts +29 -0
- package/src/time/iso8601.types.test.ts +112 -0
- package/src/time/iso8601.types.ts +21 -0
- package/src/time/parse-relative-time.test.ts +49 -0
- package/src/time/parse-relative-time.ts +50 -0
- package/src/time/relative-time.schema.test.ts +23 -0
- package/src/time/relative-time.schema.ts +38 -0
- package/src/time/since-parameter.schema.test.ts +59 -0
- package/src/time/since-parameter.schema.ts +69 -0
- package/src/time/time-helpers.test.ts +263 -0
- package/src/time/time-helpers.ts +78 -0
- package/src/time/time-schemas.test.ts +181 -0
- package/src/time/time-schemas.ts +42 -0
- package/src/time/time.schema.test.ts +237 -0
- package/src/time/timezone-independence.test.ts +188 -0
- package/src/time/timezone.types.test.ts +55 -0
- package/src/time/timezone.types.ts +22 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { detectOleMimeType } from "./detect-ole-mime-type";
|
|
4
|
+
|
|
5
|
+
describe("detectOleMimeType", () => {
|
|
6
|
+
it("returns undefined for empty buffer", () => {
|
|
7
|
+
const result = detectOleMimeType(Buffer.from([]));
|
|
8
|
+
expect(result).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns undefined for buffer with less than 8 bytes", () => {
|
|
12
|
+
const result = detectOleMimeType(Buffer.from([0xD0, 0xCF, 0x11, 0xE0]));
|
|
13
|
+
expect(result).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns undefined for non-OLE2 file", () => {
|
|
17
|
+
const nonOleHeader = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00]);
|
|
18
|
+
const result = detectOleMimeType(nonOleHeader);
|
|
19
|
+
expect(result).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("detects Word document from OLE2 signature with Word.Document indicator", () => {
|
|
23
|
+
const documentHeader = Buffer.concat([
|
|
24
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
25
|
+
Buffer.alloc(50, 0x00),
|
|
26
|
+
Buffer.from("Word.Document"),
|
|
27
|
+
]);
|
|
28
|
+
const result = detectOleMimeType(documentHeader);
|
|
29
|
+
expect(result).toBe("application/msword");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("detects Word document from OLE2 signature with Microsoft Word indicator", () => {
|
|
33
|
+
const documentHeader = Buffer.concat([
|
|
34
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
35
|
+
Buffer.alloc(50, 0x00),
|
|
36
|
+
Buffer.from("Microsoft Word"),
|
|
37
|
+
]);
|
|
38
|
+
const result = detectOleMimeType(documentHeader);
|
|
39
|
+
expect(result).toBe("application/msword");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("detects PowerPoint from OLE2 signature with PowerPoint Document indicator", () => {
|
|
43
|
+
const pptHeader = Buffer.concat([
|
|
44
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
45
|
+
Buffer.alloc(50, 0x00),
|
|
46
|
+
Buffer.from("PowerPoint Document"),
|
|
47
|
+
]);
|
|
48
|
+
const result = detectOleMimeType(pptHeader);
|
|
49
|
+
expect(result).toBe("application/vnd.ms-powerpoint");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects Excel from OLE2 signature with Workbook indicator", () => {
|
|
53
|
+
const xlsHeader = Buffer.concat([
|
|
54
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
55
|
+
Buffer.alloc(50, 0x00),
|
|
56
|
+
Buffer.from("Workbook"),
|
|
57
|
+
]);
|
|
58
|
+
const result = detectOleMimeType(xlsHeader);
|
|
59
|
+
expect(result).toBe("application/vnd.ms-excel");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("detects Excel from OLE2 signature with Excel indicator", () => {
|
|
63
|
+
const xlsHeader = Buffer.concat([
|
|
64
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
65
|
+
Buffer.alloc(50, 0x00),
|
|
66
|
+
Buffer.from("Excel"),
|
|
67
|
+
]);
|
|
68
|
+
const result = detectOleMimeType(xlsHeader);
|
|
69
|
+
expect(result).toBe("application/vnd.ms-excel");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("defaults to Word document for OLE2 without specific indicators", () => {
|
|
73
|
+
const genericOleHeader = Buffer.concat([
|
|
74
|
+
Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
|
|
75
|
+
Buffer.alloc(100, 0x00),
|
|
76
|
+
]);
|
|
77
|
+
const result = detectOleMimeType(genericOleHeader);
|
|
78
|
+
expect(result).toBe("application/msword");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns undefined for partial OLE2 signature", () => {
|
|
82
|
+
const partialOle = Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0x00]);
|
|
83
|
+
const result = detectOleMimeType(partialOle);
|
|
84
|
+
expect(result).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect OLE2 (legacy Office) formats from magic bytes
|
|
3
|
+
* Detects DOC, PPT, and XLS files which use OLE2 container format
|
|
4
|
+
*/
|
|
5
|
+
export function detectOleMimeType(buffer: Buffer): string | undefined {
|
|
6
|
+
if (buffer.length < 8) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// OLE2 signature: D0 CF 11 E0 A1 B1 1A E1
|
|
11
|
+
if (
|
|
12
|
+
!(
|
|
13
|
+
buffer[0] === 0xD0 &&
|
|
14
|
+
buffer[1] === 0xCF &&
|
|
15
|
+
buffer[2] === 0x11 &&
|
|
16
|
+
buffer[3] === 0xE0 &&
|
|
17
|
+
buffer[4] === 0xA1 &&
|
|
18
|
+
buffer[5] === 0xB1 &&
|
|
19
|
+
buffer[6] === 0x1A &&
|
|
20
|
+
buffer[7] === 0xE1
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If it's OLE2, try to detect Office type by looking for content indicators
|
|
27
|
+
// Look for "PowerPoint Document" or "Word.Document" in buffer
|
|
28
|
+
const bufferString = buffer.toString("utf8", 0, Math.min(buffer.length, 1024));
|
|
29
|
+
|
|
30
|
+
if (bufferString.includes("PowerPoint Document")) {
|
|
31
|
+
return "application/vnd.ms-powerpoint";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (bufferString.includes("Word.Document") || bufferString.includes("Microsoft Word")) {
|
|
35
|
+
return "application/msword";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (bufferString.includes("Workbook") || bufferString.includes("Excel")) {
|
|
39
|
+
return "application/vnd.ms-excel";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Default OLE2 MIME type is Word document
|
|
43
|
+
return "application/msword";
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { detectPdfMimeType } from "./detect-pdf-mime-type";
|
|
4
|
+
|
|
5
|
+
describe("detectPdfMimeType", () => {
|
|
6
|
+
it("returns undefined for empty buffer", () => {
|
|
7
|
+
const result = detectPdfMimeType(Buffer.from([]));
|
|
8
|
+
expect(result).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns undefined for buffer with less than 4 bytes", () => {
|
|
12
|
+
const result = detectPdfMimeType(Buffer.from([0x25, 0x50]));
|
|
13
|
+
expect(result).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("detects PDF from magic bytes", () => {
|
|
17
|
+
const pdfHeader = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34]);
|
|
18
|
+
const result = detectPdfMimeType(pdfHeader);
|
|
19
|
+
expect(result).toBe("application/pdf");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("detects PDF with minimal header (%PDF)", () => {
|
|
23
|
+
const pdfHeader = Buffer.from([0x25, 0x50, 0x44, 0x46]);
|
|
24
|
+
const result = detectPdfMimeType(pdfHeader);
|
|
25
|
+
expect(result).toBe("application/pdf");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns undefined for non-PDF file", () => {
|
|
29
|
+
const nonPdfHeader = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]);
|
|
30
|
+
const result = detectPdfMimeType(nonPdfHeader);
|
|
31
|
+
expect(result).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns undefined for partial PDF signature", () => {
|
|
35
|
+
const partialPdf = Buffer.from([0x25, 0x50, 0x44, 0x00]);
|
|
36
|
+
const result = detectPdfMimeType(partialPdf);
|
|
37
|
+
expect(result).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect PDF MIME type from magic bytes
|
|
3
|
+
*/
|
|
4
|
+
export function detectPdfMimeType(buffer: Buffer): string | undefined {
|
|
5
|
+
if (buffer.length < 4) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// PDF: 25 50 44 46 (%PDF)
|
|
10
|
+
if (buffer[0] === 0x25 && buffer[1] === 0x50 && buffer[2] === 0x44 && buffer[3] === 0x46) {
|
|
11
|
+
return "application/pdf";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { detectZipMimeType } from "./detect-zip-mime-type";
|
|
4
|
+
|
|
5
|
+
describe("detectZipMimeType", () => {
|
|
6
|
+
it("returns undefined for empty buffer", () => {
|
|
7
|
+
const result = detectZipMimeType(Buffer.from([]));
|
|
8
|
+
expect(result).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns undefined for buffer with less than 4 bytes", () => {
|
|
12
|
+
const result = detectZipMimeType(Buffer.from([0x50, 0x4B]));
|
|
13
|
+
expect(result).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns undefined for non-ZIP file", () => {
|
|
17
|
+
const nonZipHeader = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]);
|
|
18
|
+
const result = detectZipMimeType(nonZipHeader);
|
|
19
|
+
expect(result).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("detects DOCX from ZIP signature with word/ content", () => {
|
|
23
|
+
const docxBuffer = Buffer.concat([
|
|
24
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]), // ZIP signature
|
|
25
|
+
Buffer.alloc(26, 0x00), // Padding to offset 30
|
|
26
|
+
Buffer.from("word/document.xml"), // Office indicator
|
|
27
|
+
]);
|
|
28
|
+
const result = detectZipMimeType(docxBuffer);
|
|
29
|
+
expect(result).toBe(
|
|
30
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("detects PPTX from ZIP signature with ppt/ content", () => {
|
|
35
|
+
const pptxBuffer = Buffer.concat([
|
|
36
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]), // ZIP signature
|
|
37
|
+
Buffer.alloc(26, 0x00), // Padding to offset 30
|
|
38
|
+
Buffer.from("ppt/presentation.xml"), // Office indicator
|
|
39
|
+
]);
|
|
40
|
+
const result = detectZipMimeType(pptxBuffer);
|
|
41
|
+
expect(result).toBe(
|
|
42
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns undefined for generic ZIP file without Office content", () => {
|
|
47
|
+
const genericZip = Buffer.concat([
|
|
48
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
49
|
+
Buffer.alloc(100, 0x00),
|
|
50
|
+
]);
|
|
51
|
+
const result = detectZipMimeType(genericZip);
|
|
52
|
+
expect(result).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns undefined for ZIP file with non-Office content", () => {
|
|
56
|
+
const zipWithContent = Buffer.concat([
|
|
57
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
58
|
+
Buffer.alloc(26, 0x00),
|
|
59
|
+
Buffer.from("data/file.txt"),
|
|
60
|
+
]);
|
|
61
|
+
const result = detectZipMimeType(zipWithContent);
|
|
62
|
+
expect(result).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles case-insensitive detection for WORD/", () => {
|
|
66
|
+
const docxBuffer = Buffer.concat([
|
|
67
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
68
|
+
Buffer.alloc(26, 0x00),
|
|
69
|
+
Buffer.from("WORD/document.xml"),
|
|
70
|
+
]);
|
|
71
|
+
const result = detectZipMimeType(docxBuffer);
|
|
72
|
+
expect(result).toBe(
|
|
73
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles case-insensitive detection for PPT/", () => {
|
|
78
|
+
const pptxBuffer = Buffer.concat([
|
|
79
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
80
|
+
Buffer.alloc(26, 0x00),
|
|
81
|
+
Buffer.from("PPT/presentation.xml"),
|
|
82
|
+
]);
|
|
83
|
+
const result = detectZipMimeType(pptxBuffer);
|
|
84
|
+
expect(result).toBe(
|
|
85
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect ZIP-based Office formats from magic bytes
|
|
3
|
+
* Detects DOCX, PPTX by checking ZIP signature followed by Office content
|
|
4
|
+
*/
|
|
5
|
+
export function detectZipMimeType(buffer: Buffer): string | undefined {
|
|
6
|
+
if (buffer.length < 4) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ZIP signature: 50 4B 03 04
|
|
11
|
+
if (!(buffer[0] === 0x50 && buffer[1] === 0x4B && buffer[2] === 0x03 && buffer[3] === 0x04)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// If it's a ZIP, check for Office format indicators
|
|
16
|
+
// Look for word/ or ppt/ in the buffer to distinguish Office formats
|
|
17
|
+
const bufferString = buffer.toString("utf8", 30, Math.min(buffer.length, 8192)).toLowerCase();
|
|
18
|
+
|
|
19
|
+
if (bufferString.includes("word/")) {
|
|
20
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bufferString.includes("ppt/")) {
|
|
24
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { validateNoSuspiciousPatterns } from "./parameter-validation";
|
|
4
|
+
|
|
5
|
+
describe("validateNoSuspiciousPatterns", () => {
|
|
6
|
+
it("should allow clean input", () => {
|
|
7
|
+
expect(() => validateNoSuspiciousPatterns("Berlin, Germany", "location")).not.toThrow();
|
|
8
|
+
expect(() => validateNoSuspiciousPatterns("my-community-slug", "slug")).not.toThrow();
|
|
9
|
+
expect(() => validateNoSuspiciousPatterns("12345", "id")).not.toThrow();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should reject input with script tags", () => {
|
|
13
|
+
expect(() =>
|
|
14
|
+
validateNoSuspiciousPatterns("<script>alert(1)</script>", "location")
|
|
15
|
+
).toThrow("Parameter 'location' contains suspicious patterns: html.script_tag");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should reject input with SQL injection patterns", () => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
validateNoSuspiciousPatterns("Berlin; DROP TABLE users", "location")
|
|
21
|
+
).toThrow("Parameter 'location' contains suspicious patterns: sql.drop_table");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should reject input with embedded URLs", () => {
|
|
25
|
+
expect(() =>
|
|
26
|
+
validateNoSuspiciousPatterns("Visit https://evil.com", "slug")
|
|
27
|
+
).toThrow("Parameter 'slug' contains suspicious patterns: url.embedded");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should reject input with prompt injection", () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
validateNoSuspiciousPatterns("Ignore previous instructions", "id")
|
|
33
|
+
).toThrow("Parameter 'id' contains suspicious patterns: prompt.injection.language");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getLogger } from "@schafevormfenster/logging";
|
|
2
|
+
|
|
3
|
+
import { detectSuspiciousPatternsFromBody } from "./detect-suspicious-patterns";
|
|
4
|
+
|
|
5
|
+
const log = getLogger("rest.helpers.parameter-validation");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates that a string parameter does not contain suspicious patterns
|
|
9
|
+
* that could indicate injection attacks or other security threats.
|
|
10
|
+
*/
|
|
11
|
+
export function validateNoSuspiciousPatterns(
|
|
12
|
+
value: string,
|
|
13
|
+
parameterName: string
|
|
14
|
+
): string {
|
|
15
|
+
const suspiciousPatterns = detectSuspiciousPatternsFromBody(value);
|
|
16
|
+
|
|
17
|
+
if (suspiciousPatterns.length > 0) {
|
|
18
|
+
log.error(
|
|
19
|
+
{
|
|
20
|
+
parameterName,
|
|
21
|
+
suspiciousPatterns,
|
|
22
|
+
error: `Parameter '${parameterName}' contains suspicious patterns`,
|
|
23
|
+
},
|
|
24
|
+
`Parameter '${parameterName}' contains suspicious patterns: ${suspiciousPatterns.join(", ")}`
|
|
25
|
+
);
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Parameter '${parameterName}' contains suspicious patterns: ${suspiciousPatterns.join(", ")}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getLogger } from "@schafevormfenster/logging";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { detectMimeType } from "./detect-mime-type";
|
|
5
|
+
import { DEFAULT_MAX_FILE_SIZE } from "./eventify-constants.types";
|
|
6
|
+
|
|
7
|
+
import { EventifyRequest } from "@/app/api/[token]/eventify/eventify-request.schema";
|
|
8
|
+
import { getFileFromUrl } from "@/services/file-fetching/get-file-from-url";
|
|
9
|
+
|
|
10
|
+
const log = getLogger("api.eventify.process-request");
|
|
11
|
+
|
|
12
|
+
export interface ProcessedEventifyRequest {
|
|
13
|
+
buffer: Buffer;
|
|
14
|
+
detectedMimeType: string | null | undefined;
|
|
15
|
+
context?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract buffer from binary data
|
|
20
|
+
*/
|
|
21
|
+
function extractFromBinary(binary: string): Buffer {
|
|
22
|
+
try {
|
|
23
|
+
return Buffer.from(binary, "base64");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
log.error(
|
|
26
|
+
{ error },
|
|
27
|
+
`Invalid base64 encoding: ${error instanceof Error ? error.message : "unknown error"}`
|
|
28
|
+
);
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid base64 encoding: ${error instanceof Error ? error.message : "unknown error"}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract buffer from remote URL
|
|
37
|
+
*/
|
|
38
|
+
async function extractFromURL(fileURL: string): Promise<Buffer> {
|
|
39
|
+
log.debug({ fileURL }, "Fetching remote file");
|
|
40
|
+
const result = await getFileFromUrl(fileURL, {
|
|
41
|
+
maxSize: DEFAULT_MAX_FILE_SIZE,
|
|
42
|
+
});
|
|
43
|
+
if (!result.binary) {
|
|
44
|
+
log.error(
|
|
45
|
+
{ fileURL, error: "Failed to retrieve file content" },
|
|
46
|
+
"Failed to retrieve file content"
|
|
47
|
+
);
|
|
48
|
+
throw new Error("Failed to retrieve file content");
|
|
49
|
+
}
|
|
50
|
+
return result.binary;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate MIME type
|
|
55
|
+
*/
|
|
56
|
+
function validateMimeType(
|
|
57
|
+
detectedMimeType: string | null | undefined,
|
|
58
|
+
allowedMimeTypes: readonly string[]
|
|
59
|
+
): void {
|
|
60
|
+
if (!detectedMimeType) {
|
|
61
|
+
log.error("Unable to detect file type. Unsupported file format.");
|
|
62
|
+
throw new Error("Unable to detect file type. Unsupported file format.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!allowedMimeTypes.includes(detectedMimeType)) {
|
|
66
|
+
log.error(
|
|
67
|
+
{
|
|
68
|
+
detectedMimeType,
|
|
69
|
+
allowedMimeTypes,
|
|
70
|
+
error: `Unsupported file type: ${detectedMimeType}`,
|
|
71
|
+
},
|
|
72
|
+
`Unsupported file type: ${detectedMimeType}. Allowed types: ${allowedMimeTypes.join(", ")}`
|
|
73
|
+
);
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Unsupported file type: ${detectedMimeType}. Allowed types: ${allowedMimeTypes.join(", ")}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Process an eventify request by extracting the file content,
|
|
82
|
+
* validating size, and detecting MIME type
|
|
83
|
+
*
|
|
84
|
+
* @param request - The eventify request
|
|
85
|
+
* @param allowedMimeTypes - Array of allowed MIME types
|
|
86
|
+
* @returns Processed request with buffer and detected MIME type
|
|
87
|
+
* @throws Error if validation fails
|
|
88
|
+
*/
|
|
89
|
+
export async function processEventifyRequest(
|
|
90
|
+
request: EventifyRequest,
|
|
91
|
+
allowedMimeTypes: readonly string[]
|
|
92
|
+
): Promise<ProcessedEventifyRequest> {
|
|
93
|
+
let buffer: Buffer;
|
|
94
|
+
|
|
95
|
+
// Extract file content
|
|
96
|
+
if (request.binary) {
|
|
97
|
+
log.debug({}, "Processing binary upload");
|
|
98
|
+
buffer = extractFromBinary(request.binary);
|
|
99
|
+
} else if (request.fileURL) {
|
|
100
|
+
buffer = await extractFromURL(request.fileURL);
|
|
101
|
+
} else {
|
|
102
|
+
// This should never happen due to schema validation
|
|
103
|
+
log.error(
|
|
104
|
+
{ error: "Neither binary, fileURL, nor stream provided" },
|
|
105
|
+
"Neither binary, fileURL, nor stream provided"
|
|
106
|
+
);
|
|
107
|
+
throw new Error("Neither binary, fileURL, nor stream provided");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate file size
|
|
111
|
+
if (buffer.length === 0) {
|
|
112
|
+
log.error({ error: "File is empty" }, "File is empty");
|
|
113
|
+
throw new Error("File is empty");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (buffer.length > DEFAULT_MAX_FILE_SIZE) {
|
|
117
|
+
log.error(
|
|
118
|
+
{
|
|
119
|
+
maxSize: DEFAULT_MAX_FILE_SIZE,
|
|
120
|
+
currentSize: buffer.length,
|
|
121
|
+
error: "Size limit exceeded",
|
|
122
|
+
},
|
|
123
|
+
"Buffer size exceeds maximum allowed size"
|
|
124
|
+
);
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Buffer size exceeds maximum allowed size of ${DEFAULT_MAX_FILE_SIZE} bytes`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Detect MIME type using magic bytes
|
|
131
|
+
const detectedMimeType = detectMimeType(buffer);
|
|
132
|
+
|
|
133
|
+
// Validate MIME type
|
|
134
|
+
validateMimeType(detectedMimeType, allowedMimeTypes);
|
|
135
|
+
|
|
136
|
+
log.debug(
|
|
137
|
+
{ detectedMimeType, fileSize: buffer.length },
|
|
138
|
+
"File validated successfully"
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
buffer,
|
|
143
|
+
detectedMimeType,
|
|
144
|
+
context: request.context,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getApiSecurityHeaders } from "@schafevormfenster/security";
|
|
2
|
+
|
|
3
|
+
import { resolveEnvironment } from "./resolve-environment";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a consistent set of headers for unauthorized API responses, including
|
|
7
|
+
* security headers and correlation id propagation.
|
|
8
|
+
*/
|
|
9
|
+
export function buildApiUnauthorizedHeaders(
|
|
10
|
+
correlationId: string
|
|
11
|
+
): Record<string, string> {
|
|
12
|
+
const environment = resolveEnvironment();
|
|
13
|
+
const securityHeaders = getApiSecurityHeaders({
|
|
14
|
+
environment,
|
|
15
|
+
enforceHTTPS: environment !== "development",
|
|
16
|
+
allowEmbedding: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result: Record<string, string> = {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
"Cache-Control": "no-store",
|
|
22
|
+
"x-correlation-id": correlationId,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const h of securityHeaders) {
|
|
26
|
+
result[h.key] = h.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Environment = "development" | "staging" | "production";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Environment } from "./environment.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve environment consistently across server/middleware contexts.
|
|
5
|
+
* - VERCEL_ENV: production | preview | development
|
|
6
|
+
* - NODE_ENV: production | development
|
|
7
|
+
*/
|
|
8
|
+
export function resolveEnvironment(): Environment {
|
|
9
|
+
const vercel = process.env.VERCEL_ENV;
|
|
10
|
+
if (vercel === "production") return "production";
|
|
11
|
+
if (vercel === "preview") return "staging";
|
|
12
|
+
|
|
13
|
+
const node = process.env.NODE_ENV;
|
|
14
|
+
if (node === "production") return "production";
|
|
15
|
+
if (node === "development") return "development";
|
|
16
|
+
return "development";
|
|
17
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { slugify } from "./slugify";
|
|
4
|
+
|
|
5
|
+
describe("slugify", () => {
|
|
6
|
+
it("should convert strings to lowercase", () => {
|
|
7
|
+
expect(slugify("Hello World")).toBe("hello-world");
|
|
8
|
+
expect(slugify("UPPERCASE")).toBe("uppercase");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should replace spaces with hyphens", () => {
|
|
12
|
+
expect(slugify("Hello World")).toBe("hello-world");
|
|
13
|
+
expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should handle underscores (removes them in strict mode)", () => {
|
|
17
|
+
expect(slugify("hello_world")).toBe("helloworld");
|
|
18
|
+
expect(slugify("hello___world")).toBe("helloworld");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should handle special characters", () => {
|
|
22
|
+
expect(slugify("Hello, World!")).toBe("hello-world");
|
|
23
|
+
expect(slugify("Berlin, Germany")).toBe("berlin-germany");
|
|
24
|
+
// Note: slugify converts some special chars to words (e.g., $ -> dollar)
|
|
25
|
+
expect(slugify("Test String")).toBe("test-string");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should remove leading and trailing hyphens", () => {
|
|
29
|
+
expect(slugify("-hello-")).toBe("hello");
|
|
30
|
+
expect(slugify("---hello---")).toBe("hello");
|
|
31
|
+
expect(slugify(" hello ")).toBe("hello");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should handle multiple consecutive hyphens", () => {
|
|
35
|
+
expect(slugify("hello---world")).toBe("hello-world");
|
|
36
|
+
expect(slugify("hello - world")).toBe("hello-world");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle empty strings", () => {
|
|
40
|
+
expect(slugify("")).toBe("");
|
|
41
|
+
expect(slugify(" ")).toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should preserve alphanumeric characters", () => {
|
|
45
|
+
expect(slugify("abc123")).toBe("abc123");
|
|
46
|
+
expect(slugify("Test123")).toBe("test123");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle complex strings", () => {
|
|
50
|
+
expect(slugify("Hello, World! This is a Test.")).toBe("hello-world-this-is-a-test");
|
|
51
|
+
expect(slugify("Café in München")).toBe("cafe-in-muenchen");
|
|
52
|
+
expect(slugify("New York, NY (USA)")).toBe("new-york-ny-usa");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should trim whitespace", () => {
|
|
56
|
+
expect(slugify(" hello world ")).toBe("hello-world");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle strings with only special characters that get removed", () => {
|
|
60
|
+
expect(slugify("!!!")).toBe("");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should convert German umlauts correctly", () => {
|
|
64
|
+
expect(slugify("Äpfel")).toBe("aepfel");
|
|
65
|
+
expect(slugify("Öl")).toBe("oel");
|
|
66
|
+
expect(slugify("Über")).toBe("ueber");
|
|
67
|
+
expect(slugify("Größe")).toBe("groesse");
|
|
68
|
+
expect(slugify("Käse")).toBe("kaese");
|
|
69
|
+
expect(slugify("Schön")).toBe("schoen");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle mixed German umlauts in phrases", () => {
|
|
73
|
+
expect(slugify("Äpfel und Öl")).toBe("aepfel-und-oel");
|
|
74
|
+
expect(slugify("Größe über Äußeres")).toBe("groesse-ueber-aeusseres");
|
|
75
|
+
expect(slugify("München Köln")).toBe("muenchen-koeln");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import slugifyPackage from "slugify";
|
|
2
|
+
|
|
3
|
+
// Extend slugify to handle German umlauts correctly
|
|
4
|
+
slugifyPackage.extend({
|
|
5
|
+
'ä': 'ae',
|
|
6
|
+
'ö': 'oe',
|
|
7
|
+
'ü': 'ue',
|
|
8
|
+
'ß': 'ss',
|
|
9
|
+
'Ä': 'Ae',
|
|
10
|
+
'Ö': 'Oe',
|
|
11
|
+
'Ü': 'Ue',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a string to a URL-safe slug.
|
|
16
|
+
* Uses the slugify npm package with support for German umlauts and other special characters.
|
|
17
|
+
*
|
|
18
|
+
* @param text - Input string to slugify
|
|
19
|
+
* @returns URL-safe slug (lowercase, hyphens, no special chars)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* slugify("Hello World!") // "hello-world"
|
|
23
|
+
* slugify("Berlin, Germany") // "berlin-germany"
|
|
24
|
+
* slugify("Café in München") // "cafe-in-munchen"
|
|
25
|
+
* slugify("Äpfel und Öl") // "aepfel-und-oel"
|
|
26
|
+
*/
|
|
27
|
+
export function slugify(text: string): string {
|
|
28
|
+
return slugifyPackage(text, {
|
|
29
|
+
lower: true, // Convert to lowercase
|
|
30
|
+
strict: true, // Strip special characters except replacement
|
|
31
|
+
remove: /[*+~.()'"!:@]/g, // Remove these characters
|
|
32
|
+
trim: true, // Trim leading/trailing replacement chars
|
|
33
|
+
});
|
|
34
|
+
}
|