Documentation
¶
Index ¶
- Constants
- Variables
- func LoggerFrom(c *gin.Context) *slog.Logger
- func NewJSONLogger(level slog.Level) *slog.Logger
- func NewStoreReadinessProbe(repo StoreReadinessChecker) health.ReadyFunc
- func StructuredLoggingMiddleware() gin.HandlerFunc
- type AccountEntry
- type AccountProjectResponse
- type AccountResponse
- type AccountUpdateEntry
- type AddProjectMemberRequest
- type Artifact
- type ArtifactUploadMetadata
- type AuditActor
- type AuditLogEntry
- type AuditLogList
- type BranchCreateRequest
- type BranchResponse
- type BranchUpdateRequest
- type Claims
- type CreateAccountRequest
- type CreateProjectRequest
- type DeltaGenerateRequest
- type DeltaRegister
- type DeltaView
- type Deployment
- type DeploymentCreate
- type DeploymentList
- type DeploymentProgress
- type DeploymentStatus
- type DeviceHealth
- type DeviceList
- type DeviceListItem
- type DeviceRegistered
- type DeviceRegistration
- type DeviceRegistrationRequest
- type DeviceRegistrationResponse
- type DeviceStatus
- type ErrorBody
- type ErrorDetail
- type ErrorEnvelope
- type FabricNodeRequest
- type FabricNodeResponse
- type FabricTargetRequest
- type FabricTargetResponse
- type GroupCreate
- type GroupList
- type GroupMemberView
- type GroupMembers
- type GroupUpdate
- type GroupView
- type IPRateLimiter
- type LoginRequest
- type MemberAdd
- type MemberAddResult
- type Metrics
- type Options
- type ProjectMemberResponse
- type ProjectResponse
- type RecallRequest
- type RefreshRequest
- type Release
- type ReleaseCreate
- type ReleaseList
- type RollbackList
- type RollbackView
- type RolloutCreate
- type RolloutDecision
- type RolloutPhaseSpec
- type RolloutState
- type RolloutVerdict
- type SelectAccountRequest
- type Server
- func (s *Server) DispatchEvent(ctx context.Context, eventType, source, projectID, deploymentID string, ...)
- func (s *Server) DispatchWebhook(ctx context.Context, webhook store.Webhook, event WebhookEvent)
- func (s *Server) GetRoleVersion(userID string) int64
- func (s *Server) IncrementRoleVersion(userID string) int64
- func (s *Server) MountManagerUI(r *gin.Engine)
- func (s *Server) NotifyDeploymentFailed(ctx context.Context, projectID, deploymentID, reason string, failedCount int)
- func (s *Server) NotifyDeploymentRolledBack(ctx context.Context, projectID, deploymentID, initiatedBy, reason string)
- func (s *Server) NotifyHealthBreach(ctx context.Context, projectID, deploymentID, metricName string, ...)
- func (s *Server) NotifyRolloutStageChanged(ctx context.Context, ...)
- func (s *Server) NotifySecurityTamper(ctx context.Context, ...)
- func (s *Server) Router() *gin.Engine
- func (s *Server) StartRolloutAutoProgress(ctx context.Context)
- type SetAccountMembershipRequest
- type StaticUser
- type StaticUserDirectory
- type StoreReadinessChecker
- type TelemetryAck
- type TelemetryEventView
- type TelemetryEventWire
- type TelemetryHealth
- type TelemetryHistory
- type TelemetryOverview
- type TelemetryReport
- type TokenResponse
- type TokenSigner
- func (s *TokenSigner) Mint(subject string, roles []string, ttl time.Duration, now time.Time) (string, error)
- func (s *TokenSigner) MintAccount(subject string, roles []string, accountID string, roleVersion int64, ...) (string, error)
- func (s *TokenSigner) Verify(token string, now time.Time) (Claims, error)
- type UpdateAccountRequest
- type UpdateProjectMemberRequest
- type UpdateProjectRequest
- type UserDirectory
- type WebhookCreateRequest
- type WebhookEvent
- type WebhookListResponse
- type WebhookResponse
Constants ¶
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).
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.
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 ¶
var TestDisableClaimAccountAccess bool
var TestDisableSelectAccountMembershipCheck bool
Functions ¶
func LoggerFrom ¶
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 ¶
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 ¶
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).
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 ¶
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 ¶
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) Stop ¶
func (rl *IPRateLimiter) Stop()
type LoginRequest ¶
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 ¶
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 (*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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).
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 ¶
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.
Source Files
¶
- audit_wire.go
- bind.go
- compression.go
- embed.go
- errors.go
- handlers_account_updates.go
- handlers_accounts.go
- handlers_artifact.go
- handlers_audit.go
- handlers_auth.go
- handlers_branches.go
- handlers_client.go
- handlers_delta.go
- handlers_deployment.go
- handlers_device.go
- handlers_fabric.go
- handlers_group.go
- handlers_health.go
- handlers_project.go
- handlers_project_members.go
- handlers_recall.go
- handlers_release.go
- handlers_rollout.go
- handlers_telemetry.go
- handlers_webhook.go
- middleware.go
- multipart.go
- observability.go
- rate_limit.go
- security_headers.go
- security_middleware.go
- server.go
- test_hooks.go
- token.go
- users.go
- webhook.go
- wire.go