gateway

package
v0.2.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package gateway is the daemon's single ingress: it authenticates, resolves the user to a persistent host via the provisioner, and reverse-proxies everything else through.

Routing: /ping and /gateway/health are served locally; every other path proxies to the user's host with the provisioner-supplied transport injecting per-request auth.

preview_proxy.go — tokenized-URL subdomain proxy.

Request path: client → "preview-<token>.<root>" → this handler → gateway's Provisioner.OpenInternalConn → Metro (or other dev server) inside the sprite.

Auth model:

  • Owner-only tokens: require JWT (via the same Authenticator the main mux uses) and assert principal.sub == route.owner_user_id. Cross-tenant attempts surface as 404 (not 403) — never leak "this token exists but isn't yours."
  • Public tokens: no auth check. The URL itself is the capability; the gateway treats anyone with the link as a legitimate viewer.

Lifecycle: per-(host_id, port) tunnels are pooled lazily and never explicitly evicted from this map. Stdlib http.Transport's IdleConnTimeout closes the inner WSS connections after they go unused; the empty Tunnel wrapper remains in the pool but consumes negligible memory. A sprite suspend doesn't need active eviction — the next request opens a fresh WSS to api.sprites.dev which wakes the sprite (Sprites edge auto-wake).

preview_tokens.go — owner-facing token management API.

Routes mounted inside Handler() (inherits outer JWT auth wrap):

GET    /v1/preview/tokens                — list the caller's live tokens
POST   /v1/preview/tokens/{token}/share  — flip visibility + extend TTL
DELETE /v1/preview/tokens/{token}        — revoke a specific token

Cross-tenant gate: every mutation re-fetches the route and asserts route.OwnerUserID == principal.UserID. A token-guessing attacker gets 404 (not 403) so they can't distinguish "not yours" from "doesn't exist." The list endpoint returns only the caller's rows.

webhook_preview.go — sprite-facing webhook endpoints that mint and revoke tokenized preview URLs.

Auth model: identical to /webhooks/notifications. clank-host on the sprite carries its per-host notifier_token bearer (provisioner-issued at host create time, stored in `hosts.notifier_token`); the handler resolves it to a hosts row, and the row's user_id becomes the preview route's owner_user_id. Mounted PRE-auth via PreviewWebhookHandler so the user-JWT middleware doesn't 401 the host call before it ever reaches us.

register is idempotent on (host_id, worktree_id, service_name) so a sprite restart returns the same token — mobile's cached URL doesn't churn. revoke is best-effort idempotent so the sprite's preview/stop can call it unconditionally without the gateway 404'ing.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthConfig

type AuthConfig struct {
	AuthorizeEndpoint string   `json:"authorize_endpoint"`
	TokenEndpoint     string   `json:"token_endpoint"`
	ClientID          string   `json:"client_id"`
	Scopes            []string `json:"scopes,omitempty"`
	DefaultProvider   string   `json:"default_provider,omitempty"`

	// CallbackPort, when set, instructs the laptop to bind its
	// PKCE callback listener to exactly this port. Required when
	// the IdP matches redirect_uris strictly (e.g. Supabase OAuth
	// Server). The IdP must have `http://127.0.0.1:<port>` in its
	// redirect_uris allow-list. Zero = random kernel-assigned port
	// (RFC 8252 default for native apps).
	CallbackPort int `json:"callback_port,omitempty"`
}

AuthConfig is the public OAuth 2.0 discovery payload returned by GET /auth-config. Embedders populate Config.AuthConfig with their IdP details; the gateway serves it via AuthConfigHandler. Daemons must mount that handler PRE-auth (it's the laptop's bootstrap route — clank has no token when it calls it).

Standard OAuth 2.0 only — Supabase OAuth Server, Auth0, Keycloak, Okta, etc. all fit this shape. Nothing provider-specific.

type Config

