Ctrl K
ComunidadGuías…

internal/rest/app.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 <credential>`. 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 <token>`, 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/<rest>` is served by " +
		"`/v1/consumers/<token's consumer>/<rest>`, 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 <credential>. 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
}