feat: initial Go/JS/Rust Origin check-in client SDKs
CI / rust (push) Successful in 3m6s
CI / go (push) Successful in 33s
CI / js (push) Successful in 34s

Standalone clients that resolve a machine unique ID (OS machine ID first,
falling back to a generated UUID persisted to disk), detect hostname/IP/
container/orchestrator, and register+re-ping origin.warky.dev's public
service-checkin endpoint on a daily interval. Ported from icy2's internal
origincheckin package but extended to the full check-in contract and made
dependency-light for external reuse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 15:25:30 +02:00
co-authored by Claude Sonnet 5
parent 282f76a021
commit 2590e404f5
40 changed files with 4105 additions and 181 deletions
+213
View File
@@ -0,0 +1,213 @@
// Package originclient registers a running service with Origin
// (origin.warky.dev)'s public check-in endpoint and re-pings it on an
// interval, so Origin can track which instances of a service are alive,
// their version, and where they run. A check-in failure is logged and
// otherwise ignored: it must never affect the host application's own
// availability.
package originclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
const (
// DefaultURL is Origin's public service-checkin endpoint.
DefaultURL = "https://origin.warky.dev/api/public/service-checkin"
// DefaultIntervalHours is how often Run repeats when Config.IntervalHours is unset.
DefaultIntervalHours = 24
requestTimeout = 10 * time.Second
)
// Config configures a Client. ServiceKey, Type, Version, and Name identify
// the caller to Origin and are required; everything else is optional
// metadata, or overrides for values Client would otherwise auto-detect.
type Config struct {
// URL is the check-in endpoint. Defaults to DefaultURL.
URL string
// ServiceKey is sent as the X-Origin-Service-Key header. Required.
ServiceKey string
// Type is the Origin service type, which must already be registered in
// Origin (e.g. "icy2"). Required.
Type string
// Version is the caller's own version string. Required.
Version string
// Name identifies this deployment/instance to Origin, e.g. "icy2-nova". Required.
Name string
Site string
Environment string
DatabaseType string
DatabaseName string
// AppType defaults to "go" when empty.
AppType string
// Hostname defaults to the OS hostname (see Hostname) when empty.
Hostname string
Port int
BaseURL string
InternalURL string
// ContainerID defaults to best-effort Docker/cgroup detection (see
// ContainerID) when empty.
ContainerID string
// Orchestrator defaults to best-effort Kubernetes/Docker detection (see
// Orchestrator) when empty.
Orchestrator string
Description string
// InstallID, if set, overrides machine-ID resolution entirely.
InstallID string
// InstallIDPath is where a generated fallback install ID is persisted
// when no real OS machine ID is available. Defaults to the OS
// user-config directory (see ResolveMachineID).
InstallIDPath string
// IntervalHours sets how often Run repeats. Defaults to DefaultIntervalHours.
IntervalHours int
// Logger receives non-fatal warnings. Defaults to discarding them.
Logger Logger
}
// Client checks a service in with Origin.
type Client struct {
cfg Config
http *http.Client
installID string
}
// New validates cfg, resolves the machine/install ID, and returns a ready
// Client. Resolution happens once, at construction, so every check-in made
// by this Client reports the same UniqueInstallID.
func New(cfg Config) (*Client, error) {
if cfg.ServiceKey == "" || cfg.Type == "" || cfg.Version == "" || cfg.Name == "" {
return nil, errors.New("originclient: ServiceKey, Type, Version, and Name are required")
}
if cfg.URL == "" {
cfg.URL = DefaultURL
}
if cfg.AppType == "" {
cfg.AppType = "go"
}
if cfg.IntervalHours <= 0 {
cfg.IntervalHours = DefaultIntervalHours
}
if cfg.Logger == nil {
cfg.Logger = noopLogger{}
}
return &Client{
cfg: cfg,
http: &http.Client{Timeout: requestTimeout},
installID: ResolveMachineID(cfg.InstallID, cfg.InstallIDPath, cfg.Logger),
}, nil
}
// Run performs an immediate check-in, then repeats every
// Config.IntervalHours until ctx is done. Run blocks the calling goroutine,
// so callers invoke it with `go client.Run(ctx)`. Check-in errors are
// logged via Config.Logger and otherwise ignored; call CheckinOnce directly
// if the caller needs to observe failures.
func (c *Client) Run(ctx context.Context) {
_, _ = c.CheckinOnce(ctx)
ticker := time.NewTicker(time.Duration(c.cfg.IntervalHours) * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
_, _ = c.CheckinOnce(ctx)
}
}
}
// CheckinOnce sends a single check-in request and returns Origin's
// response. Failures are logged via Config.Logger and also returned, so
// callers that only want the fire-and-forget behavior of Run can ignore
// the returned error.
func (c *Client) CheckinOnce(ctx context.Context) (*Response, error) {
payload := c.buildPayload()
body, err := json.Marshal(payload)
if err != nil {
c.cfg.Logger.Warn("origin checkin: encode payload failed", err)
return nil, fmt.Errorf("originclient: encode payload: %w", err)
}
reqCtx, cancel := context.WithTimeout(ctx, requestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.cfg.URL, bytes.NewReader(body))
if err != nil {
c.cfg.Logger.Warn("origin checkin: build request failed", err)
return nil, fmt.Errorf("originclient: build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Origin-Service-Key", c.cfg.ServiceKey)
resp, err := c.http.Do(req)
if err != nil {
c.cfg.Logger.Warn("origin checkin: request failed", err)
return nil, fmt.Errorf("originclient: do request: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 300 {
err := fmt.Errorf("originclient: non-2xx response: %d", resp.StatusCode)
c.cfg.Logger.Warn("origin checkin: non-2xx response", err)
return nil, err
}
var result Response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
c.cfg.Logger.Warn("origin checkin: decode response failed", err)
return nil, fmt.Errorf("originclient: decode response: %w", err)
}
return &result, nil
}
func (c *Client) buildPayload() Payload {
hostname := c.cfg.Hostname
if hostname == "" {
hostname = Hostname()
}
containerID := c.cfg.ContainerID
if containerID == "" {
containerID = ContainerID()
}
orchestrator := c.cfg.Orchestrator
if orchestrator == "" {
orchestrator = Orchestrator()
}
return Payload{
Type: c.cfg.Type,
Version: c.cfg.Version,
Name: c.cfg.Name,
UniqueInstallID: c.installID,
Site: c.cfg.Site,
Environment: c.cfg.Environment,
DatabaseType: c.cfg.DatabaseType,
DatabaseName: c.cfg.DatabaseName,
AppType: c.cfg.AppType,
Hostname: hostname,
Port: c.cfg.Port,
BaseURL: c.cfg.BaseURL,
InternalURL: c.cfg.InternalURL,
ContainerID: containerID,
Orchestrator: orchestrator,
Description: c.cfg.Description,
}
}