@beignet/next 0.0.27 → 0.0.29

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.js CHANGED
@@ -19,6 +19,42 @@ function isServerLoader(source) {
19
19
  async function resolveServerSource(source) {
20
20
  return isServerLoader(source) ? source() : source;
21
21
  }
22
+ function rawRoutePipeline(server) {
23
+ const candidate = server.rawRoute;
24
+ return typeof candidate === "function"
25
+ ? candidate.bind(server)
26
+ : undefined;
27
+ }
28
+ /**
29
+ * Per-route-factory memo for the pipeline-built handler. The handler is
30
+ * rebuilt only when the resolved server instance changes (fresh server per
31
+ * test, hot reload), never per request.
32
+ */
33
+ function createRawPipelineRunner(init, fn) {
34
+ let cached;
35
+ return (server) => {
36
+ const pipeline = rawRoutePipeline(server);
37
+ if (!pipeline)
38
+ return undefined;
39
+ if (!cached || cached.server !== server) {
40
+ const built = pipeline(init).handle(fn);
41
+ cached = {
42
+ server,
43
+ handler: async (req) => toWebResponse((await built(req))),
44
+ };
45
+ }
46
+ return cached.handler;
47
+ };
48
+ }
49
+ /**
50
+ * Resolve the native `Request` behind a pipeline request. Requests routed
51
+ * through `createFetchHandler` carry it as `raw`; requests passed straight
52
+ * into a core handler are the native request itself.
53
+ */
54
+ function nativeRequestOf(req) {
55
+ const raw = req.raw;
56
+ return (raw instanceof Request ? raw : req);
57
+ }
22
58
  /**
23
59
  * Create a memoized Next server loader for route modules.
24
60
  *
@@ -478,8 +514,9 @@ async function isAuthorizedCronRequest(req, secret) {
478
514
  * lifecycle hooks in serverless apps.
479
515
  */
480
516
  export function createOutboxDrainRoute(options) {
481
- const drainPendingOutbox = async (req) => {
482
- const headers = resolveHeaders(options.headers, req);
517
+ const drainWithContext = async ({ req, ctx, }) => {
518
+ const nativeReq = nativeRequestOf(req);
519
+ const headers = resolveHeaders(options.headers, nativeReq);
483
520
  const secret = options.secret;
484
521
  if (!secret) {
485
522
  return cronRouteJson({
@@ -487,13 +524,10 @@ export function createOutboxDrainRoute(options) {
487
524
  error: "CRON_SECRET is not configured.",
488
525
  }, 500, headers);
489
526
  }
490
- if (!(await isAuthorizedCronRequest(req, secret))) {
527
+ if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
491
528
  return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
492
529
  }
493
- let ctx;
494
530
  try {
495
- const server = await resolveServerSource(options.server);
496
- ctx = await server.createRequestContext(toRequestLike(req));
497
531
  const drainCtx = ctx;
498
532
  const result = await drainOutbox({
499
533
  outbox: drainCtx.ports.outbox,
@@ -541,13 +575,54 @@ export function createOutboxDrainRoute(options) {
541
575
  }, 200, headers);
542
576
  }
543
577
  catch (error) {
544
- ctx?.ports.logger?.error("Outbox drain failed", { error });
578
+ ctx.ports.logger?.error("Outbox drain failed", { error });
545
579
  return cronRouteJson({
546
580
  ok: false,
547
581
  error: "Outbox drain failed.",
548
582
  }, 500, headers);
549
583
  }
550
584
  };
585
+ const pipelineInit = {
586
+ name: options.pipeline?.name ?? "outbox.drain",
587
+ path: options.pipeline?.path ?? "/outbox/drain",
588
+ metadata: options.pipeline?.metadata,
589
+ };
590
+ const pipelineRunners = {
591
+ GET: createRawPipelineRunner({ ...pipelineInit, method: "GET" }, drainWithContext),
592
+ POST: createRawPipelineRunner({ ...pipelineInit, method: "POST" }, drainWithContext),
593
+ };
594
+ const drainPendingOutbox = async (req) => {
595
+ const server = await resolveServerSource(options.server);
596
+ const runner = pipelineRunners[req.method.toUpperCase() === "GET" ? "GET" : "POST"];
597
+ const pipelined = runner(server);
598
+ if (pipelined)
599
+ return pipelined(req);
600
+ // Minimal servers without rawRoute(...) — usually test fakes — keep the
601
+ // original flow: authorize before touching context, then drain inline.
602
+ const headers = resolveHeaders(options.headers, req);
603
+ const secret = options.secret;
604
+ if (!secret) {
605
+ return cronRouteJson({
606
+ ok: false,
607
+ error: "CRON_SECRET is not configured.",
608
+ }, 500, headers);
609
+ }
610
+ if (!(await isAuthorizedCronRequest(req, secret))) {
611
+ return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
612
+ }
613
+ let ctx;
614
+ try {
615
+ ctx = await server.createRequestContext(toRequestLike(req));
616
+ }
617
+ catch (error) {
618
+ console.error("Outbox drain context failed", { error });
619
+ return cronRouteJson({
620
+ ok: false,
621
+ error: "Outbox drain failed.",
622
+ }, 500, headers);
623
+ }
624
+ return drainWithContext({ req: toRequestLike(req), ctx });
625
+ };
551
626
  return {
552
627
  GET: drainPendingOutbox,
553
628
  POST: drainPendingOutbox,
@@ -563,10 +638,117 @@ export function createOutboxDrainRoute(options) {
563
638
  * payload schema, and delegates fulfillment to app code.
564
639
  */
565
640
  export function createWebhookRoute(options) {
641
+ const handleWebhookRequest = async ({ req, ctx, }) => {
642
+ const nativeReq = nativeRequestOf(req);
643
+ const headers = resolveHeaders(options.headers, nativeReq);
644
+ const requestHeaders = headersToRecord(new Headers(req.headers));
645
+ let rawBody;
646
+ try {
647
+ rawBody = await req.text();
648
+ }
649
+ catch {
650
+ return webhookRouteJson({
651
+ ok: false,
652
+ error: "Webhook body read failed.",
653
+ }, 400, headers);
654
+ }
655
+ let event;
656
+ try {
657
+ const input = {
658
+ rawBody,
659
+ headers: requestHeaders,
660
+ receivedAt: new Date(),
661
+ };
662
+ const routeVerifier = options.verify;
663
+ const verifier = routeVerifier
664
+ ? {
665
+ async verify() {
666
+ return routeVerifier({
667
+ req: nativeReq,
668
+ ctx,
669
+ rawBody,
670
+ headers: requestHeaders,
671
+ input,
672
+ });
673
+ },
674
+ }
675
+ : undefined;
676
+ event = (await verifyWebhook(options.webhook, input, {
677
+ verifier,
678
+ allowUnknownEvents: options.allowUnknownEvents,
679
+ }));
680
+ }
681
+ catch (error) {
682
+ ctx.ports.logger?.error("Webhook verification failed", {
683
+ error,
684
+ webhookName: options.webhook.name,
685
+ provider: options.webhook.provider,
686
+ requestId: ctx.requestId,
687
+ traceId: ctx.traceId,
688
+ });
689
+ return webhookRouteJson({
690
+ ok: false,
691
+ error: "Webhook verification failed.",
692
+ }, 400, headers);
693
+ }
694
+ try {
695
+ const result = await options.handle?.({
696
+ req: nativeReq,
697
+ ctx,
698
+ rawBody,
699
+ event,
700
+ });
701
+ if (result instanceof Response)
702
+ return result;
703
+ if (result) {
704
+ return toWebResponse({
705
+ status: result.status ?? 200,
706
+ headers: toHttpResponseHeaders(result.headers ?? headers),
707
+ body: result.body,
708
+ });
709
+ }
710
+ return toWebResponse({
711
+ status: 200,
712
+ headers: toHttpResponseHeaders(headers),
713
+ body: {
714
+ ok: true,
715
+ webhookName: options.webhook.name,
716
+ eventId: event.id,
717
+ eventType: event.type,
718
+ },
719
+ });
720
+ }
721
+ catch (error) {
722
+ ctx.ports.logger?.error("Webhook handler failed", {
723
+ error,
724
+ webhookName: options.webhook.name,
725
+ provider: options.webhook.provider,
726
+ requestId: ctx.requestId,
727
+ traceId: ctx.traceId,
728
+ eventId: event.id,
729
+ eventType: event.type,
730
+ });
731
+ return webhookRouteJson({
732
+ ok: false,
733
+ error: "Webhook handler failed.",
734
+ }, 500, headers);
735
+ }
736
+ };
737
+ const runPipeline = createRawPipelineRunner({
738
+ name: options.pipeline?.name ?? `webhook.${options.webhook.name}`,
739
+ method: "POST",
740
+ path: options.pipeline?.path ?? `/webhooks/${options.webhook.name}`,
741
+ metadata: options.pipeline?.metadata,
742
+ }, handleWebhookRequest);
566
743
  return {
567
744
  POST: async (req) => {
745
+ const server = await resolveServerSource(options.server);
746
+ const pipelined = runPipeline(server);
747
+ if (pipelined)
748
+ return pipelined(req);
749
+ // Minimal servers without rawRoute(...) — usually test fakes — build
750
+ // context directly and run the same handler outside the hooks pipeline.
568
751
  const headers = resolveHeaders(options.headers, req);
569
- const requestHeaders = headersToRecord(req.headers);
570
752
  let rawBody;
571
753
  try {
572
754
  rawBody = await req.text();
@@ -577,10 +759,10 @@ export function createWebhookRoute(options) {
577
759
  error: "Webhook body read failed.",
578
760
  }, 400, headers);
579
761
  }
762
+ const contextRequest = toContextRequest(req, rawBody);
580
763
  let ctx;
581
764
  try {
582
- const server = await resolveServerSource(options.server);
583
- ctx = await server.createRequestContext(toContextRequest(req, rawBody));
765
+ ctx = await server.createRequestContext(contextRequest);
584
766
  }
585
767
  catch {
586
768
  return webhookRouteJson({
@@ -588,87 +770,7 @@ export function createWebhookRoute(options) {
588
770
  error: "Webhook context failed.",
589
771
  }, 500, headers);
590
772
  }
591
- let event;
592
- try {
593
- const input = {
594
- rawBody,
595
- headers: requestHeaders,
596
- receivedAt: new Date(),
597
- };
598
- const routeVerifier = options.verify;
599
- const verifier = routeVerifier
600
- ? {
601
- async verify() {
602
- return routeVerifier({
603
- req,
604
- ctx,
605
- rawBody,
606
- headers: requestHeaders,
607
- input,
608
- });
609
- },
610
- }
611
- : undefined;
612
- event = (await verifyWebhook(options.webhook, input, {
613
- verifier,
614
- allowUnknownEvents: options.allowUnknownEvents,
615
- }));
616
- }
617
- catch (error) {
618
- ctx.ports.logger?.error("Webhook verification failed", {
619
- error,
620
- webhookName: options.webhook.name,
621
- provider: options.webhook.provider,
622
- requestId: ctx.requestId,
623
- traceId: ctx.traceId,
624
- });
625
- return webhookRouteJson({
626
- ok: false,
627
- error: "Webhook verification failed.",
628
- }, 400, headers);
629
- }
630
- try {
631
- const result = await options.handle?.({
632
- req,
633
- ctx,
634
- rawBody,
635
- event,
636
- });
637
- if (result instanceof Response)
638
- return result;
639
- if (result) {
640
- return toWebResponse({
641
- status: result.status ?? 200,
642
- headers: toHttpResponseHeaders(result.headers ?? headers),
643
- body: result.body,
644
- });
645
- }
646
- return toWebResponse({
647
- status: 200,
648
- headers: toHttpResponseHeaders(headers),
649
- body: {
650
- ok: true,
651
- webhookName: options.webhook.name,
652
- eventId: event.id,
653
- eventType: event.type,
654
- },
655
- });
656
- }
657
- catch (error) {
658
- ctx.ports.logger?.error("Webhook handler failed", {
659
- error,
660
- webhookName: options.webhook.name,
661
- provider: options.webhook.provider,
662
- requestId: ctx.requestId,
663
- traceId: ctx.traceId,
664
- eventId: event.id,
665
- eventType: event.type,
666
- });
667
- return webhookRouteJson({
668
- ok: false,
669
- error: "Webhook handler failed.",
670
- }, 500, headers);
671
- }
773
+ return handleWebhookRequest({ req: toContextRequest(req, rawBody), ctx });
672
774
  },
673
775
  };
674
776
  }
@@ -684,11 +786,102 @@ export function createWebhookRoute(options) {
684
786
  */
685
787
  export function createPaymentWebhookRoute(options) {
686
788
  const signatureHeader = options.signatureHeader ?? "stripe-signature";
789
+ const handlePaymentWebhook = async ({ req, ctx, }) => {
790
+ const nativeReq = nativeRequestOf(req);
791
+ const headers = resolveHeaders(options.headers, nativeReq);
792
+ const signature = new Headers(req.headers).get(signatureHeader);
793
+ if (!signature) {
794
+ return paymentWebhookJson({
795
+ ok: false,
796
+ error: `Missing ${signatureHeader} header.`,
797
+ }, 400, headers);
798
+ }
799
+ let rawBody;
800
+ try {
801
+ rawBody = await req.text();
802
+ }
803
+ catch {
804
+ return paymentWebhookJson({
805
+ ok: false,
806
+ error: "Payment webhook body read failed.",
807
+ }, 400, headers);
808
+ }
809
+ let event;
810
+ try {
811
+ event = await ctx.ports.payments.verifyWebhook({
812
+ rawBody,
813
+ signature,
814
+ });
815
+ }
816
+ catch (error) {
817
+ ctx.ports.logger?.error("Payment webhook verification failed", {
818
+ error,
819
+ requestId: ctx.requestId,
820
+ traceId: ctx.traceId,
821
+ });
822
+ return paymentWebhookJson({
823
+ ok: false,
824
+ error: "Payment webhook verification failed.",
825
+ }, 400, headers);
826
+ }
827
+ try {
828
+ const result = await options.handle?.({
829
+ req: nativeReq,
830
+ ctx,
831
+ rawBody,
832
+ signature,
833
+ event,
834
+ });
835
+ if (result instanceof Response)
836
+ return result;
837
+ if (result) {
838
+ return toWebResponse({
839
+ status: result.status ?? 200,
840
+ headers: toHttpResponseHeaders(result.headers ?? headers),
841
+ body: result.body,
842
+ });
843
+ }
844
+ return toWebResponse({
845
+ status: 200,
846
+ headers: toHttpResponseHeaders(headers),
847
+ body: {
848
+ ok: true,
849
+ eventId: event.id,
850
+ eventType: event.type,
851
+ },
852
+ });
853
+ }
854
+ catch (error) {
855
+ ctx.ports.logger?.error("Payment webhook handler failed", {
856
+ error,
857
+ requestId: ctx.requestId,
858
+ traceId: ctx.traceId,
859
+ eventId: event.id,
860
+ eventType: event.type,
861
+ });
862
+ return paymentWebhookJson({
863
+ ok: false,
864
+ error: "Payment webhook handler failed.",
865
+ }, 500, headers);
866
+ }
867
+ };
868
+ const runPipeline = createRawPipelineRunner({
869
+ name: options.pipeline?.name ?? "webhook.payments",
870
+ method: "POST",
871
+ path: options.pipeline?.path ?? "/webhooks/payments",
872
+ metadata: options.pipeline?.metadata,
873
+ }, handlePaymentWebhook);
687
874
  return {
688
875
  POST: async (req) => {
876
+ const server = await resolveServerSource(options.server);
877
+ const pipelined = runPipeline(server);
878
+ if (pipelined)
879
+ return pipelined(req);
880
+ // Minimal servers without rawRoute(...) — usually test fakes — build
881
+ // context directly and run the same handler outside the hooks pipeline.
882
+ // The signature gate runs first so no context is created without one.
689
883
  const headers = resolveHeaders(options.headers, req);
690
- const signature = req.headers.get(signatureHeader);
691
- if (!signature) {
884
+ if (!req.headers.get(signatureHeader)) {
692
885
  return paymentWebhookJson({
693
886
  ok: false,
694
887
  error: `Missing ${signatureHeader} header.`,
@@ -706,7 +899,6 @@ export function createPaymentWebhookRoute(options) {
706
899
  }
707
900
  let ctx;
708
901
  try {
709
- const server = await resolveServerSource(options.server);
710
902
  ctx = await server.createRequestContext(toContextRequest(req, rawBody));
711
903
  }
712
904
  catch {
@@ -715,64 +907,7 @@ export function createPaymentWebhookRoute(options) {
715
907
  error: "Payment webhook context failed.",
716
908
  }, 500, headers);
717
909
  }
718
- let event;
719
- try {
720
- event = await ctx.ports.payments.verifyWebhook({
721
- rawBody,
722
- signature,
723
- });
724
- }
725
- catch (error) {
726
- ctx.ports.logger?.error("Payment webhook verification failed", {
727
- error,
728
- requestId: ctx.requestId,
729
- traceId: ctx.traceId,
730
- });
731
- return paymentWebhookJson({
732
- ok: false,
733
- error: "Payment webhook verification failed.",
734
- }, 400, headers);
735
- }
736
- try {
737
- const result = await options.handle?.({
738
- req,
739
- ctx,
740
- rawBody,
741
- signature,
742
- event,
743
- });
744
- if (result instanceof Response)
745
- return result;
746
- if (result) {
747
- return toWebResponse({
748
- status: result.status ?? 200,
749
- headers: toHttpResponseHeaders(result.headers ?? headers),
750
- body: result.body,
751
- });
752
- }
753
- return toWebResponse({
754
- status: 200,
755
- headers: toHttpResponseHeaders(headers),
756
- body: {
757
- ok: true,
758
- eventId: event.id,
759
- eventType: event.type,
760
- },
761
- });
762
- }
763
- catch (error) {
764
- ctx.ports.logger?.error("Payment webhook handler failed", {
765
- error,
766
- requestId: ctx.requestId,
767
- traceId: ctx.traceId,
768
- eventId: event.id,
769
- eventType: event.type,
770
- });
771
- return paymentWebhookJson({
772
- ok: false,
773
- error: "Payment webhook handler failed.",
774
- }, 500, headers);
775
- }
910
+ return handlePaymentWebhook({ req: toContextRequest(req, rawBody), ctx });
776
911
  },
777
912
  };
778
913
  }
@@ -796,8 +931,9 @@ export function createScheduleRoute(options) {
796
931
  ? `Unknown schedule "${options.schedule}". Available schedules: ${available.join(", ")}.`
797
932
  : `Unknown schedule "${options.schedule}". No schedules were passed to createScheduleRoute(...).`);
798
933
  }
799
- const runScheduleFromRequest = async (req) => {
800
- const headers = resolveHeaders(options.headers, req);
934
+ const runSchedule = async ({ req, ctx, }) => {
935
+ const nativeReq = nativeRequestOf(req);
936
+ const headers = resolveHeaders(options.headers, nativeReq);
801
937
  const secret = options.secret;
802
938
  if (!secret) {
803
939
  return cronRouteJson({
@@ -806,12 +942,10 @@ export function createScheduleRoute(options) {
806
942
  scheduleName: schedule.name,
807
943
  }, 500, headers);
808
944
  }
809
- if (!(await isAuthorizedCronRequest(req, secret))) {
945
+ if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
810
946
  return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
811
947
  }
812
948
  try {
813
- const server = await resolveServerSource(options.server);
814
- const ctx = await server.createRequestContext(toRequestLike(req));
815
949
  const runner = createInlineScheduleRunner({
816
950
  ctx,
817
951
  instrumentation: resolveProviderInstrumentationPort(ctx.ports),
@@ -842,6 +976,48 @@ export function createScheduleRoute(options) {
842
976
  }, 500, headers);
843
977
  }
844
978
  };
979
+ const pipelineInit = {
980
+ name: options.pipeline?.name ?? `schedule.${schedule.name}`,
981
+ path: options.pipeline?.path ?? `/schedules/${schedule.name}`,
982
+ metadata: options.pipeline?.metadata,
983
+ };
984
+ const pipelineRunners = {
985
+ GET: createRawPipelineRunner({ ...pipelineInit, method: "GET" }, runSchedule),
986
+ POST: createRawPipelineRunner({ ...pipelineInit, method: "POST" }, runSchedule),
987
+ };
988
+ const runScheduleFromRequest = async (req) => {
989
+ const server = await resolveServerSource(options.server);
990
+ const runner = pipelineRunners[req.method.toUpperCase() === "GET" ? "GET" : "POST"];
991
+ const pipelined = runner(server);
992
+ if (pipelined)
993
+ return pipelined(req);
994
+ // Minimal servers without rawRoute(...) — usually test fakes — keep the
995
+ // original flow: authorize before touching context, then run inline.
996
+ const headers = resolveHeaders(options.headers, req);
997
+ const secret = options.secret;
998
+ if (!secret) {
999
+ return cronRouteJson({
1000
+ ok: false,
1001
+ error: "CRON_SECRET is not configured.",
1002
+ scheduleName: schedule.name,
1003
+ }, 500, headers);
1004
+ }
1005
+ if (!(await isAuthorizedCronRequest(req, secret))) {
1006
+ return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
1007
+ }
1008
+ let ctx;
1009
+ try {
1010
+ ctx = await server.createRequestContext(toRequestLike(req));
1011
+ }
1012
+ catch {
1013
+ return cronRouteJson({
1014
+ ok: false,
1015
+ error: "Schedule failed.",
1016
+ scheduleName: schedule.name,
1017
+ }, 500, headers);
1018
+ }
1019
+ return runSchedule({ req: toRequestLike(req), ctx });
1020
+ };
845
1021
  return {
846
1022
  GET: runScheduleFromRequest,
847
1023
  POST: runScheduleFromRequest,
@@ -861,6 +1037,9 @@ export async function createNextServer(options) {
861
1037
  route: (contract) => ({
862
1038
  handle: (fn) => createFetchHandler(runtime.route(contract).handle(fn)),
863
1039
  }),
1040
+ rawRoute: (init) => ({
1041
+ handle: (fn) => createFetchHandler(runtime.rawRoute(init).handle(fn)),
1042
+ }),
864
1043
  createContextFromNext: async () => {
865
1044
  // "next" ships no package exports map, so Node-style ESM resolution
866
1045
  // (NodeNext) needs the explicit .js subpath. Bundlers resolve it the same.