@beignet/next 0.0.27 → 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/CHANGELOG.md +9 -0
- package/README.md +59 -7
- package/package.json +2 -2
- package/src/index.ts +570 -195
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @beignet/next
|
|
2
2
|
|
|
3
|
+
## 0.0.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 57504c5: Add `server.rawRoute(...)`: build handlers for routes that cannot be contracts — third-party callbacks, signature-verified webhooks, streaming endpoints — that still run the full pipeline (hooks, context creation, instrumentation, framework error mapping) with the request body left unconsumed. The Next.js webhook, payment webhook, schedule, and outbox drain route factories now run through this pipeline automatically when the server exposes `rawRoute`, with a new `pipeline` option for route identity and hook metadata.
|
|
8
|
+
- Updated dependencies [f21e8af]
|
|
9
|
+
- Updated dependencies [57504c5]
|
|
10
|
+
- @beignet/web@0.0.28
|
|
11
|
+
|
|
3
12
|
## 0.0.27
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -316,6 +316,52 @@ export async function GET(req: Request) {
|
|
|
316
316
|
}
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
+
#### `server.rawRoute(init)`
|
|
320
|
+
|
|
321
|
+
Builds a handler for a route that cannot be a contract — third-party callback
|
|
322
|
+
endpoints with externally defined request shapes, signature-verified
|
|
323
|
+
webhooks, streaming endpoints — that still runs the whole server pipeline:
|
|
324
|
+
correlation, hooks (rate limiting, idempotency, CORS, logging, error
|
|
325
|
+
reporting), context creation, instrumentation, and framework error mapping.
|
|
326
|
+
Request parsing and validation are skipped and the request body is left
|
|
327
|
+
unconsumed, so the handler owns body reading.
|
|
328
|
+
|
|
329
|
+
`init.name`, `init.method`, and `init.path` identify the route to hooks,
|
|
330
|
+
instrumentation, and devtools — routing itself belongs to the route file
|
|
331
|
+
that mounts the handler. `init.metadata` feeds metadata-driven hooks exactly
|
|
332
|
+
like contract metadata.
|
|
333
|
+
|
|
334
|
+
**Returns:** Builder with `handle(fn)` returning
|
|
335
|
+
`(req: Request) => Promise<Response>`.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// app/api/liveblocks-auth/route.ts
|
|
339
|
+
import { getServer } from "@/server";
|
|
340
|
+
|
|
341
|
+
let roomAuth: ((req: Request) => Promise<Response>) | undefined;
|
|
342
|
+
|
|
343
|
+
export async function POST(req: Request) {
|
|
344
|
+
roomAuth ??= (await getServer())
|
|
345
|
+
.rawRoute({
|
|
346
|
+
name: "collab.roomAuth",
|
|
347
|
+
method: "POST",
|
|
348
|
+
path: "/api/liveblocks-auth",
|
|
349
|
+
metadata: { rateLimit: { max: 300, windowSec: 60, scope: "user" } },
|
|
350
|
+
})
|
|
351
|
+
.handle(async ({ req, ctx }) => {
|
|
352
|
+
const body = await req.text();
|
|
353
|
+
return { status: 200, body: { token: "..." } };
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return roomAuth(req);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The webhook, payment webhook, schedule, and outbox drain route factories run
|
|
361
|
+
through this pipeline automatically when the `server` they receive exposes
|
|
362
|
+
`rawRoute(...)`; their `pipeline` option supplies the route identity and
|
|
363
|
+
metadata.
|
|
364
|
+
|
|
319
365
|
#### `server.createContextFromNext()`
|
|
320
366
|
|
|
321
367
|
Creates a context object from Next.js Server Components by automatically extracting headers and cookies. This allows you to call use cases directly from React Server Components without going through API routes.
|
|
@@ -556,13 +602,19 @@ The `action` segment must be `prepare`, `upload`, or `complete`.
|
|
|
556
602
|
|
|
557
603
|
Use `createPaymentWebhookRoute(...)` for billing flows backed by
|
|
558
604
|
`ctx.ports.payments`. Use `createWebhookRoute(...)` for generic inbound
|
|
559
|
-
webhooks backed by a `defineWebhook(...)` catalog and a provider verifier.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
605
|
+
webhooks backed by a `defineWebhook(...)` catalog and a provider verifier.
|
|
606
|
+
The `server` option accepts any object exposing `createRequestContext` — a
|
|
607
|
+
`NextServer`, a core `ServerInstance`, or a test fake — so
|
|
608
|
+
`server: getServer` keeps working unchanged.
|
|
609
|
+
|
|
610
|
+
When the server also exposes `rawRoute(...)` — real Beignet servers do — the
|
|
611
|
+
route runs inside the full hooks pipeline: rate limiting, CORS, logging,
|
|
612
|
+
error reporting, and instrumentation apply, the request appears in devtools,
|
|
613
|
+
and the `pipeline` option supplies the route identity and metadata for
|
|
614
|
+
metadata-driven hooks. The raw body stays unconsumed until the route reads
|
|
615
|
+
it, so signature verification still sees the exact bytes. Minimal test fakes
|
|
616
|
+
without `rawRoute` keep the direct flow: raw body first, then app context
|
|
617
|
+
through `server.createRequestContext(...)`.
|
|
566
618
|
|
|
567
619
|
```typescript
|
|
568
620
|
// app/api/webhooks/github/route.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Next.js server-side handlers for Beignet",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@beignet/web": "^0.0.
|
|
62
|
+
"@beignet/web": "^0.0.28",
|
|
63
63
|
"@standard-schema/spec": "^1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,9 @@ import type {
|
|
|
24
24
|
CreateServerOptions,
|
|
25
25
|
Handler,
|
|
26
26
|
HealthConfig,
|
|
27
|
+
HttpContractConfig,
|
|
27
28
|
HttpRequestLike,
|
|
29
|
+
RawRouteInit,
|
|
28
30
|
ResolveContract,
|
|
29
31
|
RouteDef,
|
|
30
32
|
ServerInstance,
|
|
@@ -49,6 +51,7 @@ export type {
|
|
|
49
51
|
AnyUseCaseLike,
|
|
50
52
|
CreateServerOptions,
|
|
51
53
|
HandlerRouteDef,
|
|
54
|
+
RawRouteInit,
|
|
52
55
|
RouteDef,
|
|
53
56
|
RouteGroup,
|
|
54
57
|
RouteHook,
|
|
@@ -99,6 +102,80 @@ async function resolveServerSource<TServer>(
|
|
|
99
102
|
return isServerLoader(source) ? source() : source;
|
|
100
103
|
}
|
|
101
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
|
+
|
|
102
179
|
/**
|
|
103
180
|
* Beignet client config with Next.js base URL defaults.
|
|
104
181
|
*/
|
|
@@ -293,6 +370,11 @@ type OutboxDrainContext = {
|
|
|
293
370
|
* under `bun test`: construct the route with a fake server exposing
|
|
294
371
|
* `createRequestContext` and invoke the exported handlers with a plain
|
|
295
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.
|
|
296
378
|
*/
|
|
297
379
|
export type NextRouteServer<Ctx> = {
|
|
298
380
|
createRequestContext: (req: HttpRequestLike) => Promise<Ctx>;
|
|
@@ -332,6 +414,17 @@ export type CreateOutboxDrainRouteOptions<Ctx extends OutboxDrainContext> = {
|
|
|
332
414
|
* Retry delay override passed through to `drainOutbox(...)`.
|
|
333
415
|
*/
|
|
334
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
|
+
};
|
|
335
428
|
/**
|
|
336
429
|
* Additional response headers.
|
|
337
430
|
*/
|
|
@@ -470,6 +563,17 @@ export type CreateWebhookRouteOptions<
|
|
|
470
563
|
handle?: (
|
|
471
564
|
args: WebhookRouteHandlerArgs<Ctx, Webhook, AllowUnknownEvents>,
|
|
472
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
|
+
};
|
|
473
577
|
/**
|
|
474
578
|
* Additional response headers.
|
|
475
579
|
*/
|
|
@@ -510,6 +614,17 @@ export type CreateScheduleRouteOptions<Ctx extends ScheduleRouteContext> = {
|
|
|
510
614
|
* @default "next-cron-route"
|
|
511
615
|
*/
|
|
512
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
|
+
};
|
|
513
628
|
/**
|
|
514
629
|
* Additional response headers.
|
|
515
630
|
*/
|
|
@@ -545,6 +660,17 @@ export type CreatePaymentWebhookRouteOptions<
|
|
|
545
660
|
) =>
|
|
546
661
|
| Promise<PaymentWebhookRouteHandlerResult>
|
|
547
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
|
+
};
|
|
548
674
|
/**
|
|
549
675
|
* Additional response headers.
|
|
550
676
|
*/
|
|
@@ -579,6 +705,24 @@ export interface NextServer<
|
|
|
579
705
|
fn: Handler<Ctx, ResolveContract<CLike>>,
|
|
580
706
|
) => (req: Request) => Promise<Response>;
|
|
581
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
|
+
};
|
|
582
726
|
/**
|
|
583
727
|
* Create app context from `next/headers` for Server Components.
|
|
584
728
|
*/
|
|
@@ -1297,8 +1441,15 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
|
|
|
1297
1441
|
GET: (req: Request) => Promise<Response>;
|
|
1298
1442
|
POST: (req: Request) => Promise<Response>;
|
|
1299
1443
|
} {
|
|
1300
|
-
const
|
|
1301
|
-
|
|
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);
|
|
1302
1453
|
const secret = options.secret;
|
|
1303
1454
|
|
|
1304
1455
|
if (!secret) {
|
|
@@ -1312,15 +1463,11 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
|
|
|
1312
1463
|
);
|
|
1313
1464
|
}
|
|
1314
1465
|
|
|
1315
|
-
if (!(await isAuthorizedCronRequest(
|
|
1466
|
+
if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
|
|
1316
1467
|
return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
|
|
1317
1468
|
}
|
|
1318
1469
|
|
|
1319
|
-
let ctx: Ctx | undefined;
|
|
1320
|
-
|
|
1321
1470
|
try {
|
|
1322
|
-
const server = await resolveServerSource(options.server);
|
|
1323
|
-
ctx = await server.createRequestContext(toRequestLike(req));
|
|
1324
1471
|
const drainCtx = ctx;
|
|
1325
1472
|
const result = await drainOutbox({
|
|
1326
1473
|
outbox: drainCtx.ports.outbox,
|
|
@@ -1375,7 +1522,7 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
|
|
|
1375
1522
|
headers,
|
|
1376
1523
|
);
|
|
1377
1524
|
} catch (error) {
|
|
1378
|
-
ctx
|
|
1525
|
+
ctx.ports.logger?.error("Outbox drain failed", { error });
|
|
1379
1526
|
|
|
1380
1527
|
return cronRouteJson(
|
|
1381
1528
|
{
|
|
@@ -1388,6 +1535,69 @@ export function createOutboxDrainRoute<Ctx extends OutboxDrainContext>(
|
|
|
1388
1535
|
}
|
|
1389
1536
|
};
|
|
1390
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
|
+
|
|
1391
1601
|
return {
|
|
1392
1602
|
GET: drainPendingOutbox,
|
|
1393
1603
|
POST: drainPendingOutbox,
|
|
@@ -1412,13 +1622,154 @@ export function createWebhookRoute<
|
|
|
1412
1622
|
): {
|
|
1413
1623
|
POST: (req: Request) => Promise<Response>;
|
|
1414
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
|
+
|
|
1415
1761
|
return {
|
|
1416
1762
|
POST: async (req) => {
|
|
1763
|
+
const server = await resolveServerSource(options.server);
|
|
1764
|
+
|
|
1765
|
+
const pipelined = runPipeline(server);
|
|
1766
|
+
if (pipelined) return pipelined(req);
|
|
1767
|
+
|
|
1768
|
+
// Minimal servers without rawRoute(...) — usually test fakes — build
|
|
1769
|
+
// context directly and run the same handler outside the hooks pipeline.
|
|
1417
1770
|
const headers = resolveHeaders(options.headers, req);
|
|
1418
|
-
const requestHeaders = headersToRecord(req.headers);
|
|
1419
1771
|
|
|
1420
1772
|
let rawBody: string;
|
|
1421
|
-
|
|
1422
1773
|
try {
|
|
1423
1774
|
rawBody = await req.text();
|
|
1424
1775
|
} catch {
|
|
@@ -1432,11 +1783,11 @@ export function createWebhookRoute<
|
|
|
1432
1783
|
);
|
|
1433
1784
|
}
|
|
1434
1785
|
|
|
1435
|
-
|
|
1786
|
+
const contextRequest = toContextRequest(req, rawBody);
|
|
1436
1787
|
|
|
1788
|
+
let ctx: Ctx;
|
|
1437
1789
|
try {
|
|
1438
|
-
|
|
1439
|
-
ctx = await server.createRequestContext(toContextRequest(req, rawBody));
|
|
1790
|
+
ctx = await server.createRequestContext(contextRequest);
|
|
1440
1791
|
} catch {
|
|
1441
1792
|
return webhookRouteJson(
|
|
1442
1793
|
{
|
|
@@ -1448,108 +1799,7 @@ export function createWebhookRoute<
|
|
|
1448
1799
|
);
|
|
1449
1800
|
}
|
|
1450
1801
|
|
|
1451
|
-
|
|
1452
|
-
Ctx,
|
|
1453
|
-
Webhook,
|
|
1454
|
-
AllowUnknownEvents
|
|
1455
|
-
>["event"];
|
|
1456
|
-
|
|
1457
|
-
try {
|
|
1458
|
-
const input: VerifyWebhookInput = {
|
|
1459
|
-
rawBody,
|
|
1460
|
-
headers: requestHeaders,
|
|
1461
|
-
receivedAt: new Date(),
|
|
1462
|
-
};
|
|
1463
|
-
const routeVerifier = options.verify;
|
|
1464
|
-
const verifier = routeVerifier
|
|
1465
|
-
? {
|
|
1466
|
-
async verify() {
|
|
1467
|
-
return routeVerifier({
|
|
1468
|
-
req,
|
|
1469
|
-
ctx,
|
|
1470
|
-
rawBody,
|
|
1471
|
-
headers: requestHeaders,
|
|
1472
|
-
input,
|
|
1473
|
-
});
|
|
1474
|
-
},
|
|
1475
|
-
}
|
|
1476
|
-
: undefined;
|
|
1477
|
-
|
|
1478
|
-
event = (await verifyWebhook(options.webhook, input, {
|
|
1479
|
-
verifier,
|
|
1480
|
-
allowUnknownEvents: options.allowUnknownEvents,
|
|
1481
|
-
})) as WebhookRouteHandlerArgs<
|
|
1482
|
-
Ctx,
|
|
1483
|
-
Webhook,
|
|
1484
|
-
AllowUnknownEvents
|
|
1485
|
-
>["event"];
|
|
1486
|
-
} catch (error) {
|
|
1487
|
-
ctx.ports.logger?.error("Webhook verification failed", {
|
|
1488
|
-
error,
|
|
1489
|
-
webhookName: options.webhook.name,
|
|
1490
|
-
provider: options.webhook.provider,
|
|
1491
|
-
requestId: ctx.requestId,
|
|
1492
|
-
traceId: ctx.traceId,
|
|
1493
|
-
});
|
|
1494
|
-
|
|
1495
|
-
return webhookRouteJson(
|
|
1496
|
-
{
|
|
1497
|
-
ok: false,
|
|
1498
|
-
error: "Webhook verification failed.",
|
|
1499
|
-
},
|
|
1500
|
-
400,
|
|
1501
|
-
headers,
|
|
1502
|
-
);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
try {
|
|
1506
|
-
const result = await options.handle?.({
|
|
1507
|
-
req,
|
|
1508
|
-
ctx,
|
|
1509
|
-
rawBody,
|
|
1510
|
-
event,
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
if (result instanceof Response) return result;
|
|
1514
|
-
|
|
1515
|
-
if (result) {
|
|
1516
|
-
return toWebResponse({
|
|
1517
|
-
status: result.status ?? 200,
|
|
1518
|
-
headers: toHttpResponseHeaders(result.headers ?? headers),
|
|
1519
|
-
body: result.body,
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
return toWebResponse({
|
|
1524
|
-
status: 200,
|
|
1525
|
-
headers: toHttpResponseHeaders(headers),
|
|
1526
|
-
body: {
|
|
1527
|
-
ok: true,
|
|
1528
|
-
webhookName: options.webhook.name,
|
|
1529
|
-
eventId: event.id,
|
|
1530
|
-
eventType: event.type,
|
|
1531
|
-
},
|
|
1532
|
-
});
|
|
1533
|
-
} catch (error) {
|
|
1534
|
-
ctx.ports.logger?.error("Webhook handler failed", {
|
|
1535
|
-
error,
|
|
1536
|
-
webhookName: options.webhook.name,
|
|
1537
|
-
provider: options.webhook.provider,
|
|
1538
|
-
requestId: ctx.requestId,
|
|
1539
|
-
traceId: ctx.traceId,
|
|
1540
|
-
eventId: event.id,
|
|
1541
|
-
eventType: event.type,
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
return webhookRouteJson(
|
|
1545
|
-
{
|
|
1546
|
-
ok: false,
|
|
1547
|
-
error: "Webhook handler failed.",
|
|
1548
|
-
},
|
|
1549
|
-
500,
|
|
1550
|
-
headers,
|
|
1551
|
-
);
|
|
1552
|
-
}
|
|
1802
|
+
return handleWebhookRequest({ req: toContextRequest(req, rawBody), ctx });
|
|
1553
1803
|
},
|
|
1554
1804
|
};
|
|
1555
1805
|
}
|
|
@@ -1573,12 +1823,138 @@ export function createPaymentWebhookRoute<
|
|
|
1573
1823
|
} {
|
|
1574
1824
|
const signatureHeader = options.signatureHeader ?? "stripe-signature";
|
|
1575
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
|
+
|
|
1576
1945
|
return {
|
|
1577
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.
|
|
1578
1955
|
const headers = resolveHeaders(options.headers, req);
|
|
1579
|
-
const signature = req.headers.get(signatureHeader);
|
|
1580
1956
|
|
|
1581
|
-
if (!
|
|
1957
|
+
if (!req.headers.get(signatureHeader)) {
|
|
1582
1958
|
return paymentWebhookJson(
|
|
1583
1959
|
{
|
|
1584
1960
|
ok: false,
|
|
@@ -1590,7 +1966,6 @@ export function createPaymentWebhookRoute<
|
|
|
1590
1966
|
}
|
|
1591
1967
|
|
|
1592
1968
|
let rawBody: string;
|
|
1593
|
-
|
|
1594
1969
|
try {
|
|
1595
1970
|
rawBody = await req.text();
|
|
1596
1971
|
} catch {
|
|
@@ -1605,9 +1980,7 @@ export function createPaymentWebhookRoute<
|
|
|
1605
1980
|
}
|
|
1606
1981
|
|
|
1607
1982
|
let ctx: Ctx;
|
|
1608
|
-
|
|
1609
1983
|
try {
|
|
1610
|
-
const server = await resolveServerSource(options.server);
|
|
1611
1984
|
ctx = await server.createRequestContext(toContextRequest(req, rawBody));
|
|
1612
1985
|
} catch {
|
|
1613
1986
|
return paymentWebhookJson(
|
|
@@ -1620,76 +1993,7 @@ export function createPaymentWebhookRoute<
|
|
|
1620
1993
|
);
|
|
1621
1994
|
}
|
|
1622
1995
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
try {
|
|
1626
|
-
event = await ctx.ports.payments.verifyWebhook({
|
|
1627
|
-
rawBody,
|
|
1628
|
-
signature,
|
|
1629
|
-
});
|
|
1630
|
-
} catch (error) {
|
|
1631
|
-
ctx.ports.logger?.error("Payment webhook verification failed", {
|
|
1632
|
-
error,
|
|
1633
|
-
requestId: ctx.requestId,
|
|
1634
|
-
traceId: ctx.traceId,
|
|
1635
|
-
});
|
|
1636
|
-
|
|
1637
|
-
return paymentWebhookJson(
|
|
1638
|
-
{
|
|
1639
|
-
ok: false,
|
|
1640
|
-
error: "Payment webhook verification failed.",
|
|
1641
|
-
},
|
|
1642
|
-
400,
|
|
1643
|
-
headers,
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
try {
|
|
1648
|
-
const result = await options.handle?.({
|
|
1649
|
-
req,
|
|
1650
|
-
ctx,
|
|
1651
|
-
rawBody,
|
|
1652
|
-
signature,
|
|
1653
|
-
event,
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
if (result instanceof Response) return result;
|
|
1657
|
-
|
|
1658
|
-
if (result) {
|
|
1659
|
-
return toWebResponse({
|
|
1660
|
-
status: result.status ?? 200,
|
|
1661
|
-
headers: toHttpResponseHeaders(result.headers ?? headers),
|
|
1662
|
-
body: result.body,
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
return toWebResponse({
|
|
1667
|
-
status: 200,
|
|
1668
|
-
headers: toHttpResponseHeaders(headers),
|
|
1669
|
-
body: {
|
|
1670
|
-
ok: true,
|
|
1671
|
-
eventId: event.id,
|
|
1672
|
-
eventType: event.type,
|
|
1673
|
-
},
|
|
1674
|
-
});
|
|
1675
|
-
} catch (error) {
|
|
1676
|
-
ctx.ports.logger?.error("Payment webhook handler failed", {
|
|
1677
|
-
error,
|
|
1678
|
-
requestId: ctx.requestId,
|
|
1679
|
-
traceId: ctx.traceId,
|
|
1680
|
-
eventId: event.id,
|
|
1681
|
-
eventType: event.type,
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
return paymentWebhookJson(
|
|
1685
|
-
{
|
|
1686
|
-
ok: false,
|
|
1687
|
-
error: "Payment webhook handler failed.",
|
|
1688
|
-
},
|
|
1689
|
-
500,
|
|
1690
|
-
headers,
|
|
1691
|
-
);
|
|
1692
|
-
}
|
|
1996
|
+
return handlePaymentWebhook({ req: toContextRequest(req, rawBody), ctx });
|
|
1693
1997
|
},
|
|
1694
1998
|
};
|
|
1695
1999
|
}
|
|
@@ -1726,8 +2030,15 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
|
|
|
1726
2030
|
);
|
|
1727
2031
|
}
|
|
1728
2032
|
|
|
1729
|
-
const
|
|
1730
|
-
|
|
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);
|
|
1731
2042
|
const secret = options.secret;
|
|
1732
2043
|
|
|
1733
2044
|
if (!secret) {
|
|
@@ -1742,13 +2053,11 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
|
|
|
1742
2053
|
);
|
|
1743
2054
|
}
|
|
1744
2055
|
|
|
1745
|
-
if (!(await isAuthorizedCronRequest(
|
|
2056
|
+
if (!(await isAuthorizedCronRequest(nativeReq, secret))) {
|
|
1746
2057
|
return cronRouteJson({ ok: false, error: "Unauthorized" }, 401, headers);
|
|
1747
2058
|
}
|
|
1748
2059
|
|
|
1749
2060
|
try {
|
|
1750
|
-
const server = await resolveServerSource(options.server);
|
|
1751
|
-
const ctx = await server.createRequestContext(toRequestLike(req));
|
|
1752
2061
|
const runner = createInlineScheduleRunner<Ctx>({
|
|
1753
2062
|
ctx,
|
|
1754
2063
|
instrumentation: resolveProviderInstrumentationPort(ctx.ports),
|
|
@@ -1789,6 +2098,69 @@ export function createScheduleRoute<Ctx extends ScheduleRouteContext>(
|
|
|
1789
2098
|
}
|
|
1790
2099
|
};
|
|
1791
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
|
+
|
|
1792
2164
|
return {
|
|
1793
2165
|
GET: runScheduleFromRequest,
|
|
1794
2166
|
POST: runScheduleFromRequest,
|
|
@@ -1820,6 +2192,9 @@ export async function createNextServer<
|
|
|
1820
2192
|
route: (contract) => ({
|
|
1821
2193
|
handle: (fn) => createFetchHandler(runtime.route(contract).handle(fn)),
|
|
1822
2194
|
}),
|
|
2195
|
+
rawRoute: (init) => ({
|
|
2196
|
+
handle: (fn) => createFetchHandler(runtime.rawRoute(init).handle(fn)),
|
|
2197
|
+
}),
|
|
1823
2198
|
createContextFromNext: async () => {
|
|
1824
2199
|
// "next" ships no package exports map, so Node-style ESM resolution
|
|
1825
2200
|
// (NodeNext) needs the explicit .js subpath. Bundlers resolve it the same.
|