@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 +187 -5
- package/dist/index.d.cts +75 -2
- package/dist/index.d.ts +75 -2
- package/dist/index.js +186 -7
- package/package.json +3 -3
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:
|
|
225
|
-
const headerList = await
|
|
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
|
|
366
|
-
if (typeof
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
223
|
-
const headerList = await
|
|
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
|
|
364
|
-
if (typeof
|
|
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:
|
|
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.
|
|
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"
|