package rest import ( "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" "github.com/sarathsp06/sparrow/internal/webhooks" ) // APIVersion is the served OpenAPI document's `info.version`. const APIVersion = "1.0.0" // Mount registers Huma on the given chi router: every /v1 REST operation, // the OpenAPI document at /openapi.{json,yaml} (+ 3.0 variants), and the // Scalar interactive reference at /docs. Returns the huma.API so callers can // export the spec (see cmd/openapi-export). aiDeps is optional: a zero // value registers the AI routes in their disabled state. tlsRev may be nil // (revocation checking off). func Mount(r chi.Router, svc webhooks.WebhookServiceInterface, accessDeps AccessDeps, aiDeps AIDeps, tlsRev TLSRevocation) huma.API { config := huma.DefaultConfig("Sparrow", APIVersion) config.Transformers = append(config.Transformers, emptySlices) config.Info.Description = "Sparrow is a self-hosted webhook delivery platform: register the " + "events your system produces, register the webhooks that should receive them, then push " + "event occurrences and Sparrow fans them out with retries and health tracking.\n\n" + "## Concepts\n\n" + "- **Event type** (`Event Types`) — a named, versioned schema for something that happened " + "in your system, e.g. `order.created`. Optional JSON Schema validation is *soft*: invalid " + "payloads are stored with warnings, never rejected.\n" + "- **Event** (`Events`) — one occurrence of an event type, pushed with a payload. Pushing " + "an event asynchronously fans it out to every matching subscription.\n" + "- **Webhook** (`Webhooks`) — a registered HTTP endpoint plus its delivery configuration " + "(retries, timeouts, signing). Every delivery is signed with HMAC-SHA256 (Standard Webhooks), plus Ed25519 when the webhook's signature_type is ed25519.\n" + "- **Subscription** (`Subscriptions`) — links one webhook to one event type within a " + "consumer, optionally with a Go-template payload transform and label filters. Registering a " + "webhook auto-creates matching subscriptions.\n" + "- **Delivery** (`Deliveries`) — one attempt (and its retry history) to send an event to a " + "webhook. Failures are classified as retryable (5xx, timeout, network) or not (4xx, DNS, TLS).\n" + "- **Health** (`Health`) — a per-webhook label (healthy/degraded/unhealthy/unknown) " + "derived from the last 24 hours of attempts, refreshed every `SPARROW_HEALTH_EVAL_INTERVAL`.\n\n" + "## Consumers\n\n" + "Webhooks, events, subscriptions, and deliveries are scoped to a `consumer` path segment " + "(defaults to `default` if you don't need multi-tenancy). A handful of read endpoints are " + "deliberately global (no consumer) for cross-tenant dashboards and id-only lookups where the " + "resource id is already globally unique.\n\n" + "## Authentication\n\n" + "Optional. When the server is started with `SPARROW_API_KEY` set, every `/v1/*` request " + "must carry that master key or a tenant-wide access token (`createToken` without a " + "`consumer`), in the `X-API-Key` header or as `Authorization: Bearer `. When " + "`SPARROW_API_KEY` is unset, every endpoint is open, and outside `SPARROW_ENVIRONMENT=production` " + "CORS also allows any origin: never expose such a server beyond a trusted network.\n\n" + "## Portal tokens\n\n" + "A consumer-scoped access token (`createToken` with a `consumer`) powers the embedded " + "portal at `/portal`. It is refused on `/v1` (403); its API calls go to the single " + "`/portal/api/` prefix with `Authorization: Bearer `, and the consumer is carried " + "by the token, not the URL, so exposing the portal means allowlisting only `/portal`, " + "`/_app`, and `/portal/api`. `/portal/api/` is served by " + "`/v1/consumers//`, except pushing or re-pushing events " + "(`POST events`, `POST events:rePush`, `repush-jobs`, `prepare_repush`: refused), plus these " + "global helpers: `GET event-types[/...]` (listed for the token's consumer), " + "`GET template-functions`, `GET capabilities`, and `POST subscriptions:testTemplate`. " + "With `SPARROW_PORTAL_RETRY=false` the delivery retry calls and `prepare_retry` are refused too (403), " + "and `GET capabilities` reports `portal.retry: false`. " + "Each operation's description says whether portal tokens can call it.\n\n" + "## Errors\n\n" + "Errors from the API itself are RFC 9457 problem details " + "(`Content-Type: application/problem+json`): `{\"title\", \"status\", \"detail\", " + "\"errors\"}`, where `errors` lists per-field validation problems. Two layers in front of " + "the API answer with plain `application/json` instead:\n\n" + "- Authentication (`401`, `503`): `{\"error\": \"unauthorized\", \"reason\": " + "\"missing|invalid|expired|revoked\", \"message\": \"...\"}`; `reason` is absent for " + "`503 unavailable` (retry after the `Retry-After` header).\n" + "- Authorization and the portal gateway (`401`, `403`): `{\"error\": \"forbidden\" or " + "\"unauthorized\", \"message\": \"...\"}`, e.g. a consumer-scoped token on `/v1`.\n\n" + "Status codes: `400` invalid input, `404` not found, `409` conflict or wrong state, `413` " + "body over `SPARROW_MAX_BODY_BYTES`, `429` rate limited, `500` internal (details are " + "logged, not returned), `503` temporarily unavailable, `504` timed out." config.Info.Contact = &huma.Contact{ Name: "Sparrow", URL: "https://github.com/sarathsp06/sparrow", } config.Info.License = &huma.License{ Name: "MIT", Identifier: "MIT", } config.Tags = []*huma.Tag{ {Name: "Webhooks", Description: "Registered HTTP endpoints and their delivery configuration " + "(retries, timeouts, signing, rate limits). Secrets are always masked in responses except " + "once, at creation time."}, {Name: "Event Types", Description: "Named, versioned schemas for the events your system can " + "produce. Schema validation is soft — invalid payloads are stored with warnings, not rejected."}, {Name: "Events", Description: "Pushed occurrences of an event type. Pushing an event " + "asynchronously fans it out to every subscription that matches its event name and label filters."}, {Name: "Subscriptions", Description: "The link between one webhook and one event type within a " + "consumer. Carries the optional payload-transform template and label filters that decide " + "whether a given event occurrence produces a delivery."}, {Name: "Deliveries", Description: "Individual delivery attempts and their per-attempt retry " + "history. Failed deliveries can be retried singly or in bulk via a prepared batch snapshot."}, {Name: "Health", Description: "Rolling per-webhook health status computed from recent delivery " + "outcomes, plus cross-consumer aggregate counts for dashboards."}, {Name: "Alert Configs", Description: "Opt-in email recipients for Sparrow's own self-generated " + "webhook.health_changed, webhook.delivery_failed and webhook.disabled system events."}, {Name: "Recipes", Description: "Shipped webhook adapter recipes that pre-fill destinations, headers, and payload transforms."}, {Name: "Access", Description: "Access tokens and invites. Tenant-wide tokens work like SPARROW_API_KEY but can be revoked one by one; consumer-scoped tokens only reach the portal API. Secrets are shown once."}, {Name: "Server", Description: "Deployment-level information about this server, such as which optional features are configured."}, } config.DocsPath = "/docs" config.DocsRenderer = huma.DocsRendererScalar config.OpenAPIPath = "/openapi" config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{ "ApiKeyAuth": { Type: "apiKey", In: "header", Name: "X-API-Key", Description: "The master key (SPARROW_API_KEY) or a tenant-wide access token. Also accepted as Authorization: Bearer . Required only when the server is started with SPARROW_API_KEY set; otherwise all endpoints are open. The embedded UI never receives the key: it asks for a credential on the first 401.", }, } // Optional auth: try the API key, but an empty requirement ({}) means // "or no credentials" is also acceptable. config.Security = []map[string][]string{ {"ApiKeyAuth": {}}, {}, } api := humachi.New(r, config) registerWebhookRoutes(api, svc) registerEventRoutes(api, svc) registerEventBundleRoutes(api, svc) registerSubscriptionRoutes(api, svc) registerDeliveryRoutes(api, svc) registerHealthRoutes(api, svc) registerAlertConfigRoutes(api, svc) registerRecipeRoutes(api) registerAccessRoutes(api, accessDeps) registerAIRoutes(api, svc, aiDeps) registerCapabilitiesRoute(api, svc, aiDeps, accessDeps.PortalRetry) registerTestEventRoutes(api, svc) registerTLSRoutes(api, tlsRev) return api }