@beignet/next 0.0.26 → 0.0.28

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/src/index.ts CHANGED
@@ -19,16 +19,20 @@ import {
19
19
  type StandardSchema,
20
20
  } from "@beignet/core/schedules";
21
21
  import type {
22
+ AppEnvironment,
22
23
  ContractLike,
23
24
  CreateServerOptions,
24
25
  Handler,
26
+ HealthConfig,
27
+ HttpContractConfig,
25
28
  HttpRequestLike,
29
+ RawRouteInit,
26
30
  ResolveContract,
27
31
  RouteDef,
28
32
  ServerInstance,
29
33
  StandardSchemaV1,
30
34
  } from "@beignet/core/server";
31
- import { createServer } from "@beignet/core/server";
35
+ import { createHealthHandler, createServer } from "@beignet/core/server";
32
36
  import type { UploadRouter } from "@beignet/core/uploads";
33
37
  import {
34
38
  type InferWebhookEvent,
@@ -38,7 +42,7 @@ import {
38
42
  type WebhookEvent,
39
43
  type WebhookEventSchemas,
40
44
  } from "@beignet/core/webhooks";
41
- import { createFetchHandler, toWebResponse } from "@beignet/web";
45
+ import { createFetchHandler, toRequestLike, toWebResponse } from "@beignet/web";
42
46
 
43
47
  /**
44
48
  * Server types re-exported for Next.js apps.
@@ -47,6 +51,7 @@ export type {
47
51
  AnyUseCaseLike,
48
52
  CreateServerOptions,
49
53
  HandlerRouteDef,
54
+ RawRouteInit,
50
55
  RouteDef,
51
56
  RouteGroup,
52
57
  RouteHook,
@@ -81,6 +86,96 @@ type AnyProvider = ServiceProvider<
81
86
  any
82
87
  >;
83
88
 
89
+ type MaybePromise<T> = T | Promise<T>;
90
+
91
+ type ServerSource<TServer> = TServer | (() => MaybePromise<TServer>);
92
+
93
+ function isServerLoader<TServer>(
94
+ source: ServerSource<TServer>,
95
+ ): source is () => MaybePromise<TServer> {
96
+ return typeof source === "function";
97
+ }
98
+
99
+ async function resolveServerSource<TServer>(
100
+ source: ServerSource<TServer>,
101
+ ): Promise<TServer> {
102
+ return isServerLoader(source) ? source() : source;
103
+ }
104
+
105
+ /**
106
+ * Unified view over `rawRoute` as exposed by a core `ServerInstance`
107
+ * (framework-neutral request/response) and a `NextServer` (fetch-native).
108
+ * Both accept a native `Request` structurally and both produce something
109
+ * `toWebResponse(...)` normalizes, so route factories can run their handler
110
+ * through either pipeline without knowing which flavor they were given.
111
+ */
112
+ type RawRoutePipeline<Ctx> = (init: RawRouteInit) => {
113
+ handle: (
114
+ fn: (args: {
115
+ req: HttpRequestLike;
116
+ ctx: Ctx;
117
+ }) => MaybePromise<Response | { status: number }>,
118
+ ) => (req: HttpRequestLike) => Promise<Response | object>;
119
+ };
120
+
121
+ function rawRoutePipeline<Ctx>(
122
+ server: NextRouteServer<Ctx>,
123
+ ): RawRoutePipeline<Ctx> | undefined {
124
+ const candidate = (server as { rawRoute?: unknown }).rawRoute;
125
+ return typeof candidate === "function"
126
+ ? (candidate.bind(server) as RawRoutePipeline<Ctx>)
127
+ : undefined;
128
+ }
129
+
130
+ /**
131
+ * Per-route-factory memo for the pipeline-built handler. The handler is
132
+ * rebuilt only when the resolved server instance changes (fresh server per
133
+ * test, hot reload), never per request.
134
+ */
135
+ function createRawPipelineRunner<Ctx>(
136
+ init: RawRouteInit,
137
+ fn: (args: { req: HttpRequestLike; ctx: Ctx }) => Promise<Response>,
138
+ ): (
139
+ server: NextRouteServer<Ctx>,
140
+ ) => ((req: Request) => Promise<Response>) | undefined {
141
+ let cached:
142
+ | {
143
+ server: NextRouteServer<Ctx>;
144
+ handler: (req: Request) => Promise<Response>;
145
+ }
146
+ | undefined;
147
+
148
+ return (server) => {
149
+ const pipeline = rawRoutePipeline(server);
150
+ if (!pipeline) return undefined;
151
+
152
+ if (!cached || cached.server !== server) {
153
+ const built = pipeline(init).handle(fn);
154
+ cached = {
155
+ server,
156
+ handler: async (req) =>
157
+ toWebResponse(
158
+ (await built(req as unknown as HttpRequestLike)) as Parameters<
159
+ typeof toWebResponse
160
+ >[0],
161
+ ),
162
+ };
163
+ }
164
+
165
+ return cached.handler;
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Resolve the native `Request` behind a pipeline request. Requests routed
171
+ * through `createFetchHandler` carry it as `raw`; requests passed straight
172
+ * into a core handler are the native request itself.
173
+ */
174
+ function nativeRequestOf(req: HttpRequestLike): Request {
175
+ const raw = (req as { raw?: unknown }).raw;
176
+ return (raw instanceof Request ? raw : req) as Request;
177
+ }
178
+
84
179
  /**
85
180
  * Beignet client config with Next.js base URL defaults.
86
181
  */
@@ -267,14 +362,35 @@ type OutboxDrainContext = {
267
362
  ports: OutboxDrainPorts;
268
363
  };
269
364
 
365
+ /**
366
+ * Minimal server surface required by the Next.js route factories.
367
+ *
368
+ * Both a `NextServer` and a core `ServerInstance` satisfy this shape
369
+ * structurally, so route modules built on these factories are executable
370
+ * under `bun test`: construct the route with a fake server exposing
371
+ * `createRequestContext` and invoke the exported handlers with a plain
372
+ * `Request`. No Next.js runtime (`next/headers`) is involved.
373
+ *
374
+ * When the server also exposes `rawRoute(...)` — real Beignet servers do —
375
+ * the factories run their handler through the full hooks pipeline (rate
376
+ * limiting, CORS, logging, error reporting, instrumentation). Minimal fakes
377
+ * without it keep the direct `createRequestContext` flow.
378
+ */
379
+ export type NextRouteServer<Ctx> = {
380
+ createRequestContext: (req: HttpRequestLike) => Promise<Ctx>;
381
+ };
382
+
270
383
  /**
271
384
  * Options for creating a serverless-safe outbox drain route.
272
385
  */
273
386
  export type CreateOutboxDrainRouteOptions<Ctx extends OutboxDrainContext> = {
274
387
  /**
275
- * Beignet Next server that owns app context and ports.
388
+ * Beignet server that owns app context and ports.
389
+ *
390
+ * Any object exposing `createRequestContext` works: a `NextServer`, a core
391
+ * `ServerInstance`, or a test fake driven from `bun test`.
276
392
  */
277
- server: Pick<NextServer<Ctx, Ctx["ports"]>, "createContextFromNext">;
393
+ server: ServerSource<NextRouteServer<Ctx>>;
278
394
  /**
279
395
  * Registry of events and jobs that may be delivered from the outbox.
280
396
  */
@@ -298,6 +414,17 @@ export type CreateOutboxDrainRouteOptions<Ctx extends OutboxDrainContext> = {
298
414
  * Retry delay override passed through to `drainOutbox(...)`.
299
415
  */
300
416
  retryDelayMs?: DrainOutboxOptions["retryDelayMs"];
417
+ /**
418
+ * Identity and metadata for the hooks pipeline when the server exposes
419
+ * `rawRoute(...)`. `metadata` feeds metadata-driven hooks such as rate
420
+ * limiting and idempotency; `name` and `path` identify the route to
421
+ * hooks, instrumentation, and devtools.
422
+ */
423
+ pipeline?: {
424
+ name?: string;
425
+ path?: string;
426
+ metadata?: RawRouteInit["metadata"];
427
+ };
301
428
  /**
302
429
  * Additional response headers.
303
430
  */
@@ -405,9 +532,12 @@ export type CreateWebhookRouteOptions<
405
532
  AllowUnknownEvents extends boolean = true,
406
533
  > = {
407
534
  /**
408
- * Beignet Next server that owns app context and ports.
535
+ * Beignet server that owns app context and ports.
536
+ *
537
+ * Any object exposing `createRequestContext` works: a `NextServer`, a core
538
+ * `ServerInstance`, or a test fake driven from `bun test`.
409
539
  */
410
- server: Pick<NextServer<Ctx, Ctx["ports"]>, "createContextFromNext">;
540
+ server: ServerSource<NextRouteServer<Ctx>>;
411
541
  /**
412
542
  * Inbound webhook definition.
413
543
  */
@@ -433,6 +563,17 @@ export type CreateWebhookRouteOptions<
433
563
  handle?: (
434
564
  args: WebhookRouteHandlerArgs<Ctx, Webhook, AllowUnknownEvents>,
435
565
  ) => Promise<WebhookRouteHandlerResult> | WebhookRouteHandlerResult;
566
+ /**
567
+ * Identity and metadata for the hooks pipeline when the server exposes
568
+ * `rawRoute(...)`. `metadata` feeds metadata-driven hooks such as rate
569
+ * limiting and idempotency; `name` and `path` identify the route to
570
+ * hooks, instrumentation, and devtools.
571
+ */
572
+ pipeline?: {
573
+ name?: string;
574
+ path?: string;
575
+ metadata?: RawRouteInit["metadata"];
576
+ };
436
577
  /**
437
578
  * Additional response headers.
438
579
  */
@@ -444,9 +585,12 @@ export type CreateWebhookRouteOptions<
444
585
  */
445
586
  export type CreateScheduleRouteOptions<Ctx extends ScheduleRouteContext> = {
446
587
  /**
447
- * Beignet Next server that owns app context and ports.
588
+ * Beignet server that owns app context and ports.
589
+ *
590
+ * Any object exposing `createRequestContext` works: a `NextServer`, a core
591
+ * `ServerInstance`, or a test fake driven from `bun test`.
448
592
  */
449
- server: Pick<NextServer<Ctx, Ctx["ports"]>, "createContextFromNext">;
593
+ server: ServerSource<NextRouteServer<Ctx>>;
450
594
  /**
451
595
  * Registered schedules, usually the `schedules` array from
452
596
  * `server/schedules.ts`.
@@ -470,6 +614,17 @@ export type CreateScheduleRouteOptions<Ctx extends ScheduleRouteContext> = {
470
614
  * @default "next-cron-route"
471
615
  */
472
616
  source?: string;
617
+ /**
618
+ * Identity and metadata for the hooks pipeline when the server exposes
619
+ * `rawRoute(...)`. `metadata` feeds metadata-driven hooks such as rate
620
+ * limiting and idempotency; `name` and `path` identify the route to
621
+ * hooks, instrumentation, and devtools.
622
+ */
623
+ pipeline?: {
624
+ name?: string;
625
+ path?: string;
626
+ metadata?: RawRouteInit["metadata"];
627
+ };
473
628
  /**
474
629
  * Additional response headers.
475
630
  */
@@ -483,9 +638,12 @@ export type CreatePaymentWebhookRouteOptions<
483
638
  Ctx extends PaymentWebhookRouteContext,
484
639
  > = {
485
640
  /**
486
- * Beignet Next server that owns app context and ports.
641
+ * Beignet server that owns app context and ports.
642
+ *
643
+ * Any object exposing `createRequestContext` works: a `NextServer`, a core
644
+ * `ServerInstance`, or a test fake driven from `bun test`.
487
645
  */
488
- server: Pick<NextServer<Ctx, Ctx["ports"]>, "createContextFromNext">;
646
+ server: ServerSource<NextRouteServer<Ctx>>;
489
647
  /**
490
648
  * Provider signature header. Stripe uses `stripe-signature`.
491
649
  *
@@ -502,6 +660,17 @@ export type CreatePaymentWebhookRouteOptions<
502
660
  ) =>
503
661
  | Promise<PaymentWebhookRouteHandlerResult>
504
662
  | PaymentWebhookRouteHandlerResult;
663
+ /**
664
+ * Identity and metadata for the hooks pipeline when the server exposes
665
+ * `rawRoute(...)`. `metadata` feeds metadata-driven hooks such as rate
666
+ * limiting and idempotency; `name` and `path` identify the route to
667
+ * hooks, instrumentation, and devtools.
668
+ */
669
+ pipeline?: {
670
+ name?: string;
671
+ path?: string;
672
+ metadata?: RawRouteInit["metadata"];
673
+ };
505
674
  /**
506
675
  * Additional response headers.
507
676
  */
@@ -536,6 +705,24 @@ export interface NextServer<
536
705
  fn: Handler<Ctx, ResolveContract<CLike>>,
537
706
  ) => (req: Request) => Promise<Response>;
538
707
  };
708
+ /**
709
+ * Build a Next.js route handler for a route that cannot be a contract —
710
+ * webhooks, third-party auth callbacks, streaming endpoints — that still
711
+ * runs the whole server pipeline: hooks (rate limiting, idempotency, CORS,
712
+ * logging, error reporting), context creation, instrumentation, and
713
+ * framework error mapping.
714
+ *
715
+ * Request parsing and validation are skipped and the request body is left
716
+ * unconsumed, so the handler owns body reading — for example webhook
717
+ * signature verification over the exact raw bytes. Metadata-driven hooks
718
+ * read `init.metadata`. Raw routes are not added to the route registry;
719
+ * export the returned handler from the route file that owns the path.
720
+ */
721
+ rawRoute: (init: RawRouteInit) => {
722
+ handle: (
723
+ fn: Handler<Ctx, HttpContractConfig>,
724
+ ) => (req: Request) => Promise<Response>;
725
+ };
539
726
  /**
540
727
  * Create app context from `next/headers` for Server Components.
541
728
  */
@@ -559,6 +746,18 @@ export interface NextServer<
559
746
  Ports,
560
747
  ServiceInput
561
748
  >["createServiceContext"];
749
+ /**
750
+ * Build a service context and run `fn` inside a scoped ambient correlation
751
+ * frame, returning its result.
752
+ *
753
+ * This is the script-safe counterpart to `createServiceContext(...)`.
754
+ * Requires `context.service` to be declared in `createNextServer(...)`.
755
+ */
756
+ runServiceContext: ServerInstance<
757
+ Ctx,
758
+ Ports,
759
+ ServiceInput
760
+ >["runServiceContext"];
562
761
  /**
563
762
  * Registered contract inputs.
564
763
  */
@@ -573,6 +772,108 @@ export interface NextServer<
573
772
  ports: Ports;
574
773
  }
575
774
 
775
+ /**
776
+ * Lazy, memoized loader for a Beignet Next server.
777
+ */
778
+ export type NextServerLoader<TServer> = () => Promise<TServer>;
779
+
780
+ /**
781
+ * HTTP method handlers for a central Beignet API catch-all route.
782
+ */
783
+ export type NextApiRouteHandlers = {
784
+ DELETE: (req: Request) => Promise<Response>;
785
+ GET: (req: Request) => Promise<Response>;
786
+ HEAD: (req: Request) => Promise<Response>;
787
+ OPTIONS: (req: Request) => Promise<Response>;
788
+ PATCH: (req: Request) => Promise<Response>;
789
+ POST: (req: Request) => Promise<Response>;
790
+ PUT: (req: Request) => Promise<Response>;
791
+ };
792
+
793
+ /**
794
+ * Create a memoized Next server loader for route modules.
795
+ *
796
+ * Next.js imports route modules during production builds. Exporting a lazy
797
+ * loader lets those modules stay cheap to import while still booting providers
798
+ * before the first real request in each runtime process.
799
+ */
800
+ export function createNextServerLoader<TServer>(
801
+ createServer: () => MaybePromise<TServer>,
802
+ ): NextServerLoader<TServer> {
803
+ let server: Promise<TServer> | undefined;
804
+
805
+ return () => {
806
+ if (!server) {
807
+ const nextServer = Promise.resolve().then(createServer);
808
+ server = nextServer;
809
+ void nextServer.catch(() => {
810
+ if (server === nextServer) {
811
+ server = undefined;
812
+ }
813
+ });
814
+ }
815
+
816
+ return server;
817
+ };
818
+ }
819
+
820
+ /**
821
+ * Create HTTP method exports for a catch-all API route.
822
+ */
823
+ export function createApiRoute<
824
+ TServer extends Pick<NextServer<unknown, AnyPorts, unknown>, "api">,
825
+ >(loadServer: NextServerLoader<TServer>): NextApiRouteHandlers {
826
+ const handle = async (req: Request) => {
827
+ const server = await loadServer();
828
+ return server.api(req);
829
+ };
830
+
831
+ return {
832
+ DELETE: handle,
833
+ GET: handle,
834
+ HEAD: handle,
835
+ OPTIONS: handle,
836
+ PATCH: handle,
837
+ POST: handle,
838
+ PUT: handle,
839
+ };
840
+ }
841
+
842
+ /**
843
+ * Create a readiness route whose server is loaded only when the route handles
844
+ * a request.
845
+ */
846
+ export function createHealthRoute<Ports extends AnyPorts>(
847
+ loadServer: NextServerLoader<
848
+ Pick<NextServer<unknown, Ports, unknown>, "ports">
849
+ >,
850
+ healthConfig: HealthConfig<Ports> | undefined,
851
+ environment: AppEnvironment = "production",
852
+ ): {
853
+ GET: (req: Request) => Promise<Response>;
854
+ } {
855
+ return {
856
+ GET: async (req) => {
857
+ const server = await loadServer();
858
+ const handler = createHealthHandler(
859
+ server.ports,
860
+ healthConfig,
861
+ environment,
862
+ );
863
+ const response = await handler(req);
864
+ const headers = new Headers(response.headers);
865
+ if (!headers.has("cache-control")) {
866
+ headers.set("cache-control", "no-store");
867
+ }
868
+
869
+ return Response.json(response.body, {
870
+ status: response.status,
871
+ headers,
872
+ });
873
+ },
874
+ };
875
+ }
876
+
576
877
  /**
577
878
  * Convert a Beignet response into a native `Response`.
578
879
  *
@@ -936,7 +1237,7 @@ export function createSwaggerUIHandler(
936
1237
  * all return 404 so storage routes do not leak object existence.
937
1238
  */
938
1239
  export function createStorageRoute(
939
- storage: StoragePort,
1240
+ storage: ServerSource<StoragePort>,
940
1241
  options: CreateStorageRouteOptions = {},
941
1242
  ): {
942
1243
  GET: (req: Request) => Promise<Response>;
@@ -945,8 +1246,22 @@ export function createStorageRoute(
945
1246
  const basePath = normalizeStorageBasePath(options.basePath);
946
1247
 
947
1248
  return {
948
- GET: (req) => storageResponse(storage, req, basePath, options, true),
949
- HEAD: (req) => storageResponse(storage, req, basePath, options, false),
1249
+ GET: async (req) =>
1250
+ storageResponse(
1251
+ await resolveServerSource(storage),
1252
+ req,
1253
+ basePath,
1254
+ options,
1255
+ true,
1256
+ ),
1257
+ HEAD: async (req) =>
1258
+ storageResponse(
1259
+ await resolveServerSource(storage),
1260
+ req,
1261
+ basePath,
1262
+ options,
1263
+ false,
1264
+ ),
950
1265
  };
951
1266
  }
952
1267
 
@@ -958,7 +1273,7 @@ export function createStorageRoute(
958
1273
  * `prepare`, `complete`, or `upload`.
959
1274
  */
960
1275
  export function createUploadRoute(
961
- router: UploadRouter,
1276
+ router: ServerSource<UploadRouter>,
962
1277
  options: CreateUploadRouteOptions = {},
963
1278
  ): {
964
1279
  POST: (req: Request, ctx?: UploadRouteContext) => Promise<Response>;
@@ -999,7 +1314,9 @@ export function createUploadRoute(
999
1314
  );
1000
1315
  }
1001
1316
 
1002
- return router.handleRequest(req, {
1317
+ const resolvedRouter = await resolveServerSource(router);
1318
+
1319
+ return resolvedRouter.handleRequest(req, {
1003
1320
  uploadName,
1004
1321
  action,
1005
1322
  });
@@ -1052,6 +1369,24 @@ function toHttpResponseHeaders(
1052
1369
  return result;
1053
1370
  }
1054
1371
 
1372
+ /**
1373
+ * Rebuild a request whose body was already consumed.
1374
+ *
1375
+ * Webhook routes read the raw body before anything else so signature
1376
+ * verification never races the context factory over one body stream. Context
1377
+ * creation then receives a rehydrated request carrying the same method, URL,
1378
+ * headers, and raw body.
1379
+ */
1380
+ function toContextRequest(req: Request, rawBody: string): HttpRequestLike {
1381
+ return toRequestLike(
1382
+ new Request(req.url, {
1383
+ method: req.method,
1384
+ headers: req.headers,
1385
+ body: rawBody,
1386
+ }),
1387
+ );
1388
+ }
1389
+
1055
1390
  function headersToRecord(headers: Headers): Record<string, string> {
1056
1391
  const result: Record<string, string> = {};
1057
1392
  headers.forEach((value, key) => {
@@ -1106,8 +1441,15 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
1106
1441
  GET: (req: Request) => Promise<Response>;
1107
1442
  POST: (req: Request) => Promise<Response>;
1108
1443
  } {
1109
- const drainPendingOutbox = async (req: Request): Promise<Response> => {
1110
- const headers = resolveHeaders(options.headers, req);
1444
+ const drainWithContext = async ({
1445
+ req,
1446
+ ctx,
1447
+ }: {
1448
+ req: HttpRequestLike;
1449
+ ctx: Ctx;
1450
+ }): Promise<Response> => {
1451
+ const nativeReq = nativeRequestOf(req);
1452
+ const headers = resolveHeaders(options.headers, nativeReq);
1111
1453
  const secret = options.secret;
1112
1454
 
1113
1455
  if (!secret) {
@@ -1121,14 +1463,11 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
1121
1463
  );
1122
1464
  }
1123
1465
 
1124
- if (!(await isAuthorizedCronRequest(req, secret))) {
1466
+ if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
1125
1467
  return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
1126
1468
  }
1127
1469
 
1128
- let ctx: Ctx | undefined;
1129
-
1130
1470
  try {
1131
- ctx = await options.server.createContextFromNext();
1132
1471
  const drainCtx = ctx;
1133
1472
  const result = await drainOutbox({
1134
1473
  outbox: drainCtx.ports.outbox,
@@ -1183,7 +1522,7 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
1183
1522
  headers,
1184
1523
  );
1185
1524
  } catch (error) {
1186
- ctx?.ports.logger?.error("Outbox drain failed", { error });
1525
+ ctx.ports.logger?.error("Outbox drain failed", { error });
1187
1526
 
1188
1527
  return cronRouteJson(
1189
1528
  {
@@ -1196,6 +1535,69 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
1196
1535
  }
1197
1536
  };
1198
1537
 
1538
+ const pipelineInit = {
1539
+ name: options.pipeline?.name ?? "outbox.drain",
1540
+ path: options.pipeline?.path ?? "/outbox/drain",
1541
+ metadata: options.pipeline?.metadata,
1542
+ };
1543
+ const pipelineRunners = {
1544
+ GET: createRawPipelineRunner<Ctx>(
1545
+ { ...pipelineInit, method: "GET" },
1546
+ drainWithContext,
1547
+ ),
1548
+ POST: createRawPipelineRunner<Ctx>(
1549
+ { ...pipelineInit, method: "POST" },
1550
+ drainWithContext,
1551
+ ),
1552
+ };
1553
+
1554
+ const drainPendingOutbox = async (req: Request): Promise<Response> => {
1555
+ const server = await resolveServerSource(options.server);
1556
+
1557
+ const runner =
1558
+ pipelineRunners[req.method.toUpperCase() === "GET" ? "GET" : "POST"];
1559
+ const pipelined = runner(server);
1560
+ if (pipelined) return pipelined(req);
1561
+
1562
+ // Minimal servers without rawRoute(...) — usually test fakes — keep the
1563
+ // original flow: authorize before touching context, then drain inline.
1564
+ const headers = resolveHeaders(options.headers, req);
1565
+ const secret = options.secret;
1566
+
1567
+ if (!secret) {
1568
+ return cronRouteJson(
1569
+ {
1570
+ ok: false,
1571
+ error: "CRON_SECRET is not configured.",
1572
+ },
1573
+ 500,
1574
+ headers,
1575
+ );
1576
+ }
1577
+
1578
+ if (!(await isAuthorizedCronRequest(req, secret))) {
1579
+ return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
1580
+ }
1581
+
1582
+ let ctx: Ctx;
1583
+ try {
1584
+ ctx = await server.createRequestContext(toRequestLike(req));
1585
+ } catch (error) {
1586
+ console.error("Outbox drain context failed", { error });
1587
+
1588
+ return cronRouteJson(
1589
+ {
1590
+ ok: false,
1591
+ error: "Outbox drain failed.",
1592
+ },
1593
+ 500,
1594
+ headers,
1595
+ );
1596
+ }
1597
+
1598
+ return drainWithContext({ req: toRequestLike(req), ctx });
1599
+ };
1600
+
1199
1601
  return {
1200
1602
  GET: drainPendingOutbox,
1201
1603
  POST: drainPendingOutbox,
@@ -1206,9 +1608,10 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
1206
1608
  * Create a Next.js POST handler for generic verified inbound webhooks.
1207
1609
  *
1208
1610
  * The route reads the raw request body as text, normalizes request headers,
1209
- * verifies the event through the webhook definition or a context-aware
1210
- * verifier, validates the matching event payload schema, and delegates
1211
- * fulfillment to app code.
1611
+ * builds app context from the real request through
1612
+ * `server.createRequestContext(...)`, verifies the event through the webhook
1613
+ * definition or a context-aware verifier, validates the matching event
1614
+ * payload schema, and delegates fulfillment to app code.
1212
1615
  */
1213
1616
  export function createWebhookRoute<
1214
1617
  Ctx extends WebhookRouteContext,
@@ -1219,130 +1622,184 @@ export function createWebhookRoute<
1219
1622
  ): {
1220
1623
  POST: (req: Request) => Promise<Response>;
1221
1624
  } {
1625
+ const handleWebhookRequest = async ({
1626
+ req,
1627
+ ctx,
1628
+ }: {
1629
+ req: HttpRequestLike;
1630
+ ctx: Ctx;
1631
+ }): Promise<Response> => {
1632
+ const nativeReq = nativeRequestOf(req);
1633
+ const headers = resolveHeaders(options.headers, nativeReq);
1634
+ const requestHeaders = headersToRecord(new Headers(req.headers));
1635
+
1636
+ let rawBody: string;
1637
+
1638
+ try {
1639
+ rawBody = await req.text();
1640
+ } catch {
1641
+ return webhookRouteJson(
1642
+ {
1643
+ ok: false,
1644
+ error: "Webhook body read failed.",
1645
+ },
1646
+ 400,
1647
+ headers,
1648
+ );
1649
+ }
1650
+
1651
+ let event: WebhookRouteHandlerArgs<
1652
+ Ctx,
1653
+ Webhook,
1654
+ AllowUnknownEvents
1655
+ >["event"];
1656
+
1657
+ try {
1658
+ const input: VerifyWebhookInput = {
1659
+ rawBody,
1660
+ headers: requestHeaders,
1661
+ receivedAt: new Date(),
1662
+ };
1663
+ const routeVerifier = options.verify;
1664
+ const verifier = routeVerifier
1665
+ ? {
1666
+ async verify() {
1667
+ return routeVerifier({
1668
+ req: nativeReq,
1669
+ ctx,
1670
+ rawBody,
1671
+ headers: requestHeaders,
1672
+ input,
1673
+ });
1674
+ },
1675
+ }
1676
+ : undefined;
1677
+
1678
+ event = (await verifyWebhook(options.webhook, input, {
1679
+ verifier,
1680
+ allowUnknownEvents: options.allowUnknownEvents,
1681
+ })) as WebhookRouteHandlerArgs<Ctx, Webhook, AllowUnknownEvents>["event"];
1682
+ } catch (error) {
1683
+ ctx.ports.logger?.error("Webhook verification failed", {
1684
+ error,
1685
+ webhookName: options.webhook.name,
1686
+ provider: options.webhook.provider,
1687
+ requestId: ctx.requestId,
1688
+ traceId: ctx.traceId,
1689
+ });
1690
+
1691
+ return webhookRouteJson(
1692
+ {
1693
+ ok: false,
1694
+ error: "Webhook verification failed.",
1695
+ },
1696
+ 400,
1697
+ headers,
1698
+ );
1699
+ }
1700
+
1701
+ try {
1702
+ const result = await options.handle?.({
1703
+ req: nativeReq,
1704
+ ctx,
1705
+ rawBody,
1706
+ event,
1707
+ });
1708
+
1709
+ if (result instanceof Response) return result;
1710
+
1711
+ if (result) {
1712
+ return toWebResponse({
1713
+ status: result.status ?? 200,
1714
+ headers: toHttpResponseHeaders(result.headers ?? headers),
1715
+ body: result.body,
1716
+ });
1717
+ }
1718
+
1719
+ return toWebResponse({
1720
+ status: 200,
1721
+ headers: toHttpResponseHeaders(headers),
1722
+ body: {
1723
+ ok: true,
1724
+ webhookName: options.webhook.name,
1725
+ eventId: event.id,
1726
+ eventType: event.type,
1727
+ },
1728
+ });
1729
+ } catch (error) {
1730
+ ctx.ports.logger?.error("Webhook handler failed", {
1731
+ error,
1732
+ webhookName: options.webhook.name,
1733
+ provider: options.webhook.provider,
1734
+ requestId: ctx.requestId,
1735
+ traceId: ctx.traceId,
1736
+ eventId: event.id,
1737
+ eventType: event.type,
1738
+ });
1739
+
1740
+ return webhookRouteJson(
1741
+ {
1742
+ ok: false,
1743
+ error: "Webhook handler failed.",
1744
+ },
1745
+ 500,
1746
+ headers,
1747
+ );
1748
+ }
1749
+ };
1750
+
1751
+ const runPipeline = createRawPipelineRunner<Ctx>(
1752
+ {
1753
+ name: options.pipeline?.name ?? `webhook.${options.webhook.name}`,
1754
+ method: "POST",
1755
+ path: options.pipeline?.path ?? `/webhooks/${options.webhook.name}`,
1756
+ metadata: options.pipeline?.metadata,
1757
+ },
1758
+ handleWebhookRequest,
1759
+ );
1760
+
1222
1761
  return {
1223
1762
  POST: async (req) => {
1224
- const headers = resolveHeaders(options.headers, req);
1225
- const requestHeaders = headersToRecord(req.headers);
1763
+ const server = await resolveServerSource(options.server);
1226
1764
 
1227
- let ctx: Ctx;
1765
+ const pipelined = runPipeline(server);
1766
+ if (pipelined) return pipelined(req);
1228
1767
 
1229
- try {
1230
- ctx = await options.server.createContextFromNext();
1231
- } catch {
1232
- return webhookRouteJson(
1233
- {
1234
- ok: false,
1235
- error: "Webhook context failed.",
1236
- },
1237
- 500,
1238
- headers,
1239
- );
1240
- }
1768
+ // Minimal servers without rawRoute(...) — usually test fakes — build
1769
+ // context directly and run the same handler outside the hooks pipeline.
1770
+ const headers = resolveHeaders(options.headers, req);
1241
1771
 
1242
1772
  let rawBody: string;
1243
- let event: WebhookRouteHandlerArgs<
1244
- Ctx,
1245
- Webhook,
1246
- AllowUnknownEvents
1247
- >["event"];
1248
-
1249
1773
  try {
1250
1774
  rawBody = await req.text();
1251
- const input: VerifyWebhookInput = {
1252
- rawBody,
1253
- headers: requestHeaders,
1254
- receivedAt: new Date(),
1255
- };
1256
- const routeVerifier = options.verify;
1257
- const verifier = routeVerifier
1258
- ? {
1259
- async verify() {
1260
- return routeVerifier({
1261
- req,
1262
- ctx,
1263
- rawBody,
1264
- headers: requestHeaders,
1265
- input,
1266
- });
1267
- },
1268
- }
1269
- : undefined;
1270
-
1271
- event = (await verifyWebhook(options.webhook, input, {
1272
- verifier,
1273
- allowUnknownEvents: options.allowUnknownEvents,
1274
- })) as WebhookRouteHandlerArgs<
1275
- Ctx,
1276
- Webhook,
1277
- AllowUnknownEvents
1278
- >["event"];
1279
- } catch (error) {
1280
- ctx.ports.logger?.error("Webhook verification failed", {
1281
- error,
1282
- webhookName: options.webhook.name,
1283
- provider: options.webhook.provider,
1284
- requestId: ctx.requestId,
1285
- traceId: ctx.traceId,
1286
- });
1287
-
1775
+ } catch {
1288
1776
  return webhookRouteJson(
1289
1777
  {
1290
1778
  ok: false,
1291
- error: "Webhook verification failed.",
1779
+ error: "Webhook body read failed.",
1292
1780
  },
1293
1781
  400,
1294
1782
  headers,
1295
1783
  );
1296
1784
  }
1297
1785
 
1298
- try {
1299
- const result = await options.handle?.({
1300
- req,
1301
- ctx,
1302
- rawBody,
1303
- event,
1304
- });
1305
-
1306
- if (result instanceof Response) return result;
1307
-
1308
- if (result) {
1309
- return toWebResponse({
1310
- status: result.status ?? 200,
1311
- headers: toHttpResponseHeaders(result.headers ?? headers),
1312
- body: result.body,
1313
- });
1314
- }
1315
-
1316
- return toWebResponse({
1317
- status: 200,
1318
- headers: toHttpResponseHeaders(headers),
1319
- body: {
1320
- ok: true,
1321
- webhookName: options.webhook.name,
1322
- eventId: event.id,
1323
- eventType: event.type,
1324
- },
1325
- });
1326
- } catch (error) {
1327
- ctx.ports.logger?.error("Webhook handler failed", {
1328
- error,
1329
- webhookName: options.webhook.name,
1330
- provider: options.webhook.provider,
1331
- requestId: ctx.requestId,
1332
- traceId: ctx.traceId,
1333
- eventId: event.id,
1334
- eventType: event.type,
1335
- });
1786
+ const contextRequest = toContextRequest(req, rawBody);
1336
1787
 
1788
+ let ctx: Ctx;
1789
+ try {
1790
+ ctx = await server.createRequestContext(contextRequest);
1791
+ } catch {
1337
1792
  return webhookRouteJson(
1338
1793
  {
1339
1794
  ok: false,
1340
- error: "Webhook handler failed.",
1795
+ error: "Webhook context failed.",
1341
1796
  },
1342
1797
  500,
1343
1798
  headers,
1344
1799
  );
1345
1800
  }
1801
+
1802
+ return handleWebhookRequest({ req: toContextRequest(req, rawBody), ctx });
1346
1803
  },
1347
1804
  };
1348
1805
  }
@@ -1350,9 +1807,10 @@ export function createWebhookRoute<
1350
1807
  /**
1351
1808
  * Create a Next.js POST handler for provider-verified payment webhooks.
1352
1809
  *
1353
- * The route reads the raw request body as text, extracts the provider
1354
- * signature header, verifies the event through `ctx.ports.payments`, and then
1355
- * delegates fulfillment to app code. This is the canonical helper for Beignet
1810
+ * The route extracts the provider signature header, reads the raw request
1811
+ * body as text, builds app context from the real request through
1812
+ * `server.createRequestContext(...)`, verifies the event through
1813
+ * `ctx.ports.payments`, and then delegates fulfillment to app code. This is the canonical helper for Beignet
1356
1814
  * billing flows backed by `PaymentsPort`. Keep this route on the Node.js
1357
1815
  * runtime when your provider SDK requires Node APIs.
1358
1816
  */
@@ -1365,12 +1823,138 @@ export function createPaymentWebhookRoute<
1365
1823
  } {
1366
1824
  const signatureHeader = options.signatureHeader ?? "stripe-signature";
1367
1825
 
1826
+ const handlePaymentWebhook = async ({
1827
+ req,
1828
+ ctx,
1829
+ }: {
1830
+ req: HttpRequestLike;
1831
+ ctx: Ctx;
1832
+ }): Promise<Response> => {
1833
+ const nativeReq = nativeRequestOf(req);
1834
+ const headers = resolveHeaders(options.headers, nativeReq);
1835
+ const signature = new Headers(req.headers).get(signatureHeader);
1836
+
1837
+ if (!signature) {
1838
+ return paymentWebhookJson(
1839
+ {
1840
+ ok: false,
1841
+ error: `Missing ${signatureHeader} header.`,
1842
+ },
1843
+ 400,
1844
+ headers,
1845
+ );
1846
+ }
1847
+
1848
+ let rawBody: string;
1849
+
1850
+ try {
1851
+ rawBody = await req.text();
1852
+ } catch {
1853
+ return paymentWebhookJson(
1854
+ {
1855
+ ok: false,
1856
+ error: "Payment webhook body read failed.",
1857
+ },
1858
+ 400,
1859
+ headers,
1860
+ );
1861
+ }
1862
+
1863
+ let event: PaymentWebhookEvent;
1864
+
1865
+ try {
1866
+ event = await ctx.ports.payments.verifyWebhook({
1867
+ rawBody,
1868
+ signature,
1869
+ });
1870
+ } catch (error) {
1871
+ ctx.ports.logger?.error("Payment webhook verification failed", {
1872
+ error,
1873
+ requestId: ctx.requestId,
1874
+ traceId: ctx.traceId,
1875
+ });
1876
+
1877
+ return paymentWebhookJson(
1878
+ {
1879
+ ok: false,
1880
+ error: "Payment webhook verification failed.",
1881
+ },
1882
+ 400,
1883
+ headers,
1884
+ );
1885
+ }
1886
+
1887
+ try {
1888
+ const result = await options.handle?.({
1889
+ req: nativeReq,
1890
+ ctx,
1891
+ rawBody,
1892
+ signature,
1893
+ event,
1894
+ });
1895
+
1896
+ if (result instanceof Response) return result;
1897
+
1898
+ if (result) {
1899
+ return toWebResponse({
1900
+ status: result.status ?? 200,
1901
+ headers: toHttpResponseHeaders(result.headers ?? headers),
1902
+ body: result.body,
1903
+ });
1904
+ }
1905
+
1906
+ return toWebResponse({
1907
+ status: 200,
1908
+ headers: toHttpResponseHeaders(headers),
1909
+ body: {
1910
+ ok: true,
1911
+ eventId: event.id,
1912
+ eventType: event.type,
1913
+ },
1914
+ });
1915
+ } catch (error) {
1916
+ ctx.ports.logger?.error("Payment webhook handler failed", {
1917
+ error,
1918
+ requestId: ctx.requestId,
1919
+ traceId: ctx.traceId,
1920
+ eventId: event.id,
1921
+ eventType: event.type,
1922
+ });
1923
+
1924
+ return paymentWebhookJson(
1925
+ {
1926
+ ok: false,
1927
+ error: "Payment webhook handler failed.",
1928
+ },
1929
+ 500,
1930
+ headers,
1931
+ );
1932
+ }
1933
+ };
1934
+
1935
+ const runPipeline = createRawPipelineRunner<Ctx>(
1936
+ {
1937
+ name: options.pipeline?.name ?? "webhook.payments",
1938
+ method: "POST",
1939
+ path: options.pipeline?.path ?? "/webhooks/payments",
1940
+ metadata: options.pipeline?.metadata,
1941
+ },
1942
+ handlePaymentWebhook,
1943
+ );
1944
+
1368
1945
  return {
1369
1946
  POST: async (req) => {
1947
+ const server = await resolveServerSource(options.server);
1948
+
1949
+ const pipelined = runPipeline(server);
1950
+ if (pipelined) return pipelined(req);
1951
+
1952
+ // Minimal servers without rawRoute(...) — usually test fakes — build
1953
+ // context directly and run the same handler outside the hooks pipeline.
1954
+ // The signature gate runs first so no context is created without one.
1370
1955
  const headers = resolveHeaders(options.headers, req);
1371
- const signature = req.headers.get(signatureHeader);
1372
1956
 
1373
- if (!signature) {
1957
+ if (!req.headers.get(signatureHeader)) {
1374
1958
  return paymentWebhookJson(
1375
1959
  {
1376
1960
  ok: false,
@@ -1381,93 +1965,35 @@ export function createPaymentWebhookRoute<
1381
1965
  );
1382
1966
  }
1383
1967
 
1384
- let ctx: Ctx;
1385
-
1386
- try {
1387
- ctx = await options.server.createContextFromNext();
1388
- } catch {
1389
- return paymentWebhookJson(
1390
- {
1391
- ok: false,
1392
- error: "Payment webhook context failed.",
1393
- },
1394
- 500,
1395
- headers,
1396
- );
1397
- }
1398
-
1399
1968
  let rawBody: string;
1400
- let event: PaymentWebhookEvent;
1401
-
1402
1969
  try {
1403
1970
  rawBody = await req.text();
1404
- event = await ctx.ports.payments.verifyWebhook({
1405
- rawBody,
1406
- signature,
1407
- });
1408
- } catch (error) {
1409
- ctx.ports.logger?.error("Payment webhook verification failed", {
1410
- error,
1411
- requestId: ctx.requestId,
1412
- traceId: ctx.traceId,
1413
- });
1414
-
1971
+ } catch {
1415
1972
  return paymentWebhookJson(
1416
1973
  {
1417
1974
  ok: false,
1418
- error: "Payment webhook verification failed.",
1975
+ error: "Payment webhook body read failed.",
1419
1976
  },
1420
1977
  400,
1421
1978
  headers,
1422
1979
  );
1423
1980
  }
1424
1981
 
1982
+ let ctx: Ctx;
1425
1983
  try {
1426
- const result = await options.handle?.({
1427
- req,
1428
- ctx,
1429
- rawBody,
1430
- signature,
1431
- event,
1432
- });
1433
-
1434
- if (result instanceof Response) return result;
1435
-
1436
- if (result) {
1437
- return toWebResponse({
1438
- status: result.status ?? 200,
1439
- headers: toHttpResponseHeaders(result.headers ?? headers),
1440
- body: result.body,
1441
- });
1442
- }
1443
-
1444
- return toWebResponse({
1445
- status: 200,
1446
- headers: toHttpResponseHeaders(headers),
1447
- body: {
1448
- ok: true,
1449
- eventId: event.id,
1450
- eventType: event.type,
1451
- },
1452
- });
1453
- } catch (error) {
1454
- ctx.ports.logger?.error("Payment webhook handler failed", {
1455
- error,
1456
- requestId: ctx.requestId,
1457
- traceId: ctx.traceId,
1458
- eventId: event.id,
1459
- eventType: event.type,
1460
- });
1461
-
1984
+ ctx = await server.createRequestContext(toContextRequest(req, rawBody));
1985
+ } catch {
1462
1986
  return paymentWebhookJson(
1463
1987
  {
1464
1988
  ok: false,
1465
- error: "Payment webhook handler failed.",
1989
+ error: "Payment webhook context failed.",
1466
1990
  },
1467
1991
  500,
1468
1992
  headers,
1469
1993
  );
1470
1994
  }
1995
+
1996
+ return handlePaymentWebhook({ req: toContextRequest(req, rawBody), ctx });
1471
1997
  },
1472
1998
  };
1473
1999
  }
@@ -1476,8 +2002,9 @@ export function createPaymentWebhookRoute<
1476
2002
  * Create Next.js route handlers that trigger one registered schedule.
1477
2003
  *
1478
2004
  * This helper is intended for serverless cron routes. It authenticates the
1479
- * caller with a timing-safe bearer comparison, creates app context, runs the
1480
- * schedule inline, and records `schedule` events through the resolved
2005
+ * caller with a timing-safe bearer comparison, builds app context from the
2006
+ * real request through `server.createRequestContext(...)`, runs the schedule
2007
+ * inline, and records `schedule` events through the resolved
1481
2008
  * provider instrumentation port (`ports.instrumentation`, then
1482
2009
  * `ports.devtools`) when one is installed.
1483
2010
  */
@@ -1503,8 +2030,15 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
1503
2030
  );
1504
2031
  }
1505
2032
 
1506
- const runScheduleFromRequest = async (req: Request): Promise<Response> => {
1507
- const headers = resolveHeaders(options.headers, req);
2033
+ const runSchedule = async ({
2034
+ req,
2035
+ ctx,
2036
+ }: {
2037
+ req: HttpRequestLike;
2038
+ ctx: Ctx;
2039
+ }): Promise<Response> => {
2040
+ const nativeReq = nativeRequestOf(req);
2041
+ const headers = resolveHeaders(options.headers, nativeReq);
1508
2042
  const secret = options.secret;
1509
2043
 
1510
2044
  if (!secret) {
@@ -1519,12 +2053,11 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
1519
2053
  );
1520
2054
  }
1521
2055
 
1522
- if (!(await isAuthorizedCronRequest(req, secret))) {
2056
+ if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
1523
2057
  return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
1524
2058
  }
1525
2059
 
1526
2060
  try {
1527
- const ctx = await options.server.createContextFromNext();
1528
2061
  const runner = createInlineScheduleRunner<Ctx>({
1529
2062
  ctx,
1530
2063
  instrumentation: resolveProviderInstrumentationPort(ctx.ports),
@@ -1565,6 +2098,69 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
1565
2098
  }
1566
2099
  };
1567
2100
 
2101
+ const pipelineInit = {
2102
+ name: options.pipeline?.name ?? `schedule.${schedule.name}`,
2103
+ path: options.pipeline?.path ?? `/schedules/${schedule.name}`,
2104
+ metadata: options.pipeline?.metadata,
2105
+ };
2106
+ const pipelineRunners = {
2107
+ GET: createRawPipelineRunner<Ctx>(
2108
+ { ...pipelineInit, method: "GET" },
2109
+ runSchedule,
2110
+ ),
2111
+ POST: createRawPipelineRunner<Ctx>(
2112
+ { ...pipelineInit, method: "POST" },
2113
+ runSchedule,
2114
+ ),
2115
+ };
2116
+
2117
+ const runScheduleFromRequest = async (req: Request): Promise<Response> => {
2118
+ const server = await resolveServerSource(options.server);
2119
+
2120
+ const runner =
2121
+ pipelineRunners[req.method.toUpperCase() === "GET" ? "GET" : "POST"];
2122
+ const pipelined = runner(server);
2123
+ if (pipelined) return pipelined(req);
2124
+
2125
+ // Minimal servers without rawRoute(...) — usually test fakes — keep the
2126
+ // original flow: authorize before touching context, then run inline.
2127
+ const headers = resolveHeaders(options.headers, req);
2128
+ const secret = options.secret;
2129
+
2130
+ if (!secret) {
2131
+ return cronRouteJson(
2132
+ {
2133
+ ok: false,
2134
+ error: "CRON_SECRET is not configured.",
2135
+ scheduleName: schedule.name,
2136
+ },
2137
+ 500,
2138
+ headers,
2139
+ );
2140
+ }
2141
+
2142
+ if (!(await isAuthorizedCronRequest(req, secret))) {
2143
+ return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
2144
+ }
2145
+
2146
+ let ctx: Ctx;
2147
+ try {
2148
+ ctx = await server.createRequestContext(toRequestLike(req));
2149
+ } catch {
2150
+ return cronRouteJson(
2151
+ {
2152
+ ok: false,
2153
+ error: "Schedule failed.",
2154
+ scheduleName: schedule.name,
2155
+ },
2156
+ 500,
2157
+ headers,
2158
+ );
2159
+ }
2160
+
2161
+ return runSchedule({ req: toRequestLike(req), ctx });
2162
+ };
2163
+
1568
2164
  return {
1569
2165
  GET: runScheduleFromRequest,
1570
2166
  POST: runScheduleFromRequest,
@@ -1596,6 +2192,9 @@ export async function createNextServer<
1596
2192
  route: (contract) => ({
1597
2193
  handle: (fn) => createFetchHandler(runtime.route(contract).handle(fn)),
1598
2194
  }),
2195
+ rawRoute: (init) => ({
2196
+ handle: (fn) => createFetchHandler(runtime.rawRoute(init).handle(fn)),
2197
+ }),
1599
2198
  createContextFromNext: async () => {
1600
2199
  // "next" ships no package exports map, so Node-style ESM resolution
1601
2200
  // (NodeNext) needs the explicit .js subpath. Bundlers resolve it the same.
@@ -1641,6 +2240,7 @@ export async function createNextServer<
1641
2240
  },
1642
2241
  createRequestContext: runtime.createRequestContext,
1643
2242
  createServiceContext: runtime.createServiceContext,
2243
+ runServiceContext: runtime.runServiceContext,
1644
2244
  contracts: runtime.contracts,
1645
2245
  stop: () => runtime.stop(),
1646
2246
  ports: runtime.ports,