device

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const (
	EventSourceHeartbeat     = httpapi.Heartbeat
	EventSourceManual        = httpapi.Manual
	EventSourceExpiry        = httpapi.Expiry
	EventSourceLimitExceeded = httpapi.LimitExceeded
)
View Source
const (
	AttrKeyError            = logging.AttrKeyError
	AttrKeyAddressID        = "address_id"
	AttrKeyAddressIP        = "address_ip"
	AttrKeyClientIP         = "client_ip"
	AttrKeyCount            = "count"
	AttrKeyDeviceID         = "device_id"
	AttrKeyDeviceName       = "device_name"
	AttrKeyAddressEventType = "address_event_type"
)

Slog attribute key names for the device domain. Use these constants when logging so keys are consistent and typo-safe across handlers and services.

View Source
const APIKeyPrefix = "wdk_"

Variables

View Source
var (
	ErrDeviceNotFound          = errors.New("device not found")
	ErrDeviceDisabled          = errors.New("device is disabled")
	ErrOwnerNotFound           = errors.New("device owner not found")
	ErrNoAPIKey                = errors.New("device has no API key")
	ErrDuplicateDeviceName     = errors.New("device name already in use")
	ErrAddressNotFound         = errors.New("device address not found")
	ErrInvalidIPFormat         = errors.New("invalid IP address format")
	ErrInvalidDeviceIP         = errors.New("ip address is not valid for device registration")
	ErrTrustedProxyIPRejected  = errors.New("ip address belongs to trusted proxy")
	ErrAddressNotOwnedByDevice = errors.New("address is not owned by the device")
	ErrInvalidAPIKey           = errors.New("invalid api key")
	ErrInvalidDeviceName       = errors.New("device name must be between 1 and 50 characters")
	ErrDescriptionTooLong      = errors.New("description must be 200 characters or fewer")
	ErrIconTooLong             = errors.New("icon must be 80 characters or fewer")
)

Functions

func GenerateAPIKey added in v0.3.0

func GenerateAPIKey() (rawKey string, keyHash string, keyPrefix string, error error)

GenerateAPIKey generates a new API key and returns the raw key (to send to user), the key hash (to store in DB), and the key prefix (for display).

func HashAPIKey added in v0.3.0

func HashAPIKey(rawKey string) string

HashAPIKey hashes the API key using SHA-256 and returns base64url encoded hash. This mirrors the pattern used in auth.hashRawToken.

func ParseAndValidateIP added in v0.3.0

func ParseAndValidateIP(ipInput string) (netip.Addr, error)

ParseAndValidateIP parses and validates that the given string is a valid IPv4 or IPv6 address. It ignores the port if present and only cares about the IP component. The result is always canonical: an IPv4-mapped IPv6 address (::ffff:a.b.c.d) is unmapped to its plain IPv4 form so stored addresses never depend on representation. TODO: Make private once the address_test go through the NewCreateAddressParams

func PrincipalDeviceContextMiddleware

func PrincipalDeviceContextMiddleware(apiKeyAuthenticator APIKeyAuthenticator) func(http.Handler) http.Handler

PrincipalDeviceContextMiddleware resolves the api key into a Device Principal and injects it into the context.

func WithPrincipal

func WithPrincipal(ctx context.Context, principal Principal) context.Context

Types

type APIKeyAuthenticator

type APIKeyAuthenticator interface {
	Authenticate(ctx context.Context, rawKey string) (*Principal, error)
}

APIKeyAuthenticator defines the interface for authenticating device API keys.

type Address

type Address struct {
	ID        ids.AddressID `db:"id"`
	DeviceID  ids.DeviceID  `db:"device_id"`
	IP        string        `db:"ip"`
	IsEnabled bool          `db:"is_enabled"`
	Source    EventSource   `db:"source"`
	CreatedAt time.Time     `db:"created_at"`
	UpdatedAt time.Time     `db:"updated_at"`
}

Address represents an address row with its current enabled/disabled state and metadata.

type AddressEvent

type AddressEvent struct {
	Type       EventType
	AddressID  ids.AddressID
	DeviceID   ids.DeviceID
	OccurredAt time.Time
}

func NewAddressEvent

func NewAddressEvent(address *Address, eventType EventType) AddressEvent

func NewDeviceEvent added in v0.3.0

func NewDeviceEvent(deviceID ids.DeviceID, eventType EventType) AddressEvent

