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 ¶
NewGateway constructs a Gateway.
func (*Gateway) AuthConfigHandler ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
Source Files
¶
- account_delete.go
- gateway.go
- github_connect.go
- github_disconnect.go
- github_pr.go
- github_proxy.go
- github_pull_request.go
- github_repos.go
- github_repository.go
- github_status.go
- preview_overlay_api.go
- preview_overlay_context.go
- preview_proxy.go
- preview_tokens.go
- projects_create.go
- projects_import.go
- proxy.go
- remote_sync.go
- repos_proxy.go
- responses.go
- webhook_preview.go
- worktrees_delete.go
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. |