@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 +57 -0
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +57 -2
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
52
|
+
"@cmssy/react": "0.2.4"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"jose": "^6.2.3"
|