api

package
v0.0.0-...-ccb4f2f Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 43 Imported by: 0

Documentation

Index

Constants

View Source
const (
	CodeUnauthenticated     = "UNAUTHENTICATED"
	CodeForbidden           = "FORBIDDEN"
	CodeNotFound            = "NOT_FOUND"
	CodeValidationFailed    = "VALIDATION_FAILED"
	CodeConflict            = "CONFLICT"
	CodeUnsupportedMedia    = "UNSUPPORTED_MEDIA_TYPE"
	CodePayloadTooLarge     = "PAYLOAD_TOO_LARGE"
	CodeRateLimited         = "RATE_LIMITED"
	CodeSignatureInvalid    = "SIGNATURE_INVALID"
	CodeHashMismatch        = "HASH_MISMATCH"
	CodeVersionNotMonotonic = "VERSION_NOT_MONOTONIC"
	CodeInternal            = "INTERNAL"
)

Stable machine-readable error codes (endpoints.md §6 enumerated set).

View Source
const (
	RoleAdmin      = "admin"
	RoleOperator   = "operator"
	RoleViewer     = "viewer"
	RoleDevice     = "device"
	RoleSuperAdmin = "super_admin"
)

RBAC roles (endpoints.md §4.2). RoleSuperAdmin is the global "sees/controls everything" identity flag per the Accounts design §3.4 — it bypasses the per-account tenant predicate. Set only via config/bootstrap, never a request.

View Source
const (
	EventRolloutStageChanged  = "rollout.stage_changed"
	EventDeploymentFailed     = "deployment.failed"
	EventDeploymentRolledBack = "deployment.rolled_back"
	EventHealthBreach         = "health.breach"
	EventSecurityTamper       = "security.tamper_detected"
)

Event type constants matching the closed set from webhook-payload-schema.md.

Variables

View Source
var TestDisableClaimAccountAccess bool
View Source
var TestDisableSelectAccountMembershipCheck bool

Functions

func LoggerFrom

func LoggerFrom(c *gin.Context) *slog.Logger

LoggerFrom returns the request-scoped *slog.Logger stored in the Gin context by StructuredLoggingMiddleware. If no logger is found (middleware not wired), the process-default logger is returned so callers never get a nil logger.

func NewJSONLogger

func NewJSONLogger(level slog.Level) *slog.Logger

NewJSONLogger creates a slog.JSONHandler writing to os.Stderr at the given level. This is the canonical structured-logging setup for the server entry point (main.go).

func NewStoreReadinessProbe

func NewStoreReadinessProbe(repo StoreReadinessChecker) health.ReadyFunc

NewStoreReadinessProbe builds the readiness probe wired into /readyz. It reports ready only when a cheap, bounded round-trip against the store succeeds (ListProjects over the small projects table). Any error — store unreachable, query failure, or the bounded deadline elapsing — reports NOT ready, so /readyz returns 503 and an orchestrator withholds traffic. This replaces the earlier probe that reported ready unconditionally regardless of real store health (SRV-NEW-2 / OTA-063).

func StructuredLoggingMiddleware

func StructuredLoggingMiddleware() gin.HandlerFunc

StructuredLoggingMiddleware returns a Gin middleware that creates a per-request *slog.Logger carrying the request-id (set by requestIDMiddleware, which MUST run before this middleware) and stores it in the Gin context.

Types

type AccountEntry

type AccountEntry struct {
	AccountID   string `json:"account_id"`
	AccountName string `json:"account_name"`
	Role        string `json:"role"`
}

AccountEntry is a single available account in the post-login account picker (Accounts M4, design §4.3). The SPA renders this list so the user can pick which account to scope their session to.

type AccountProjectResponse

