@alpaca-headless/dynamic-renderings 1.0.4694 → 1.0.4696

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,8 @@ Optional dynamic renderings for Alpaca Headless: React components stored as
4
4
  source on Sitecore items, compiled and run by the host itself through
5
5
  `@alpaca-headless/dynamic-renderings-engine` (compile threads and QuickJS render threads inside the
6
6
  head's process), and turned back into React. Design and threat model:
7
- `Headless/dynamic-renderings.md` in the Alpaca repository. No Parhelia installation is required.
7
+ `Headless/dynamic-renderings.md` in the Alpaca repository. No Parhelia installation is required; a head
8
+ the Parhelia editor serves adds the `/parhelia` subpaths (see below).
8
9
 
9
10
  ## Requirements
10
11
 
@@ -125,6 +126,30 @@ Editor integrations configure the rest in `defineDynamicRenderings`:
125
126
  - `editor.clientCodeHintAttribute`: in preview mode, `dynamic.head` lists the renderings with
126
127
  client code in `<script type="application/json" {attribute}="{id},...">`.
127
128
 
129
+ ## Heads the Parhelia editor serves
130
+
131
+ Everything Parhelia-specific lives under `/parhelia`: the editor's markers, the CM's proxy
132
+ prefixes (`/parhelia/proxy/{name}/...`, `/parhelia/preview/{token}/{name}/...`) and the sealed
133
+ "Run client code" preview (its token as the request's identity, and its media). Use its
134
+ `defineDynamicRenderings` in place of the one from `/server`, and its `withDynamicRenderings`:
135
+
136
+ ```ts
137
+ // src/dynamic.config.ts
138
+ import { defineDynamicRenderings } from "@alpaca-headless/dynamic-renderings/parhelia";
139
+
140
+ // src/app/api/parhelia/media/[code]/[...path]/route.ts
141
+ import { previewMediaRoute } from "@alpaca-headless/dynamic-renderings/parhelia/next";
142
+ export const { GET } = previewMediaRoute(dynamicConfig);
143
+ ```
144
+
145
+ ```js
146
+ // next.config.js
147
+ const { withDynamicRenderings } = require("@alpaca-headless/dynamic-renderings/parhelia/next/config");
148
+ ```
149
+
150
+ Everything else (`readRequest`, `buildRoute`, `clientRoute`, `createRenderer`) comes from the
151
+ subpaths below as in any head.
152
+
128
153
  ## Subpaths
129
154
 
130
155
  | Subpath | Contents | Loads in |
@@ -139,6 +164,9 @@ Editor integrations configure the rest in `defineDynamicRenderings`:
139
164
  | `/handlers/rewrites` | the CORS exclusion source and the bundle route rewrites | plain Node |
140
165
  | `/next` | `readRequest`, `requestIdentity`, `buildRoute`, `clientRoute`, `nextThemeFile` | Next.js server |
141
166
  | `/next/config` | `withDynamicRenderings` (CommonJS and ESM) | plain Node |
167
+ | `/parhelia` | the Parhelia editor preset: `defineDynamicRenderings`, the preview token's `requestAuth`, `rewriteMediaUrls`, `splitProxyPrefix`, `handlePreviewMedia` | server |
168
+ | `/parhelia/next` | `previewMediaRoute` | Next.js server |
169
+ | `/parhelia/next/config` | `withDynamicRenderings` with the Parhelia CM's proxy prefixes and the preview media rewrite | plain Node |
142
170
 
143
171
  ## Versions
144
172
 
@@ -0,0 +1,22 @@
1
+ import { type DynamicRenderingsConfig, type DynamicRenderingsOptions } from "../server/config.js";
2
+ export { splitProxyPrefix } from "./prefix.js";
3
+ export { rewriteMediaUrls } from "./preview-media.js";
4
+ export { handlePreviewMedia } from "./media.js";
5
+ /** The header the CM's preview proxy adds to every request of the sealed preview frame. */
6
+ export declare const PREVIEW_TOKEN_HEADER = "x-parhelia-preview-token";
7
+ /**
8
+ * The sealed preview ("Run client code"): a request carrying the preview token reads the CM as the
9
+ * token's subject and nothing else. The proxy strips every cookie; the token is the whole identity.
10
+ */
11
+ export declare function parheliaRequestAuth(headers: Headers): {
12
+ headers: {
13
+ "X-Parhelia-Preview-Token": string;
14
+ };
15
+ } | null;
16
+ /**
17
+ * defineDynamicRenderings as the Parhelia editor needs it: its nonce and slot markers, the
18
+ * client-code hint for the preview strip, pages served through the CM's proxies
19
+ * (/parhelia/proxy/{name}/..., /parhelia/preview/{token}/{name}/...), and the sealed preview's
20
+ * token identity and media URLs. Options given here win.
21
+ */
22
+ export declare function defineDynamicRenderings(options: DynamicRenderingsOptions): DynamicRenderingsConfig;
@@ -0,0 +1,42 @@
1
+ // Dynamic renderings in a head the Parhelia CM and editor serve. Everything Parhelia-specific about
2
+ // the runtime is here: the editor's markers, the CM's proxy prefixes and the sealed "Run client
3
+ // code" preview. The rest of the package knows no editor.
4
+ import { defineDynamicRenderings as defineAlpacaDynamicRenderings } from "../server/config.js";
5
+ import { splitProxyPrefix } from "./prefix.js";
6
+ import { rewriteMediaUrls } from "./preview-media.js";
7
+ export { splitProxyPrefix } from "./prefix.js";
8
+ export { rewriteMediaUrls } from "./preview-media.js";
9
+ export { handlePreviewMedia } from "./media.js";
10
+ /** The header the CM's preview proxy adds to every request of the sealed preview frame. */
11
+ export const PREVIEW_TOKEN_HEADER = "x-parhelia-preview-token";
12
+ /**
13
+ * The sealed preview ("Run client code"): a request carrying the preview token reads the CM as the
14
+ * token's subject and nothing else. The proxy strips every cookie; the token is the whole identity.
15
+ */
16
+ export function parheliaRequestAuth(headers) {
17
+ const token = headers.get(PREVIEW_TOKEN_HEADER);
18
+ return token ? { headers: { "X-Parhelia-Preview-Token": token } } : null;
19
+ }
20
+ /**
21
+ * defineDynamicRenderings as the Parhelia editor needs it: its nonce and slot markers, the
22
+ * client-code hint for the preview strip, pages served through the CM's proxies
23
+ * (/parhelia/proxy/{name}/..., /parhelia/preview/{token}/{name}/...), and the sealed preview's
24
+ * token identity and media URLs. Options given here win.
25
+ */
26
+ export function defineDynamicRenderings(options) {
27
+ return defineAlpacaDynamicRenderings({
28
+ editor: { nonceAttribute: "data-parhelia-nonce", slotAttribute: "data-parhelia-slot", clientCodeHintAttribute: "data-parhelia-has-client-code" },
29
+ // The editor appends ?parhelia=1 in both edit and preview mode, so edit mode is only forced
30
+ // when the requested mode is not explicitly preview or normal. A sealed request is never an
31
+ // editor frame; readRequest overrides this for it.
32
+ requestAdapter(path, search) {
33
+ const editorFrame = search.parhelia === "1";
34
+ return { ...splitProxyPrefix(path), editorFrame, editing: editorFrame && search.mode !== "preview" && search.mode !== "normal" };
35
+ },
36
+ requestAuth: parheliaRequestAuth,
37
+ // The sealed frame sends no cookies, so its media URLs go through the head's media route,
38
+ // which reads them from the CM with the preview token.
39
+ transformPage: (page, { sealed, prefix }) => (sealed ? rewriteMediaUrls(page, prefix) : page),
40
+ ...options,
41
+ });
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { DynamicRenderingsConfig } from "../server/config.js";
2
+ /** GET /api/parhelia/media/{code}/{...path}: one media file of the sealed preview. */
3
+ export declare function handlePreviewMedia(config: DynamicRenderingsConfig, request: Request, code: string, path: string[]): Promise<Response>;
@@ -0,0 +1,37 @@
1
+ // Media for the sealed preview. That frame has an opaque origin and sends no cookies, so its
2
+ // page's media URLs point here (see server/preview-media.ts), and this route reads the original
3
+ // media path from the CM with the preview token. The CM serves it as the token's subject with the
4
+ // media item's own access rules. Reached only through the CM's preview proxy, which adds the token
5
+ // header; without it the route answers 404.
6
+ const PREFIXES = {
7
+ s: "/sitecore/shell/-/media/",
8
+ m: "/-/media/",
9
+ j: "/-/jssmedia/",
10
+ };
11
+ const PASS_HEADERS = ["content-type", "content-length", "content-range", "accept-ranges", "last-modified", "etag"];
12
+ /** GET /api/parhelia/media/{code}/{...path}: one media file of the sealed preview. */
13
+ export async function handlePreviewMedia(config, request, code, path) {
14
+ const auth = config.requestAuth?.(request.headers) ?? null;
15
+ const prefix = PREFIXES[code];
16
+ if (!auth || !prefix || !path?.length || path.some((segment) => !segment || segment === "." || segment === ".." || /[\\\0]/.test(segment))) {
17
+ return new Response(null, { status: 404 });
18
+ }
19
+ const headers = new Headers(auth.headers);
20
+ const range = request.headers.get("range");
21
+ if (range)
22
+ headers.set("range", range);
23
+ const search = new URL(request.url).search;
24
+ // Redirects (a login page) are failures, never followed.
25
+ const upstream = await fetch(config.layoutServiceUrl() + prefix + path.map(encodeURIComponent).join("/") + search, { headers, redirect: "manual", cache: "no-store" });
26
+ const type = upstream.headers.get("content-type") || "";
27
+ if ((upstream.status !== 200 && upstream.status !== 206) || type.includes("text/html")) {
28
+ return new Response(null, { status: upstream.status === 401 || upstream.status === 403 ? upstream.status : 404 });
29
+ }
30
+ const out = new Headers({ "cache-control": "private, max-age=300", "x-content-type-options": "nosniff" });
31
+ for (const name of PASS_HEADERS) {
32
+ const value = upstream.headers.get(name);
33
+ if (value)
34
+ out.set(name, value);
35
+ }
36
+ return new Response(upstream.body, { status: upstream.status, headers: out });
37
+ }
@@ -0,0 +1,10 @@
1
+ import type { DynamicRenderingsConfig } from "../server/config.js";
2
+ /** src/app/api/parhelia/media/[code]/[...path]/route.ts */
3
+ export declare function previewMediaRoute(config: DynamicRenderingsConfig): {
4
+ GET: (request: Request, { params }: {
5
+ params: Promise<{
6
+ code: string;
7
+ path: string[];
8
+ }>;
9
+ }) => Promise<Response>;
10
+ };
@@ -0,0 +1,10 @@
1
+ import { handlePreviewMedia } from "./media.js";
2
+ /** src/app/api/parhelia/media/[code]/[...path]/route.ts */
3
+ export function previewMediaRoute(config) {
4
+ return {
5
+ GET: async (request, { params }) => {
6
+ const { code, path } = await params;
7
+ return handlePreviewMedia(config, request, code, path);
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The Parhelia CM's proxies are prefix-preserving: editor requests arrive as
3
+ * /parhelia/proxy/{name}/{route}, the sealed preview's as /parhelia/preview/{token}/{name}/{route}.
4
+ * Editor requests carry sc_itemid and resolve by item ID regardless of path; stripping the prefix
5
+ * also makes bare navigation under the proxy resolve the right route.
6
+ */
7
+ export declare function splitProxyPrefix(path: string[] | undefined): {
8
+ route: string | string[];
9
+ prefix: string;
10
+ };
@@ -0,0 +1,21 @@
1
+ // Pure helper (no Next or SDK imports), shared by the Parhelia preset and tests.
2
+ /**
3
+ * The Parhelia CM's proxies are prefix-preserving: editor requests arrive as
4
+ * /parhelia/proxy/{name}/{route}, the sealed preview's as /parhelia/preview/{token}/{name}/{route}.
5
+ * Editor requests carry sc_itemid and resolve by item ID regardless of path; stripping the prefix
6
+ * also makes bare navigation under the proxy resolve the right route.
7
+ */
8
+ export function splitProxyPrefix(path) {
9
+ const routePath = path ?? [];
10
+ const head = routePath[0]?.toLowerCase();
11
+ const kind = routePath[1]?.toLowerCase();
12
+ const prefixLength = head === "parhelia" ? (kind === "proxy" ? 3 : kind === "preview" ? 4 : 0) : 0;
13
+ if (prefixLength && routePath.length >= prefixLength) {
14
+ const strippedPath = routePath.slice(prefixLength);
15
+ return {
16
+ route: strippedPath.length > 0 ? strippedPath : "/",
17
+ prefix: "/" + routePath.slice(0, prefixLength).map(encodeURIComponent).join("/"),
18
+ };
19
+ }
20
+ return { route: routePath.length > 0 ? routePath : "/", prefix: "" };
21
+ }
@@ -0,0 +1 @@
1
+ export declare function rewriteMediaUrls<T>(value: T, prefix: string): T;
@@ -0,0 +1,24 @@
1
+ // The sandboxed interactive preview has an opaque origin and sends no cookies, so a master
2
+ // media URL (/sitecore/shell/-/media/...) would reach the CM anonymously and end on the login
3
+ // page. In that mode every root-relative media URL in the page data is moved under the preview
4
+ // prefix; the CM's preview proxy passes it to this host, and the media route
5
+ // (the head's route src/app/api/parhelia/media, previewMediaRoute) reads the original path from the CM with the preview token, so
6
+ // the request reads as the token's subject. The query, including the media hash, is untouched.
7
+ //
8
+ // The moved URL must not contain "-/media/": Sitecore treats any request path containing a media
9
+ // trigger as a media request, so the original prefix is encoded as a short letter instead
10
+ // (s = /sitecore/shell/-/media/, m = /-/media/, j = /-/jssmedia/).
11
+ const MEDIA_URL = /(?<![\w/.:-])\/(sitecore\/shell\/-\/media\/|-\/media\/|-\/jssmedia\/)/g;
12
+ const CODES = {
13
+ "sitecore/shell/-/media/": "s",
14
+ "-/media/": "m",
15
+ "-/jssmedia/": "j",
16
+ };
17
+ export function rewriteMediaUrls(value, prefix) {
18
+ if (!prefix || value === null || value === undefined)
19
+ return value;
20
+ const json = JSON.stringify(value);
21
+ if (!json.includes("-/media/") && !json.includes("-/jssmedia/"))
22
+ return value;
23
+ return JSON.parse(json.replace(MEDIA_URL, (_match, kind) => `${prefix}/_media/${CODES[kind]}/`));
24
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ // next.config.js wrapper for heads the Parhelia CM serves: withDynamicRenderings with the CM's
3
+ // proxy prefixes, plus the sealed preview's media rewrite. Plain CommonJS, like index.cjs: it loads
4
+ // in ordinary Node.
5
+ const alpaca = require("./index.cjs");
6
+
7
+ /** The editor proxy and the sealed preview proxy; bundle URLs under them reach the bundle route. */
8
+ const ROUTE_PREFIXES = ["/parhelia/proxy/:host", "/parhelia/preview/:token/:host"];
9
+
10
+ /** A sealed preview page's media URLs (see rewriteMediaUrls) go to the head's media route. */
11
+ const PREVIEW_MEDIA_REWRITE = {
12
+ source: "/parhelia/preview/:token/:host/_media/:code/:path*",
13
+ destination: "/api/parhelia/media/:code/:path*",
14
+ };
15
+
16
+ /**
17
+ * @param {import("next").NextConfig} nextConfig
18
+ * @param {{ themeFile?: string, routePrefixes?: string[] }} [options] as withDynamicRenderings';
19
+ * routePrefixes adds to the Parhelia prefixes.
20
+ */
21
+ function withDynamicRenderings(nextConfig = {}, options = {}) {
22
+ const own = nextConfig.rewrites;
23
+ return alpaca.withDynamicRenderings(
24
+ {
25
+ ...nextConfig,
26
+ async rewrites() {
27
+ const rules = typeof own === "function" ? await own() : own;
28
+ return alpaca.prependRewrites(rules, [PREVIEW_MEDIA_REWRITE]);
29
+ },
30
+ },
31
+ { ...options, routePrefixes: [...ROUTE_PREFIXES, ...(options.routePrefixes || [])] },
32
+ );
33
+ }
34
+
35
+ module.exports = { ...alpaca, withDynamicRenderings, ROUTE_PREFIXES, PREVIEW_MEDIA_REWRITE };
@@ -0,0 +1,14 @@
1
+ import type { NextConfig } from "next";
2
+ import type { DynamicRenderingsNextOptions } from "./index.cjs";
3
+
4
+ export { prependRewrites, corsExclusionSource, ENGINE_PACKAGE } from "./index.cjs";
5
+ export type { DynamicRenderingsNextOptions };
6
+
7
+ /**
8
+ * withDynamicRenderings for a head the Parhelia CM serves: the CM's proxy prefixes
9
+ * (/parhelia/proxy/:host, /parhelia/preview/:token/:host) and the sealed preview's media rewrite
10
+ * to /api/parhelia/media.
11
+ */
12
+ export declare function withDynamicRenderings(nextConfig?: NextConfig, options?: DynamicRenderingsNextOptions): NextConfig;
13
+ export declare const ROUTE_PREFIXES: readonly string[];
14
+ export declare const PREVIEW_MEDIA_REWRITE: { source: string; destination: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-headless/dynamic-renderings",
3
- "version": "1.0.4694",
3
+ "version": "1.0.4696",
4
4
  "description": "Optional Alpaca Headless dynamic renderings: source client, safe React rendering, client code and Next.js integration.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -47,6 +47,18 @@
47
47
  "types": "./next-config/index.d.cts",
48
48
  "default": "./next-config/index.cjs"
49
49
  },
50
+ "./parhelia": {
51
+ "types": "./dist/parhelia/index.d.ts",
52
+ "import": "./dist/parhelia/index.js"
53
+ },
54
+ "./parhelia/next": {
55
+ "types": "./dist/parhelia/next.d.ts",
56
+ "import": "./dist/parhelia/next.js"
57
+ },
58
+ "./parhelia/next/config": {
59
+ "types": "./next-config/parhelia.d.cts",
60
+ "default": "./next-config/parhelia.cjs"
61
+ },
50
62
  "./package.json": "./package.json"
51
63
  },
52
64
  "files": [
@@ -67,7 +79,7 @@
67
79
  "acceptance": "node scripts/acceptance.mjs"
68
80
  },
69
81
  "dependencies": {
70
- "@alpaca-headless/dynamic-renderings-engine": "1.0.4694",
82
+ "@alpaca-headless/dynamic-renderings-engine": "1.0.4696",
71
83
  "lucide-react": "^1.38.0",
72
84
  "postcss": "^8.5.26",
73
85
  "postcss-selector-parser": "^7.1.6",