func (AddressEvent) IsAddressEnabled

func (e AddressEvent) IsAddressEnabled() bool

IsAddressEnabled returns true for Created, Enabled, and Refreshed events (address is enabled); false for Disabled.

func (AddressEvent) LogValue

func (e AddressEvent) LogValue() slog.Value

type AddressObserver

type AddressObserver interface {
	OnAddressEvent(ctx context.Context, event AddressEvent)
}

type CreateAddressParams

type CreateAddressParams struct {
	DeviceID ids.DeviceID
	IP       netip.Addr
}

CreateAddressParams holds only what is necessary to create an address.

func NewCreateAddressParams

func NewCreateAddressParams(deviceID ids.DeviceID, ipAddress string, trustedProxy netip.Addr) (CreateAddressParams, error)

type CreateDeviceInput added in v0.4.0

type CreateDeviceInput struct {
	Name           string
	OwnerID        *ids.UserID // nil = owned by the calling principal
	Description    *string
	Icon           *string
	GenerateAPIKey bool // mint an API key in the same transaction, returned once
}

CreateDeviceInput carries the full set of create-time choices. Profile fields and the credential are optional — a device is valid with only a name and owner, so the bare CreateDevice below is just this with everything else zero.

type CreateDeviceParams

type CreateDeviceParams struct {
	Name        string
	OwnerID     ids.UserID
	Description *string
	Icon        *string
}

type Device

type Device struct {
	ID          ids.DeviceID `db:"id"`
	Name        string       `db:"name"`
	Description *string      `db:"description"`
	Icon        *string      `db:"icon"`
	CreatedAt   time.Time    `db:"created_at"`
	UpdatedAt   time.Time    `db:"updated_at"`
	DeletedAt   *time.Time   `db:"deleted_at"`
	DisabledAt  *time.Time   `db:"disabled_at"`
	KeyPrefix   *string      `db:"key_prefix"`
	OwnerID     ids.UserID   `db:"owner_id"`
}

func (*Device) Update added in v0.2.0

func (d *Device) Update(name *string, description **string, icon **string, ownerID *ids.UserID) error

Update applies patch inputs to the device in-place, validating each field. Fields with a nil pointer are left unchanged. The device is not mutated unless all validations pass.

  • name: nil = keep current; non-nil = rename (validated)
  • description: nil = keep current; non-nil ptr to nil = clear; non-nil ptr to value = set
  • icon: same semantics as description

type EventSource

type EventSource = httpapi.AddressEventSource

EventSource is an alias for the API-generated type, making openapi.yaml the single source of truth for valid values.

type EventType

type EventType string
const (
	EventTypeAddressCreated         EventType = "address_created"
	EventTypeAddressEnabled         EventType = "address_enabled"
	EventTypeAddressRefreshed       EventType = "address_refreshed"
	EventTypeAddressDisabled        EventType = "address_disabled"
	EventTypeDeviceOwnershipChanged EventType = "device_ownership_changed"
)

type HTTPHandler

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

func NewHTTPHandler

func NewHTTPHandler(service *Service, logger *slog.Logger) *HTTPHandler

func (*HTTPHandler) APIKeyAuthenticator

func (h *HTTPHandler) APIKeyAuthenticator() APIKeyAuthenticator

func (*HTTPHandler) AddAddress

func (*HTTPHandler) DeleteDeviceAPIKey added in v0.3.0

func (*HTTPHandler) DisableDevice added in v0.4.0

func (*HTTPHandler) EnableDevice added in v0.4.0

func (*HTTPHandler) UpdateDevice added in v0.2.0

type IPEntry

type IPEntry struct {
	IP        string        `db:"ip"`
	DeviceID  ids.DeviceID  `db:"device_id"`
	AddressID ids.AddressID `db:"address_id"`
	UserID    ids.UserID    `db:"user_id"`
}

IPEntry associates an enabled IP address with the device, address, and owning user. All enabled rows are returned (multiple per IP when devices share an address); the policy layer merges them with deny-wins intersection.

type Principal

type Principal struct {
	DeviceID ids.DeviceID
}

func NewPrincipal

func NewPrincipal(deviceID ids.DeviceID) *Principal

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (*Principal, bool)

func PrincipalFromDevice

func PrincipalFromDevice(device *Device) *Principal

type Repository

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

