webapi

package
v0.7.19 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: MIT Imports: 40 Imported by: 0

Documentation

Overview

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

internal/webapi/skill_repo_jobs.go

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 ChatConfig added in v0.7.16

type ChatConfig struct {
	// Enabled gates the entire chat surface. When false, /api/chat/* return
	// 503 and the SPA hides the dock. Defaults to false; the operator must
	// opt in by setting the assistant env vars.
	Enabled bool
	// Provider is the llm.NewProvider name ("openai", "anthropic",
	// "copilot-enterprise", ...).
	Provider string
	// Model is the provider-specific model id (e.g. "claude-sonnet-4-5").
	Model string
	// APIKeySecret is the secret store NAME holding the API key. Used by
	// providers that authenticate with a long-lived API key (openai,
	// anthropic, azure-foundry, openrouter). Ignored when Provider is
	// "copilot-enterprise" — see CopilotPrefix.
	APIKeySecret string
	// CopilotPrefix is the secret-store prefix that holds the Copilot
	// Enterprise token bundle (e.g. "copilot" maps to
	// copilot-access-token + cached copilot-ide-token / -ide-expiry).
	// Used only when Provider == "copilot-enterprise". The operator
	// connects Copilot once via the OAuth device flow (Settings →
	// Providers); ResolveCopilotToken handles IDE-token caching on the
	// way to api.githubcopilot.com.
	CopilotPrefix string
	// MaxTurns caps the tool-using loop. 0 ⇒ chat package default.
	MaxTurns int
	// MaxTokens caps each turn's output. 0 ⇒ chat package default.
	MaxTokens int
}

ChatConfig configures the in-app assistant. The config is operator-set at startup (env vars in cmd/cronfoundry/serve.go); it is intentionally separate from per-schedule provider config so jobs and the assistant can use different models / API keys / budgets.

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
	// SkillRepoClient handles GitHub round-trips for `POST /api/skill-repo/jobs`.
	// Injected from cmd/cronfoundry/serve.go as a *skillrepo.Client.
	// In tests we substitute a fake satisfying the SkillRepoClient interface
	// declared in skill_repo_jobs.go.
	SkillRepoClient SkillRepoClient
	// YamlEditAppendSchedule is the YAML editor used by the proposeJob handler.
	// Wired from internal/yamledit.AppendScheduleToSkill in production; tests
	// inject a function-typed fake.
	YamlEditAppendSchedule YamlAppendScheduleFunc
	// GitHubAppSlug is the GitHub App slug (e.g. "cronfoundry-tng") used to
	// construct the permissions-review URL surfaced in the 412 response from
	// proposeJob when the App lacks pull_requests:write. Empty falls back to
	// "cronfoundry".
	GitHubAppSlug string

	// 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
	// Clock provides the most recent scheduler tick time. May be nil in
	// test environments — handlers degrade gracefully (status="down").
	Clock *scheduler.TickClock
	// SweepInterval is the configured cadence between scheduler ticks.
	// Used to classify scheduler health: <2x healthy, <5x degraded, else down.
	SweepInterval time.Duration
	// Chat configures the in-app assistant. Disabled by default; the
	// operator opts in by setting the assistant env vars.
	Chat ChatConfig
	// contains filtered or unexported fields
}

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 SkillRepoClient added in v0.7.16

type SkillRepoClient interface {
	GetFile(ctx context.Context, installID int64, owner, repo, path, ref string) (*skillrepo.FileContents, error)
	CreateBranch(ctx context.Context, installID int64, owner, repo, branch, fromSHA string) error
	PutFile(ctx context.Context, installID int64, owner, repo, branch, path, fileSHA, message string, content []byte) error
	CreatePR(ctx context.Context, installID int64, req skillrepo.PRRequest) (*skillrepo.PRResult, error)
}

SkillRepoClient is the subset of *skillrepo.Client that proposeJob uses. Declared as an interface so tests can inject a fake.

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".

type YamlAppendScheduleFunc added in v0.7.16

type YamlAppendScheduleFunc func(yamlBytes []byte, skillPath string, sched *config.Schedule) ([]byte, error)

YamlAppendScheduleFunc is the function-shape of yamledit.AppendScheduleToSkill.

Jump to

Keyboard shortcuts

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