// Package geoip is the platform's GeoIP vocabulary (spec // docs/superpowers/specs/2026-10-06-platform-geoip-design.md): the bucket // records the console writes and every edge reads, the GeoLite2 expiry rule // (D1), attribution, and the request headers. No I/O lives here. package geoip import ( "encoding/hex" "encoding/json" "errors" "fmt" "strings" "time" ) const ( Prefix = "control/geoip/" PointerKey = Prefix + "current.json" FeedKey = Prefix + "feed.json" MaxBlobBytes = int64(1 << 30) MaxPointerBytes = 64 << 10 geoLiteTTL = 30 * 24 * time.Hour GeoLiteWarn = 21 * 24 * time.Hour StaleAfter = 45 * 24 * time.Hour ) // BlobKey is the content-addressed object for a database whose bytes hash // to sha (lower-case hex). func BlobKey(sha string) string { return Prefix + sha + ".mmdb" } // ErrUnknownVersion is a pointer this binary cannot read. Spec §3: treat it // as no pointer (GeoIP off), never as "keep what is loaded". var ErrUnknownVersion = errors.New("unknown geoip pointer version") // Pointer is control/geoip/current.json (spec §3). type Pointer struct { V int `json:"v"` SHA256 string `json:"sha256"` Key string `json:"key"` Size int64 `json:"size"` DatabaseType string `json:"database_type"` BuildEpoch int64 `json:"build_epoch"` NodeCount uint64 `json:"node_count"` Published time.Time `json:"published"` Origin string `json:"origin"` Edition string `json:"edition"` Expired bool `json:"expired,omitempty"` } // Build is BuildEpoch as a time. func (p Pointer) Build() time.Time { return time.Unix(p.BuildEpoch, 0).UTC() } // ParsePointer decodes and checks a pointer. An unknown version, a key that // is not this sha's blob key, or an impossible size is an error: an edge // never follows a pointer it cannot fully vouch for. func ParsePointer(raw []byte) (Pointer, error) { var p Pointer if err := json.Unmarshal(raw, &p); err != nil { return Pointer{}, fmt.Errorf("geoip pointer: %w", err) } if p.V != 1 { return Pointer{}, fmt.Errorf("geoip pointer: version %d: %w", p.V, ErrUnknownVersion) } if !IsSHA256Hex(p.SHA256) { return Pointer{}, errors.New("geoip pointer: sha256 is not 64 lower-case hex characters") } if p.Key != BlobKey(p.SHA256) { return Pointer{}, errors.New("geoip pointer: key is not the sha's blob key") } if p.Size <= 0 || p.Size > MaxBlobBytes { return Pointer{}, fmt.Errorf("geoip pointer: size %d out of range", p.Size) } return p, nil } // IsSHA256Hex is 64 lower-case hex characters. func IsSHA256Hex(s string) bool { if len(s) != 64 || strings.ToLower(s) != s { return false } _, err := hex.DecodeString(s) return err == nil } // ExpiresAt is D1: a GeoLite2 database expires 30 days after it was built. // ok is false for every other type, which never expires. func ExpiresAt(dbType string, build time.Time) (time.Time, bool) { if !strings.HasPrefix(dbType, "GeoLite2-") { return time.Time{}, false } return build.Add(geoLiteTTL), true } // Expired reports whether now is at or past ExpiresAt. func Expired(dbType string, build, now time.Time) bool { at, ok := ExpiresAt(dbType, build) return ok && !now.Before(at) } // Attribution is the credit a page showing the data must carry (spec §13). type Attribution struct{ Text, URL string } // AttributionFor names the attribution for a database type. ok is false // when none is known: the operator's own licence governs. func AttributionFor(dbType string) (Attribution, bool) { switch { case strings.HasPrefix(dbType, "DBIP-"): return Attribution{"IP Geolocation by DB-IP", "https://db-ip.com"}, true case strings.HasPrefix(dbType, "GeoLite2-"): return Attribution{"This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com", "https://www.maxmind.com"}, true } return Attribution{}, false } // LocalCurrentFile is the one mutable file in a box's local GeoIP directory // (spec §5.2): it names the selected generation, .mmdb beside it. // The edge writes it; the console's follower (spec §11.3) reads it. const LocalCurrentFile = "current" // LocalCurrent is LocalCurrentFile's content. An empty SHA256 means no // database is selected. type LocalCurrent struct { SHA256 string `json:"sha256,omitempty"` DatabaseType string `json:"database_type,omitempty"` BuildEpoch int64 `json:"build_epoch,omitempty"` } // ParseLocalCurrent decodes LocalCurrentFile, refusing a sha that is not // 64 lower-case hex characters (it becomes a path). func ParseLocalCurrent(raw []byte) (LocalCurrent, error) { var c LocalCurrent if err := json.Unmarshal(raw, &c); err != nil { return LocalCurrent{}, err } if c.SHA256 != "" && !IsSHA256Hex(c.SHA256) { return LocalCurrent{}, errors.New("geoip local current: bad sha") } return c, nil }