@cmssy/next 0.5.4 → 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 +122 -96
- package/dist/index.js +123 -97
- 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
|
};
|
|
@@ -509,79 +629,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
509
629
|
return server.NextResponse.next({ request: { headers: headers3 } });
|
|
510
630
|
};
|
|
511
631
|
}
|
|
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
632
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
586
633
|
siteMemberLogin(input: $input) {
|
|
587
634
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -1289,27 +1336,6 @@ function createCmssyCartRoute(config) {
|
|
|
1289
1336
|
}
|
|
1290
1337
|
};
|
|
1291
1338
|
}
|
|
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
1339
|
function isPrefetch(request2) {
|
|
1314
1340
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1315
1341
|
}
|
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, 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 } 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
|
};
|
|
@@ -507,79 +627,6 @@ function createCmssyLocaleMiddleware(config) {
|
|
|
507
627
|
return NextResponse.next({ request: { headers: headers3 } });
|
|
508
628
|
};
|
|
509
629
|
}
|
|
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
630
|
var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
|
|
584
631
|
siteMemberLogin(input: $input) {
|
|
585
632
|
success message accessToken refreshToken accessTokenExpiresIn
|
|
@@ -1287,27 +1334,6 @@ function createCmssyCartRoute(config) {
|
|
|
1287
1334
|
}
|
|
1288
1335
|
};
|
|
1289
1336
|
}
|
|
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
1337
|
function isPrefetch(request2) {
|
|
1312
1338
|
return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
|
|
1313
1339
|
}
|
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"
|