@cmssy/next 0.1.0

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/dist/index.cjs ADDED
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ var headers = require('next/headers');
4
+ var navigation = require('next/navigation');
5
+ var react = require('@cmssy/react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var crypto = require('crypto');
8
+
9
+ // src/create-cmssy-page.tsx
10
+
11
+ // src/csp.ts
12
+ function toCspOrigin(origin) {
13
+ if (origin === "*") return "*";
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(origin);
17
+ } catch {
18
+ throw new Error(`cmssy: invalid editorOrigin "${origin}"`);
19
+ }
20
+ if (parsed.origin === "null") {
21
+ throw new Error(`cmssy: editorOrigin "${origin}" has no usable origin`);
22
+ }
23
+ return parsed.origin;
24
+ }
25
+ function frameAncestors(editorOrigin) {
26
+ const origins = Array.isArray(editorOrigin) ? editorOrigin : [editorOrigin];
27
+ if (origins.length === 0) {
28
+ throw new Error(
29
+ "cmssy: editorOrigin must contain at least one valid origin"
30
+ );
31
+ }
32
+ const normalized = origins.map((origin) => toCspOrigin(origin.trim()));
33
+ return `frame-ancestors ${normalized.join(" ")}`;
34
+ }
35
+ function cmssyCspHeaders(options) {
36
+ return {
37
+ "Content-Security-Policy": frameAncestors(options.editorOrigin)
38
+ };
39
+ }
40
+ function mergeFrameAncestors(existing, directive) {
41
+ if (!existing) return directive;
42
+ const kept = existing.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !/^frame-ancestors\b/i.test(part));
43
+ kept.push(directive);
44
+ return kept.join("; ");
45
+ }
46
+ function applyCmssyCsp(response, options) {
47
+ const merged = mergeFrameAncestors(
48
+ response.headers.get("Content-Security-Policy"),
49
+ frameAncestors(options.editorOrigin)
50
+ );
51
+ response.headers.set("Content-Security-Policy", merged);
52
+ response.headers.delete("X-Frame-Options");
53
+ return response;
54
+ }
55
+ var EDIT_QUERY_PARAM = "cmssyEdit";
56
+ function hasEditFlag(value) {
57
+ return Array.isArray(value) ? value.includes("1") : value === "1";
58
+ }
59
+ function createCmssyPage(config, blocks, options) {
60
+ if (!Array.isArray(blocks)) {
61
+ throw new Error(
62
+ "cmssy: createCmssyPage(config, blocks) requires a blocks array \u2014 pass your defineBlock(...) array"
63
+ );
64
+ }
65
+ const Editor = options?.editor;
66
+ const clientConfig = {
67
+ apiUrl: config.apiUrl,
68
+ workspaceSlug: config.workspaceSlug
69
+ };
70
+ const defaultLocale = config.defaultLocale ?? "en";
71
+ return async function CmssyCatchAllPage({
72
+ params,
73
+ searchParams
74
+ }) {
75
+ const { path } = await params;
76
+ const { isEnabled } = await headers.draftMode();
77
+ const query = searchParams ? await searchParams : {};
78
+ const editMode = isEnabled || hasEditFlag(query[EDIT_QUERY_PARAM]);
79
+ const locale = config.resolveLocale ? await config.resolveLocale() : defaultLocale;
80
+ const page = await react.fetchPage(clientConfig, path, {
81
+ previewSecret: editMode ? config.draftSecret : void 0
82
+ });
83
+ if (!page) {
84
+ navigation.notFound();
85
+ }
86
+ if (editMode) {
87
+ if (!Editor) {
88
+ throw new Error(
89
+ 'cmssy: edit mode requires options.editor \u2014 pass a "use client" editor that imports your blocks and renders <CmssyEditablePage blocks={blocks} \u2026 />'
90
+ );
91
+ }
92
+ const bridgeOrigin = resolveBridgeOrigin(config.editorOrigin);
93
+ return /* @__PURE__ */ jsxRuntime.jsx(
94
+ Editor,
95
+ {
96
+ page,
97
+ locale,
98
+ defaultLocale,
99
+ edit: { editorOrigin: bridgeOrigin }
100
+ }
101
+ );
102
+ }
103
+ return /* @__PURE__ */ jsxRuntime.jsx(
104
+ react.CmssyServerPage,
105
+ {
106
+ page,
107
+ blocks,
108
+ locale,
109
+ defaultLocale
110
+ }
111
+ );
112
+ };
113
+ }
114
+ function resolveBridgeOrigin(editorOrigin) {
115
+ const origins = Array.isArray(editorOrigin) ? editorOrigin : [editorOrigin];
116
+ if (origins.length === 0) {
117
+ throw new Error("cmssy: editorOrigin must be set to frame the editor");
118
+ }
119
+ if (origins.length > 1 && typeof console !== "undefined") {
120
+ console.warn(
121
+ "[cmssy] multiple editorOrigins configured; the live-edit bridge uses only the first"
122
+ );
123
+ }
124
+ const origin = toCspOrigin(origins[0].trim());
125
+ if (origin === "*") {
126
+ throw new Error(
127
+ "cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://app.cmssy.io)"
128
+ );
129
+ }
130
+ return origin;
131
+ }
132
+ var MIN_SECRET_LENGTH = 16;
133
+ function secretsMatch(a, b) {
134
+ const ha = crypto.createHash("sha256").update(a).digest();
135
+ const hb = crypto.createHash("sha256").update(b).digest();
136
+ return crypto.timingSafeEqual(ha, hb);
137
+ }
138
+ function safeRedirect(redirect2, fallback) {
139
+ if (!redirect2 || !redirect2.startsWith("/")) return fallback;
140
+ if (redirect2.startsWith("//") || redirect2.includes("\\")) return fallback;
141
+ try {
142
+ if (new URL(redirect2, "https://cmssy.invalid").origin !== "https://cmssy.invalid") {
143
+ return fallback;
144
+ }
145
+ } catch {
146
+ return fallback;
147
+ }
148
+ return redirect2;
149
+ }
150
+ function createDraftRoute(config) {
151
+ const fallbackRedirect = config.defaultRedirect ?? "/";
152
+ if (safeRedirect(fallbackRedirect, "/") !== fallbackRedirect) {
153
+ throw new Error(
154
+ "cmssy: defaultRedirect must be a same-origin path starting with '/'"
155
+ );
156
+ }
157
+ return async function GET(request) {
158
+ if (config.draftSecret.length < MIN_SECRET_LENGTH) {
159
+ return new Response(
160
+ `cmssy: draftSecret must be at least ${MIN_SECRET_LENGTH} characters`,
161
+ { status: 500 }
162
+ );
163
+ }
164
+ const url = new URL(request.url);
165
+ const secret = url.searchParams.get("secret");
166
+ if (!secret || !secretsMatch(secret, config.draftSecret)) {
167
+ return new Response("Invalid draft secret", { status: 401 });
168
+ }
169
+ const location = safeRedirect(
170
+ url.searchParams.get("redirect"),
171
+ fallbackRedirect
172
+ );
173
+ const draft = await headers.draftMode();
174
+ draft.enable();
175
+ navigation.redirect(location);
176
+ };
177
+ }
178
+ var CMSSY_EDIT_HEADER = "x-cmssy-edit";
179
+ function isCmssyEditRequest(request) {
180
+ return request.cookies.has("__prerender_bypass") || request.nextUrl.searchParams.getAll("cmssyEdit").includes("1");
181
+ }
182
+ async function isCmssyEditMode() {
183
+ const h = await headers.headers();
184
+ return h.get(CMSSY_EDIT_HEADER) === "1";
185
+ }
186
+
187
+ exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
188
+ exports.applyCmssyCsp = applyCmssyCsp;
189
+ exports.cmssyCspHeaders = cmssyCspHeaders;
190
+ exports.createCmssyPage = createCmssyPage;
191
+ exports.createDraftRoute = createDraftRoute;
192
+ exports.isCmssyEditMode = isCmssyEditMode;
193
+ exports.isCmssyEditRequest = isCmssyEditRequest;
@@ -0,0 +1,66 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ComponentType } from 'react';
3
+ import { CmssyPageData, BlockDefinition } from '@cmssy/react';
4
+ import { EditBridgeConfig } from '@cmssy/react/client';
5
+
6
+ interface CmssyNextConfig {
7
+ apiUrl: string;
8
+ workspaceSlug: string;
9
+ draftSecret: string;
10
+ editorOrigin: string | string[];
11
+ defaultLocale?: string;
12
+ resolveLocale?: () => string | Promise<string>;
13
+ }
14
+
15
+ interface CmssyEditorProps {
16
+ page: CmssyPageData;
17
+ locale: string;
18
+ defaultLocale: string;
19
+ edit: EditBridgeConfig;
20
+ }
21
+ interface CreateCmssyPageOptions {
22
+ editor?: ComponentType<CmssyEditorProps>;
23
+ }
24
+ interface CatchAllParams {
25
+ path?: string[];
26
+ }
27
+ type SearchParams = Record<string, string | string[] | undefined>;
28
+ interface CatchAllProps {
29
+ params: Promise<CatchAllParams>;
30
+ searchParams?: Promise<SearchParams>;
31
+ }
32
+ declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
33
+
34
+ type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
35
+ defaultRedirect?: string;
36
+ };
37
+ declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
38
+
39
+ interface CmssyCspOptions {
40
+ editorOrigin: string | string[];
41
+ }
42
+ interface MutableHeaders {
43
+ headers: {
44
+ get: (name: string) => string | null;
45
+ set: (name: string, value: string) => void;
46
+ delete: (name: string) => void;
47
+ };
48
+ }
49
+ declare function cmssyCspHeaders(options: CmssyCspOptions): Record<string, string>;
50
+ declare function applyCmssyCsp<T extends MutableHeaders>(response: T, options: CmssyCspOptions): T;
51
+
52
+ declare const CMSSY_EDIT_HEADER = "x-cmssy-edit";
53
+ interface EditRequestLike {
54
+ cookies: {
55
+ has: (name: string) => boolean;
56
+ };
57
+ nextUrl: {
58
+ searchParams: {
59
+ getAll: (name: string) => string[];
60
+ };
61
+ };
62
+ }
63
+ declare function isCmssyEditRequest(request: EditRequestLike): boolean;
64
+ declare function isCmssyEditMode(): Promise<boolean>;
65
+
66
+ export { CMSSY_EDIT_HEADER, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CreateCmssyPageOptions, applyCmssyCsp, cmssyCspHeaders, createCmssyPage, createDraftRoute, isCmssyEditMode, isCmssyEditRequest };
@@ -0,0 +1,66 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ComponentType } from 'react';
3
+ import { CmssyPageData, BlockDefinition } from '@cmssy/react';
4
+ import { EditBridgeConfig } from '@cmssy/react/client';
5
+
6
+ interface CmssyNextConfig {
7
+ apiUrl: string;
8
+ workspaceSlug: string;
9
+ draftSecret: string;
10
+ editorOrigin: string | string[];
11
+ defaultLocale?: string;
12
+ resolveLocale?: () => string | Promise<string>;
13
+ }
14
+
15
+ interface CmssyEditorProps {
16
+ page: CmssyPageData;
17
+ locale: string;
18
+ defaultLocale: string;
19
+ edit: EditBridgeConfig;
20
+ }
21
+ interface CreateCmssyPageOptions {
22
+ editor?: ComponentType<CmssyEditorProps>;
23
+ }
24
+ interface CatchAllParams {
25
+ path?: string[];
26
+ }
27
+ type SearchParams = Record<string, string | string[] | undefined>;
28
+ interface CatchAllProps {
29
+ params: Promise<CatchAllParams>;
30
+ searchParams?: Promise<SearchParams>;
31
+ }
32
+ declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
33
+
34
+ type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
35
+ defaultRedirect?: string;
36
+ };
37
+ declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
38
+
39
+ interface CmssyCspOptions {
40
+ editorOrigin: string | string[];
41
+ }
42
+ interface MutableHeaders {
43
+ headers: {
44
+ get: (name: string) => string | null;
45
+ set: (name: string, value: string) => void;
46
+ delete: (name: string) => void;
47
+ };
48
+ }
49
+ declare function cmssyCspHeaders(options: CmssyCspOptions): Record<string, string>;
50
+ declare function applyCmssyCsp<T extends MutableHeaders>(response: T, options: CmssyCspOptions): T;
51
+
52
+ declare const CMSSY_EDIT_HEADER = "x-cmssy-edit";
53
+ interface EditRequestLike {
54
+ cookies: {
55
+ has: (name: string) => boolean;
56
+ };
57
+ nextUrl: {
58
+ searchParams: {
59
+ getAll: (name: string) => string[];
60
+ };
61
+ };
62
+ }
63
+ declare function isCmssyEditRequest(request: EditRequestLike): boolean;
64
+ declare function isCmssyEditMode(): Promise<boolean>;
65
+
66
+ export { CMSSY_EDIT_HEADER, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CreateCmssyPageOptions, applyCmssyCsp, cmssyCspHeaders, createCmssyPage, createDraftRoute, isCmssyEditMode, isCmssyEditRequest };
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ import { draftMode, headers } from 'next/headers';
2
+ import { notFound, redirect } from 'next/navigation';
3
+ import { fetchPage, CmssyServerPage } from '@cmssy/react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+ import { createHash, timingSafeEqual } from 'crypto';
6
+
7
+ // src/create-cmssy-page.tsx
8
+
9
+ // src/csp.ts
10
+ function toCspOrigin(origin) {
11
+ if (origin === "*") return "*";
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(origin);
15
+ } catch {
16
+ throw new Error(`cmssy: invalid editorOrigin "${origin}"`);
17
+ }
18
+ if (parsed.origin === "null") {
19
+ throw new Error(`cmssy: editorOrigin "${origin}" has no usable origin`);
20
+ }
21
+ return parsed.origin;
22
+ }
23
+ function frameAncestors(editorOrigin) {
24
+ const origins = Array.isArray(editorOrigin) ? editorOrigin : [editorOrigin];
25
+ if (origins.length === 0) {
26
+ throw new Error(
27
+ "cmssy: editorOrigin must contain at least one valid origin"
28
+ );
29
+ }
30
+ const normalized = origins.map((origin) => toCspOrigin(origin.trim()));
31
+ return `frame-ancestors ${normalized.join(" ")}`;
32
+ }
33
+ function cmssyCspHeaders(options) {
34
+ return {
35
+ "Content-Security-Policy": frameAncestors(options.editorOrigin)
36
+ };
37
+ }
38
+ function mergeFrameAncestors(existing, directive) {
39
+ if (!existing) return directive;
40
+ const kept = existing.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !/^frame-ancestors\b/i.test(part));
41
+ kept.push(directive);
42
+ return kept.join("; ");
43
+ }
44
+ function applyCmssyCsp(response, options) {
45
+ const merged = mergeFrameAncestors(
46
+ response.headers.get("Content-Security-Policy"),
47
+ frameAncestors(options.editorOrigin)
48
+ );
49
+ response.headers.set("Content-Security-Policy", merged);
50
+ response.headers.delete("X-Frame-Options");
51
+ return response;
52
+ }
53
+ var EDIT_QUERY_PARAM = "cmssyEdit";
54
+ function hasEditFlag(value) {
55
+ return Array.isArray(value) ? value.includes("1") : value === "1";
56
+ }
57
+ function createCmssyPage(config, blocks, options) {
58
+ if (!Array.isArray(blocks)) {
59
+ throw new Error(
60
+ "cmssy: createCmssyPage(config, blocks) requires a blocks array \u2014 pass your defineBlock(...) array"
61
+ );
62
+ }
63
+ const Editor = options?.editor;
64
+ const clientConfig = {
65
+ apiUrl: config.apiUrl,
66
+ workspaceSlug: config.workspaceSlug
67
+ };
68
+ const defaultLocale = config.defaultLocale ?? "en";
69
+ return async function CmssyCatchAllPage({
70
+ params,
71
+ searchParams
72
+ }) {
73
+ const { path } = await params;
74
+ const { isEnabled } = await draftMode();
75
+ const query = searchParams ? await searchParams : {};
76
+ const editMode = isEnabled || hasEditFlag(query[EDIT_QUERY_PARAM]);
77
+ const locale = config.resolveLocale ? await config.resolveLocale() : defaultLocale;
78
+ const page = await fetchPage(clientConfig, path, {
79
+ previewSecret: editMode ? config.draftSecret : void 0
80
+ });
81
+ if (!page) {
82
+ notFound();
83
+ }
84
+ if (editMode) {
85
+ if (!Editor) {
86
+ throw new Error(
87
+ 'cmssy: edit mode requires options.editor \u2014 pass a "use client" editor that imports your blocks and renders <CmssyEditablePage blocks={blocks} \u2026 />'
88
+ );
89
+ }
90
+ const bridgeOrigin = resolveBridgeOrigin(config.editorOrigin);
91
+ return /* @__PURE__ */ jsx(
92
+ Editor,
93
+ {
94
+ page,
95
+ locale,
96
+ defaultLocale,
97
+ edit: { editorOrigin: bridgeOrigin }
98
+ }
99
+ );
100
+ }
101
+ return /* @__PURE__ */ jsx(
102
+ CmssyServerPage,
103
+ {
104
+ page,
105
+ blocks,
106
+ locale,
107
+ defaultLocale
108
+ }
109
+ );
110
+ };
111
+ }
112
+ function resolveBridgeOrigin(editorOrigin) {
113
+ const origins = Array.isArray(editorOrigin) ? editorOrigin : [editorOrigin];
114
+ if (origins.length === 0) {
115
+ throw new Error("cmssy: editorOrigin must be set to frame the editor");
116
+ }
117
+ if (origins.length > 1 && typeof console !== "undefined") {
118
+ console.warn(
119
+ "[cmssy] multiple editorOrigins configured; the live-edit bridge uses only the first"
120
+ );
121
+ }
122
+ const origin = toCspOrigin(origins[0].trim());
123
+ if (origin === "*") {
124
+ throw new Error(
125
+ "cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://app.cmssy.io)"
126
+ );
127
+ }
128
+ return origin;
129
+ }
130
+ var MIN_SECRET_LENGTH = 16;
131
+ function secretsMatch(a, b) {
132
+ const ha = createHash("sha256").update(a).digest();
133
+ const hb = createHash("sha256").update(b).digest();
134
+ return timingSafeEqual(ha, hb);
135
+ }
136
+ function safeRedirect(redirect2, fallback) {
137
+ if (!redirect2 || !redirect2.startsWith("/")) return fallback;
138
+ if (redirect2.startsWith("//") || redirect2.includes("\\")) return fallback;
139
+ try {
140
+ if (new URL(redirect2, "https://cmssy.invalid").origin !== "https://cmssy.invalid") {
141
+ return fallback;
142
+ }
143
+ } catch {
144
+ return fallback;
145
+ }
146
+ return redirect2;
147
+ }
148
+ function createDraftRoute(config) {
149
+ const fallbackRedirect = config.defaultRedirect ?? "/";
150
+ if (safeRedirect(fallbackRedirect, "/") !== fallbackRedirect) {
151
+ throw new Error(
152
+ "cmssy: defaultRedirect must be a same-origin path starting with '/'"
153
+ );
154
+ }
155
+ return async function GET(request) {
156
+ if (config.draftSecret.length < MIN_SECRET_LENGTH) {
157
+ return new Response(
158
+ `cmssy: draftSecret must be at least ${MIN_SECRET_LENGTH} characters`,
159
+ { status: 500 }
160
+ );
161
+ }
162
+ const url = new URL(request.url);
163
+ const secret = url.searchParams.get("secret");
164
+ if (!secret || !secretsMatch(secret, config.draftSecret)) {
165
+ return new Response("Invalid draft secret", { status: 401 });
166
+ }
167
+ const location = safeRedirect(
168
+ url.searchParams.get("redirect"),
169
+ fallbackRedirect
170
+ );
171
+ const draft = await draftMode();
172
+ draft.enable();
173
+ redirect(location);
174
+ };
175
+ }
176
+ var CMSSY_EDIT_HEADER = "x-cmssy-edit";
177
+ function isCmssyEditRequest(request) {
178
+ return request.cookies.has("__prerender_bypass") || request.nextUrl.searchParams.getAll("cmssyEdit").includes("1");
179
+ }
180
+ async function isCmssyEditMode() {
181
+ const h = await headers();
182
+ return h.get(CMSSY_EDIT_HEADER) === "1";
183
+ }
184
+
185
+ export { CMSSY_EDIT_HEADER, applyCmssyCsp, cmssyCspHeaders, createCmssyPage, createDraftRoute, isCmssyEditMode, isCmssyEditRequest };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@cmssy/next",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "peerDependencies": {
24
+ "@cmssy/react": "^0.1.0",
25
+ "next": ">=15",
26
+ "react": "^18.2.0 || ^19.0.0",
27
+ "react-dom": "^18.2.0 || ^19.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "@types/react": "^19.0.0",
32
+ "next": "^16.0.0",
33
+ "react": "^19.0.0",
34
+ "tsup": "^8.3.0",
35
+ "typescript": "^5.6.0",
36
+ "vitest": "^2.1.0",
37
+ "@cmssy/react": "0.1.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit"
43
+ }
44
+ }