@warlock.js/core 5.15.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/esm/cli/commands/migrate.command.mjs +5 -1
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/seed.command.mjs +2 -1
  5. package/esm/cli/commands/seed.command.mjs.map +1 -1
  6. package/esm/cli/commands/typings-generator.command.mjs +16 -23
  7. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  8. package/esm/config/locale-configuration.mjs +7 -1
  9. package/esm/config/locale-configuration.mjs.map +1 -1
  10. package/esm/connectors/http-connector.d.mts.map +1 -1
  11. package/esm/connectors/http-connector.mjs +1 -18
  12. package/esm/connectors/http-connector.mjs.map +1 -1
  13. package/esm/connectors/read-bound-port.mjs +32 -0
  14. package/esm/connectors/read-bound-port.mjs.map +1 -0
  15. package/esm/dev-server/development-server.mjs +5 -1
  16. package/esm/dev-server/development-server.mjs.map +1 -1
  17. package/esm/dev-server/extract-translation-keys.mjs +68 -0
  18. package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
  19. package/esm/dev-server/translation-keys-sources.mjs +19 -0
  20. package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
  21. package/esm/dev-server/type-generator.mjs +64 -11
  22. package/esm/dev-server/type-generator.mjs.map +1 -1
  23. package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
  24. package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
  25. package/esm/generations/features/sitemap.feature.mjs +50 -47
  26. package/esm/generations/features/sitemap.feature.mjs.map +1 -1
  27. package/esm/generations/stubs.mjs +7 -6
  28. package/esm/generations/stubs.mjs.map +1 -1
  29. package/esm/http/csrf-default-guard.d.mts +32 -0
  30. package/esm/http/csrf-default-guard.d.mts.map +1 -0
  31. package/esm/http/csrf-default-guard.mjs +156 -0
  32. package/esm/http/csrf-default-guard.mjs.map +1 -0
  33. package/esm/http/csrf-origin-policy.d.mts +53 -0
  34. package/esm/http/csrf-origin-policy.d.mts.map +1 -0
  35. package/esm/http/csrf-origin-policy.mjs +104 -0
  36. package/esm/http/csrf-origin-policy.mjs.map +1 -0
  37. package/esm/http/error-codes.d.mts +14 -1
  38. package/esm/http/error-codes.d.mts.map +1 -1
  39. package/esm/http/error-codes.mjs +13 -0
  40. package/esm/http/error-codes.mjs.map +1 -1
  41. package/esm/http/index.d.mts +8 -1
  42. package/esm/http/index.mjs +7 -1
  43. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  44. package/esm/http/middleware/idempotency.middleware.mjs +1 -1
  45. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  46. package/esm/http/middleware/inject-request-context.mjs +6 -0
  47. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  48. package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
  49. package/esm/http/middleware/maintenance.middleware.mjs +1 -1
  50. package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
  51. package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
  52. package/esm/http/parse-urlencoded-body.mjs +27 -0
  53. package/esm/http/parse-urlencoded-body.mjs.map +1 -0
  54. package/esm/http/plugins.d.mts.map +1 -1
  55. package/esm/http/plugins.mjs +8 -0
  56. package/esm/http/plugins.mjs.map +1 -1
  57. package/esm/http/request.d.mts +2 -2
  58. package/esm/http/request.d.mts.map +1 -1
  59. package/esm/http/request.mjs +7 -3
  60. package/esm/http/request.mjs.map +1 -1
  61. package/esm/http/response.d.mts +9 -2
  62. package/esm/http/response.d.mts.map +1 -1
  63. package/esm/http/response.mjs +18 -4
  64. package/esm/http/response.mjs.map +1 -1
  65. package/esm/http/uploads/detect-image-format.mjs +59 -0
  66. package/esm/http/uploads/detect-image-format.mjs.map +1 -0
  67. package/esm/http/uploads/generate-image-variant.mjs +46 -0
  68. package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
  69. package/esm/http/uploads/generate-image-variants.d.mts +49 -0
  70. package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
  71. package/esm/http/uploads/generate-image-variants.mjs +141 -0
  72. package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
  73. package/esm/http/uploads/image-variant-types.d.mts +93 -0
  74. package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
  75. package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
  76. package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
  77. package/esm/http/uploads/image-variants-config-error.mjs +17 -0
  78. package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
  79. package/esm/http/uploads/index.d.mts +4 -0
  80. package/esm/http/uploads/index.mjs +5 -0
  81. package/esm/http/uploads/is-path-inside.mjs +26 -0
  82. package/esm/http/uploads/is-path-inside.mjs.map +1 -0
  83. package/esm/http/uploads/load-variant-source.mjs +42 -0
  84. package/esm/http/uploads/load-variant-source.mjs.map +1 -0
  85. package/esm/http/uploads/matches-if-none-match.mjs +13 -0
  86. package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
  87. package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
  88. package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
  89. package/esm/http/uploads/read-file-head.mjs +20 -0
  90. package/esm/http/uploads/read-file-head.mjs.map +1 -0
  91. package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
  92. package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
  93. package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
  94. package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
  95. package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
  96. package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
  97. package/esm/http/uploads/resolve-upload-path.mjs +56 -0
  98. package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
  99. package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
  100. package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
  101. package/esm/http/uploads/single-flight.mjs +40 -0
  102. package/esm/http/uploads/single-flight.mjs.map +1 -0
  103. package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
  104. package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
  105. package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
  106. package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
  107. package/esm/http/uploads/variant-cache-key.mjs +45 -0
  108. package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
  109. package/esm/http/uploads/variant-generations.mjs +16 -0
  110. package/esm/http/uploads/variant-generations.mjs.map +1 -0
  111. package/esm/http/uploads-types.d.mts +102 -1
  112. package/esm/http/uploads-types.d.mts.map +1 -1
  113. package/esm/http/xmlable.d.mts +14 -0
  114. package/esm/http/xmlable.d.mts.map +1 -0
  115. package/esm/index.d.mts +10 -2
  116. package/esm/index.mjs +8 -2
  117. package/esm/router/types.d.mts +23 -0
  118. package/esm/router/types.d.mts.map +1 -1
  119. package/esm/storage/index.d.mts +1 -0
  120. package/esm/storage/index.mjs +1 -0
  121. package/esm/storage/scoped-storage.d.mts +1 -0
  122. package/esm/storage/scoped-storage.d.mts.map +1 -1
  123. package/esm/storage/scoped-storage.mjs +3 -0
  124. package/esm/storage/scoped-storage.mjs.map +1 -1
  125. package/esm/storage/storage.d.mts +2 -0
  126. package/esm/storage/storage.d.mts.map +1 -1
  127. package/esm/storage/storage.mjs +4 -0
  128. package/esm/storage/storage.mjs.map +1 -1
  129. package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
  130. package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
  131. package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
  132. package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
  133. package/llms-full.txt +47 -7
  134. package/llms.txt +1 -1
  135. package/package.json +22 -21
  136. package/skills/configure-app/SKILL.md +6 -6
  137. package/skills/create-controller/SKILL.md +14 -0
  138. package/skills/health-checks/SKILL.md +8 -0
  139. package/skills/send-response/SKILL.md +2 -0
  140. package/skills/use-localization/SKILL.md +16 -0
  141. package/skills/use-middleware/SKILL.md +1 -1
  142. package/esm/dev-server/translation-type-generator.mjs +0 -28
  143. 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,5 @@
1
+ import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
2
+ import { generateImageVariants } from "./generate-image-variants.mjs";
3
+ import { uploadedFileController } from "./uploaded-file.controller.mjs";
4
+
5
+ export { };
@@ -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":"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"}