versioncheck

package
v0.1.85 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package versioncheck reports whether a newer GoModel release exists.

It reads a plain-text manifest on a daily schedule and on the first dashboard visit of each day. The request carries the running version, the distribution name, an anonymous install identifier, and the dashboard's own hostname; it never carries API keys, provider credentials, model names, prompts, or usage data.

Every outbound request is jittered so gateways started together — a Helm rollout, a restarted docker-compose stack — do not query in lockstep.

Index

Constants

View Source
const CookieMaxAge = 31536000

CookieMaxAge keeps a browser's id stable for a year.

View Source
const CookieName = "gomodel_version_check"

CookieName is the browser cookie holding the visit marker. Its value is YYYY-MM-DD-{id}: the day this browser last checked, plus a random id generated on the first visit. Dashboard JavaScript reads the date half to decide whether today's check has already happened, so the cookie is deliberately not HttpOnly.

View Source
const DefaultURL = "https://gomodel.enterpilot.io/version"

DefaultURL is the public release manifest served by the GoModel website. The channel file ("core.txt" or "pro.txt") is appended to it.

View Source
const InstallIDKey = "install_id"

InstallIDKey is the key the identifier is kept under in the deployment's database, alongside the runtime settings.

Variables

This section is empty.

Functions

func DueToday

func DueToday(value string, now time.Time) bool

DueToday reports whether a browser presenting this cookie value has yet to check in on the given day.

func IsNewer

func IsNewer(current, latest string) bool

IsNewer reports whether latest is a later release than current.

It is deliberately conservative: an unparseable or non-release local version ("dev", a bare commit) never reports an update, and build metadata is ignored, so a Pro build stamped "1.0.0+core.0.1.81" compares as 1.0.0. Within one release number a prerelease ranks below the final release ("1.0.0-rc1" < "1.0.0"), matching semantic versioning.

func LeaksQueryInCleartext

func LeaksQueryInCleartext(raw string) bool

LeaksQueryInCleartext reports whether a configured manifest URL would put its query string on the wire unencrypted. A private mirror may authenticate with a query token, and over plain HTTP that token is readable by anything on the path — redacting it from logs does not help there.

Reported rather than rejected: the URL is a deliberate operator choice, an internal mirror on a trusted network is a legitimate setup, and refusing to start the gateway over a non-essential update check would be a worse outcome than telling the operator what they have configured.

func NewVisit

func NewVisit(id string, now time.Time) string

NewVisit builds a cookie value for today, reusing id when the browser already has one and minting a fresh one otherwise.

The day is UTC on both sides of the cookie: the dashboard reads the same value back, and a browser in a different timezone to the gateway would otherwise disagree about when a new day starts.

func SafeURL

func SafeURL(raw string) string

safeURL identifies the manifest host for errors and logs without quoting anything an operator may have embedded in the configured URL. A private mirror can carry a secret in userinfo, in the query, or in the path itself, so only the scheme and host survive.

func SplitVisit

func SplitVisit(value string) (date, id string)

SplitVisit separates a cookie value into its date and id halves. An empty or malformed value yields empty strings, which callers treat as a first visit.

The id must be exactly the canonical UUID NewVisit generates. /version is unauthenticated and the id is echoed into an outbound request header and back into Set-Cookie, so an unvalidated one would let any caller put text of their choosing into both. Anything else is discarded and replaced with a fresh id, which is also the right answer for a cookie corrupted in transit.

Types

type Beacon

type Beacon struct {
	UserAgent      string
	AcceptLanguage string
	ClientHints    map[string]string
	Visit          string
}

Beacon carries the allowlisted slice of a dashboard visit that travels with an update check.

func BeaconFromRequest

func BeaconFromRequest(r *http.Request, visit string) Beacon

BeaconFromRequest extracts the allowlisted fields of a dashboard request.

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker owns the cached manifest result and the request budget.

func New

func New(cfg Config) *Checker

New builds a Checker for the given distribution. It never returns an error: a disabled or misconfigured check degrades to reporting the local version.

func (*Checker) Enabled

func (c *Checker) Enabled() bool

Enabled reports whether outbound checks are configured.

func (*Checker) Refresh

func (c *Checker) Refresh(ctx context.Context, beacon Beacon) (Status, error)

Refresh fetches the manifest and updates the cache. It returns the current status even when the request is throttled, out of budget, or fails, so callers can always answer with the local version.

