@styleflow.app/flowpress-astro-core 1.0.0-beta.15 → 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,17 @@
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
+
5
16
  ## 1.0.0-beta.15
6
17
 
7
18
  - Fixed `proxyFlowpressMedia` forwarding the `/media/` route prefix to the
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.15 \
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`,
@@ -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,5 +1,18 @@
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;
@@ -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;AA8DD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAOnB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAOnB"}
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",
@@ -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,8 +151,7 @@ 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
- stripPrefix: false,
93
- rewriteLocation: (location) => rewriteFlowpressBackendUrl(location, backendUrl)
154
+ stripPrefix: false
94
155
  });
95
156
  }
96
157
  /**
@@ -103,8 +164,7 @@ export function proxyFlowpressMedia(request, mediaUrl, fetchImpl = fetch) {
103
164
  return proxyFlowpressAsset(request, mediaUrl, fetchImpl, {
104
165
  prefix: FLOWPRESS_MEDIA_PREFIX,
105
166
  preserveUpstreamPath: true,
106
- stripPrefix: true,
107
- rewriteLocation: (location, target) => new URL(location, target).toString()
167
+ stripPrefix: true
108
168
  });
109
169
  }
110
170
  function assetTarget(requestUrl, upstreamUrl, prefix, preserveUpstreamPath, stripPrefix) {
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 stripPrefix: 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(\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 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 stripPrefix: false,\n rewriteLocation: (location) => rewriteFlowpressBackendUrl(location, backendUrl)\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 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 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"]}
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,wBAAwB,GAAG,eAAe,CAAC;AACxD,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC;AAUhD,iFAAiF;AACjF,MAAM,UAAU,wBAAwB,CACtC,KAAa,EACb,OAAsC;IAEtC,IAAI,CAAC;QACH,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC3F,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7E,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/H,IAAI,QAAgB,CAAC;QACrB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACpH,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC7B,CAAC;aAAM,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;YACvF,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAChD,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;YAClE,QAAQ,GAAG,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3F,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAQ,EACR,OAAsC;IAEtC,SAAS,OAAO,CAAC,IAAa,EAAE,GAAG,GAAG,EAAE;QACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;oBACpC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACrC,OAAO,GAAG,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/E,CAAC,CAAC,CAAC;YACL,CAAC;YACD,OAAO,wBAAwB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACpE,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACvG,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAM,CAAC;AAC7B,CAAC;AAED,MAAM,cAAc,GAAG;IACrB,QAAQ;IACR,UAAU;IACV,mBAAmB;IACnB,eAAe;IACf,qBAAqB;IACrB,OAAO;CACC,CAAC;AAEX,MAAM,eAAe,GAAG;IACtB,eAAe;IACf,eAAe;IACf,qBAAqB;IACrB,kBAAkB;IAClB,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,MAAM;IACN,eAAe;IACf,MAAM;CACE,CAAC;AAEX,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,OAAO,QAAQ,KAAK,wBAAwB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;WACpD,QAAQ,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,OAAO,QAAQ,KAAK,sBAAsB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;WAClD,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,KAAa,EACb,UAAkB,EAClB,cAAc,GAAG,EAAE;IAEnB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACnD,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAChG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAQD,KAAK,UAAU,mBAAmB,CAChC,OAAgB,EAChB,WAAmB,EACnB,SAAuB,EACvB,OAA0B;IAE1B,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC1D,OAAO,IAAI,QAAQ,CAAC,oBAAoB,EAAE;YACxC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE;SAChC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,WAAW,CAClB,OAAO,CAAC,GAAG,EACX,WAAW,EACX,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,oBAAoB,EAC5B,OAAO,CAAC,WAAW,CACpB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,CAAC;QACH,yEAAyE;QACzE,yEAAyE;QACzE,IAAI,QAAkB,CAAC;QACvB,KAAK,IAAI,SAAS,GAAG,CAAC,GAAI,SAAS,EAAE,EAAE,CAAC;YACtC,QAAQ,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC5F,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAClD,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ;gBAAE,MAAM;YAC7E,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;YAC9B,IAAI,SAAS,IAAI,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;YACjE,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACnC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC7E,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,OAAO,EAAE,CAAC;QAClC,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,KAAK;gBAAE,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;YACpE,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,WAAW;SACrB,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,OAAgB,EAChB,UAAkB,EAClB,YAA0B,KAAK;IAE/B,OAAO,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE;QACzD,MAAM,EAAE,wBAAwB;QAChC,oBAAoB,EAAE,KAAK;QAC3B,WAAW,EAAE,KAAK;KACnB,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,QAAgB,EAChB,YAA0B,KAAK;IAE/B,OAAO,mBAAmB,CAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE;QACvD,MAAM,EAAE,sBAAsB;QAC9B,oBAAoB,EAAE,IAAI;QAC1B,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,WAAW,CAClB,UAAkB,EAClB,WAAmB,EACnB,MAAc,EACd,oBAA6B,EAC7B,WAAoB;IAEpB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,OAAO,CAAC,QAAQ,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACrF,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IACtC,IACE,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;WACjC,QAAQ,CAAC,QAAQ;WACjB,QAAQ,CAAC,QAAQ;WACjB,QAAQ,CAAC,MAAM;WACf,QAAQ,CAAC,IAAI,EAChB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,WAAW,GAAG,WAAW;QAC7B,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG;QAClD,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACrB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,YAAY,GAAG,oBAAoB,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvF,MAAM,CAAC,QAAQ,GAAG,GAAG,YAAY,GAAG,WAAW,EAAE,CAAC;IAClD,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,OAAO,MAAM,CAAC;AAChB,CAAC","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.15",
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,