@warlock.js/core 5.16.0 → 5.17.0
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/CHANGELOG.md +22 -0
- package/esm/cli/commands/migrate.command.mjs +5 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs +2 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +16 -23
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +7 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +1 -18
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/read-bound-port.mjs +32 -0
- package/esm/connectors/read-bound-port.mjs.map +1 -0
- package/esm/dev-server/development-server.mjs +5 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/extract-translation-keys.mjs +68 -0
- package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
- package/esm/dev-server/translation-keys-sources.mjs +19 -0
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +64 -11
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/stubs.mjs +7 -6
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/csrf-default-guard.d.mts +32 -0
- package/esm/http/csrf-default-guard.d.mts.map +1 -0
- package/esm/http/csrf-default-guard.mjs +156 -0
- package/esm/http/csrf-default-guard.mjs.map +1 -0
- package/esm/http/csrf-origin-policy.d.mts +53 -0
- package/esm/http/csrf-origin-policy.d.mts.map +1 -0
- package/esm/http/csrf-origin-policy.mjs +104 -0
- package/esm/http/csrf-origin-policy.mjs.map +1 -0
- package/esm/http/error-codes.d.mts +14 -1
- package/esm/http/error-codes.d.mts.map +1 -1
- package/esm/http/error-codes.mjs +13 -0
- package/esm/http/error-codes.mjs.map +1 -1
- package/esm/http/index.d.mts +7 -1
- package/esm/http/index.mjs +7 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +6 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/request.d.mts +2 -2
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +7 -3
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +7 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/uploads/detect-image-format.mjs +59 -0
- package/esm/http/uploads/detect-image-format.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variant.mjs +46 -0
- package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variants.d.mts +49 -0
- package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
- package/esm/http/uploads/generate-image-variants.mjs +141 -0
- package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
- package/esm/http/uploads/image-variant-types.d.mts +93 -0
- package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
- package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.mjs +17 -0
- package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
- package/esm/http/uploads/index.d.mts +4 -0
- package/esm/http/uploads/index.mjs +5 -0
- package/esm/http/uploads/is-path-inside.mjs +26 -0
- package/esm/http/uploads/is-path-inside.mjs.map +1 -0
- package/esm/http/uploads/load-variant-source.mjs +42 -0
- package/esm/http/uploads/load-variant-source.mjs.map +1 -0
- package/esm/http/uploads/matches-if-none-match.mjs +13 -0
- package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
- package/esm/http/uploads/read-file-head.mjs +20 -0
- package/esm/http/uploads/read-file-head.mjs.map +1 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
- package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-upload-path.mjs +56 -0
- package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
- package/esm/http/uploads/single-flight.mjs +40 -0
- package/esm/http/uploads/single-flight.mjs.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
- package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
- package/esm/http/uploads/variant-cache-key.mjs +45 -0
- package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
- package/esm/http/uploads/variant-generations.mjs +16 -0
- package/esm/http/uploads/variant-generations.mjs.map +1 -0
- package/esm/http/uploads-types.d.mts +102 -1
- package/esm/http/uploads-types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -2
- package/esm/index.mjs +8 -2
- package/esm/router/types.d.mts +23 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/index.d.mts +1 -0
- package/esm/storage/index.mjs +1 -0
- package/esm/storage/scoped-storage.d.mts +1 -0
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +3 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +2 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +4 -0
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
- package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
- package/llms-full.txt +17 -1
- package/package.json +22 -21
- package/skills/use-localization/SKILL.md +16 -0
- package/skills/use-middleware/SKILL.md +1 -1
- package/esm/dev-server/translation-type-generator.mjs +0 -28
- package/esm/dev-server/translation-type-generator.mjs.map +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { HttpError } from "../errors/errors.mjs";
|
|
2
|
+
import { storage } from "../../storage/storage.mjs";
|
|
3
|
+
import "../../storage/index.mjs";
|
|
4
|
+
import { Image } from "../../image/image.mjs";
|
|
5
|
+
import "../../image/index.mjs";
|
|
6
|
+
import { uploadsConfig } from "../uploads-config.mjs";
|
|
7
|
+
import { generateImageVariant } from "./generate-image-variant.mjs";
|
|
8
|
+
import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
|
|
9
|
+
import { loadVariantSource } from "./load-variant-source.mjs";
|
|
10
|
+
import { resolveImageVariantsConfig } from "./resolve-image-variants-config.mjs";
|
|
11
|
+
import { resolveVariantCandidate } from "./resolve-variant-candidate.mjs";
|
|
12
|
+
import { variantCacheKey, variantCachePath } from "./variant-cache-key.mjs";
|
|
13
|
+
import { variantGenerations } from "./variant-generations.mjs";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
|
|
16
|
+
//#region ../core/src/http/uploads/generate-image-variants.ts
|
|
17
|
+
async function exists(target) {
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(target);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function encodeUploadsPath(relativePath) {
|
|
26
|
+
return relativePath.split("/").map(encodeURIComponent).join("/");
|
|
27
|
+
}
|
|
28
|
+
function variantUrl(src, name, format) {
|
|
29
|
+
return `${src}?${format ? `variant=${encodeURIComponent(name)}&format=${format}` : `variant=${encodeURIComponent(name)}`}`;
|
|
30
|
+
}
|
|
31
|
+
async function readDerivativeDimensions(target) {
|
|
32
|
+
const { width, height } = await new Image(await fs.readFile(target)).metadata();
|
|
33
|
+
return {
|
|
34
|
+
width: width ?? 0,
|
|
35
|
+
height: height ?? 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every distinct format a variant is rendered in: the source format plus
|
|
40
|
+
* whatever `uploads.images.formats` adds, with duplicates dropped so a format
|
|
41
|
+
* that is both the source format and an allowed format is generated once.
|
|
42
|
+
*/
|
|
43
|
+
function outputFormatsFor(sourceFormat, configuredFormats) {
|
|
44
|
+
return [...new Set([sourceFormat, ...configuredFormats])];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render every configured variant of one uploaded image, right after it is
|
|
48
|
+
* saved.
|
|
49
|
+
*
|
|
50
|
+
* Uses the exact same normalized config (`uploads.images`), cache key and
|
|
51
|
+
* derivative path as `uploadedFileController`, so a variant generated here is
|
|
52
|
+
* already on disk the first time `GET /uploads/<path>?variant=<name>` is
|
|
53
|
+
* requested — no second render. `variantGenerations` (the single-flight used
|
|
54
|
+
* by the route) is reused, so a concurrent request for a variant this call is
|
|
55
|
+
* still generating joins the same run instead of starting a second one.
|
|
56
|
+
*
|
|
57
|
+
* The returned descriptor is plain JSON, safe to store next to the record
|
|
58
|
+
* that owns the image, or to carry through loader data. On local storage the
|
|
59
|
+
* `url`/`urls` already point at `uploadedFileController`; an app serving
|
|
60
|
+
* uploads from object storage or a CDN should not proxy the remote files
|
|
61
|
+
* through this route at all — store the descriptor once generation runs
|
|
62
|
+
* against the same bucket the CDN serves from, or resolve its URLs through a
|
|
63
|
+
* custom `ImageLoader` on the client instead.
|
|
64
|
+
*
|
|
65
|
+
* @param relativePath - path of the saved upload, relative to the storage root
|
|
66
|
+
* @param options.variants - limit generation to these variant names
|
|
67
|
+
* @throws ImageVariantsConfigError when there is no local storage root, or
|
|
68
|
+
* `uploads.images` is not configured — both are server misconfiguration, not
|
|
69
|
+
* a bad request
|
|
70
|
+
* @throws HttpError(400) when a name in `options.variants` is not declared in
|
|
71
|
+
* `uploads.images.variants`
|
|
72
|
+
* @throws HttpError(404) when `relativePath` does not resolve to a file
|
|
73
|
+
* inside the storage root
|
|
74
|
+
* @throws HttpError(413) when the source is over `maxSourceBytes` or
|
|
75
|
+
* `maxSourcePixels`
|
|
76
|
+
* @throws HttpError(415) when the source is not jpeg, png, webp or avif
|
|
77
|
+
*/
|
|
78
|
+
async function generateImageVariants(relativePath, options = {}) {
|
|
79
|
+
const storageRoot = storage.root();
|
|
80
|
+
if (!storageRoot || storageRoot === ".") throw new ImageVariantsConfigError("generateImageVariants needs a local storage driver with a root.");
|
|
81
|
+
const images = resolveImageVariantsConfig(uploadsConfig("images"), storageRoot);
|
|
82
|
+
if (!images) throw new ImageVariantsConfigError("uploads.images is not configured.");
|
|
83
|
+
const names = options.variants ?? Object.keys(images.variants);
|
|
84
|
+
for (const name of names) if (!Object.prototype.hasOwnProperty.call(images.variants, name)) throw new HttpError(400, `Unknown variant "${name}".`);
|
|
85
|
+
const candidate = await resolveVariantCandidate(relativePath, storageRoot, images);
|
|
86
|
+
if (!candidate) throw new HttpError(404, "File Not Found");
|
|
87
|
+
const { source, stats, sourceFormat } = candidate;
|
|
88
|
+
const image = await loadVariantSource(source.absolutePath, {
|
|
89
|
+
maxSourceBytes: images.maxSourceBytes,
|
|
90
|
+
maxSourcePixels: images.maxSourcePixels,
|
|
91
|
+
expectedFormat: sourceFormat
|
|
92
|
+
});
|
|
93
|
+
const { width: sourceWidth, height: sourceHeight } = await image.metadata();
|
|
94
|
+
const src = `/uploads/${encodeUploadsPath(source.relativePath)}`;
|
|
95
|
+
const variants = {};
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
const variant = images.variants[name];
|
|
98
|
+
if (variant === void 0) throw new HttpError(400, `Unknown variant "${name}".`);
|
|
99
|
+
const formats = outputFormatsFor(sourceFormat, images.formats);
|
|
100
|
+
let dimensions;
|
|
101
|
+
for (const format of formats) {
|
|
102
|
+
const hash = variantCacheKey({
|
|
103
|
+
relativePath: source.relativePath,
|
|
104
|
+
sourceSize: stats.size,
|
|
105
|
+
sourceMtimeMs: stats.mtimeMs,
|
|
106
|
+
variant,
|
|
107
|
+
format
|
|
108
|
+
});
|
|
109
|
+
const target = variantCachePath(images.cacheDirectory, hash, format);
|
|
110
|
+
if (!await exists(target)) await variantGenerations.run(hash, async () => {
|
|
111
|
+
if (await exists(target)) return;
|
|
112
|
+
await generateImageVariant({
|
|
113
|
+
image: image.clone(),
|
|
114
|
+
variant,
|
|
115
|
+
format,
|
|
116
|
+
targetPath: target
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
if (dimensions === void 0) dimensions = await readDerivativeDimensions(target);
|
|
120
|
+
}
|
|
121
|
+
const urls = {};
|
|
122
|
+
for (const format of images.formats) urls[format] = variantUrl(src, name, format);
|
|
123
|
+
variants[name] = {
|
|
124
|
+
width: dimensions?.width ?? variant.width,
|
|
125
|
+
height: dimensions?.height ?? variant.height ?? Math.round(variant.width * ((sourceHeight ?? 1) / (sourceWidth ?? 1))),
|
|
126
|
+
url: variantUrl(src, name),
|
|
127
|
+
...Object.keys(urls).length > 0 ? { urls } : {}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
src,
|
|
132
|
+
width: sourceWidth ?? 0,
|
|
133
|
+
height: sourceHeight ?? 0,
|
|
134
|
+
variants,
|
|
135
|
+
...images.formats.length > 0 ? { formats: images.formats } : {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
export { generateImageVariants };
|
|
141
|
+
//# sourceMappingURL=generate-image-variants.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-image-variants.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/generate-image-variants.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport { Image } from \"../../image\";\nimport { storage } from \"../../storage\";\nimport { HttpError } from \"../errors\";\nimport { uploadsConfig } from \"../uploads-config\";\nimport type { ImageVariantOutputFormat } from \"../uploads-types\";\nimport { generateImageVariant } from \"./generate-image-variant\";\nimport { ImageVariantsConfigError } from \"./image-variants-config-error\";\nimport type {\n GeneratedImageDescriptor,\n GeneratedImageVariant,\n VariantOutputFormat,\n} from \"./image-variant-types\";\nimport { loadVariantSource } from \"./load-variant-source\";\nimport { resolveImageVariantsConfig } from \"./resolve-image-variants-config\";\nimport { resolveVariantCandidate } from \"./resolve-variant-candidate\";\nimport { variantCacheKey, variantCachePath } from \"./variant-cache-key\";\nimport { variantGenerations } from \"./variant-generations\";\n\n/**\n * Options for `generateImageVariants`\n */\nexport type GenerateImageVariantsOptions = {\n /**\n * Limit generation to these variant names, instead of every variant in\n * `uploads.images.variants`.\n */\n variants?: readonly string[];\n};\n\nasync function exists(target: string): Promise<boolean> {\n try {\n await fs.access(target);\n\n return true;\n } catch {\n return false;\n }\n}\n\nfunction encodeUploadsPath(relativePath: string): string {\n return relativePath.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n\nfunction variantUrl(src: string, name: string, format?: VariantOutputFormat): string {\n const query = format\n ? `variant=${encodeURIComponent(name)}&format=${format}`\n : `variant=${encodeURIComponent(name)}`;\n\n return `${src}?${query}`;\n}\n\nasync function readDerivativeDimensions(\n target: string,\n): Promise<{ width: number; height: number }> {\n const derivative = new Image(await fs.readFile(target));\n const { width, height } = await derivative.metadata();\n\n return { width: width ?? 0, height: height ?? 0 };\n}\n\n/**\n * Every distinct format a variant is rendered in: the source format plus\n * whatever `uploads.images.formats` adds, with duplicates dropped so a format\n * that is both the source format and an allowed format is generated once.\n */\nfunction outputFormatsFor(\n sourceFormat: VariantOutputFormat,\n configuredFormats: readonly ImageVariantOutputFormat[],\n): VariantOutputFormat[] {\n return [...new Set<VariantOutputFormat>([sourceFormat, ...configuredFormats])];\n}\n\n/**\n * Render every configured variant of one uploaded image, right after it is\n * saved.\n *\n * Uses the exact same normalized config (`uploads.images`), cache key and\n * derivative path as `uploadedFileController`, so a variant generated here is\n * already on disk the first time `GET /uploads/<path>?variant=<name>` is\n * requested — no second render. `variantGenerations` (the single-flight used\n * by the route) is reused, so a concurrent request for a variant this call is\n * still generating joins the same run instead of starting a second one.\n *\n * The returned descriptor is plain JSON, safe to store next to the record\n * that owns the image, or to carry through loader data. On local storage the\n * `url`/`urls` already point at `uploadedFileController`; an app serving\n * uploads from object storage or a CDN should not proxy the remote files\n * through this route at all — store the descriptor once generation runs\n * against the same bucket the CDN serves from, or resolve its URLs through a\n * custom `ImageLoader` on the client instead.\n *\n * @param relativePath - path of the saved upload, relative to the storage root\n * @param options.variants - limit generation to these variant names\n * @throws ImageVariantsConfigError when there is no local storage root, or\n * `uploads.images` is not configured — both are server misconfiguration, not\n * a bad request\n * @throws HttpError(400) when a name in `options.variants` is not declared in\n * `uploads.images.variants`\n * @throws HttpError(404) when `relativePath` does not resolve to a file\n * inside the storage root\n * @throws HttpError(413) when the source is over `maxSourceBytes` or\n * `maxSourcePixels`\n * @throws HttpError(415) when the source is not jpeg, png, webp or avif\n */\nexport async function generateImageVariants(\n relativePath: string,\n options: GenerateImageVariantsOptions = {},\n): Promise<GeneratedImageDescriptor> {\n const storageRoot = storage.root();\n\n if (!storageRoot || storageRoot === \".\") {\n throw new ImageVariantsConfigError(\n \"generateImageVariants needs a local storage driver with a root.\",\n );\n }\n\n const images = resolveImageVariantsConfig(uploadsConfig(\"images\"), storageRoot);\n\n if (!images) {\n throw new ImageVariantsConfigError(\"uploads.images is not configured.\");\n }\n\n const names = options.variants ?? Object.keys(images.variants);\n\n for (const name of names) {\n if (!Object.prototype.hasOwnProperty.call(images.variants, name)) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n }\n\n const candidate = await resolveVariantCandidate(relativePath, storageRoot, images);\n\n if (!candidate) {\n throw new HttpError(404, \"File Not Found\");\n }\n\n const { source, stats, sourceFormat } = candidate;\n\n const image = await loadVariantSource(source.absolutePath, {\n maxSourceBytes: images.maxSourceBytes,\n maxSourcePixels: images.maxSourcePixels,\n expectedFormat: sourceFormat,\n });\n\n const { width: sourceWidth, height: sourceHeight } = await image.metadata();\n const src = `/uploads/${encodeUploadsPath(source.relativePath)}`;\n const variants: Record<string, GeneratedImageVariant> = {};\n\n for (const name of names) {\n const variant = images.variants[name];\n if (variant === undefined) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n const formats = outputFormatsFor(sourceFormat, images.formats);\n let dimensions: { width: number; height: number } | undefined;\n\n for (const format of formats) {\n const hash = variantCacheKey({\n relativePath: source.relativePath,\n sourceSize: stats.size,\n sourceMtimeMs: stats.mtimeMs,\n variant,\n format,\n });\n const target = variantCachePath(images.cacheDirectory, hash, format);\n\n if (!(await exists(target))) {\n await variantGenerations.run(hash, async () => {\n if (await exists(target)) return;\n\n await generateImageVariant({ image: image.clone(), variant, format, targetPath: target });\n });\n }\n\n if (dimensions === undefined) {\n dimensions = await readDerivativeDimensions(target);\n }\n }\n\n const urls: Partial<Record<ImageVariantOutputFormat, string>> = {};\n\n for (const format of images.formats) {\n urls[format] = variantUrl(src, name, format);\n }\n\n variants[name] = {\n width: dimensions?.width ?? variant.width,\n height:\n dimensions?.height ??\n variant.height ??\n Math.round(variant.width * ((sourceHeight ?? 1) / (sourceWidth ?? 1))),\n url: variantUrl(src, name),\n ...(Object.keys(urls).length > 0 ? { urls } : {}),\n };\n }\n\n return {\n src,\n width: sourceWidth ?? 0,\n height: sourceHeight ?? 0,\n variants,\n ...(images.formats.length > 0 ? { formats: images.formats } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ImageVariantDefinition, ImageVariantOutputFormat } from "../uploads-types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/uploads/image-variant-types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The `uploads.images` config after validation, with every default applied
|
|
6
|
+
*/
|
|
7
|
+
type ResolvedImageVariantsConfig = {
|
|
8
|
+
variants: Readonly<Record<string, ImageVariantDefinition>>;
|
|
9
|
+
formats: readonly ImageVariantOutputFormat[];
|
|
10
|
+
maxSourceBytes: number;
|
|
11
|
+
maxSourcePixels: number;
|
|
12
|
+
cacheDirectory: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Raster formats a variant can be generated from, detected by magic bytes
|
|
16
|
+
*/
|
|
17
|
+
type VariantSourceFormat = "jpeg" | "png" | "webp" | "avif";
|
|
18
|
+
/**
|
|
19
|
+
* Every format `detectImageFormat` can recognise, including the refused ones
|
|
20
|
+
*/
|
|
21
|
+
type DetectedImageFormat = VariantSourceFormat | "gif" | "svg";
|
|
22
|
+
/**
|
|
23
|
+
* Format a derivative is written in
|
|
24
|
+
*/
|
|
25
|
+
type VariantOutputFormat = VariantSourceFormat;
|
|
26
|
+
/**
|
|
27
|
+
* What the query string of an uploads request asks for
|
|
28
|
+
*/
|
|
29
|
+
type UploadedFileQuery = {
|
|
30
|
+
type: "original";
|
|
31
|
+
} | {
|
|
32
|
+
type: "variant";
|
|
33
|
+
variant: string;
|
|
34
|
+
format?: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "invalid";
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One rendered size of an image produced by `generateImageVariants`.
|
|
41
|
+
*
|
|
42
|
+
* Field names match web's `ImageDescriptor`/`ImageVariantDescriptor`
|
|
43
|
+
* (`web/src/image/types.ts`) field for field, so the result can be stored or
|
|
44
|
+
* passed through loader data as is. Core does not import that type — web
|
|
45
|
+
* depends on core, not the other way around — this is a structural match.
|
|
46
|
+
*/
|
|
47
|
+
type GeneratedImageVariant = {
|
|
48
|
+
/**
|
|
49
|
+
* Actual rendered width, in pixels
|
|
50
|
+
*/
|
|
51
|
+
width: number;
|
|
52
|
+
/**
|
|
53
|
+
* Actual rendered height, in pixels
|
|
54
|
+
*/
|
|
55
|
+
height: number;
|
|
56
|
+
/**
|
|
57
|
+
* URL of the default-format rendition: `/uploads/<path>?variant=<name>`
|
|
58
|
+
*/
|
|
59
|
+
url: string;
|
|
60
|
+
/**
|
|
61
|
+
* URLs of the extra configured formats, keyed by format
|
|
62
|
+
*/
|
|
63
|
+
urls?: Partial<Record<ImageVariantOutputFormat, string>>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Serializable descriptor returned by `generateImageVariants`, matching web's
|
|
67
|
+
* `ImageDescriptor` field for field.
|
|
68
|
+
*/
|
|
69
|
+
type GeneratedImageDescriptor = {
|
|
70
|
+
/**
|
|
71
|
+
* The original source path: `/uploads/<path>`
|
|
72
|
+
*/
|
|
73
|
+
src: string;
|
|
74
|
+
/**
|
|
75
|
+
* Intrinsic width of the source, in pixels
|
|
76
|
+
*/
|
|
77
|
+
width: number;
|
|
78
|
+
/**
|
|
79
|
+
* Intrinsic height of the source, in pixels
|
|
80
|
+
*/
|
|
81
|
+
height: number;
|
|
82
|
+
/**
|
|
83
|
+
* Every generated variant, keyed by name
|
|
84
|
+
*/
|
|
85
|
+
variants: Readonly<Record<string, GeneratedImageVariant>>;
|
|
86
|
+
/**
|
|
87
|
+
* The extra formats configured in `uploads.images.formats`, when any
|
|
88
|
+
*/
|
|
89
|
+
formats?: readonly ImageVariantOutputFormat[];
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { DetectedImageFormat, GeneratedImageDescriptor, GeneratedImageVariant, ResolvedImageVariantsConfig, UploadedFileQuery, VariantOutputFormat, VariantSourceFormat };
|
|
93
|
+
//# sourceMappingURL=image-variant-types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-variant-types.d.mts","names":[],"sources":["../../../../../../../../core/src/http/uploads/image-variant-types.ts"],"mappings":";;;;;AAKA;KAAY,2BAAA;EACV,QAAA,EAAU,QAAA,CAAS,MAAA,SAAe,sBAAA;EAClC,OAAA,WAAkB,wBAAA;EAClB,cAAA;EACA,eAAA;EACA,cAAA;AAAA;;;;KAMU,mBAAA;;;;KAKA,mBAAA,GAAsB,mBAAmB;;;;KAKzC,mBAAA,GAAsB,mBAAmB;AAVrD;;;AAAA,KAeY,iBAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAiB,OAAA;EAAiB,MAAA;AAAA;EAClC,IAAA;EAAiB,MAAA;AAAA;;AAR8B;AAKrD;;;;;;KAaY,qBAAA;EAX4B;;;EAetC,KAAA;EAd2B;AAU7B;;EASE,MAAA;EAUsB;;;EALtB,GAAA;EAKc;;;EAAd,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,wBAAA;AAAA;;;;;KAOZ,wBAAA;EAAA;;;EAIV,GAAA;EAemB;;;EAVnB,KAAA;EAe2C;;;EAV3C,MAAA;EAKA;;;EAAA,QAAA,EAAU,QAAA,CAAS,MAAA,SAAe,qBAAA;EAKlC;;;EAAA,OAAA,YAAmB,wBAAA;AAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../core/src/http/uploads/image-variants-config-error.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the `uploads.images` configuration is invalid.
|
|
4
|
+
*
|
|
5
|
+
* Raised the first time a variant is requested, so a bad config fails loudly
|
|
6
|
+
* on the first image instead of producing unbounded or surprising derivatives.
|
|
7
|
+
*/
|
|
8
|
+
declare class ImageVariantsConfigError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { ImageVariantsConfigError };
|
|
13
|
+
//# sourceMappingURL=image-variants-config-error.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-variants-config-error.d.mts","names":[],"sources":["../../../../../../../../core/src/http/uploads/image-variants-config-error.ts"],"mappings":";;AAMA;;;;;cAAa,wBAAA,SAAiC,KAAK;cAC9B,OAAA;AAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../core/src/http/uploads/image-variants-config-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the `uploads.images` configuration is invalid.
|
|
4
|
+
*
|
|
5
|
+
* Raised the first time a variant is requested, so a bad config fails loudly
|
|
6
|
+
* on the first image instead of producing unbounded or surprising derivatives.
|
|
7
|
+
*/
|
|
8
|
+
var ImageVariantsConfigError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(`Invalid uploads.images configuration: ${message}`);
|
|
11
|
+
this.name = "ImageVariantsConfigError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ImageVariantsConfigError };
|
|
17
|
+
//# sourceMappingURL=image-variants-config-error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-variants-config-error.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/image-variants-config-error.ts"],"sourcesContent":["/**\n * Thrown when the `uploads.images` configuration is invalid.\n *\n * Raised the first time a variant is requested, so a bad config fails loudly\n * on the first image instead of producing unbounded or surprising derivatives.\n */\nexport class ImageVariantsConfigError extends Error {\n public constructor(message: string) {\n super(`Invalid uploads.images configuration: ${message}`);\n\n this.name = \"ImageVariantsConfigError\";\n }\n}\n"],"mappings":";;;;;;;AAMA,IAAa,2BAAb,cAA8C,MAAM;CAClD,AAAO,YAAY,SAAiB;EAClC,MAAM,yCAAyC,SAAS;EAExD,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DetectedImageFormat, GeneratedImageDescriptor, GeneratedImageVariant, ResolvedImageVariantsConfig, UploadedFileQuery, VariantOutputFormat, VariantSourceFormat } from "./image-variant-types.mjs";
|
|
2
|
+
import { GenerateImageVariantsOptions, generateImageVariants } from "./generate-image-variants.mjs";
|
|
3
|
+
import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
|
|
4
|
+
import { uploadedFileController } from "./uploaded-file.controller.mjs";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/uploads/is-path-inside.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether `candidate` is strictly inside `directory` (never equal to it).
|
|
6
|
+
*
|
|
7
|
+
* Both paths must already be absolute and resolved. The check compares against
|
|
8
|
+
* `directory + path.sep`, so a sibling sharing the directory's name as a prefix
|
|
9
|
+
* (`/srv/storage-evil` next to `/srv/storage`) is outside. On win32 the
|
|
10
|
+
* comparison is case-insensitive, matching the filesystem.
|
|
11
|
+
*/
|
|
12
|
+
function isPathInside(candidate, directory) {
|
|
13
|
+
const prefix = directory.endsWith(path.sep) ? directory : directory + path.sep;
|
|
14
|
+
if (process.platform === "win32") return candidate.toLowerCase().startsWith(prefix.toLowerCase());
|
|
15
|
+
return candidate.startsWith(prefix);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether `candidate` is `directory` itself or anything inside it
|
|
19
|
+
*/
|
|
20
|
+
function isPathWithin(candidate, directory) {
|
|
21
|
+
return (process.platform === "win32" ? candidate.toLowerCase() === directory.toLowerCase() : candidate === directory) || isPathInside(candidate, directory);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { isPathInside, isPathWithin };
|
|
26
|
+
//# sourceMappingURL=is-path-inside.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-path-inside.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/is-path-inside.ts"],"sourcesContent":["import path from \"node:path\";\n\n/**\n * Whether `candidate` is strictly inside `directory` (never equal to it).\n *\n * Both paths must already be absolute and resolved. The check compares against\n * `directory + path.sep`, so a sibling sharing the directory's name as a prefix\n * (`/srv/storage-evil` next to `/srv/storage`) is outside. On win32 the\n * comparison is case-insensitive, matching the filesystem.\n */\nexport function isPathInside(candidate: string, directory: string): boolean {\n const prefix = directory.endsWith(path.sep) ? directory : directory + path.sep;\n\n if (process.platform === \"win32\") {\n return candidate.toLowerCase().startsWith(prefix.toLowerCase());\n }\n\n return candidate.startsWith(prefix);\n}\n\n/**\n * Whether `candidate` is `directory` itself or anything inside it\n */\nexport function isPathWithin(candidate: string, directory: string): boolean {\n const same =\n process.platform === \"win32\"\n ? candidate.toLowerCase() === directory.toLowerCase()\n : candidate === directory;\n\n return same || isPathInside(candidate, directory);\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,aAAa,WAAmB,WAA4B;CAC1E,MAAM,SAAS,UAAU,SAAS,KAAK,GAAG,IAAI,YAAY,YAAY,KAAK;CAE3E,IAAI,QAAQ,aAAa,SACvB,OAAO,UAAU,YAAY,CAAC,CAAC,WAAW,OAAO,YAAY,CAAC;CAGhE,OAAO,UAAU,WAAW,MAAM;AACpC;;;;AAKA,SAAgB,aAAa,WAAmB,WAA4B;CAM1E,QAJE,QAAQ,aAAa,UACjB,UAAU,YAAY,MAAM,UAAU,YAAY,IAClD,cAAc,cAEL,aAAa,WAAW,SAAS;AAClD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HttpError } from "../errors/errors.mjs";
|
|
2
|
+
import { Image } from "../../image/image.mjs";
|
|
3
|
+
import "../../image/index.mjs";
|
|
4
|
+
import { detectImageFormat } from "./detect-image-format.mjs";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
//#region ../core/src/http/uploads/load-variant-source.ts
|
|
8
|
+
/**
|
|
9
|
+
* Read a variant source in full and check it before it is decoded.
|
|
10
|
+
*
|
|
11
|
+
* The caller has already sniffed the format from the file head; this re-checks
|
|
12
|
+
* the bytes actually read, the same buffer that is then resized, so the file
|
|
13
|
+
* cannot be swapped between the check and the render.
|
|
14
|
+
*
|
|
15
|
+
* - bytes over `maxSourceBytes` → 413
|
|
16
|
+
* - magic bytes that no longer match `expectedFormat` → 415
|
|
17
|
+
* - a header sharp cannot read (e.g. avif without a decoder) → 415
|
|
18
|
+
* - width × height over `maxSourcePixels` → 413, read from the header only
|
|
19
|
+
*
|
|
20
|
+
* @throws HttpError with status 413 or 415. Any other error (sharp missing,
|
|
21
|
+
* I/O) propagates unchanged.
|
|
22
|
+
*/
|
|
23
|
+
async function loadVariantSource(absolutePath, { maxSourceBytes, maxSourcePixels, expectedFormat }) {
|
|
24
|
+
const buffer = await fs.readFile(absolutePath);
|
|
25
|
+
if (buffer.length > maxSourceBytes) throw new HttpError(413, "The source image is too large.");
|
|
26
|
+
if (detectImageFormat(buffer) !== expectedFormat) throw new HttpError(415, "The source image changed format while it was read.");
|
|
27
|
+
const image = new Image(buffer);
|
|
28
|
+
let width;
|
|
29
|
+
let height;
|
|
30
|
+
try {
|
|
31
|
+
({width, height} = await image.metadata());
|
|
32
|
+
} catch {
|
|
33
|
+
throw new HttpError(415, "The source image cannot be decoded.");
|
|
34
|
+
}
|
|
35
|
+
if (!width || !height) throw new HttpError(415, "The source image cannot be decoded.");
|
|
36
|
+
if (width * height > maxSourcePixels) throw new HttpError(413, "The source image has too many pixels.");
|
|
37
|
+
return image;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { loadVariantSource };
|
|
42
|
+
//# sourceMappingURL=load-variant-source.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-variant-source.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/load-variant-source.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport { Image } from \"../../image\";\nimport { HttpError } from \"../errors\";\nimport { detectImageFormat } from \"./detect-image-format\";\nimport type { VariantSourceFormat } from \"./image-variant-types\";\n\n/**\n * Bounds and the expected format a source must match\n */\nexport type LoadVariantSourceOptions = {\n maxSourceBytes: number;\n maxSourcePixels: number;\n expectedFormat: VariantSourceFormat;\n};\n\n/**\n * Read a variant source in full and check it before it is decoded.\n *\n * The caller has already sniffed the format from the file head; this re-checks\n * the bytes actually read, the same buffer that is then resized, so the file\n * cannot be swapped between the check and the render.\n *\n * - bytes over `maxSourceBytes` → 413\n * - magic bytes that no longer match `expectedFormat` → 415\n * - a header sharp cannot read (e.g. avif without a decoder) → 415\n * - width × height over `maxSourcePixels` → 413, read from the header only\n *\n * @throws HttpError with status 413 or 415. Any other error (sharp missing,\n * I/O) propagates unchanged.\n */\nexport async function loadVariantSource(\n absolutePath: string,\n { maxSourceBytes, maxSourcePixels, expectedFormat }: LoadVariantSourceOptions,\n): Promise<Image> {\n const buffer = await fs.readFile(absolutePath);\n\n if (buffer.length > maxSourceBytes) {\n throw new HttpError(413, \"The source image is too large.\");\n }\n\n if (detectImageFormat(buffer) !== expectedFormat) {\n throw new HttpError(415, \"The source image changed format while it was read.\");\n }\n\n // Constructed outside the try below: sharp being absent is a server fault\n // (500 with the install hint), not an unsupported image.\n const image = new Image(buffer);\n\n let width: number | undefined;\n let height: number | undefined;\n\n try {\n ({ width, height } = await image.metadata());\n } catch {\n throw new HttpError(415, \"The source image cannot be decoded.\");\n }\n\n if (!width || !height) {\n throw new HttpError(415, \"The source image cannot be decoded.\");\n }\n\n if (width * height > maxSourcePixels) {\n throw new HttpError(413, \"The source image has too many pixels.\");\n }\n\n return image;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA8BA,eAAsB,kBACpB,cACA,EAAE,gBAAgB,iBAAiB,kBACnB;CAChB,MAAM,SAAS,MAAM,GAAG,SAAS,YAAY;CAE7C,IAAI,OAAO,SAAS,gBAClB,MAAM,IAAI,UAAU,KAAK,gCAAgC;CAG3D,IAAI,kBAAkB,MAAM,MAAM,gBAChC,MAAM,IAAI,UAAU,KAAK,oDAAoD;CAK/E,MAAM,QAAQ,IAAI,MAAM,MAAM;CAE9B,IAAI;CACJ,IAAI;CAEJ,IAAI;EACF,CAAC,CAAE,OAAO,UAAW,MAAM,MAAM,SAAS;CAC5C,QAAQ;EACN,MAAM,IAAI,UAAU,KAAK,qCAAqC;CAChE;CAEA,IAAI,CAAC,SAAS,CAAC,QACb,MAAM,IAAI,UAAU,KAAK,qCAAqC;CAGhE,IAAI,QAAQ,SAAS,iBACnB,MAAM,IAAI,UAAU,KAAK,uCAAuC;CAGlE,OAAO;AACT"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../core/src/http/uploads/matches-if-none-match.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether an `If-None-Match` header matches an entity tag (weak comparison,
|
|
4
|
+
* RFC 9110 §13.1.2): `*`, or any listed tag equal to `etag` once `W/` is dropped.
|
|
5
|
+
*/
|
|
6
|
+
function matchesIfNoneMatch(header, etag) {
|
|
7
|
+
if (typeof header !== "string" || header === "") return false;
|
|
8
|
+
return header.split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === "*" || tag === etag);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { matchesIfNoneMatch };
|
|
13
|
+
//# sourceMappingURL=matches-if-none-match.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matches-if-none-match.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/matches-if-none-match.ts"],"sourcesContent":["/**\n * Whether an `If-None-Match` header matches an entity tag (weak comparison,\n * RFC 9110 §13.1.2): `*`, or any listed tag equal to `etag` once `W/` is dropped.\n */\nexport function matchesIfNoneMatch(header: unknown, etag: string): boolean {\n if (typeof header !== \"string\" || header === \"\") return false;\n\n return header\n .split(\",\")\n .map((tag) => tag.trim().replace(/^W\\//, \"\"))\n .some((tag) => tag === \"*\" || tag === etag);\n}\n"],"mappings":";;;;;AAIA,SAAgB,mBAAmB,QAAiB,MAAuB;CACzE,IAAI,OAAO,WAAW,YAAY,WAAW,IAAI,OAAO;CAExD,OAAO,OACJ,MAAM,GAAG,CAAC,CACV,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,CAC5C,MAAM,QAAQ,QAAQ,OAAO,QAAQ,IAAI;AAC9C"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region ../core/src/http/uploads/parse-uploaded-file-query.ts
|
|
2
|
+
const ALLOWED_KEYS = new Set(["variant", "format"]);
|
|
3
|
+
/**
|
|
4
|
+
* Read the query string of an uploads request, strictly.
|
|
5
|
+
*
|
|
6
|
+
* It parses the raw URL rather than the framework's parsed query, because the
|
|
7
|
+
* parsed form folds a repeated key into an array and a lone key into a string,
|
|
8
|
+
* and a strict allowlist has to see both. Only `variant` and `format` are
|
|
9
|
+
* accepted, each at most once, and `format` only next to `variant`.
|
|
10
|
+
*
|
|
11
|
+
* @param url - the raw request URL, path and query
|
|
12
|
+
*/
|
|
13
|
+
function parseUploadedFileQuery(url) {
|
|
14
|
+
const queryStart = url.indexOf("?");
|
|
15
|
+
const rawQuery = queryStart === -1 ? "" : url.slice(queryStart + 1);
|
|
16
|
+
if (rawQuery === "") return { type: "original" };
|
|
17
|
+
const values = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const [key, value] of new URLSearchParams(rawQuery)) {
|
|
19
|
+
if (!ALLOWED_KEYS.has(key)) return {
|
|
20
|
+
type: "invalid",
|
|
21
|
+
reason: `Unknown query parameter "${key}".`
|
|
22
|
+
};
|
|
23
|
+
if (values.has(key)) return {
|
|
24
|
+
type: "invalid",
|
|
25
|
+
reason: `Query parameter "${key}" is repeated.`
|
|
26
|
+
};
|
|
27
|
+
values.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
const variant = values.get("variant");
|
|
30
|
+
const format = values.get("format");
|
|
31
|
+
if (variant === void 0) return values.size === 0 ? { type: "original" } : {
|
|
32
|
+
type: "invalid",
|
|
33
|
+
reason: `"format" requires "variant".`
|
|
34
|
+
};
|
|
35
|
+
if (variant === "") return {
|
|
36
|
+
type: "invalid",
|
|
37
|
+
reason: `"variant" must not be empty.`
|
|
38
|
+
};
|
|
39
|
+
return format === void 0 ? {
|
|
40
|
+
type: "variant",
|
|
41
|
+
variant
|
|
42
|
+
} : {
|
|
43
|
+
type: "variant",
|
|
44
|
+
variant,
|
|
45
|
+
format
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { parseUploadedFileQuery };
|
|
51
|
+
//# sourceMappingURL=parse-uploaded-file-query.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-uploaded-file-query.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/parse-uploaded-file-query.ts"],"sourcesContent":["import type { UploadedFileQuery } from \"./image-variant-types\";\n\nconst ALLOWED_KEYS = new Set([\"variant\", \"format\"]);\n\n/**\n * Read the query string of an uploads request, strictly.\n *\n * It parses the raw URL rather than the framework's parsed query, because the\n * parsed form folds a repeated key into an array and a lone key into a string,\n * and a strict allowlist has to see both. Only `variant` and `format` are\n * accepted, each at most once, and `format` only next to `variant`.\n *\n * @param url - the raw request URL, path and query\n */\nexport function parseUploadedFileQuery(url: string): UploadedFileQuery {\n const queryStart = url.indexOf(\"?\");\n const rawQuery = queryStart === -1 ? \"\" : url.slice(queryStart + 1);\n\n if (rawQuery === \"\") return { type: \"original\" };\n\n const values = new Map<string, string>();\n\n for (const [key, value] of new URLSearchParams(rawQuery)) {\n if (!ALLOWED_KEYS.has(key)) {\n return { type: \"invalid\", reason: `Unknown query parameter \"${key}\".` };\n }\n\n if (values.has(key)) {\n return { type: \"invalid\", reason: `Query parameter \"${key}\" is repeated.` };\n }\n\n values.set(key, value);\n }\n\n const variant = values.get(\"variant\");\n const format = values.get(\"format\");\n\n if (variant === undefined) {\n return values.size === 0\n ? { type: \"original\" }\n : { type: \"invalid\", reason: `\"format\" requires \"variant\".` };\n }\n\n if (variant === \"\") {\n return { type: \"invalid\", reason: `\"variant\" must not be empty.` };\n }\n\n return format === undefined ? { type: \"variant\", variant } : { type: \"variant\", variant, format };\n}\n"],"mappings":";AAEA,MAAM,eAAe,IAAI,IAAI,CAAC,WAAW,QAAQ,CAAC;;;;;;;;;;;AAYlD,SAAgB,uBAAuB,KAAgC;CACrE,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,WAAW,eAAe,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;CAElE,IAAI,aAAa,IAAI,OAAO,EAAE,MAAM,WAAW;CAE/C,MAAM,yBAAS,IAAI,IAAoB;CAEvC,KAAK,MAAM,CAAC,KAAK,UAAU,IAAI,gBAAgB,QAAQ,GAAG;EACxD,IAAI,CAAC,aAAa,IAAI,GAAG,GACvB,OAAO;GAAE,MAAM;GAAW,QAAQ,4BAA4B,IAAI;EAAI;EAGxE,IAAI,OAAO,IAAI,GAAG,GAChB,OAAO;GAAE,MAAM;GAAW,QAAQ,oBAAoB,IAAI;EAAgB;EAG5E,OAAO,IAAI,KAAK,KAAK;CACvB;CAEA,MAAM,UAAU,OAAO,IAAI,SAAS;CACpC,MAAM,SAAS,OAAO,IAAI,QAAQ;CAElC,IAAI,YAAY,QACd,OAAO,OAAO,SAAS,IACnB,EAAE,MAAM,WAAW,IACnB;EAAE,MAAM;EAAW,QAAQ;CAA+B;CAGhE,IAAI,YAAY,IACd,OAAO;EAAE,MAAM;EAAW,QAAQ;CAA+B;CAGnE,OAAO,WAAW,SAAY;EAAE,MAAM;EAAW;CAAQ,IAAI;EAAE,MAAM;EAAW;EAAS;CAAO;AAClG"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/http/uploads/read-file-head.ts
|
|
4
|
+
/**
|
|
5
|
+
* Read at most the first `length` bytes of a file
|
|
6
|
+
*/
|
|
7
|
+
async function readFileHead(absolutePath, length) {
|
|
8
|
+
const handle = await fs.open(absolutePath, "r");
|
|
9
|
+
try {
|
|
10
|
+
const buffer = Buffer.alloc(length);
|
|
11
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
12
|
+
return buffer.subarray(0, bytesRead);
|
|
13
|
+
} finally {
|
|
14
|
+
await handle.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { readFileHead };
|
|
20
|
+
//# sourceMappingURL=read-file-head.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-file-head.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/read-file-head.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\n\n/**\n * Read at most the first `length` bytes of a file\n */\nexport async function readFileHead(absolutePath: string, length: number): Promise<Buffer> {\n const handle = await fs.open(absolutePath, \"r\");\n\n try {\n const buffer = Buffer.alloc(length);\n const { bytesRead } = await handle.read(buffer, 0, length, 0);\n\n return buffer.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n}\n"],"mappings":";;;;;;AAKA,eAAsB,aAAa,cAAsB,QAAiC;CACxF,MAAM,SAAS,MAAM,GAAG,KAAK,cAAc,GAAG;CAE9C,IAAI;EACF,MAAM,SAAS,OAAO,MAAM,MAAM;EAClC,MAAM,EAAE,cAAc,MAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ,CAAC;EAE5D,OAAO,OAAO,SAAS,GAAG,SAAS;CACrC,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;AACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/http/uploads/resolve-image-variants-config.ts
|
|
5
|
+
/**
|
|
6
|
+
* Largest width or height a variant may declare
|
|
7
|
+
*/
|
|
8
|
+
const MAX_VARIANT_DIMENSION = 8192;
|
|
9
|
+
/**
|
|
10
|
+
* Default `maxSourceBytes`: 25 MB
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MAX_SOURCE_BYTES = 25 * 1024 * 1024;
|
|
13
|
+
/**
|
|
14
|
+
* Default `maxSourcePixels`
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_MAX_SOURCE_PIXELS = 4e7;
|
|
17
|
+
const FITS = [
|
|
18
|
+
"cover",
|
|
19
|
+
"contain",
|
|
20
|
+
"inside"
|
|
21
|
+
];
|
|
22
|
+
const OUTPUT_FORMATS = ["avif", "webp"];
|
|
23
|
+
const VARIANT_NAME = /^[A-Za-z0-9_-]+$/;
|
|
24
|
+
function isPositiveInteger(value, max = Number.MAX_SAFE_INTEGER) {
|
|
25
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= max;
|
|
26
|
+
}
|
|
27
|
+
function resolveVariant(name, raw) {
|
|
28
|
+
const key = `uploads.images.variants.${name}`;
|
|
29
|
+
if (!VARIANT_NAME.test(name)) throw new ImageVariantsConfigError(`${key}: a variant name may only use letters, digits, "-" and "_".`);
|
|
30
|
+
if (!raw || typeof raw !== "object") throw new ImageVariantsConfigError(`${key} must be an object with a width.`);
|
|
31
|
+
const { width, height, fit, quality, enlarge } = raw;
|
|
32
|
+
if (!isPositiveInteger(width, 8192)) throw new ImageVariantsConfigError(`${key}.width must be an integer from 1 to ${MAX_VARIANT_DIMENSION}, got ${JSON.stringify(width)}.`);
|
|
33
|
+
const variant = { width };
|
|
34
|
+
if (height !== void 0) {
|
|
35
|
+
if (!isPositiveInteger(height, 8192)) throw new ImageVariantsConfigError(`${key}.height must be an integer from 1 to ${MAX_VARIANT_DIMENSION}, got ${JSON.stringify(height)}.`);
|
|
36
|
+
variant.height = height;
|
|
37
|
+
}
|
|
38
|
+
if (fit !== void 0) {
|
|
39
|
+
if (!FITS.includes(fit)) throw new ImageVariantsConfigError(`${key}.fit must be one of ${FITS.join(", ")}, got ${JSON.stringify(fit)}.`);
|
|
40
|
+
variant.fit = fit;
|
|
41
|
+
}
|
|
42
|
+
if (quality !== void 0) {
|
|
43
|
+
if (!isPositiveInteger(quality, 100)) throw new ImageVariantsConfigError(`${key}.quality must be an integer from 1 to 100, got ${JSON.stringify(quality)}.`);
|
|
44
|
+
variant.quality = quality;
|
|
45
|
+
}
|
|
46
|
+
if (enlarge !== void 0 && typeof enlarge !== "boolean") throw new ImageVariantsConfigError(`${key}.enlarge must be a boolean, got ${JSON.stringify(enlarge)}.`);
|
|
47
|
+
variant.enlarge = enlarge === true;
|
|
48
|
+
return variant;
|
|
49
|
+
}
|
|
50
|
+
function resolveFormats(raw) {
|
|
51
|
+
if (raw === void 0) return [];
|
|
52
|
+
if (!Array.isArray(raw)) throw new ImageVariantsConfigError(`uploads.images.formats must be an array.`);
|
|
53
|
+
for (const format of raw) if (!OUTPUT_FORMATS.includes(format)) throw new ImageVariantsConfigError(`uploads.images.formats may only contain ${OUTPUT_FORMATS.join(", ")}, got ${JSON.stringify(format)}.`);
|
|
54
|
+
return [...new Set(raw)];
|
|
55
|
+
}
|
|
56
|
+
function resolveLimit(name, raw, fallback) {
|
|
57
|
+
if (raw === void 0) return fallback;
|
|
58
|
+
if (!isPositiveInteger(raw)) throw new ImageVariantsConfigError(`uploads.images.${name} must be a positive integer, got ${JSON.stringify(raw)}.`);
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate the `uploads.images` config and apply its defaults.
|
|
63
|
+
*
|
|
64
|
+
* Every variant is copied field by field, so an unknown key in the config can
|
|
65
|
+
* never reach sharp, and the result is what the cache key is computed from.
|
|
66
|
+
*
|
|
67
|
+
* @param images - the raw `uploads.images` value
|
|
68
|
+
* @param storageRoot - the local storage root, base of the default cache directory
|
|
69
|
+
* @returns the resolved config, or `undefined` when variants are not configured
|
|
70
|
+
* @throws ImageVariantsConfigError when any value is out of bounds
|
|
71
|
+
*/
|
|
72
|
+
function resolveImageVariantsConfig(images, storageRoot) {
|
|
73
|
+
if (images === void 0 || images === null) return void 0;
|
|
74
|
+
if (typeof images !== "object") throw new ImageVariantsConfigError("uploads.images must be an object.");
|
|
75
|
+
const rawVariants = images.variants;
|
|
76
|
+
if (!rawVariants || typeof rawVariants !== "object" || Object.keys(rawVariants).length === 0) throw new ImageVariantsConfigError("uploads.images.variants must declare at least one variant.");
|
|
77
|
+
const variants = {};
|
|
78
|
+
for (const [name, raw] of Object.entries(rawVariants)) variants[name] = resolveVariant(name, raw);
|
|
79
|
+
const cacheDirectory = images.cacheDirectory ? path.resolve(images.cacheDirectory) : path.join(storageRoot, ".cache", "image-variants");
|
|
80
|
+
return {
|
|
81
|
+
variants,
|
|
82
|
+
formats: resolveFormats(images.formats),
|
|
83
|
+
maxSourceBytes: resolveLimit("maxSourceBytes", images.maxSourceBytes, DEFAULT_MAX_SOURCE_BYTES),
|
|
84
|
+
maxSourcePixels: resolveLimit("maxSourcePixels", images.maxSourcePixels, DEFAULT_MAX_SOURCE_PIXELS),
|
|
85
|
+
cacheDirectory
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { resolveImageVariantsConfig };
|
|
91
|
+
//# sourceMappingURL=resolve-image-variants-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-image-variants-config.mjs","names":[],"sources":["../../../../../../../../core/src/http/uploads/resolve-image-variants-config.ts"],"sourcesContent":["import path from \"node:path\";\nimport type {\n ImageVariantDefinition,\n ImageVariantFit,\n ImageVariantOutputFormat,\n UploadsImagesConfigurations,\n} from \"../uploads-types\";\nimport { ImageVariantsConfigError } from \"./image-variants-config-error\";\nimport type { ResolvedImageVariantsConfig } from \"./image-variant-types\";\n\n/**\n * Largest width or height a variant may declare\n */\nexport const MAX_VARIANT_DIMENSION = 8192;\n\n/**\n * Default `maxSourceBytes`: 25 MB\n */\nexport const DEFAULT_MAX_SOURCE_BYTES = 25 * 1024 * 1024;\n\n/**\n * Default `maxSourcePixels`\n */\nexport const DEFAULT_MAX_SOURCE_PIXELS = 40_000_000;\n\nconst FITS: readonly ImageVariantFit[] = [\"cover\", \"contain\", \"inside\"];\n\nconst OUTPUT_FORMATS: readonly ImageVariantOutputFormat[] = [\"avif\", \"webp\"];\n\nconst VARIANT_NAME = /^[A-Za-z0-9_-]+$/;\n\nfunction isPositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 1 && value <= max;\n}\n\nfunction resolveVariant(name: string, raw: unknown): ImageVariantDefinition {\n const key = `uploads.images.variants.${name}`;\n\n if (!VARIANT_NAME.test(name)) {\n throw new ImageVariantsConfigError(\n `${key}: a variant name may only use letters, digits, \"-\" and \"_\".`,\n );\n }\n\n if (!raw || typeof raw !== \"object\") {\n throw new ImageVariantsConfigError(`${key} must be an object with a width.`);\n }\n\n const { width, height, fit, quality, enlarge } = raw as Record<string, unknown>;\n\n if (!isPositiveInteger(width, MAX_VARIANT_DIMENSION)) {\n throw new ImageVariantsConfigError(\n `${key}.width must be an integer from 1 to ${MAX_VARIANT_DIMENSION}, got ${JSON.stringify(width)}.`,\n );\n }\n\n const variant: ImageVariantDefinition = { width };\n\n if (height !== undefined) {\n if (!isPositiveInteger(height, MAX_VARIANT_DIMENSION)) {\n throw new ImageVariantsConfigError(\n `${key}.height must be an integer from 1 to ${MAX_VARIANT_DIMENSION}, got ${JSON.stringify(height)}.`,\n );\n }\n\n variant.height = height;\n }\n\n if (fit !== undefined) {\n if (!FITS.includes(fit as ImageVariantFit)) {\n throw new ImageVariantsConfigError(\n `${key}.fit must be one of ${FITS.join(\", \")}, got ${JSON.stringify(fit)}.`,\n );\n }\n\n variant.fit = fit as ImageVariantFit;\n }\n\n if (quality !== undefined) {\n if (!isPositiveInteger(quality, 100)) {\n throw new ImageVariantsConfigError(\n `${key}.quality must be an integer from 1 to 100, got ${JSON.stringify(quality)}.`,\n );\n }\n\n variant.quality = quality;\n }\n\n if (enlarge !== undefined && typeof enlarge !== \"boolean\") {\n throw new ImageVariantsConfigError(\n `${key}.enlarge must be a boolean, got ${JSON.stringify(enlarge)}.`,\n );\n }\n\n // Always set, never left undefined: the effective flag (default false) is\n // part of the normalized variant, so toggling it changes the cache key.\n variant.enlarge = enlarge === true;\n\n return variant;\n}\n\nfunction resolveFormats(raw: unknown): readonly ImageVariantOutputFormat[] {\n if (raw === undefined) return [];\n\n if (!Array.isArray(raw)) {\n throw new ImageVariantsConfigError(`uploads.images.formats must be an array.`);\n }\n\n for (const format of raw) {\n if (!OUTPUT_FORMATS.includes(format)) {\n throw new ImageVariantsConfigError(\n `uploads.images.formats may only contain ${OUTPUT_FORMATS.join(\", \")}, got ${JSON.stringify(format)}.`,\n );\n }\n }\n\n return [...new Set(raw as ImageVariantOutputFormat[])];\n}\n\nfunction resolveLimit(name: string, raw: unknown, fallback: number): number {\n if (raw === undefined) return fallback;\n\n if (!isPositiveInteger(raw)) {\n throw new ImageVariantsConfigError(\n `uploads.images.${name} must be a positive integer, got ${JSON.stringify(raw)}.`,\n );\n }\n\n return raw;\n}\n\n/**\n * Validate the `uploads.images` config and apply its defaults.\n *\n * Every variant is copied field by field, so an unknown key in the config can\n * never reach sharp, and the result is what the cache key is computed from.\n *\n * @param images - the raw `uploads.images` value\n * @param storageRoot - the local storage root, base of the default cache directory\n * @returns the resolved config, or `undefined` when variants are not configured\n * @throws ImageVariantsConfigError when any value is out of bounds\n */\nexport function resolveImageVariantsConfig(\n images: UploadsImagesConfigurations | undefined,\n storageRoot: string,\n): ResolvedImageVariantsConfig | undefined {\n if (images === undefined || images === null) return undefined;\n\n if (typeof images !== \"object\") {\n throw new ImageVariantsConfigError(\"uploads.images must be an object.\");\n }\n\n const rawVariants = (images as { variants?: unknown }).variants;\n\n if (!rawVariants || typeof rawVariants !== \"object\" || Object.keys(rawVariants).length === 0) {\n throw new ImageVariantsConfigError(\n \"uploads.images.variants must declare at least one variant.\",\n );\n }\n\n const variants: Record<string, ImageVariantDefinition> = {};\n\n for (const [name, raw] of Object.entries(rawVariants)) {\n variants[name] = resolveVariant(name, raw);\n }\n\n const cacheDirectory = images.cacheDirectory\n ? path.resolve(images.cacheDirectory)\n : path.join(storageRoot, \".cache\", \"image-variants\");\n\n return {\n variants,\n formats: resolveFormats(images.formats),\n maxSourceBytes: resolveLimit(\"maxSourceBytes\", images.maxSourceBytes, DEFAULT_MAX_SOURCE_BYTES),\n maxSourcePixels: resolveLimit(\n \"maxSourcePixels\",\n images.maxSourcePixels,\n DEFAULT_MAX_SOURCE_PIXELS,\n ),\n cacheDirectory,\n };\n}\n"],"mappings":";;;;;;;AAaA,MAAa,wBAAwB;;;;AAKrC,MAAa,2BAA2B,KAAK,OAAO;;;;AAKpD,MAAa,4BAA4B;AAEzC,MAAM,OAAmC;CAAC;CAAS;CAAW;AAAQ;AAEtE,MAAM,iBAAsD,CAAC,QAAQ,MAAM;AAE3E,MAAM,eAAe;AAErB,SAAS,kBAAkB,OAAgB,MAAM,OAAO,kBAAmC;CACzF,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,SAAS;AACxF;AAEA,SAAS,eAAe,MAAc,KAAsC;CAC1E,MAAM,MAAM,2BAA2B;CAEvC,IAAI,CAAC,aAAa,KAAK,IAAI,GACzB,MAAM,IAAI,yBACR,GAAG,IAAI,4DACT;CAGF,IAAI,CAAC,OAAO,OAAO,QAAQ,UACzB,MAAM,IAAI,yBAAyB,GAAG,IAAI,iCAAiC;CAG7E,MAAM,EAAE,OAAO,QAAQ,KAAK,SAAS,YAAY;CAEjD,IAAI,CAAC,kBAAkB,WAA4B,GACjD,MAAM,IAAI,yBACR,GAAG,IAAI,sCAAsC,sBAAsB,QAAQ,KAAK,UAAU,KAAK,EAAE,EACnG;CAGF,MAAM,UAAkC,EAAE,MAAM;CAEhD,IAAI,WAAW,QAAW;EACxB,IAAI,CAAC,kBAAkB,YAA6B,GAClD,MAAM,IAAI,yBACR,GAAG,IAAI,uCAAuC,sBAAsB,QAAQ,KAAK,UAAU,MAAM,EAAE,EACrG;EAGF,QAAQ,SAAS;CACnB;CAEA,IAAI,QAAQ,QAAW;EACrB,IAAI,CAAC,KAAK,SAAS,GAAsB,GACvC,MAAM,IAAI,yBACR,GAAG,IAAI,sBAAsB,KAAK,KAAK,IAAI,EAAE,QAAQ,KAAK,UAAU,GAAG,EAAE,EAC3E;EAGF,QAAQ,MAAM;CAChB;CAEA,IAAI,YAAY,QAAW;EACzB,IAAI,CAAC,kBAAkB,SAAS,GAAG,GACjC,MAAM,IAAI,yBACR,GAAG,IAAI,iDAAiD,KAAK,UAAU,OAAO,EAAE,EAClF;EAGF,QAAQ,UAAU;CACpB;CAEA,IAAI,YAAY,UAAa,OAAO,YAAY,WAC9C,MAAM,IAAI,yBACR,GAAG,IAAI,kCAAkC,KAAK,UAAU,OAAO,EAAE,EACnE;CAKF,QAAQ,UAAU,YAAY;CAE9B,OAAO;AACT;AAEA,SAAS,eAAe,KAAmD;CACzE,IAAI,QAAQ,QAAW,OAAO,CAAC;CAE/B,IAAI,CAAC,MAAM,QAAQ,GAAG,GACpB,MAAM,IAAI,yBAAyB,0CAA0C;CAG/E,KAAK,MAAM,UAAU,KACnB,IAAI,CAAC,eAAe,SAAS,MAAM,GACjC,MAAM,IAAI,yBACR,2CAA2C,eAAe,KAAK,IAAI,EAAE,QAAQ,KAAK,UAAU,MAAM,EAAE,EACtG;CAIJ,OAAO,CAAC,GAAG,IAAI,IAAI,GAAiC,CAAC;AACvD;AAEA,SAAS,aAAa,MAAc,KAAc,UAA0B;CAC1E,IAAI,QAAQ,QAAW,OAAO;CAE9B,IAAI,CAAC,kBAAkB,GAAG,GACxB,MAAM,IAAI,yBACR,kBAAkB,KAAK,mCAAmC,KAAK,UAAU,GAAG,EAAE,EAChF;CAGF,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,2BACd,QACA,aACyC;CACzC,IAAI,WAAW,UAAa,WAAW,MAAM,OAAO;CAEpD,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,yBAAyB,mCAAmC;CAGxE,MAAM,cAAe,OAAkC;CAEvD,IAAI,CAAC,eAAe,OAAO,gBAAgB,YAAY,OAAO,KAAK,WAAW,CAAC,CAAC,WAAW,GACzF,MAAM,IAAI,yBACR,4DACF;CAGF,MAAM,WAAmD,CAAC;CAE1D,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,WAAW,GAClD,SAAS,QAAQ,eAAe,MAAM,GAAG;CAG3C,MAAM,iBAAiB,OAAO,iBAC1B,KAAK,QAAQ,OAAO,cAAc,IAClC,KAAK,KAAK,aAAa,UAAU,gBAAgB;CAErD,OAAO;EACL;EACA,SAAS,eAAe,OAAO,OAAO;EACtC,gBAAgB,aAAa,kBAAkB,OAAO,gBAAgB,wBAAwB;EAC9F,iBAAiB,aACf,mBACA,OAAO,iBACP,yBACF;EACA;CACF;AACF"}
|