@cmssy/next 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1117,10 +1117,21 @@ var ORDER_FIELDS = `
1117
1117
  customerEmail
1118
1118
  refundedAmount
1119
1119
  paymentProvider
1120
+ paymentStatus
1121
+ fulfillmentStatus
1122
+ amountPaid
1123
+ balanceDue
1124
+ paymentReference
1125
+ trackingNumber
1126
+ trackingCarrier
1127
+ invoiceNumber
1128
+ invoiceUrl
1129
+ invoiceProvider
1120
1130
  paidAt
1121
1131
  fulfilledAt
1122
1132
  createdAt
1123
1133
  items { name price currency quantity sku }
1134
+ payments { amount reference provider at }
1124
1135
  `;
1125
1136
  var MY_ORDERS = `query MyOrders($workspaceId: ID!, $skip: Int, $limit: Int) {
1126
1137
  myOrders(workspaceId: $workspaceId, skip: $skip, limit: $limit) {
@@ -1231,11 +1242,67 @@ function createCmssyOrdersRoute(config) {
1231
1242
  }
1232
1243
  };
1233
1244
  }
1245
+ var CmssyWebhookError = class extends Error {
1246
+ constructor(message) {
1247
+ super(message);
1248
+ this.name = "CmssyWebhookError";
1249
+ }
1250
+ };
1251
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1252
+ function parseSignatureHeader(header) {
1253
+ let timestamp = null;
1254
+ let signature = null;
1255
+ for (const part of header.split(",")) {
1256
+ const idx = part.indexOf("=");
1257
+ if (idx === -1) continue;
1258
+ const key = part.slice(0, idx).trim();
1259
+ const value = part.slice(idx + 1).trim();
1260
+ if (key === "t") timestamp = Number(value);
1261
+ else if (key === "v1") signature = value;
1262
+ }
1263
+ if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
1264
+ throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
1265
+ }
1266
+ return { timestamp, signature };
1267
+ }
1268
+ function timingSafeHexEqual(expectedHex, providedHex) {
1269
+ const expected = Buffer.from(expectedHex, "hex");
1270
+ const provided = Buffer.from(providedHex, "hex");
1271
+ if (expected.length !== provided.length) return false;
1272
+ return crypto$1.timingSafeEqual(expected, provided);
1273
+ }
1274
+ function verifyCmssyWebhook(options) {
1275
+ const { body, signatureHeader, secret } = options;
1276
+ if (!signatureHeader) {
1277
+ throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
1278
+ }
1279
+ if (!secret) {
1280
+ throw new CmssyWebhookError("Missing webhook secret");
1281
+ }
1282
+ const { timestamp, signature } = parseSignatureHeader(signatureHeader);
1283
+ const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
1284
+ const now = options.now ?? Date.now();
1285
+ if (Math.abs(now - timestamp) > toleranceMs) {
1286
+ throw new CmssyWebhookError("Webhook timestamp outside tolerance");
1287
+ }
1288
+ const expected = crypto$1.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
1289
+ if (!timingSafeHexEqual(expected, signature)) {
1290
+ throw new CmssyWebhookError("Webhook signature mismatch");
1291
+ }
1292
+ let parsed;
1293
+ try {
1294
+ parsed = JSON.parse(body);
1295
+ } catch {
1296
+ throw new CmssyWebhookError("Webhook body is not valid JSON");
1297
+ }
1298
+ return parsed;
1299
+ }
1234
1300
 
1235
1301
  exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
1236
1302
  exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
1237
1303
  exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
1238
1304
  exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
1305
+ exports.CmssyWebhookError = CmssyWebhookError;
1239
1306
  exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
1240
1307
  exports.applyCmssyCsp = applyCmssyCsp;
1241
1308
  exports.assertAuthConfig = assertAuthConfig;
@@ -1259,3 +1326,4 @@ exports.openSession = openSession;
1259
1326
  exports.sealSession = sealSession;
1260
1327
  exports.sessionCookieOptions = sessionCookieOptions;
1261
1328
  exports.splitCmssyLocale = splitCmssyLocale;
1329
+ exports.verifyCmssyWebhook = verifyCmssyWebhook;
package/dist/index.d.cts CHANGED
@@ -160,4 +160,66 @@ interface MyOrdersResult {
160
160
  hasMore: boolean;
161
161
  }
162
162
 
163
- 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, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
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
@@ -160,4 +160,66 @@ interface MyOrdersResult {
160
160
  hasMore: boolean;
161
161
  }
162
162
 
163
- 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, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, sealSession, sessionCookieOptions, splitCmssyLocale };
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
 
@@ -1115,10 +1115,21 @@ var ORDER_FIELDS = `
1115
1115
  customerEmail