func (*Checker) Run

func (c *Checker) Run(ctx context.Context)

Run performs the background schedule until ctx is cancelled. The first check waits a random slice of the interval (capped at ten minutes) so a fleet restarting together spreads its requests out.

func (*Checker) Status

func (c *Checker) Status() Status

Status returns the cached result without touching the network.

type Config

type Config struct {
	Enabled   bool
	URL       string
	App       string
	Version   string
	InstallID string
	// InstallIDFunc, when set, supplies the identifier per request instead
	// of InstallID. An Identity uses it to keep retrying its database until
	// it has confirmed which id this deployment has.
	InstallIDFunc  func(context.Context) string
	Interval       time.Duration
	Timeout        time.Duration
	MaxDailyChecks int

	// Client overrides the HTTP client, for tests.
	Client *http.Client
	// Now overrides the clock, for tests.
	Now func() time.Time
}

Config configures a Checker. Zero values fall back to package defaults.

type Identity added in v0.1.85

type Identity struct {
	// contains filtered or unexported fields
}

Identity resolves and remembers the stable, anonymous identifier for this deployment. It is a UUID that encodes nothing about the host, the operator, or the configuration, and only ever leaves the process on an update check.

"This deployment" is defined by whatever survives longest, in this order:

  1. The database, when a store is given. A gateway's database outlives its container (it is on a volume or on another host), so the identifier lives there first, and replicas sharing one database count as one.
  2. The install-id file in the data directory. The canonical location before the database was used; an existing id migrates to the database unchanged, so upgrading never creates a new deployment.
  3. An HMAC of the operator's secret, when one is configured. Nothing on disk survived (a container recreated without a volume) but the configuration did, and the same configuration is the same deployment.
  4. A random UUID.

Whichever step wins is written to the database and the file, so the copies converge and the next start takes the shortest path. The database write is insert-if-absent: when two replicas initialise at once, or the file and the database disagree, the database's value wins for everyone. The file is the copy that gets recreated by accident, the database the one that gets migrated on purpose.

A store that errors is not a store that is empty. The candidate from the file or the fallbacks is used for now, and the database is asked again on the next call, so an outage at startup can never mint a new identity or hide the real one for the life of the process.

func NewIdentity added in v0.1.85

func NewIdentity(store Store, secret string) *Identity

NewIdentity prepares a resolver. store may be nil (no database) and secret may be empty (no derived fallback). Nothing is read until Resolve or ID.

func (*Identity) ID added in v0.1.85

func (i *Identity) ID(ctx context.Context) string

ID returns the identifier for a request, resolving it on first use.

func (*Identity) Resolve added in v0.1.85

func (i *Identity) Resolve(ctx context.Context) (string, InstallIDSource)

Resolve returns the identifier and where it came from. Once the database has confirmed the value (or there is no database to ask) the answer is fixed; until then each call asks again, but keeps returning the same provisional value rather than minting another.

type InstallIDSource added in v0.1.85

type InstallIDSource string

InstallIDSource names where a resolved identifier came from, for the startup log.

const (
	// SourceDatabase: read from the deployment's database.
	SourceDatabase InstallIDSource = "database"
	// SourceFile: read from the install-id file in the data directory.
	SourceFile InstallIDSource = "file"
	// SourceDerived: computed from the operator's secret because nothing was
	// stored; stable as long as the secret is.
	SourceDerived InstallIDSource = "derived"
	// SourceGenerated: freshly minted; nothing durable was available.
	SourceGenerated InstallIDSource = "generated"
)

type Status

type Status struct {
	App             string `json:"app"`
	Version         string `json:"version"`
	Latest          string `json:"latest,omitempty"`
	UpdateAvailable bool   `json:"update_available"`
	CheckedAt       string `json:"checked_at,omitempty"`
	Enabled         bool   `json:"enabled"`
}

Status is the snapshot the /version endpoint serves. It is safe to expose: it contains only release metadata.

type Store added in v0.1.85

type Store interface {
	Get(ctx context.Context, key string) (value string, found bool, err error)
	// SetDefault stores value only when key has no value yet, and returns
	// whatever is stored afterwards. It must be atomic across instances.
	SetDefault(ctx context.Context, key, value string) (string, error)
}

Store is the durable key/value the identifier is kept in. It is satisfied by runtimesettings.Store: the identifier is per-deployment state exactly like a runtime setting, so it shares the table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL