@cmssy/next 0.2.2 → 0.2.4

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
@@ -221,8 +221,8 @@ async function splitCmssyLocale(config, path) {
221
221
  return react.splitLocaleFromPath(path, siteLocales);
222
222
  }
223
223
  async function getCmssyLocale(config) {
224
- const { headers: headers2 } = await import('next/headers');
225
- const headerList = await headers2();
224
+ const { headers: headers3 } = await import('next/headers');
225
+ const headerList = await headers3();
226
226
  const fromHeader = headerList.get(CMSSY_LOCALE_HEADER);
227
227
  if (fromHeader) return fromHeader;
228
228
  const { defaultLocale } = await react.resolveSiteLocales(config);
@@ -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 json3 = JSON.parse(new TextDecoder().decode(bytes));
366
- if (typeof json3.recordId !== "string" || typeof json3.email !== "string" || json3.type !== "site_member") {
365
+ const json4 = JSON.parse(new TextDecoder().decode(bytes));
366
+ if (typeof json4.recordId !== "string" || typeof json4.email !== "string" || json4.type !== "site_member") {
367
367
  return null;
368
368
  }
369
- return { recordId: json3.recordId, email: json3.email };
369
+ return { recordId: json4.recordId, email: json4.email };
370
370
  } catch {
371
371
  return null;
372
372
  }
@@ -1107,11 +1107,191 @@ async function fetchProduct(config, options) {
1107
1107
  });
1108
1108
  return products[0] ?? null;
1109
1109
  }
1110
+ var ORDER_FIELDS = `
1111
+ id
1112
+ status
1113
+ subtotal
1114
+ tax
1115
+ total
1116
+ currency
1117
+ customerEmail
1118
+ refundedAmount
1119
+ paymentProvider
1120
+ paidAt
1121
+ fulfilledAt
1122
+ createdAt
1123
+ items { name price currency quantity sku }
1124
+ `;
1125
+ var MY_ORDERS = `query MyOrders($workspaceId: ID!, $skip: Int, $limit: Int) {
1126
+ myOrders(workspaceId: $workspaceId, skip: $skip, limit: $limit) {
1127
+ total
1128
+ hasMore
1129
+ items { ${ORDER_FIELDS} }
1130
+ }
1131
+ }`;
1132
+ var MY_ORDER = `query MyOrder($workspaceId: ID!, $id: ID!) {
1133
+ myOrder(workspaceId: $workspaceId, id: $id) { ${ORDER_FIELDS} }
1134
+ }`;
1135
+ var workspaceIdCache3 = /* @__PURE__ */ new Map();
1136
+ function workspaceIdFor3(config) {
1137
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
1138
+ const existing = workspaceIdCache3.get(key);
1139
+ if (existing) return existing;
1140
+ const fresh = react.resolveWorkspaceId(config).catch((err) => {
1141
+ workspaceIdCache3.delete(key);
1142
+ throw err;
1143
+ });
1144
+ workspaceIdCache3.set(key, fresh);
1145
+ return fresh;
1146
+ }
1147
+ function headers2(workspaceId, accessToken) {
1148
+ return {
1149
+ "x-workspace-id": workspaceId,
1150
+ authorization: `Bearer ${accessToken}`
1151
+ };
1152
+ }
1153
+ async function backendMyOrders(config, accessToken, options) {
1154
+ const workspaceId = await workspaceIdFor3(config);
1155
+ const data = await react.graphqlRequest(
1156
+ config,
1157
+ MY_ORDERS,
1158
+ { workspaceId, skip: options.skip, limit: options.limit },
1159
+ { headers: headers2(workspaceId, accessToken) },
1160
+ "my orders"
1161
+ );
1162
+ return data.myOrders;
1163
+ }
1164
+ async function backendMyOrder(config, accessToken, id) {
1165
+ const workspaceId = await workspaceIdFor3(config);
1166
+ const data = await react.graphqlRequest(
1167
+ config,
1168
+ MY_ORDER,
1169
+ { workspaceId, id },
1170
+ { headers: headers2(workspaceId, accessToken) },
1171
+ "my order"
1172
+ );
1173
+ return data.myOrder;
1174
+ }
1175
+
1176
+ // src/create-orders-route.ts
1177
+ var DEFAULT_LIMIT = 20;
1178
+ var MAX_LIMIT = 100;
1179
+ function json3(body, status = 200) {
1180
+ return new Response(JSON.stringify(body), {
1181
+ status,
1182
+ headers: {
1183
+ "content-type": "application/json",
1184
+ "cache-control": "no-store"
1185
+ }
1186
+ });
1187
+ }
1188
+ function createCmssyOrdersRoute(config) {
1189
+ async function memberAccessToken() {
1190
+ if (!config.auth) return void 0;
1191
+ const jar = await headers.cookies();
1192
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
1193
+ if (!raw) return void 0;
1194
+ const session = await openSession(
1195
+ raw,
1196
+ config.auth.sessionSecret,
1197
+ config.workspaceSlug
1198
+ );
1199
+ if (!session || isAccessExpired(session)) return void 0;
1200
+ return session.accessToken;
1201
+ }
1202
+ return {
1203
+ async GET(request2) {
1204
+ const accessToken = await memberAccessToken();
1205
+ if (!accessToken) {
1206
+ return json3({ message: "Not signed in." }, 401);
1207
+ }
1208
+ const url = new URL(request2.url);
1209
+ try {
1210
+ const id = url.searchParams.get("id");
1211
+ if (id) {
1212
+ return json3({ order: await backendMyOrder(config, accessToken, id) });
1213
+ }
1214
+ const skip = Math.max(
1215
+ 0,
1216
+ Math.floor(Number(url.searchParams.get("skip")) || 0)
1217
+ );
1218
+ const limitParam = Math.floor(Number(url.searchParams.get("limit")));
1219
+ const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, MAX_LIMIT) : DEFAULT_LIMIT;
1220
+ const result = await backendMyOrders(config, accessToken, {
1221
+ skip,
1222
+ limit
1223
+ });
1224
+ return json3(result);
1225
+ } catch (err) {
1226
+ return json3(
1227
+ { message: err instanceof Error ? err.message : "Orders error" },
1228
+ 502
1229
+ );
1230
+ }
1231
+ }
1232
+ };
1233
+ }
1234
+ var CmssyWebhookError = class extends Error {
1235
+ constructor(message) {
1236
+ super(message);
1237
+ this.name = "CmssyWebhookError";
1238
+ }
1239
+ };
1240
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1241
+ function parseSignatureHeader(header) {
1242
+ let timestamp = null;
1243
+ let signature = null;
1244
+ for (const part of header.split(",")) {
1245
+ const idx = part.indexOf("=");
1246
+ if (idx === -1) continue;
1247
+ const key = part.slice(0, idx).trim();
1248
+ const value = part.slice(idx + 1).trim();
1249
+ if (key === "t") timestamp = Number(value);
1250
+ else if (key === "v1") signature = value;
1251
+ }
1252
+ if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
1253
+ throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
1254
+ }
1255
+ return { timestamp, signature };
1256
+ }
1257
+ function timingSafeHexEqual(expectedHex, providedHex) {
1258
+ const expected = Buffer.from(expectedHex, "hex");
1259
+ const provided = Buffer.from(providedHex, "hex");
1260
+ if (expected.length !== provided.length) return false;
1261
+ return crypto$1.timingSafeEqual(expected, provided);
1262
+ }
1263
+ function verifyCmssyWebhook(options) {
1264
+ const { body, signatureHeader, secret } = options;
1265
+ if (!signatureHeader) {
1266
+ throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
1267
+ }
1268
+ if (!secret) {
1269
+ throw new CmssyWebhookError("Missing webhook secret");
1270
+ }
1271
+ const { timestamp, signature } = parseSignatureHeader(signatureHeader);
1272
+ const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
1273
+ const now = options.now ?? Date.now();
1274
+ if (Math.abs(now - timestamp) > toleranceMs) {
1275
+ throw new CmssyWebhookError("Webhook timestamp outside tolerance");
1276
+ }
1277
+ const expected = crypto$1.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
1278
+ if (!timingSafeHexEqual(expected, signature)) {
1279
+ throw new CmssyWebhookError("Webhook signature mismatch");
1280
+ }
1281
+ let parsed;
1282
+ try {
1283
+ parsed = JSON.parse(body);
1284
+ } catch {
1285
+ throw new CmssyWebhookError("Webhook body is not valid JSON");
1286
+ }
1287
+ return parsed;
1288
+ }
1110
1289
 
1111
1290
  exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
1112
1291
  exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
1113
1292
  exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
1114
1293
  exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
1294
+ exports.CmssyWebhookError = CmssyWebhookError;
1115
1295
  exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
