@cmssy/next 4.7.1 → 5.0.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/README.md +1 -1
- package/dist/index-D-mfavEO.d.cts +46 -0
- package/dist/index-D-mfavEO.d.ts +46 -0
- package/dist/index.cjs +31 -2070
- package/dist/index.d.cts +6 -330
- package/dist/index.d.ts +6 -330
- package/dist/index.js +1 -2024
- package/dist/middleware.cjs +185 -0
- package/dist/middleware.d.cts +106 -0
- package/dist/middleware.d.ts +106 -0
- package/dist/middleware.js +156 -0
- package/dist/server.cjs +1063 -0
- package/dist/server.d.cts +206 -0
- package/dist/server.d.ts +206 -0
- package/dist/server.js +1044 -0
- package/dist/testing.cjs +7 -60
- package/dist/testing.d.cts +1 -39
- package/dist/testing.d.ts +1 -39
- package/dist/testing.js +1 -61
- package/package.json +16 -10
- package/dist/config-DNaPqq1f.d.cts +0 -63
- package/dist/config-DNaPqq1f.d.ts +0 -63
- package/dist/preset.cjs +0 -235
- package/dist/preset.d.cts +0 -81
- package/dist/preset.d.ts +0 -81
- package/dist/preset.js +0 -231
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var server = require('next/server');
|
|
4
|
+
var react = require('@cmssy/react');
|
|
5
|
+
var core = require('@cmssy/core');
|
|
6
|
+
|
|
7
|
+
// src/preset/proxy.ts
|
|
8
|
+
var CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
9
|
+
async function cmssyEditRewrite(request, config, options = {}) {
|
|
10
|
+
const { pathname, searchParams } = request.nextUrl;
|
|
11
|
+
if (pathname.startsWith(CMSSY_EDIT_PATH_PREFIX)) return null;
|
|
12
|
+
if (!searchParams.getAll(core.CMSSY_EDIT_QUERY_PARAM).includes("1")) return null;
|
|
13
|
+
const provided = searchParams.get(core.CMSSY_SECRET_QUERY_PARAM);
|
|
14
|
+
if (!provided || !config.draftSecret) return null;
|
|
15
|
+
if (!await core.cmssySecretsMatch(provided, config.draftSecret)) return null;
|
|
16
|
+
const url = request.nextUrl.clone();
|
|
17
|
+
url.pathname = `${CMSSY_EDIT_PATH_PREFIX}${pathname === "/" ? "" : pathname}`;
|
|
18
|
+
warnIfEditRouteMissing(url);
|
|
19
|
+
return server.NextResponse.rewrite(
|
|
20
|
+
url,
|
|
21
|
+
options.requestHeaders ? { request: { headers: options.requestHeaders } } : void 0
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function createCmssyEditMiddleware(config) {
|
|
25
|
+
return async function cmssyEditMiddleware(request) {
|
|
26
|
+
return await cmssyEditRewrite(request, config) ?? server.NextResponse.next();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
var probed = false;
|
|
30
|
+
function warnIfEditRouteMissing(url) {
|
|
31
|
+
if (process.env.NODE_ENV === "production" || probed) return;
|
|
32
|
+
probed = true;
|
|
33
|
+
void fetch(url, { method: "HEAD" }).then((response) => {
|
|
34
|
+
if (response.status !== 404) return;
|
|
35
|
+
console.error(
|
|
36
|
+
`[cmssy] The editor request was rewritten to ${url.pathname}, but nothing is mounted there (404). Add the edit route:
|
|
37
|
+
|
|
38
|
+
// app/cmssy-edit/[[...path]]/page.tsx
|
|
39
|
+
export const dynamic = "force-dynamic";
|
|
40
|
+
export default createCmssyEditPage(cmssy, blocks, { editor: CmssyEditor });
|
|
41
|
+
|
|
42
|
+
Until then the editor preview stays blank.`
|
|
43
|
+
);
|
|
44
|
+
}).catch(() => {
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function createCmssyProxy(config, options = {}) {
|
|
48
|
+
return async function cmssyProxy(request) {
|
|
49
|
+
const { pathname } = request.nextUrl;
|
|
50
|
+
const requestHeaders = new Headers(request.headers);
|
|
51
|
+
requestHeaders.delete(core.CMSSY_EDIT_HEADER);
|
|
52
|
+
requestHeaders.delete(core.CMSSY_LOCALE_HEADER);
|
|
53
|
+
const locale = await core.localeForPathname(config, pathname);
|
|
54
|
+
requestHeaders.set(core.CMSSY_LOCALE_HEADER, locale);
|
|
55
|
+
const editHeaders = new Headers(requestHeaders);
|
|
56
|
+
editHeaders.set(core.CMSSY_EDIT_HEADER, "1");
|
|
57
|
+
const editRewrite = await cmssyEditRewrite(request, config, {
|
|
58
|
+
requestHeaders: editHeaders
|
|
59
|
+
});
|
|
60
|
+
if (editRewrite) {
|
|
61
|
+
core.applyCmssyCsp(editRewrite, { editorOrigin: config.editorOrigin });
|
|
62
|
+
return editRewrite;
|
|
63
|
+
}
|
|
64
|
+
if (options.stripLocalePrefix && pathname.startsWith(`/${locale}`)) {
|
|
65
|
+
const { defaultLocale } = await react.resolveSiteLocales(config);
|
|
66
|
+
if (locale !== defaultLocale) {
|
|
67
|
+
const url = request.nextUrl.clone();
|
|
68
|
+
url.pathname = pathname.slice(locale.length + 1) || "/";
|
|
69
|
+
return server.NextResponse.rewrite(url, {
|
|
70
|
+
request: { headers: requestHeaders }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return server.NextResponse.next({ request: { headers: requestHeaders } });
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
var cmssyProxyMatcher = ["/((?!_next/|api/|.*\\..*).*)"];
|
|
78
|
+
async function resolveLocaleFromPathname(config, pathname) {
|
|
79
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
80
|
+
const hasStaticLocales = !!config.enabledLocales?.length;
|
|
81
|
+
const fetched = config.defaultLocale && hasStaticLocales ? null : await react.resolveSiteLocales({
|
|
82
|
+
apiUrl: config.apiUrl,
|
|
83
|
+
org: config.org,
|
|
84
|
+
workspaceSlug: config.workspaceSlug
|
|
85
|
+
});
|
|
86
|
+
const siteLocales = {
|
|
87
|
+
defaultLocale: config.defaultLocale ?? fetched.defaultLocale,
|
|
88
|
+
locales: hasStaticLocales ? config.enabledLocales : fetched.locales
|
|
89
|
+
};
|
|
90
|
+
return react.splitLocaleFromPath(segments, siteLocales).locale;
|
|
91
|
+
}
|
|
92
|
+
function createCmssyLocaleMiddleware(config) {
|
|
93
|
+
return async function cmssyLocaleMiddleware(request) {
|
|
94
|
+
const locale = await resolveLocaleFromPathname(
|
|
95
|
+
config,
|
|
96
|
+
request.nextUrl.pathname
|
|
97
|
+
);
|
|
98
|
+
const headers = new Headers(request.headers);
|
|
99
|
+
headers.set(core.CMSSY_LOCALE_HEADER, locale);
|
|
100
|
+
return server.NextResponse.next({ request: { headers } });
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isPrefetch(request) {
|
|
104
|
+
return request.headers.get("next-router-prefetch") !== null || request.headers.get("purpose") === "prefetch" || (request.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
105
|
+
}
|
|
106
|
+
function createCmssyAuthMiddleware(config) {
|
|
107
|
+
const auth = core.assertAuthConfig(config);
|
|
108
|
+
return async function cmssyAuthMiddleware(request) {
|
|
109
|
+
const raw = request.cookies.get(core.CMSSY_SESSION_COOKIE)?.value;
|
|
110
|
+
if (!raw) return server.NextResponse.next();
|
|
111
|
+
const session = await core.openSession(
|
|
112
|
+
raw,
|
|
113
|
+
auth.sessionSecret,
|
|
114
|
+
config.workspaceSlug
|
|
115
|
+
);
|
|
116
|
+
if (!session) {
|
|
117
|
+
const response = server.NextResponse.next();
|
|
118
|
+
response.cookies.set(core.CMSSY_SESSION_COOKIE, "", {
|
|
119
|
+
...core.sessionCookieOptions(),
|
|
120
|
+
maxAge: 0
|
|
121
|
+
});
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
if (!core.isAccessExpired(session)) return server.NextResponse.next();
|
|
125
|
+
if (isPrefetch(request)) return server.NextResponse.next();
|
|
126
|
+
let payload = null;
|
|
127
|
+
try {
|
|
128
|
+
const result = await core.backendRefresh(config, session.refreshToken);
|
|
129
|
+
payload = core.toSessionPayload(result);
|
|
130
|
+
} catch {
|
|
131
|
+
return server.NextResponse.next();
|
|
132
|
+
}
|
|
133
|
+
if (!payload) {
|
|
134
|
+
const cleared = server.NextResponse.next();
|
|
135
|
+
cleared.cookies.set(core.CMSSY_SESSION_COOKIE, "", {
|
|
136
|
+
...core.sessionCookieOptions(),
|
|
137
|
+
maxAge: 0
|
|
138
|
+
});
|
|
139
|
+
return cleared;
|
|
140
|
+
}
|
|
141
|
+
const sealed = await core.sealSession(
|
|
142
|
+
payload,
|
|
143
|
+
auth.sessionSecret,
|
|
144
|
+
config.workspaceSlug
|
|
145
|
+
);
|
|
146
|
+
request.cookies.set(core.CMSSY_SESSION_COOKIE, sealed);
|
|
147
|
+
const refreshed = server.NextResponse.next({ request });
|
|
148
|
+
refreshed.cookies.set(core.CMSSY_SESSION_COOKIE, sealed, core.sessionCookieOptions());
|
|
149
|
+
return refreshed;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function isCmssyEditRequest(request, config) {
|
|
153
|
+
if (request.cookies.has("__prerender_bypass")) return true;
|
|
154
|
+
return core.isVerifiedEditUrl(request.nextUrl, config);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Object.defineProperty(exports, "CMSSY_EDIT_HEADER", {
|
|
158
|
+
enumerable: true,
|
|
159
|
+
get: function () { return core.CMSSY_EDIT_HEADER; }
|
|
160
|
+
});
|
|
161
|
+
Object.defineProperty(exports, "CMSSY_LOCALE_HEADER", {
|
|
162
|
+
enumerable: true,
|
|
163
|
+
get: function () { return core.CMSSY_LOCALE_HEADER; }
|
|
164
|
+
});
|
|
165
|
+
Object.defineProperty(exports, "applyCmssyCsp", {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
get: function () { return core.applyCmssyCsp; }
|
|
168
|
+
});
|
|
169
|
+
Object.defineProperty(exports, "cmssyCspHeaders", {
|
|
170
|
+
enumerable: true,
|
|
171
|
+
get: function () { return core.cmssyCspHeaders; }
|
|
172
|
+
});
|
|
173
|
+
Object.defineProperty(exports, "localeForPathname", {
|
|
174
|
+
enumerable: true,
|
|
175
|
+
get: function () { return core.localeForPathname; }
|
|
176
|
+
});
|
|
177
|
+
exports.CMSSY_EDIT_PATH_PREFIX = CMSSY_EDIT_PATH_PREFIX;
|
|
178
|
+
exports.cmssyEditRewrite = cmssyEditRewrite;
|
|
179
|
+
exports.cmssyProxyMatcher = cmssyProxyMatcher;
|
|
180
|
+
exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
|
|
181
|
+
exports.createCmssyEditMiddleware = createCmssyEditMiddleware;
|
|
182
|
+
exports.createCmssyLocaleMiddleware = createCmssyLocaleMiddleware;
|
|
183
|
+
exports.createCmssyProxy = createCmssyProxy;
|
|
184
|
+
exports.isCmssyEditRequest = isCmssyEditRequest;
|
|
185
|
+
exports.resolveLocaleFromPathname = resolveLocaleFromPathname;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { CmssyConfig } from '@cmssy/core';
|
|
3
|
+
export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CmssyCspOptions, applyCmssyCsp, cmssyCspHeaders, localeForPathname } from '@cmssy/core';
|
|
4
|
+
|
|
5
|
+
interface CmssyProxyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Strip the language prefix before the app sees it, so static routes like
|
|
8
|
+
* `/shop/cart` serve `/no/shop/cart` too. Leave it off for a catch-all app
|
|
9
|
+
* that reads the language off the path itself.
|
|
10
|
+
*/
|
|
11
|
+
stripLocalePrefix?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The whole middleware a cmssy app needs, in the order it has to happen:
|
|
15
|
+
*
|
|
16
|
+
* 1. resolve the language and pass it on (a route cannot read the prefix it
|
|
17
|
+
* never sees);
|
|
18
|
+
* 2. send a VERIFIED editor request to /cmssy-edit, carrying that language and
|
|
19
|
+
* the edit flag - the public pages are static, and a static page never sees
|
|
20
|
+
* the query string that would put it in edit mode;
|
|
21
|
+
* 3. strip the language prefix for everything else, if asked.
|
|
22
|
+
*
|
|
23
|
+
* The order is not a detail: resolve the locale after the rewrite and the editor
|
|
24
|
+
* preview renders in the wrong language; forget the edit flag and the header and
|
|
25
|
+
* footer become markup the editor can select but not fill. Both mistakes shipped
|
|
26
|
+
* before this existed.
|
|
27
|
+
*/
|
|
28
|
+
declare function createCmssyProxy(config: CmssyConfig, options?: CmssyProxyOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* The matcher a cmssy app wants: everything except Next internals, API routes
|
|
31
|
+
* and files with an extension.
|
|
32
|
+
*
|
|
33
|
+
* Next parses `export const config` at COMPILE time, so it rejects an imported
|
|
34
|
+
* constant - copy this value into your proxy literally:
|
|
35
|
+
*
|
|
36
|
+
* export const config = { matcher: ["/((?!_next/|api/|.*\\..*).*)"] };
|
|
37
|
+
*/
|
|
38
|
+
declare const cmssyProxyMatcher: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where the middleware rewrites editor-preview traffic. Mount the matching
|
|
42
|
+
* dynamic route in the consumer app:
|
|
43
|
+
*
|
|
44
|
+
* app/cmssy-edit/[[...path]]/page.tsx
|
|
45
|
+
* export const dynamic = "force-dynamic";
|
|
46
|
+
* export default createCmssyEditPage(cmssy, blocks, { editor: Editor });
|
|
47
|
+
*/
|
|
48
|
+
declare const CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
49
|
+
/**
|
|
50
|
+
* Rewrite VERIFIED editor requests (`cmssyEdit=1` + a `cmssySecret` matching
|
|
51
|
+
* the site's draft secret, CMS-948) onto the dedicated dynamic edit route, so
|
|
52
|
+
* the public catch-all can stay fully static. A static page never sees its
|
|
53
|
+
* query string - this rewrite is what makes the editor iframe work at all
|
|
54
|
+
* once ISR is on. Draft-mode preview (the authenticated /api/draft cookie) is
|
|
55
|
+
* NOT rewritten: it renders draft content on the public route via
|
|
56
|
+
* `draftMode()`, without the editor. Returns null for normal traffic so it
|
|
57
|
+
* composes with other middleware.
|
|
58
|
+
*/
|
|
59
|
+
declare function cmssyEditRewrite(request: NextRequest, config: {
|
|
60
|
+
draftSecret: string;
|
|
61
|
+
}, options?: {
|
|
62
|
+
/**
|
|
63
|
+
* Headers to forward to the edit route, for a site whose middleware tells
|
|
64
|
+
* the app something the path alone does not - a resolved locale, say.
|
|
65
|
+
* Without them the editor preview renders in the default language while the
|
|
66
|
+
* public page renders in the visitor's.
|
|
67
|
+
*/
|
|
68
|
+
requestHeaders?: Headers;
|
|
69
|
+
}): Promise<NextResponse | null>;
|
|
70
|
+
/** Standalone middleware when the consumer has no other middleware to compose. */
|
|
71
|
+
declare function createCmssyEditMiddleware(config: {
|
|
72
|
+
draftSecret: string;
|
|
73
|
+
}): (request: NextRequest) => Promise<NextResponse>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the active locale from a pathname's leading segment. Uses the
|
|
77
|
+
* config's static `defaultLocale` + `enabledLocales` when both are set (no
|
|
78
|
+
* network); otherwise fetches the workspace locales (cached).
|
|
79
|
+
*/
|
|
80
|
+
declare function resolveLocaleFromPathname(config: CmssyConfig, pathname: string): Promise<string>;
|
|
81
|
+
/**
|
|
82
|
+
* Middleware that derives the locale from the URL path prefix and forwards it as
|
|
83
|
+
* the `x-cmssy-locale` request header, so server components that can't read the
|
|
84
|
+
* path (root layout) resolve the right locale via `getCmssyLocale`.
|
|
85
|
+
*/
|
|
86
|
+
declare function createCmssyLocaleMiddleware(config: CmssyConfig): (request: NextRequest) => Promise<NextResponse>;
|
|
87
|
+
|
|
88
|
+
type CmssyAuthMiddleware = (request: NextRequest) => Promise<NextResponse>;
|
|
89
|
+
declare function createCmssyAuthMiddleware(config: CmssyConfig): CmssyAuthMiddleware;
|
|
90
|
+
|
|
91
|
+
interface EditRequestLike {
|
|
92
|
+
cookies: {
|
|
93
|
+
has: (name: string) => boolean;
|
|
94
|
+
};
|
|
95
|
+
nextUrl: {
|
|
96
|
+
searchParams: {
|
|
97
|
+
getAll: (name: string) => string[];
|
|
98
|
+
get: (name: string) => string | null;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
declare function isCmssyEditRequest(request: EditRequestLike, config: {
|
|
103
|
+
draftSecret: string;
|
|
104
|
+
}): Promise<boolean>;
|
|
105
|
+
|
|
106
|
+
export { CMSSY_EDIT_PATH_PREFIX, type CmssyAuthMiddleware, type CmssyProxyOptions, cmssyEditRewrite, cmssyProxyMatcher, createCmssyAuthMiddleware, createCmssyEditMiddleware, createCmssyLocaleMiddleware, createCmssyProxy, isCmssyEditRequest, resolveLocaleFromPathname };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { CmssyConfig } from '@cmssy/core';
|
|
3
|
+
export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CmssyCspOptions, applyCmssyCsp, cmssyCspHeaders, localeForPathname } from '@cmssy/core';
|
|
4
|
+
|
|
5
|
+
interface CmssyProxyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Strip the language prefix before the app sees it, so static routes like
|
|
8
|
+
* `/shop/cart` serve `/no/shop/cart` too. Leave it off for a catch-all app
|
|
9
|
+
* that reads the language off the path itself.
|
|
10
|
+
*/
|
|
11
|
+
stripLocalePrefix?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The whole middleware a cmssy app needs, in the order it has to happen:
|
|
15
|
+
*
|
|
16
|
+
* 1. resolve the language and pass it on (a route cannot read the prefix it
|
|
17
|
+
* never sees);
|
|
18
|
+
* 2. send a VERIFIED editor request to /cmssy-edit, carrying that language and
|
|
19
|
+
* the edit flag - the public pages are static, and a static page never sees
|
|
20
|
+
* the query string that would put it in edit mode;
|
|
21
|
+
* 3. strip the language prefix for everything else, if asked.
|
|
22
|
+
*
|
|
23
|
+
* The order is not a detail: resolve the locale after the rewrite and the editor
|
|
24
|
+
* preview renders in the wrong language; forget the edit flag and the header and
|
|
25
|
+
* footer become markup the editor can select but not fill. Both mistakes shipped
|
|
26
|
+
* before this existed.
|
|
27
|
+
*/
|
|
28
|
+
declare function createCmssyProxy(config: CmssyConfig, options?: CmssyProxyOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* The matcher a cmssy app wants: everything except Next internals, API routes
|
|
31
|
+
* and files with an extension.
|
|
32
|
+
*
|
|
33
|
+
* Next parses `export const config` at COMPILE time, so it rejects an imported
|
|
34
|
+
* constant - copy this value into your proxy literally:
|
|
35
|
+
*
|
|
36
|
+
* export const config = { matcher: ["/((?!_next/|api/|.*\\..*).*)"] };
|
|
37
|
+
*/
|
|
38
|
+
declare const cmssyProxyMatcher: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where the middleware rewrites editor-preview traffic. Mount the matching
|
|
42
|
+
* dynamic route in the consumer app:
|
|
43
|
+
*
|
|
44
|
+
* app/cmssy-edit/[[...path]]/page.tsx
|
|
45
|
+
* export const dynamic = "force-dynamic";
|
|
46
|
+
* export default createCmssyEditPage(cmssy, blocks, { editor: Editor });
|
|
47
|
+
*/
|
|
48
|
+
declare const CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
49
|
+
/**
|
|
50
|
+
* Rewrite VERIFIED editor requests (`cmssyEdit=1` + a `cmssySecret` matching
|
|
51
|
+
* the site's draft secret, CMS-948) onto the dedicated dynamic edit route, so
|
|
52
|
+
* the public catch-all can stay fully static. A static page never sees its
|
|
53
|
+
* query string - this rewrite is what makes the editor iframe work at all
|
|
54
|
+
* once ISR is on. Draft-mode preview (the authenticated /api/draft cookie) is
|
|
55
|
+
* NOT rewritten: it renders draft content on the public route via
|
|
56
|
+
* `draftMode()`, without the editor. Returns null for normal traffic so it
|
|
57
|
+
* composes with other middleware.
|
|
58
|
+
*/
|
|
59
|
+
declare function cmssyEditRewrite(request: NextRequest, config: {
|
|
60
|
+
draftSecret: string;
|
|
61
|
+
}, options?: {
|
|
62
|
+
/**
|
|
63
|
+
* Headers to forward to the edit route, for a site whose middleware tells
|
|
64
|
+
* the app something the path alone does not - a resolved locale, say.
|
|
65
|
+
* Without them the editor preview renders in the default language while the
|
|
66
|
+
* public page renders in the visitor's.
|
|
67
|
+
*/
|
|
68
|
+
requestHeaders?: Headers;
|
|
69
|
+
}): Promise<NextResponse | null>;
|
|
70
|
+
/** Standalone middleware when the consumer has no other middleware to compose. */
|
|
71
|
+
declare function createCmssyEditMiddleware(config: {
|
|
72
|
+
draftSecret: string;
|
|
73
|
+
}): (request: NextRequest) => Promise<NextResponse>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the active locale from a pathname's leading segment. Uses the
|
|
77
|
+
* config's static `defaultLocale` + `enabledLocales` when both are set (no
|
|
78
|
+
* network); otherwise fetches the workspace locales (cached).
|
|
79
|
+
*/
|
|
80
|
+
declare function resolveLocaleFromPathname(config: CmssyConfig, pathname: string): Promise<string>;
|
|
81
|
+
/**
|
|
82
|
+
* Middleware that derives the locale from the URL path prefix and forwards it as
|
|
83
|
+
* the `x-cmssy-locale` request header, so server components that can't read the
|
|
84
|
+
* path (root layout) resolve the right locale via `getCmssyLocale`.
|
|
85
|
+
*/
|
|
86
|
+
declare function createCmssyLocaleMiddleware(config: CmssyConfig): (request: NextRequest) => Promise<NextResponse>;
|
|
87
|
+
|
|
88
|
+
type CmssyAuthMiddleware = (request: NextRequest) => Promise<NextResponse>;
|
|
89
|
+
declare function createCmssyAuthMiddleware(config: CmssyConfig): CmssyAuthMiddleware;
|
|
90
|
+
|
|
91
|
+
interface EditRequestLike {
|
|
92
|
+
cookies: {
|
|
93
|
+
has: (name: string) => boolean;
|
|
94
|
+
};
|
|
95
|
+
nextUrl: {
|
|
96
|
+
searchParams: {
|
|
97
|
+
getAll: (name: string) => string[];
|
|
98
|
+
get: (name: string) => string | null;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
declare function isCmssyEditRequest(request: EditRequestLike, config: {
|
|
103
|
+
draftSecret: string;
|
|
104
|
+
}): Promise<boolean>;
|
|
105
|
+
|
|
106
|
+
export { CMSSY_EDIT_PATH_PREFIX, type CmssyAuthMiddleware, type CmssyProxyOptions, cmssyEditRewrite, cmssyProxyMatcher, createCmssyAuthMiddleware, createCmssyEditMiddleware, createCmssyLocaleMiddleware, createCmssyProxy, isCmssyEditRequest, resolveLocaleFromPathname };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { resolveSiteLocales, splitLocaleFromPath } from '@cmssy/react';
|
|
3
|
+
import { CMSSY_EDIT_QUERY_PARAM, CMSSY_SECRET_QUERY_PARAM, cmssySecretsMatch, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, localeForPathname, applyCmssyCsp, assertAuthConfig, CMSSY_SESSION_COOKIE, openSession, sessionCookieOptions, isAccessExpired, backendRefresh, toSessionPayload, sealSession, isVerifiedEditUrl } from '@cmssy/core';
|
|
4
|
+
export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, applyCmssyCsp, cmssyCspHeaders, localeForPathname } from '@cmssy/core';
|
|
5
|
+
|
|
6
|
+
// src/preset/proxy.ts
|
|
7
|
+
var CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
8
|
+
async function cmssyEditRewrite(request, config, options = {}) {
|
|
9
|
+
const { pathname, searchParams } = request.nextUrl;
|
|
10
|
+
if (pathname.startsWith(CMSSY_EDIT_PATH_PREFIX)) return null;
|
|
11
|
+
if (!searchParams.getAll(CMSSY_EDIT_QUERY_PARAM).includes("1")) return null;
|
|
12
|
+
const provided = searchParams.get(CMSSY_SECRET_QUERY_PARAM);
|
|
13
|
+
if (!provided || !config.draftSecret) return null;
|
|
14
|
+
if (!await cmssySecretsMatch(provided, config.draftSecret)) return null;
|
|
15
|
+
const url = request.nextUrl.clone();
|
|
16
|
+
url.pathname = `${CMSSY_EDIT_PATH_PREFIX}${pathname === "/" ? "" : pathname}`;
|
|
17
|
+
warnIfEditRouteMissing(url);
|
|
18
|
+
return NextResponse.rewrite(
|
|
19
|
+
url,
|
|
20
|
+
options.requestHeaders ? { request: { headers: options.requestHeaders } } : void 0
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function createCmssyEditMiddleware(config) {
|
|
24
|
+
return async function cmssyEditMiddleware(request) {
|
|
25
|
+
return await cmssyEditRewrite(request, config) ?? NextResponse.next();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var probed = false;
|
|
29
|
+
function warnIfEditRouteMissing(url) {
|
|
30
|
+
if (process.env.NODE_ENV === "production" || probed) return;
|
|
31
|
+
probed = true;
|
|
32
|
+
void fetch(url, { method: "HEAD" }).then((response) => {
|
|
33
|
+
if (response.status !== 404) return;
|
|
34
|
+
console.error(
|
|
35
|
+
`[cmssy] The editor request was rewritten to ${url.pathname}, but nothing is mounted there (404). Add the edit route:
|
|
36
|
+
|
|
37
|
+
// app/cmssy-edit/[[...path]]/page.tsx
|
|
38
|
+
export const dynamic = "force-dynamic";
|
|
39
|
+
export default createCmssyEditPage(cmssy, blocks, { editor: CmssyEditor });
|
|
40
|
+
|
|
41
|
+
Until then the editor preview stays blank.`
|
|
42
|
+
);
|
|
43
|
+
}).catch(() => {
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function createCmssyProxy(config, options = {}) {
|
|
47
|
+
return async function cmssyProxy(request) {
|
|
48
|
+
const { pathname } = request.nextUrl;
|
|
49
|
+
const requestHeaders = new Headers(request.headers);
|
|
50
|
+
requestHeaders.delete(CMSSY_EDIT_HEADER);
|
|
51
|
+
requestHeaders.delete(CMSSY_LOCALE_HEADER);
|
|
52
|
+
const locale = await localeForPathname(config, pathname);
|
|
53
|
+
requestHeaders.set(CMSSY_LOCALE_HEADER, locale);
|
|
54
|
+
const editHeaders = new Headers(requestHeaders);
|
|
55
|
+
editHeaders.set(CMSSY_EDIT_HEADER, "1");
|
|
56
|
+
const editRewrite = await cmssyEditRewrite(request, config, {
|
|
57
|
+
requestHeaders: editHeaders
|
|
58
|
+
});
|
|
59
|
+
if (editRewrite) {
|
|
60
|
+
applyCmssyCsp(editRewrite, { editorOrigin: config.editorOrigin });
|
|
61
|
+
return editRewrite;
|
|
62
|
+
}
|
|
63
|
+
if (options.stripLocalePrefix && pathname.startsWith(`/${locale}`)) {
|
|
64
|
+
const { defaultLocale } = await resolveSiteLocales(config);
|
|
65
|
+
if (locale !== defaultLocale) {
|
|
66
|
+
const url = request.nextUrl.clone();
|
|
67
|
+
url.pathname = pathname.slice(locale.length + 1) || "/";
|
|
68
|
+
return NextResponse.rewrite(url, {
|
|
69
|
+
request: { headers: requestHeaders }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var cmssyProxyMatcher = ["/((?!_next/|api/|.*\\..*).*)"];
|
|
77
|
+
async function resolveLocaleFromPathname(config, pathname) {
|
|
78
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
79
|
+
const hasStaticLocales = !!config.enabledLocales?.length;
|
|
80
|
+
const fetched = config.defaultLocale && hasStaticLocales ? null : await resolveSiteLocales({
|
|
81
|
+
apiUrl: config.apiUrl,
|
|
82
|
+
org: config.org,
|
|
83
|
+
workspaceSlug: config.workspaceSlug
|
|
84
|
+
});
|
|
85
|
+
const siteLocales = {
|
|
86
|
+
defaultLocale: config.defaultLocale ?? fetched.defaultLocale,
|
|
87
|
+
locales: hasStaticLocales ? config.enabledLocales : fetched.locales
|
|
88
|
+
};
|
|
89
|
+
return splitLocaleFromPath(segments, siteLocales).locale;
|
|
90
|
+
}
|
|
91
|
+
function createCmssyLocaleMiddleware(config) {
|
|
92
|
+
return async function cmssyLocaleMiddleware(request) {
|
|
93
|
+
const locale = await resolveLocaleFromPathname(
|
|
94
|
+
config,
|
|
95
|
+
request.nextUrl.pathname
|
|
96
|
+
);
|
|
97
|
+
const headers = new Headers(request.headers);
|
|
98
|
+
headers.set(CMSSY_LOCALE_HEADER, locale);
|
|
99
|
+
return NextResponse.next({ request: { headers } });
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function isPrefetch(request) {
|
|
103
|
+
return request.headers.get("next-router-prefetch") !== null || request.headers.get("purpose") === "prefetch" || (request.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
104
|
+
}
|
|
105
|
+
function createCmssyAuthMiddleware(config) {
|
|
106
|
+
const auth = assertAuthConfig(config);
|
|
107
|
+
return async function cmssyAuthMiddleware(request) {
|
|
108
|
+
const raw = request.cookies.get(CMSSY_SESSION_COOKIE)?.value;
|
|
109
|
+
if (!raw) return NextResponse.next();
|
|
110
|
+
const session = await openSession(
|
|
111
|
+
raw,
|
|
112
|
+
auth.sessionSecret,
|
|
113
|
+
config.workspaceSlug
|
|
114
|
+
);
|
|
115
|
+
if (!session) {
|
|
116
|
+
const response = NextResponse.next();
|
|
117
|
+
response.cookies.set(CMSSY_SESSION_COOKIE, "", {
|
|
118
|
+
...sessionCookieOptions(),
|
|
119
|
+
maxAge: 0
|
|
120
|
+
});
|
|
121
|
+
return response;
|
|
122
|
+
}
|
|
123
|
+
if (!isAccessExpired(session)) return NextResponse.next();
|
|
124
|
+
if (isPrefetch(request)) return NextResponse.next();
|
|
125
|
+
let payload = null;
|
|
126
|
+
try {
|
|
127
|
+
const result = await backendRefresh(config, session.refreshToken);
|
|
128
|
+
payload = toSessionPayload(result);
|
|
129
|
+
} catch {
|
|
130
|
+
return NextResponse.next();
|
|
131
|
+
}
|
|
132
|
+
if (!payload) {
|
|
133
|
+
const cleared = NextResponse.next();
|
|
134
|
+
cleared.cookies.set(CMSSY_SESSION_COOKIE, "", {
|
|
135
|
+
...sessionCookieOptions(),
|
|
136
|
+
maxAge: 0
|
|
137
|
+
});
|
|
138
|
+
return cleared;
|
|
139
|
+
}
|
|
140
|
+
const sealed = await sealSession(
|
|
141
|
+
payload,
|
|
142
|
+
auth.sessionSecret,
|
|
143
|
+
config.workspaceSlug
|
|
144
|
+
);
|
|
145
|
+
request.cookies.set(CMSSY_SESSION_COOKIE, sealed);
|
|
146
|
+
const refreshed = NextResponse.next({ request });
|
|
147
|
+
refreshed.cookies.set(CMSSY_SESSION_COOKIE, sealed, sessionCookieOptions());
|
|
148
|
+
return refreshed;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async function isCmssyEditRequest(request, config) {
|
|
152
|
+
if (request.cookies.has("__prerender_bypass")) return true;
|
|
153
|
+
return isVerifiedEditUrl(request.nextUrl, config);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { CMSSY_EDIT_PATH_PREFIX, cmssyEditRewrite, cmssyProxyMatcher, createCmssyAuthMiddleware, createCmssyEditMiddleware, createCmssyLocaleMiddleware, createCmssyProxy, isCmssyEditRequest, resolveLocaleFromPathname };
|