@pracht/image 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,8 +40,10 @@ export const GET = imageHandler;
40
40
  export const HEAD = imageHandler;
41
41
  ```
42
42
 
43
- Set `localOrigin` to the same trusted public origin used by
44
- `nodeAdapter({ canonicalOrigin })`. It may be omitted for loopback development.
43
+ Set `localOrigin` to the same trusted origin used by
44
+ `nodeAdapter({ canonicalOrigin })` in development and production. Relative
45
+ sources fail closed when it is omitted; request and Host-derived origins are
46
+ never trusted, even when they look like loopback addresses.
45
47
 
46
48
  See [docs/IMAGES.md](https://github.com/JoviDeCroock/pracht/blob/main/docs/IMAGES.md)
47
49
  for the full guide: loader configuration, endpoint security options, and
package/dist/index.mjs CHANGED
@@ -16,10 +16,11 @@ function getNodeEnv() {
16
16
  return globalThis.process?.env?.NODE_ENV;
17
17
  }
18
18
  function getImportMetaDev() {
19
- const env = import.meta.env;
20
- if (env?.MODE === "production") return false;
21
- if (typeof env?.DEV === "boolean") return env.DEV;
22
- if (typeof env?.MODE === "string") return env.MODE !== "production";
19
+ const mode = import.meta.env?.MODE;
20
+ if (mode === "production") return false;
21
+ const dev = import.meta.env?.DEV;
22
+ if (typeof dev === "boolean") return dev;
23
+ if (typeof mode === "string") return mode !== "production";
23
24
  }
24
25
  function isDevWarningsEnabled() {
25
26
  const nodeEnv = getNodeEnv();
package/dist/node.d.mts CHANGED
@@ -18,10 +18,9 @@ interface CreateImageHandlerOptions {
18
18
  /** Remote sources to allow. Defaults to none (same-origin only). */
19
19
  remotePatterns?: RemotePattern[];
20
20
  /**
21
- * Trusted public origin used to resolve relative image paths in production.
22
- * Required for non-loopback requests so an attacker-controlled Host header
23
- * cannot turn the endpoint into an open proxy. Loopback origins remain
24
- * available without configuration for local development.
21
+ * Trusted origin used to resolve relative image paths. Required whenever
22
+ * relative sources are served so an attacker-controlled Host header cannot
23
+ * turn the endpoint into an open proxy.
25
24
  */
26
25
  localOrigin?: string;
27
26
  /**
@@ -67,7 +66,9 @@ interface ImageHandlerArgs {
67
66
  * ```ts
68
67
  * // src/api/_pracht/image.ts
69
68
  * import { createImageHandler } from "@pracht/image/node";
70
- * const imageHandler = createImageHandler();
69
+ * const imageHandler = createImageHandler({
70
+ * localOrigin: process.env.PRACHT_ORIGIN,
71
+ * });
71
72
  * export const GET = imageHandler;
72
73
  * export const HEAD = imageHandler;
73
74
  * ```
@@ -75,7 +76,7 @@ interface ImageHandlerArgs {
75
76
  * The handler resizes and re-encodes images with sharp (an optional peer
76
77
  * dependency — install it in your app), negotiates WebP/AVIF via the `Accept`
77
78
  * header, and answers with cacheable responses keyed on the query string.
78
- * Relative sources resolve against a trusted `localOrigin` in production;
79
+ * Relative sources resolve only against a configured, trusted `localOrigin`;
79
80
  * `remotePatterns` opts specific remote hosts in.
80
81
  */
81
82
  declare function createImageHandler(options?: CreateImageHandlerOptions): (args: ImageHandlerArgs) => Promise<Response>;
package/dist/node.mjs CHANGED
@@ -55,10 +55,6 @@ function normalizeLocalOrigin(value) {
55
55
  if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("createImageHandler({ localOrigin }) expects an http(s) origin without a path.");
56
56
  return url.origin;
57
57
  }
58
- function isLoopbackHostname(hostname) {
59
- const host = hostname.toLowerCase();
60
- return host === "localhost" || host.endsWith(".localhost") || host === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host);
61
- }
62
58
  function isAllowedTarget(url, localOrigin, patterns) {
63
59
  if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) return false;
64
60
  return localOrigin !== void 0 && url.origin === localOrigin || matchesRemotePatterns(url, patterns);
@@ -114,7 +110,9 @@ function imageResponseBody(request, bytes) {
114
110
  * ```ts
