@cmssy/next 0.2.3 → 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
@@ -1231,11 +1231,67 @@ function createCmssyOrdersRoute(config) {
1231
1231
  }
1232
1232
  };
1233
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
+ }
1234
1289
 
1235
1290
  exports.CMSSY_CART_COOKIE = CMSSY_CART_COOKIE;
1236
1291
  exports.CMSSY_EDIT_HEADER = CMSSY_EDIT_HEADER;
1237
1292
  exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
1238
1293
  exports.CMSSY_SESSION_COOKIE = CMSSY_SESSION_COOKIE;
1294
+ exports.CmssyWebhookError = CmssyWebhookError;
1239
1295
  exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
1240
1296
  exports.applyCmssyCsp = applyCmssyCsp;
1241
1297
  exports.assertAuthConfig = assertAuthConfig;
@@ -1259,3 +1315,4 @@ exports.openSession = openSession;
1259
1315
  exports.sealSession = sealSession;
1260
1316
  exports.sessionCookieOptions = sessionCookieOptions;
1261
1317
  exports.splitCmssyLocale = splitCmssyLocale;
1318
+ 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
 
@@ -1229,5 +1229,60 @@ function createCmssyOrdersRoute(config) {
1229
1229
  }
1230
1230
  };
1231
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
+ }
1232
1287
 
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 };
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.3",
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.3",
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.3"
52
+ "@cmssy/react": "0.2.4"
53
53
  },
54
54
  "dependencies": {
55
55
  "jose": "^6.2.3"