sites

package
v1.800.1 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 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 projects store, injected via SetResolver at mount. sites does NOT import projects (projects imports cloud, cloud imports sites — importing projects 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 (projects/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. projects.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
	// SelfDomains are the registrable domains of OUR OWN infrastructure (e.g.
	// hanzo.ai, hanzo.app). A Host at or under any of them is never a customer
	// custom-domain candidate, so the high-traffic api/console path is never
	// subjected to a per-request binding lookup, and a customer binding can never
	// shadow a real Hanzo host. The apex is always treated as self.
	SelfDomains []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 projects 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. Three outcomes, in order:

  1. `<slug>.<apex>` (e.g. maxpower.hanzo.app) → serve the slug's site (terminal).
  2. a bound CUSTOM domain (e.g. yadota.tech, a customer's own apex pointed at this edge) → serve that project's site from its S3 prefix (terminal). Only an external host (not one of OUR self domains) with a LIVE binding qualifies.
  3. anything else — our API/console hosts, or an unbound external host routed here — → Continue(), so the normal /v1 + console pipeline runs unchanged.

A published site (either shape) is a PUBLIC artifact: it returns HERE, never entering the authenticated/billed API pipeline.

type Site

type Site struct {
	Org    string
	Slug   string
	Bucket string
	Prefix string
	Status string
	// CrossOriginIsolation, when true, makes the site server emit the cross-origin
	// isolation headers (COOP/COEP on documents, CORP on assets) so a multithreaded
	// Unity/Unreal/Godot WebGL build can use SharedArrayBuffer. It is OPT-IN per
	// site — isolation blocks embedding third-party cross-origin content, so it is
	// NEVER global. Server-owned like every other field: the resolver sets it (from
	// the project's declared WebGL game-engine framework), never the request.
	CrossOriginIsolation bool
}

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