@cmssy/next 0.5.4 → 0.5.6
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 +149 -102
- package/dist/index.d.cts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +142 -104
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -4,12 +4,117 @@ 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
|
+
var DEFAULT_CMSSY_EDITOR_ORIGIN = "https://www.cmssy.io";
|
|
75
|
+
function resolveEditorOrigin(editorOrigin) {
|
|
76
|
+
if (editorOrigin === void 0) return DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
77
|
+
if (Array.isArray(editorOrigin)) {
|
|
78
|
+
const cleaned = editorOrigin.filter((o) => o && o.trim().length > 0);
|
|
79
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
80
|
+
}
|
|
81
|
+
return editorOrigin.trim().length > 0 ? editorOrigin : DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
82
|
+
}
|
|
83
|
+
function assertAuthConfig(config) {
|
|
84
|
+
const auth = config.auth;
|
|
85
|
+
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
86
|
+
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
87
|
+
}
|
|
88
|
+
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return auth;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/auth-server.ts
|
|
97
|
+
async function readValidSession(config) {
|
|
98
|
+
const auth = assertAuthConfig(config);
|
|
99
|
+
const jar = await headers.cookies();
|
|
100
|
+
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
101
|
+
if (!raw) return null;
|
|
102
|
+
const session = await openSession(
|
|
103
|
+
raw,
|
|
104
|
+
auth.sessionSecret,
|
|
105
|
+
config.workspaceSlug
|
|
106
|
+
);
|
|
107
|
+
if (!session || isAccessExpired(session)) return null;
|
|
108
|
+
return session;
|
|
109
|
+
}
|
|
110
|
+
async function getCmssyUser(config) {
|
|
111
|
+
const session = await readValidSession(config);
|
|
112
|
+
return session?.user ?? null;
|
|
113
|
+
}
|
|
114
|
+
async function getCmssyAccessToken(config) {
|
|
115
|
+
const session = await readValidSession(config);
|
|
116
|
+
return session?.accessToken ?? null;
|
|
117
|
+
}
|
|
13
118
|
|
|
14
119
|
// src/csp.ts
|
|
15
120
|
function toCspOrigin(origin) {
|
|
@@ -26,7 +131,8 @@ function toCspOrigin(origin) {
|
|
|
26
131
|
return parsed.origin;
|
|
27
132
|
}
|
|
28
133
|
function frameAncestors(editorOrigin) {
|
|
29
|
-
const
|
|
134
|
+
const resolved = resolveEditorOrigin(editorOrigin);
|
|
135
|
+
const origins = Array.isArray(resolved) ? resolved : [resolved];
|
|
30
136
|
if (origins.length === 0) {
|
|
31
137
|
throw new Error(
|
|
32
138
|
"cmssy: editorOrigin must contain at least one valid origin"
|
|
@@ -70,6 +176,7 @@ function createCmssyPage(config, blocks, options) {
|
|
|
70
176
|
apiUrl: config.apiUrl,
|
|
71
177
|
workspaceSlug: config.workspaceSlug
|
|
72
178
|
};
|
|
179
|
+
const client$1 = react.createCmssyClient(clientConfig);
|
|
73
180
|
return async function CmssyCatchAllPage({
|
|
74
181
|
params,
|
|
75
182
|
searchParams
|
|
@@ -131,6 +238,27 @@ function createCmssyPage(config, blocks, options) {
|
|
|
131
238
|
}
|
|
132
239
|
) });
|
|
133
240
|
}
|
|
241
|
+
let auth;
|
|
242
|
+
if (config.auth) {
|
|
243
|
+
try {
|
|
244
|
+
const user = await getCmssyUser(config);
|
|
245
|
+
auth = {
|
|
246
|
+
isAuthenticated: !!user,
|
|
247
|
+
member: user ? { recordId: user.recordId, email: user.email } : null
|
|
248
|
+
};
|
|
249
|
+
} catch {
|
|
250
|
+
auth = void 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let workspace;
|
|
254
|
+
try {
|
|
255
|
+
workspace = {
|
|
256
|
+
id: await client$1.resolveWorkspaceId(),
|
|
257
|
+
slug: config.workspaceSlug
|
|
258
|
+
};
|
|
259
|
+
} catch {
|
|
260
|
+
workspace = void 0;
|
|
261
|
+
}
|
|
134
262
|
return /* @__PURE__ */ jsxRuntime.jsx(client.CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
135
263
|
react.CmssyServerPage,
|
|
136
264
|
{
|
|
@@ -139,13 +267,16 @@ function createCmssyPage(config, blocks, options) {
|
|
|
139
267
|
locale,
|
|
140
268
|
defaultLocale,
|
|
141
269
|
enabledLocales,
|
|
142
|
-
forms
|
|
270
|
+
forms,
|
|
271
|
+
auth,
|
|
272
|
+
workspace
|
|
143
273
|
}
|
|
144
274
|
) });
|
|
145
275
|
};
|
|
146
276
|
}
|
|
147
277
|
function resolveBridgeOrigin(editorOrigin) {
|
|
148
|
-
const
|
|
278
|
+
const resolved = resolveEditorOrigin(editorOrigin);
|
|
279
|
+
const origins = Array.isArray(resolved) ? resolved : [resolved];
|
|
149
280
|
if (origins.length === 0) {
|
|
150
281
|
throw new Error("cmssy: editorOrigin must be set to frame the editor");
|
|
151
282
|
}
|
|
@@ -157,7 +288,7 @@ function resolveBridgeOrigin(editorOrigin) {
|
|
|
157
288
|
const origin = toCspOrigin(origins[0].trim());
|
|
158
289
|
if (origin === "*") {
|
|
159
290
|
throw new Error(
|
|
160
|
-
"cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://
|
|
291
|
+
"cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://www.cmssy.io)"
|
|
161
292
|
);
|
|
162
293
|
}
|
|
163
294
|
return origin;
|
|
@@ -509,79 +640,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
509
640
|
return server.NextResponse.next({ request: { headers: headers3 } });
|
|
510
641
|
};
|
|
511
642
|
}
|
|
512
|
-
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
513
|
-
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
514
|
-
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
515
|
-
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
516
|
-
async function deriveSessionKey(secret) {
|
|
517
|
-
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
518
|
-
throw new Error(
|
|
519
|
-
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
const digest = await crypto.subtle.digest(
|
|
523
|
-
"SHA-256",
|
|
524
|
-
new TextEncoder().encode(secret)
|
|
525
|
-
);
|
|
526
|
-
return new Uint8Array(digest);
|
|
527
|
-
}
|
|
528
|
-
async function sealSession(payload, secret, audience) {
|
|
529
|
-
const key = await deriveSessionKey(secret);
|
|
530
|
-
const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
531
|
-
if (audience) jwt.setAudience(audience);
|
|
532
|
-
return jwt.encrypt(key);
|
|
533
|
-
}
|
|
534
|
-
async function openSession(token, secret, audience) {
|
|
535
|
-
const key = await deriveSessionKey(secret);
|
|
536
|
-
try {
|
|
537
|
-
const { payload } = await jose.jwtDecrypt(token, key, {
|
|
538
|
-
keyManagementAlgorithms: ["dir"],
|
|
539
|
-
contentEncryptionAlgorithms: ["A256GCM"],
|
|
540
|
-
...audience ? { audience } : {}
|
|
541
|
-
});
|
|
542
|
-
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
543
|
-
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
return {
|
|
547
|
-
accessToken,
|
|
548
|
-
refreshToken,
|
|
549
|
-
accessExpiresAt,
|
|
550
|
-
user: {
|
|
551
|
-
recordId: user.recordId,
|
|
552
|
-
email: user.email
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
} catch {
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
function isAccessExpired(payload, now = Date.now()) {
|
|
560
|
-
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
561
|
-
}
|
|
562
|
-
function sessionCookieOptions() {
|
|
563
|
-
return {
|
|
564
|
-
httpOnly: true,
|
|
565
|
-
secure: process.env.NODE_ENV !== "development",
|
|
566
|
-
sameSite: "lax",
|
|
567
|
-
path: "/",
|
|
568
|
-
maxAge: SESSION_MAX_AGE_SECONDS
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/config.ts
|
|
573
|
-
function assertAuthConfig(config) {
|
|
574
|
-
const auth = config.auth;
|
|
575
|
-
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
576
|
-
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
577
|
-
}
|
|
578
|
-
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
579
|
-
throw new Error(
|
|
580
|
-
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
return auth;
|
|
584
|
-
}
|
|
585
643
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
586
644
|
siteMemberLogin(input: $input) {
|
|
587
645
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -612,7 +670,7 @@ var VERIFY_MUTATION = `mutation SiteMemberVerifyEmail($token: String!) {
|
|
|
612
670
|
}`;
|
|
613
671
|
var workspaceIdCache = /* @__PURE__ */ new Map();
|
|
614
672
|
function workspaceIdFor(config) {
|
|
615
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
673
|
+
const key = `${react.resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
616
674
|
const existing = workspaceIdCache.get(key);
|
|
617
675
|
if (existing) return existing;
|
|
618
676
|
const fresh = react.resolveWorkspaceId(config).catch((err) => {
|
|
@@ -999,7 +1057,7 @@ var PRODUCT = `query Product($workspaceId: String!, $modelSlug: String!, $filter
|
|
|
999
1057
|
}`;
|
|
1000
1058
|
var workspaceIdCache2 = /* @__PURE__ */ new Map();
|
|
1001
1059
|
function workspaceIdFor2(config) {
|
|
1002
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
1060
|
+
const key = `${react.resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
1003
1061
|
const existing = workspaceIdCache2.get(key);
|
|
1004
1062
|
if (existing) return existing;
|
|
1005
1063
|
const fresh = react.resolveWorkspaceId(config).catch((err) => {
|
|
@@ -1289,27 +1347,6 @@ function createCmssyCartRoute(config) {
|
|
|
1289
1347
|
}
|
|
1290
1348
|
};
|
|
1291
1349
|
}
|
|
1292
|
-
async function readValidSession(config) {
|
|
1293
|
-
const auth = assertAuthConfig(config);
|
|
1294
|
-
const jar = await headers.cookies();
|
|
1295
|
-
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
1296
|
-
if (!raw) return null;
|
|
1297
|
-
const session = await openSession(
|
|
1298
|
-
raw,
|
|
1299
|
-
auth.sessionSecret,
|
|
1300
|
-
config.workspaceSlug
|
|
1301
|
-
);
|
|
1302
|
-
if (!session || isAccessExpired(session)) return null;
|
|
1303
|
-
return session;
|
|
1304
|
-
}
|
|
1305
|
-
async function getCmssyUser(config) {
|
|
1306
|
-
const session = await readValidSession(config);
|
|
1307
|
-
return session?.user ?? null;
|
|
1308
|
-
}
|
|
1309
|
-
async function getCmssyAccessToken(config) {
|
|
1310
|
-
const session = await readValidSession(config);
|
|
1311
|
-
return session?.accessToken ?? null;
|
|
1312
|
-
}
|
|
1313
1350
|
function isPrefetch(request2) {
|
|
1314
1351
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1315
1352
|
}
|
|
@@ -1428,7 +1465,7 @@ var MY_ORDER = `query MyOrder($workspaceId: ID!, $id: ID!) {
|
|
|
1428
1465
|
}`;
|
|
1429
1466
|
var workspaceIdCache3 = /* @__PURE__ */ new Map();
|
|
1430
1467
|
function workspaceIdFor3(config) {
|
|
1431
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
1468
|
+
const key = `${react.resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
1432
1469
|
const existing = workspaceIdCache3.get(key);
|
|
1433
1470
|
if (existing) return existing;
|
|
1434
1471
|
const fresh = react.resolveWorkspaceId(config).catch((err) => {
|
|
@@ -1581,11 +1618,20 @@ function verifyCmssyWebhook(options) {
|
|
|
1581
1618
|
return parsed;
|
|
1582
1619
|
}
|
|
1583
1620
|
|
|
1621
|
+
Object.defineProperty(exports, "DEFAULT_CMSSY_API_URL", {
|
|
1622
|
+
enumerable: true,
|
|
1623
|
+
get: function () { return react.DEFAULT_CMSSY_API_URL; }
|
|
1624
|
+
});
|
|
1625
|
+
Object.defineProperty(exports, "resolveApiUrl", {
|
|
1626
|
+
enumerable: true,
|
|
1627
|
+
get: function () { return react.resolveApiUrl; }
|
|
1628
|
+
});
|
|
1584
1629
|
exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
|
|
1585
1630
|
exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
|
|
1586
1631
|
exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
|
|
1587
1632
|
exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
|
|
1588
1633
|
exports.CmssyWebhookError = CmssyWebhookError;
|
|
1634
|
+
exports.DEFAULT_CMSSY_EDITOR_ORIGIN = DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
1589
1635
|
exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
|
|
1590
1636
|
exports.applyCmssyCsp = applyCmssyCsp;
|
|
1591
1637
|
exports.assertAuthConfig = assertAuthConfig;
|
|
@@ -1611,6 +1657,7 @@ exports.isCmssyEditMode = isCmssyEditMode;
|
|
|
1611
1657
|
exports.isCmssyEditRequest = isCmssyEditRequest;
|
|
1612
1658
|
exports.localeForPathname = localeForPathname;
|
|
1613
1659
|
exports.openSession = openSession;
|
|
1660
|
+
exports.resolveEditorOrigin = resolveEditorOrigin;
|
|
1614
1661
|
exports.resolveLocaleFromPathname = resolveLocaleFromPathname;
|
|
1615
1662
|
exports.sealSession = sealSession;
|
|
1616
1663
|
exports.sessionCookieOptions = sessionCookieOptions;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
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
|
+
export { DEFAULT_CMSSY_API_URL, resolveApiUrl } from '@cmssy/react';
|
|
4
5
|
import { EditBridgeConfig } from '@cmssy/react/client';
|
|
5
6
|
import { MetadataRoute, Metadata } from 'next';
|
|
6
7
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Origin of the cmssy admin/editor that frames your site (postMessage source +
|
|
11
|
+
* CSP `frame-ancestors`). Identical for every workspace on cmssy cloud, so
|
|
12
|
+
* `editorOrigin` defaults to this. Self-hosted admins override it via config.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_CMSSY_EDITOR_ORIGIN = "https://www.cmssy.io";
|
|
15
|
+
/** Resolves `editorOrigin`, falling back to the cmssy cloud admin when unset. */
|
|
16
|
+
declare function resolveEditorOrigin(editorOrigin: string | string[] | undefined): string | string[];
|
|
8
17
|
interface CmssyAuthConfig {
|
|
9
18
|
modelSlug: string;
|
|
10
19
|
sessionSecret: string;
|
|
11
20
|
}
|
|
12
21
|
interface CmssyNextConfig {
|
|
13
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Full GraphQL delivery endpoint. Defaults to the cmssy cloud endpoint
|
|
24
|
+
* (`https://api.cmssy.io/graphql`); set it only for self-hosted / staging.
|
|
25
|
+
*/
|
|
26
|
+
apiUrl?: string;
|
|
14
27
|
workspaceSlug: string;
|
|
15
28
|
draftSecret: string;
|
|
16
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Origin allowed to frame your app in the editor. Defaults to
|
|
31
|
+
* {@link DEFAULT_CMSSY_EDITOR_ORIGIN}; set it only for self-hosted admins.
|
|
32
|
+
*/
|
|
33
|
+
editorOrigin?: string | string[];
|
|
17
34
|
/**
|
|
18
35
|
* Canonical absolute site URL (e.g. https://cmssy.com), used by
|
|
19
36
|
* createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
|
|
@@ -141,7 +158,8 @@ type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
|
|
|
141
158
|
declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
|
|
142
159
|
|
|
143
160
|
interface CmssyCspOptions {
|
|
144
|
-
|
|
161
|
+
/** Defaults to the cmssy cloud admin origin when unset. */
|
|
162
|
+
editorOrigin?: string | string[];
|
|
145
163
|
}
|
|
146
164
|
interface MutableHeaders {
|
|
147
165
|
headers: {
|
|
@@ -328,4 +346,4 @@ declare class CmssyWebhookError extends Error {
|
|
|
328
346
|
*/
|
|
329
347
|
declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
|
|
330
348
|
|
|
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 };
|
|
349
|
+
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, DEFAULT_CMSSY_EDITOR_ORIGIN, 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, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
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
|
+
export { DEFAULT_CMSSY_API_URL, resolveApiUrl } from '@cmssy/react';
|
|
4
5
|
import { EditBridgeConfig } from '@cmssy/react/client';
|
|
5
6
|
import { MetadataRoute, Metadata } from 'next';
|
|
6
7
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Origin of the cmssy admin/editor that frames your site (postMessage source +
|
|
11
|
+
* CSP `frame-ancestors`). Identical for every workspace on cmssy cloud, so
|
|
12
|
+
* `editorOrigin` defaults to this. Self-hosted admins override it via config.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_CMSSY_EDITOR_ORIGIN = "https://www.cmssy.io";
|
|
15
|
+
/** Resolves `editorOrigin`, falling back to the cmssy cloud admin when unset. */
|
|
16
|
+
declare function resolveEditorOrigin(editorOrigin: string | string[] | undefined): string | string[];
|
|
8
17
|
interface CmssyAuthConfig {
|
|
9
18
|
modelSlug: string;
|
|
10
19
|
sessionSecret: string;
|
|
11
20
|
}
|
|
12
21
|
interface CmssyNextConfig {
|
|
13
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Full GraphQL delivery endpoint. Defaults to the cmssy cloud endpoint
|
|
24
|
+
* (`https://api.cmssy.io/graphql`); set it only for self-hosted / staging.
|
|
25
|
+
*/
|
|
26
|
+
apiUrl?: string;
|
|
14
27
|
workspaceSlug: string;
|
|
15
28
|
draftSecret: string;
|
|
16
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Origin allowed to frame your app in the editor. Defaults to
|
|
31
|
+
* {@link DEFAULT_CMSSY_EDITOR_ORIGIN}; set it only for self-hosted admins.
|
|
32
|
+
*/
|
|
33
|
+
editorOrigin?: string | string[];
|
|
17
34
|
/**
|
|
18
35
|
* Canonical absolute site URL (e.g. https://cmssy.com), used by
|
|
19
36
|
* createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
|
|
@@ -141,7 +158,8 @@ type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
|
|
|
141
158
|
declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
|
|
142
159
|
|
|
143
160
|
interface CmssyCspOptions {
|
|
144
|
-
|
|
161
|
+
/** Defaults to the cmssy cloud admin origin when unset. */
|
|
162
|
+
editorOrigin?: string | string[];
|
|
145
163
|
}
|
|
146
164
|
interface MutableHeaders {
|
|
147
165
|
headers: {
|
|
@@ -328,4 +346,4 @@ declare class CmssyWebhookError extends Error {
|
|
|
328
346
|
*/
|
|
329
347
|
declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
|
|
330
348
|
|
|
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 };
|
|
349
|
+
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, DEFAULT_CMSSY_EDITOR_ORIGIN, 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, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,119 @@
|
|
|
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, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
|
|
3
|
+
import { createCmssyClient, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest, resolveApiUrl } from '@cmssy/react';
|
|
4
|
+
export { DEFAULT_CMSSY_API_URL, resolveApiUrl } from '@cmssy/react';
|
|
4
5
|
import { CmssyLocaleProvider } from '@cmssy/react/client';
|
|
6
|
+
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
5
7
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
8
|
import { createHmac, createHash, timingSafeEqual } from 'crypto';
|
|
7
9
|
import { NextResponse } from 'next/server';
|
|
8
|
-
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
9
10
|
|
|
10
11
|
// src/create-cmssy-page.tsx
|
|
12
|
+
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
13
|
+
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
14
|
+
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
15
|
+
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
16
|
+
async function deriveSessionKey(secret) {
|
|
17
|
+
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const digest = await crypto.subtle.digest(
|
|
23
|
+
"SHA-256",
|
|
24
|
+
new TextEncoder().encode(secret)
|
|
25
|
+
);
|
|
26
|
+
return new Uint8Array(digest);
|
|
27
|
+
}
|
|
28
|
+
async function sealSession(payload, secret, audience) {
|
|
29
|
+
const key = await deriveSessionKey(secret);
|
|
30
|
+
const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
31
|
+
if (audience) jwt.setAudience(audience);
|
|
32
|
+
return jwt.encrypt(key);
|
|
33
|
+
}
|
|
34
|
+
async function openSession(token, secret, audience) {
|
|
35
|
+
const key = await deriveSessionKey(secret);
|
|
36
|
+
try {
|
|
37
|
+
const { payload } = await jwtDecrypt(token, key, {
|
|
38
|
+
keyManagementAlgorithms: ["dir"],
|
|
39
|
+
contentEncryptionAlgorithms: ["A256GCM"],
|
|
40
|
+
...audience ? { audience } : {}
|
|
41
|
+
});
|
|
42
|
+
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
43
|
+
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
accessToken,
|
|
48
|
+
refreshToken,
|
|
49
|
+
accessExpiresAt,
|
|
50
|
+
user: {
|
|
51
|
+
recordId: user.recordId,
|
|
52
|
+
email: user.email
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isAccessExpired(payload, now = Date.now()) {
|
|
60
|
+
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
61
|
+
}
|
|
62
|
+
function sessionCookieOptions() {
|
|
63
|
+
return {
|
|
64
|
+
httpOnly: true,
|
|
65
|
+
secure: process.env.NODE_ENV !== "development",
|
|
66
|
+
sameSite: "lax",
|
|
67
|
+
path: "/",
|
|
68
|
+
maxAge: SESSION_MAX_AGE_SECONDS
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/config.ts
|
|
73
|
+
var DEFAULT_CMSSY_EDITOR_ORIGIN = "https://www.cmssy.io";
|
|
74
|
+
function resolveEditorOrigin(editorOrigin) {
|
|
75
|
+
if (editorOrigin === void 0) return DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
76
|
+
if (Array.isArray(editorOrigin)) {
|
|
77
|
+
const cleaned = editorOrigin.filter((o) => o && o.trim().length > 0);
|
|
78
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
79
|
+
}
|
|
80
|
+
return editorOrigin.trim().length > 0 ? editorOrigin : DEFAULT_CMSSY_EDITOR_ORIGIN;
|
|
81
|
+
}
|
|
82
|
+
function assertAuthConfig(config) {
|
|
83
|
+
const auth = config.auth;
|
|
84
|
+
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
85
|
+
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
86
|
+
}
|
|
87
|
+
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return auth;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/auth-server.ts
|
|
96
|
+
async function readValidSession(config) {
|
|
97
|
+
const auth = assertAuthConfig(config);
|
|
98
|
+
const jar = await cookies();
|
|
99
|
+
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
100
|
+
if (!raw) return null;
|
|
101
|
+
const session = await openSession(
|
|
102
|
+
raw,
|
|
103
|
+
auth.sessionSecret,
|
|
104
|
+
config.workspaceSlug
|
|
105
|
+
);
|
|
106
|
+
if (!session || isAccessExpired(session)) return null;
|
|
107
|
+
return session;
|
|
108
|
+
}
|
|
109
|
+
async function getCmssyUser(config) {
|
|
110
|
+
const session = await readValidSession(config);
|
|
111
|
+
return session?.user ?? null;
|
|
112
|
+
}
|
|
113
|
+
async function getCmssyAccessToken(config) {
|
|
114
|
+
const session = await readValidSession(config);
|
|
115
|
+
return session?.accessToken ?? null;
|
|
116
|
+
}
|
|
11
117
|
|
|
12
118
|
// src/csp.ts
|
|
13
119
|
function toCspOrigin(origin) {
|
|
@@ -24,7 +130,8 @@ function toCspOrigin(origin) {
|
|
|
24
130
|
return parsed.origin;
|
|
25
131
|
}
|
|
26
132
|
function frameAncestors(editorOrigin) {
|
|
27
|
-
const
|
|
133
|
+
const resolved = resolveEditorOrigin(editorOrigin);
|
|
134
|
+
const origins = Array.isArray(resolved) ? resolved : [resolved];
|
|
28
135
|
if (origins.length === 0) {
|
|
29
136
|
throw new Error(
|
|
30
137
|
"cmssy: editorOrigin must contain at least one valid origin"
|
|
@@ -68,6 +175,7 @@ function createCmssyPage(config, blocks, options) {
|
|
|
68
175
|
apiUrl: config.apiUrl,
|
|
69
176
|
workspaceSlug: config.workspaceSlug
|
|
70
177
|
};
|
|
178
|
+
const client = createCmssyClient(clientConfig);
|
|
71
179
|
return async function CmssyCatchAllPage({
|
|
72
180
|
params,
|
|
73
181
|
searchParams
|
|
@@ -129,6 +237,27 @@ function createCmssyPage(config, blocks, options) {
|
|
|
129
237
|
}
|
|
130
238
|
) });
|
|
131
239
|
}
|
|
240
|
+
let auth;
|
|
241
|
+
if (config.auth) {
|
|
242
|
+
try {
|
|
243
|
+
const user = await getCmssyUser(config);
|
|
244
|
+
auth = {
|
|
245
|
+
isAuthenticated: !!user,
|
|
246
|
+
member: user ? { recordId: user.recordId, email: user.email } : null
|
|
247
|
+
};
|
|
248
|
+
} catch {
|
|
249
|
+
auth = void 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let workspace;
|
|
253
|
+
try {
|
|
254
|
+
workspace = {
|
|
255
|
+
id: await client.resolveWorkspaceId(),
|
|
256
|
+
slug: config.workspaceSlug
|
|
257
|
+
};
|
|
258
|
+
} catch {
|
|
259
|
+
workspace = void 0;
|
|
260
|
+
}
|
|
132
261
|
return /* @__PURE__ */ jsx(CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsx(
|
|
133
262
|
CmssyServerPage,
|
|
134
263
|
{
|
|
@@ -137,13 +266,16 @@ function createCmssyPage(config, blocks, options) {
|
|
|
137
266
|
locale,
|
|
138
267
|
defaultLocale,
|
|
139
268
|
enabledLocales,
|
|
140
|
-
forms
|
|
269
|
+
forms,
|
|
270
|
+
auth,
|
|
271
|
+
workspace
|
|
141
272
|
}
|
|
142
273
|
) });
|
|
143
274
|
};
|
|
144
275
|
}
|
|
145
276
|
function resolveBridgeOrigin(editorOrigin) {
|
|
146
|
-
const
|
|
277
|
+
const resolved = resolveEditorOrigin(editorOrigin);
|
|
278
|
+
const origins = Array.isArray(resolved) ? resolved : [resolved];
|
|
147
279
|
if (origins.length === 0) {
|
|
148
280
|
throw new Error("cmssy: editorOrigin must be set to frame the editor");
|
|
149
281
|
}
|
|
@@ -155,7 +287,7 @@ function resolveBridgeOrigin(editorOrigin) {
|
|
|
155
287
|
const origin = toCspOrigin(origins[0].trim());
|
|
156
288
|
if (origin === "*") {
|
|
157
289
|
throw new Error(
|
|
158
|
-
"cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://
|
|
290
|
+
"cmssy: editorOrigin '*' is not allowed for the live-edit bridge; set the concrete editor origin (e.g. https://www.cmssy.io)"
|
|
159
291
|
);
|
|
160
292
|
}
|
|
161
293
|
return origin;
|
|
@@ -507,79 +639,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
507
639
|
return NextResponse.next({ request: { headers: headers3 } });
|
|
508
640
|
};
|
|
509
641
|
}
|
|
510
|
-
var CMSSY_SESSION_COOKIE = "cmssy_session";
|
|
511
|
-
var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
512
|
-
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
513
|
-
var ACCESS_EXPIRY_SKEW_MS = 3e4;
|
|
514
|
-
async function deriveSessionKey(secret) {
|
|
515
|
-
if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
516
|
-
throw new Error(
|
|
517
|
-
`cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
const digest = await crypto.subtle.digest(
|
|
521
|
-
"SHA-256",
|
|
522
|
-
new TextEncoder().encode(secret)
|
|
523
|
-
);
|
|
524
|
-
return new Uint8Array(digest);
|
|
525
|
-
}
|
|
526
|
-
async function sealSession(payload, secret, audience) {
|
|
527
|
-
const key = await deriveSessionKey(secret);
|
|
528
|
-
const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
|
|
529
|
-
if (audience) jwt.setAudience(audience);
|
|
530
|
-
return jwt.encrypt(key);
|
|
531
|
-
}
|
|
532
|
-
async function openSession(token, secret, audience) {
|
|
533
|
-
const key = await deriveSessionKey(secret);
|
|
534
|
-
try {
|
|
535
|
-
const { payload } = await jwtDecrypt(token, key, {
|
|
536
|
-
keyManagementAlgorithms: ["dir"],
|
|
537
|
-
contentEncryptionAlgorithms: ["A256GCM"],
|
|
538
|
-
...audience ? { audience } : {}
|
|
539
|
-
});
|
|
540
|
-
const { accessToken, refreshToken, accessExpiresAt, user } = payload;
|
|
541
|
-
if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
return {
|
|
545
|
-
accessToken,
|
|
546
|
-
refreshToken,
|
|
547
|
-
accessExpiresAt,
|
|
548
|
-
user: {
|
|
549
|
-
recordId: user.recordId,
|
|
550
|
-
email: user.email
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
} catch {
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function isAccessExpired(payload, now = Date.now()) {
|
|
558
|
-
return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
|
|
559
|
-
}
|
|
560
|
-
function sessionCookieOptions() {
|
|
561
|
-
return {
|
|
562
|
-
httpOnly: true,
|
|
563
|
-
secure: process.env.NODE_ENV !== "development",
|
|
564
|
-
sameSite: "lax",
|
|
565
|
-
path: "/",
|
|
566
|
-
maxAge: SESSION_MAX_AGE_SECONDS
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// src/config.ts
|
|
571
|
-
function assertAuthConfig(config) {
|
|
572
|
-
const auth = config.auth;
|
|
573
|
-
if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
|
|
574
|
-
throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
|
|
575
|
-
}
|
|
576
|
-
if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
577
|
-
throw new Error(
|
|
578
|
-
`cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
return auth;
|
|
582
|
-
}
|
|
583
642
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
584
643
|
siteMemberLogin(input: $input) {
|
|
585
644
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -610,7 +669,7 @@ var VERIFY_MUTATION = `mutation SiteMemberVerifyEmail($token: String!) {
|
|
|
610
669
|
}`;
|
|
611
670
|
var workspaceIdCache = /* @__PURE__ */ new Map();
|
|
612
671
|
function workspaceIdFor(config) {
|
|
613
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
672
|
+
const key = `${resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
614
673
|
const existing = workspaceIdCache.get(key);
|
|
615
674
|
if (existing) return existing;
|
|
616
675
|
const fresh = resolveWorkspaceId(config).catch((err) => {
|
|
@@ -997,7 +1056,7 @@ var PRODUCT = `query Product($workspaceId: String!, $modelSlug: String!, $filter
|
|
|
997
1056
|
}`;
|
|
998
1057
|
var workspaceIdCache2 = /* @__PURE__ */ new Map();
|
|
999
1058
|
function workspaceIdFor2(config) {
|
|
1000
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
1059
|
+
const key = `${resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
1001
1060
|
const existing = workspaceIdCache2.get(key);
|
|
1002
1061
|
if (existing) return existing;
|
|
1003
1062
|
const fresh = resolveWorkspaceId(config).catch((err) => {
|
|
@@ -1287,27 +1346,6 @@ function createCmssyCartRoute(config) {
|
|
|
1287
1346
|
}
|
|
1288
1347
|
};
|
|
1289
1348
|
}
|
|
1290
|
-
async function readValidSession(config) {
|
|
1291
|
-
const auth = assertAuthConfig(config);
|
|
1292
|
-
const jar = await cookies();
|
|
1293
|
-
const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
|
|
1294
|
-
if (!raw) return null;
|
|
1295
|
-
const session = await openSession(
|
|
1296
|
-
raw,
|
|
1297
|
-
auth.sessionSecret,
|
|
1298
|
-
config.workspaceSlug
|
|
1299
|
-
);
|
|
1300
|
-
if (!session || isAccessExpired(session)) return null;
|
|
1301
|
-
return session;
|
|
1302
|
-
}
|
|
1303
|
-
async function getCmssyUser(config) {
|
|
1304
|
-
const session = await readValidSession(config);
|
|
1305
|
-
return session?.user ?? null;
|
|
1306
|
-
}
|
|
1307
|
-
async function getCmssyAccessToken(config) {
|
|
1308
|
-
const session = await readValidSession(config);
|
|
1309
|
-
return session?.accessToken ?? null;
|
|
1310
|
-
}
|
|
1311
1349
|
function isPrefetch(request2) {
|
|
1312
1350
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1313
1351
|
}
|
|
@@ -1426,7 +1464,7 @@ var MY_ORDER = `query MyOrder($workspaceId: ID!, $id: ID!) {
|
|
|
1426
1464
|
}`;
|
|
1427
1465
|
var workspaceIdCache3 = /* @__PURE__ */ new Map();
|
|
1428
1466
|
function workspaceIdFor3(config) {
|
|
1429
|
-
const key = `${config.apiUrl}::${config.workspaceSlug}`;
|
|
1467
|
+
const key = `${resolveApiUrl(config.apiUrl)}::${config.workspaceSlug}`;
|
|
1430
1468
|
const existing = workspaceIdCache3.get(key);
|
|
1431
1469
|
if (existing) return existing;
|
|
1432
1470
|
const fresh = resolveWorkspaceId(config).catch((err) => {
|
|
@@ -1579,4 +1617,4 @@ function verifyCmssyWebhook(options) {
|
|
|
1579
1617
|
return parsed;
|
|
1580
1618
|
}
|
|
1581
1619
|
|
|
1582
|
-
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 };
|
|
1620
|
+
export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, DEFAULT_CMSSY_EDITOR_ORIGIN, 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, resolveEditorOrigin, 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.6",
|
|
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.6",
|
|
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.6"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"jose": "^6.2.3"
|