115
111
  * // src/api/_pracht/image.ts
116
112
  * import { createImageHandler } from "@pracht/image/node";
117
- * const imageHandler = createImageHandler();
113
+ * const imageHandler = createImageHandler({
114
+ * localOrigin: process.env.PRACHT_ORIGIN,
115
+ * });
118
116
  * export const GET = imageHandler;
119
117
  * export const HEAD = imageHandler;
120
118
  * ```
@@ -122,7 +120,7 @@ function imageResponseBody(request, bytes) {
122
120
  * The handler resizes and re-encodes images with sharp (an optional peer
123
121
  * dependency — install it in your app), negotiates WebP/AVIF via the `Accept`
124
122
  * header, and answers with cacheable responses keyed on the query string.
125
- * Relative sources resolve against a trusted `localOrigin` in production;
123
+ * Relative sources resolve only against a configured, trusted `localOrigin`;
126
124
  * `remotePatterns` opts specific remote hosts in.
127
125
  */
128
126
  function createImageHandler(options = {}) {
@@ -147,7 +145,6 @@ function createImageHandler(options = {}) {
147
145
  headers: { allow: "GET, HEAD" }
148
146
  });
149
147
  const requestUrl = new URL(request.url);
150
- const localOrigin = configuredLocalOrigin ?? (isLoopbackHostname(requestUrl.hostname) ? requestUrl.origin : void 0);
151
148
  const source = requestUrl.searchParams.get("url");
152
149
  const widthParam = requestUrl.searchParams.get("w");
153
150
  const qualityParam = requestUrl.searchParams.get("q");
@@ -163,8 +160,9 @@ function createImageHandler(options = {}) {
163
160
  if (target.username || target.password) return errorResponse(400, "The \"url\" parameter may not contain credentials.");
164
161
  if (!matchesRemotePatterns(target, remotePatterns)) return errorResponse(403, `Remote image "${source}" is not allowed. Add its host to the remotePatterns option of createImageHandler() to opt it in.`);
165
162
  } else if (source.startsWith("/")) {
166
- if (!localOrigin) return errorResponse(500, "Relative image sources require createImageHandler({ localOrigin }) outside local development.");
167
- target = new URL(source, localOrigin);
163
+ if (!configuredLocalOrigin) return errorResponse(500, "Relative image sources require createImageHandler({ localOrigin }).");
164
+ target = new URL(source, configuredLocalOrigin);
165
+ if (target.origin !== configuredLocalOrigin) return errorResponse(400, "Relative \"url\" values must remain on the configured localOrigin.");
168
166
  } else return errorResponse(400, "The \"url\" parameter must be a relative path (starting with \"/\") or an absolute http(s) URL.");
169
167
  if (!widthParam) return errorResponse(400, "Missing required \"w\" query parameter.");
170
168
  const width = Number(widthParam);
@@ -195,7 +193,7 @@ function createImageHandler(options = {}) {
195
193
  } catch {
196
194
  return errorResponse(502, `Source image "${source}" returned an invalid redirect.`);
197
195
  }
198
- if (!isAllowedTarget(nextTarget, localOrigin, remotePatterns)) return errorResponse(403, `Source image "${source}" redirected to a host that is not allowed.`);
196
+ if (!isAllowedTarget(nextTarget, configuredLocalOrigin, remotePatterns)) return errorResponse(403, `Source image "${source}" redirected to a host that is not allowed.`);
199
197
  try {
200
198
  await upstream.body?.cancel();
201
199
  } catch {}
@@ -209,7 +207,7 @@ function createImageHandler(options = {}) {
209
207
  } catch {
210
208
  finalUrl = void 0;
211
209
  }
212
- if (finalUrl && !isAllowedTarget(finalUrl, localOrigin, remotePatterns)) return errorResponse(403, `Source image "${source}" redirected to a host that is not allowed.`);
210
+ if (finalUrl && !isAllowedTarget(finalUrl, configuredLocalOrigin, remotePatterns)) return errorResponse(403, `Source image "${source}" redirected to a host that is not allowed.`);
213
211
  }
214
212
  if (!upstream.ok) return errorResponse(502, `Source image "${source}" responded with ${upstream.status}.`);
215
213
  const sourceType = (upstream.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/image",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Responsive, CLS-safe <Image> component for Pracht with pluggable optimization loaders and a Node image endpoint.",
5
5
  "keywords": [
6
6
  "pracht",