type Config struct {
	// Provisioner resolves a userID into the user's HostRef. EnsureHost
	// is called per-request; the provisioner caches in-process.
	Provisioner provisioner.Provisioner

	// Images is the embedded image-upload presign server. When non-nil,
	// the gateway mounts POST /v1/images. When nil, the route isn't
	// mounted (404).
	Images *images.Server

	// AuthConfig, when non-nil, makes AuthConfigHandler() return a
	// handler that serves this payload as JSON. Daemons wire that
	// handler pre-auth on GET /auth-config so the laptop can
	// discover the IdP before it has a token.
	AuthConfig *AuthConfig

	// Notify, when non-nil, exposes mobile push notifications:
	//   - /devices (POST, DELETE) mount inside Handler() and inherit
	//     whatever user-auth middleware wraps it (same model as Sync).
	//   - /webhooks/notifications mounts pre-auth via the daemon's
	//     parent mux — see NotifyWebhookHandler. The dispatcher does
	//     its own host-bearer verification, so wrapping it with user
	//     auth would reject the host call outright.
	//
	// Construct the dispatcher with notify.NewDispatcher; the laptop
	// daemon passes its in-process store, cloud embedders pass their
	// pgx-backed implementations. Either way, the gateway just mounts.
	Notify *notify.Dispatcher

	// PreviewRoutes is the persistence for tokenized preview URLs. When
	// set together with PreviewHostLookup and PreviewRootDomain, the
	// gateway mounts:
	//   - /v1/preview/tokens/{token}/share, DELETE /v1/preview/tokens/{token},
	//     GET /v1/preview/tokens — owner-facing token management. Inherits
	//     the outer auth wrap.
	//   - PreviewWebhookHandler() — sprite-facing register/revoke,
	//     analogous to NotifyWebhookHandler. Mounted pre-auth by the
	//     daemon.
	// All four must be set together or none — leaving any nil
	// disables the entire preview surface.
	PreviewRoutes routestore.Store

	// PreviewHostLookup resolves a sprite's notifier_token bearer to
	// the host row, used to authenticate /webhooks/preview/*. Same
	// pattern as notify.Dispatcher's HostLookup.
	PreviewHostLookup PreviewHostLookup

	// PreviewRootDomain is the wildcard zone preview URLs live under,
	// e.g. "clankexample.dev". Combined with the per-token leftmost label
	// (preview-<token>) by pkg/preview/tokens.HostFor. Required to
	// render the URL that the register webhook returns to the sprite.
	PreviewRootDomain string

	// PreviewAuthenticator verifies JWTs on owner-only preview-URL
	// requests. Same Authenticator the daemon's main auth.Middleware
	// uses (clank passes its OIDC verifier here). The subdomain
	// proxy lives OUTSIDE auth.Middleware because public-visibility
	// tokens must accept anonymous requests — so we run Verify inline
	// for owner-only tokens only.
	//
	// Required when PreviewRoutes is set.
	PreviewAuthenticator auth.Authenticator

	// PreviewSigningKey is the HMAC secret used to sign short-lived
	// owner-only preview URLs. Clients that can't carry an
	// Authorization header (Expo's dev-launcher, the RN bundle
	// runtime) authenticate via a `?clank_sig=…&clank_exp=…` bearer
	// minted by POST /v1/preview/tokens/{token}/sign.
	//
	// Required when PreviewRoutes is set. Must be at least
	// tokens.MinSigningKeyBytes (32). When empty in a wired-up
	// gateway, NewGateway generates a random key and logs a warning —
	// fine for dev, but a restart invalidates outstanding signed
	// URLs, so production should persist a configured value.
	PreviewSigningKey []byte

	// IdPDeleter, when non-nil, is invoked as the final step of
	// DELETE /v1/account to delete/disable the user in the operator's
	// external SSO. Optional — nil skips the IdP step (clank-data-only
	// deletion). See the IdPDeleter interface.
	IdPDeleter IdPDeleter
}

Config wires the gateway's dependencies. Provisioner is required.

Authentication is the responsibility of an outer middleware (see pkg/auth.Middleware) — by the time a request reaches the gateway, the verified Principal is already in r.Context().

type Gateway

type Gateway struct {
	// contains filtered or unexported fields
}

Gateway is the public ingress.

func NewGateway

func NewGateway(cfg Config, lg *log.Logger) (*Gateway, error)

NewGateway constructs a Gateway.

func (*Gateway) AuthConfigHandler

func (g *Gateway) AuthConfigHandler() http.Handler