1116
1296
  exports.applyCmssyCsp = applyCmssyCsp;
1117
1297
  exports.assertAuthConfig = assertAuthConfig;
@@ -1119,6 +1299,7 @@ exports.cmssyCspHeaders = cmssyCspHeaders;
1119
1299
  exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
1120
1300
  exports.createCmssyAuthRoute = createCmssyAuthRoute;
1121
1301
  exports.createCmssyCartRoute = createCmssyCartRoute;
1302
+ exports.createCmssyOrdersRoute = createCmssyOrdersRoute;
1122
1303
  exports.createCmssyPage = createCmssyPage;
1123
1304
  exports.createDraftRoute = createDraftRoute;
1124
1305
  exports.fetchProduct = fetchProduct;
@@ -1134,3 +1315,4 @@ exports.openSession = openSession;
1134
1315
  exports.sealSession = sealSession;
1135
1316
  exports.sessionCookieOptions = sessionCookieOptions;
1136
1317
  exports.splitCmssyLocale = splitCmssyLocale;
1318
+ exports.verifyCmssyWebhook = verifyCmssyWebhook;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ComponentType } from 'react';
3
- import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct } from '@cmssy/react';
3
+ import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
4
4
  import { EditBridgeConfig } from '@cmssy/react/client';
5
5
  import { NextRequest, NextResponse } from 'next/server';
6
6
 
@@ -149,4 +149,77 @@ interface FetchProductOptions {
149
149
  }
150
150
  declare function fetchProduct(config: CmssyNextConfig, options: FetchProductOptions): Promise<CmssyProduct | null>;
151
151
 
152
- 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, type FetchProductOptions, type FetchProductsOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
152
+ interface CmssyOrdersRouteHandlers {
153
+ GET(request: Request): Promise<Response>;
154
+ }
155
+ declare function createCmssyOrdersRoute(config: CmssyNextConfig): CmssyOrdersRouteHandlers;
156
+
157
+ interface MyOrdersResult {
158
+ items: CmssyOrder[];
159
+ total: number;
160
+ hasMore: boolean;
161
+ }
162
+
163
+ /**
164
+ * Verify + parse an inbound cmssy webhook (CMS-693 / CMS-694).
165
+ *
166
+ * cmssy signs each delivery with HMAC-SHA256 over `${timestamp}.${body}`
167
+ * and sends the result in the `X-Cmssy-Signature: t=<ms>,v1=<hex>`
168
+ * header, plus a unique `X-Cmssy-Webhook-Id`. This helper recomputes the
169
+ * signature (timing-safe compare), rejects stale timestamps to bound
170
+ * replay, and returns the typed event.
171
+ *
172
+ * IMPORTANT: pass the RAW request body string (e.g. `await req.text()`),
173
+ * never a re-serialized object - the signed bytes must match exactly.
174
+ */
175
+ /** Serialized order carried in an order.* webhook (mirrors the backend). */
176
+ interface CmssyWebhookOrder {
177
+ id: string;
178
+ workspaceId: string;
179
+ displayStatus: string;
180
+ paymentStatus: string;
181
+ fulfillmentStatus: string;
182
+ total: number;
183
+ currency: string;
184
+ customerId: string | null;
185
+ customerEmail: string;
186
+ paymentProvider: string | null;
187
+ paymentReference: string | null;
188
+ refundedAmount: number;
189
+ createdAt: string;
190
+ updatedAt: string;
191
+ }
192
+ interface CmssyWebhookEvent {
193
+ /** Delivery id - also sent in X-Cmssy-Webhook-Id; dedup on it. */
194
+ id: string;
195
+ /** e.g. "order.paid", "order.refunded". */
196
+ event: string;
197
+ createdAt: string;
198
+ data: {
199
+ workspaceId: string;
200
+ order: CmssyWebhookOrder;
201
+ };
202
+ }
203
+ interface VerifyCmssyWebhookOptions {
204
+ /** Raw request body string (NOT parsed JSON). */
205
+ body: string;
206
+ /** The `X-Cmssy-Signature` header value, or null if absent. */
207
+ signatureHeader: string | null;
208
+ /** The endpoint's signing secret. */
209
+ secret: string;
210
+ /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
211
+ toleranceSeconds?: number;
212
+ /** Override the current time (ms) - for tests. */
213
+ now?: number;
214
+ }
215
+ declare class CmssyWebhookError extends Error {
216
+ constructor(message: string);
217
+ }
218
+ /**
219
+ * Verify the signature + freshness and return the parsed event. Throws
220
+ * `CmssyWebhookError` on any failure (missing/malformed header, bad
221
+ * signature, stale timestamp, invalid JSON) - catch it and respond 400.
222
+ */
223
+ declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
224
+
225
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ComponentType } from 'react';
3
- import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct } from '@cmssy/react';
3
+ import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
4
4
  import { EditBridgeConfig } from '@cmssy/react/client';
