geoip: paid MaxMind feeds (geoip2-city, geoip2-country); spec rev 13 records Paul's rulings R7-R9 (D1, D2 accepted; free DB-IP by default, paid MaxMind by choice)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+56
−7
docs/superpowers/specs/2026-10-06-platform-geoip-design.md+29 −5internal/console/geoipfeed.go+1 −1internal/geoip/archive_test.go+20 −1internal/geoip/feed.go+6 −0
CI cancelled — superseded by a newer push — run details
diff --git a/docs/superpowers/specs/2026-10-06-platform-geoip-design.md b/docs/superpowers/specs/2026-10-06-platform-geoip-design.md| index 45501503a..1ab5f75fc 100644 |
| --- a/docs/superpowers/specs/2026-10-06-platform-geoip-design.md |
| +++ b/docs/superpowers/specs/2026-10-06-platform-geoip-design.md |
| @@ -1,7 +1,7 @@ |
| # Platform GeoIP for apps: headers, a mirrored database, a lookup endpoint |
| **Date:** 2026-10-06 |
| -**Revision:** 12 |
| +**Revision:** 13 |
| **Discussion:** amadan carlos/platform #241 (quoted in Appendix A) |
| **Branch:** `geoip-for-apps` |
| @@ -84,7 +84,12 @@ |
| clock), so expiry never clears an alert while the bucket is still |
| unmarked. |
| - **Astra's round 12 (`CODEX-FINDINGS-GEOIP-R12.md`) accepted rev 12 with no |
| - findings.** The spec awaits Paul's sign-off on D1 and D2. |
| + findings.** |
| +- **Rev 13 records Paul's rulings of 2026-10-07:** |
| + - he accepts D1 and D2 (now R7 and R8); |
| + - the free DB-IP Lite is the default, and operators (the flagship |
| + included) may upgrade to paid MaxMind through two more feeds, |
| + `geoip2-city` and `geoip2-country` (R9). |
| ## 0. Rulings and decisions |
| @@ -158,6 +163,25 @@ reverse. |
| caches always matches what the app saw, whatever the transport, on |
| reconnect and on fleet fallback. |
| +- **R7, R8 (Paul, 2026-10-07):** D1 and D2 above are accepted as they |
| + stand. |
| +- **R9 (Paul, 2026-10-07): free by default, paid by choice.** |
| + - The documented default feed is `dbip-city-lite`. DB-IP Lite is free, |
| + CC BY 4.0, and allows commercial use with the attribution the |
| + platform already shows. |
| + - Operators, the flagship included, may upgrade to MaxMind's paid |
| + databases with `geoip2-city` or `geoip2-country`. These use the same |
| + download API and credentials as GeoLite2. |
| + - Paid editions are not GeoLite2, so D1's expiry does not apply, and |
| + the platform shows no attribution for them: the operator's MaxMind |
| + licence governs. |
| + - The feed stays off until `CONSOLE_GEOIP_FEED` is set, so no |
| + deployment starts downloading, or takes on a licence, without its |
| + operator choosing to. |
| + - GeoLite2 remains supported, but the operator docs note that MaxMind |
| + calls it not recommended for commercial use and that its EULA limits |
| + redistribution, which serving answers to customer apps may engage. |
| + |
| ## 1. Why, and what this is not |
| The nearest-region work gave the console a GeoIP reader (`internal/geo`, |
| @@ -951,9 +975,9 @@ process per deployment, so there is only ever one fetcher. |
| | Variable | Meaning | |
| |---|---| |
| -| `CONSOLE_GEOIP_FEED` | `dbip-city-lite`, `dbip-country-lite`, `geolite2-city`, `geolite2-country`; unset means no feed | |
| -| `CONSOLE_GEOIP_MAXMIND_ACCOUNT` | MaxMind account ID, for `geolite2-*` | |
| -| `CONSOLE_GEOIP_MAXMIND_KEY` | MaxMind license key, for `geolite2-*` | |
| +| `CONSOLE_GEOIP_FEED` | `dbip-city-lite` (the documented default, R9), `dbip-country-lite`, `geolite2-city`, `geolite2-country`, and the paid `geoip2-city` and `geoip2-country`; unset means no feed | |
| +| `CONSOLE_GEOIP_MAXMIND_ACCOUNT` | MaxMind account ID, for `geolite2-*` and `geoip2-*` | |
| +| `CONSOLE_GEOIP_MAXMIND_KEY` | MaxMind license key, for `geolite2-*` and `geoip2-*` | |
| - An unknown feed name, or a GeoLite2 feed with no credentials, is logged |
| once at startup. The feed stays off and the console still starts. |
diff --git a/internal/console/geoipfeed.go b/internal/console/geoipfeed.go| index 46e7eb493..e43b4c53f 100644 |
| --- a/internal/console/geoipfeed.go |
| +++ b/internal/console/geoipfeed.go |
| @@ -53,7 +53,7 @@ func newGeoipFeeder(cfg Config, pub *geoipPublisher) (*geoipFeeder, bool) { |
| } |
| spec, ok := geoip.ParseFeed(name) |
| if !ok { |
| - log.Printf("console: geoip feed misconfigured: CONSOLE_GEOIP_FEED %q is not one of dbip-city-lite, dbip-country-lite, geolite2-city, geolite2-country; the feed is off", name) |
| + log.Printf("console: geoip feed misconfigured: CONSOLE_GEOIP_FEED %q is not one of dbip-city-lite, dbip-country-lite, geolite2-city, geolite2-country, geoip2-city, geoip2-country; the feed is off", name) |
| return nil, false |
| } |
| if spec.Provider == "maxmind" && (cfg.GeoipMaxMindAccount == "" || cfg.GeoipMaxMindKey == "") { |
diff --git a/internal/geoip/archive_test.go b/internal/geoip/archive_test.go| index 030a3903d..768c8bcf9 100644 |
| --- a/internal/geoip/archive_test.go |
| +++ b/internal/geoip/archive_test.go |
| @@ -87,7 +87,7 @@ func TestFeedSpecsAndURLs(t *testing.T) { |
| if got := MaxMindURL("", g, "tar.gz.sha256"); got != "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" { |
| t.Fatalf("maxmind url = %s", got) |
| } |
| - if _, ok := ParseFeed("geoip2-city"); ok { |
| + if _, ok := ParseFeed("ipinfo-lite"); ok { |
| t.Fatal("unknown feed accepted") |
| } |
| } |
| @@ -109,3 +109,22 @@ func TestFeedStatusFailing(t *testing.T) { |
| t.Fatalf("parse = %+v %v", got, err) |
| } |
| } |
| + |
| +// The paid MaxMind upgrade (Paul, 2026-10-07): same API, no D1 expiry. |
| +func TestPaidMaxMindFeeds(t *testing.T) { |
| + for name, typ := range map[string]string{"geoip2-city": "GeoIP2-City", "geoip2-country": "GeoIP2-Country"} { |
| + s, ok := ParseFeed(name) |
| + if !ok || s.Type != typ || s.Provider != "maxmind" { |
| + t.Fatalf("%s = %+v %v", name, s, ok) |
| + } |
| + if got := MaxMindURL("", s, "tar.gz"); got != "https://download.maxmind.com/geoip/databases/"+typ+"/download?suffix=tar.gz" { |
| + t.Fatalf("%s url = %s", name, got) |
| + } |
| + if Expired(typ, time.Unix(0, 0), time.Now()) { |
| + t.Fatalf("%s must never expire under D1", typ) |
| + } |
| + if _, ok := AttributionFor(typ); ok { |
| + t.Fatalf("%s has no platform-known attribution", typ) |
| + } |
| + } |
| +} |
diff --git a/internal/geoip/feed.go b/internal/geoip/feed.go| index 9c176a006..f6d385581 100644 |
| --- a/internal/geoip/feed.go |
| +++ b/internal/geoip/feed.go |
| @@ -16,6 +16,12 @@ var feeds = map[string]FeedSpec{ |
| "dbip-country-lite": {"dbip-country-lite", "DBIP-Country-Lite", "dbip"}, |
| "geolite2-city": {"geolite2-city", "GeoLite2-City", "maxmind"}, |
| "geolite2-country": {"geolite2-country", "GeoLite2-Country", "maxmind"}, |
| + // The paid upgrade (Paul, 2026-10-07): MaxMind's commercial databases, |
| + // the same download API and credentials. Not GeoLite2, so no D1 expiry, |
| + // and no attribution the platform knows of: the operator's MaxMind |
| + // licence governs. |
| + "geoip2-city": {"geoip2-city", "GeoIP2-City", "maxmind"}, |
| + "geoip2-country": {"geoip2-country", "GeoIP2-Country", "maxmind"}, |
| } |
| // ParseFeed resolves CONSOLE_GEOIP_FEED. |