type AccountProjectResponse struct {
	ProjectID   string    `json:"project_id"`
	AccountID   string    `json:"account_id"`
	Name        string    `json:"name"`
	Description string    `json:"description,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

AccountProjectResponse is the per-project response body (Accounts M2).

type AccountResponse

type AccountResponse struct {
	AccountID string              `json:"account_id"`
	Name      string              `json:"name"`
	Slug      string              `json:"slug"`
	Status    store.AccountStatus `json:"status"`
	CreatedAt time.Time           `json:"created_at"`
	UpdatedAt time.Time           `json:"updated_at"`
}

AccountResponse is the response body for GET /admin/accounts[/:id].

type AccountUpdateEntry

type AccountUpdateEntry struct {
	OSType      otaprotocol.OSType `json:"os_type"`
	TargetModel string             `json:"target_model"`
	Version     string             `json:"version"`
	ReleaseID   string             `json:"release_id"`
}

AccountUpdateEntry is one available update for an account fleet target.

type AddProjectMemberRequest

type AddProjectMemberRequest struct {
	CallerID string            `json:"caller_id"`
	Role     store.ProjectRole `json:"role"`
}

AddProjectMemberRequest is POST /projects/:projectId/members body.

type Artifact

type Artifact struct {
	ArtifactID  string             `json:"artifact_id"`
	SHA256      string             `json:"sha256"`
	Size        int64              `json:"size"`
	OS          otaprotocol.OSType `json:"os"`
	TargetModel string             `json:"target_model"`
	Version     string             `json:"version"`
	StorageRef  string             `json:"storage_ref,omitempty"`
	Verified    bool               `json:"verified"`
	UploadedAt  time.Time          `json:"uploaded_at"`
}

Artifact is the 200/201 artifact metadata body (Artifact schema).

type ArtifactUploadMetadata

type ArtifactUploadMetadata struct {
	SHA256        string             `json:"sha256"`
	Signature     string             `json:"signature"`
	Version       string             `json:"version"`
	OS            otaprotocol.OSType `json:"os"`
	TargetModel   string             `json:"target_model"`
	FileHash      string             `json:"file_hash,omitempty"`
	FileSize      int64              `json:"file_size,omitempty"`
	MetadataHash  string             `json:"metadata_hash,omitempty"`
	MetadataSize  int64              `json:"metadata_size,omitempty"`
	PayloadOffset int64              `json:"payload_offset,omitempty"`
	PayloadSize   int64              `json:"payload_size,omitempty"`
}

ArtifactUploadMetadata is the JSON metadata part of the multipart upload (ArtifactUploadMetadata schema).

type AuditActor

type AuditActor struct {
	UserID  string `json:"user_id,omitempty"`
	Subject string `json:"subject"`
}

AuditActor identifies who performed an audited action (operational_endpoints.md §4.2): the durable users-row key (UserID, nullable) plus the token Subject. Subject is always set; UserID is empty when the actor did not resolve to a users row.

type AuditLogEntry

type AuditLogEntry struct {
	ID           string            `json:"id"`
	Actor        AuditActor        `json:"actor"`
	Action       string            `json:"action"`
	ResourceType string            `json:"resource_type"`
	ResourceID   string            `json:"resource_id,omitempty"`
	Details      map[string]string `json:"details,omitempty"`
	IPAddress    string            `json:"ip_address,omitempty"`
	UserAgent    string            `json:"user_agent,omitempty"`
	CreatedAt    time.Time         `json:"created_at"`
}

AuditLogEntry is the response projection of one audit_logs row (operational_endpoints.md §4.3). Secrets are never present — the store only holds the redacted Details map.

type AuditLogList

type AuditLogList struct {
	Items      []AuditLogEntry `json:"items"`
	NextCursor *string         `json:"next_cursor"`
}

AuditLogList is the paged GET /audit body.

type BranchCreateRequest

type BranchCreateRequest struct {
	ProjectID   string `json:"project_id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

BranchCreateRequest is POST /api/v1/branches body.

type BranchResponse

type BranchResponse struct {
	ID          string    `json:"id"`
	ProjectID   string    `json:"project_id"`
	Name        string    `json:"name"`
	Description string    `json:"description,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
	CreatedBy   string    `json:"created_by,omitempty"`
}

BranchResponse is the branch response body.

type BranchUpdateRequest

type BranchUpdateRequest struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
}

BranchUpdateRequest is PATCH /api/v1/branches/:id body.

type Claims

type Claims struct {
	Subject     string   `json:"sub"`
	Roles       []string `json:"roles"`
	AccountID   string   `json:"account_id,omitempty"`
	RoleVersion int64    `json:"role_ver,omitempty"`
	IssuedAt    int64    `json:"iat"`
	Expiry      int64    `json:"exp"`
}

Claims is the subject + roles + expiry carried by a token. This MVP models a signed-opaque token (a JWT-shaped, HMAC-signed claims blob) rather than re-implementing full OAuth2/JWT — the `auth`/`security` catalogue bricks provide the production signer (endpoints.md §4; architecture.md §8). The shape (sub, roles, exp, iat) is JWT-forward so the brick drops in without changing callers.

AccountID is the per-claim account scope added by Accounts M2 (design §3.2). It is SERVER-MINTED (at account-select/device-register time), SERVER-VERIFIED (HMAC-SHA256, secret from config only), and the trust boundary is identical to `resolvePublicKey`: the claim is NEVER derived from a header/body/request field. A token with no account_id (legacy/pre-M2, or a fresh sign-in that has not yet selected an account) parses as the empty string — the auth layer treats an empty AccountID as unscoped and the account-scoped middleware (requireAccountAccess) denies it on every account-scoped route (fail-closed per design §3.3/J).

func (Claims) HasRole

func (c Claims) HasRole(role string) bool

HasRole reports whether the claims carry the given role.

type CreateAccountRequest

type CreateAccountRequest struct {
	Name      string              `json:"name"`
	Slug      string              `json:"slug"`
	Status    store.AccountStatus `json:"status,omitempty"`
	OwnerID   string              `json:"owner_user_id,omitempty"`
	OwnerRole store.AccountRole   `json:"owner_role,omitempty"`
}

CreateAccountRequest is POST /admin/accounts body.

type CreateProjectRequest

type CreateProjectRequest struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

CreateProjectRequest is POST /projects (admin/operator).

type DeltaGenerateRequest

type DeltaGenerateRequest struct {
	BaseArtifactID   string `json:"base_artifact_id"`
	TargetArtifactID string `json:"target_artifact_id"`
}

DeltaGenerateRequest is POST /deltas/generate body.

type DeltaRegister

type DeltaRegister struct {
	BaseArtifactID   string `json:"base_artifact_id"`
	TargetArtifactID string `json:"target_artifact_id"`
	SHA256           string `json:"sha256,omitempty"`
	Size             int64  `json:"size,omitempty"`
	StorageRef       string `json:"storage_ref,omitempty"`
}

DeltaRegister is the POST /deltas body: register a generated base->target delta payload (delta_updates_design.md §3/§4).

type DeltaView

type DeltaView struct {
	ID               string    `json:"id"`
	BaseArtifactID   string    `json:"base_artifact_id"`
	TargetArtifactID string    `json:"target_artifact_id"`
	SHA256           string    `json:"sha256,omitempty"`
	Size             int64     `json:"size,omitempty"`
	StorageRef       string    `json:"storage_ref,omitempty"`
	CreatedAt        time.Time `json:"created_at"`
}

DeltaView is a delta-artifact response.

type Deployment

type Deployment struct {
	DeploymentID string    `json:"deployment_id"`
	ReleaseID    string    `json:"release_id"`
	Strategy     string    `json:"strategy"`
	Group        string    `json:"group,omitempty"`
	Status       string    `json:"status"`
	TargetCount  int       `json:"target_count"`
	CreatedAt    time.Time `json:"created_at"`
}

Deployment is the deployment body (Deployment schema).

type DeploymentCreate

type DeploymentCreate struct {
	ReleaseID string `json:"release_id"`
	Strategy  string `json:"strategy"`
	Group     string `json:"group,omitempty"`
}

DeploymentCreate is POST /deployments (DeploymentCreate schema).

type DeploymentList

type DeploymentList struct {
	Items      []Deployment `json:"items"`
	NextCursor *string      `json:"next_cursor"`
}

DeploymentList is the GET /deployments list body (DeploymentList schema): the set of active deployments. NextCursor is reserved for future pagination parity with ReleaseList (the MVP returns all active deployments in one page).

type DeploymentProgress

type DeploymentProgress struct {
	Pending     int `json:"pending"`
	Downloading int `json:"downloading"`
	Installed   int `json:"installed"`
	Succeeded   int `json:"succeeded"`
	Failed      int `json:"failed"`
}

DeploymentProgress is the aggregate progress block of DeploymentStatus.

type DeploymentStatus

type DeploymentStatus struct {
	Deployment
	Progress DeploymentProgress `json:"progress"`
}

DeploymentStatus is GET /deployments/{id} (DeploymentStatus schema).

type DeviceHealth

type DeviceHealth struct {
	OK            bool    `json:"ok"`
	LastErrorCode *string `json:"last_error_code"`
}

DeviceHealth is the nested health object of DeviceStatus.

type DeviceList

type DeviceList struct {
	Items      []DeviceListItem `json:"items"`
	NextCursor *string          `json:"next_cursor"`
}

DeviceList is the paginated list body (DeviceList schema).

type DeviceListItem

type DeviceListItem struct {
	DeviceID       string             `json:"device_id"`
	HardwareID     string             `json:"hardware_id"`
	Model          string             `json:"model"`
	OS             otaprotocol.OSType `json:"os"`
	OSVersion      string             `json:"os_version,omitempty"`
	CurrentVersion string             `json:"current_version,omitempty"`
	TargetVersion  *string            `json:"target_version"`
	Group          string             `json:"group,omitempty"`
	UpdateState    string             `json:"update_state"`
	ActiveSlot     string             `json:"active_slot,omitempty"`
	HealthOK       bool               `json:"health_ok"`
	LastSeen       *time.Time         `json:"last_seen,omitempty"`
	RegisteredAt   time.Time          `json:"registered_at"`
}

DeviceListItem is a single device in a paginated list (DeviceListItem schema).

type DeviceRegistered

type DeviceRegistered struct {
	DeviceID     string    `json:"device_id"`
	HardwareID   string    `json:"hardware_id"`
	DeviceToken  string    `json:"device_token"`
	TokenType    string    `json:"token_type"`
	ExpiresIn    int       `json:"expires_in"`
	RegisteredAt time.Time `json:"registered_at"`
}

DeviceRegistered is the 201 body (DeviceRegistered schema).

type DeviceRegistration

type DeviceRegistration struct {
	HardwareID     string             `json:"hardware_id"`
	Model          string             `json:"model"`
	OS             otaprotocol.OSType `json:"os"`
	OSVersion      string             `json:"os_version,omitempty"`
	CurrentVersion string             `json:"current_version,omitempty"`
	Group          string             `json:"group,omitempty"`
	Metadata       map[string]string  `json:"metadata,omitempty"`
}

DeviceRegistration is POST /devices/register (DeviceRegistration schema).

type DeviceRegistrationRequest

type DeviceRegistrationRequest struct {
	HardwareID string             `json:"hardware_id"`
	Model      string             `json:"model"`
	OSType     otaprotocol.OSType `json:"os_type"`
	OSVersion  string             `json:"os_version"`
	Group      string             `json:"group,omitempty"`
	Metadata   map[string]string  `json:"metadata,omitempty"`
}

DeviceRegistrationRequest is POST /accounts/:accountId/devices body.

type DeviceRegistrationResponse

type DeviceRegistrationResponse struct {
	DeviceID     string             `json:"device_id"`
	HardwareID   string             `json:"hardware_id"`
	AccountID    string             `json:"account_id"`
	Model        string             `json:"model"`
	OSType       otaprotocol.OSType `json:"os_type"`
	OSVersion    string             `json:"os_version"`
	Group        string             `json:"group,omitempty"`
	RegisteredAt time.Time          `json:"registered_at"`
}

DeviceRegistrationResponse is the response for a registered device.

type DeviceStatus

type DeviceStatus struct {
	DeviceID       string       `json:"device_id"`
	HardwareID     string       `json:"hardware_id"`
	CurrentVersion string       `json:"current_version,omitempty"`
	TargetVersion  *string      `json:"target_version"`
	LastSeen       *time.Time   `json:"last_seen,omitempty"`
	UpdateState    string       `json:"update_state"`
	ActiveSlot     string       `json:"active_slot,omitempty"`
	Health         DeviceHealth `json:"health"`
}

DeviceStatus is the 200 body for GET /devices/{id}/status (DeviceStatus schema).

type ErrorBody

type ErrorBody struct {
	Code      string        `json:"code"`
	Message   string        `json:"message"`
	RequestID string        `json:"request_id,omitempty"`
	Details   []ErrorDetail `json:"details,omitempty"`
}

ErrorBody is the body of the consistent error envelope.

type ErrorDetail

type ErrorDetail struct {
	Field string `json:"field,omitempty"`
	Issue string `json:"issue,omitempty"`
}

ErrorDetail is a field-level problem (endpoints.md §6 details[]).

type ErrorEnvelope

type ErrorEnvelope struct {
	Error ErrorBody `json:"error"`
}

ErrorEnvelope is the single, consistent non-2xx JSON envelope (endpoints.md §6). The message never leaks stack traces, secrets, or internal paths.

type FabricNodeRequest

type FabricNodeRequest struct {
	NodeID string            `json:"node_id"`
	Kind   string            `json:"kind"`
	Arch   string            `json:"arch"`
	HasKVM bool              `json:"has_kvm,omitempty"`
	HasHVF bool              `json:"has_hvf,omitempty"`
	Labels map[string]string `json:"labels,omitempty"`
}

FabricNodeRequest is POST /fabric/nodes body.

type FabricNodeResponse

type FabricNodeResponse struct {
	NodeID     string            `json:"node_id"`
	Kind       string            `json:"kind"`
	Arch       string            `json:"arch"`
	HasKVM     bool              `json:"has_kvm"`
	HasHVF     bool              `json:"has_hvf"`
	Labels     map[string]string `json:"labels,omitempty"`
	LastSeenAt string            `json:"last_seen_at,omitempty"`
	CreatedAt  string            `json:"created_at"`
}

FabricNodeResponse is a fabric node response.

type FabricTargetRequest

type FabricTargetRequest struct {
	TargetID  string `json:"target_id"`
	Tier      string `json:"tier"`
	Tech      string `json:"tech"`
	Model     string `json:"model,omitempty"`
	OSType    string `json:"os_type,omitempty"`
	Exclusive bool   `json:"exclusive,omitempty"`
	NodeID    string `json:"node_id,omitempty"`
}

FabricTargetRequest is POST /fabric/targets body.

type FabricTargetResponse

type FabricTargetResponse struct {
	TargetID  string `json:"target_id"`
	Tier      string `json:"tier"`
	Tech      string `json:"tech"`
	Model     string `json:"model,omitempty"`
	OSType    string `json:"os_type"`
	Exclusive bool   `json:"exclusive"`
	NodeID    string `json:"node_id,omitempty"`
	Status    string `json:"status"`
	CreatedAt string `json:"created_at"`
}

FabricTargetResponse is a fabric target response.

type GroupCreate

type GroupCreate struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

GroupCreate is the POST /groups body.

type GroupList

type GroupList struct {
	Items      []GroupView `json:"items"`
	NextCursor *string     `json:"next_cursor"`
}

GroupList is the GET /groups body — cursor-paginated. NextCursor is nil on the last page.

type GroupMemberView

type GroupMemberView struct {
	DeviceID string    `json:"device_id"`
	AddedAt  time.Time `json:"added_at"`
}

GroupMemberView is one membership with its join time.

type GroupMembers

type GroupMembers struct {
	GroupID    string            `json:"group_id"`
	Items      []GroupMemberView `json:"items"`
	NextCursor *string           `json:"next_cursor"`
}

GroupMembers is the GET /groups/{id}/members body — items of {device_id, added_at}, oldest-first and cursor-paginated. NextCursor is nil on the last page.

type GroupUpdate

type GroupUpdate struct {
	Name        string  `json:"name"`
	Description *string `json:"description,omitempty"`
}

GroupUpdate is the PATCH /groups/{id} body. Description is a pointer so the handler can distinguish "field entirely absent from the JSON body" (nil -- leave the stored description untouched) from "field present with an explicit empty string" (non-nil pointing to "" -- clear the description). A plain string cannot make that distinction: its zero value is indistinguishable from an explicitly-empty field, which previously caused any partial PATCH that only changed `name` to silently wipe the existing description.

type GroupView

type GroupView struct {
	GroupID     string    `json:"group_id"`
	Name        string    `json:"name"`
	Description string    `json:"description,omitempty"`
	MemberCount int       `json:"member_count"`
	CreatedAt   time.Time `json:"created_at"`
}

GroupView is a device-group response.

type IPRateLimiter

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

func NewIPRateLimiter

func NewIPRateLimiter(rate, capacity float64) *IPRateLimiter

func (*IPRateLimiter) Allow

func (rl *IPRateLimiter) Allow(ip string, now time.Time) bool

func (*IPRateLimiter) Stop

func (rl *IPRateLimiter) Stop()

type LoginRequest

type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

LoginRequest is POST /auth/login (LoginRequest schema).

type MemberAdd

type MemberAdd struct {
	DeviceIDs []string `json:"device_ids"`
}

MemberAdd is the POST /groups/{id}/members body — a BATCH of device ids (operational_endpoints.md §6.4).

type MemberAddResult

type MemberAddResult struct {
	Added         []string `json:"added"`
	AlreadyMember []string `json:"already_member"`
	NotFound      []string `json:"not_found"`
}

MemberAddResult is the 200 response of a batch member-add: which ids were newly added, were already members, or are not registered devices.

type Metrics

type Metrics struct {
	RequestsTotal   *prometheus.CounterVec
	RequestDuration *prometheus.HistogramVec
	InFlightGauge   prometheus.Gauge
	// contains filtered or unexported fields
}

Metrics holds the Prometheus metrics wired into the control plane (OTA-034).

func NewMetrics

func NewMetrics(reg prometheus.Registerer) *Metrics

NewMetrics creates the OTA-034 Prometheus metric set and registers it with reg. When reg is nil the process-default registry is used (production); callers that need isolation (tests) pass prometheus.NewRegistry().

func (*Metrics) Handler

func (m *Metrics) Handler() gin.HandlerFunc

Handler returns a Gin handler that serves Prometheus metrics from the registered gatherer at GET /metrics.

func (*Metrics) Middleware

func (m *Metrics) Middleware() gin.HandlerFunc

Middleware returns a Gin middleware that records request count, duration, and in-flight concurrency for every HTTP request processed by the router.

type Options

type Options struct {
	Config       config.Config
	Repo         store.Repository
	Users        UserDirectory
	Health       health.Checker
	TargetPolicy otavalidator.TargetPolicy
	ArtifactKey  ed25519.PublicKey
	Now          func() time.Time
	NewID        func() string
	// Rollout, when non-nil, is the staged-rollout service the server uses (e.g.
	// a pgx-backed one in production). Nil falls back to an in-memory service.
	Rollout *rollout.Service
	// Metrics, when non-nil, is the registered Prometheus metric set the server
	// exposes at GET /metrics and records on every request. Nil (the typical
	// production path) creates the default-process-registry metric set.
	Metrics *Metrics
}

Options configures a Server. Fields left nil/zero fall back to sensible defaults (real clock, random UUID-ish ids, always-ready health).

type ProjectMemberResponse

type ProjectMemberResponse struct {
	CallerID  string `json:"caller_id"`
	ProjectID string `json:"project_id"`
	Role      string `json:"role"`
}

ProjectMemberResponse is the JSON shape for a single project member.

type ProjectResponse

type ProjectResponse struct {
	ProjectID   string    `json:"project_id"`
	AccountID   string    `json:"account_id,omitempty"`
	Name        string    `json:"name"`
	Description string    `json:"description,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

ProjectResponse is a project response body.

type RecallRequest

type RecallRequest struct {
	ToReleaseID string `json:"to_release_id"`
	Reason      string `json:"reason,omitempty"`
}

RecallRequest is the POST /deployments/{id}/recall body: a server-driven rollback of the deployment's current release back to a previous-good release.

type RefreshRequest

type RefreshRequest struct {
	RefreshToken string `json:"refresh_token"`
}

RefreshRequest is POST /auth/refresh (RefreshRequest schema).

type Release

type Release struct {
	ReleaseID   string             `json:"release_id"`
	ArtifactID  string             `json:"artifact_id"`
	Version     string             `json:"version"`
	OS          otaprotocol.OSType `json:"os"`
	TargetModel string             `json:"target_model"`
	Status      string             `json:"status"`
	Notes       string             `json:"notes,omitempty"`
	// SRV-1: echo the stored eligibility floor so an operator can confirm the
	// min_current_version they submitted actually stuck (the value enforced in
	// handleClientUpdate). Omitempty keeps legacy releases (no floor) byte-identical.
	MinCurrentVersion string    `json:"min_current_version,omitempty"`
	CreatedAt         time.Time `json:"created_at"`
}

Release is the release body (Release schema).

type ReleaseCreate

type ReleaseCreate struct {
	ArtifactID        string             `json:"artifact_id"`
	Version           string             `json:"version"`
	OS                otaprotocol.OSType `json:"os"`
	TargetModel       string             `json:"target_model"`
	Notes             string             `json:"notes,omitempty"`
	MinCurrentVersion string             `json:"min_current_version,omitempty"`
}

ReleaseCreate is POST /releases (ReleaseCreate schema).

type ReleaseList

type ReleaseList struct {
	Items      []Release `json:"items"`
	NextCursor *string   `json:"next_cursor"`
}

ReleaseList is the paginated list body (ReleaseList schema).

type RollbackList

type RollbackList struct {
	Items []RollbackView `json:"items"`
}

RollbackList is the GET /deployments/{id}/rollbacks body.

type RollbackView

type RollbackView struct {
	ID                 string            `json:"id"`
	DeploymentID       string            `json:"deployment_id"`
	Kind               string            `json:"kind"`
	FromReleaseID      string            `json:"from_release_id,omitempty"`
	ToReleaseID        string            `json:"to_release_id,omitempty"`
	RecallDeploymentID string            `json:"recall_deployment_id,omitempty"`
	Reason             string            `json:"reason,omitempty"`
	TriggeredBy        string            `json:"triggered_by,omitempty"`
	Details            map[string]string `json:"details,omitempty"`
	CreatedAt          time.Time         `json:"created_at"`
}

RollbackView is a rollback_history response row.

type RolloutCreate

type RolloutCreate struct {
	Phases []RolloutPhaseSpec `json:"phases"`
}

RolloutCreate is the POST /deployments/{id}/rollout body.

type RolloutDecision

type RolloutDecision struct {
	Action string       `json:"action"`
	Reason string       `json:"reason"`
	State  RolloutState `json:"state"`
}

RolloutDecision is the response to an evaluation.

type RolloutPhaseSpec

type RolloutPhaseSpec struct {
	Percentage       int     `json:"percentage"`
	SuccessThreshold float64 `json:"success_threshold"`
	ErrorThreshold   float64 `json:"error_threshold"`
	DurationSeconds  int64   `json:"duration_seconds"`
	AutoProgress     bool    `json:"auto_progress"`
}

RolloutPhaseSpec is one phase in a create-rollout request.

type RolloutState

type RolloutState struct {
	DeploymentID string             `json:"deployment_id"`
	Status       string             `json:"status"`
	CurrentPhase int                `json:"current_phase"`
	Phases       []RolloutPhaseSpec `json:"phases"`
	UpdatedAt    time.Time          `json:"updated_at"`
}

RolloutState is a rollout-state response.

type RolloutVerdict

type RolloutVerdict struct {
	SuccessRate          float64 `json:"success_rate"`
	ErrorRate            float64 `json:"error_rate"`
	PostBootHealthFailed bool    `json:"post_boot_health_failed"`
}

RolloutVerdict is the POST /deployments/{id}/rollout/evaluate body — the telemetry-derived health summary for the current phase cohort.

type SelectAccountRequest

type SelectAccountRequest struct {
	AccountID string `json:"account_id"`
}

SelectAccountRequest is POST /auth/select-account (Accounts M4).

type Server

type Server struct {

	// Degraded indicates the server started with a fallback persistence layer
	// (e.g. in-memory store after PostgreSQL connection failure). When true the
	// /healthz endpoint reports {"status":"degraded"} so an operator can detect
	// the degraded state programmatically.
	Degraded bool
	// contains filtered or unexported fields
}

Server holds the wired dependencies for the REST API handlers.

func NewServer

func NewServer(opts Options) *Server

NewServer builds a Server from the given options.

func (*Server) DispatchEvent

func (s *Server) DispatchEvent(ctx context.Context, eventType, source, projectID, deploymentID string, data interface{})

DispatchEvent matches the event type against all active webhooks for the given project and dispatches the event to every matching webhook concurrently.

func (*Server) DispatchWebhook

func (s *Server) DispatchWebhook(ctx context.Context, webhook store.Webhook, event WebhookEvent)

DispatchWebhook sends a CloudEvents-format event to a webhook URL with HMAC-SHA256 signature and exponential-backoff retry. It updates the webhook's last_success_at / last_failure_at based on the delivery outcome.

func (*Server) GetRoleVersion

func (s *Server) GetRoleVersion(userID string) int64

GetRoleVersion returns the current role version for a user. A user with no stored version returns 0 (the compatible legacy default — any token minted before T040 carries an implicit version of 0).

func (*Server) IncrementRoleVersion

func (s *Server) IncrementRoleVersion(userID string) int64

IncrementRoleVersion bumps the role version for a user so any existing token carrying the old version is rejected at the next authenticated request (T040 — session invalidation on role change).

func (*Server) MountManagerUI

func (s *Server) MountManagerUI(r *gin.Engine)

mountManagerUI registers the embedded OTA Manager SPA routes on the provided Gin engine. Call it from Router() after registering the API routes.

func (*Server) NotifyDeploymentFailed

func (s *Server) NotifyDeploymentFailed(ctx context.Context, projectID, deploymentID, reason string, failedCount int)

NotifyDeploymentFailed fires when a deployment failure is detected.

func (*Server) NotifyDeploymentRolledBack

func (s *Server) NotifyDeploymentRolledBack(ctx context.Context, projectID, deploymentID, initiatedBy, reason string)

NotifyDeploymentRolledBack fires when a rollback is initiated.

func (*Server) NotifyHealthBreach

func (s *Server) NotifyHealthBreach(ctx context.Context, projectID, deploymentID, metricName string, value, threshold float64, deviceID string)

NotifyHealthBreach fires when a health metric exceeds threshold.

func (*Server) NotifyRolloutStageChanged

func (s *Server) NotifyRolloutStageChanged(ctx context.Context, projectID, deploymentID, stageFrom, stageTo, rolloutID string, autoProgressed bool)

NotifyRolloutStageChanged fires when a rollout moves to a new stage.

func (*Server) NotifySecurityTamper

func (s *Server) NotifySecurityTamper(ctx context.Context, projectID, artifactID, expectedHash, actualHash, uploadedBy string)

NotifySecurityTamper fires when artifact hash/signature mismatch is detected.

func (*Server) Router

func (s *Server) Router() *gin.Engine

Router builds the Gin engine with all middleware and routes registered under the configured API base path, plus the unversioned /healthz and /readyz probes.

func (*Server) StartRolloutAutoProgress

func (s *Server) StartRolloutAutoProgress(ctx context.Context)

StartRolloutAutoProgress launches a background goroutine that periodically polls active rollouts and auto-advances phases whose duration has elapsed.

type SetAccountMembershipRequest

type SetAccountMembershipRequest struct {
	UserID string            `json:"user_id"`
	Role   store.AccountRole `json:"role"`
}

SetAccountMembershipRequest is POST /admin/accounts/:accountId/members body.

type StaticUser

type StaticUser struct {
	Username string
	Password string
	Roles    []string
}

StaticUser describes one entry for NewStaticUserDirectory.

type StaticUserDirectory

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

StaticUserDirectory is a simple in-memory UserDirectory for the MVP login stub. Credentials are supplied at construction (never hard-coded in this package) and hashed with bcrypt (cost 12) before storage — plaintext is never retained. Production replaces it with the `auth` brick / identity store.

func MustNewStaticUserDirectory

func MustNewStaticUserDirectory(users ...StaticUser) *StaticUserDirectory

MustNewStaticUserDirectory is like NewStaticUserDirectory but calls os.Exit(1) if any password cannot be hashed. Use in main() where a broken credential is a fatal startup error.

func NewStaticUserDirectory

func NewStaticUserDirectory(users ...StaticUser) *StaticUserDirectory

NewStaticUserDirectory builds a directory from the given users. Every password is hashed with bcrypt (cost 12) at construction time — the plaintext is never stored. A user whose password cannot be hashed is skipped with a log warning (this should never happen with valid input). The returned directory is always non-nil; it may be empty if no users could be hashed.

func (*StaticUserDirectory) Authenticate

func (d *StaticUserDirectory) Authenticate(username, password string) ([]string, bool)

Authenticate verifies the username/password against the stored bcrypt hash. It returns the user's roles and true on success, or false on a unknown user or wrong password. The username-enumeration timing signal is reduced by always running a bcrypt comparison (when the user is unknown a dummy hash that can never validate is compared against).

type StoreReadinessChecker

type StoreReadinessChecker interface {
	ListProjects(ctx context.Context) ([]store.Project, error)
}

StoreReadinessChecker is the minimal persistence surface the readiness probe exercises: one cheap, bounded round-trip that returns a non-nil error when the store cannot serve. store.Repository satisfies it.

type TelemetryAck

type TelemetryAck struct {
	Accepted  int    `json:"accepted"`
	Rejected  int    `json:"rejected"`
	RequestID string `json:"request_id,omitempty"`
}

TelemetryAck is the 202 body (TelemetryAck schema).

type TelemetryEventView

type TelemetryEventView struct {
	Event        otaprotocol.TelemetryEvent `json:"event"`
	Version      string                     `json:"version,omitempty"`
	DeploymentID string                     `json:"deployment_id,omitempty"`
	ErrorCode    string                     `json:"error_code,omitempty"`
	Detail       string                     `json:"detail,omitempty"`
	Timestamp    time.Time                  `json:"timestamp"`
	ReceivedAt   time.Time                  `json:"received_at"`
	// DurationMS / BytesTransferred surface the optional per-event annotations
	// when the device reported them (spec_impl_alignment.md row 4). omitempty +
	// pointer: a legacy event that never carried them is rendered without the key.
	DurationMS       *int64 `json:"duration_ms,omitempty"`
	BytesTransferred *int64 `json:"bytes_transferred,omitempty"`
	CurrentVersion   string `json:"current_version,omitempty"`
}

TelemetryEventView is one event in a device's telemetry history.

type TelemetryEventWire

type TelemetryEventWire struct {
	Event     otaprotocol.TelemetryEvent `json:"event"`
	Version   string                     `json:"version,omitempty"`
	Timestamp time.Time                  `json:"timestamp"`
	ErrorCode *string                    `json:"error_code"`
	Detail    *string                    `json:"detail"`
	// DurationMS / BytesTransferred are the optional per-event annotations the
	// device may report (spec_impl_alignment.md row 4). Pointers + omitempty:
	// absent on legacy device payloads, byte-identical to before this addition.
	DurationMS       *int64 `json:"duration_ms,omitempty"`
	BytesTransferred *int64 `json:"bytes_transferred,omitempty"`
	// CurrentVersion is the device's current software version as reported by the
	// device (OTA-037). Distinct from Version — which is the update being
	// processed — this is the version ALREADY running, so the dashboard can
	// track which devices meet the per-release min_current_version floor.
	CurrentVersion string `json:"current_version,omitempty"`
}

TelemetryEventWire is one event in a TelemetryReport (TelemetryEvent schema).

type TelemetryHealth

type TelemetryHealth struct {
	BatteryPct    *int   `json:"battery_pct,omitempty"`
	StorageFreeMB *int   `json:"storage_free_mb,omitempty"`
	ActiveSlot    string `json:"active_slot,omitempty"`
}

TelemetryHealth is the optional device health block in a TelemetryReport.

type TelemetryHistory

type TelemetryHistory struct {
	DeviceID   string               `json:"device_id"`
	Items      []TelemetryEventView `json:"items"`
	NextCursor *string              `json:"next_cursor"`
}

TelemetryHistory is the GET /devices/{id}/telemetry body — newest-first, cursor-paginated (operational_endpoints.md §5). NextCursor is nil on the last page. Per-item duration_ms/bytes_transferred (spec_impl_alignment.md row 4) are now ingested + persisted + surfaced on each TelemetryEventView (optional; present only when the device reported them).

type TelemetryOverview

type TelemetryOverview struct {
	EventCounts map[string]int64 `json:"event_counts"`
	Total       int64            `json:"total"`
	// FailureRate is failed terminal outcomes among terminal outcomes:
	// failure / (success + failure). 0 when no terminal events yet.
	FailureRate float64 `json:"failure_rate"`
	// ByState is the fleet device count keyed by last-known update state.
	ByState map[string]int64 `json:"by_state"`
}

TelemetryOverview is the GET /telemetry/overview body: fleet-wide counts by event type (operational_endpoints.md §5).

type TelemetryReport

type TelemetryReport struct {
	SchemaVersion int                  `json:"schema_version"`
	DeviceID      string               `json:"device_id"`
	DeploymentID  string               `json:"deployment_id,omitempty"`
	Events        []TelemetryEventWire `json:"events"`
	Health        *TelemetryHealth     `json:"health,omitempty"`
}

TelemetryReport is POST /client/telemetry (TelemetryReport schema): a batch. SchemaVersion identifies the wire-format version of this payload. The server rejects any value that does not match the known current version, so old-agent and future-agent formats are never silently misinterpreted.

type TokenResponse

type TokenResponse struct {
	AccessToken  string         `json:"access_token"`
	TokenType    string         `json:"token_type"`
	ExpiresIn    int            `json:"expires_in"`
	RefreshToken string         `json:"refresh_token"`
	Roles        []string       `json:"roles,omitempty"`
	Accounts     []AccountEntry `json:"accounts,omitempty"`
}

TokenResponse is the 200 body for login/refresh (TokenResponse schema).

type TokenSigner

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

TokenSigner mints and verifies the signed-opaque tokens. It is the seam the production `security` brick replaces; the MVP uses HMAC-SHA256 over the base64url claims, matching a JWT's "<payload>.<sig>" layout in spirit.

func NewTokenSigner

func NewTokenSigner(secret []byte) *TokenSigner

NewTokenSigner builds a signer over the configured secret.

func (*TokenSigner) Mint

func (s *TokenSigner) Mint(subject string, roles []string, ttl time.Duration, now time.Time) (string, error)

Mint issues a signed token for the subject/roles with the given TTL. This is the legacy (pre-M2) signature — tokens minted through this path carry an empty AccountID (unscoped). Callers that need an account-scoped token should use MintAccount instead. roleVersion is bound into the token claim so the authMiddleware can reject a stale token after a role change (T040).

func (*TokenSigner) MintAccount

func (s *TokenSigner) MintAccount(subject string, roles []string, accountID string, roleVersion int64, ttl time.Duration, now time.Time) (string, error)

MintAccount issues a signed, account-scoped token. When accountID is non-empty the token carries the account claim that the auth middleware extracts and requireAccountAccess enforces. An empty accountID produces an unscoped (legacy) token — the account-scoped middleware will deny it on every account-scoped route (fail-closed per design §3.3/J). roleVersion is bound into the token claim so the authMiddleware can reject a stale token after a role change (T040).

func (*TokenSigner) Verify

func (s *TokenSigner) Verify(token string, now time.Time) (Claims, error)

Verify parses and verifies a token, returning its claims. It rejects a bad signature, malformed structure, or an expired token (relative to now).

type UpdateAccountRequest

type UpdateAccountRequest struct {
	Name   string              `json:"name,omitempty"`
	Slug   string              `json:"slug,omitempty"`
	Status store.AccountStatus `json:"status,omitempty"`
}

UpdateAccountRequest is PATCH /admin/accounts/:id body.

type UpdateProjectMemberRequest

type UpdateProjectMemberRequest struct {
	Role store.ProjectRole `json:"role"`
}

UpdateProjectMemberRequest is PATCH /projects/:projectId/members/:userId body.

type UpdateProjectRequest

type UpdateProjectRequest struct {
	Name        string  `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	AccountID   *string `json:"account_id,omitempty"`
}

UpdateProjectRequest is PATCH /projects/{projectId} (admin). Description is a pointer so the handler can distinguish "field entirely absent from the JSON body" (nil -- leave the stored description untouched) from "field present with an explicit empty string" (non-nil pointing to "" -- clear the description). A plain string cannot make that distinction: its zero value is indistinguishable from an explicitly-empty field, which previously caused any partial PATCH that only changed `name` to silently wipe the existing description. AccountID is a pointer for the same reason: absent (nil) leaves the stored account_id untouched; non-nil (including an explicit empty string) updates it.

type UserDirectory

type UserDirectory interface {
	Authenticate(username, password string) (roles []string, ok bool)
}

UserDirectory authenticates admin/operator/viewer credentials for the login stub (endpoints.md §7.1). In production this is the `auth` brick / identity store; the MVP wires a static directory. It returns the roles for the authenticated user, or ok=false on bad credentials.

type WebhookCreateRequest

type WebhookCreateRequest struct {
	ProjectID string   `json:"project_id"`
	URL       string   `json:"url"`
	Secret    string   `json:"secret"`
	Events    []string `json:"events"`
}

WebhookCreateRequest is the POST /api/v1/webhooks body.

type WebhookEvent

type WebhookEvent struct {
	SpecVersion     string      `json:"specversion"`
	Type            string      `json:"type"`
	Source          string      `json:"source"`
	ID              string      `json:"id"`
	Time            string      `json:"time"`
	DataContentType string      `json:"datacontenttype"`
	ProjectID       string      `json:"project_id,omitempty"`
	DeploymentID    string      `json:"deployment_id,omitempty"`
	Data            interface{} `json:"data"`
}

WebhookEvent is a CloudEvents-format event envelope for webhook delivery.

func BuildWebhookEvent

func BuildWebhookEvent(eventType, source, eventID, projectID, deploymentID string, data interface{}) WebhookEvent

BuildWebhookEvent constructs a CloudEvents envelope.

type WebhookListResponse

type WebhookListResponse struct {
	Items []WebhookResponse `json:"items"`
}

WebhookListResponse is the GET /api/v1/webhooks response.

type WebhookResponse

type WebhookResponse struct {
	ID            string   `json:"id"`
	ProjectID     string   `json:"project_id"`
	URL           string   `json:"url"`
	Events        []string `json:"events"`
	Active        bool     `json:"active"`
	LastSuccessAt *string  `json:"last_success_at,omitempty"`
	LastFailureAt *string  `json:"last_failure_at,omitempty"`
	CreatedAt     string   `json:"created_at"`
}

WebhookResponse is the webhook response body.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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