webapi

package
v0.7.14 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

Package webapi hosts the /api/* and /oauth/* browser-facing HTTP surface. Runner-facing endpoints live in internal/api.

Index

Constants

View Source
const CSRFCookieName = "cf_csrf"

CSRFCookieName is the cookie that carries the per-session CSRF token. It is non-HttpOnly so the SPA can read it via document.cookie and echo it in X-CSRF-Token. The cookie's presence on the cronfoundry origin is the security primitive — an attacker on a foreign origin cannot read it.

View Source
const CSRFHeaderName = "X-CSRF-Token"

CSRFHeaderName is the header the SPA must set on every state-changing request. The middleware compares its value to the cookie with a constant-time compare.

View Source
const CopilotClientID = "Iv1.b507a08c87ecfe98"

CopilotClientID is the public OAuth client_id of the GitHub Copilot platform — used for device-flow against GitHub's /login/device/code and /login/oauth/access_token endpoints to mint a Copilot seat token. This value is published by GitHub for any consumer doing Copilot OAuth (including the official VS Code and JetBrains Copilot extensions).

It is NOT the operator's CronFoundry GitHub App client ID; that App authenticates webhooks and per-installation API calls and has nothing to do with the user's Copilot seat.

Variables

View Source
var ErrCopilotReauthRequired = errors.New("copilot: oauth re-auth required")

ErrCopilotReauthRequired indicates the operator must re-authenticate the Copilot integration (the OAuth token is missing/empty or GitHub rejected it as revoked). Surfaced as 401 by the HTTP handler so the runner — and any future SPA "Copilot status" widget — can prompt for re-auth instead of treating it as a transient outage.

Functions

func CSRF

func CSRF(cfg CSRFConfig) func(http.Handler) http.Handler

CSRF returns a middleware that enforces double-submit cookie + Origin check on all non-safe HTTP methods. GET, HEAD, and OPTIONS pass through unchanged (these are the IETF "safe methods"); every other method — including POST, PATCH, PUT, DELETE, and any unknown verb — must present a matching cf_csrf cookie and X-CSRF-Token header.

func ClearCSRFCookie

func ClearCSRFCookie(w http.ResponseWriter)

ClearCSRFCookie deletes the cf_csrf cookie. Called from logout.

func NewCSRFToken

func NewCSRFToken() (string, error)

NewCSRFToken returns a 32-byte random token, base64url-encoded without padding (43 chars). Suitable for use as a per-session CSRF token.

func NewTestSessionCookie

func NewTestSessionCookie(masterKey []byte, login, role string) (*http.Cookie, error)

NewTestSessionCookie creates a signed session cookie for use in handler tests.

func RegisterRoutes

func RegisterRoutes(mux *http.ServeMux, deps Deps)

RegisterRoutes registers /oauth/*, /api/*, and /* (SPA catch-all) on mux.

func RequireRole

func RequireRole(masterKey []byte, role string, next http.Handler) http.Handler

RequireRole wraps a handler requiring a valid session with the given role. "admin" role may access "viewer" routes; "viewer" may not access "admin" routes.

func RequireSession

func RequireSession(masterKey []byte, next http.Handler) http.Handler

RequireSession is middleware that validates the cf_session cookie. Attaches SessionClaims to the request context on success; returns 401 otherwise.

func ResolveCopilotToken

func ResolveCopilotToken(ctx context.Context, store server.SecretStore, prefix string, githubOverrideURL *string) (string, time.Time, error)

ResolveCopilotToken returns a fresh Copilot IDE token (the API key sent to api.githubcopilot.com), minting a new one when the cached one is near expiry.

GitHub Copilot uses a two-token system:

  1. OAuth access token ("gho_..."): obtained once via device flow, stored under <prefix>-access-token. Long-lived; doesn't expire on its own. The empty <prefix>-refresh-token slot is a vestige of an earlier (incorrect) assumption that Copilot's device flow returned an OAuth refresh token — it doesn't, and we don't need one.

  2. IDE token / API key: short-lived (~25-30 min), minted by GET-ing https://api.github.com/copilot_internal/v2/token with the OAuth token in `Authorization: token <oauth>`. Returns {"token": "<api-key>", "expires_at": <unix>}. This is what internal/llm/copilot.go sends as the Bearer token to api.githubcopilot.com. We cache it under <prefix>-ide-token / <prefix>-ide-expiry to avoid hitting GitHub on every run.

On a fresh install the IDE-token cache is empty, so we mint unconditionally on the first call. After that we mint only when the cached token is within 60s of expiry.

githubOverrideURL overrides the api.github.com base for tests; pass nil to use the real endpoint. The override applies to the IDE-token URL only; the path /copilot_internal/v2/token is appended.

func SetCSRFCookie

func SetCSRFCookie(w http.ResponseWriter, token string, maxAge int, host string)

SetCSRFCookie writes the cf_csrf cookie. Called from the OAuth callback alongside SetCookie for cf_session. HttpOnly is intentionally false.

func SignOAuthState

func SignOAuthState(key []byte, ttl time.Duration) (string, error)

SignOAuthState generates a signed random state token for CSRF protection.

func SignSession

func SignSession(claims SessionClaims, key []byte, ttl time.Duration) (string, error)

SignSession encodes claims as a signed cookie value:

base64url(JSON) + "." + base64url(HMAC-SHA256(payload, key))

Types

type CSRFConfig

type CSRFConfig struct {
	// AllowedOrigin is the scheme+host of the trusted public origin
	// (e.g. "https://cronfoundry.example.com"). When empty, the
	// Origin/Referer check is skipped — intended for local dev only.
	AllowedOrigin string
}

CSRFConfig configures the CSRF middleware.

type CopilotTokenRefsJSON

type CopilotTokenRefsJSON struct {
	Prefix string `json:"prefix"`
}

CopilotTokenRefsJSON is stored on the schedule row and identifies which KV secrets hold the token pair for a copilot-enterprise schedule.

type Deps

type Deps struct {
	MasterKey         []byte
	OAuthClientID     string
	OAuthClientSecret string
	AdminLogins       []string
	ViewerLogins      []string
	// GitHubAPIBase overrides the GitHub API base URL in tests. Empty = real GitHub.
	GitHubAPIBase string
	// Queries provides DB access for /api/* handlers.
	Queries *dbgen.Queries
	// Secrets provides secret store access for /api/secrets handlers.
	Secrets server.SecretStore
	// APIBaseURL is the base URL for the internal API (used by run-now).
	APIBaseURL string
	// WebhookSecret is the shared HMAC secret registered with the GitHub App.
	// When empty, POST /webhook/github responds 503 Service Unavailable.
	WebhookSecret []byte
	// Syncer triggers a one-off repo sync. Injected from cmd/cronfoundry/serve.go
	// as a thin wrapper around sync.Poller.SyncOne.
	Syncer RepoSyncer
	// RateLimit configures per-IP rate limiting on public routes.
	RateLimit RateLimiterConfig
	// PublicBaseURL is the externally-reachable base URL of the service
	// (scheme+host, e.g. "https://cronfoundry.example.com"). Used by the CSRF
	// middleware as the Origin/Referer allowlist. Empty disables the Origin
	// check (dev mode); the cookie+header double-submit check still runs.
	PublicBaseURL string
}

Deps holds everything webapi handlers need.

type RateLimiter

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

RateLimiter holds per-group token-bucket state plus an SSE concurrency counter. Construct with NewRateLimiter.

func NewRateLimiter

func NewRateLimiter(cfg RateLimiterConfig) (*RateLimiter, error)

NewRateLimiter returns a RateLimiter. LRUSize defaults to 4096 if < 1.

func (*RateLimiter) Group

func (rl *RateLimiter) Group(name string, next http.Handler) http.Handler

Group returns a middleware applying the named group's per-IP token bucket. Unknown group name panics — caller bug.

func (*RateLimiter) SSE

func (rl *RateLimiter) SSE(next http.Handler) http.Handler

SSE returns a middleware enforcing a per-IP concurrent-stream cap. Increments at request start, decrements when the handler returns. 5-second Retry-After to discourage browser reconnect storms.

type RateLimiterConfig

type RateLimiterConfig struct {
	TrustProxy       bool
	Disabled         bool
	APIRPM           int
	OAuthRPM         int
	WebhookRPM       int
	SSEMaxConcurrent int
	LRUSize          int
}

RateLimiterConfig holds the operator-tunable rate-limit knobs. Zero RPM for a group disables that group; Disabled=true disables the entire middleware (kill switch).

type RepoSyncer

type RepoSyncer interface {
	SyncOne(ctx context.Context, connID pgtype.UUID) error
}

RepoSyncer resolves a repo_connection by ID and triggers a single sync pass. The concrete implementation in serve.go wraps sync.Poller.SyncOne.

type SessionClaims

type SessionClaims struct {
	Login string `json:"login"`
	Role  string `json:"role"`
	Exp   int64  `json:"exp"` // Unix seconds
}

SessionClaims is the payload embedded in the cf_session cookie.

func SessionClaimsFromContext

func SessionClaimsFromContext(ctx context.Context) SessionClaims

SessionClaimsFromContext returns the claims attached by RequireSession. Returns zero value if the middleware was not applied.

func VerifySession

func VerifySession(cookie string, key []byte) (SessionClaims, error)

VerifySession parses and validates a signed cookie value produced by SignSession.

type UIOverrides

type UIOverrides struct {
	Cron       *string `json:"cron,omitempty"`
	Timezone   *string `json:"timezone,omitempty"`
	TimeoutSec *int32  `json:"timeout_sec,omitempty"`
	Enabled    *bool   `json:"enabled,omitempty"`
}

UIOverrides is the subset of schedule fields editable via the UI. Pointer fields: nil means "not overridden".

Jump to

Keyboard shortcuts

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