func NewRepository

func NewRepository(db *database.DB) *Repository

func (*Repository) CheckAddressOwnership

func (r *Repository) CheckAddressOwnership(ctx context.Context, deviceID ids.DeviceID, addressID ids.AddressID) error

func (*Repository) CreateAddress

func (r *Repository) CreateAddress(ctx context.Context, params CreateAddressParams, source EventSource) (*Address, error)

func (*Repository) CreateDevice

func (r *Repository) CreateDevice(ctx context.Context, params CreateDeviceParams) (*Device, error)

func (*Repository) DeleteAPIKey added in v0.3.0

func (r *Repository) DeleteAPIKey(ctx context.Context, deviceID ids.DeviceID) error

func (*Repository) DeleteAddressEventsOlderThan added in v0.4.0

func (r *Repository) DeleteAddressEventsOlderThan(ctx context.Context, before time.Time) (int64, error)

DeleteAddressEventsOlderThan removes address_events rows with created_at before the given time. Returns the number of rows deleted.

func (*Repository) DeleteDevice

func (r *Repository) DeleteDevice(ctx context.Context, deviceID ids.DeviceID) error

func (*Repository) DisableAddress

func (r *Repository) DisableAddress(ctx context.Context, addressID ids.AddressID) (*Address, error)

func (*Repository) DisableAddresses

func (r *Repository) DisableAddresses(ctx context.Context, addressIDs []ids.AddressID, source EventSource) ([]Address, error)

func (*Repository) EnableAddress

func (r *Repository) EnableAddress(ctx context.Context, addressID ids.AddressID, source EventSource) (*Address, error)

func (*Repository) GetAddress

func (r *Repository) GetAddress(ctx context.Context, addressID ids.AddressID) (*Address, error)

func (*Repository) GetAddressForDeviceByIP

func (r *Repository) GetAddressForDeviceByIP(ctx context.Context, deviceID ids.DeviceID, ip netip.Addr) (*Address, error)

func (*Repository) GetDevice

func (r *Repository) GetDevice(ctx context.Context, id ids.DeviceID) (*Device, error)

func (*Repository) GetDeviceByAPIKeyHash

func (r *Repository) GetDeviceByAPIKeyHash(ctx context.Context, keyHash string) (*Device, error)

func (*Repository) GetDeviceIDsByOwner added in v0.4.0

func (r *Repository) GetDeviceIDsByOwner(ctx context.Context, ownerID ids.UserID) ([]ids.DeviceID, error)

func (*Repository) GetEnabledAddressesForDevice added in v0.2.0

func (r *Repository) GetEnabledAddressesForDevice(ctx context.Context, deviceID ids.DeviceID) ([]Address, error)

GetEnabledAddressesForDevice returns all enabled addresses for a device, ordered by updated_at DESC.

func (*Repository) GetEnabledIPEntries

func (r *Repository) GetEnabledIPEntries(ctx context.Context) ([]IPEntry, error)

func (*Repository) IsDeviceDisabled added in v0.4.0

func (r *Repository) IsDeviceDisabled(ctx context.Context, id ids.DeviceID) (bool, error)

IsDeviceDisabled reports whether a device has been disabled (disabled_at set). A missing or soft-deleted device returns ErrDeviceNotFound, so callers can use this as both an existence and a state check.

func (*Repository) RefreshAddress

func (r *Repository) RefreshAddress(ctx context.Context, addressID ids.AddressID, source EventSource) (*Address, error)

RefreshAddress records activity for an already-enabled address

func (*Repository) SetDeviceDisabled added in v0.4.0

func (r *Repository) SetDeviceDisabled(ctx context.Context, deviceID ids.DeviceID, disabled bool) error

SetDeviceDisabled flips a device's disabled state. When disabled is true the disabled_at timestamp is stamped; when false it is cleared (re-enabled).

func (*Repository) UpdateDevice added in v0.2.0

func (r *Repository) UpdateDevice(ctx context.Context, device *Device) (*Device, error)

func (*Repository) UpsertAPIKey added in v0.3.0

func (r *Repository) UpsertAPIKey(ctx context.Context, deviceID ids.DeviceID, keyHash string, keyPrefix string) error

type Service

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

func NewService

func NewService(repo repository, transactor transactor, logger *slog.Logger, trustedProxy netip.Addr) *Service

func (*Service) AddAddressObserver

