eleven / messenger Public

server: enterprise-edition checkout — public Stripe mint for /enterprise/ (#1174)

The Eleven Enterprise Edition page (elevenmessenger.com/enterprise/) gets
a working till: POST /api/enterprise/checkout on the cloud service mints
a Stripe Checkout Session for year one — base €979,000 (€1,100,000 minus
the €121,000 only-for-you offer), €11 × seats, and the €1 intern when the
blessed 1010.09 headcount asks for one. Amounts are compiled in; the
client sends only seats/intern/currency (eur·usd·gbp, same numerals — no
exchange rates).

Unlike the caller-token billing routes this one is public (the static
marketing site's browser JS calls it), so: CORS pinned to the marketing
origin, fixed server-side redirect URLs, a global mint rate limit, and a
seat cap at 1,909 — Stripe's 8-digit per-charge maximum, which the
only-for-you offer is what squeezes the invoice under.

Its key is LCHAT_STRIPE_ENTERPRISE_KEY, separate from the live billing
key with no fallback: unset = feature off (501), sandbox first, and the
live flip is an explicit operator swap of that one value. docs/secrets.md
gains the entry.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 932bfe15b331c5b0f464b0aa72948b383afd9a88 parent d877c80
4 files changed, +444 −0
  • cloud.go +17 −0
  • docs/secrets.md +8 −0
  • enterprise.go +246 −0
  • enterprise_test.go +173 −0
diff --git a/cloud.go b/cloud.go
index 2787224..c9c22a0 100644
--- a/cloud.go
+++ b/cloud.go
@@ -102,6 +102,10 @@ type cloudServer struct {
reapDryRun bool // log what the reaper would reclaim, delete nothing
reapMax int // max reclaims per sweep (blast-radius cap; 0 = unlimited)
billing *stripeBilling // optional space upgrades (billing.go); nil = not offered
+ // enterprise is the Enterprise Edition page's public checkout
+ // (enterprise.go) — its own Stripe key, deliberately separate from
+ // billing's so it can run against the sandbox; nil = off.
+ enterprise *enterpriseCheckout
storageCaps map[string]int64 // tier key (+ "free") → storage cap pushed to instances; empty = no enforcement
// replicationClasses maps tier keys (+ "free") to durability-ladder classes
// (cloudreplication.go); empty = classes never pushed, every space streams.
@@ -138,6 +142,11 @@ func (s *cloudServer) handler() http.Handler {
mux.HandleFunc("GET /api/instances/{host}/demo-status", s.callerAuth(s.demoStatusHandler))
mux.HandleFunc("POST /api/instances/{host}/portal", s.callerAuth(s.portalHandler))
mux.HandleFunc("POST /api/stripe/webhook", s.stripeWebhookHandler)
+ // The Enterprise Edition page's checkout (enterprise.go): public but
+ // CORS-scoped to the marketing origin — the static site's browser JS is
+ // the caller, so there is no token to gate with; amounts are compiled in.
+ mux.HandleFunc("POST /api/enterprise/checkout", s.enterpriseCheckoutHandler)
+ mux.HandleFunc("OPTIONS /api/enterprise/checkout", s.enterprisePreflightHandler)
// Usage ingest (#857): meter-token-gated, not caller-gated — an instance
// can only ever report as itself.
mux.HandleFunc("POST /api/usage", s.usageHandler)
@@ -635,6 +644,13 @@ func runCloud(args []string) {
// rollout (every live Price must resolve a tax_behavior first).
billing.automaticTax = os.Getenv("LCHAT_STRIPE_AUTOMATIC_TAX") == "1"
}
+ // The Enterprise Edition page's checkout: off until its own key is set
+ // (sandbox first — the live flip is swapping that one value; enterprise.go).
+ enterprise := newEnterpriseCheckout(
+ os.Getenv("LCHAT_STRIPE_ENTERPRISE_KEY"),
+ envOr("LCHAT_STRIPE_API_BASE", "https://api.stripe.com"),
+ envOr("LCHAT_ENTERPRISE_ORIGIN", "https://elevenmessenger.com"),
+ )
poolCosts, err := parsePoolCosts(os.Getenv("LCHAT_POOL_COSTS"))
if err != nil {
die("cloud: LCHAT_POOL_COSTS: %v", err)
@@ -653,6 +669,7 @@ func runCloud(args []string) {
reapDryRun: *reapDryRun,
reapMax: *reapMax,
billing: billing,
+ enterprise: enterprise,
storageCaps: storageCaps,
replicationClasses: replicationClasses,
createBudget: *createBudget,
diff --git a/docs/secrets.md b/docs/secrets.md
index 94f350b..0b52436 100644
--- a/docs/secrets.md
+++ b/docs/secrets.md
@@ -162,6 +162,14 @@ private — is `docs/secrets-vault.md`.
connected accounts (sellers' Stripe accounts) within the platform's
permissions; the webhook secret forges membership entitlements on that
instance. *Rotate*: same shapes as the cloud pair above.
+- **Enterprise Edition checkout key** (`LCHAT_STRIPE_ENTERPRISE_KEY`) — the
+ cloud env's *separate* Stripe key for the elevenmessenger.com/enterprise/
+ page (enterprise.go). Deliberately independent of
+ `LCHAT_STRIPE_SECRET_KEY` with no fallback: it holds the **sandbox** key
+ while real billing stays live, and the live flip is swapping this one
+ value. *Blast radius*: whatever account the key belongs to — sandbox ≈
+ none; if flipped live, same as the live key above. *Rotate*: roll in the
+ matching Stripe mode's dashboard, swap the cloud env, restart.
- Kept alongside but **not secrets**: `LCHAT_STRIPE_PRICES` (the price book —
price ids are public by the time Checkout renders) and
`LCHAT_STRIPE_PORTAL_CONFIG` (a portal configuration id).
diff --git a/enterprise.go b/enterprise.go
new file mode 100644
index 0000000..f4b279e
--- /dev/null
+++ b/enterprise.go
@@ -0,0 +1,246 @@
+package main
+
+// 🤖 enterprise.go — checkout for the Eleven Enterprise Edition page
+// (elevenmessenger.com/enterprise/): the satirical-but-real €1,100,000/year
+// invoice. A public, CORS-scoped endpoint that mints a Stripe Checkout
+// Session for year one; the page redirects the buyer to Stripe's hosted
+// payment page and celebrates on return.
+//
+// Deliberate shape:
+// - Its Stripe key is LCHAT_STRIPE_ENTERPRISE_KEY, separate from the space
+// billing key and with NO fallback to it — so the page runs against the
+// sandbox while real billing stays live, and going live is an explicit
+// operator act (swapping that one value), never a config accident.
+// - The browser calls this directly (the marketing site is static GitHub
+// Pages with no backend), so unlike every other billing route this one is
+// public: CORS pinned to the marketing origin, fixed server-side redirect
+// URLs (the client picks nothing), and a global mint rate limit.
+// - Amounts are compiled in, not client-supplied: the client sends only a
+// seat count, the intern flag, and a currency. The numerals are identical
+// in every currency — the elevens are load-bearing, we don't do exchange
+// rates.
+// - mode=payment (year one, one invoice). The +11%/year escalator stays
+// contractual comedy until a subscription-schedule slice earns its keep.
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+// The price list, in minor units (cents/pence — EUR, USD and GBP all ×100).
+// Base €1,100,000 minus the €121,000 only-for-you offer (11% of base, 11²
+// thousand); €11 a seat; the intern — one eleventh of an employee — exactly 1.
+const (
+ enterpriseBaseMinor = (1_100_000 - 121_000) * 100
+ enterpriseSeatMinor = 11 * 100
+ enterpriseInternMinor = 1 * 100
+ // The intern is only ever the .09 of the blessed 1010.09 headcount.
+ enterpriseInternSeats = 1010
+ // Stripe caps a single payment at 8 digits of the minor unit
+ // (99,999,999 ≈ €999,999.99). The only-for-you offer is what carries the
+ // invoice under that cap at all; past this many seats even the offer
+ // can't save it, and Sales will call you.
+ enterpriseMaxSeats = (99_999_999 - enterpriseBaseMinor) / enterpriseSeatMinor
+)
+
+var enterpriseCurrencies = map[string]bool{"eur": true, "usd": true, "gbp": true}
+
+// enterpriseCheckout holds the page's Stripe config. nil on cloudServer =
+// the feature is off and the endpoint answers 501.
+type enterpriseCheckout struct {
+ key string // LCHAT_STRIPE_ENTERPRISE_KEY — sandbox until the deliberate live flip
+ apiBase string // https://api.stripe.com; tests point it at a fake
+ origin string // the marketing origin: CORS allow + redirect base
+ client *http.Client
+ limiter mintLimiter
+}
+
+func newEnterpriseCheckout(key, apiBase, origin string) *enterpriseCheckout {
+ key = strings.TrimSpace(key)
+ if key == "" {
+ return nil
+ }
+ return &enterpriseCheckout{
+ key: key,
+ apiBase: apiBase,
+ origin: strings.TrimRight(origin, "/"),
+ client: &http.Client{Timeout: 20 * time.Second},
+ limiter: mintLimiter{max: 30, window: time.Minute},
+ }
+}
+
+// mintLimiter is a global rolling-window cap on session mints — this endpoint
+// is public, and each hit costs a Stripe API call. Per-IP bookkeeping isn't
+// worth carrying for a page whose realistic traffic is one CFO at a time.
+type mintLimiter struct {
+ mu sync.Mutex
+ stamps []time.Time
+ max int
+ window time.Duration
+}
+
+func (l *mintLimiter) allow(now time.Time) bool {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ keep := l.stamps[:0]
+ for _, t := range l.stamps {
+ if now.Sub(t) < l.window {
+ keep = append(keep, t)
+ }
+ }
+ l.stamps = keep
+ if len(l.stamps) >= l.max {
+ return false
+ }
+ l.stamps = append(l.stamps, now)
+ return true
+}
+
+// enterpriseCORS stamps the CORS headers on every response from the endpoint.
+// The allow-origin is the single marketing origin, never *: the page is the
+// only legitimate caller, and scoping it keeps random sites from spending our
+// Stripe rate limit from their visitors' browsers.
+func (s *cloudServer) enterpriseCORS(w http.ResponseWriter) bool {
+ if s.enterprise == nil {
+ return false
+ }
+ h := w.Header()
+ h.Set("Access-Control-Allow-Origin", s.enterprise.origin)
+ h.Set("Vary", "Origin")
+ return true
+}
+
+// enterprisePreflightHandler: OPTIONS /api/enterprise/checkout — the CORS
+// preflight for the cross-origin JSON POST below.
+func (s *cloudServer) enterprisePreflightHandler(w http.ResponseWriter, r *http.Request) {
+ if !s.enterpriseCORS(w) {
+ w.WriteHeader(http.StatusNotImplemented)
+ return
+ }
+ h := w.Header()
+ h.Set("Access-Control-Allow-Methods", "POST")
+ h.Set("Access-Control-Allow-Headers", "Content-Type")
+ h.Set("Access-Control-Max-Age", "86400")
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// enterpriseCheckoutHandler: POST /api/enterprise/checkout — mint the year-one
+// Checkout Session. Body: {"seats": n, "intern": bool, "currency": "eur"}.
+func (s *cloudServer) enterpriseCheckoutHandler(w http.ResponseWriter, r *http.Request) {
+ if !s.enterpriseCORS(w) {
+ writeControlErr(w, http.StatusNotImplemented, "unavailable", "enterprise checkout is not configured")
+ return
+ }
+ e := s.enterprise
+ if !e.limiter.allow(time.Now()) {
+ writeControlErr(w, http.StatusTooManyRequests, "slow_down", "the till is busy — try again in a minute")
+ return
+ }
+ var req struct {
+ Seats int `json:"seats"`
+ Intern bool `json:"intern"`
+ Currency string `json:"currency"`
+ }
+ if err := json.NewDecoder(io.LimitReader(r.Body, 4<<10)).Decode(&req); err != nil {
+ writeControlErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
+ return
+ }
+ cur := strings.ToLower(strings.TrimSpace(req.Currency))
+ if !enterpriseCurrencies[cur] {
+ writeControlErr(w, http.StatusBadRequest, "bad_request", "currency must be eur, usd or gbp")
+ return
+ }
+ if req.Seats < 1 {
+ writeControlErr(w, http.StatusBadRequest, "bad_request", "at least one employee required")
+ return
+ }
+ // The intern is exactly the .09 of 1010.09 — any other fractional
+ // headcount was already rounded to a person by the page.
+ if req.Intern && req.Seats != enterpriseInternSeats {
+ writeControlErr(w, http.StatusBadRequest, "bad_request", "the intern only joins a 1,010-person team")
+ return
+ }
+ if req.Seats > enterpriseMaxSeats {
+ writeControlErr(w, http.StatusBadRequest, "over_stripe_cap",
+ fmt.Sprintf("Stripe cannot carry this much money in one payment — %s employees is the most it can hold. Sales will call you.",
+ formatCount(enterpriseMaxSeats)))
+ return
+ }
+ checkoutURL, err := e.createSession(cur, req.Seats, req.Intern)
+ if err != nil {
+ log.Printf("cloud: enterprise checkout (%s, %d seats, intern=%v): %v", cur, req.Seats, req.Intern, err)
+ writeControlErr(w, http.StatusBadGateway, "checkout_failed", "couldn't start the checkout — please try again")
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]any{"url": checkoutURL})
+}
+
+// createSession builds the hosted Checkout page: three price_data line items
+// (no dashboard Prices to manage), fixed redirect URLs on the marketing
+// origin, and no customer identity — Stripe collects what it needs.
+func (e *enterpriseCheckout) createSession(currency string, seats int, intern bool) (string, error) {
+ form := url.Values{}
+ form.Set("mode", "payment")
+ form.Set("line_items[0][price_data][currency]", currency)
+ form.Set("line_items[0][price_data][product_data][name]", "Eleven Enterprise Edition — year one (only-for-you offer applied)")
+ form.Set("line_items[0][price_data][unit_amount]", strconv.Itoa(enterpriseBaseMinor))
+ form.Set("line_items[0][quantity]", "1")
+ form.Set("line_items[1][price_data][currency]", currency)
+ form.Set("line_items[1][price_data][product_data][name]", "Seat — one employee, one year")
+ form.Set("line_items[1][price_data][unit_amount]", strconv.Itoa(enterpriseSeatMinor))
+ form.Set("line_items[1][quantity]", strconv.Itoa(seats))
+ if intern {
+ form.Set("line_items[2][price_data][currency]", currency)
+ form.Set("line_items[2][price_data][product_data][name]", "The intern (one eleventh of an employee)")
+ form.Set("line_items[2][price_data][unit_amount]", strconv.Itoa(enterpriseInternMinor))
+ form.Set("line_items[2][quantity]", "1")
+ }
+ form.Set("metadata[enterprise]", "1")
+ form.Set("metadata[seats]", strconv.Itoa(seats))
+ form.Set("metadata[intern]", strconv.FormatBool(intern))
+ form.Set("success_url", e.origin+"/enterprise/?paid=1")
+ form.Set("cancel_url", e.origin+"/enterprise/")
+
+ req, err := http.NewRequest("POST", e.apiBase+"/v1/checkout/sessions", strings.NewReader(form.Encode()))
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Authorization", "Bearer "+e.key)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ resp, err := e.client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ var body struct {
+ URL string `json:"url"`
+ Error struct {
+ Message string `json:"message"`
+ } `json:"error"`
+ }
+ if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
+ return "", fmt.Errorf("stripe: HTTP %d (unparseable body)", resp.StatusCode)
+ }
+ if resp.StatusCode != http.StatusOK || body.URL == "" {
+ return "", fmt.Errorf("stripe: HTTP %d: %s", resp.StatusCode, body.Error.Message)
+ }
+ return body.URL, nil
+}
+
+// formatCount renders a count with locale-style thousands separators for the
+// one English error message that carries a number ("1,909 employees…").
+func formatCount(n int) string {
+ s := strconv.Itoa(n)
+ for i := len(s) - 3; i > 0; i -= 3 {
+ s = s[:i] + "," + s[i:]
+ }
+ return s
+}
diff --git a/enterprise_test.go b/enterprise_test.go
new file mode 100644
index 0000000..d8ac7e0
--- /dev/null
+++ b/enterprise_test.go
@@ -0,0 +1,173 @@
+package main
+
+// 🤖 enterprise_test.go — the Enterprise Edition checkout (enterprise.go):
+// the mint path against a fake Stripe, the compiled-in amounts, the Stripe
+// 8-digit cap, the blessed-intern rule, and the CORS scoping that makes a
+// public billing endpoint tolerable.
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+)
+
+// enterpriseTestCloud is newTestCloud plus an enterpriseCheckout pointed at
+// the fake Stripe (reused from cloud_billing_test.go — same sessions path).
+func enterpriseTestCloud(t *testing.T) (*httptest.Server, *cloudServer, *url.Values) {
+ t.Helper()
+ stripe, captured := fakeStripe(t)
+ srv, s := newTestCloud(t, okControl(t))
+ s.enterprise = newEnterpriseCheckout("sk_test_enterprise", stripe.URL, "https://elevenmessenger.com")
+ return srv, s, captured
+}
+
+func postEnterprise(t *testing.T, srv *httptest.Server, body map[string]any) *http.Response {
+ t.Helper()
+ raw, _ := json.Marshal(body)
+ resp, err := http.Post(srv.URL+"/api/enterprise/checkout", "application/json", bytes.NewReader(raw))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { resp.Body.Close() })
+ return resp
+}
+
+func TestEnterpriseCheckoutMintsSession(t *testing.T) {
+ srv, _, captured := enterpriseTestCloud(t)
+
+ resp := postEnterprise(t, srv, map[string]any{"seats": 1010, "intern": true, "currency": "eur"})
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("status = %d, want 200", resp.StatusCode)
+ }
+ if u, _ := decodeBody(t, resp)["url"].(string); !strings.Contains(u, "checkout.stripe.com") {
+ t.Errorf("url = %q, want the hosted checkout page", u)
+ }
+ if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://elevenmessenger.com" {
+ t.Errorf("Access-Control-Allow-Origin = %q", got)
+ }
+
+ // The compiled-in invoice: €979,000 base (after the −€121,000 offer),
+ // €11 × 1,010 seats, and the €1 intern — €990,111, all in minor units,
+ // under Stripe's 8-digit per-charge cap.
+ f := *captured
+ if f.Get("mode") != "payment" {
+ t.Errorf("mode = %q, want payment", f.Get("mode"))
+ }
+ if f.Get("line_items[0][price_data][unit_amount]") != "97900000" ||
+ f.Get("line_items[1][price_data][unit_amount]") != "1100" ||
+ f.Get("line_items[1][quantity]") != "1010" ||
+ f.Get("line_items[2][price_data][unit_amount]") != "100" {
+ t.Errorf("amounts: %v", f)
+ }
+ for i := 0; i < 3; i++ {
+ if got := f.Get("line_items[" + string(rune('0'+i)) + "][price_data][currency]"); got != "eur" {
+ t.Errorf("line %d currency = %q, want eur", i, got)
+ }
+ }
+ // Redirects are ours, fixed server-side; the client picked nothing.
+ if !strings.HasPrefix(f.Get("success_url"), "https://elevenmessenger.com/enterprise/") ||
+ !strings.HasPrefix(f.Get("cancel_url"), "https://elevenmessenger.com/enterprise/") {
+ t.Errorf("redirects: success=%q cancel=%q", f.Get("success_url"), f.Get("cancel_url"))
+ }
+ for k := range f {
+ if strings.Contains(k, "customer") || strings.Contains(k, "email") {
+ t.Errorf("checkout form leaks a customer field: %s", k)
+ }
+ }
+}
+
+func TestEnterpriseCheckoutWithoutIntern(t *testing.T) {
+ srv, _, captured := enterpriseTestCloud(t)
+ if resp := postEnterprise(t, srv, map[string]any{"seats": 3, "currency": "usd"}); resp.StatusCode != http.StatusOK {
+ t.Fatalf("status = %d, want 200", resp.StatusCode)
+ }
+ f := *captured
+ if f.Get("line_items[1][quantity]") != "3" || f.Get("line_items[1][price_data][currency]") != "usd" {
+ t.Errorf("seat line: %v", f)
+ }
+ if f.Has("line_items[2][price_data][unit_amount]") {
+ t.Errorf("no intern was ordered, but one is on the invoice: %v", f)
+ }
+}
+
+func TestEnterpriseCheckoutValidation(t *testing.T) {
+ srv, _, _ := enterpriseTestCloud(t)
+ cases := []struct {
+ name string
+ body map[string]any
+ code string
+ }{
+ {"bad currency", map[string]any{"seats": 10, "currency": "chf"}, "bad_request"},
+ {"zero seats", map[string]any{"seats": 0, "currency": "eur"}, "bad_request"},
+ {"intern needs 1010", map[string]any{"seats": 11, "intern": true, "currency": "eur"}, "bad_request"},
+ // 1,910 seats pushes the invoice past Stripe's 99,999,999-minor-unit
+ // per-charge maximum; 1,909 (asserted below) is the last legal count.
+ {"over the Stripe cap", map[string]any{"seats": 1910, "currency": "eur"}, "over_stripe_cap"},
+ }
+ for _, tc := range cases {
+ resp := postEnterprise(t, srv, tc.body)
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("%s: status = %d, want 400", tc.name, resp.StatusCode)
+ continue
+ }
+ if got, _ := decodeBody(t, resp)["error"].(string); got != tc.code {
+ t.Errorf("%s: error = %q, want %q", tc.name, got, tc.code)
+ }
+ }
+ if enterpriseMaxSeats != 1909 {
+ t.Errorf("enterpriseMaxSeats = %d, want 1909 ((99,999,999 − 97,900,000) / 1,100)", enterpriseMaxSeats)
+ }
+ if resp := postEnterprise(t, srv, map[string]any{"seats": 1909, "currency": "gbp"}); resp.StatusCode != http.StatusOK {
+ t.Errorf("1,909 seats (the cap) = %d, want 200", resp.StatusCode)
+ }
+}
+
+func TestEnterpriseCheckoutUnconfigured(t *testing.T) {
+ srv, s, _ := enterpriseTestCloud(t)
+ s.enterprise = nil
+ if resp := postEnterprise(t, srv, map[string]any{"seats": 10, "currency": "eur"}); resp.StatusCode != http.StatusNotImplemented {
+ t.Errorf("status = %d, want 501", resp.StatusCode)
+ }
+ // And the empty-key constructor is how "off" happens in production.
+ if newEnterpriseCheckout("", "https://api.stripe.com", "https://elevenmessenger.com") != nil {
+ t.Error("empty key should disable enterprise checkout")
+ }
+}
+
+func TestEnterprisePreflight(t *testing.T) {
+ srv, _, _ := enterpriseTestCloud(t)
+ req, _ := http.NewRequest("OPTIONS", srv.URL+"/api/enterprise/checkout", nil)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusNoContent {
+ t.Fatalf("status = %d, want 204", resp.StatusCode)
+ }
+ h := resp.Header
+ if h.Get("Access-Control-Allow-Origin") != "https://elevenmessenger.com" ||
+ !strings.Contains(h.Get("Access-Control-Allow-Methods"), "POST") ||
+ !strings.Contains(strings.ToLower(h.Get("Access-Control-Allow-Headers")), "content-type") {
+ t.Errorf("preflight headers: %v", h)
+ }
+}
+
+func TestEnterpriseMintLimiter(t *testing.T) {
+ l := mintLimiter{max: 2, window: time.Minute}
+ now := time.Now()
+ if !l.allow(now) || !l.allow(now) {
+ t.Fatal("first two mints should pass")
+ }
+ if l.allow(now) {
+ t.Error("third mint inside the window should be refused")
+ }
+ if !l.allow(now.Add(2 * time.Minute)) {
+ t.Error("a fresh window should allow again")
+ }
+}