@styleflow.app/flowpress-astro-core 1.0.0-beta.14 → 1.0.0-beta.16
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/README.md +18 -2
- package/dist/astro/CompositionRenderer.astro +18 -2
- package/dist/media.d.ts +19 -0
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +84 -13
- package/dist/media.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.0-beta.16
|
|
6
|
+
|
|
7
|
+
- Rebase owned composition media to the current frontend, including nested
|
|
8
|
+
query data, authoring attributes and responsive sources. WordPress uploads
|
|
9
|
+
keep `/app/uploads/`; offloaded assets keep `/media/`. External media and
|
|
10
|
+
raw HTML remain unchanged.
|
|
11
|
+
- Export `rewriteFlowpressMediaUrl`, `rewriteFlowpressMediaUrls` and explicit
|
|
12
|
+
frontend/backend/offload origin options for project integrations.
|
|
13
|
+
- Follow storage redirects server-side with a bounded redirect count. Proxy
|
|
14
|
+
responses no longer expose backend/CDN `Location` headers to the browser.
|
|
15
|
+
|
|
16
|
+
## 1.0.0-beta.15
|
|
17
|
+
|
|
18
|
+
- Fixed `proxyFlowpressMedia` forwarding the `/media/` route prefix to the
|
|
19
|
+
upstream `FLOWPRESS_MEDIA` origin. `/media/` is a frontend-only routing
|
|
20
|
+
marker with no equivalent on either upstream (an offload CDN root, or the
|
|
21
|
+
WordPress backend as a fallback), so it is now stripped before the request
|
|
22
|
+
is forwarded: `/media/2026/09/x.jpg` resolves to `<origin>/2026/09/x.jpg`
|
|
23
|
+
instead of `<origin>/media/2026/09/x.jpg`, which 404s.
|
|
24
|
+
- `proxyFlowpressUpload` (`/app/uploads/`) is unaffected: that prefix mirrors
|
|
25
|
+
a real path on the WordPress backend and continues to be forwarded as-is.
|
|
26
|
+
|
|
5
27
|
## 1.0.0-beta.14
|
|
6
28
|
|
|
7
29
|
- Adopted `@styleflow.app/astro@1.0.0-beta.7` and Typography vNext contract
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ The package must not import from an application or project plugin.
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pnpm add @styleflow.app/flowpress-astro-core@1.0.0-beta.
|
|
12
|
+
pnpm add @styleflow.app/flowpress-astro-core@1.0.0-beta.16 \
|
|
13
13
|
@styleflow.app/astro@1.0.0-beta.6
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -96,6 +96,22 @@ renderer and passes them as `styleflowProps`. Project adapters can therefore
|
|
|
96
96
|
apply the authored semantic context to their real root without accepting raw
|
|
97
97
|
WordPress visual values.
|
|
98
98
|
|
|
99
|
+
At the composition root, owned media URLs are rebased to the current request
|
|
100
|
+
origin using explicit `BACKEND_URL`, `FLOWPRESS_MEDIA`, `SITE_URL` and the
|
|
101
|
+
composition's canonical frontend origin. This also covers nested query items
|
|
102
|
+
and responsive `srcSet` data passed to project adapters. WordPress uploads
|
|
103
|
+
remain under `/app/uploads/` and offloaded files under `/media/`; third-party
|
|
104
|
+
assets and raw HTML/CSS/scripts are preserved. A `media` prop of type
|
|
105
|
+
`FlowpressFrontendMediaOptions` can supply explicit origins. Projects can
|
|
106
|
+
also use exported `rewriteFlowpressMediaUrl` / `rewriteFlowpressMediaUrls`
|
|
107
|
+
for data rendered outside a composition. These functions do not mutate the
|
|
108
|
+
CMS payload.
|
|
109
|
+
|
|
110
|
+
The upload and media proxies follow HTTP(S) storage redirects on the server
|
|
111
|
+
(at most five), forwarding only safe request headers and preserving range,
|
|
112
|
+
validator and cache semantics. They never return an upstream `Location` or
|
|
113
|
+
`Set-Cookie` header: the browser stays on the public frontend URL.
|
|
114
|
+
|
|
99
115
|
Every adapter also receives required normalized `htmlProps` (`anchor` → `id`
|
|
100
116
|
and the additional classes), the full composition and a deterministic render
|
|
101
117
|
context. The public HTML allowlist also preserves `title`, `role`, `lang`,
|
|
@@ -246,7 +262,7 @@ The TypeScript template-hierarchy suite is self-contained. Its additional PHP
|
|
|
246
262
|
parity test runs when the sibling `flowpress-wordpress-core` checkout is
|
|
247
263
|
available locally and is skipped in a standalone package checkout.
|
|
248
264
|
|
|
249
|
-
Publish `1.0.0-beta.
|
|
265
|
+
Publish `1.0.0-beta.15` on `next` only after the exact Styleflow prerelease is
|
|
250
266
|
available. Inspect `npm view`, `gitHead`, integrity and the downloaded tarball.
|
|
251
267
|
Promote to `latest` only after the DMO canary and rollback gate.
|
|
252
268
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type StyleflowContract
|
|
12
12
|
} from "../styleflow.js";
|
|
13
13
|
import type { FlowpressBlock, FlowpressCompositionV1 } from "../types.js";
|
|
14
|
+
import { rewriteFlowpressMediaUrls, type FlowpressFrontendMediaOptions } from "../media.js";
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
17
|
composition: FlowpressCompositionV1;
|
|
@@ -20,18 +21,33 @@ interface Props {
|
|
|
20
21
|
parentBlockName?: string;
|
|
21
22
|
production?: boolean;
|
|
22
23
|
depth?: number;
|
|
24
|
+
media?: FlowpressFrontendMediaOptions;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const {
|
|
26
|
-
composition,
|
|
28
|
+
composition: sourceComposition,
|
|
27
29
|
adapters,
|
|
28
|
-
blocks
|
|
30
|
+
blocks: sourceBlocks,
|
|
29
31
|
contract,
|
|
30
32
|
parentBlockName,
|
|
31
33
|
production = import.meta.env.PROD,
|
|
32
34
|
depth = 0
|
|
33
35
|
} = Astro.props;
|
|
34
36
|
|
|
37
|
+
const mediaOptions: FlowpressFrontendMediaOptions = Astro.props.media ?? {
|
|
38
|
+
frontendOrigin: Astro.url.origin,
|
|
39
|
+
backendUrl: import.meta.env.BACKEND_URL,
|
|
40
|
+
mediaUrl: import.meta.env.FLOWPRESS_MEDIA,
|
|
41
|
+
frontendUrls: [import.meta.env.SITE_URL, sourceComposition.route.seo?.canonicalUrl]
|
|
42
|
+
.filter((url): url is string => typeof url === "string" && Boolean(url))
|
|
43
|
+
};
|
|
44
|
+
const composition = depth === 0
|
|
45
|
+
? rewriteFlowpressMediaUrls(sourceComposition, mediaOptions)
|
|
46
|
+
: sourceComposition;
|
|
47
|
+
const blocks = sourceBlocks
|
|
48
|
+
? depth === 0 ? rewriteFlowpressMediaUrls(sourceBlocks, mediaOptions) : sourceBlocks
|
|
49
|
+
: composition.blocks;
|
|
50
|
+
|
|
35
51
|
const blocking = composition.diagnostics.filter((diagnostic) => diagnostic.blocking);
|
|
36
52
|
if (blocking.length > 0 && production) {
|
|
37
53
|
throw new Error(
|
package/dist/media.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
export declare const FLOWPRESS_UPLOADS_PREFIX = "/app/uploads/";
|
|
2
2
|
export declare const FLOWPRESS_MEDIA_PREFIX = "/media/";
|
|
3
|
+
export interface FlowpressFrontendMediaOptions {
|
|
4
|
+
frontendOrigin: string;
|
|
5
|
+
backendUrl?: string | undefined;
|
|
6
|
+
mediaUrl?: string | undefined;
|
|
7
|
+
/** Canonical frontend URLs and aliases owned by the selected backend. */
|
|
8
|
+
frontendUrls?: readonly string[] | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Keep owned media on the current frontend without changing external assets. */
|
|
11
|
+
export declare function rewriteFlowpressMediaUrl(value: string, options: FlowpressFrontendMediaOptions): string;
|
|
12
|
+
/** Normalize JSON media data, including nested query items and responsive sources.
|
|
13
|
+
* HTML, CSS and scripts are left intact: only URL values and srcset fields change.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rewriteFlowpressMediaUrls<T>(value: T, options: FlowpressFrontendMediaOptions): T;
|
|
3
16
|
export declare function isFlowpressUploadPath(pathname: string): boolean;
|
|
4
17
|
export declare function isFlowpressMediaPath(pathname: string): boolean;
|
|
5
18
|
export declare function rewriteFlowpressBackendUrl(value: string, backendUrl: string, frontendOrigin?: string): string;
|
|
6
19
|
export declare function proxyFlowpressUpload(request: Request, backendUrl: string, fetchImpl?: typeof fetch): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Proxy /media/<path> to the current FLOWPRESS_MEDIA origin (an offload CDN,
|
|
22
|
+
* or the WordPress backend as a fallback). /media/ is a frontend-only routing
|
|
23
|
+
* marker with no equivalent on either upstream, so it is stripped before the
|
|
24
|
+
* request is forwarded: /media/2026/09/x.jpg becomes <origin>/2026/09/x.jpg.
|
|
25
|
+
*/
|
|
7
26
|
export declare function proxyFlowpressMedia(request: Request, mediaUrl: string, fetchImpl?: typeof fetch): Promise<Response>;
|
|
8
27
|
//# sourceMappingURL=media.d.ts.map
|
package/dist/media.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AACxD,eAAO,MAAM,sBAAsB,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AACxD,eAAO,MAAM,sBAAsB,YAAY,CAAC;AAEhD,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,yEAAyE;IACzE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9C;AAED,iFAAiF;AACjF,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,6BAA6B,GACrC,MAAM,CAuBR;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EACzC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,6BAA6B,GACrC,CAAC,CAkBH;AAwBD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG/D;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,cAAc,SAAK,GAClB,MAAM,CASR;AAoED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAMnB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAMnB"}
|
package/dist/media.js
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
export const FLOWPRESS_UPLOADS_PREFIX = "/app/uploads/";
|
|
2
2
|
export const FLOWPRESS_MEDIA_PREFIX = "/media/";
|
|
3
|
+
/** Keep owned media on the current frontend without changing external assets. */
|
|
4
|
+
export function rewriteFlowpressMediaUrl(value, options) {
|
|
5
|
+
try {
|
|
6
|
+
if (!value || /\s/.test(value))
|
|
7
|
+
return value;
|
|
8
|
+
const frontend = new URL(options.frontendOrigin);
|
|
9
|
+
const source = new URL(value, frontend);
|
|
10
|
+
if (!/^https?:$/.test(source.protocol) || source.username || source.password)
|
|
11
|
+
return value;
|
|
12
|
+
const backend = options.backendUrl ? new URL(options.backendUrl) : undefined;
|
|
13
|
+
const media = options.mediaUrl ? new URL(options.mediaUrl) : undefined;
|
|
14
|
+
const origins = new Set([frontend.origin, backend?.origin, ...(options.frontendUrls ?? []).map((url) => new URL(url).origin)]);
|
|
15
|
+
let pathname;
|
|
16
|
+
if (origins.has(source.origin) && (isFlowpressUploadPath(source.pathname) || isFlowpressMediaPath(source.pathname))) {
|
|
17
|
+
pathname = source.pathname;
|
|
18
|
+
}
|
|
19
|
+
else if (media && media.origin !== backend?.origin && source.origin === media.origin) {
|
|
20
|
+
const base = media.pathname.replace(/\/+$/, "");
|
|
21
|
+
if (base && !source.pathname.startsWith(`${base}/`))
|
|
22
|
+
return value;
|
|
23
|
+
pathname = `${FLOWPRESS_MEDIA_PREFIX.slice(0, -1)}${source.pathname.slice(base.length)}`;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
return `${frontend.origin}${pathname}${source.search}${source.hash}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Normalize JSON media data, including nested query items and responsive sources.
|
|
35
|
+
* HTML, CSS and scripts are left intact: only URL values and srcset fields change.
|
|
36
|
+
*/
|
|
37
|
+
export function rewriteFlowpressMediaUrls(value, options) {
|
|
38
|
+
function rewrite(item, key = "") {
|
|
39
|
+
if (typeof item === "string") {
|
|
40
|
+
if (/^srcset$/i.test(key)) {
|
|
41
|
+
return item.replace(/\S+/g, (token) => {
|
|
42
|
+
const url = token.replace(/,+$/, "");
|
|
43
|
+
return `${rewriteFlowpressMediaUrl(url, options)}${token.slice(url.length)}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return rewriteFlowpressMediaUrl(item, options);
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(item))
|
|
49
|
+
return item.map((entry) => rewrite(entry));
|
|
50
|
+
if (item && typeof item === "object") {
|
|
51
|
+
return Object.fromEntries(Object.entries(item).map(([name, entry]) => [name, rewrite(entry, name)]));
|
|
52
|
+
}
|
|
53
|
+
return item;
|
|
54
|
+
}
|
|
55
|
+
return rewrite(value);
|
|
56
|
+
}
|
|
3
57
|
const requestHeaders = [
|
|
4
58
|
"accept",
|
|
5
59
|
"if-match",
|
|
@@ -49,7 +103,7 @@ async function proxyFlowpressAsset(request, upstreamUrl, fetchImpl, options) {
|
|
|
49
103
|
}
|
|
50
104
|
let target;
|
|
51
105
|
try {
|
|
52
|
-
target = assetTarget(request.url, upstreamUrl, options.prefix, options.preserveUpstreamPath);
|
|
106
|
+
target = assetTarget(request.url, upstreamUrl, options.prefix, options.preserveUpstreamPath, options.stripPrefix);
|
|
53
107
|
}
|
|
54
108
|
catch {
|
|
55
109
|
return new Response("Bad Gateway", { status: 502 });
|
|
@@ -61,20 +115,28 @@ async function proxyFlowpressAsset(request, upstreamUrl, fetchImpl, options) {
|
|
|
61
115
|
headers.set(name, value);
|
|
62
116
|
}
|
|
63
117
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
// Follow storage redirects on the server: Location must never reveal the
|
|
119
|
+
// backend/CDN origin to the browser or move it off the public media URL.
|
|
120
|
+
let upstream;
|
|
121
|
+
for (let redirects = 0;; redirects++) {
|
|
122
|
+
upstream = await fetchImpl(target, { method: request.method, headers, redirect: "manual" });
|
|
123
|
+
const location = upstream.headers.get("location");
|
|
124
|
+
if (![301, 302, 303, 307, 308].includes(upstream.status) || !location)
|
|
125
|
+
break;
|
|
126
|
+
await upstream.body?.cancel();
|
|
127
|
+
if (redirects >= 5)
|
|
128
|
+
throw new Error("Too many media redirects.");
|
|
129
|
+
target = new URL(location, target);
|
|
130
|
+
if (!/^https?:$/.test(target.protocol) || target.username || target.password) {
|
|
131
|
+
throw new Error("Invalid media redirect.");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
69
134
|
const safeHeaders = new Headers();
|
|
70
135
|
for (const name of responseHeaders) {
|
|
71
136
|
const value = upstream.headers.get(name);
|
|
72
137
|
if (value)
|
|
73
138
|
safeHeaders.set(name, value);
|
|
74
139
|
}
|
|
75
|
-
const location = upstream.headers.get("location");
|
|
76
|
-
if (location)
|
|
77
|
-
safeHeaders.set("location", options.rewriteLocation(location, target));
|
|
78
140
|
return new Response(request.method === "HEAD" ? null : upstream.body, {
|
|
79
141
|
status: upstream.status,
|
|
80
142
|
statusText: upstream.statusText,
|
|
@@ -89,17 +151,23 @@ export function proxyFlowpressUpload(request, backendUrl, fetchImpl = fetch) {
|
|
|
89
151
|
return proxyFlowpressAsset(request, backendUrl, fetchImpl, {
|
|
90
152
|
prefix: FLOWPRESS_UPLOADS_PREFIX,
|
|
91
153
|
preserveUpstreamPath: false,
|
|
92
|
-
|
|
154
|
+
stripPrefix: false
|
|
93
155
|
});
|
|
94
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Proxy /media/<path> to the current FLOWPRESS_MEDIA origin (an offload CDN,
|
|
159
|
+
* or the WordPress backend as a fallback). /media/ is a frontend-only routing
|
|
160
|
+
* marker with no equivalent on either upstream, so it is stripped before the
|
|
161
|
+
* request is forwarded: /media/2026/09/x.jpg becomes <origin>/2026/09/x.jpg.
|
|
162
|
+
*/
|
|
95
163
|
export function proxyFlowpressMedia(request, mediaUrl, fetchImpl = fetch) {
|
|
96
164
|
return proxyFlowpressAsset(request, mediaUrl, fetchImpl, {
|
|
97
165
|
prefix: FLOWPRESS_MEDIA_PREFIX,
|
|
98
166
|
preserveUpstreamPath: true,
|
|
99
|
-
|
|
167
|
+
stripPrefix: true
|
|
100
168
|
});
|
|
101
169
|
}
|
|
102
|
-
function assetTarget(requestUrl, upstreamUrl, prefix, preserveUpstreamPath) {
|
|
170
|
+
function assetTarget(requestUrl, upstreamUrl, prefix, preserveUpstreamPath, stripPrefix) {
|
|
103
171
|
const request = new URL(requestUrl);
|
|
104
172
|
if (request.pathname !== prefix.slice(0, -1) && !request.pathname.startsWith(prefix)) {
|
|
105
173
|
throw new Error("Unsupported FlowPress asset path.");
|
|
@@ -112,9 +180,12 @@ function assetTarget(requestUrl, upstreamUrl, prefix, preserveUpstreamPath) {
|
|
|
112
180
|
|| upstream.hash) {
|
|
113
181
|
throw new Error("Invalid FlowPress asset upstream.");
|
|
114
182
|
}
|
|
183
|
+
const requestPath = stripPrefix
|
|
184
|
+
? request.pathname.slice(prefix.length - 1) || "/"
|
|
185
|
+
: request.pathname;
|
|
115
186
|
const target = new URL(upstream.origin);
|
|
116
187
|
const upstreamPath = preserveUpstreamPath ? upstream.pathname.replace(/\/+$/, "") : "";
|
|
117
|
-
target.pathname = `${upstreamPath}${
|
|
188
|
+
target.pathname = `${upstreamPath}${requestPath}`;
|
|
118
189
|
target.search = request.search;
|
|
119
190
|
return target;
|
|
120
191
|
}
|
package/dist/media.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"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","sourcesContent":["export const FLOWPRESS_UPLOADS_PREFIX = \"/app/uploads/\";\nexport const FLOWPRESS_MEDIA_PREFIX = \"/media/\";\n\nconst requestHeaders = [\n \"accept\",\n \"if-match\",\n \"if-modified-since\",\n \"if-none-match\",\n \"if-unmodified-since\",\n \"range\"\n] as const;\n\nconst responseHeaders = [\n \"accept-ranges\",\n \"cache-control\",\n \"content-disposition\",\n \"content-encoding\",\n \"content-length\",\n \"content-range\",\n \"content-type\",\n \"etag\",\n \"last-modified\",\n \"vary\"\n] as const;\n\nexport function isFlowpressUploadPath(pathname: string): boolean {\n return pathname === FLOWPRESS_UPLOADS_PREFIX.slice(0, -1)\n || pathname.startsWith(FLOWPRESS_UPLOADS_PREFIX);\n}\n\nexport function isFlowpressMediaPath(pathname: string): boolean {\n return pathname === FLOWPRESS_MEDIA_PREFIX.slice(0, -1)\n || pathname.startsWith(FLOWPRESS_MEDIA_PREFIX);\n}\n\nexport function rewriteFlowpressBackendUrl(\n value: string,\n backendUrl: string,\n frontendOrigin = \"\"\n): string {\n try {\n const source = new URL(value);\n const backend = new URL(backendUrl);\n if (source.origin !== backend.origin) return value;\n return `${frontendOrigin.replace(/\\/$/, \"\")}${source.pathname}${source.search}${source.hash}`;\n } catch {\n return value;\n }\n}\n\ninterface AssetProxyOptions {\n prefix: string;\n preserveUpstreamPath: boolean;\n rewriteLocation: (location: string, target: URL) => string;\n}\n\nasync function proxyFlowpressAsset(\n request: Request,\n upstreamUrl: string,\n fetchImpl: typeof fetch,\n options: AssetProxyOptions\n): Promise<Response> {\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n return new Response(\"Method Not Allowed\", {\n status: 405,\n headers: { allow: \"GET, HEAD\" }\n });\n }\n let target: URL;\n try {\n target = assetTarget(request.url, upstreamUrl, options.prefix, options.preserveUpstreamPath);\n } catch {\n return new Response(\"Bad Gateway\", { status: 502 });\n }\n\n const headers = new Headers();\n for (const name of requestHeaders) {\n const value = request.headers.get(name);\n if (value) headers.set(name, value);\n }\n try {\n const upstream = await fetchImpl(target, {\n method: request.method,\n headers,\n redirect: \"manual\"\n });\n const safeHeaders = new Headers();\n for (const name of responseHeaders) {\n const value = upstream.headers.get(name);\n if (value) safeHeaders.set(name, value);\n }\n const location = upstream.headers.get(\"location\");\n if (location) safeHeaders.set(\"location\", options.rewriteLocation(location, target));\n return new Response(request.method === \"HEAD\" ? null : upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: safeHeaders\n });\n } catch {\n return new Response(\"Bad Gateway\", { status: 502 });\n }\n}\n\nexport function proxyFlowpressUpload(\n request: Request,\n backendUrl: string,\n fetchImpl: typeof fetch = fetch\n): Promise<Response> {\n return proxyFlowpressAsset(request, backendUrl, fetchImpl, {\n prefix: FLOWPRESS_UPLOADS_PREFIX,\n preserveUpstreamPath: false,\n rewriteLocation: (location) => rewriteFlowpressBackendUrl(location, backendUrl)\n });\n}\n\nexport function proxyFlowpressMedia(\n request: Request,\n mediaUrl: string,\n fetchImpl: typeof fetch = fetch\n): Promise<Response> {\n return proxyFlowpressAsset(request, mediaUrl, fetchImpl, {\n prefix: FLOWPRESS_MEDIA_PREFIX,\n preserveUpstreamPath: true,\n rewriteLocation: (location, target) => new URL(location, target).toString()\n });\n}\n\nfunction assetTarget(\n requestUrl: string,\n upstreamUrl: string,\n prefix: string,\n preserveUpstreamPath: boolean\n): URL {\n const request = new URL(requestUrl);\n if (request.pathname !== prefix.slice(0, -1) && !request.pathname.startsWith(prefix)) {\n throw new Error(\"Unsupported FlowPress asset path.\");\n }\n const upstream = new URL(upstreamUrl);\n if (\n !/^https?:$/.test(upstream.protocol)\n || upstream.username\n || upstream.password\n || upstream.search\n || upstream.hash\n ) {\n throw new Error(\"Invalid FlowPress asset upstream.\");\n }\n const target = new URL(upstream.origin);\n const upstreamPath = preserveUpstreamPath ? upstream.pathname.replace(/\\/+$/, \"\") : \"\";\n target.pathname = `${upstreamPath}${request.pathname}`;\n target.search = request.search;\n return target;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"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","sourcesContent":["export const FLOWPRESS_UPLOADS_PREFIX = \"/app/uploads/\";\nexport const FLOWPRESS_MEDIA_PREFIX = \"/media/\";\n\nexport interface FlowpressFrontendMediaOptions {\n frontendOrigin: string;\n backendUrl?: string | undefined;\n mediaUrl?: string | undefined;\n /** Canonical frontend URLs and aliases owned by the selected backend. */\n frontendUrls?: readonly string[] | undefined;\n}\n\n/** Keep owned media on the current frontend without changing external assets. */\nexport function rewriteFlowpressMediaUrl(\n value: string,\n options: FlowpressFrontendMediaOptions\n): string {\n try {\n if (!value || /\\s/.test(value)) return value;\n const frontend = new URL(options.frontendOrigin);\n const source = new URL(value, frontend);\n if (!/^https?:$/.test(source.protocol) || source.username || source.password) return value;\n const backend = options.backendUrl ? new URL(options.backendUrl) : undefined;\n const media = options.mediaUrl ? new URL(options.mediaUrl) : undefined;\n const origins = new Set([frontend.origin, backend?.origin, ...(options.frontendUrls ?? []).map((url) => new URL(url).origin)]);\n let pathname: string;\n if (origins.has(source.origin) && (isFlowpressUploadPath(source.pathname) || isFlowpressMediaPath(source.pathname))) {\n pathname = source.pathname;\n } else if (media && media.origin !== backend?.origin && source.origin === media.origin) {\n const base = media.pathname.replace(/\\/+$/, \"\");\n if (base && !source.pathname.startsWith(`${base}/`)) return value;\n pathname = `${FLOWPRESS_MEDIA_PREFIX.slice(0, -1)}${source.pathname.slice(base.length)}`;\n } else {\n return value;\n }\n return `${frontend.origin}${pathname}${source.search}${source.hash}`;\n } catch {\n return value;\n }\n}\n\n/** Normalize JSON media data, including nested query items and responsive sources.\n * HTML, CSS and scripts are left intact: only URL values and srcset fields change.\n */\nexport function rewriteFlowpressMediaUrls<T>(\n value: T,\n options: FlowpressFrontendMediaOptions\n): T {\n function rewrite(item: unknown, key = \"\"): unknown {\n if (typeof item === \"string\") {\n if (/^srcset$/i.test(key)) {\n return item.replace(/\\S+/g, (token) => {\n const url = token.replace(/,+$/, \"\");\n return `${rewriteFlowpressMediaUrl(url, options)}${token.slice(url.length)}`;\n });\n }\n return rewriteFlowpressMediaUrl(item, options);\n }\n if (Array.isArray(item)) return item.map((entry) => rewrite(entry));\n if (item && typeof item === \"object\") {\n return Object.fromEntries(Object.entries(item).map(([name, entry]) => [name, rewrite(entry, name)]));\n }\n return item;\n }\n return rewrite(value) as T;\n}\n\nconst requestHeaders = [\n \"accept\",\n \"if-match\",\n \"if-modified-since\",\n \"if-none-match\",\n \"if-unmodified-since\",\n \"range\"\n] as const;\n\nconst responseHeaders = [\n \"accept-ranges\",\n \"cache-control\",\n \"content-disposition\",\n \"content-encoding\",\n \"content-length\",\n \"content-range\",\n \"content-type\",\n \"etag\",\n \"last-modified\",\n \"vary\"\n] as const;\n\nexport function isFlowpressUploadPath(pathname: string): boolean {\n return pathname === FLOWPRESS_UPLOADS_PREFIX.slice(0, -1)\n || pathname.startsWith(FLOWPRESS_UPLOADS_PREFIX);\n}\n\nexport function isFlowpressMediaPath(pathname: string): boolean {\n return pathname === FLOWPRESS_MEDIA_PREFIX.slice(0, -1)\n || pathname.startsWith(FLOWPRESS_MEDIA_PREFIX);\n}\n\nexport function rewriteFlowpressBackendUrl(\n value: string,\n backendUrl: string,\n frontendOrigin = \"\"\n): string {\n try {\n const source = new URL(value);\n const backend = new URL(backendUrl);\n if (source.origin !== backend.origin) return value;\n return `${frontendOrigin.replace(/\\/$/, \"\")}${source.pathname}${source.search}${source.hash}`;\n } catch {\n return value;\n }\n}\n\ninterface AssetProxyOptions {\n prefix: string;\n preserveUpstreamPath: boolean;\n stripPrefix: boolean;\n}\n\nasync function proxyFlowpressAsset(\n request: Request,\n upstreamUrl: string,\n fetchImpl: typeof fetch,\n options: AssetProxyOptions\n): Promise<Response> {\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n return new Response(\"Method Not Allowed\", {\n status: 405,\n headers: { allow: \"GET, HEAD\" }\n });\n }\n let target: URL;\n try {\n target = assetTarget(\n request.url,\n upstreamUrl,\n options.prefix,\n options.preserveUpstreamPath,\n options.stripPrefix\n );\n } catch {\n return new Response(\"Bad Gateway\", { status: 502 });\n }\n\n const headers = new Headers();\n for (const name of requestHeaders) {\n const value = request.headers.get(name);\n if (value) headers.set(name, value);\n }\n try {\n // Follow storage redirects on the server: Location must never reveal the\n // backend/CDN origin to the browser or move it off the public media URL.\n let upstream: Response;\n for (let redirects = 0; ; redirects++) {\n upstream = await fetchImpl(target, { method: request.method, headers, redirect: \"manual\" });\n const location = upstream.headers.get(\"location\");\n if (![301, 302, 303, 307, 308].includes(upstream.status) || !location) break;\n await upstream.body?.cancel();\n if (redirects >= 5) throw new Error(\"Too many media redirects.\");\n target = new URL(location, target);\n if (!/^https?:$/.test(target.protocol) || target.username || target.password) {\n throw new Error(\"Invalid media redirect.\");\n }\n }\n const safeHeaders = new Headers();\n for (const name of responseHeaders) {\n const value = upstream.headers.get(name);\n if (value) safeHeaders.set(name, value);\n }\n return new Response(request.method === \"HEAD\" ? null : upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: safeHeaders\n });\n } catch {\n return new Response(\"Bad Gateway\", { status: 502 });\n }\n}\n\nexport function proxyFlowpressUpload(\n request: Request,\n backendUrl: string,\n fetchImpl: typeof fetch = fetch\n): Promise<Response> {\n return proxyFlowpressAsset(request, backendUrl, fetchImpl, {\n prefix: FLOWPRESS_UPLOADS_PREFIX,\n preserveUpstreamPath: false,\n stripPrefix: false\n });\n}\n\n/**\n * Proxy /media/<path> to the current FLOWPRESS_MEDIA origin (an offload CDN,\n * or the WordPress backend as a fallback). /media/ is a frontend-only routing\n * marker with no equivalent on either upstream, so it is stripped before the\n * request is forwarded: /media/2026/09/x.jpg becomes <origin>/2026/09/x.jpg.\n */\nexport function proxyFlowpressMedia(\n request: Request,\n mediaUrl: string,\n fetchImpl: typeof fetch = fetch\n): Promise<Response> {\n return proxyFlowpressAsset(request, mediaUrl, fetchImpl, {\n prefix: FLOWPRESS_MEDIA_PREFIX,\n preserveUpstreamPath: true,\n stripPrefix: true\n });\n}\n\nfunction assetTarget(\n requestUrl: string,\n upstreamUrl: string,\n prefix: string,\n preserveUpstreamPath: boolean,\n stripPrefix: boolean\n): URL {\n const request = new URL(requestUrl);\n if (request.pathname !== prefix.slice(0, -1) && !request.pathname.startsWith(prefix)) {\n throw new Error(\"Unsupported FlowPress asset path.\");\n }\n const upstream = new URL(upstreamUrl);\n if (\n !/^https?:$/.test(upstream.protocol)\n || upstream.username\n || upstream.password\n || upstream.search\n || upstream.hash\n ) {\n throw new Error(\"Invalid FlowPress asset upstream.\");\n }\n const requestPath = stripPrefix\n ? request.pathname.slice(prefix.length - 1) || \"/\"\n : request.pathname;\n const target = new URL(upstream.origin);\n const upstreamPath = preserveUpstreamPath ? upstream.pathname.replace(/\\/+$/, \"\") : \"\";\n target.pathname = `${upstreamPath}${requestPath}`;\n target.search = request.search;\n return target;\n}\n"]}
|
package/package.json
CHANGED