@cmssy/next 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4,7 +4,9 @@ var headers = require('next/headers');
4
4
  var navigation = require('next/navigation');
5
5
  var react = require('@cmssy/react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
- var crypto = require('crypto');
7
+ var crypto$1 = require('crypto');
8
+ var jose = require('jose');
9
+ var server = require('next/server');
8
10
 
9
11
  // src/create-cmssy-page.tsx
10
12
 
@@ -156,9 +158,9 @@ function resolveBridgeOrigin(editorOrigin) {
156
158
  }
157
159
  var MIN_SECRET_LENGTH = 16;
158
160
  function secretsMatch(a, b) {
159
- const ha = crypto.createHash("sha256").update(a).digest();
160
- const hb = crypto.createHash("sha256").update(b).digest();
161
- return crypto.timingSafeEqual(ha, hb);
161
+ const ha = crypto$1.createHash("sha256").update(a).digest();
162
+ const hb = crypto$1.createHash("sha256").update(b).digest();
163
+ return crypto$1.timingSafeEqual(ha, hb);
162
164
  }
163
165
  function safeRedirect(redirect2, fallback) {
164
166
  if (!redirect2 || !redirect2.startsWith("/")) return fallback;
@@ -179,14 +181,14 @@ function createDraftRoute(config) {
179
181
  "cmssy: defaultRedirect must be a same-origin path starting with '/'"
180
182
  );
181
183
  }
182
- return async function GET(request) {
184
+ return async function GET(request2) {
183
185
  if (config.draftSecret.length < MIN_SECRET_LENGTH) {
184
186
  return new Response(
185
187
  `cmssy: draftSecret must be at least ${MIN_SECRET_LENGTH} characters`,
186
188
  { status: 500 }
187
189
  );
188
190
  }
189
- const url = new URL(request.url);
191
+ const url = new URL(request2.url);
190
192
  const secret = url.searchParams.get("secret");
191
193
  if (!secret || !secretsMatch(secret, config.draftSecret)) {
192
194
  return new Response("Invalid draft secret", { status: 401 });
@@ -201,8 +203,8 @@ function createDraftRoute(config) {
201
203
  };
202
204
  }
203
205
  var CMSSY_EDIT_HEADER = "x-cmssy-edit";
204
- function isCmssyEditRequest(request) {
205
- return request.cookies.has("__prerender_bypass") || request.nextUrl.searchParams.getAll("cmssyEdit").includes("1");
206
+ function isCmssyEditRequest(request2) {
207
+ return request2.cookies.has("__prerender_bypass") || request2.nextUrl.searchParams.getAll("cmssyEdit").includes("1");
206
208
  }
207
209
  async function isCmssyEditMode() {
208
210
  const h = await headers.headers();
@@ -226,15 +228,878 @@ async function getCmssyLocale(config) {
226
228
  const { defaultLocale } = await react.resolveSiteLocales(config);
227
229
  return defaultLocale;
228
230
  }
231
+ var CMSSY_SESSION_COOKIE = "cmssy_session";
232
+ var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
233
+ var MIN_SESSION_SECRET_LENGTH = 32;
234
+ var ACCESS_EXPIRY_SKEW_MS = 3e4;
235
+ async function deriveSessionKey(secret) {
236
+ if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
237
+ throw new Error(
238
+ `cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
239
+ );
240
+ }
241
+ const digest = await crypto.subtle.digest(
242
+ "SHA-256",
243
+ new TextEncoder().encode(secret)
244
+ );
245
+ return new Uint8Array(digest);
246
+ }
247
+ async function sealSession(payload, secret, audience) {
248
+ const key = await deriveSessionKey(secret);
249
+ const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
250
+ if (audience) jwt.setAudience(audience);
251
+ return jwt.encrypt(key);
252
+ }
253
+ async function openSession(token, secret, audience) {
254
+ const key = await deriveSessionKey(secret);
255
+ try {
256
+ const { payload } = await jose.jwtDecrypt(token, key, {
257
+ keyManagementAlgorithms: ["dir"],
258
+ contentEncryptionAlgorithms: ["A256GCM"],
259
+ ...audience ? { audience } : {}
260
+ });
261
+ const { accessToken, refreshToken, accessExpiresAt, user } = payload;
262
+ if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
263
+ return null;
264
+ }
265
+ return {
266
+ accessToken,
267
+ refreshToken,
268
+ accessExpiresAt,
269
+ user: {
270
+ recordId: user.recordId,
271
+ email: user.email
272
+ }
273
+ };
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ function isAccessExpired(payload, now = Date.now()) {
279
+ return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
280
+ }
281
+ function sessionCookieOptions() {
282
+ return {
283
+ httpOnly: true,
284
+ secure: process.env.NODE_ENV !== "development",
285
+ sameSite: "lax",
286
+ path: "/",
287
+ maxAge: SESSION_MAX_AGE_SECONDS
288
+ };
289
+ }
290
+
291
+ // src/config.ts
292
+ function assertAuthConfig(config) {
293
+ const auth = config.auth;
294
+ if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
295
+ throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
296
+ }
297
+ if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
298
+ throw new Error(
299
+ `cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
300
+ );
301
+ }
302
+ return auth;
303
+ }
304
+ var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
305
+ siteMemberLogin(input: $input) {
306
+ success message accessToken refreshToken accessTokenExpiresIn
307
+ }
308
+ }`;
309
+ var REGISTER_MUTATION = `mutation SiteMemberRegister($input: SiteMemberRegisterInput!) {
310
+ siteMemberRegister(input: $input) { success message }
311
+ }`;
312
+ var REFRESH_MUTATION = `mutation SiteMemberRefresh($refreshToken: String!) {
313
+ siteMemberRefresh(refreshToken: $refreshToken) {
314
+ success message accessToken refreshToken accessTokenExpiresIn
315
+ }
316
+ }`;
317
+ var LOGOUT_MUTATION = `mutation SiteMemberLogout($refreshToken: String!) {
318
+ siteMemberLogout(refreshToken: $refreshToken) { success message }
319
+ }`;
320
+ var LOGOUT_EVERYWHERE_MUTATION = `mutation SiteMemberLogoutEverywhere {
321
+ siteMemberLogoutEverywhere { success message }
322
+ }`;
323
+ var FORGOT_MUTATION = `mutation SiteMemberForgotPassword($modelSlug: String!, $identity: String!) {
324
+ siteMemberForgotPassword(modelSlug: $modelSlug, identity: $identity) { success message }
325
+ }`;
326
+ var RESET_MUTATION = `mutation SiteMemberResetPassword($token: String!, $newPassword: String!) {
327
+ siteMemberResetPassword(token: $token, newPassword: $newPassword) { success message }
328
+ }`;
329
+ var VERIFY_MUTATION = `mutation SiteMemberVerifyEmail($token: String!) {
330
+ siteMemberVerifyEmail(token: $token) { success message }
331
+ }`;
332
+ var workspaceIdCache = /* @__PURE__ */ new Map();
333
+ function workspaceIdFor(config) {
334
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
335
+ const existing = workspaceIdCache.get(key);
336
+ if (existing) return existing;
337
+ const fresh = react.resolveWorkspaceId(config).catch((err) => {
338
+ workspaceIdCache.delete(key);
339
+ throw err;
340
+ });
341
+ workspaceIdCache.set(key, fresh);
342
+ return fresh;
343
+ }
344
+ async function authRequest(config, query, variables, label, accessToken) {
345
+ const workspaceId = await workspaceIdFor(config);
346
+ return react.graphqlRequest(
347
+ config,
348
+ query,
349
+ variables,
350
+ {
351
+ headers: {
352
+ "x-workspace-id": workspaceId,
353
+ ...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
354
+ }
355
+ },
356
+ label
357
+ );
358
+ }
359
+ function decodeAccessClaims(accessToken) {
360
+ const parts = accessToken.split(".");
361
+ if (parts.length !== 3) return null;
362
+ try {
363
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
364
+ const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
365
+ const json3 = JSON.parse(new TextDecoder().decode(bytes));
366
+ if (typeof json3.recordId !== "string" || typeof json3.email !== "string" || json3.type !== "site_member") {
367
+ return null;
368
+ }
369
+ return { recordId: json3.recordId, email: json3.email };
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+ function toSessionPayload(result) {
375
+ if (!result.success || !result.accessToken || !result.refreshToken || !result.accessTokenExpiresIn) {
376
+ return null;
377
+ }
378
+ const user = decodeAccessClaims(result.accessToken);
379
+ if (!user) return null;
380
+ return {
381
+ accessToken: result.accessToken,
382
+ refreshToken: result.refreshToken,
383
+ accessExpiresAt: Date.now() + result.accessTokenExpiresIn * 1e3,
384
+ user
385
+ };
386
+ }
387
+ async function backendSignIn(config, modelSlug, identity, password) {
388
+ const data = await authRequest(
389
+ config,
390
+ LOGIN_MUTATION,
391
+ {
392
+ input: { modelSlug, identity, password }
393
+ },
394
+ "site member login"
395
+ );
396
+ return data.siteMemberLogin;
397
+ }
398
+ async function backendRegister(config, modelSlug, identity, password, fields) {
399
+ const data = await authRequest(
400
+ config,
401
+ REGISTER_MUTATION,
402
+ {
403
+ input: { modelSlug, identity, password, fields }
404
+ },
405
+ "site member register"
406
+ );
407
+ return data.siteMemberRegister;
408
+ }
409
+ async function backendRefresh(config, refreshToken) {
410
+ const data = await authRequest(
411
+ config,
412
+ REFRESH_MUTATION,
413
+ { refreshToken },
414
+ "site member refresh"
415
+ );
416
+ return data.siteMemberRefresh;
417
+ }
418
+ async function backendSignOut(config, refreshToken) {
419
+ try {
420
+ await authRequest(
421
+ config,
422
+ LOGOUT_MUTATION,
423
+ { refreshToken },
424
+ "site member logout"
425
+ );
426
+ } catch {
427
+ return;
428
+ }
429
+ }
430
+ async function backendSignOutEverywhere(config, accessToken) {
431
+ try {
432
+ await authRequest(
433
+ config,
434
+ LOGOUT_EVERYWHERE_MUTATION,
435
+ {},
436
+ "site member logout everywhere",
437
+ accessToken
438
+ );
439
+ } catch {
440
+ return;
441
+ }
442
+ }
443
+ async function backendForgotPassword(config, modelSlug, identity) {
444
+ const data = await authRequest(
445
+ config,
446
+ FORGOT_MUTATION,
447
+ { modelSlug, identity },
448
+ "site member forgot password"
449
+ );
450
+ return data.siteMemberForgotPassword;
451
+ }
452
+ async function backendResetPassword(config, token, newPassword) {
453
+ const data = await authRequest(
454
+ config,
455
+ RESET_MUTATION,
456
+ { token, newPassword },
457
+ "site member reset password"
458
+ );
459
+ return data.siteMemberResetPassword;
460
+ }
461
+ async function backendVerifyEmail(config, token) {
462
+ const data = await authRequest(
463
+ config,
464
+ VERIFY_MUTATION,
465
+ { token },
466
+ "site member verify email"
467
+ );
468
+ return data.siteMemberVerifyEmail;
469
+ }
470
+
471
+ // src/create-auth-route.ts
472
+ var MAX_BODY_CHARS = 16 * 1024;
473
+ function json(body, status = 200) {
474
+ return new Response(JSON.stringify(body), {
475
+ status,
476
+ headers: {
477
+ "content-type": "application/json",
478
+ "cache-control": "no-store"
479
+ }
480
+ });
481
+ }
482
+ async function readBody(request2) {
483
+ const contentType = request2.headers.get("content-type") ?? "";
484
+ if (!contentType.toLowerCase().includes("application/json")) {
485
+ throw new Error("content-type must be application/json");
486
+ }
487
+ const text = await request2.text();
488
+ if (text.length > MAX_BODY_CHARS) {
489
+ throw new Error("body too large");
490
+ }
491
+ if (!text) return {};
492
+ const parsed = JSON.parse(text);
493
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
494
+ throw new Error("body must be a JSON object");
495
+ }
496
+ return parsed;
497
+ }
498
+ function str(value) {
499
+ return typeof value === "string" ? value : "";
500
+ }
501
+ function plainObject(value) {
502
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
503
+ return value;
504
+ }
505
+ async function readSession(config, auth) {
506
+ const jar = await headers.cookies();
507
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
508
+ if (!raw) return null;
509
+ return openSession(raw, auth.sessionSecret, config.workspaceSlug);
510
+ }
511
+ async function writeSession(config, auth, payload) {
512
+ const sealed = await sealSession(
513
+ payload,
514
+ auth.sessionSecret,
515
+ config.workspaceSlug
516
+ );
517
+ const jar = await headers.cookies();
518
+ jar.set(CMSSY_SESSION_COOKIE, sealed, sessionCookieOptions());
519
+ }
520
+ async function clearSession() {
521
+ const jar = await headers.cookies();
522
+ jar.set(CMSSY_SESSION_COOKIE, "", {
523
+ ...sessionCookieOptions(),
524
+ maxAge: 0
525
+ });
526
+ }
527
+ async function refreshSession(config, auth, session) {
528
+ const result = await backendRefresh(config, session.refreshToken);
529
+ const payload = toSessionPayload(result);
530
+ if (!payload) {
531
+ await clearSession();
532
+ return null;
533
+ }
534
+ await writeSession(config, auth, payload);
535
+ return payload;
536
+ }
537
+ function createCmssyAuthRoute(config) {
538
+ const auth = assertAuthConfig(config);
539
+ async function handleSignIn(body) {
540
+ const identity = str(body.identity);
541
+ const password = str(body.password);
542
+ if (!identity || !password) {
543
+ return json({ ok: false, message: "Invalid credentials." }, 400);
544
+ }
545
+ const result = await backendSignIn(
546
+ config,
547
+ auth.modelSlug,
548
+ identity,
549
+ password
550
+ );
551
+ const payload = toSessionPayload(result);
552
+ if (!payload) {
553
+ return json({ ok: false, message: result.message }, 401);
554
+ }
555
+ await writeSession(config, auth, payload);
556
+ return json({ ok: true, user: payload.user });
557
+ }
558
+ async function handleRegister(body) {
559
+ const identity = str(body.identity);
560
+ const password = str(body.password);
561
+ if (!identity || !password) {
562
+ return json({ ok: false, message: "Invalid input." }, 400);
563
+ }
564
+ const result = await backendRegister(
565
+ config,
566
+ auth.modelSlug,
567
+ identity,
568
+ password,
569
+ plainObject(body.fields)
570
+ );
571
+ return json({ ok: result.success, message: result.message });
572
+ }
573
+ async function handleSignOut() {
574
+ const session = await readSession(config, auth);
575
+ if (session) {
576
+ await backendSignOut(config, session.refreshToken);
577
+ }
578
+ await clearSession();
579
+ return json({ ok: true });
580
+ }
581
+ async function handleSignOutEverywhere() {
582
+ let session = await readSession(config, auth);
583
+ if (session && isAccessExpired(session)) {
584
+ session = await refreshSession(config, auth, session);
585
+ }
586
+ if (session) {
587
+ await backendSignOutEverywhere(config, session.accessToken);
588
+ }
589
+ await clearSession();
590
+ return json({ ok: true });
591
+ }
592
+ async function handleRefresh() {
593
+ const session = await readSession(config, auth);
594
+ if (!session) {
595
+ return json({ ok: false, user: null }, 401);
596
+ }
597
+ const refreshed = await refreshSession(config, auth, session);
598
+ if (!refreshed) {
599
+ return json({ ok: false, user: null }, 401);
600
+ }
601
+ return json({ ok: true, user: refreshed.user });
602
+ }
603
+ async function handleMe() {
604
+ let session = await readSession(config, auth);
605
+ if (session && isAccessExpired(session)) {
606
+ session = await refreshSession(config, auth, session);
607
+ }
608
+ return json({ user: session?.user ?? null });
609
+ }
610
+ async function handleForgotPassword(body) {
611
+ const identity = str(body.identity);
612
+ if (!identity) {
613
+ return json({ ok: false, message: "Invalid input." }, 400);
614
+ }
615
+ const result = await backendForgotPassword(
616
+ config,
617
+ auth.modelSlug,
618
+ identity
619
+ );
620
+ return json({ ok: result.success, message: result.message });
621
+ }
622
+ async function handleResetPassword(body) {
623
+ const token = str(body.token);
624
+ const newPassword = str(body.newPassword);
625
+ if (!token || !newPassword) {
626
+ return json({ ok: false, message: "Invalid input." }, 400);
627
+ }
628
+ const result = await backendResetPassword(config, token, newPassword);
629
+ return json({ ok: result.success, message: result.message });
630
+ }
631
+ async function handleVerifyEmail(body) {
632
+ const token = str(body.token);
633
+ if (!token) {
634
+ return json({ ok: false, message: "Invalid input." }, 400);
635
+ }
636
+ const result = await backendVerifyEmail(config, token);
637
+ return json({ ok: result.success, message: result.message });
638
+ }
639
+ return {
640
+ async POST(request2, context) {
641
+ const { action } = await context.params;
642
+ let body;
643
+ try {
644
+ body = await readBody(request2);
645
+ } catch {
646
+ return json({ ok: false, message: "Invalid request body." }, 400);
647
+ }
648
+ try {
649
+ switch (action) {
650
+ case "sign-in":
651
+ return await handleSignIn(body);
652
+ case "register":
653
+ return await handleRegister(body);
654
+ case "sign-out":
655
+ return await handleSignOut();
656
+ case "sign-out-everywhere":
657
+ return await handleSignOutEverywhere();
658
+ case "refresh":
659
+ return await handleRefresh();
660
+ case "forgot-password":
661
+ return await handleForgotPassword(body);
662
+ case "reset-password":
663
+ return await handleResetPassword(body);
664
+ case "verify-email":
665
+ return await handleVerifyEmail(body);
666
+ default:
667
+ return json({ ok: false, message: "Not found." }, 404);
668
+ }
669
+ } catch {
670
+ return json({ ok: false, message: "Something went wrong." }, 500);
671
+ }
672
+ },
673
+ async GET(_request, context) {
674
+ const { action } = await context.params;
675
+ if (action !== "me") {
676
+ return json({ ok: false, message: "Not found." }, 404);
677
+ }
678
+ try {
679
+ return await handleMe();
680
+ } catch {
681
+ return json({ user: null });
682
+ }
683
+ }
684
+ };
685
+ }
686
+ var CART_FIELDS = `
687
+ id
688
+ status
689
+ itemCount
690
+ subtotal
691
+ currency
692
+ discountedTotal
693
+ appliedDiscount { code type value computedAmount }
694
+ items {
695
+ id
696
+ recordId
697
+ quantity
698
+ variantSelections
699
+ currentPrice
700
+ priceMismatch
701
+ snapshot { name price currency imageUrl sku }
702
+ }
703
+ `;
704
+ var CART_QUERY = `query Cart($workspaceId: ID!) { cart(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
705
+ var ADD_TO_CART = `mutation AddToCart($input: AddToCartInput!) { addToCart(input: $input) { ${CART_FIELDS} } }`;
706
+ var UPDATE_ITEM = `mutation UpdateCartItem($input: UpdateCartItemInput!) { updateCartItem(input: $input) { ${CART_FIELDS} } }`;
707
+ var REMOVE_ITEM = `mutation RemoveCartItem($workspaceId: ID!, $itemId: ID!) { removeCartItem(workspaceId: $workspaceId, itemId: $itemId) { ${CART_FIELDS} } }`;
708
+ var CLEAR_CART = `mutation ClearCart($workspaceId: ID!) { clearCart(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
709
+ var APPLY_DISCOUNT = `mutation ApplyDiscount($workspaceId: ID!, $code: String!) { applyDiscount(workspaceId: $workspaceId, code: $code) { ${CART_FIELDS} } }`;
710
+ var REMOVE_DISCOUNT = `mutation RemoveDiscount($workspaceId: ID!) { removeDiscount(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
711
+ var CHECKOUT = `mutation Checkout($input: CheckoutInput!) {
712
+ checkout(input: $input) { id status subtotal total currency customerEmail }
713
+ }`;
714
+ var PRODUCT = `query Product($workspaceId: String!, $modelSlug: String!, $filter: JSON) {
715
+ publicModelRecords(workspaceId: $workspaceId, modelSlug: $modelSlug, filter: $filter, limit: 1) {
716
+ items { id data variants { id sku price inventory selectedOptions { name value } } }
717
+ }
718
+ }`;
719
+ var workspaceIdCache2 = /* @__PURE__ */ new Map();
720
+ function workspaceIdFor2(config) {
721
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
722
+ const existing = workspaceIdCache2.get(key);
723
+ if (existing) return existing;
724
+ const fresh = react.resolveWorkspaceId(config).catch((err) => {
725
+ workspaceIdCache2.delete(key);
726
+ throw err;
727
+ });
728
+ workspaceIdCache2.set(key, fresh);
729
+ return fresh;
730
+ }
731
+ async function request(config, ctx, workspaceId, query, variables, label) {
732
+ return react.graphqlRequest(
733
+ config,
734
+ query,
735
+ variables,
736
+ {
737
+ headers: {
738
+ "x-workspace-id": workspaceId,
739
+ "x-cart-session": ctx.cartToken,
740
+ ...ctx.accessToken ? { authorization: `Bearer ${ctx.accessToken}` } : {}
741
+ }
742
+ },
743
+ label
744
+ );
745
+ }
746
+ async function backendGetCart(config, ctx) {
747
+ const workspaceId = await workspaceIdFor2(config);
748
+ const data = await request(
749
+ config,
750
+ ctx,
751
+ workspaceId,
752
+ CART_QUERY,
753
+ { workspaceId },
754
+ "cart query"
755
+ );
756
+ return data.cart;
757
+ }
758
+ async function backendAddToCart(config, ctx, input) {
759
+ const workspaceId = await workspaceIdFor2(config);
760
+ const data = await request(
761
+ config,
762
+ ctx,
763
+ workspaceId,
764
+ ADD_TO_CART,
765
+ { input: { workspaceId, ...input } },
766
+ "add to cart"
767
+ );
768
+ return data.addToCart;
769
+ }
770
+ async function backendUpdateItem(config, ctx, input) {
771
+ const workspaceId = await workspaceIdFor2(config);
772
+ const data = await request(
773
+ config,
774
+ ctx,
775
+ workspaceId,
776
+ UPDATE_ITEM,
777
+ { input: { workspaceId, ...input } },
778
+ "update cart item"
779
+ );
780
+ return data.updateCartItem;
781
+ }
782
+ async function backendRemoveItem(config, ctx, itemId) {
783
+ const workspaceId = await workspaceIdFor2(config);
784
+ const data = await request(
785
+ config,
786
+ ctx,
787
+ workspaceId,
788
+ REMOVE_ITEM,
789
+ { workspaceId, itemId },
790
+ "remove cart item"
791
+ );
792
+ return data.removeCartItem;
793
+ }
794
+ async function backendClearCart(config, ctx) {
795
+ const workspaceId = await workspaceIdFor2(config);
796
+ const data = await request(
797
+ config,
798
+ ctx,
799
+ workspaceId,
800
+ CLEAR_CART,
801
+ { workspaceId },
802
+ "clear cart"
803
+ );
804
+ return data.clearCart;
805
+ }
806
+ async function backendApplyDiscount(config, ctx, code) {
807
+ const workspaceId = await workspaceIdFor2(config);
808
+ const data = await request(
809
+ config,
810
+ ctx,
811
+ workspaceId,
812
+ APPLY_DISCOUNT,
813
+ { workspaceId, code },
814
+ "apply discount"
815
+ );
816
+ return data.applyDiscount;
817
+ }
818
+ async function backendRemoveDiscount(config, ctx) {
819
+ const workspaceId = await workspaceIdFor2(config);
820
+ const data = await request(
821
+ config,
822
+ ctx,
823
+ workspaceId,
824
+ REMOVE_DISCOUNT,
825
+ { workspaceId },
826
+ "remove discount"
827
+ );
828
+ return data.removeDiscount;
829
+ }
830
+ async function backendCheckout(config, ctx, customerEmail) {
831
+ const workspaceId = await workspaceIdFor2(config);
832
+ const data = await request(
833
+ config,
834
+ ctx,
835
+ workspaceId,
836
+ CHECKOUT,
837
+ { input: { workspaceId, customerEmail } },
838
+ "checkout"
839
+ );
840
+ return data.checkout;
841
+ }
842
+ async function backendProduct(config, ctx, modelSlug, filter) {
843
+ const workspaceId = await workspaceIdFor2(config);
844
+ const data = await request(
845
+ config,
846
+ ctx,
847
+ workspaceId,
848
+ PRODUCT,
849
+ { workspaceId, modelSlug, filter },
850
+ "product query"
851
+ );
852
+ return data.publicModelRecords.items[0] ?? null;
853
+ }
854
+
855
+ // src/create-cart-route.ts
856
+ var CMSSY_CART_COOKIE = "cmssy_cart";
857
+ var CART_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
858
+ var CART_TOKEN_BYTES = 32;
859
+ var MAX_BODY_CHARS2 = 16 * 1024;
860
+ function json2(body, status = 200) {
861
+ return new Response(JSON.stringify(body), {
862
+ status,
863
+ headers: {
864
+ "content-type": "application/json",
865
+ "cache-control": "no-store"
866
+ }
867
+ });
868
+ }
869
+ function cartCookieOptions() {
870
+ return {
871
+ httpOnly: true,
872
+ secure: process.env.NODE_ENV !== "development",
873
+ sameSite: "lax",
874
+ path: "/",
875
+ maxAge: CART_MAX_AGE_SECONDS
876
+ };
877
+ }
878
+ function mintToken() {
879
+ const bytes = new Uint8Array(CART_TOKEN_BYTES);
880
+ crypto.getRandomValues(bytes);
881
+ let binary = "";
882
+ for (const byte of bytes) binary += String.fromCharCode(byte);
883
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
884
+ }
885
+ async function readBody2(request2) {
886
+ const contentType = request2.headers.get("content-type") ?? "";
887
+ if (!contentType.toLowerCase().includes("application/json")) {
888
+ throw new Error("content-type must be application/json");
889
+ }
890
+ const text = await request2.text();
891
+ if (text.length > MAX_BODY_CHARS2) throw new Error("body too large");
892
+ if (!text) return {};
893
+ const parsed = JSON.parse(text);
894
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
895
+ throw new Error("body must be a JSON object");
896
+ }
897
+ return parsed;
898
+ }
899
+ function str2(value) {
900
+ return typeof value === "string" ? value : "";
901
+ }
902
+ function plainObject2(value) {
903
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
904
+ return value;
905
+ }
906
+ function createCmssyCartRoute(config) {
907
+ async function ensureCartToken() {
908
+ const jar = await headers.cookies();
909
+ const existing = jar.get(CMSSY_CART_COOKIE)?.value;
910
+ if (existing) return existing;
911
+ const token = mintToken();
912
+ jar.set(CMSSY_CART_COOKIE, token, cartCookieOptions());
913
+ return token;
914
+ }
915
+ async function clearCartToken() {
916
+ const jar = await headers.cookies();
917
+ jar.set(CMSSY_CART_COOKIE, "", { ...cartCookieOptions(), maxAge: 0 });
918
+ }
919
+ async function memberAccessToken() {
920
+ if (!config.auth) return void 0;
921
+ const jar = await headers.cookies();
922
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
923
+ if (!raw) return void 0;
924
+ const session = await openSession(
925
+ raw,
926
+ config.auth.sessionSecret,
927
+ config.workspaceSlug
928
+ );
929
+ if (!session || isAccessExpired(session)) return void 0;
930
+ return session.accessToken;
931
+ }
932
+ async function buildContext() {
933
+ const cartToken = await ensureCartToken();
934
+ const accessToken = await memberAccessToken();
935
+ return accessToken ? { cartToken, accessToken } : { cartToken };
936
+ }
937
+ return {
938
+ async POST(request2, context) {
939
+ const { action } = await context.params;
940
+ let body;
941
+ try {
942
+ body = await readBody2(request2);
943
+ } catch {
944
+ return json2({ message: "Invalid request body." }, 400);
945
+ }
946
+ try {
947
+ const ctx = await buildContext();
948
+ switch (action) {
949
+ case "cart":
950
+ return json2({ cart: await backendGetCart(config, ctx) });
951
+ case "add":
952
+ return json2({
953
+ cart: await backendAddToCart(config, ctx, {
954
+ recordId: str2(body.recordId),
955
+ quantity: typeof body.quantity === "number" ? body.quantity : 1,
956
+ variantSelections: body.variantSelections,
957
+ notes: typeof body.notes === "string" ? body.notes : void 0
958
+ })
959
+ });
960
+ case "update":
961
+ return json2({
962
+ cart: await backendUpdateItem(config, ctx, {
963
+ itemId: str2(body.itemId),
964
+ quantity: typeof body.quantity === "number" ? body.quantity : 0
965
+ })
966
+ });
967
+ case "remove":
968
+ return json2({
969
+ cart: await backendRemoveItem(config, ctx, str2(body.itemId))
970
+ });
971
+ case "clear":
972
+ return json2({ cart: await backendClearCart(config, ctx) });
973
+ case "apply-discount":
974
+ return json2({
975
+ cart: await backendApplyDiscount(config, ctx, str2(body.code))
976
+ });
977
+ case "remove-discount":
978
+ return json2({ cart: await backendRemoveDiscount(config, ctx) });
979
+ case "checkout": {
980
+ const order = await backendCheckout(
981
+ config,
982
+ ctx,
983
+ str2(body.customerEmail)
984
+ );
985
+ await clearCartToken();
986
+ return json2({ order });
987
+ }
988
+ case "product":
989
+ return json2({
990
+ product: await backendProduct(
991
+ config,
992
+ ctx,
993
+ str2(body.modelSlug),
994
+ plainObject2(body.filter)
995
+ )
996
+ });
997
+ default:
998
+ return json2({ message: "Not found." }, 404);
999
+ }
1000
+ } catch (err) {
1001
+ return json2(
1002
+ {
1003
+ message: err instanceof Error ? err.message : "Commerce request failed"
1004
+ },
1005
+ 502
1006
+ );
1007
+ }
1008
+ }
1009
+ };
1010
+ }
1011
+ async function readValidSession(config) {
1012
+ const auth = assertAuthConfig(config);
1013
+ const jar = await headers.cookies();
1014
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
1015
+ if (!raw) return null;
1016
+ const session = await openSession(
1017
+ raw,
1018
+ auth.sessionSecret,
1019
+ config.workspaceSlug
1020
+ );
1021
+ if (!session || isAccessExpired(session)) return null;
1022
+ return session;
1023
+ }
1024
+ async function getCmssyUser(config) {
1025
+ const session = await readValidSession(config);
1026
+ return session?.user ?? null;
1027
+ }
1028
+ async function getCmssyAccessToken(config) {
1029
+ const session = await readValidSession(config);
1030
+ return session?.accessToken ?? null;
1031
+ }
1032
+ function isPrefetch(request2) {
1033
+ return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
1034
+ }
1035
+ function createCmssyAuthMiddleware(config) {
1036
+ const auth = assertAuthConfig(config);
1037
+ return async function cmssyAuthMiddleware(request2) {
1038
+ const raw = request2.cookies.get(CMSSY_SESSION_COOKIE)?.value;
1039
+ if (!raw) return server.NextResponse.next();
1040
+ const session = await openSession(
1041
+ raw,
1042
+ auth.sessionSecret,
1043
+ config.workspaceSlug
1044
+ );
1045
+ if (!session) {
1046
+ const response = server.NextResponse.next();
1047
+ response.cookies.set(CMSSY_SESSION_COOKIE, "", {
1048
+ ...sessionCookieOptions(),
1049
+ maxAge: 0
1050
+ });
1051
+ return response;
1052
+ }
1053
+ if (!isAccessExpired(session)) return server.NextResponse.next();
1054
+ if (isPrefetch(request2)) return server.NextResponse.next();
1055
+ let payload = null;
1056
+ try {
1057
+ const result = await backendRefresh(config, session.refreshToken);
1058
+ payload = toSessionPayload(result);
1059
+ } catch {
1060
+ return server.NextResponse.next();
1061
+ }
1062
+ if (!payload) {
1063
+ const cleared = server.NextResponse.next();
1064
+ cleared.cookies.set(CMSSY_SESSION_COOKIE, "", {
1065
+ ...sessionCookieOptions(),
1066
+ maxAge: 0
1067
+ });
1068
+ return cleared;
1069
+ }
1070
+ const sealed = await sealSession(
1071
+ payload,
1072
+ auth.sessionSecret,
1073
+ config.workspaceSlug
1074
+ );
1075
+ request2.cookies.set(CMSSY_SESSION_COOKIE, sealed);
1076
+ const refreshed = server.NextResponse.next({ request: request2 });
1077
+ refreshed.cookies.set(CMSSY_SESSION_COOKIE, sealed, sessionCookieOptions());
1078
+ return refreshed;
1079
+ };
1080
+ }
229
1081
 
1082
+ exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
230
1083
  exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
231
1084
  exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
1085
+ exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
1086
+ exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
232
1087
  exports.applyCmssyCsp = applyCmssyCsp;
1088
+ exports.assertAuthConfig = assertAuthConfig;
233
1089
  exports.cmssyCspHeaders = cmssyCspHeaders;
1090
+ exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
1091
+ exports.createCmssyAuthRoute = createCmssyAuthRoute;
1092
+ exports.createCmssyCartRoute = createCmssyCartRoute;
234
1093
  exports.createCmssyPage = createCmssyPage;
235
1094
  exports.createDraftRoute = createDraftRoute;
1095
+ exports.getCmssyAccessToken = getCmssyAccessToken;
236
1096
  exports.getCmssyLocale = getCmssyLocale;
1097
+ exports.getCmssyUser = getCmssyUser;
1098
+ exports.isAccessExpired = isAccessExpired;
237
1099
  exports.isCmssyEditMode = isCmssyEditMode;
238
1100
  exports.isCmssyEditRequest = isCmssyEditRequest;
239
1101
  exports.localeForPathname = localeForPathname;
1102
+ exports.openSession = openSession;
1103
+ exports.sealSession = sealSession;
1104
+ exports.sessionCookieOptions = sessionCookieOptions;
240
1105
  exports.splitCmssyLocale = splitCmssyLocale;