sites

package
v1.786.82 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package sites is the public site-server for published projects: the host-routed edge that turns `<slug>.hanzo.app` into the static site a user deployed to OUR S3.

It is NOT a /v1 API. It owns the ROOT path space for requests whose Host is a site host (`<slug>.<apex>`, apex default hanzo.app). It is installed as the FIRST middleware in the compose root (serve.go), before identity/billing, so a public site GET never enters the authenticated API pipeline — a site is a public artifact, not a tenant API call.

Tenant isolation is the whole point (this is RED-reviewed). The org and the S3 prefix a request may read come ONLY from the store lookup keyed by the validated subdomain slug — NEVER from the request path, a client header, or the Host beyond the one validated label. One slug ⇒ exactly one `<org>/<slug>/` S3 prefix, hard-bounded, and the object key is rooted-clean so no `..`/encoded traversal can escape that prefix into another project or org. See resolveKey + its exhaustive test.

The resolver (slug → {org,bucket,prefix,status}) is the projectsvc store, injected via SetResolver at mount. sites does NOT import projectsvc (projectsvc imports cloud, cloud imports sites — importing projectsvc here would form a cycle); the store implements the tiny Resolver interface and registers itself.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CacheControlFor

func CacheControlFor(key, htmlOverride string) string

CacheControlFor is the ONE canonical cache policy by asset class, used both when WRITING an object at deploy (projectsvc/blob.go) and when SERVING one here, so a site's TTL is identical on the site-server path and the direct-S3 path.

  • HTML documents → short browser TTL + long shared-cache TTL: a redeploy is seen fast (60s), while the CDN holds it for a day (purged instantly on redeploy by cache-tag). htmlOverride, when non-empty, replaces this for a project (the per-project cacheControl knob); it applies to documents only.
  • content-hashed / immutable assets → cache for a year (a new build changes the hash, so the URL is safe forever). NOT overridable — always correct.
  • everything else → a conservative middle TTL.

func CacheTag

func CacheTag(org, slug string) string

CacheTag is the ONE canonical cache-tag for a site's objects. The site server stamps it as the Cache-Tag response header; the Purger purges it on redeploy / delete. Both derive it from server-owned Org+Slug so they always agree.

func IsReserved

func IsReserved(label string) bool

IsReserved reports whether a subdomain label may NOT be a published site. This is the ONE predicate every enforcement point calls, so serve/create/bind never drift. The comparison is on the lowercased label.

func ReservedLabels

func ReservedLabels() []string

ReservedLabels returns the sorted union of baked-in and operator-configured reserved labels (the empty apex label omitted). Exposed for diagnostics / tests.

func SetReservedExtra

func SetReservedExtra(labels []string)

SetReservedExtra registers operator-supplied extra reserved labels (from CLOUD_SITES_RESERVED). It ADDS to baseReserved; it can never remove a baked-in reserved label. Called once at startup by New.

func SetResolver

func SetResolver(r Resolver)

SetResolver installs the slug→Site resolver. projectsvc.Mount calls this once with its store. Until it is set, every site request is an honest 404 (the projects subsystem is not mounted), never a crash.

Types

type Config

type Config struct {
	Apex     string
	Reserved []string
}

Config configures the site host-router. Apex is the zone whose subdomains are site hosts (hanzo.app). Reserved is the set of subdomain labels that are NOT sites (they belong to real app hosts) and must fall through to the normal pipeline — the reserved-host exclusion that stops a site from shadowing a real hanzo.app app or api.

type Purger

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

Purger purges the Cloudflare edge cache by cache-tag so a redeploy is instantly live at the edge. It is deliberately minimal: one POST to the zone purge API with the tags we stamped onto every site object (Cache-Tag: site-<org>-<slug>).

Credentials come ONLY from the environment the operator injects from KMS (CF_API_TOKEN, CF_ZONE_ID) — never hard-coded, never logged. When either is unset the Purger is a no-op that warns once per call: a missing CF token must degrade to "stale-until-TTL", never fail a deploy.

func NewPurger

func NewPurger(log luxlog.Logger) *Purger

NewPurger reads CF_API_TOKEN + CF_ZONE_ID from the environment. The returned Purger is safe to hold for the process; Configured() reports whether it can actually reach Cloudflare.

func (*Purger) Configured

func (p *Purger) Configured() bool

Configured reports whether both a token and a zone id are present.

func (*Purger) PurgeTags

func (p *Purger) PurgeTags(ctx context.Context, tags ...string) error

PurgeTags purges every listed cache-tag. A no-op (warn-only) when unconfigured, so callers invoke it unconditionally after a publish. A purge failure is returned so the caller can log it, but callers treat it as non-fatal: the new content still becomes visible when the short HTML TTL lapses.

type Resolver

type Resolver interface {
	Resolve(ctx context.Context, slug string) (Site, bool, error)
}

Resolver maps a validated subdomain slug to its authoritative Site. It is the projectsvc store (the ONE source of project truth). found=false ⇒ no such published subdomain (honest 404); err ⇒ a real store failure (honest 500).

type Server

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

Server is the host-routed public site edge. It holds the S3 access path (s3admin, the SAME credentials as the deploy blob store) plus the apex. The reserved-label policy is the package-level shared source (reserved.go), so serve/create/bind never disagree. It reads the resolver at request time.

func New

func New(cfg Config, log luxlog.Logger) *Server

New builds the Server from Config. An empty apex defaults to hanzo.app. Operator-supplied Reserved labels are registered into the shared reserved source (ADDING to the baked-in defaults, never removing them), so createProject and BindHost enforce the exact same set the serve gate does.

func (*Server) Middleware

func (s *Server) Middleware() zip.Handler

Middleware is the host-router. For a site host it serves the request and returns (terminal — the API pipeline is never entered). For every other host it calls Continue() so the normal /v1 + console pipeline runs unchanged.

type Site

type Site struct {
	Org    string
	Slug   string
	Bucket string
	Prefix string
	Status string
}

Site is the authoritative binding for a published subdomain: the tenant (Org), the S3 location (Bucket + Prefix), and the publish Status. Every field is server-owned — produced by the store from the validated slug, never from the request. Prefix is the hard tenant boundary for object reads.

Jump to

Keyboard shortcuts

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