@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 +4 -2
- package/dist/index.mjs +5 -4
- package/dist/node.d.mts +7 -6
- package/dist/node.mjs +9 -11
- package/package.json +1 -1
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
|
|
44
|
-
`nodeAdapter({ canonicalOrigin })
|
|
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
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
if (typeof
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|
|
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 (!
|
|
167
|
-
target = new URL(source,
|
|
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,
|
|
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,
|
|
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();
|