func (s *Service) AddAddressObserver(o AddressObserver)

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, rawKey string) (*Principal, error)

func (*Service) CreateDevice

func (s *Service) CreateDevice(ctx context.Context, principal *auth.Principal, name string, requestedOwnerID *ids.UserID) (*Device, error)

CreateDevice creates a bare device (name + owner only). It is the primitive a device needs to exist; richer creation goes through CreateDeviceWithOptions.

func (*Service) CreateDeviceWithOptions added in v0.4.0

func (s *Service) CreateDeviceWithOptions(ctx context.Context, principal *auth.Principal, input CreateDeviceInput) (*Device, string, error)

CreateDeviceWithOptions creates a device and, when requested, mints its API key in the same transaction so a "shown once" key is never stranded on a half-created device. The raw key is returned only when GenerateAPIKey is set. Pairing codes and address rules are provisioned separately on their own endpoints (a credential-less device is perfectly valid).

func (*Service) DeleteAPIKey added in v0.3.0

func (s *Service) DeleteAPIKey(ctx context.Context, deviceID ids.DeviceID) error

func (*Service) DeleteDevice

func (s *Service) DeleteDevice(ctx context.Context, deviceID ids.DeviceID) error

func (*Service) DisableAddress

func (s *Service) DisableAddress(ctx context.Context, deviceID ids.DeviceID, addressID ids.AddressID) (*Address, error)

func (*Service) DisableAddresses

func (s *Service) DisableAddresses(ctx context.Context, addressIDs []ids.AddressID, source EventSource) error

func (*Service) DisableDevice added in v0.4.0

func (s *Service) DisableDevice(ctx context.Context, deviceID ids.DeviceID) (*Device, error)

DisableDevice is a reversible freeze: it disables every active address in one transaction (removing the device's IPs from the access allowlist) and stamps disabled_at, which blocks address enable/refresh until the device is re-enabled. The API key is deliberately kept — disable is a safety toggle, not a de-credentialing (use DeleteAPIKey for that). EnableDevice reverses it.

func (*Service) EnableDevice added in v0.4.0

func (s *Service) EnableDevice(ctx context.Context, deviceID ids.DeviceID) (*Device, error)

EnableDevice reverses DisableDevice by clearing the disabled_at flag, which re-permits address enable/refresh. The device's API key was never removed, so it can heartbeat its addresses back to enabled on its next check-in; addresses disabled at freeze time are not re-enabled here.

func (*Service) GetDevice

func (s *Service) GetDevice(ctx context.Context, deviceID ids.DeviceID) (*Device, error)

func (*Service) GetEnabledAddressesForDevice added in v0.2.0

func (s *Service) GetEnabledAddressesForDevice(ctx context.Context, deviceID ids.DeviceID) ([]Address, error)

GetEnabledAddressesForDevice returns all enabled addresses for a device, ordered by updated_at DESC.

func (*Service) GetEnabledIPEntries

func (s *Service) GetEnabledIPEntries(ctx context.Context) ([]IPEntry, error)

func (*Service) OnUserEvent added in v0.4.0

func (s *Service) OnUserEvent(ctx context.Context, event auth.UserEvent)

OnUserEvent implements auth.UserObserver. On deletion it soft-deletes every device owned by the user and disables their addresses, firing the AddressDisabled events that trigger a policy cache refresh.

func (*Service) RegenerateAPIKey

func (s *Service) RegenerateAPIKey(ctx context.Context, deviceID ids.DeviceID) (*Device, string, error)

func (*Service) RegisterAddressActivity

func (s *Service) RegisterAddressActivity(ctx context.Context, deviceID ids.DeviceID, inputIP string, source EventSource) (*Address, EventType, error)

func (*Service) UpdateDevice added in v0.2.0

func (s *Service) UpdateDevice(ctx context.Context, deviceID ids.DeviceID, input UpdateDeviceInput) (*Device, error)

type UpdateDeviceInput added in v0.2.0

type UpdateDeviceInput struct {
	Name        *string
	Description **string
	Icon        **string
	OwnerID     *ids.UserID
}

UpdateDeviceInput carries the raw nullable API values for a device profile update. nil pointer = field was absent in the request (leave unchanged). For Description and Icon, **string semantics apply: nil = absent, *nil = clear, *&s = set.

Jump to

Keyboard shortcuts

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