@beignet/next 0.0.50 → 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 +26 -0
- package/README.md +57 -25
- package/dist/index.d.ts +41 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +271 -76
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/routes-server/SKILL.md +13 -3
- package/src/index.ts +391 -108
package/dist/index.js
CHANGED
|
@@ -241,31 +241,51 @@ async function storageResponse(storage, req, basePath, options, includeBody) {
|
|
|
241
241
|
if (!key)
|
|
242
242
|
return storageNotFoundResponse();
|
|
243
243
|
const object = await storage.get(key);
|
|
244
|
-
if (object
|
|
244
|
+
if (!object) {
|
|
245
245
|
return storageNotFoundResponse();
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!headers.has("x-content-type-options")) {
|
|
251
|
-
headers.set("x-content-type-options", "nosniff");
|
|
252
|
-
}
|
|
253
|
-
if (object.contentType && !headers.has("content-type")) {
|
|
254
|
-
headers.set("content-type", object.contentType);
|
|
247
|
+
if (object.visibility !== "public") {
|
|
248
|
+
await object.cancel().catch(() => undefined);
|
|
249
|
+
return storageNotFoundResponse();
|
|
255
250
|
}
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
try {
|
|
252
|
+
const headers = new Headers(resolveHeaders(options.headers, req));
|
|
253
|
+
headers.set("content-length", String(object.size));
|
|
254
|
+
headers.set("last-modified", object.lastModified.toUTCString());
|
|
255
|
+
if (!headers.has("x-content-type-options")) {
|
|
256
|
+
headers.set("x-content-type-options", "nosniff");
|
|
257
|
+
}
|
|
258
|
+
if (object.contentType && !headers.has("content-type")) {
|
|
259
|
+
headers.set("content-type", object.contentType);
|
|
260
|
+
}
|
|
261
|
+
if (!headers.has("content-disposition")) {
|
|
262
|
+
const contentDisposition = resolveStorageContentDisposition(options, req, object);
|
|
263
|
+
if (contentDisposition) {
|
|
264
|
+
headers.set("content-disposition", contentDisposition);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (object.cacheControl && !headers.has("cache-control")) {
|
|
268
|
+
headers.set("cache-control", object.cacheControl);
|
|
269
|
+
}
|
|
270
|
+
if (!includeBody) {
|
|
271
|
+
await object.cancel();
|
|
272
|
+
return new Response(null, { status: 200, headers });
|
|
273
|
+
}
|
|
274
|
+
const body = object.stream();
|
|
275
|
+
try {
|
|
276
|
+
return new Response(body, { status: 200, headers });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
await body.cancel(error).catch(() => undefined);
|
|
280
|
+
throw error;
|
|
260
281
|
}
|
|
261
282
|
}
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (!object.bodyUsed) {
|
|
285
|
+
await object.cancel(error).catch(() => undefined);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
264
288
|
}
|
|
265
|
-
return new Response(includeBody ? object.stream() : null, {
|
|
266
|
-
status: 200,
|
|
267
|
-
headers,
|
|
268
|
-
});
|
|
269
289
|
}
|
|
270
290
|
function currentRequestServer(req) {
|
|
271
291
|
const url = new URL(req.url);
|
|
@@ -466,12 +486,84 @@ function toHttpResponseHeaders(headers) {
|
|
|
466
486
|
* creation then receives a rehydrated request carrying the same method, URL,
|
|
467
487
|
* headers, and raw body.
|
|
468
488
|
*/
|
|
469
|
-
function
|
|
470
|
-
return
|
|
489
|
+
function rehydrateWebhookRequest(req, rawBody) {
|
|
490
|
+
return new Request(req.url, {
|
|
471
491
|
method: req.method,
|
|
472
492
|
headers: req.headers,
|
|
473
493
|
body: rawBody,
|
|
474
|
-
|
|
494
|
+
signal: req.signal,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const DEFAULT_WEBHOOK_BODY_MAX_BYTES = 1024 * 1024;
|
|
498
|
+
class WebhookBodyTooLargeError extends Error {
|
|
499
|
+
maxBytes;
|
|
500
|
+
constructor(maxBytes) {
|
|
501
|
+
super("Webhook body exceeds the configured size limit.");
|
|
502
|
+
this.maxBytes = maxBytes;
|
|
503
|
+
this.name = "WebhookBodyTooLargeError";
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function webhookBodyLimit(maxBytes) {
|
|
507
|
+
const resolved = maxBytes ?? DEFAULT_WEBHOOK_BODY_MAX_BYTES;
|
|
508
|
+
if (!Number.isFinite(resolved) || resolved <= 0) {
|
|
509
|
+
throw new Error("Webhook maxBodyBytes must be a positive number.");
|
|
510
|
+
}
|
|
511
|
+
return resolved;
|
|
512
|
+
}
|
|
513
|
+
function cancelWebhookBody(source, reason) {
|
|
514
|
+
try {
|
|
515
|
+
void source.cancel(reason).catch(() => { });
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Cancellation is best-effort and must not replace the selected response.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function cancelWebhookRequestBody(req, reason) {
|
|
522
|
+
const body = req.raw?.body ?? nativeRequestOf(req).body;
|
|
523
|
+
if (body)
|
|
524
|
+
cancelWebhookBody(body, reason);
|
|
525
|
+
}
|
|
526
|
+
async function readWebhookBody(req, maxBytes) {
|
|
527
|
+
const body = req.raw?.body ?? nativeRequestOf(req).body;
|
|
528
|
+
const contentLength = req.headers.get("content-length");
|
|
529
|
+
if (contentLength !== null) {
|
|
530
|
+
const declaredBytes = Number(contentLength);
|
|
531
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
532
|
+
const error = new WebhookBodyTooLargeError(maxBytes);
|
|
533
|
+
if (body)
|
|
534
|
+
cancelWebhookBody(body, error);
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!body) {
|
|
539
|
+
const text = await req.text();
|
|
540
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
541
|
+
throw new WebhookBodyTooLargeError(maxBytes);
|
|
542
|
+
}
|
|
543
|
+
return text;
|
|
544
|
+
}
|
|
545
|
+
const reader = body.getReader();
|
|
546
|
+
const decoder = new TextDecoder();
|
|
547
|
+
let receivedBytes = 0;
|
|
548
|
+
let text = "";
|
|
549
|
+
try {
|
|
550
|
+
while (true) {
|
|
551
|
+
const result = await reader.read();
|
|
552
|
+
if (result.done)
|
|
553
|
+
break;
|
|
554
|
+
receivedBytes += result.value.byteLength;
|
|
555
|
+
if (receivedBytes > maxBytes) {
|
|
556
|
+
const error = new WebhookBodyTooLargeError(maxBytes);
|
|
557
|
+
cancelWebhookBody(reader, error);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
text += decoder.decode(result.value, { stream: true });
|
|
561
|
+
}
|
|
562
|
+
return text + decoder.decode();
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
reader.releaseLock();
|
|
566
|
+
}
|
|
475
567
|
}
|
|
476
568
|
function headersToRecord(headers) {
|
|
477
569
|
const result = {};
|
|
@@ -539,9 +631,15 @@ async function runOutboxDrainWithContext(ctx, registry, options, mechanism) {
|
|
|
539
631
|
eventBus: ctx.ports.eventBus,
|
|
540
632
|
jobs: ctx.ports.jobs,
|
|
541
633
|
batchSize: options.batchSize,
|
|
634
|
+
concurrency: options.concurrency,
|
|
542
635
|
leaseMs: options.leaseMs,
|
|
636
|
+
heartbeatMs: options.heartbeatMs,
|
|
637
|
+
maxActiveMs: options.maxActiveMs,
|
|
543
638
|
retryDelayMs: options.retryDelayMs,
|
|
544
|
-
instrumentation:
|
|
639
|
+
instrumentation: {
|
|
640
|
+
instrumentation: resolveProviderInstrumentationPort(ctx.ports),
|
|
641
|
+
tracing: ctx.ports.tracing,
|
|
642
|
+
},
|
|
545
643
|
instrumentationContext: {
|
|
546
644
|
requestId: ctx.requestId,
|
|
547
645
|
traceId: ctx.traceId,
|
|
@@ -584,10 +682,47 @@ async function runOutboxDrainWithContext(ctx, registry, options, mechanism) {
|
|
|
584
682
|
},
|
|
585
683
|
});
|
|
586
684
|
},
|
|
587
|
-
async
|
|
685
|
+
async onLeaseError(failure) {
|
|
686
|
+
const outcome = failure.state === "lost"
|
|
687
|
+
? "leaseLost"
|
|
688
|
+
: failure.state === "recovered"
|
|
689
|
+
? "leaseRecovered"
|
|
690
|
+
: "leaseDegraded";
|
|
691
|
+
await tryReportException({
|
|
692
|
+
reporter: ctx.ports.errorReporter,
|
|
693
|
+
error: failure.error,
|
|
694
|
+
reportOptions: {
|
|
695
|
+
level: failure.state === "lost" ? "error" : "warning",
|
|
696
|
+
mechanism,
|
|
697
|
+
handled: failure.state !== "lost",
|
|
698
|
+
requestId: ctx.requestId,
|
|
699
|
+
traceId: ctx.traceId,
|
|
700
|
+
tags: {
|
|
701
|
+
"beignet.kind": "outbox",
|
|
702
|
+
"beignet.outbox.kind": failure.message.kind,
|
|
703
|
+
"beignet.outbox.name": failure.message.name,
|
|
704
|
+
"beignet.outbox.outcome": outcome,
|
|
705
|
+
},
|
|
706
|
+
contexts: {
|
|
707
|
+
outbox: {
|
|
708
|
+
messageId: failure.message.id,
|
|
709
|
+
kind: failure.message.kind,
|
|
710
|
+
name: failure.message.name,
|
|
711
|
+
attempts: failure.message.attempts,
|
|
712
|
+
maxAttempts: failure.message.maxAttempts,
|
|
713
|
+
operation: failure.operation,
|
|
714
|
+
state: failure.state,
|
|
715
|
+
confirmedLost: failure.confirmedLost,
|
|
716
|
+
outcome,
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
},
|
|
722
|
+
async onSettlementError(failure) {
|
|
588
723
|
await tryReportException({
|
|
589
724
|
reporter: ctx.ports.errorReporter,
|
|
590
|
-
error:
|
|
725
|
+
error: failure.error,
|
|
591
726
|
reportOptions: {
|
|
592
727
|
level: "error",
|
|
593
728
|
mechanism,
|
|
@@ -596,17 +731,19 @@ async function runOutboxDrainWithContext(ctx, registry, options, mechanism) {
|
|
|
596
731
|
traceId: ctx.traceId,
|
|
597
732
|
tags: {
|
|
598
733
|
"beignet.kind": "outbox",
|
|
599
|
-
"beignet.outbox.kind": message.kind,
|
|
600
|
-
"beignet.outbox.name": message.name,
|
|
734
|
+
"beignet.outbox.kind": failure.message.kind,
|
|
735
|
+
"beignet.outbox.name": failure.message.name,
|
|
601
736
|
"beignet.outbox.outcome": "settlementFailed",
|
|
602
737
|
},
|
|
603
738
|
contexts: {
|
|
604
739
|
outbox: {
|
|
605
|
-
messageId: message.id,
|
|
606
|
-
kind: message.kind,
|
|
607
|
-
name: message.name,
|
|
608
|
-
attempts: message.attempts,
|
|
609
|
-
maxAttempts: message.maxAttempts,
|
|
740
|
+
messageId: failure.message.id,
|
|
741
|
+
kind: failure.message.kind,
|
|
742
|
+
name: failure.message.name,
|
|
743
|
+
attempts: failure.message.attempts,
|
|
744
|
+
maxAttempts: failure.message.maxAttempts,
|
|
745
|
+
operation: failure.operation,
|
|
746
|
+
deliverySucceeded: failure.deliverySucceeded,
|
|
610
747
|
outcome: "settlementFailed",
|
|
611
748
|
},
|
|
612
749
|
},
|
|
@@ -620,7 +757,7 @@ async function runOutboxDrainWithContext(ctx, registry, options, mechanism) {
|
|
|
620
757
|
watcher: "outbox",
|
|
621
758
|
name: "outbox.drain",
|
|
622
759
|
label: "Outbox drain",
|
|
623
|
-
summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered`,
|
|
760
|
+
summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered, ${result.settlementFailed} settlement-uncertain, ${result.leaseLost} lease-lost`,
|
|
624
761
|
requestId: ctx.requestId,
|
|
625
762
|
traceId: ctx.traceId,
|
|
626
763
|
details: result,
|
|
@@ -633,6 +770,12 @@ async function runOutboxDrainWithContext(ctx, registry, options, mechanism) {
|
|
|
633
770
|
}
|
|
634
771
|
return result;
|
|
635
772
|
}
|
|
773
|
+
function hasUncertainOutboxResult(result) {
|
|
774
|
+
return result.settlementFailed > 0 || result.leaseLost > 0;
|
|
775
|
+
}
|
|
776
|
+
function uncertainOutboxError(result) {
|
|
777
|
+
return new Error(`Outbox drain completed with ${result.settlementFailed} settlement failure(s) and ${result.leaseLost} lost lease(s).`);
|
|
778
|
+
}
|
|
636
779
|
async function resolveOutboxRegistry(source) {
|
|
637
780
|
return typeof source === "function" ? source() : source;
|
|
638
781
|
}
|
|
@@ -704,7 +847,11 @@ export function createNextOutboxDrainTrigger(options) {
|
|
|
704
847
|
try {
|
|
705
848
|
ctx = await options.createContext();
|
|
706
849
|
const registry = await resolveOutboxRegistry(options.registry);
|
|
707
|
-
await runOutboxDrainWithContext(ctx, registry, options, "beignet.outbox.next.after");
|
|
850
|
+
const result = await runOutboxDrainWithContext(ctx, registry, options, "beignet.outbox.next.after");
|
|
851
|
+
if (hasUncertainOutboxResult(result)) {
|
|
852
|
+
const error = uncertainOutboxError(result);
|
|
853
|
+
await notifyDeferredWorkError(options.onError, error);
|
|
854
|
+
}
|
|
708
855
|
}
|
|
709
856
|
catch (error) {
|
|
710
857
|
if (ctx) {
|
|
@@ -747,6 +894,16 @@ export function createOutboxDrainRoute(options) {
|
|
|
747
894
|
const headers = resolveHeaders(options.headers, nativeReq);
|
|
748
895
|
try {
|
|
749
896
|
const result = await runOutboxDrainWithContext(ctx, options.registry, options, "beignet.outbox.next");
|
|
897
|
+
if (hasUncertainOutboxResult(result)) {
|
|
898
|
+
return routeJson({
|
|
899
|
+
ok: false,
|
|
900
|
+
error: {
|
|
901
|
+
code: "OUTBOX_DRAIN_UNCERTAIN",
|
|
902
|
+
message: "Outbox drain completed with unconfirmed delivery state.",
|
|
903
|
+
},
|
|
904
|
+
result,
|
|
905
|
+
}, 500, headers);
|
|
906
|
+
}
|
|
750
907
|
return routeJson({
|
|
751
908
|
ok: true,
|
|
752
909
|
result,
|
|
@@ -798,23 +955,31 @@ export function createOutboxDrainRoute(options) {
|
|
|
798
955
|
/**
|
|
799
956
|
* Create a Next.js POST handler for generic verified inbound webhooks.
|
|
800
957
|
*
|
|
801
|
-
* The route reads the raw request body
|
|
802
|
-
* builds app context from
|
|
958
|
+
* The route bounds and reads the raw request body before resolving the server,
|
|
959
|
+
* then normalizes request headers and builds app context from a rehydrated
|
|
960
|
+
* request through
|
|
803
961
|
* `server.createRequestContext(...)`, verifies the event through the webhook
|
|
804
962
|
* definition or a context-aware verifier, validates the matching event
|
|
805
963
|
* payload schema, and delegates fulfillment to app code.
|
|
806
964
|
*/
|
|
807
965
|
export function createWebhookRoute(options) {
|
|
808
|
-
const
|
|
966
|
+
const maxBodyBytes = webhookBodyLimit(options.maxBodyBytes);
|
|
967
|
+
const preflightBodies = new WeakMap();
|
|
968
|
+
const handleWebhookRequest = async ({ req, ctx, rawBody: providedRawBody, }) => {
|
|
809
969
|
const nativeReq = nativeRequestOf(req);
|
|
810
970
|
const headers = resolveHeaders(options.headers, nativeReq);
|
|
811
971
|
const requestHeaders = headersToRecord(new Headers(req.headers));
|
|
812
|
-
let rawBody;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
972
|
+
let rawBody = providedRawBody ?? preflightBodies.get(nativeReq);
|
|
973
|
+
preflightBodies.delete(nativeReq);
|
|
974
|
+
if (rawBody === undefined) {
|
|
975
|
+
try {
|
|
976
|
+
rawBody = await readWebhookBody(req, maxBodyBytes);
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
return error instanceof WebhookBodyTooLargeError
|
|
980
|
+
? operationalErrorJson("WEBHOOK_BODY_TOO_LARGE", "Webhook body exceeds the configured size limit.", 413, headers)
|
|
981
|
+
: operationalErrorJson("WEBHOOK_BODY_READ_FAILED", "Webhook body read failed.", 400, headers);
|
|
982
|
+
}
|
|
818
983
|
}
|
|
819
984
|
let event;
|
|
820
985
|
try {
|
|
@@ -900,37 +1065,47 @@ export function createWebhookRoute(options) {
|
|
|
900
1065
|
}, handleWebhookRequest);
|
|
901
1066
|
return {
|
|
902
1067
|
POST: async (req) => {
|
|
903
|
-
const server = await resolveServerSource(options.server);
|
|
904
|
-
const pipelined = runPipeline(server);
|
|
905
|
-
if (pipelined)
|
|
906
|
-
return pipelined(req);
|
|
907
|
-
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
908
|
-
// context directly and run the same handler outside the hooks pipeline.
|
|
909
1068
|
const headers = resolveHeaders(options.headers, req);
|
|
910
1069
|
let rawBody;
|
|
911
1070
|
try {
|
|
912
|
-
rawBody = await req
|
|
1071
|
+
rawBody = await readWebhookBody(toRequestLike(req), maxBodyBytes);
|
|
913
1072
|
}
|
|
914
|
-
catch {
|
|
915
|
-
return
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
return error instanceof WebhookBodyTooLargeError
|
|
1075
|
+
? operationalErrorJson("WEBHOOK_BODY_TOO_LARGE", "Webhook body exceeds the configured size limit.", 413, headers)
|
|
1076
|
+
: operationalErrorJson("WEBHOOK_BODY_READ_FAILED", "Webhook body read failed.", 400, headers);
|
|
1077
|
+
}
|
|
1078
|
+
const contextRequest = rehydrateWebhookRequest(req, rawBody);
|
|
1079
|
+
const server = await resolveServerSource(options.server);
|
|
1080
|
+
const pipelined = runPipeline(server);
|
|
1081
|
+
if (pipelined) {
|
|
1082
|
+
preflightBodies.set(contextRequest, rawBody);
|
|
1083
|
+
return pipelined(contextRequest);
|
|
916
1084
|
}
|
|
917
|
-
|
|
1085
|
+
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
1086
|
+
// context directly and run the same handler outside the hooks pipeline.
|
|
1087
|
+
const requestLike = toRequestLike(contextRequest);
|
|
918
1088
|
let ctx;
|
|
919
1089
|
try {
|
|
920
|
-
ctx = await server.createRequestContext(
|
|
1090
|
+
ctx = await server.createRequestContext(requestLike);
|
|
921
1091
|
}
|
|
922
1092
|
catch {
|
|
923
1093
|
return operationalErrorJson("WEBHOOK_CONTEXT_FAILED", "Webhook context failed.", 500, headers);
|
|
924
1094
|
}
|
|
925
|
-
return handleWebhookRequest({
|
|
1095
|
+
return handleWebhookRequest({
|
|
1096
|
+
req: requestLike,
|
|
1097
|
+
ctx,
|
|
1098
|
+
rawBody,
|
|
1099
|
+
});
|
|
926
1100
|
},
|
|
927
1101
|
};
|
|
928
1102
|
}
|
|
929
1103
|
/**
|
|
930
1104
|
* Create a Next.js POST handler for provider-verified payment webhooks.
|
|
931
1105
|
*
|
|
932
|
-
* The route
|
|
933
|
-
* body
|
|
1106
|
+
* The route checks the provider signature header and bounds the raw request
|
|
1107
|
+
* body before resolving the server, then builds app context from a rehydrated
|
|
1108
|
+
* request through
|
|
934
1109
|
* `server.createRequestContext(...)`, verifies the event through
|
|
935
1110
|
* `ctx.ports.payments`, and then delegates fulfillment to app code. This is the canonical helper for Beignet
|
|
936
1111
|
* billing flows backed by `PaymentsPort`. Keep this route on the Node.js
|
|
@@ -938,19 +1113,27 @@ export function createWebhookRoute(options) {
|
|
|
938
1113
|
*/
|
|
939
1114
|
export function createPaymentWebhookRoute(options) {
|
|
940
1115
|
const signatureHeader = options.signatureHeader ?? "stripe-signature";
|
|
941
|
-
const
|
|
1116
|
+
const maxBodyBytes = webhookBodyLimit(options.maxBodyBytes);
|
|
1117
|
+
const preflightBodies = new WeakMap();
|
|
1118
|
+
const handlePaymentWebhook = async ({ req, ctx, rawBody: providedRawBody, }) => {
|
|
942
1119
|
const nativeReq = nativeRequestOf(req);
|
|
943
1120
|
const headers = resolveHeaders(options.headers, nativeReq);
|
|
944
1121
|
const signature = new Headers(req.headers).get(signatureHeader);
|
|
945
1122
|
if (!signature) {
|
|
1123
|
+
cancelWebhookRequestBody(req, new Error(`Missing ${signatureHeader} header.`));
|
|
946
1124
|
return operationalErrorJson("PAYMENT_WEBHOOK_SIGNATURE_MISSING", `Missing ${signatureHeader} header.`, 400, headers);
|
|
947
1125
|
}
|
|
948
|
-
let rawBody;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1126
|
+
let rawBody = providedRawBody ?? preflightBodies.get(nativeReq);
|
|
1127
|
+
preflightBodies.delete(nativeReq);
|
|
1128
|
+
if (rawBody === undefined) {
|
|
1129
|
+
try {
|
|
1130
|
+
rawBody = await readWebhookBody(req, maxBodyBytes);
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
return error instanceof WebhookBodyTooLargeError
|
|
1134
|
+
? operationalErrorJson("PAYMENT_WEBHOOK_BODY_TOO_LARGE", "Payment webhook body exceeds the configured size limit.", 413, headers)
|
|
1135
|
+
: operationalErrorJson("PAYMENT_WEBHOOK_BODY_READ_FAILED", "Payment webhook body read failed.", 400, headers);
|
|
1136
|
+
}
|
|
954
1137
|
}
|
|
955
1138
|
let event;
|
|
956
1139
|
try {
|
|
@@ -1013,32 +1196,44 @@ export function createPaymentWebhookRoute(options) {
|
|
|
1013
1196
|
}, handlePaymentWebhook);
|
|
1014
1197
|
return {
|
|
1015
1198
|
POST: async (req) => {
|
|
1016
|
-
const server = await resolveServerSource(options.server);
|
|
1017
|
-
const pipelined = runPipeline(server);
|
|
1018
|
-
if (pipelined)
|
|
1019
|
-
return pipelined(req);
|
|
1020
|
-
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
1021
|
-
// context directly and run the same handler outside the hooks pipeline.
|
|
1022
|
-
// The signature gate runs first so no context is created without one.
|
|
1023
1199
|
const headers = resolveHeaders(options.headers, req);
|
|
1024
1200
|
if (!req.headers.get(signatureHeader)) {
|
|
1201
|
+
if (req.body) {
|
|
1202
|
+
cancelWebhookBody(req.body, new Error(`Missing ${signatureHeader} header.`));
|
|
1203
|
+
}
|
|
1025
1204
|
return operationalErrorJson("PAYMENT_WEBHOOK_SIGNATURE_MISSING", `Missing ${signatureHeader} header.`, 400, headers);
|
|
1026
1205
|
}
|
|
1027
1206
|
let rawBody;
|
|
1028
1207
|
try {
|
|
1029
|
-
rawBody = await req
|
|
1208
|
+
rawBody = await readWebhookBody(toRequestLike(req), maxBodyBytes);
|
|
1030
1209
|
}
|
|
1031
|
-
catch {
|
|
1032
|
-
return
|
|
1210
|
+
catch (error) {
|
|
1211
|
+
return error instanceof WebhookBodyTooLargeError
|
|
1212
|
+
? operationalErrorJson("PAYMENT_WEBHOOK_BODY_TOO_LARGE", "Payment webhook body exceeds the configured size limit.", 413, headers)
|
|
1213
|
+
: operationalErrorJson("PAYMENT_WEBHOOK_BODY_READ_FAILED", "Payment webhook body read failed.", 400, headers);
|
|
1033
1214
|
}
|
|
1215
|
+
const contextRequest = rehydrateWebhookRequest(req, rawBody);
|
|
1216
|
+
const server = await resolveServerSource(options.server);
|
|
1217
|
+
const pipelined = runPipeline(server);
|
|
1218
|
+
if (pipelined) {
|
|
1219
|
+
preflightBodies.set(contextRequest, rawBody);
|
|
1220
|
+
return pipelined(contextRequest);
|
|
1221
|
+
}
|
|
1222
|
+
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
1223
|
+
// context directly and run the same handler outside the hooks pipeline.
|
|
1224
|
+
const requestLike = toRequestLike(contextRequest);
|
|
1034
1225
|
let ctx;
|
|
1035
1226
|
try {
|
|
1036
|
-
ctx = await server.createRequestContext(
|
|
1227
|
+
ctx = await server.createRequestContext(requestLike);
|
|
1037
1228
|
}
|
|
1038
1229
|
catch {
|
|
1039
1230
|
return operationalErrorJson("PAYMENT_WEBHOOK_CONTEXT_FAILED", "Payment webhook context failed.", 500, headers);
|
|
1040
1231
|
}
|
|
1041
|
-
return handlePaymentWebhook({
|
|
1232
|
+
return handlePaymentWebhook({
|
|
1233
|
+
req: requestLike,
|
|
1234
|
+
ctx,
|
|
1235
|
+
rawBody,
|
|
1236
|
+
});
|
|
1042
1237
|
},
|
|
1043
1238
|
};
|
|
1044
1239
|
}
|