@cmssy/next 0.5.3 → 0.5.5
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 +201 -103
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +202 -105
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -4,12 +4,108 @@ var headers = require('next/headers');
|
|
|
4
4
|
var navigation = require('next/navigation');
|
|
5
5
|
var react = require('@cmssy/react');
|
|
6
6
|
var client = require('@cmssy/react/client');
|
|
7
|
+
var jose = require('jose');
|
|
7
8
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
9
|
var crypto$1 = require('crypto');
|
|
9
10
|
var server = require('next/server');
|
|
10
|
-
var jose = require('jose');
|
|
11
11
|
|
|
12
12
|
// src/create-cmssy-page.tsx
|
|
13
|
+
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
14
|
+
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
15
|
+
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
16
|
+
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
17
|
+
async function deriveSessionKey(secret) {
|
|
18
|
+
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const digest = await crypto.subtle.digest(
|
|
24
|
+
"SHA-256",
|
|
25
|
+
new TextEncoder().encode(secret)
|
|
26
|
+
);
|
|
27
|
+
return new Uint8Array(digest);
|
|
28
|
+
}
|
|
29
|
+
async function sealSession(payload, secret, audience) {
|
|
30
|
+
const key = await deriveSessionKey(secret);
|
|
31
|
+
const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
32
|
+
if (audience) jwt.setAudience(audience);
|
|
33
|
+
return jwt.encrypt(key);
|
|
34
|
+
}
|
|
35
|
+
async function openSession(token, secret, audience) {
|
|
36
|
+
const key = await deriveSessionKey(secret);
|
|
37
|
+
try {
|
|
38
|
+
const { payload } = await jose.jwtDecrypt(token, key, {
|
|
39
|
+
keyManagementAlgorithms: ["dir"],
|
|
40
|
+
contentEncryptionAlgorithms: ["A256GCM"],
|
|
41
|
+
...audience ? { audience } : {}
|
|
42
|
+
});
|
|
43
|
+
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
44
|
+
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
accessToken,
|
|
49
|
+
refreshToken,
|
|
50
|
+
accessExpiresAt,
|
|
51
|
+
user: {
|
|
52
|
+
recordId: user.recordId,
|
|
53
|
+
email: user.email
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isAccessExpired(payload, now = Date.now()) {
|
|
61
|
+
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
62
|
+
}
|
|
63
|
+
function sessionCookieOptions() {
|
|
64
|
+
return {
|
|
65
|
+
httpOnly: true,
|
|
66
|
+
secure: process.env.NODE_ENV !== "development",
|
|
67
|
+
sameSite: "lax",
|
|
68
|
+
path: "/",
|
|
69
|
+
maxAge: SESSION_MAX_AGE_SECONDS
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/config.ts
|
|
74
|
+
function assertAuthConfig(config) {
|
|
75
|
+
const auth = config.auth;
|
|
76
|
+
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
77
|
+
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
78
|
+
}
|
|
79
|
+
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return auth;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/auth-server.ts
|
|
88
|
+
async function readValidSession(config) {
|
|
89
|
+
const auth = assertAuthConfig(config);
|
|
90
|
+
const jar = await headers.cookies();
|
|
91
|
+
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
92
|
+
if (!raw) return null;
|
|
93
|
+
const session = await openSession(
|
|
94
|
+
raw,
|
|
95
|
+
auth.sessionSecret,
|
|
96
|
+
config.workspaceSlug
|
|
97
|
+
);
|
|
98
|
+
if (!session || isAccessExpired(session)) return null;
|
|
99
|
+
return session;
|
|
100
|
+
}
|
|
101
|
+
async function getCmssyUser(config) {
|
|
102
|
+
const session = await readValidSession(config);
|
|
103
|
+
return session?.user ?? null;
|
|
104
|
+
}
|
|
105
|
+
async function getCmssyAccessToken(config) {
|
|
106
|
+
const session = await readValidSession(config);
|
|
107
|
+
return session?.accessToken ?? null;
|
|
108
|
+
}
|
|
13
109
|
|
|
14
110
|
// src/csp.ts
|
|
15
111
|
function toCspOrigin(origin) {
|
|
@@ -70,6 +166,7 @@ function createCmssyPage(config, blocks, options) {
|
|
|
70
166
|
apiUrl: config.apiUrl,
|
|
71
167
|
workspaceSlug: config.workspaceSlug
|
|
72
168
|
};
|
|
169
|
+
const client$1 = react.createCmssyClient(clientConfig);
|
|
73
170
|
return async function CmssyCatchAllPage({
|
|
74
171
|
params,
|
|
75
172
|
searchParams
|
|
@@ -131,6 +228,27 @@ function createCmssyPage(config, blocks, options) {
|
|
|
131
228
|
}
|
|
132
229
|
) });
|
|
133
230
|
}
|
|
231
|
+
let auth;
|
|
232
|
+
if (config.auth) {
|
|
233
|
+
try {
|
|
234
|
+
const user = await getCmssyUser(config);
|
|
235
|
+
auth = {
|
|
236
|
+
isAuthenticated: !!user,
|
|
237
|
+
member: user ? { recordId: user.recordId, email: user.email } : null
|
|
238
|
+
};
|
|
239
|
+
} catch {
|
|
240
|
+
auth = void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let workspace;
|
|
244
|
+
try {
|
|
245
|
+
workspace = {
|
|
246
|
+
id: await client$1.resolveWorkspaceId(),
|
|
247
|
+
slug: config.workspaceSlug
|
|
248
|
+
};
|
|
249
|
+
} catch {
|
|
250
|
+
workspace = void 0;
|
|
251
|
+
}
|
|
134
252
|
return /* @__PURE__ */ jsxRuntime.jsx(client.CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
135
253
|
react.CmssyServerPage,
|
|
136
254
|
{
|
|
@@ -139,7 +257,9 @@ function createCmssyPage(config, blocks, options) {
|
|
|
139
257
|
locale,
|
|
140
258
|
defaultLocale,
|
|
141
259
|
enabledLocales,
|
|
142
|
-
forms
|
|
260
|
+
forms,
|
|
261
|
+
auth,
|
|
262
|
+
workspace
|
|
143
263
|
}
|
|
144
264
|
) });
|
|
145
265
|
};
|
|
@@ -288,14 +408,24 @@ function createCmssyRobots(config, options = {}) {
|
|
|
288
408
|
};
|
|
289
409
|
};
|
|
290
410
|
}
|
|
411
|
+
|
|
412
|
+
// src/seo-paths.ts
|
|
413
|
+
function resolveSeoLocales(config, siteConfig) {
|
|
414
|
+
const defaultLocale = config.defaultLocale ?? siteConfig?.defaultLanguage ?? "en";
|
|
415
|
+
const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : siteConfig?.enabledLanguages && siteConfig.enabledLanguages.length > 0 ? siteConfig.enabledLanguages : [defaultLocale];
|
|
416
|
+
return { defaultLocale, locales };
|
|
417
|
+
}
|
|
291
418
|
function normalizeSlug(slug) {
|
|
292
419
|
if (slug === "/" || slug === "") return "/";
|
|
293
420
|
return slug.startsWith("/") ? slug : `/${slug}`;
|
|
294
421
|
}
|
|
295
422
|
function localizedPath(slug, locale, defaultLocale) {
|
|
296
|
-
const normalized = slug
|
|
297
|
-
|
|
423
|
+
const normalized = normalizeSlug(slug);
|
|
424
|
+
const base = normalized === "/" ? "" : normalized;
|
|
425
|
+
return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
|
|
298
426
|
}
|
|
427
|
+
|
|
428
|
+
// src/create-cmssy-sitemap.ts
|
|
299
429
|
function createCmssySitemap(config, options = {}) {
|
|
300
430
|
const clientConfig = {
|
|
301
431
|
apiUrl: config.apiUrl,
|
|
@@ -311,15 +441,15 @@ function createCmssySitemap(config, options = {}) {
|
|
|
311
441
|
}
|
|
312
442
|
pages = [];
|
|
313
443
|
}
|
|
314
|
-
let
|
|
444
|
+
let siteConfig = null;
|
|
315
445
|
try {
|
|
316
|
-
|
|
446
|
+
siteConfig = await react.fetchSiteConfig(clientConfig);
|
|
317
447
|
} catch {
|
|
318
|
-
|
|
448
|
+
siteConfig = null;
|
|
319
449
|
}
|
|
450
|
+
const notFoundPageId = siteConfig?.notFoundPageId ?? null;
|
|
320
451
|
const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
|
|
321
|
-
const defaultLocale = config
|
|
322
|
-
const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
|
|
452
|
+
const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
|
|
323
453
|
const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
|
|
324
454
|
const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
|
|
325
455
|
const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
|
|
@@ -342,6 +472,67 @@ function createCmssySitemap(config, options = {}) {
|
|
|
342
472
|
return options.extra ? [...entries, ...options.extra] : entries;
|
|
343
473
|
};
|
|
344
474
|
}
|
|
475
|
+
function pick(value, locale, defaultLocale) {
|
|
476
|
+
if (!value) return "";
|
|
477
|
+
if (typeof value === "string") return value;
|
|
478
|
+
return value[locale] || value[defaultLocale] || Object.values(value)[0] || "";
|
|
479
|
+
}
|
|
480
|
+
async function buildCmssyMetadata(config, path, options = {}) {
|
|
481
|
+
const clientConfig = {
|
|
482
|
+
apiUrl: config.apiUrl,
|
|
483
|
+
workspaceSlug: config.workspaceSlug
|
|
484
|
+
};
|
|
485
|
+
const [meta, siteConfig, baseUrl] = await Promise.all([
|
|
486
|
+
react.fetchPageMeta(clientConfig, path).catch(() => null),
|
|
487
|
+
react.fetchSiteConfig(clientConfig).catch(() => null),
|
|
488
|
+
resolveSeoBaseUrl(config, options.baseUrl)
|
|
489
|
+
]);
|
|
490
|
+
const { defaultLocale, locales: enabledLocales } = resolveSeoLocales(
|
|
491
|
+
config,
|
|
492
|
+
siteConfig
|
|
493
|
+
);
|
|
494
|
+
const locale = await config.resolveLocale?.() ?? defaultLocale;
|
|
495
|
+
const slug = react.normalizeSlug(path);
|
|
496
|
+
const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
|
|
497
|
+
const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
|
|
498
|
+
const description = pick(meta?.seoDescription, locale, defaultLocale);
|
|
499
|
+
const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
|
|
500
|
+
const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
|
|
501
|
+
const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
|
|
502
|
+
const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
|
|
503
|
+
enabledLocales.map((l) => [
|
|
504
|
+
l,
|
|
505
|
+
`${baseUrl}${localizedPath(slug, l, defaultLocale)}`
|
|
506
|
+
])
|
|
507
|
+
) : void 0;
|
|
508
|
+
return {
|
|
509
|
+
...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
|
|
510
|
+
...title ? { title } : {},
|
|
511
|
+
...description ? { description } : {},
|
|
512
|
+
...keywords ? { keywords } : {},
|
|
513
|
+
...canonical || languages ? {
|
|
514
|
+
alternates: {
|
|
515
|
+
...canonical ? { canonical } : {},
|
|
516
|
+
...languages ? { languages } : {}
|
|
517
|
+
}
|
|
518
|
+
} : {},
|
|
519
|
+
openGraph: {
|
|
520
|
+
...title ? { title } : {},
|
|
521
|
+
...description ? { description } : {},
|
|
522
|
+
...canonical ? { url: canonical } : {},
|
|
523
|
+
...siteName ? { siteName } : {},
|
|
524
|
+
type: options.ogType ?? "website",
|
|
525
|
+
locale,
|
|
526
|
+
...image ? { images: [{ url: image }] } : {}
|
|
527
|
+
},
|
|
528
|
+
twitter: {
|
|
529
|
+
card: image ? options.twitterCard ?? "summary_large_image" : "summary",
|
|
530
|
+
...title ? { title } : {},
|
|
531
|
+
...description ? { description } : {},
|
|
532
|
+
...image ? { images: [image] } : {}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
345
536
|
var MIN_SECRET_LENGTH = 16;
|
|
346
537
|
function secretsMatch(a, b) {
|
|
347
538
|
const ha = crypto$1.createHash("sha256").update(a).digest();
|
|
@@ -438,79 +629,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
438
629
|
return server.NextResponse.next({ request: { headers: headers3 } });
|
|
439
630
|
};
|
|
440
631
|
}
|
|
441
|
-
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
442
|
-
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
443
|
-
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
444
|
-
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
445
|
-
async function deriveSessionKey(secret) {
|
|
446
|
-
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
const digest = await crypto.subtle.digest(
|
|
452
|
-
"SHA-256",
|
|
453
|
-
new TextEncoder().encode(secret)
|
|
454
|
-
);
|
|
455
|
-
return new Uint8Array(digest);
|
|
456
|
-
}
|
|
457
|
-
async function sealSession(payload, secret, audience) {
|
|
458
|
-
const key = await deriveSessionKey(secret);
|
|
459
|
-
const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
460
|
-
if (audience) jwt.setAudience(audience);
|
|
461
|
-
return jwt.encrypt(key);
|
|
462
|
-
}
|
|
463
|
-
async function openSession(token, secret, audience) {
|
|
464
|
-
const key = await deriveSessionKey(secret);
|
|
465
|
-
try {
|
|
466
|
-
const { payload } = await jose.jwtDecrypt(token, key, {
|
|
467
|
-
keyManagementAlgorithms: ["dir"],
|
|
468
|
-
contentEncryptionAlgorithms: ["A256GCM"],
|
|
469
|
-
...audience ? { audience } : {}
|
|
470
|
-
});
|
|
471
|
-
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
472
|
-
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
accessToken,
|
|
477
|
-
refreshToken,
|
|
478
|
-
accessExpiresAt,
|
|
479
|
-
user: {
|
|
480
|
-
recordId: user.recordId,
|
|
481
|
-
email: user.email
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
} catch {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function isAccessExpired(payload, now = Date.now()) {
|
|
489
|
-
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
490
|
-
}
|
|
491
|
-
function sessionCookieOptions() {
|
|
492
|
-
return {
|
|
493
|
-
httpOnly: true,
|
|
494
|
-
secure: process.env.NODE_ENV !== "development",
|
|
495
|
-
sameSite: "lax",
|
|
496
|
-
path: "/",
|
|
497
|
-
maxAge: SESSION_MAX_AGE_SECONDS
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// src/config.ts
|
|
502
|
-
function assertAuthConfig(config) {
|
|
503
|
-
const auth = config.auth;
|
|
504
|
-
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
505
|
-
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
506
|
-
}
|
|
507
|
-
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
508
|
-
throw new Error(
|
|
509
|
-
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
return auth;
|
|
513
|
-
}
|
|
514
632
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
515
633
|
siteMemberLogin(input: $input) {
|
|
516
634
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -1218,27 +1336,6 @@ function createCmssyCartRoute(config) {
|
|
|
1218
1336
|
}
|
|
1219
1337
|
};
|
|
1220
1338
|
}
|
|
1221
|
-
async function readValidSession(config) {
|
|
1222
|
-
const auth = assertAuthConfig(config);
|
|
1223
|
-
const jar = await headers.cookies();
|
|
1224
|
-
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
1225
|
-
if (!raw) return null;
|
|
1226
|
-
const session = await openSession(
|
|
1227
|
-
raw,
|
|
1228
|
-
auth.sessionSecret,
|
|
1229
|
-
config.workspaceSlug
|
|
1230
|
-
);
|
|
1231
|
-
if (!session || isAccessExpired(session)) return null;
|
|
1232
|
-
return session;
|
|
1233
|
-
}
|
|
1234
|
-
async function getCmssyUser(config) {
|
|
1235
|
-
const session = await readValidSession(config);
|
|
1236
|
-
return session?.user ?? null;
|
|
1237
|
-
}
|
|
1238
|
-
async function getCmssyAccessToken(config) {
|
|
1239
|
-
const session = await readValidSession(config);
|
|
1240
|
-
return session?.accessToken ?? null;
|
|
1241
|
-
}
|
|
1242
1339
|
function isPrefetch(request2) {
|
|
1243
1340
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1244
1341
|
}
|
|
@@ -1518,6 +1615,7 @@ exports.CmssyWebhookError = CmssyWebhookError;
|
|
|
1518
1615
|
exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
|
|
1519
1616
|
exports.applyCmssyCsp = applyCmssyCsp;
|
|
1520
1617
|
exports.assertAuthConfig = assertAuthConfig;
|
|
1618
|
+
exports.buildCmssyMetadata = buildCmssyMetadata;
|
|
1521
1619
|
exports.cmssyCspHeaders = cmssyCspHeaders;
|
|
1522
1620
|
exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
|
|
1523
1621
|
exports.createCmssyAuthRoute = createCmssyAuthRoute;
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { ComponentType, ReactNode } from 'react';
|
|
3
3
|
import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
|
|
4
4
|
import { EditBridgeConfig } from '@cmssy/react/client';
|
|
5
|
-
import { MetadataRoute } from 'next';
|
|
5
|
+
import { MetadataRoute, Metadata } from 'next';
|
|
6
6
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
7
|
|
|
8
8
|
interface CmssyAuthConfig {
|
|
@@ -114,6 +114,27 @@ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
|
|
|
114
114
|
*/
|
|
115
115
|
declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
|
|
116
116
|
|
|
117
|
+
interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
|
|
118
|
+
/** Override the Open Graph / Twitter image (defaults to workspace branding). */
|
|
119
|
+
image?: string;
|
|
120
|
+
/** Open Graph type. Defaults to "website". */
|
|
121
|
+
ogType?: string;
|
|
122
|
+
/** Twitter card. Defaults to "summary_large_image" when an image exists. */
|
|
123
|
+
twitterCard?: "summary" | "summary_large_image";
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
|
|
127
|
+
* the workspace branding: title/description/keywords, canonical + per-locale
|
|
128
|
+
* `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
|
|
129
|
+
* image). Use in a route's `generateMetadata`:
|
|
130
|
+
*
|
|
131
|
+
* export const generateMetadata = ({ params }) =>
|
|
132
|
+
* buildCmssyMetadata(cmssy, (await params).path);
|
|
133
|
+
*
|
|
134
|
+
* `path` is the catch-all segments with the locale prefix already stripped.
|
|
135
|
+
*/
|
|
136
|
+
declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
|
|
137
|
+
|
|
117
138
|
type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
|
|
118
139
|
defaultRedirect?: string;
|
|
119
140
|
};
|
|
@@ -307,4 +328,4 @@ declare class CmssyWebhookError extends Error {
|
|
|
307
328
|
*/
|
|
308
329
|
declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
|
|
309
330
|
|
|
310
|
-
export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
|
331
|
+
export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { ComponentType, ReactNode } from 'react';
|
|
3
3
|
import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
|
|
4
4
|
import { EditBridgeConfig } from '@cmssy/react/client';
|
|
5
|
-
import { MetadataRoute } from 'next';
|
|
5
|
+
import { MetadataRoute, Metadata } from 'next';
|
|
6
6
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
7
|
|
|
8
8
|
interface CmssyAuthConfig {
|
|
@@ -114,6 +114,27 @@ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
|
|
|
114
114
|
*/
|
|
115
115
|
declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
|
|
116
116
|
|
|
117
|
+
interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
|
|
118
|
+
/** Override the Open Graph / Twitter image (defaults to workspace branding). */
|
|
119
|
+
image?: string;
|
|
120
|
+
/** Open Graph type. Defaults to "website". */
|
|
121
|
+
ogType?: string;
|
|
122
|
+
/** Twitter card. Defaults to "summary_large_image" when an image exists. */
|
|
123
|
+
twitterCard?: "summary" | "summary_large_image";
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
|
|
127
|
+
* the workspace branding: title/description/keywords, canonical + per-locale
|
|
128
|
+
* `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
|
|
129
|
+
* image). Use in a route's `generateMetadata`:
|
|
130
|
+
*
|
|
131
|
+
* export const generateMetadata = ({ params }) =>
|
|
132
|
+
* buildCmssyMetadata(cmssy, (await params).path);
|
|
133
|
+
*
|
|
134
|
+
* `path` is the catch-all segments with the locale prefix already stripped.
|
|
135
|
+
*/
|
|
136
|
+
declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
|
|
137
|
+
|
|
117
138
|
type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
|
|
118
139
|
defaultRedirect?: string;
|
|
119
140
|
};
|
|
@@ -307,4 +328,4 @@ declare class CmssyWebhookError extends Error {
|
|
|
307
328
|
*/
|
|
308
329
|
declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
|
|
309
330
|
|
|
310
|
-
export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
|
331
|
+
export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,109 @@
|
|
|
1
1
|
import { draftMode, headers, cookies } from 'next/headers';
|
|
2
2
|
import { notFound, redirect } from 'next/navigation';
|
|
3
|
-
import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
|
|
3
|
+
import { createCmssyClient, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
|
|
4
4
|
import { CmssyLocaleProvider } from '@cmssy/react/client';
|
|
5
|
+
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
5
6
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
7
|
import { createHmac, createHash, timingSafeEqual } from 'crypto';
|
|
7
8
|
import { NextResponse } from 'next/server';
|
|
8
|
-
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
9
9
|
|
|
10
10
|
// src/create-cmssy-page.tsx
|
|
11
|
+
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
12
|
+
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
13
|
+
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
14
|
+
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
15
|
+
async function deriveSessionKey(secret) {
|
|
16
|
+
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const digest = await crypto.subtle.digest(
|
|
22
|
+
"SHA-256",
|
|
23
|
+
new TextEncoder().encode(secret)
|
|
24
|
+
);
|
|
25
|
+
return new Uint8Array(digest);
|
|
26
|
+
}
|
|
27
|
+
async function sealSession(payload, secret, audience) {
|
|
28
|
+
const key = await deriveSessionKey(secret);
|
|
29
|
+
const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
30
|
+
if (audience) jwt.setAudience(audience);
|
|
31
|
+
return jwt.encrypt(key);
|
|
32
|
+
}
|
|
33
|
+
async function openSession(token, secret, audience) {
|
|
34
|
+
const key = await deriveSessionKey(secret);
|
|
35
|
+
try {
|
|
36
|
+
const { payload } = await jwtDecrypt(token, key, {
|
|
37
|
+
keyManagementAlgorithms: ["dir"],
|
|
38
|
+
contentEncryptionAlgorithms: ["A256GCM"],
|
|
39
|
+
...audience ? { audience } : {}
|
|
40
|
+
});
|
|
41
|
+
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
42
|
+
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
accessToken,
|
|
47
|
+
refreshToken,
|
|
48
|
+
accessExpiresAt,
|
|
49
|
+
user: {
|
|
50
|
+
recordId: user.recordId,
|
|
51
|
+
email: user.email
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isAccessExpired(payload, now = Date.now()) {
|
|
59
|
+
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
60
|
+
}
|
|
61
|
+
function sessionCookieOptions() {
|
|
62
|
+
return {
|
|
63
|
+
httpOnly: true,
|
|
64
|
+
secure: process.env.NODE_ENV !== "development",
|
|
65
|
+
sameSite: "lax",
|
|
66
|
+
path: "/",
|
|
67
|
+
maxAge: SESSION_MAX_AGE_SECONDS
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/config.ts
|
|
72
|
+
function assertAuthConfig(config) {
|
|
73
|
+
const auth = config.auth;
|
|
74
|
+
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
75
|
+
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
76
|
+
}
|
|
77
|
+
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return auth;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/auth-server.ts
|
|
86
|
+
async function readValidSession(config) {
|
|
87
|
+
const auth = assertAuthConfig(config);
|
|
88
|
+
const jar = await cookies();
|
|
89
|
+
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
90
|
+
if (!raw) return null;
|
|
91
|
+
const session = await openSession(
|
|
92
|
+
raw,
|
|
93
|
+
auth.sessionSecret,
|
|
94
|
+
config.workspaceSlug
|
|
95
|
+
);
|
|
96
|
+
if (!session || isAccessExpired(session)) return null;
|
|
97
|
+
return session;
|
|
98
|
+
}
|
|
99
|
+
async function getCmssyUser(config) {
|
|
100
|
+
const session = await readValidSession(config);
|
|
101
|
+
return session?.user ?? null;
|
|
102
|
+
}
|
|
103
|
+
async function getCmssyAccessToken(config) {
|
|
104
|
+
const session = await readValidSession(config);
|
|
105
|
+
return session?.accessToken ?? null;
|
|
106
|
+
}
|
|
11
107
|
|
|
12
108
|
// src/csp.ts
|
|
13
109
|
function toCspOrigin(origin) {
|
|
@@ -68,6 +164,7 @@ function createCmssyPage(config, blocks, options) {
|
|
|
68
164
|
apiUrl: config.apiUrl,
|
|
69
165
|
workspaceSlug: config.workspaceSlug
|
|
70
166
|
};
|
|
167
|
+
const client = createCmssyClient(clientConfig);
|
|
71
168
|
return async function CmssyCatchAllPage({
|
|
72
169
|
params,
|
|
73
170
|
searchParams
|
|
@@ -129,6 +226,27 @@ function createCmssyPage(config, blocks, options) {
|
|
|
129
226
|
}
|
|
130
227
|
) });
|
|
131
228
|
}
|
|
229
|
+
let auth;
|
|
230
|
+
if (config.auth) {
|
|
231
|
+
try {
|
|
232
|
+
const user = await getCmssyUser(config);
|
|
233
|
+
auth = {
|
|
234
|
+
isAuthenticated: !!user,
|
|
235
|
+
member: user ? { recordId: user.recordId, email: user.email } : null
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
auth = void 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let workspace;
|
|
242
|
+
try {
|
|
243
|
+
workspace = {
|
|
244
|
+
id: await client.resolveWorkspaceId(),
|
|
245
|
+
slug: config.workspaceSlug
|
|
246
|
+
};
|
|
247
|
+
} catch {
|
|
248
|
+
workspace = void 0;
|
|
249
|
+
}
|
|
132
250
|
return /* @__PURE__ */ jsx(CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsx(
|
|
133
251
|
CmssyServerPage,
|
|
134
252
|
{
|
|
@@ -137,7 +255,9 @@ function createCmssyPage(config, blocks, options) {
|
|
|
137
255
|
locale,
|
|
138
256
|
defaultLocale,
|
|
139
257
|
enabledLocales,
|
|
140
|
-
forms
|
|
258
|
+
forms,
|
|
259
|
+
auth,
|
|
260
|
+
workspace
|
|
141
261
|
}
|
|
142
262
|
) });
|
|
143
263
|
};
|
|
@@ -286,14 +406,24 @@ function createCmssyRobots(config, options = {}) {
|
|
|
286
406
|
};
|
|
287
407
|
};
|
|
288
408
|
}
|
|
409
|
+
|
|
410
|
+
// src/seo-paths.ts
|
|
411
|
+
function resolveSeoLocales(config, siteConfig) {
|
|
412
|
+
const defaultLocale = config.defaultLocale ?? siteConfig?.defaultLanguage ?? "en";
|
|
413
|
+
const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : siteConfig?.enabledLanguages && siteConfig.enabledLanguages.length > 0 ? siteConfig.enabledLanguages : [defaultLocale];
|
|
414
|
+
return { defaultLocale, locales };
|
|
415
|
+
}
|
|
289
416
|
function normalizeSlug(slug) {
|
|
290
417
|
if (slug === "/" || slug === "") return "/";
|
|
291
418
|
return slug.startsWith("/") ? slug : `/${slug}`;
|
|
292
419
|
}
|
|
293
420
|
function localizedPath(slug, locale, defaultLocale) {
|
|
294
|
-
const normalized = slug
|
|
295
|
-
|
|
421
|
+
const normalized = normalizeSlug(slug);
|
|
422
|
+
const base = normalized === "/" ? "" : normalized;
|
|
423
|
+
return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
|
|
296
424
|
}
|
|
425
|
+
|
|
426
|
+
// src/create-cmssy-sitemap.ts
|
|
297
427
|
function createCmssySitemap(config, options = {}) {
|
|
298
428
|
const clientConfig = {
|
|
299
429
|
apiUrl: config.apiUrl,
|
|
@@ -309,15 +439,15 @@ function createCmssySitemap(config, options = {}) {
|
|
|
309
439
|
}
|
|
310
440
|
pages = [];
|
|
311
441
|
}
|
|
312
|
-
let
|
|
442
|
+
let siteConfig = null;
|
|
313
443
|
try {
|
|
314
|
-
|
|
444
|
+
siteConfig = await fetchSiteConfig(clientConfig);
|
|
315
445
|
} catch {
|
|
316
|
-
|
|
446
|
+
siteConfig = null;
|
|
317
447
|
}
|
|
448
|
+
const notFoundPageId = siteConfig?.notFoundPageId ?? null;
|
|
318
449
|
const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
|
|
319
|
-
const defaultLocale = config
|
|
320
|
-
const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
|
|
450
|
+
const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
|
|
321
451
|
const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
|
|
322
452
|
const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
|
|
323
453
|
const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
|
|
@@ -340,6 +470,67 @@ function createCmssySitemap(config, options = {}) {
|
|
|
340
470
|
return options.extra ? [...entries, ...options.extra] : entries;
|
|
341
471
|
};
|
|
342
472
|
}
|
|
473
|
+
function pick(value, locale, defaultLocale) {
|
|
474
|
+
if (!value) return "";
|
|
475
|
+
if (typeof value === "string") return value;
|
|
476
|
+
return value[locale] || value[defaultLocale] || Object.values(value)[0] || "";
|
|
477
|
+
}
|
|
478
|
+
async function buildCmssyMetadata(config, path, options = {}) {
|
|
479
|
+
const clientConfig = {
|
|
480
|
+
apiUrl: config.apiUrl,
|
|
481
|
+
workspaceSlug: config.workspaceSlug
|
|
482
|
+
};
|
|
483
|
+
const [meta, siteConfig, baseUrl] = await Promise.all([
|
|
484
|
+
fetchPageMeta(clientConfig, path).catch(() => null),
|
|
485
|
+
fetchSiteConfig(clientConfig).catch(() => null),
|
|
486
|
+
resolveSeoBaseUrl(config, options.baseUrl)
|
|
487
|
+
]);
|
|
488
|
+
const { defaultLocale, locales: enabledLocales } = resolveSeoLocales(
|
|
489
|
+
config,
|
|
490
|
+
siteConfig
|
|
491
|
+
);
|
|
492
|
+
const locale = await config.resolveLocale?.() ?? defaultLocale;
|
|
493
|
+
const slug = normalizeSlug$1(path);
|
|
494
|
+
const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
|
|
495
|
+
const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
|
|
496
|
+
const description = pick(meta?.seoDescription, locale, defaultLocale);
|
|
497
|
+
const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
|
|
498
|
+
const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
|
|
499
|
+
const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
|
|
500
|
+
const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
|
|
501
|
+
enabledLocales.map((l) => [
|
|
502
|
+
l,
|
|
503
|
+
`${baseUrl}${localizedPath(slug, l, defaultLocale)}`
|
|
504
|
+
])
|
|
505
|
+
) : void 0;
|
|
506
|
+
return {
|
|
507
|
+
...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
|
|
508
|
+
...title ? { title } : {},
|
|
509
|
+
...description ? { description } : {},
|
|
510
|
+
...keywords ? { keywords } : {},
|
|
511
|
+
...canonical || languages ? {
|
|
512
|
+
alternates: {
|
|
513
|
+
...canonical ? { canonical } : {},
|
|
514
|
+
...languages ? { languages } : {}
|
|
515
|
+
}
|
|
516
|
+
} : {},
|
|
517
|
+
openGraph: {
|
|
518
|
+
...title ? { title } : {},
|
|
519
|
+
...description ? { description } : {},
|
|
520
|
+
...canonical ? { url: canonical } : {},
|
|
521
|
+
...siteName ? { siteName } : {},
|
|
522
|
+
type: options.ogType ?? "website",
|
|
523
|
+
locale,
|
|
524
|
+
...image ? { images: [{ url: image }] } : {}
|
|
525
|
+
},
|
|
526
|
+
twitter: {
|
|
527
|
+
card: image ? options.twitterCard ?? "summary_large_image" : "summary",
|
|
528
|
+
...title ? { title } : {},
|
|
529
|
+
...description ? { description } : {},
|
|
530
|
+
...image ? { images: [image] } : {}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
343
534
|
var MIN_SECRET_LENGTH = 16;
|
|
344
535
|
function secretsMatch(a, b) {
|
|
345
536
|
const ha = createHash("sha256").update(a).digest();
|
|
@@ -436,79 +627,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
436
627
|
return NextResponse.next({ request: { headers: headers3 } });
|
|
437
628
|
};
|
|
438
629
|
}
|
|
439
|
-
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
440
|
-
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
441
|
-
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
442
|
-
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
443
|
-
async function deriveSessionKey(secret) {
|
|
444
|
-
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
445
|
-
throw new Error(
|
|
446
|
-
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
const digest = await crypto.subtle.digest(
|
|
450
|
-
"SHA-256",
|
|
451
|
-
new TextEncoder().encode(secret)
|
|
452
|
-
);
|
|
453
|
-
return new Uint8Array(digest);
|
|
454
|
-
}
|
|
455
|
-
async function sealSession(payload, secret, audience) {
|
|
456
|
-
const key = await deriveSessionKey(secret);
|
|
457
|
-
const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
458
|
-
if (audience) jwt.setAudience(audience);
|
|
459
|
-
return jwt.encrypt(key);
|
|
460
|
-
}
|
|
461
|
-
async function openSession(token, secret, audience) {
|
|
462
|
-
const key = await deriveSessionKey(secret);
|
|
463
|
-
try {
|
|
464
|
-
const { payload } = await jwtDecrypt(token, key, {
|
|
465
|
-
keyManagementAlgorithms: ["dir"],
|
|
466
|
-
contentEncryptionAlgorithms: ["A256GCM"],
|
|
467
|
-
...audience ? { audience } : {}
|
|
468
|
-
});
|
|
469
|
-
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
470
|
-
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
return {
|
|
474
|
-
accessToken,
|
|
475
|
-
refreshToken,
|
|
476
|
-
accessExpiresAt,
|
|
477
|
-
user: {
|
|
478
|
-
recordId: user.recordId,
|
|
479
|
-
email: user.email
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
} catch {
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function isAccessExpired(payload, now = Date.now()) {
|
|
487
|
-
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
488
|
-
}
|
|
489
|
-
function sessionCookieOptions() {
|
|
490
|
-
return {
|
|
491
|
-
httpOnly: true,
|
|
492
|
-
secure: process.env.NODE_ENV !== "development",
|
|
493
|
-
sameSite: "lax",
|
|
494
|
-
path: "/",
|
|
495
|
-
maxAge: SESSION_MAX_AGE_SECONDS
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// src/config.ts
|
|
500
|
-
function assertAuthConfig(config) {
|
|
501
|
-
const auth = config.auth;
|
|
502
|
-
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
503
|
-
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
504
|
-
}
|
|
505
|
-
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
506
|
-
throw new Error(
|
|
507
|
-
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
return auth;
|
|
511
|
-
}
|
|
512
630
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
513
631
|
siteMemberLogin(input: $input) {
|
|
514
632
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -1216,27 +1334,6 @@ function createCmssyCartRoute(config) {
|
|
|
1216
1334
|
}
|
|
1217
1335
|
};
|
|
1218
1336
|
}
|
|
1219
|
-
async function readValidSession(config) {
|
|
1220
|
-
const auth = assertAuthConfig(config);
|
|
1221
|
-
const jar = await cookies();
|
|
1222
|
-
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
1223
|
-
if (!raw) return null;
|
|
1224
|
-
const session = await openSession(
|
|
1225
|
-
raw,
|
|
1226
|
-
auth.sessionSecret,
|
|
1227
|
-
config.workspaceSlug
|
|
1228
|
-
);
|
|
1229
|
-
if (!session || isAccessExpired(session)) return null;
|
|
1230
|
-
return session;
|
|
1231
|
-
}
|
|
1232
|
-
async function getCmssyUser(config) {
|
|
1233
|
-
const session = await readValidSession(config);
|
|
1234
|
-
return session?.user ?? null;
|
|
1235
|
-
}
|
|
1236
|
-
async function getCmssyAccessToken(config) {
|
|
1237
|
-
const session = await readValidSession(config);
|
|
1238
|
-
return session?.accessToken ?? null;
|
|
1239
|
-
}
|
|
1240
1337
|
function isPrefetch(request2) {
|
|
1241
1338
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1242
1339
|
}
|
|
@@ -1508,4 +1605,4 @@ function verifyCmssyWebhook(options) {
|
|
|
1508
1605
|
return parsed;
|
|
1509
1606
|
}
|
|
1510
1607
|
|
|
1511
|
-
export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
|
1608
|
+
export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmssy/next",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Next.js App Router bindings for cmssy headless sites (createCmssyPage + draft preview)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cmssy",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dist"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@cmssy/react": "^0.5.
|
|
44
|
+
"@cmssy/react": "^0.5.5",
|
|
45
45
|
"next": ">=15",
|
|
46
46
|
"react": "^18.2.0 || ^19.0.0",
|
|
47
47
|
"react-dom": "^18.2.0 || ^19.0.0"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"tsup": "^8.3.0",
|
|
55
55
|
"typescript": "^5.6.0",
|
|
56
56
|
"vitest": "^2.1.0",
|
|
57
|
-
"@cmssy/react": "0.5.
|
|
57
|
+
"@cmssy/react": "0.5.5"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"jose": "^6.2.3"
|