1116
1116
  refundedAmount
1117
1117
  paymentProvider
1118
+ paymentStatus
1119
+ fulfillmentStatus
1120
+ amountPaid
1121
+ balanceDue
1122
+ paymentReference
1123
+ trackingNumber
1124
+ trackingCarrier
1125
+ invoiceNumber
1126
+ invoiceUrl
1127
+ invoiceProvider
1118
1128
  paidAt
1119
1129
  fulfilledAt
1120
1130
  createdAt
1121
1131
  items { name price currency quantity sku }
1132
+ payments { amount reference provider at }
1122
1133
  `;
1123
1134
  var MY_ORDERS = `query MyOrders($workspaceId: ID!, $skip: Int, $limit: Int) {
1124
1135
  myOrders(workspaceId: $workspaceId, skip: $skip, limit: $limit) {
@@ -1229,5 +1240,60 @@ function createCmssyOrdersRoute(config) {
1229
1240
  }
1230
1241
  };
1231
1242
  }
1243
+ var CmssyWebhookError = class extends Error {
1244
+ constructor(message) {
1245
+ super(message);
1246
+ this.name = "CmssyWebhookError";
1247
+ }
1248
+ };
1249
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1250
+ function parseSignatureHeader(header) {
1251
+ let timestamp = null;
1252
+ let signature = null;
1253
+ for (const part of header.split(",")) {
1254
+ const idx = part.indexOf("=");
1255
+ if (idx === -1) continue;
1256
+ const key = part.slice(0, idx).trim();
1257
+ const value = part.slice(idx + 1).trim();
1258
+ if (key === "t") timestamp = Number(value);
1259
+ else if (key === "v1") signature = value;
1260
+ }
1261
+ if (timestamp === null || !Number.isFinite(timestamp) || !signature) {
1262
+ throw new CmssyWebhookError("Malformed X-Cmssy-Signature header");
1263
+ }
1264
+ return { timestamp, signature };
1265
+ }
1266
+ function timingSafeHexEqual(expectedHex, providedHex) {
1267
+ const expected = Buffer.from(expectedHex, "hex");
1268
+ const provided = Buffer.from(providedHex, "hex");
1269
+ if (expected.length !== provided.length) return false;
1270
+ return timingSafeEqual(expected, provided);
1271
+ }
1272
+ function verifyCmssyWebhook(options) {
1273
+ const { body, signatureHeader, secret } = options;
1274
+ if (!signatureHeader) {
1275
+ throw new CmssyWebhookError("Missing X-Cmssy-Signature header");
1276
+ }
1277
+ if (!secret) {
1278
+ throw new CmssyWebhookError("Missing webhook secret");
1279
+ }
1280
+ const { timestamp, signature } = parseSignatureHeader(signatureHeader);
1281
+ const toleranceMs = (options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS) * 1e3;
1282
+ const now = options.now ?? Date.now();
1283
+ if (Math.abs(now - timestamp) > toleranceMs) {
1284
+ throw new CmssyWebhookError("Webhook timestamp outside tolerance");
1285
+ }
1286
+ const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
1287
+ if (!timingSafeHexEqual(expected, signature)) {
1288
+ throw new CmssyWebhookError("Webhook signature mismatch");
1289
+ }
1290
+ let parsed;
1291
+ try {
1292
+ parsed = JSON.parse(body);
1293
+ } catch {
1294
+ throw new CmssyWebhookError("Webhook body is not valid JSON");
1295
+ }
1296
+ return parsed;
1297
+ }
1232
1298
 
1233
- export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, 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 };
1299
+ 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.3",
3
+ "version": "0.2.5",
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.3",
39
+ "@cmssy/react": "^0.2.5",
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.3"
52
+ "@cmssy/react": "0.2.5"
53
53
  },
54
54
  "dependencies": {
55
55
  "jose": "^6.2.3"