carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package geoip is the platform's GeoIP vocabulary (spec
2// docs/superpowers/specs/2026-10-06-platform-geoip-design.md): the bucket
3// records the console writes and every edge reads, the GeoLite2 expiry rule
4// (D1), attribution, and the request headers. No I/O lives here.
5package geoip
6
7import (
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "strings"
13 "time"
14)
15
16const (
17 Prefix = "control/geoip/"
18 PointerKey = Prefix + "current.json"
19 FeedKey = Prefix + "feed.json"
20 MaxBlobBytes = int64(1 << 30)
21 MaxPointerBytes = 64 << 10
22
23 geoLiteTTL = 30 * 24 * time.Hour
24 GeoLiteWarn = 21 * 24 * time.Hour
25 StaleAfter = 45 * 24 * time.Hour
26)
27
28// BlobKey is the content-addressed object for a database whose bytes hash
29// to sha (lower-case hex).
30func BlobKey(sha string) string { return Prefix + sha + ".mmdb" }
31
32// ErrUnknownVersion is a pointer this binary cannot read. Spec §3: treat it
33// as no pointer (GeoIP off), never as "keep what is loaded".
34var ErrUnknownVersion = errors.New("unknown geoip pointer version")
35
36// Pointer is control/geoip/current.json (spec §3).
37type Pointer struct {
38 V int `json:"v"`
39 SHA256 string `json:"sha256"`
40 Key string `json:"key"`
41 Size int64 `json:"size"`
42 DatabaseType string `json:"database_type"`
43 BuildEpoch int64 `json:"build_epoch"`
44 NodeCount uint64 `json:"node_count"`
45 Published time.Time `json:"published"`
46 Origin string `json:"origin"`
47 Edition string `json:"edition"`
48 Expired bool `json:"expired,omitempty"`
49}
50
51// Build is BuildEpoch as a time.
52func (p Pointer) Build() time.Time { return time.Unix(p.BuildEpoch, 0).UTC() }
53
54// ParsePointer decodes and checks a pointer. An unknown version, a key that
55// is not this sha's blob key, or an impossible size is an error: an edge
56// never follows a pointer it cannot fully vouch for.
57func ParsePointer(raw []byte) (Pointer, error) {
58 var p Pointer
59 if err := json.Unmarshal(raw, &p); err != nil {
60 return Pointer{}, fmt.Errorf("geoip pointer: %w", err)
61 }
62 if p.V != 1 {
63 return Pointer{}, fmt.Errorf("geoip pointer: version %d: %w", p.V, ErrUnknownVersion)
64 }
65 if !IsSHA256Hex(p.SHA256) {
66 return Pointer{}, errors.New("geoip pointer: sha256 is not 64 lower-case hex characters")
67 }
68 if p.Key != BlobKey(p.SHA256) {
69 return Pointer{}, errors.New("geoip pointer: key is not the sha's blob key")
70 }
71 if p.Size <= 0 || p.Size > MaxBlobBytes {
72 return Pointer{}, fmt.Errorf("geoip pointer: size %d out of range", p.Size)
73 }
74 return p, nil
75}
76
77// IsSHA256Hex is 64 lower-case hex characters.
78func IsSHA256Hex(s string) bool {
79 if len(s) != 64 || strings.ToLower(s) != s {
80 return false
81 }
82 _, err := hex.DecodeString(s)
83 return err == nil
84}
85
86// ExpiresAt is D1: a GeoLite2 database expires 30 days after it was built.
87// ok is false for every other type, which never expires.
88func ExpiresAt(dbType string, build time.Time) (time.Time, bool) {
89 if !strings.HasPrefix(dbType, "GeoLite2-") {
90 return time.Time{}, false
91 }
92 return build.Add(geoLiteTTL), true
93}
94
95// Expired reports whether now is at or past ExpiresAt.
96func Expired(dbType string, build, now time.Time) bool {
97 at, ok := ExpiresAt(dbType, build)
98 return ok && !now.Before(at)
99}
100
101// Attribution is the credit a page showing the data must carry (spec §13).
102type Attribution struct{ Text, URL string }
103
104// AttributionFor names the attribution for a database type. ok is false
105// when none is known: the operator's own licence governs.
106func AttributionFor(dbType string) (Attribution, bool) {
107 switch {
108 case strings.HasPrefix(dbType, "DBIP-"):
109 return Attribution{"IP Geolocation by DB-IP", "https://db-ip.com"}, true
110 case strings.HasPrefix(dbType, "GeoLite2-"):
111 return Attribution{"This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com", "https://www.maxmind.com"}, true
112 }
113 return Attribution{}, false
114}
115
116// LocalCurrentFile is the one mutable file in a box's local GeoIP directory
117// (spec §5.2): it names the selected generation, <sha256>.mmdb beside it.
118// The edge writes it; the console's follower (spec §11.3) reads it.
119const LocalCurrentFile = "current"
120
121// LocalCurrent is LocalCurrentFile's content. An empty SHA256 means no
122// database is selected.
123type LocalCurrent struct {
124 SHA256 string `json:"sha256,omitempty"`
125 DatabaseType string `json:"database_type,omitempty"`
126 BuildEpoch int64 `json:"build_epoch,omitempty"`
127}
128
129// ParseLocalCurrent decodes LocalCurrentFile, refusing a sha that is not
130// 64 lower-case hex characters (it becomes a path).
131func ParseLocalCurrent(raw []byte) (LocalCurrent, error) {
132 var c LocalCurrent
133 if err := json.Unmarshal(raw, &c); err != nil {
134 return LocalCurrent{}, err
135 }
136 if c.SHA256 != "" && !IsSHA256Hex(c.SHA256) {
137 return LocalCurrent{}, errors.New("geoip local current: bad sha")
138 }
139 return c, nil
140}
141