// Package plugins is the API Server's plugin system: a uniform contract for // integrating external third-party services (vehicle data, parts catalogs, // notifications, file storage, …). // // Two plugin kinds share one contract: // - "builtin" — a Go connector compiled into the server (type-safe, first-party). // Adding a new builtin requires a rebuild. See builtin/builtin.go. // - "external" — a remote service registered at runtime (no rebuild) that speaks // a small JSON contract over HTTP. See external.go. // // Enable-state and per-plugin config (including secrets) are persisted by the // Manager to PocketBase — the app_settings singleton, in the same pluginSettings // field the org and user layers of the cascade use. Nothing is kept on disk. See // doc.go for the deliberately-deferred extension points. package plugins import ( "context" "encoding/json" ) // Plugin kinds. const ( KindBuiltin = "builtin" KindExternal = "external" ) // AuthType describes how a plugin authenticates to its upstream. It is metadata // for the UI/operators; each plugin implements the mechanics itself. type AuthType string const ( AuthNone AuthType = "none" AuthAPIKey AuthType = "apikey" AuthBasic AuthType = "basic" AuthOAuth2 AuthType = "oauth2" AuthWebhook AuthType = "webhook" ) // Health status values. const ( StatusOK = "ok" StatusDegraded = "degraded" StatusDown = "down" ) // SelectOption is one choice for a ConfigField of Type "select" (pick one) or // "multiselect" (pick any number; stored as a comma-separated list of values). type SelectOption struct { Value string `json:"value"` Label string `json:"label"` } // ConfigField declares one configurable setting a plugin accepts. It drives the // panel's generated config form and controls secret masking. type ConfigField struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` // "text" | "password" | "number" | "select" | "multiselect" Required bool `json:"required"` Secret bool `json:"secret"` // never echoed back to clients in clear Help string `json:"help,omitempty"` Default string `json:"default,omitempty"` // effective default when unset Options []SelectOption `json:"options,omitempty"` // for Type "select" and "multiselect" } // Capability is one operation a plugin exposes. It maps a stable id to the // upstream endpoint it calls and a human description shown in the panel. type Capability struct { ID string `json:"id"` Method string `json:"method,omitempty"` // e.g. "GET" Endpoint string `json:"endpoint,omitempty"` // upstream path, e.g. "/states/all" Description string `json:"description,omitempty"` } // UnmarshalJSON accepts either a bare string ("states.all") or a full object, so // external manifests can advertise capabilities in either form. func (c *Capability) UnmarshalJSON(b []byte) error { var s string if json.Unmarshal(b, &s) == nil { c.ID = s return nil } type alias Capability var a alias if err := json.Unmarshal(b, &a); err != nil { return err } *c = Capability(a) return nil } // Category groups a plugin under a tab in the admin panel. A plugin with an // empty or unrecognised category is treated as CategoryAPIsExternal by the panel. const ( CategoryVehicles = "vehicles" // car manufacturers' connected-car services CategoryChargers = "chargers" // EV chargers and charging hardware CategoryNotifications = "notifications" // notification gateways and message delivery CategoryAPIsExternal = "apis-external" // remote HTTP APIs (external plugins) CategoryDrivesExternal = "drives-external" // remote file stores (FTP/SFTP) CategoryDrivesLocal = "drives-local" // drives on the host machine ) // Descriptor is the static metadata a plugin advertises about itself. type Descriptor struct { Name string `json:"name"` Provider string `json:"provider"` Version string `json:"version"` Kind string `json:"kind"` // KindBuiltin | KindExternal Category string `json:"category"` // one of Category* — groups the plugin in the panel Capabilities []Capability `json:"capabilities"` AuthType AuthType `json:"authType"` ConfigFields []ConfigField `json:"configFields"` } // Health is the outcome of a plugin's HealthCheck. type Health struct { Status string `json:"status"` // StatusOK | StatusDegraded | StatusDown LatencyMs int64 `json:"latencyMs,omitempty"` Detail string `json:"detail,omitempty"` Credits *HealthCredits `json:"credits,omitempty"` Usage *HealthUsage `json:"usage,omitempty"` } // HealthUsage is optional call-usage accounting a plugin may report when its // upstream does NOT expose remaining quota (e.g. OpenWeather). Unlike // HealthCredits — which reflects a balance the upstream reports — these are // process-local counts of the calls this server has made, bucketed into the // current minute and day, so the UI can render an approximate usage gauge. type HealthUsage struct { MinuteUsed int `json:"minuteUsed"` // calls made in the current minute MinuteLimit int `json:"minuteLimit,omitempty"` // the plan's per-minute limit DayUsed int `json:"dayUsed"` // calls made so far today (UTC) } // HealthCredits is optional structured rate-limit/credit accounting a plugin may // report alongside a probe, when its upstream exposes a remaining balance. It // lets the UI render a dedicated usage meter instead of parsing it back out of // Detail. type HealthCredits struct { Remaining *int `json:"remaining,omitempty"` // credits left today; nil when the upstream didn't report it (e.g. anonymous) Daily int `json:"daily,omitempty"` // the plan's daily allowance ProbeCost int `json:"probeCost,omitempty"` // credits one query/probe costs Mode string `json:"mode,omitempty"` // "authenticated" | "anonymous" } // Plugin is the contract every plugin (builtin or external) implements. type Plugin interface { // Descriptor returns the plugin's static metadata. It may be enriched after // Init (e.g. an external plugin fetching its manifest). Descriptor() Descriptor // Init prepares the plugin with its resolved config (secrets included). It is // called when the plugin is enabled or its config changes. Init(ctx context.Context, config map[string]string) error // HealthCheck probes the upstream and classifies the result. HealthCheck(ctx context.Context) Health // Invoke runs a named capability. Part of the contract for future use; v1 // exposes no HTTP endpoint for it. Invoke(ctx context.Context, action string, params json.RawMessage) (json.RawMessage, error) // Shutdown releases any resources held by the plugin. Shutdown(ctx context.Context) error } // Factory builds a fresh instance of a builtin plugin. type Factory func() Plugin // registry holds the builtin plugin factories keyed by descriptor name. var registry = map[string]Factory{} // Register adds a builtin plugin factory. Called from a builtin package's init(). // Panics on a duplicate name so wiring mistakes surface at startup. func Register(name string, f Factory) { if _, dup := registry[name]; dup { panic("plugins: duplicate registration for " + name) } registry[name] = f } // builtinFactories returns a copy of the registered builtin factories. func builtinFactories() map[string]Factory { out := make(map[string]Factory, len(registry)) for k, v := range registry { out[k] = v } return out }