c0b84595a3
## Summary The server half of hosted webhooks: the public ingress endpoint, signature verification, the delivery pipeline (Postgres partitioned storage + ClickHouse for ordering), the in-app partition manager, the HTTP API, and the dashboard (Deliveries, Endpoints, and the in-app test console). The public SDK and docs half is #4537. That PR carries the user-facing API (`webhook()`, `chat.event` / `chat.channels`, the `@trigger.dev/slack` connector) and builds on the shared `@trigger.dev/core` schemas that ship here. ## Shipping behind a flag A `WEBHOOK_ENABLED` env var (default off) gates the public ingress route and the engine worker plus partition cron, so merging and deploying this changes nothing in production until it is flipped on per environment. The dashboard is separately gated per org by the `hasWebhooksAccess` feature flag. ## Note on packages This PR includes the `@trigger.dev/core` schema additions the server compiles against, but carries no changeset. Core is not consumed independently of the SDK, so it is released together with the SDK via #4537. Keeping its changeset off `main` means no release cut from `main` publishes it early.
46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readBodyWithCap } from "~/utils/readBodyWithCap.server";
|
|
|
|
function streamingRequest(chunks: Uint8Array[]): Request {
|
|
const body = new ReadableStream<Uint8Array>({
|
|
pull(controller) {
|
|
const next = chunks.shift();
|
|
if (next) controller.enqueue(next);
|
|
else controller.close();
|
|
},
|
|
});
|
|
return new Request("https://example.com/ingest", {
|
|
method: "POST",
|
|
body,
|
|
// @ts-expect-error duplex isn't in the lib types yet but Node requires it for a stream body
|
|
duplex: "half",
|
|
});
|
|
}
|
|
|
|
const chunk = (n: number) => new Uint8Array(n).fill(122);
|
|
|
|
describe("readBodyWithCap", () => {
|
|
it("returns the full body when under the cap", async () => {
|
|
const bytes = await readBodyWithCap(streamingRequest([chunk(100), chunk(100)]), 1024);
|
|
expect(bytes).not.toBeNull();
|
|
expect(bytes!.byteLength).toBe(200);
|
|
});
|
|
|
|
it("returns null when the streamed body exceeds the cap (no full buffering)", async () => {
|
|
const bytes = await readBodyWithCap(
|
|
streamingRequest([chunk(1024), chunk(1024), chunk(1024)]),
|
|
2048
|
|
);
|
|
expect(bytes).toBeNull();
|
|
});
|
|
|
|
it("treats an empty body as zero bytes", async () => {
|
|
const bytes = await readBodyWithCap(
|
|
new Request("https://example.com/ingest", { method: "POST" }),
|
|
1024
|
|
);
|
|
expect(bytes).not.toBeNull();
|
|
expect(bytes!.byteLength).toBe(0);
|
|
});
|
|
});
|