AuthConfigHandler returns an http.Handler that serves the configured AuthConfig as JSON, or nil when AuthConfig is unset. Daemons must mount this PRE-auth (GET /auth-config is the laptop's bootstrap discovery route — clank has no token yet at that point).

Returning a nil handler when AuthConfig is unset lets callers wire the route conditionally without ceremony — `if h := gw.AuthConfigHandler(); h != nil { mux.Handle("GET /auth-config", h) }`.

func (*Gateway) Handler

func (g *Gateway) Handler() http.Handler

Handler returns the public-listener http.Handler.

/ping and /gateway/health answer locally without waking a host; the /v1/* routes below are gateway-orchestrated (mostly pure proxies to the user's host); every other path proxies to the user's host verbatim. Authentication is handled by an outer middleware (pkg/auth.Middleware); handlers read the Principal from r.Context() via auth.MustPrincipal.

func (*Gateway) NotifyWebhookHandler

func (g *Gateway) NotifyWebhookHandler() http.Handler

NotifyWebhookHandler returns the dispatcher's host-side webhook handler when Config.Notify is set, otherwise nil. Daemons mount it PRE-auth on POST /webhooks/notifications — the dispatcher verifies the host bearer token itself, and the outer user-auth middleware would 401 the host call before it ever reached the dispatcher.

Returning nil when Notify is unset mirrors AuthConfigHandler so callers can wire the route conditionally — `if h := gw.NotifyWebhookHandler(); h != nil { mux.Handle("POST /webhooks/notifications", h) }`.

func (*Gateway) PreviewWebhookHandler

func (g *Gateway) PreviewWebhookHandler() http.Handler

PreviewWebhookHandler returns the sprite-facing register/revoke router for /webhooks/preview/*, or nil when PreviewRoutes is unset. Daemons mount this PRE-auth so the per-host notifier_token bearer (which the handler verifies itself) reaches the resolver — the outer user-JWT middleware would 401 the host call outright.

Routes mounted on the returned handler:

POST /webhooks/preview/register  →  upsert (host_id, wid, svc) → {token, url, expires_at}
POST /webhooks/preview/revoke    →  RevokeByService (idempotent)

func (*Gateway) WrapPreviewSubdomain

func (g *Gateway) WrapPreviewSubdomain(fallback http.Handler) http.Handler

WrapPreviewSubdomain returns an http.Handler that dispatches by Host header: requests to preview-<token>.<PreviewRootDomain> hit the tokenized-URL proxy (which does its own per-token auth depending on visibility), and every other request falls through to fallback (typically the auth-wrapped main mux).

When PreviewRoutes is unset the wrapper is a no-op: it returns fallback unchanged so daemons can call this unconditionally without branching on preview-config presence.

This is the OUTERMOST layer at the daemon's parent mux because public-visibility tokens must accept anonymous requests; if we went through the outer JWT middleware first, those public URLs would 401 before reaching our visibility check.

type IdPDeleter

type IdPDeleter interface {
	DeleteUser(ctx context.Context, userID string) error
}

IdPDeleter optionally deletes or disables a user in the operator's external SSO/identity provider when their clank account is deleted. clank itself only verifies tokens (pkg/auth.Authenticator) and has no IdP write access, so this is an extension point: when Config.IdPDeleter is nil the account-deletion endpoint skips the IdP step. Operators wire a concrete implementation (Supabase admin API, Auth0 Management API, …); they choose delete-vs-disable semantics inside DeleteUser.

type PreviewHostLookup

type PreviewHostLookup interface {
	GetHostByNotifierToken(ctx context.Context, notifierToken string) (hoststore.Host, error)
}

PreviewHostLookup is the narrow surface preview handlers need from the hoststore: resolve a sprite's per-host notifier bearer to the owning host row (and through it the user_id). Same shape as notify.HostLookup; defined here to keep gateway from importing the notify package just for an interface alias.

hoststore.HostStore satisfies this; cloud embedders wire their pgx-backed implementation into both notify and preview.

Directories

Path Synopsis
Package previewtunnel exposes the preview-app's HTTP transport: a thin wrapper around an stdlib *http.Transport whose DialContext opens a fresh net.Conn to a sprite's internal port via the configured Provisioner.
Package previewtunnel exposes the preview-app's HTTP transport: a thin wrapper around an stdlib *http.Transport whose DialContext opens a fresh net.Conn to a sprite's internal port via the configured Provisioner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL