/* 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", { 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 => 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}`;