mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
* chore(notice): pin PocketBase provenance and deviations; sweep product branding (#48) * feat(waitlist): unify position on points; boost awards through the seam Reconcile the points->position engine with #43's boost + waitlisted-access gate onto one mechanic: POINTS are the single position number. An entry's rank is its competition rank by points DESC (tied entries share a rank, earlier joiners break ties); points move only through award(), the one dedup'd choke point (UNIQUE(entry, dedupKey)) that also feeds the append-only waitlist_events ledger, activity feed, and per-source breakdown. - schema: waitlist_entries carries points (indexed rank) + breakdown, and keeps #43's sticky accessGranted; new waitlist_events ledger. Upgrade-safe ensureSchema reconciles fields in place and still seeds DefaultSlugs. - access gate kept exactly as #43: grantsAccess/hasAccess over the Open and AccessCapacity knobs, sticky capacity grant, superuser/AdminSecret model. - /boost stays service-authed (superuser/AdminSecret, e.g. hanzod) but is reimplemented as a caller-amount award through award() — no parallel boost store. Empty dedupKey accumulates; a keyed boost is replay-safe. - /status returns points-derived rank + access; /neighborhood is the keyset view; /export includes points. /award stays the AwardSecret connector seam. - tests: #43 gate tests adapted to points (competition-rank ordering, sticky access, default slugs); points_integration + the 10M rankbench carried over, the bench gated behind WL_BENCH and capped under -short; new boost seam test. --------- Co-authored-by: Hanzo Dev <[email protected]>
98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
// Copyright (C) 2020-2026, Hanzo AI Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package waitlist
|
|
|
|
import (
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/hook"
|
|
"github.com/hanzoai/base/tools/router"
|
|
luxlog "github.com/luxfi/log"
|
|
)
|
|
|
|
// MustRegister installs the waitlist plugin on the given app and panics on
|
|
// error. Suitable for a Base process's startup wiring.
|
|
func MustRegister(app core.App, cfg Config) {
|
|
if err := Register(app, cfg); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Register installs the points-based waitlist plugin.
|
|
//
|
|
// On OnBootstrap it auto-creates/upgrades three collections (`waitlists`,
|
|
// `waitlist_entries`, `waitlist_events`) and seeds any configured default
|
|
// waitlists. On OnServe it mounts the REST surface under /v1/waitlist. The
|
|
// plugin owns its collections (no public CRUD); the dashboard remains
|
|
// available to superusers.
|
|
func Register(app core.App, cfg Config) error {
|
|
if !cfg.Enabled {
|
|
return nil
|
|
}
|
|
cfg.resolve()
|
|
if err := cfg.validate(); err != nil {
|
|
return err
|
|
}
|
|
domains := cfg.DisposableDomains
|
|
if domains == nil {
|
|
domains = defaultDisposableDomains
|
|
}
|
|
|
|
p := &plugin{
|
|
app: app,
|
|
config: cfg,
|
|
logger: luxlog.New("component", "waitlist"),
|
|
limiter: newSlidingLimiter(cfg.JoinRateLimit, cfg.JoinRateWindow),
|
|
turnstile: newTurnstileVerifier(cfg.TurnstileSecret),
|
|
disposable: newDomainSet(domains),
|
|
}
|
|
|
|
app.OnBootstrap().Bind(&hook.Handler[*core.BootstrapEvent]{
|
|
Id: "__waitlistBootstrap__",
|
|
Func: func(e *core.BootstrapEvent) error {
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
return p.ensureSchema()
|
|
},
|
|
})
|
|
|
|
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
|
|
Id: "__waitlistServe__",
|
|
Func: func(e *core.ServeEvent) error {
|
|
app.Store().Set("waitlist", p)
|
|
p.registerRoutes(e.Router)
|
|
return e.Next()
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
type plugin struct {
|
|
app core.App
|
|
config Config
|
|
logger luxlog.Logger
|
|
limiter *slidingLimiter
|
|
turnstile *turnstileVerifier
|
|
disposable map[string]struct{}
|
|
}
|
|
|
|
func (p *plugin) registerRoutes(r *router.Router[*core.RequestEvent]) {
|
|
g := r.Group("/v1/waitlist")
|
|
// public
|
|
g.POST("/join", p.handleJoin)
|
|
g.GET("/status", p.handleStatus)
|
|
g.GET("/neighborhood", p.handleNeighborhood)
|
|
g.GET("/list", p.handleList)
|
|
g.GET("/activity", p.handleActivity)
|
|
g.POST("/track-share", p.handleTrackShare)
|
|
g.POST("/invite", p.handleInvite)
|
|
// service-authed (superuser / AdminSecret) — the caller-amount boost seam
|
|
g.POST("/boost", p.handleBoost)
|
|
// server-to-server (AwardSecret) — the verified-event award seam
|
|
g.POST("/award", p.handleAward)
|
|
// admin
|
|
g.GET("/export", p.handleExport)
|
|
}
|