@cmssy/next 0.1.9 → 0.2.1

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
@@ -181,14 +181,14 @@ function createDraftRoute(config) {
181
181
  "cmssy: defaultRedirect must be a same-origin path starting with '/'"
182
182
  );
183
183
  }
184
- return async function GET(request) {
184
+ return async function GET(request2) {
185
185
  if (config.draftSecret.length < MIN_SECRET_LENGTH) {
186
186
  return new Response(
187
187
  `cmssy: draftSecret must be at least ${MIN_SECRET_LENGTH} characters`,
188
188
  { status: 500 }
189
189
  );
190
190
  }
191
- const url = new URL(request.url);
191
+ const url = new URL(request2.url);
192
192
  const secret = url.searchParams.get("secret");
193
193
  if (!secret || !secretsMatch(secret, config.draftSecret)) {
194
194
  return new Response("Invalid draft secret", { status: 401 });
@@ -203,8 +203,8 @@ function createDraftRoute(config) {
203
203
  };
204
204
  }
205
205
  var CMSSY_EDIT_HEADER = "x-cmssy-edit";
206
- function isCmssyEditRequest(request) {
207
- 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");
208
208
  }
209
209
  async function isCmssyEditMode() {
210
210
  const h = await headers.headers();
@@ -362,11 +362,11 @@ function decodeAccessClaims(accessToken) {
362
362
  try {
363
363
  const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
364
364
  const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
365
- const json2 = JSON.parse(new TextDecoder().decode(bytes));
366
- if (typeof json2.recordId !== "string" || typeof json2.email !== "string" || json2.type !== "site_member") {
365
+ const json3 = JSON.parse(new TextDecoder().decode(bytes));
366
+ if (typeof json3.recordId !== "string" || typeof json3.email !== "string" || json3.type !== "site_member") {
367
367
  return null;
368
368
  }
369
- return { recordId: json2.recordId, email: json2.email };
369
+ return { recordId: json3.recordId, email: json3.email };
370
370
  } catch {
371
371
  return null;
372
372
  }
@@ -479,12 +479,12 @@ function json(body, status = 200) {
479
479
  }
480
480
  });
481
481
  }
482
- async function readBody(request) {
483
- const contentType = request.headers.get("content-type") ?? "";
482
+ async function readBody(request2) {
483
+ const contentType = request2.headers.get("content-type") ?? "";
484
484
  if (!contentType.toLowerCase().includes("application/json")) {
485
485
  throw new Error("content-type must be application/json");
486
486
  }
487
- const text = await request.text();
487
+ const text = await request2.text();
488
488
  if (text.length > MAX_BODY_CHARS) {
489
489
  throw new Error("body too large");
490
490
  }
@@ -637,11 +637,11 @@ function createCmssyAuthRoute(config) {
637
637
  return json({ ok: result.success, message: result.message });
638
638
  }
639
639
  return {
640
- async POST(request, context) {
640
+ async POST(request2, context) {
641
641
  const { action } = await context.params;
642
642
  let body;
643
643
  try {
644
- body = await readBody(request);
644
+ body = await readBody(request2);
645
645
  } catch {
646
646
  return json({ ok: false, message: "Invalid request body." }, 400);
647
647
  }
@@ -683,6 +683,331 @@ function createCmssyAuthRoute(config) {
683
683
  }
684
684
  };
685
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
+ }
686
1011
  async function readValidSession(config) {
687
1012
  const auth = assertAuthConfig(config);
688
1013
  const jar = await headers.cookies();
@@ -704,13 +1029,13 @@ async function getCmssyAccessToken(config) {
704
1029
  const session = await readValidSession(config);
705
1030
  return session?.accessToken ?? null;
706
1031
  }
707
- function isPrefetch(request) {
708
- return request.headers.get("next-router-prefetch") !== null || request.headers.get("purpose") === "prefetch" || (request.headers.get("sec-purpose") ?? "").includes("prefetch");
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");
709
1034
  }
710
1035
  function createCmssyAuthMiddleware(config) {
711
1036
  const auth = assertAuthConfig(config);
712
- return async function cmssyAuthMiddleware(request) {
713
- const raw = request.cookies.get(CMSSY_SESSION_COOKIE)?.value;
1037
+ return async function cmssyAuthMiddleware(request2) {
1038
+ const raw = request2.cookies.get(CMSSY_SESSION_COOKIE)?.value;
714
1039
  if (!raw) return server.NextResponse.next();
715
1040
  const session = await openSession(
716
1041
  raw,
@@ -726,7 +1051,7 @@ function createCmssyAuthMiddleware(config) {
726
1051
  return response;
727
1052
  }
728
1053
  if (!isAccessExpired(session)) return server.NextResponse.next();
729
- if (isPrefetch(request)) return server.NextResponse.next();
1054
+ if (isPrefetch(request2)) return server.NextResponse.next();
730
1055
  let payload = null;
731
1056
  try {
732
1057
  const result = await backendRefresh(config, session.refreshToken);
@@ -747,13 +1072,14 @@ function createCmssyAuthMiddleware(config) {
747
1072
  auth.sessionSecret,
748
1073
  config.workspaceSlug
749
1074
  );
750
- request.cookies.set(CMSSY_SESSION_COOKIE, sealed);
751
- const refreshed = server.NextResponse.next({ request });
1075
+ request2.cookies.set(CMSSY_SESSION_COOKIE, sealed);
1076
+ const refreshed = server.NextResponse.next({ request: request2 });
752
1077
  refreshed.cookies.set(CMSSY_SESSION_COOKIE, sealed, sessionCookieOptions());
753
1078
  return refreshed;
754
1079
  };
755
1080
  }
756
1081
 
1082
+ exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
757
1083
  exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
758
1084
  exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
759
1085
  exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
@@ -763,6 +1089,7 @@ exports.assertAuthConfig = assertAuthConfig;
763
1089
  exports.cmssyCspHeaders = cmssyCspHeaders;
764
1090
  exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
765
1091
  exports.createCmssyAuthRoute = createCmssyAuthRoute;
1092
+ exports.createCmssyCartRoute = createCmssyCartRoute;
766
1093
  exports.createCmssyPage = createCmssyPage;
767
1094
  exports.createDraftRoute = createDraftRoute;
768
1095
  exports.getCmssyAccessToken = getCmssyAccessToken;
package/dist/index.d.cts CHANGED
@@ -120,10 +120,20 @@ interface CmssyAuthRouteHandlers {
120
120
  }
121
121
  declare function createCmssyAuthRoute(config: CmssyNextConfig): CmssyAuthRouteHandlers;
122
122
 
123
+ declare const CMSSY_CART_COOKIE = "cmssy_cart";
124
+ interface CmssyCartRouteHandlers {
125
+ POST(request: Request, context: {
126
+ params: Promise<{
127
+ action: string;
128
+ }>;
129
+ }): Promise<Response>;
130
+ }
131
+ declare function createCmssyCartRoute(config: CmssyNextConfig): CmssyCartRouteHandlers;
132
+
123
133
  declare function getCmssyUser(config: CmssyNextConfig): Promise<CmssySessionUser | null>;
124
134
  declare function getCmssyAccessToken(config: CmssyNextConfig): Promise<string | null>;
125
135
 
126
136
  type CmssyAuthMiddleware = (request: NextRequest) => Promise<NextResponse>;
127
137
  declare function createCmssyAuthMiddleware(config: CmssyNextConfig): CmssyAuthMiddleware;
128
138
 
129
- export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssySessionPayload, type CmssySessionUser, type CreateCmssyPageOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
139
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssySessionPayload, type CmssySessionUser, type CreateCmssyPageOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
package/dist/index.d.ts CHANGED
@@ -120,10 +120,20 @@ interface CmssyAuthRouteHandlers {
120
120
  }
121
121
  declare function createCmssyAuthRoute(config: CmssyNextConfig): CmssyAuthRouteHandlers;
122
122
 
123
+ declare const CMSSY_CART_COOKIE = "cmssy_cart";
124
+ interface CmssyCartRouteHandlers {
125
+ POST(request: Request, context: {
126
+ params: Promise<{
127
+ action: string;
128
+ }>;
129
+ }): Promise<Response>;
130
+ }
131
+ declare function createCmssyCartRoute(config: CmssyNextConfig): CmssyCartRouteHandlers;
132
+
123
133
  declare function getCmssyUser(config: CmssyNextConfig): Promise<CmssySessionUser | null>;
124
134
  declare function getCmssyAccessToken(config: CmssyNextConfig): Promise<string | null>;
125
135
 
126
136
  type CmssyAuthMiddleware = (request: NextRequest) => Promise<NextResponse>;
127
137
  declare function createCmssyAuthMiddleware(config: CmssyNextConfig): CmssyAuthMiddleware;
128
138
 
129
- export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssySessionPayload, type CmssySessionUser, type CreateCmssyPageOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
139
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssySessionPayload, type CmssySessionUser, type CreateCmssyPageOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { draftMode, headers, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
- import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, graphqlRequest, resolveWorkspaceId } from '@cmssy/react';
3
+ import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
4
4
  import { jsx } from 'react/jsx-runtime';
5
5
  import { createHash, timingSafeEqual } from 'crypto';
6
6
  import { EncryptJWT, jwtDecrypt } from 'jose';
@@ -179,14 +179,14 @@ function createDraftRoute(config) {
179
179
  "cmssy: defaultRedirect must be a same-origin path starting with '/'"
180
180
  );
181
181
  }
182
- return async function GET(request) {
182
+ return async function GET(request2) {
183
183
  if (config.draftSecret.length < MIN_SECRET_LENGTH) {
184
184
  return new Response(
185
185
  `cmssy: draftSecret must be at least ${MIN_SECRET_LENGTH} characters`,
186
186
  { status: 500 }
187
187
  );
188
188
  }
189
- const url = new URL(request.url);
189
+ const url = new URL(request2.url);
190
190
  const secret = url.searchParams.get("secret");
191
191
  if (!secret || !secretsMatch(secret, config.draftSecret)) {
192
192
  return new Response("Invalid draft secret", { status: 401 });
@@ -201,8 +201,8 @@ function createDraftRoute(config) {
201
201
  };
202
202
  }
203
203
  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");
204
+ function isCmssyEditRequest(request2) {
205
+ return request2.cookies.has("__prerender_bypass") || request2.nextUrl.searchParams.getAll("cmssyEdit").includes("1");
206
206
  }
207
207
  async function isCmssyEditMode() {
208
208
  const h = await headers();
@@ -360,11 +360,11 @@ function decodeAccessClaims(accessToken) {
360
360
  try {
361
361
  const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
362
362
  const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
363
- const json2 = JSON.parse(new TextDecoder().decode(bytes));
364
- if (typeof json2.recordId !== "string" || typeof json2.email !== "string" || json2.type !== "site_member") {
363
+ const json3 = JSON.parse(new TextDecoder().decode(bytes));
364
+ if (typeof json3.recordId !== "string" || typeof json3.email !== "string" || json3.type !== "site_member") {
365
365
  return null;
366
366
  }
367
- return { recordId: json2.recordId, email: json2.email };
367
+ return { recordId: json3.recordId, email: json3.email };
368
368
  } catch {
369
369
  return null;
370
370
  }
@@ -477,12 +477,12 @@ function json(body, status = 200) {
477
477
  }
478
478
  });
479
479
  }
480
- async function readBody(request) {
481
- const contentType = request.headers.get("content-type") ?? "";
480
+ async function readBody(request2) {
481
+ const contentType = request2.headers.get("content-type") ?? "";
482
482
  if (!contentType.toLowerCase().includes("application/json")) {
483
483
  throw new Error("content-type must be application/json");
484
484
  }
485
- const text = await request.text();
485
+ const text = await request2.text();
486
486
  if (text.length > MAX_BODY_CHARS) {
487
487
  throw new Error("body too large");
488
488
  }
@@ -635,11 +635,11 @@ function createCmssyAuthRoute(config) {
635
635
  return json({ ok: result.success, message: result.message });
636
636
  }
637
637
  return {
638
- async POST(request, context) {
638
+ async POST(request2, context) {
639
639
  const { action } = await context.params;
640
640
  let body;
641
641
  try {
642
- body = await readBody(request);
642
+ body = await readBody(request2);
643
643
  } catch {
644
644
  return json({ ok: false, message: "Invalid request body." }, 400);
645
645
  }
@@ -681,6 +681,331 @@ function createCmssyAuthRoute(config) {
681
681
  }
682
682
  };
683
683
  }
684
+ var CART_FIELDS = `
685
+ id
686
+ status
687
+ itemCount
688
+ subtotal
689
+ currency
690
+ discountedTotal
691
+ appliedDiscount { code type value computedAmount }
692
+ items {
693
+ id
694
+ recordId
695
+ quantity
696
+ variantSelections
697
+ currentPrice
698
+ priceMismatch
699
+ snapshot { name price currency imageUrl sku }
700
+ }
701
+ `;
702
+ var CART_QUERY = `query Cart($workspaceId: ID!) { cart(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
703
+ var ADD_TO_CART = `mutation AddToCart($input: AddToCartInput!) { addToCart(input: $input) { ${CART_FIELDS} } }`;
704
+ var UPDATE_ITEM = `mutation UpdateCartItem($input: UpdateCartItemInput!) { updateCartItem(input: $input) { ${CART_FIELDS} } }`;
705
+ var REMOVE_ITEM = `mutation RemoveCartItem($workspaceId: ID!, $itemId: ID!) { removeCartItem(workspaceId: $workspaceId, itemId: $itemId) { ${CART_FIELDS} } }`;
706
+ var CLEAR_CART = `mutation ClearCart($workspaceId: ID!) { clearCart(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
707
+ var APPLY_DISCOUNT = `mutation ApplyDiscount($workspaceId: ID!, $code: String!) { applyDiscount(workspaceId: $workspaceId, code: $code) { ${CART_FIELDS} } }`;
708
+ var REMOVE_DISCOUNT = `mutation RemoveDiscount($workspaceId: ID!) { removeDiscount(workspaceId: $workspaceId) { ${CART_FIELDS} } }`;
709
+ var CHECKOUT = `mutation Checkout($input: CheckoutInput!) {
710
+ checkout(input: $input) { id status subtotal total currency customerEmail }
711
+ }`;
712
+ var PRODUCT = `query Product($workspaceId: String!, $modelSlug: String!, $filter: JSON) {
713
+ publicModelRecords(workspaceId: $workspaceId, modelSlug: $modelSlug, filter: $filter, limit: 1) {
714
+ items { id data variants { id sku price inventory selectedOptions { name value } } }
715
+ }
716
+ }`;
717
+ var workspaceIdCache2 = /* @__PURE__ */ new Map();
718
+ function workspaceIdFor2(config) {
719
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
720
+ const existing = workspaceIdCache2.get(key);
721
+ if (existing) return existing;
722
+ const fresh = resolveWorkspaceId(config).catch((err) => {
723
+ workspaceIdCache2.delete(key);
724
+ throw err;
725
+ });
726
+ workspaceIdCache2.set(key, fresh);
727
+ return fresh;
728
+ }
729
+ async function request(config, ctx, workspaceId, query, variables, label) {
730
+ return graphqlRequest(
731
+ config,
732
+ query,
733
+ variables,
734
+ {
735
+ headers: {
736
+ "x-workspace-id": workspaceId,
737
+ "x-cart-session": ctx.cartToken,
738
+ ...ctx.accessToken ? { authorization: `Bearer ${ctx.accessToken}` } : {}
739
+ }
740
+ },
741
+ label
742
+ );
743
+ }
744
+ async function backendGetCart(config, ctx) {
745
+ const workspaceId = await workspaceIdFor2(config);
746
+ const data = await request(
747
+ config,
748
+ ctx,
749
+ workspaceId,
750
+ CART_QUERY,
751
+ { workspaceId },
752
+ "cart query"
753
+ );
754
+ return data.cart;
755
+ }
756
+ async function backendAddToCart(config, ctx, input) {
757
+ const workspaceId = await workspaceIdFor2(config);
758
+ const data = await request(
759
+ config,
760
+ ctx,
761
+ workspaceId,
762
+ ADD_TO_CART,
763
+ { input: { workspaceId, ...input } },
764
+ "add to cart"
765
+ );
766
+ return data.addToCart;
767
+ }
768
+ async function backendUpdateItem(config, ctx, input) {
769
+ const workspaceId = await workspaceIdFor2(config);
770
+ const data = await request(
771
+ config,
772
+ ctx,
773
+ workspaceId,
774
+ UPDATE_ITEM,
775
+ { input: { workspaceId, ...input } },
776
+ "update cart item"
777
+ );
778
+ return data.updateCartItem;
779
+ }
780
+ async function backendRemoveItem(config, ctx, itemId) {
781
+ const workspaceId = await workspaceIdFor2(config);
782
+ const data = await request(
783
+ config,
784
+ ctx,
785
+ workspaceId,
786
+ REMOVE_ITEM,
787
+ { workspaceId, itemId },
788
+ "remove cart item"
789
+ );
790
+ return data.removeCartItem;
791
+ }
792
+ async function backendClearCart(config, ctx) {
793
+ const workspaceId = await workspaceIdFor2(config);
794
+ const data = await request(
795
+ config,
796
+ ctx,
797
+ workspaceId,
798
+ CLEAR_CART,
799
+ { workspaceId },
800
+ "clear cart"
801
+ );
802
+ return data.clearCart;
803
+ }
804
+ async function backendApplyDiscount(config, ctx, code) {
805
+ const workspaceId = await workspaceIdFor2(config);
806
+ const data = await request(
807
+ config,
808
+ ctx,
809
+ workspaceId,
810
+ APPLY_DISCOUNT,
811
+ { workspaceId, code },
812
+ "apply discount"
813
+ );
814
+ return data.applyDiscount;
815
+ }
816
+ async function backendRemoveDiscount(config, ctx) {
817
+ const workspaceId = await workspaceIdFor2(config);
818
+ const data = await request(
819
+ config,
820
+ ctx,
821
+ workspaceId,
822
+ REMOVE_DISCOUNT,
823
+ { workspaceId },
824
+ "remove discount"
825
+ );
826
+ return data.removeDiscount;
827
+ }
828
+ async function backendCheckout(config, ctx, customerEmail) {
829
+ const workspaceId = await workspaceIdFor2(config);
830
+ const data = await request(
831
+ config,
832
+ ctx,
833
+ workspaceId,
834
+ CHECKOUT,
835
+ { input: { workspaceId, customerEmail } },
836
+ "checkout"
837
+ );
838
+ return data.checkout;
839
+ }
840
+ async function backendProduct(config, ctx, modelSlug, filter) {
841
+ const workspaceId = await workspaceIdFor2(config);
842
+ const data = await request(
843
+ config,
844
+ ctx,
845
+ workspaceId,
846
+ PRODUCT,
847
+ { workspaceId, modelSlug, filter },
848
+ "product query"
849
+ );
850
+ return data.publicModelRecords.items[0] ?? null;
851
+ }
852
+
853
+ // src/create-cart-route.ts
854
+ var CMSSY_CART_COOKIE = "cmssy_cart";
855
+ var CART_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
856
+ var CART_TOKEN_BYTES = 32;
857
+ var MAX_BODY_CHARS2 = 16 * 1024;
858
+ function json2(body, status = 200) {
859
+ return new Response(JSON.stringify(body), {
860
+ status,
861
+ headers: {
862
+ "content-type": "application/json",
863
+ "cache-control": "no-store"
864
+ }
865
+ });
866
+ }
867
+ function cartCookieOptions() {
868
+ return {
869
+ httpOnly: true,
870
+ secure: process.env.NODE_ENV !== "development",
871
+ sameSite: "lax",
872
+ path: "/",
873
+ maxAge: CART_MAX_AGE_SECONDS
874
+ };
875
+ }
876
+ function mintToken() {
877
+ const bytes = new Uint8Array(CART_TOKEN_BYTES);
878
+ crypto.getRandomValues(bytes);
879
+ let binary = "";
880
+ for (const byte of bytes) binary += String.fromCharCode(byte);
881
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
882
+ }
883
+ async function readBody2(request2) {
884
+ const contentType = request2.headers.get("content-type") ?? "";
885
+ if (!contentType.toLowerCase().includes("application/json")) {
886
+ throw new Error("content-type must be application/json");
887
+ }
888
+ const text = await request2.text();
889
+ if (text.length > MAX_BODY_CHARS2) throw new Error("body too large");
890
+ if (!text) return {};
891
+ const parsed = JSON.parse(text);
892
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
893
+ throw new Error("body must be a JSON object");
894
+ }
895
+ return parsed;
896
+ }
897
+ function str2(value) {
898
+ return typeof value === "string" ? value : "";
899
+ }
900
+ function plainObject2(value) {
901
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
902
+ return value;
903
+ }
904
+ function createCmssyCartRoute(config) {
905
+ async function ensureCartToken() {
906
+ const jar = await cookies();
907
+ const existing = jar.get(CMSSY_CART_COOKIE)?.value;
908
+ if (existing) return existing;
909
+ const token = mintToken();
910
+ jar.set(CMSSY_CART_COOKIE, token, cartCookieOptions());
911
+ return token;
912
+ }
913
+ async function clearCartToken() {
914
+ const jar = await cookies();
915
+ jar.set(CMSSY_CART_COOKIE, "", { ...cartCookieOptions(), maxAge: 0 });
916
+ }
917
+ async function memberAccessToken() {
918
+ if (!config.auth) return void 0;
919
+ const jar = await cookies();
920
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
921
+ if (!raw) return void 0;
922
+ const session = await openSession(
923
+ raw,
924
+ config.auth.sessionSecret,
925
+ config.workspaceSlug
926
+ );
927
+ if (!session || isAccessExpired(session)) return void 0;
928
+ return session.accessToken;
929
+ }
930
+ async function buildContext() {
931
+ const cartToken = await ensureCartToken();
932
+ const accessToken = await memberAccessToken();
933
+ return accessToken ? { cartToken, accessToken } : { cartToken };
934
+ }
935
+ return {
936
+ async POST(request2, context) {
937
+ const { action } = await context.params;
938
+ let body;
939
+ try {
940
+ body = await readBody2(request2);
941
+ } catch {
942
+ return json2({ message: "Invalid request body." }, 400);
943
+ }
944
+ try {
945
+ const ctx = await buildContext();
946
+ switch (action) {
947
+ case "cart":
948
+ return json2({ cart: await backendGetCart(config, ctx) });
949
+ case "add":
950
+ return json2({
951
+ cart: await backendAddToCart(config, ctx, {
952
+ recordId: str2(body.recordId),
953
+ quantity: typeof body.quantity === "number" ? body.quantity : 1,
954
+ variantSelections: body.variantSelections,
955
+ notes: typeof body.notes === "string" ? body.notes : void 0
956
+ })
957
+ });
958
+ case "update":
959
+ return json2({
960
+ cart: await backendUpdateItem(config, ctx, {
961
+ itemId: str2(body.itemId),
962
+ quantity: typeof body.quantity === "number" ? body.quantity : 0
963
+ })
964
+ });
965
+ case "remove":
966
+ return json2({
967
+ cart: await backendRemoveItem(config, ctx, str2(body.itemId))
968
+ });
969
+ case "clear":
970
+ return json2({ cart: await backendClearCart(config, ctx) });
971
+ case "apply-discount":
972
+ return json2({
973
+ cart: await backendApplyDiscount(config, ctx, str2(body.code))
974
+ });
975
+ case "remove-discount":
976
+ return json2({ cart: await backendRemoveDiscount(config, ctx) });
977
+ case "checkout": {
978
+ const order = await backendCheckout(
979
+ config,
980
+ ctx,
981
+ str2(body.customerEmail)
982
+ );
983
+ await clearCartToken();
984
+ return json2({ order });
985
+ }
986
+ case "product":
987
+ return json2({
988
+ product: await backendProduct(
989
+ config,
990
+ ctx,
991
+ str2(body.modelSlug),
992
+ plainObject2(body.filter)
993
+ )
994
+ });
995
+ default:
996
+ return json2({ message: "Not found." }, 404);
997
+ }
998
+ } catch (err) {
999
+ return json2(
1000
+ {
1001
+ message: err instanceof Error ? err.message : "Commerce request failed"
1002
+ },
1003
+ 502
1004
+ );
1005
+ }
1006
+ }
1007
+ };
1008
+ }
684
1009
  async function readValidSession(config) {
685
1010
  const auth = assertAuthConfig(config);
686
1011
  const jar = await cookies();
@@ -702,13 +1027,13 @@ async function getCmssyAccessToken(config) {
702
1027
  const session = await readValidSession(config);
703
1028
  return session?.accessToken ?? null;
704
1029
  }
705
- function isPrefetch(request) {
706
- return request.headers.get("next-router-prefetch") !== null || request.headers.get("purpose") === "prefetch" || (request.headers.get("sec-purpose") ?? "").includes("prefetch");
1030
+ function isPrefetch(request2) {
1031
+ return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
707
1032
  }
708
1033
  function createCmssyAuthMiddleware(config) {
709
1034
  const auth = assertAuthConfig(config);
710
- return async function cmssyAuthMiddleware(request) {
711
- const raw = request.cookies.get(CMSSY_SESSION_COOKIE)?.value;
1035
+ return async function cmssyAuthMiddleware(request2) {
1036
+ const raw = request2.cookies.get(CMSSY_SESSION_COOKIE)?.value;
712
1037
  if (!raw) return NextResponse.next();
713
1038
  const session = await openSession(
714
1039
  raw,
@@ -724,7 +1049,7 @@ function createCmssyAuthMiddleware(config) {
724
1049
  return response;
725
1050
  }
726
1051
  if (!isAccessExpired(session)) return NextResponse.next();
727
- if (isPrefetch(request)) return NextResponse.next();
1052
+ if (isPrefetch(request2)) return NextResponse.next();
728
1053
  let payload = null;
729
1054
  try {
730
1055
  const result = await backendRefresh(config, session.refreshToken);
@@ -745,11 +1070,11 @@ function createCmssyAuthMiddleware(config) {
745
1070
  auth.sessionSecret,
746
1071
  config.workspaceSlug
747
1072
  );
748
- request.cookies.set(CMSSY_SESSION_COOKIE, sealed);
749
- const refreshed = NextResponse.next({ request });
1073
+ request2.cookies.set(CMSSY_SESSION_COOKIE, sealed);
1074
+ const refreshed = NextResponse.next({ request: request2 });
750
1075
  refreshed.cookies.set(CMSSY_SESSION_COOKIE, sealed, sessionCookieOptions());
751
1076
  return refreshed;
752
1077
  };
753
1078
  }
754
1079
 
755
- export { CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
1080
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyPage, createDraftRoute, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Next.js App Router bindings for cmssy headless sites (createCmssyPage + draft preview)",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -36,7 +36,7 @@
36
36
  "dist"
37
37
  ],
38
38
  "peerDependencies": {
39
- "@cmssy/react": "^0.1.7",
39
+ "@cmssy/react": "^0.2.1",
40
40
  "next": ">=15",
41
41
  "react": "^18.2.0 || ^19.0.0",
42
42
  "react-dom": "^18.2.0 || ^19.0.0"
@@ -49,7 +49,7 @@
49
49
  "tsup": "^8.3.0",
50
50
  "typescript": "^5.6.0",
51
51
  "vitest": "^2.1.0",
52
- "@cmssy/react": "0.1.9"
52
+ "@cmssy/react": "0.2.1"
53
53
  },
54
54
  "dependencies": {
55
55
  "jose": "^6.2.3"