@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 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.13 \
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.13` on `next` only after the exact Styleflow prerelease is
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 = composition.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
@@ -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;AAwBhD,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;AAuDD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAMnB;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAMnB"}
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
- const upstream = await fetchImpl(target, {
65
- method: request.method,
66
- headers,
67
- redirect: "manual"
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
- rewriteLocation: (location) => rewriteFlowpressBackendUrl(location, backendUrl)
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
- rewriteLocation: (location, target) => new URL(location, target).toString()
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}${request.pathname}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@styleflow.app/flowpress-astro-core",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.16",
4
4
  "description": "Project-agnostic WordPress routing, templates, SEO and GraphQL runtime for FlowPress Astro apps.",
5
5
  "type": "module",
6
6
  "private": false,