5
5
  import { NextRequest, NextResponse } from 'next/server';
6
6
 
@@ -149,4 +149,77 @@ interface FetchProductOptions {
149
149
  }
150
150
  declare function fetchProduct(config: CmssyNextConfig, options: FetchProductOptions): Promise<CmssyProduct | null>;
151
151
 
152
- 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, type FetchProductOptions, type FetchProductsOptions, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
152
+ interface CmssyOrdersRouteHandlers {
153
+ GET(request: Request): Promise<Response>;
154
+ }
155
+ declare function createCmssyOrdersRoute(config: CmssyNextConfig): CmssyOrdersRouteHandlers;
156
+
157
+ interface MyOrdersResult {
158
+ items: CmssyOrder[];
159
+ total: number;
160
+ hasMore: boolean;
161
+ }
162
+
163
+ /**
164
+ * Verify + parse an inbound cmssy webhook (CMS-693 / CMS-694).
165
+ *
166
+ * cmssy signs each delivery with HMAC-SHA256 over `${timestamp}.${body}`
167
+ * and sends the result in the `X-Cmssy-Signature: t=<ms>,v1=<hex>`
168
+ * header, plus a unique `X-Cmssy-Webhook-Id`. This helper recomputes the
169
+ * signature (timing-safe compare), rejects stale timestamps to bound
170
+ * replay, and returns the typed event.
171
+ *
172
+ * IMPORTANT: pass the RAW request body string (e.g. `await req.text()`),
173
+ * never a re-serialized object - the signed bytes must match exactly.
174
+ */
175
+ /** Serialized order carried in an order.* webhook (mirrors the backend). */
176
+ interface CmssyWebhookOrder {
177
+ id: string;
178
+ workspaceId: string;
179
+ displayStatus: string;
180
+ paymentStatus: string;
181
+ fulfillmentStatus: string;
182
+ total: number;
183
+ currency: string;
184
+ customerId: string | null;
185
+ customerEmail: string;
186
+ paymentProvider: string | null;
187
+ paymentReference: string | null;
188
+ refundedAmount: number;
189
+ createdAt: string;
190
+ updatedAt: string;
191
+ }
192
+ interface CmssyWebhookEvent {
193
+ /** Delivery id - also sent in X-Cmssy-Webhook-Id; dedup on it. */
194
+ id: string;
195
+ /** e.g. "order.paid", "order.refunded". */
196
+ event: string;
197
+ createdAt: string;
198
+ data: {
199
+ workspaceId: string;
200
+ order: CmssyWebhookOrder;
201
+ };
202
+ }
203
+ interface VerifyCmssyWebhookOptions {
204
+ /** Raw request body string (NOT parsed JSON). */
205
+ body: string;
206
+ /** The `X-Cmssy-Signature` header value, or null if absent. */
207
+ signatureHeader: string | null;
208
+ /** The endpoint's signing secret. */
209
+ secret: string;
210
+ /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
211
+ toleranceSeconds?: number;
212
+ /** Override the current time (ms) - for tests. */
213
+ now?: number;
214
+ }
215
+ declare class CmssyWebhookError extends Error {
216
+ constructor(message: string);
217
+ }
218
+ /**
219
+ * Verify the signature + freshness and return the parsed event. Throws
220
+ * `CmssyWebhookError` on any failure (missing/malformed header, bad
221
+ * signature, stale timestamp, invalid JSON) - catch it and respond 400.
222
+ */
223
+ declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
224
+
225
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { draftMode, headers, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
3
  import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
4
4
  import { jsx } from 'react/jsx-runtime';
5
- import { createHash, timingSafeEqual } from 'crypto';
5
+ import { createHmac, createHash, timingSafeEqual } from 'crypto';
6
6
  import { EncryptJWT, jwtDecrypt } from 'jose';
7
7
  import { NextResponse } from 'next/server';
8
8
 
@@ -219,8 +219,8 @@ async function splitCmssyLocale(config, path) {
219
219
  return splitLocaleFromPath(path, siteLocales);
220
220
  }
221
221
  async function getCmssyLocale(config) {
222
- const { headers: headers2 } = await import('next/headers');
223
- const headerList = await headers2();
222
+ const { headers: headers3 } = await import('next/headers');
223
+ const headerList = await headers3();
224
224
  const fromHeader = headerList.get(CMSSY_LOCALE_HEADER);
225
225
  if (fromHeader) return fromHeader;
226
226
  const { defaultLocale } = await resolveSiteLocales(config);
@@ -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 json3 = JSON.parse(new TextDecoder().decode(bytes));
364
- if (typeof json3.recordId !== "string" || typeof json3.email !== "string" || json3.type !== "site_member") {
363
+ const json4 = JSON.parse(new TextDecoder().decode(bytes));
364
+ if (typeof json4.recordId !== "string" || typeof json4.email !== "string" || json4.type !== "site_member") {
365
365
  return null;
366
366
  }
367
- return { recordId: json3.recordId, email: json3.email };
367
+ return { recordId: json4.recordId, email: json4.email };
368
368
  } catch {
369
369
  return null;
370
370
  }
@@ -1105,5 +1105,184 @@ async function fetchProduct(config, options) {
1105
1105
  });
1106
1106
  return products[0] ?? null;
1107
1107
  }
