Files
zopu-code/packages/primitives/src/git-webhook.ts
2026-07-31 21:00:47 +05:30

185 lines
5.1 KiB
TypeScript

/* eslint-disable max-classes-per-file -- webhook event envelopes and errors form one normalized module. */
import { Effect, Schema } from "effect";
import { GitProvider } from "./git-provider";
const Text = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, {
expected: "a non-empty string",
})
);
// ---------------------------------------------------------------------------
// Supported event names
// ---------------------------------------------------------------------------
export const GithubEventName = Schema.Literals([
"ping",
"push",
"repository",
"delete",
"create",
"public",
"member",
"organization",
"fork",
"release",
]);
export type GithubEventName = typeof GithubEventName.Type;
export const PuterEventName = Schema.Literals([
"ping",
"push",
"repository",
"create",
"delete",
"fork",
"release",
]);
export type PuterEventName = typeof PuterEventName.Type;
/**
* Events we normalize into repository lifecycle now: repository changes,
* pushes, deletions, transfers, and visibility changes.
*/
export const PROCESSED_EVENTS = new Set([
"push",
"repository",
"delete",
"create",
"public",
"fork",
]);
export const isProcessedEvent = (event: string): boolean =>
PROCESSED_EVENTS.has(event);
// ---------------------------------------------------------------------------
// Signature verification inputs
// ---------------------------------------------------------------------------
export const SignatureVerificationInput = Schema.Struct({
body: Schema.String,
provider: GitProvider,
signature: Text,
/** Hex-encoded HMAC signature from the provider header. */
signatureHeaderName: Text,
token: Text,
});
export type SignatureVerificationInput = typeof SignatureVerificationInput.Type;
// ---------------------------------------------------------------------------
// Webhook delivery processing states
// ---------------------------------------------------------------------------
export const DeliveryState = Schema.Literals([
"received",
"processing",
"processed",
"ignored",
"failed",
"duplicate",
]);
export type DeliveryState = typeof DeliveryState.Type;
export const isTerminalDelivery = (state: DeliveryState): boolean =>
state === "processed" ||
state === "ignored" ||
state === "failed" ||
state === "duplicate";
// ---------------------------------------------------------------------------
// Normalized repository event envelope
// ---------------------------------------------------------------------------
export const RepositoryEventAction = Schema.Literals([
"push",
"created",
"deleted",
"transferred",
"visibility_changed",
"default_branch_changed",
"renamed",
"forked",
"other",
]);
export type RepositoryEventAction = typeof RepositoryEventAction.Type;
export const RepositoryEventEnvelope = Schema.Struct({
action: RepositoryEventAction,
/** Provider-assigned delivery GUID. */
deliveryId: Text,
event: Text,
/** External repository ID from the provider payload. */
externalRepositoryId: Text,
/** Full owner/name from the provider payload. */
fullName: Text,
owner: Text,
provider: GitProvider,
pushedTo: Schema.UndefinedOr(Text),
/** Ref or branch that changed, when applicable. */
ref: Schema.UndefinedOr(Text),
refType: Schema.UndefinedOr(Text),
repositoryName: Text,
/** ISO-like timestamp from the provider, or epoch 0 when absent. */
repositoryUpdatedAt: Schema.Number,
serverUrl: Text,
visibility: Schema.UndefinedOr(Text),
});
export type RepositoryEventEnvelope = typeof RepositoryEventEnvelope.Type;
// ---------------------------------------------------------------------------
// Webhook errors
// ---------------------------------------------------------------------------
export const WebhookErrorReason = Schema.Literals([
"InvalidInput",
"Unauthorized",
"NotFound",
"Conflict",
"InvalidResponse",
"PayloadTooLarge",
]);
export type WebhookErrorReason = typeof WebhookErrorReason.Type;
export class WebhookError extends Schema.TaggedErrorClass<WebhookError>()(
"WebhookError",
{
message: Schema.String,
reason: WebhookErrorReason,
}
) {}
export const MAX_WEBHOOK_PAYLOAD_BYTES = 10 * 1024 * 1024;
/**
* Apply a payload-size limit before JSON decoding. Rejects payloads larger
* than the maximum before any parsing.
*/
export const assertPayloadSize = (
byteLength: number
): Effect.Effect<void, WebhookError> =>
Effect.gen(function* assert() {
if (byteLength > MAX_WEBHOOK_PAYLOAD_BYTES) {
return yield* Effect.fail(
new WebhookError({
message: `Webhook payload exceeds ${MAX_WEBHOOK_PAYLOAD_BYTES} bytes`,
reason: "PayloadTooLarge",
})
);
}
});
// ---------------------------------------------------------------------------
// Delivery idempotency helpers
// ---------------------------------------------------------------------------
/**
* Construct the idempotency key for a webhook delivery. Duplicate deliveries
* with the same provider/deliveryId are no-ops.
*/
export const deliveryIdempotencyKey = (
provider: GitProvider,
deliveryId: string
): string => `webhook:${provider}:${deliveryId}`;