@beignet/next 0.0.51 → 0.0.52
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/CHANGELOG.md +19 -0
- package/README.md +40 -20
- package/dist/index.d.ts +25 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +194 -64
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/routes-server/SKILL.md +8 -0
- package/src/index.ts +284 -94
package/src/index.ts
CHANGED
|
@@ -606,6 +606,15 @@ export type CreateWebhookRouteOptions<
|
|
|
606
606
|
* @default false
|
|
607
607
|
*/
|
|
608
608
|
allowUnknownEvents?: AllowUnknownEvents;
|
|
609
|
+
/**
|
|
610
|
+
* Maximum raw webhook body size in bytes.
|
|
611
|
+
*
|
|
612
|
+
* Bodies above this limit are rejected before server resolution, hooks,
|
|
613
|
+
* context creation, verification, or fulfillment.
|
|
614
|
+
*
|
|
615
|
+
* @default 1048576
|
|
616
|
+
*/
|
|
617
|
+
maxBodyBytes?: number;
|
|
609
618
|
/**
|
|
610
619
|
* App-owned fulfillment handler. The webhook is verified before this runs.
|
|
611
620
|
*
|
|
@@ -701,6 +710,16 @@ export type CreatePaymentWebhookRouteOptions<
|
|
|
701
710
|
* @default "stripe-signature"
|
|
702
711
|
*/
|
|
703
712
|
signatureHeader?: string;
|
|
713
|
+
/**
|
|
714
|
+
* Maximum raw webhook body size in bytes.
|
|
715
|
+
*
|
|
716
|
+
* Signed bodies above this limit are rejected before provider verification
|
|
717
|
+
* or fulfillment, and before server resolution, hooks, or context creation.
|
|
718
|
+
* A missing signature takes precedence and cancels the unread body.
|
|
719
|
+
*
|
|
720
|
+
* @default 1048576
|
|
721
|
+
*/
|
|
722
|
+
maxBodyBytes?: number;
|
|
704
723
|
/**
|
|
705
724
|
* App-owned fulfillment handler. The webhook is verified before this runs.
|
|
706
725
|
*
|
|
@@ -1117,40 +1136,59 @@ async function storageResponse(
|
|
|
1117
1136
|
if (!key) return storageNotFoundResponse();
|
|
1118
1137
|
|
|
1119
1138
|
const object = await storage.get(key);
|
|
1120
|
-
if (object
|
|
1139
|
+
if (!object) {
|
|
1121
1140
|
return storageNotFoundResponse();
|
|
1122
1141
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
headers.set("last-modified", object.lastModified.toUTCString());
|
|
1127
|
-
if (!headers.has("x-content-type-options")) {
|
|
1128
|
-
headers.set("x-content-type-options", "nosniff");
|
|
1142
|
+
if (object.visibility !== "public") {
|
|
1143
|
+
await object.cancel().catch(() => undefined);
|
|
1144
|
+
return storageNotFoundResponse();
|
|
1129
1145
|
}
|
|
1130
1146
|
|
|
1131
|
-
|
|
1132
|
-
headers.
|
|
1133
|
-
|
|
1147
|
+
try {
|
|
1148
|
+
const headers = new Headers(resolveHeaders(options.headers, req));
|
|
1149
|
+
headers.set("content-length", String(object.size));
|
|
1150
|
+
headers.set("last-modified", object.lastModified.toUTCString());
|
|
1151
|
+
if (!headers.has("x-content-type-options")) {
|
|
1152
|
+
headers.set("x-content-type-options", "nosniff");
|
|
1153
|
+
}
|
|
1134
1154
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
options,
|
|
1138
|
-
req,
|
|
1139
|
-
object,
|
|
1140
|
-
);
|
|
1141
|
-
if (contentDisposition) {
|
|
1142
|
-
headers.set("content-disposition", contentDisposition);
|
|
1155
|
+
if (object.contentType && !headers.has("content-type")) {
|
|
1156
|
+
headers.set("content-type", object.contentType);
|
|
1143
1157
|
}
|
|
1144
|
-
}
|
|
1145
1158
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1159
|
+
if (!headers.has("content-disposition")) {
|
|
1160
|
+
const contentDisposition = resolveStorageContentDisposition(
|
|
1161
|
+
options,
|
|
1162
|
+
req,
|
|
1163
|
+
object,
|
|
1164
|
+
);
|
|
1165
|
+
if (contentDisposition) {
|
|
1166
|
+
headers.set("content-disposition", contentDisposition);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1149
1169
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1170
|
+
if (object.cacheControl && !headers.has("cache-control")) {
|
|
1171
|
+
headers.set("cache-control", object.cacheControl);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!includeBody) {
|
|
1175
|
+
await object.cancel();
|
|
1176
|
+
return new Response(null, { status: 200, headers });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const body = object.stream();
|
|
1180
|
+
try {
|
|
1181
|
+
return new Response(body, { status: 200, headers });
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
await body.cancel(error).catch(() => undefined);
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
if (!object.bodyUsed) {
|
|
1188
|
+
await object.cancel(error).catch(() => undefined);
|
|
1189
|
+
}
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1154
1192
|
}
|
|
1155
1193
|
|
|
1156
1194
|
function currentRequestServer(req: Request): OpenAPIServer {
|
|
@@ -1399,6 +1437,7 @@ type OperationalErrorCode =
|
|
|
1399
1437
|
| "CRON_SECRET_NOT_CONFIGURED"
|
|
1400
1438
|
| "OUTBOX_DRAIN_FAILED"
|
|
1401
1439
|
| "PAYMENT_WEBHOOK_BODY_READ_FAILED"
|
|
1440
|
+
| "PAYMENT_WEBHOOK_BODY_TOO_LARGE"
|
|
1402
1441
|
| "PAYMENT_WEBHOOK_CONTEXT_FAILED"
|
|
1403
1442
|
| "PAYMENT_WEBHOOK_HANDLER_FAILED"
|
|
1404
1443
|
| "PAYMENT_WEBHOOK_SIGNATURE_MISSING"
|
|
@@ -1406,6 +1445,7 @@ type OperationalErrorCode =
|
|
|
1406
1445
|
| "SCHEDULE_FAILED"
|
|
1407
1446
|
| "UNAUTHORIZED"
|
|
1408
1447
|
| "WEBHOOK_BODY_READ_FAILED"
|
|
1448
|
+
| "WEBHOOK_BODY_TOO_LARGE"
|
|
1409
1449
|
| "WEBHOOK_CONTEXT_FAILED"
|
|
1410
1450
|
| "WEBHOOK_HANDLER_FAILED"
|
|
1411
1451
|
| "WEBHOOK_VERIFICATION_FAILED";
|
|
@@ -1448,14 +1488,94 @@ function toHttpResponseHeaders(
|
|
|
1448
1488
|
* creation then receives a rehydrated request carrying the same method, URL,
|
|
1449
1489
|
* headers, and raw body.
|
|
1450
1490
|
*/
|
|
1451
|
-
function
|
|
1452
|
-
return
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1491
|
+
function rehydrateWebhookRequest(req: Request, rawBody: string): Request {
|
|
1492
|
+
return new Request(req.url, {
|
|
1493
|
+
method: req.method,
|
|
1494
|
+
headers: req.headers,
|
|
1495
|
+
body: rawBody,
|
|
1496
|
+
signal: req.signal,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const DEFAULT_WEBHOOK_BODY_MAX_BYTES = 1024 * 1024;
|
|
1501
|
+
|
|
1502
|
+
class WebhookBodyTooLargeError extends Error {
|
|
1503
|
+
constructor(readonly maxBytes: number) {
|
|
1504
|
+
super("Webhook body exceeds the configured size limit.");
|
|
1505
|
+
this.name = "WebhookBodyTooLargeError";
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function webhookBodyLimit(maxBytes: number | undefined): number {
|
|
1510
|
+
const resolved = maxBytes ?? DEFAULT_WEBHOOK_BODY_MAX_BYTES;
|
|
1511
|
+
if (!Number.isFinite(resolved) || resolved <= 0) {
|
|
1512
|
+
throw new Error("Webhook maxBodyBytes must be a positive number.");
|
|
1513
|
+
}
|
|
1514
|
+
return resolved;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function cancelWebhookBody(
|
|
1518
|
+
source: { cancel(reason?: unknown): Promise<void> },
|
|
1519
|
+
reason: unknown,
|
|
1520
|
+
): void {
|
|
1521
|
+
try {
|
|
1522
|
+
void source.cancel(reason).catch(() => {});
|
|
1523
|
+
} catch {
|
|
1524
|
+
// Cancellation is best-effort and must not replace the selected response.
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function cancelWebhookRequestBody(req: HttpRequestLike, reason: unknown): void {
|
|
1529
|
+
const body = req.raw?.body ?? nativeRequestOf(req).body;
|
|
1530
|
+
if (body) cancelWebhookBody(body, reason);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async function readWebhookBody(
|
|
1534
|
+
req: HttpRequestLike,
|
|
1535
|
+
maxBytes: number,
|
|
1536
|
+
): Promise<string> {
|
|
1537
|
+
const body = req.raw?.body ?? nativeRequestOf(req).body;
|
|
1538
|
+
const contentLength = req.headers.get("content-length");
|
|
1539
|
+
|
|
1540
|
+
if (contentLength !== null) {
|
|
1541
|
+
const declaredBytes = Number(contentLength);
|
|
1542
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
1543
|
+
const error = new WebhookBodyTooLargeError(maxBytes);
|
|
1544
|
+
if (body) cancelWebhookBody(body, error);
|
|
1545
|
+
throw error;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (!body) {
|
|
1550
|
+
const text = await req.text();
|
|
1551
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
1552
|
+
throw new WebhookBodyTooLargeError(maxBytes);
|
|
1553
|
+
}
|
|
1554
|
+
return text;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const reader = body.getReader();
|
|
1558
|
+
const decoder = new TextDecoder();
|
|
1559
|
+
let receivedBytes = 0;
|
|
1560
|
+
let text = "";
|
|
1561
|
+
|
|
1562
|
+
try {
|
|
1563
|
+
while (true) {
|
|
1564
|
+
const result = await reader.read();
|
|
1565
|
+
if (result.done) break;
|
|
1566
|
+
|
|
1567
|
+
receivedBytes += result.value.byteLength;
|
|
1568
|
+
if (receivedBytes > maxBytes) {
|
|
1569
|
+
const error = new WebhookBodyTooLargeError(maxBytes);
|
|
1570
|
+
cancelWebhookBody(reader, error);
|
|
1571
|
+
throw error;
|
|
1572
|
+
}
|
|
1573
|
+
text += decoder.decode(result.value, { stream: true });
|
|
1574
|
+
}
|
|
1575
|
+
return text + decoder.decode();
|
|
1576
|
+
} finally {
|
|
1577
|
+
reader.releaseLock();
|
|
1578
|
+
}
|
|
1459
1579
|
}
|
|
1460
1580
|
|
|
1461
1581
|
function headersToRecord(headers: Headers): Record<string, string> {
|
|
@@ -1955,8 +2075,9 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
|
|
|
1955
2075
|
/**
|
|
1956
2076
|
* Create a Next.js POST handler for generic verified inbound webhooks.
|
|
1957
2077
|
*
|
|
1958
|
-
* The route reads the raw request body
|
|
1959
|
-
* builds app context from
|
|
2078
|
+
* The route bounds and reads the raw request body before resolving the server,
|
|
2079
|
+
* then normalizes request headers and builds app context from a rehydrated
|
|
2080
|
+
* request through
|
|
1960
2081
|
* `server.createRequestContext(...)`, verifies the event through the webhook
|
|
1961
2082
|
* definition or a context-aware verifier, validates the matching event
|
|
1962
2083
|
* payload schema, and delegates fulfillment to app code.
|
|
@@ -1970,28 +2091,43 @@ export function createWebhookRoute<
|
|
|
1970
2091
|
): {
|
|
1971
2092
|
POST: (req: Request) => Promise<Response>;
|
|
1972
2093
|
} {
|
|
2094
|
+
const maxBodyBytes = webhookBodyLimit(options.maxBodyBytes);
|
|
2095
|
+
const preflightBodies = new WeakMap<Request, string>();
|
|
2096
|
+
|
|
1973
2097
|
const handleWebhookRequest = async ({
|
|
1974
2098
|
req,
|
|
1975
2099
|
ctx,
|
|
2100
|
+
rawBody: providedRawBody,
|
|
1976
2101
|
}: {
|
|
1977
2102
|
req: HttpRequestLike;
|
|
1978
2103
|
ctx: Ctx;
|
|
2104
|
+
rawBody?: string;
|
|
1979
2105
|
}): Promise<Response> => {
|
|
1980
2106
|
const nativeReq = nativeRequestOf(req);
|
|
1981
2107
|
const headers = resolveHeaders(options.headers, nativeReq);
|
|
1982
2108
|
const requestHeaders = headersToRecord(new Headers(req.headers));
|
|
1983
2109
|
|
|
1984
|
-
let rawBody
|
|
2110
|
+
let rawBody = providedRawBody ?? preflightBodies.get(nativeReq);
|
|
2111
|
+
preflightBodies.delete(nativeReq);
|
|
1985
2112
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2113
|
+
if (rawBody === undefined) {
|
|
2114
|
+
try {
|
|
2115
|
+
rawBody = await readWebhookBody(req, maxBodyBytes);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
return error instanceof WebhookBodyTooLargeError
|
|
2118
|
+
? operationalErrorJson(
|
|
2119
|
+
"WEBHOOK_BODY_TOO_LARGE",
|
|
2120
|
+
"Webhook body exceeds the configured size limit.",
|
|
2121
|
+
413,
|
|
2122
|
+
headers,
|
|
2123
|
+
)
|
|
2124
|
+
: operationalErrorJson(
|
|
2125
|
+
"WEBHOOK_BODY_READ_FAILED",
|
|
2126
|
+
"Webhook body read failed.",
|
|
2127
|
+
400,
|
|
2128
|
+
headers,
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
1995
2131
|
}
|
|
1996
2132
|
|
|
1997
2133
|
let event: WebhookRouteHandlerArgs<
|
|
@@ -2102,32 +2238,42 @@ export function createWebhookRoute<
|
|
|
2102
2238
|
|
|
2103
2239
|
return {
|
|
2104
2240
|
POST: async (req) => {
|
|
2105
|
-
const server = await resolveServerSource(options.server);
|
|
2106
|
-
|
|
2107
|
-
const pipelined = runPipeline(server);
|
|
2108
|
-
if (pipelined) return pipelined(req);
|
|
2109
|
-
|
|
2110
|
-
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
2111
|
-
// context directly and run the same handler outside the hooks pipeline.
|
|
2112
2241
|
const headers = resolveHeaders(options.headers, req);
|
|
2113
2242
|
|
|
2114
2243
|
let rawBody: string;
|
|
2115
2244
|
try {
|
|
2116
|
-
rawBody = await req
|
|
2117
|
-
} catch {
|
|
2118
|
-
return
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2245
|
+
rawBody = await readWebhookBody(toRequestLike(req), maxBodyBytes);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
return error instanceof WebhookBodyTooLargeError
|
|
2248
|
+
? operationalErrorJson(
|
|
2249
|
+
"WEBHOOK_BODY_TOO_LARGE",
|
|
2250
|
+
"Webhook body exceeds the configured size limit.",
|
|
2251
|
+
413,
|
|
2252
|
+
headers,
|
|
2253
|
+
)
|
|
2254
|
+
: operationalErrorJson(
|
|
2255
|
+
"WEBHOOK_BODY_READ_FAILED",
|
|
2256
|
+
"Webhook body read failed.",
|
|
2257
|
+
400,
|
|
2258
|
+
headers,
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const contextRequest = rehydrateWebhookRequest(req, rawBody);
|
|
2263
|
+
const server = await resolveServerSource(options.server);
|
|
2264
|
+
const pipelined = runPipeline(server);
|
|
2265
|
+
if (pipelined) {
|
|
2266
|
+
preflightBodies.set(contextRequest, rawBody);
|
|
2267
|
+
return pipelined(contextRequest);
|
|
2124
2268
|
}
|
|
2125
2269
|
|
|
2126
|
-
|
|
2270
|
+
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
2271
|
+
// context directly and run the same handler outside the hooks pipeline.
|
|
2272
|
+
const requestLike = toRequestLike(contextRequest);
|
|
2127
2273
|
|
|
2128
2274
|
let ctx: Ctx;
|
|
2129
2275
|
try {
|
|
2130
|
-
ctx = await server.createRequestContext(
|
|
2276
|
+
ctx = await server.createRequestContext(requestLike);
|
|
2131
2277
|
} catch {
|
|
2132
2278
|
return operationalErrorJson(
|
|
2133
2279
|
"WEBHOOK_CONTEXT_FAILED",
|
|
@@ -2137,7 +2283,11 @@ export function createWebhookRoute<
|
|
|
2137
2283
|
);
|
|
2138
2284
|
}
|
|
2139
2285
|
|
|
2140
|
-
return handleWebhookRequest({
|
|
2286
|
+
return handleWebhookRequest({
|
|
2287
|
+
req: requestLike,
|
|
2288
|
+
ctx,
|
|
2289
|
+
rawBody,
|
|
2290
|
+
});
|
|
2141
2291
|
},
|
|
2142
2292
|
};
|
|
2143
2293
|
}
|
|
@@ -2145,8 +2295,9 @@ export function createWebhookRoute<
|
|
|
2145
2295
|
/**
|
|
2146
2296
|
* Create a Next.js POST handler for provider-verified payment webhooks.
|
|
2147
2297
|
*
|
|
2148
|
-
* The route
|
|
2149
|
-
* body
|
|
2298
|
+
* The route checks the provider signature header and bounds the raw request
|
|
2299
|
+
* body before resolving the server, then builds app context from a rehydrated
|
|
2300
|
+
* request through
|
|
2150
2301
|
* `server.createRequestContext(...)`, verifies the event through
|
|
2151
2302
|
* `ctx.ports.payments`, and then delegates fulfillment to app code. This is the canonical helper for Beignet
|
|
2152
2303
|
* billing flows backed by `PaymentsPort`. Keep this route on the Node.js
|
|
@@ -2160,19 +2311,27 @@ export function createPaymentWebhookRoute<
|
|
|
2160
2311
|
POST: (req: Request) => Promise<Response>;
|
|
2161
2312
|
} {
|
|
2162
2313
|
const signatureHeader = options.signatureHeader ?? "stripe-signature";
|
|
2314
|
+
const maxBodyBytes = webhookBodyLimit(options.maxBodyBytes);
|
|
2315
|
+
const preflightBodies = new WeakMap<Request, string>();
|
|
2163
2316
|
|
|
2164
2317
|
const handlePaymentWebhook = async ({
|
|
2165
2318
|
req,
|
|
2166
2319
|
ctx,
|
|
2320
|
+
rawBody: providedRawBody,
|
|
2167
2321
|
}: {
|
|
2168
2322
|
req: HttpRequestLike;
|
|
2169
2323
|
ctx: Ctx;
|
|
2324
|
+
rawBody?: string;
|
|
2170
2325
|
}): Promise<Response> => {
|
|
2171
2326
|
const nativeReq = nativeRequestOf(req);
|
|
2172
2327
|
const headers = resolveHeaders(options.headers, nativeReq);
|
|
2173
2328
|
const signature = new Headers(req.headers).get(signatureHeader);
|
|
2174
2329
|
|
|
2175
2330
|
if (!signature) {
|
|
2331
|
+
cancelWebhookRequestBody(
|
|
2332
|
+
req,
|
|
2333
|
+
new Error(`Missing ${signatureHeader} header.`),
|
|
2334
|
+
);
|
|
2176
2335
|
return operationalErrorJson(
|
|
2177
2336
|
"PAYMENT_WEBHOOK_SIGNATURE_MISSING",
|
|
2178
2337
|
`Missing ${signatureHeader} header.`,
|
|
@@ -2181,17 +2340,27 @@ export function createPaymentWebhookRoute<
|
|
|
2181
2340
|
);
|
|
2182
2341
|
}
|
|
2183
2342
|
|
|
2184
|
-
let rawBody
|
|
2343
|
+
let rawBody = providedRawBody ?? preflightBodies.get(nativeReq);
|
|
2344
|
+
preflightBodies.delete(nativeReq);
|
|
2185
2345
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2346
|
+
if (rawBody === undefined) {
|
|
2347
|
+
try {
|
|
2348
|
+
rawBody = await readWebhookBody(req, maxBodyBytes);
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
return error instanceof WebhookBodyTooLargeError
|
|
2351
|
+
? operationalErrorJson(
|
|
2352
|
+
"PAYMENT_WEBHOOK_BODY_TOO_LARGE",
|
|
2353
|
+
"Payment webhook body exceeds the configured size limit.",
|
|
2354
|
+
413,
|
|
2355
|
+
headers,
|
|
2356
|
+
)
|
|
2357
|
+
: operationalErrorJson(
|
|
2358
|
+
"PAYMENT_WEBHOOK_BODY_READ_FAILED",
|
|
2359
|
+
"Payment webhook body read failed.",
|
|
2360
|
+
400,
|
|
2361
|
+
headers,
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2195
2364
|
}
|
|
2196
2365
|
|
|
2197
2366
|
let event: PaymentWebhookEvent;
|
|
@@ -2274,17 +2443,15 @@ export function createPaymentWebhookRoute<
|
|
|
2274
2443
|
|
|
2275
2444
|
return {
|
|
2276
2445
|
POST: async (req) => {
|
|
2277
|
-
const server = await resolveServerSource(options.server);
|
|
2278
|
-
|
|
2279
|
-
const pipelined = runPipeline(server);
|
|
2280
|
-
if (pipelined) return pipelined(req);
|
|
2281
|
-
|
|
2282
|
-
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
2283
|
-
// context directly and run the same handler outside the hooks pipeline.
|
|
2284
|
-
// The signature gate runs first so no context is created without one.
|
|
2285
2446
|
const headers = resolveHeaders(options.headers, req);
|
|
2286
2447
|
|
|
2287
2448
|
if (!req.headers.get(signatureHeader)) {
|
|
2449
|
+
if (req.body) {
|
|
2450
|
+
cancelWebhookBody(
|
|
2451
|
+
req.body,
|
|
2452
|
+
new Error(`Missing ${signatureHeader} header.`),
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2288
2455
|
return operationalErrorJson(
|
|
2289
2456
|
"PAYMENT_WEBHOOK_SIGNATURE_MISSING",
|
|
2290
2457
|
`Missing ${signatureHeader} header.`,
|
|
@@ -2295,19 +2462,38 @@ export function createPaymentWebhookRoute<
|
|
|
2295
2462
|
|
|
2296
2463
|
let rawBody: string;
|
|
2297
2464
|
try {
|
|
2298
|
-
rawBody = await req
|
|
2299
|
-
} catch {
|
|
2300
|
-
return
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2465
|
+
rawBody = await readWebhookBody(toRequestLike(req), maxBodyBytes);
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
return error instanceof WebhookBodyTooLargeError
|
|
2468
|
+
? operationalErrorJson(
|
|
2469
|
+
"PAYMENT_WEBHOOK_BODY_TOO_LARGE",
|
|
2470
|
+
"Payment webhook body exceeds the configured size limit.",
|
|
2471
|
+
413,
|
|
2472
|
+
headers,
|
|
2473
|
+
)
|
|
2474
|
+
: operationalErrorJson(
|
|
2475
|
+
"PAYMENT_WEBHOOK_BODY_READ_FAILED",
|
|
2476
|
+
"Payment webhook body read failed.",
|
|
2477
|
+
400,
|
|
2478
|
+
headers,
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const contextRequest = rehydrateWebhookRequest(req, rawBody);
|
|
2483
|
+
const server = await resolveServerSource(options.server);
|
|
2484
|
+
const pipelined = runPipeline(server);
|
|
2485
|
+
if (pipelined) {
|
|
2486
|
+
preflightBodies.set(contextRequest, rawBody);
|
|
2487
|
+
return pipelined(contextRequest);
|
|
2306
2488
|
}
|
|
2307
2489
|
|
|
2490
|
+
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
2491
|
+
// context directly and run the same handler outside the hooks pipeline.
|
|
2492
|
+
const requestLike = toRequestLike(contextRequest);
|
|
2493
|
+
|
|
2308
2494
|
let ctx: Ctx;
|
|
2309
2495
|
try {
|
|
2310
|
-
ctx = await server.createRequestContext(
|
|
2496
|
+
ctx = await server.createRequestContext(requestLike);
|
|
2311
2497
|
} catch {
|
|
2312
2498
|
return operationalErrorJson(
|
|
2313
2499
|
"PAYMENT_WEBHOOK_CONTEXT_FAILED",
|
|
@@ -2317,7 +2503,11 @@ export function createPaymentWebhookRoute<
|
|
|
2317
2503
|
);
|
|
2318
2504
|
}
|
|
2319
2505
|
|
|
2320
|
-
return handlePaymentWebhook({
|
|
2506
|
+
return handlePaymentWebhook({
|
|
2507
|
+
req: requestLike,
|
|
2508
|
+
ctx,
|
|
2509
|
+
rawBody,
|
|
2510
|
+
});
|
|
2321
2511
|
},
|
|
2322
2512
|
};
|
|
2323
2513
|
}
|