1108
+ var ORDER_FIELDS = `
1109
+ id
1110
+ status
1111
+ subtotal
1112
+ tax
1113
+ total
1114
+ currency
1115
+ customerEmail
1116
+ refundedAmount
1117
+ paymentProvider
1118
+ paidAt
1119
+ fulfilledAt
1120
+ createdAt
1121
+ items { name price currency quantity sku }
1122
+ `;
1123
+ var MY_ORDERS = `query MyOrders($workspaceId: ID!, $skip: Int, $limit: Int) {
1124
+ myOrders(workspaceId: $workspaceId, skip: $skip, limit: $limit) {
1125
+ total
1126
+ hasMore
1127
+ items { ${ORDER_FIELDS} }
1128
+ }
1129
+ }`;
1130
+ var MY_ORDER = `query MyOrder($workspaceId: ID!, $id: ID!) {
1131
+ myOrder(workspaceId: $workspaceId, id: $id) { ${ORDER_FIELDS} }
1132
+ }`;
1133
+ var workspaceIdCache3 = /* @__PURE__ */ new Map();
1134
+ function workspaceIdFor3(config) {
1135
+ const key = `${config.apiUrl}::${config.workspaceSlug}`;
1136
+ const existing = workspaceIdCache3.get(key);
1137
+ if (existing) return existing;
1138
+ const fresh = resolveWorkspaceId(config).catch((err) => {
1139
+ workspaceIdCache3.delete(key);
1140
+ throw err;
1141
+ });
1142
+ workspaceIdCache3.set(key, fresh);
1143
+ return fresh;
1144
+ }
1145
+ function headers2(workspaceId, accessToken) {
1146
+ return {
1147
+ "x-workspace-id": workspaceId,
1148
+ authorization: `Bearer ${accessToken}`
1149
+ };
1150
+ }
1151
+ async function backendMyOrders(config, accessToken, options) {
1152
+ const workspaceId = await workspaceIdFor3(config);
1153
+ const data = await graphqlRequest(
1154
+ config,
1155
+ MY_ORDERS,
1156
+ { workspaceId, skip: options.skip, limit: options.limit },
1157
+ { headers: headers2(workspaceId, accessToken) },
1158
+ "my orders"
1159
+ );
1160
+ return data.myOrders;
1161
+ }
1162
+ async function backendMyOrder(config, accessToken, id) {
1163
+ const workspaceId = await workspaceIdFor3(config);
1164
+ const data = await graphqlRequest(
1165
+ config,
1166
+ MY_ORDER,
1167
+ { workspaceId, id },
1168
+ { headers: headers2(workspaceId, accessToken) },
1169
+ "my order"
1170
+ );
1171
+ return data.myOrder;
1172
+ }
1173
+
1174
+ // src/create-orders-route.ts
1175
+ var DEFAULT_LIMIT = 20;
1176
+ var MAX_LIMIT = 100;
1177
+ function json3(body, status = 200) {
1178
+ return new Response(JSON.stringify(body), {
1179
+ status,
1180
+ headers: {
1181
+ "content-type": "application/json",
1182
+ "cache-control": "no-store"
1183
+ }
1184
+ });
1185
+ }
1186
+ function createCmssyOrdersRoute(config) {
1187
+ async function memberAccessToken() {
1188
+ if (!config.auth) return void 0;
1189
+ const jar = await cookies();
1190
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
1191
+ if (!raw) return void 0;
1192
+ const session = await openSession(
1193
+ raw,
1194
+ config.auth.sessionSecret,
1195
+ config.workspaceSlug
1196
+ );
1197
+ if (!session || isAccessExpired(session)) return void 0;
1198
+ return session.accessToken;
1199
+ }
1200
+ return {
1201
+ async GET(request2) {
1202
+ const accessToken = await memberAccessToken();
1203
+ if (!accessToken) {
1204
+ return json3({ message: "Not signed in." }, 401);
1205
+ }
1206
+ const url = new URL(request2.url);
1207
+ try {
1208
+ const id = url.searchParams.get("id");
1209
+ if (id) {
1210
+ return json3({ order: await backendMyOrder(config, accessToken, id) });
1211
+ }
1212
+ const skip = Math.max(
1213
+ 0,
1214
+ Math.floor(Number(url.searchParams.get("skip")) || 0)
1215
+ );
1216
+ const limitParam = Math.floor(Number(url.searchParams.get("limit")));
1217
+ const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, MAX_LIMIT) : DEFAULT_LIMIT;
1218
+ const result = await backendMyOrders(config, accessToken, {
1219
+ skip,
1220
+ limit
1221
+ });
1222
+ return json3(result);
1223
+ } catch (err) {
1224
+ return json3(
1225
+ { message: err instanceof Error ? err.message : "Orders error" },
1226
+ 502
1227
+ );
1228
+ }
1229
+ }
1230
+ };
1231
+ }
1232
+ var CmssyWebhookError = class extends Error {
1233
+ constructor(message) {
1234
+ super(message);
1235
+ this.name = "CmssyWebhookError";
1236
+ }
1237
+ };
1238
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1239
+ function parseSignatureHeader(header) {
1240
+ let timestamp = null;
1241
+ let signature = null;
1242
+ for (const part of header.split(",")) {
1243
+ const idx = part.indexOf("=");
1244
+ if (idx === -1) continue;
1245
+ const key = part.slice(0, idx).trim();
1246
+ const value = part.slice(idx + 1).trim();
1247
+ if (key === "t") timestamp = Number(value);
1248
+ else if (key === "v1") signature = value;
1249
+ }
1250
+ if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
1251
+ throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
1252
+ }
1253
+ return { timestamp, signature };
1254
+ }
1255
+ function timingSafeHexEqual(expectedHex, providedHex) {
1256
+ const expected = Buffer.from(expectedHex, "hex");
1257
+ const provided = Buffer.from(providedHex, "hex");
1258
+ if (expected.length !== provided.length) return false;
1259
+ return timingSafeEqual(expected, provided);
1260
+ }
1261
+ function verifyCmssyWebhook(options) {
1262
+ const { body, signatureHeader, secret } = options;
1263
+ if (!signatureHeader) {
1264
+ throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
1265
+ }
1266
+ if (!secret) {
1267
+ throw new CmssyWebhookError("Missing webhook secret");
1268
+ }
1269
+ const { timestamp, signature } = parseSignatureHeader(signatureHeader);
1270
+ const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
1271
+ const now = options.now ?? Date.now();
1272
+ if (Math.abs(now - timestamp) > toleranceMs) {
1273
+ throw new CmssyWebhookError("Webhook timestamp outside tolerance");
1274
+ }
1275
+ const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
1276
+ if (!timingSafeHexEqual(expected, signature)) {
1277
+ throw new CmssyWebhookError("Webhook signature mismatch");
1278
+ }
1279
+ let parsed;
1280
+ try {
1281
+ parsed = JSON.parse(body);
1282
+ } catch {
1283
+ throw new CmssyWebhookError("Webhook body is not valid JSON");
1284
+ }
1285
+ return parsed;
1286
+ }
1108
1287
 
1109
- 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, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
1288
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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.2.2",
39
+ "@cmssy/react": "^0.2.4",
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.2.2"
52
+ "@cmssy/react": "0.2.4"
53
53
  },
54
54
  "dependencies": {
55
55
  "jose": "^6.2.3"