Documentation
¶
Overview ¶
Package device manages device registration, pairing, and push subscriptions.
Index ¶
- Constants
- Variables
- type CreateDeviceRequest
- type CreatedDeviceResponse
- type Device
- type DeviceKeyDescriptor
- type DeviceKeysResponse
- type DeviceResponse
- type DeviceTopic
- type DevicesListResponse
- type Handler
- func (h *Handler) CreateDevice(w http.ResponseWriter, r *http.Request)
- func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request)
- func (h *Handler) GetDevices(w http.ResponseWriter, r *http.Request)
- func (h *Handler) Pair(w http.ResponseWriter, r *http.Request)
- func (h *Handler) PairingStatus(w http.ResponseWriter, r *http.Request)
- func (h *Handler) RegeneratePairingOTP(w http.ResponseWriter, r *http.Request)
- func (h *Handler) UnpairDevice(w http.ResponseWriter, r *http.Request)
- func (h *Handler) UpdateDevice(w http.ResponseWriter, r *http.Request)
- type PairRequest
- type PairResponse
- type PairingCode
- type PairingCodeResponse
- type PairingStatus
- type PairingStatusResponse
- type PushSubscription
- type Repository
- func (r *Repository) AddTopicToDevice(ctx context.Context, deviceID, topicID string) error
- func (r *Repository) ClearPushSubscriptionWithStatus(ctx context.Context, deviceID string, status PairingStatus) error
- func (r *Repository) ConsumePairingCode(ctx context.Context, codeHash string, sub PushSubscription, ...) (string, error)
- func (r *Repository) CreateDevice(ctx context.Context, id, userID, name, description string) error
- func (r *Repository) CreateDeviceWithTopicsAndPairingCode(ctx context.Context, id, userID, name, description string, topicIDs []string, ...) error
- func (r *Repository) CreatePairingCode(ctx context.Context, codeHash, deviceID string, expiresAt int64) error
- func (r *Repository) DeleteDevice(ctx context.Context, deviceID string) error
- func (r *Repository) DeletePushSubscription(ctx context.Context, deviceID string) error
- func (r *Repository) DeleteTopicFromDevice(ctx context.Context, deviceID, topicID string) error
- func (r *Repository) GetActivePairingCode(ctx context.Context, codeHash string) (*PairingCode, error)
- func (r *Repository) GetDeviceByID(ctx context.Context, deviceID string) (*Device, error)
- func (r *Repository) GetDeviceByIDAndTokenHash(ctx context.Context, deviceID, tokenHash string) (*Device, error)
- func (r *Repository) GetDeviceByIDAndUser(ctx context.Context, deviceID, userID string) (*Device, error)
- func (r *Repository) GetDeviceKeysByUser(ctx context.Context, userID string) ([]Device, error)
- func (r *Repository) GetDeviceTopicIDs(ctx context.Context, deviceID string) ([]string, error)
- func (r *Repository) GetPushSubscriptionsByUserAndTopic(ctx context.Context, userID, topicName string) ([]PushSubscription, error)
- func (r *Repository) IncrementAndGetAttempts(ctx context.Context, codeHash string) (int, error)
- func (r *Repository) InvalidatePairingCodes(ctx context.Context, deviceID string) error
- func (r *Repository) ListDevicesByUser(ctx context.Context, userID string) ([]Device, error)
- func (r *Repository) UpdateDevice(ctx context.Context, deviceID, name, description string) error
- func (r *Repository) UpdateDeviceWithTopics(ctx context.Context, userID, deviceID, name, description string, ...) error
- type Service
- func (s *Service) CreateDevice(ctx context.Context, userID, name, description string, topicIDs []string) (*Device, string, time.Time, error)
- func (s *Service) DeleteDevice(ctx context.Context, userID, deviceID string) error
- func (s *Service) DeletePushSubscription(ctx context.Context, deviceID string) error
- func (s *Service) GetDeviceKeysByUser(ctx context.Context, userID string) ([]DeviceKeyDescriptor, error)
- func (s *Service) GetPairingStatus(ctx context.Context, deviceID, deviceToken string) (*PairingStatusResponse, error)
- func (s *Service) GetSubscribedDevices(ctx context.Context, userID, topicName string) ([]PushSubscription, error)
- func (s *Service) ListDevices(ctx context.Context, userID string) ([]DeviceResponse, error)
- func (s *Service) MarkSubscriptionGone(ctx context.Context, deviceID string) error
- func (s *Service) Pair(ctx context.Context, otp, endpoint, p256dh, authKey, ageRecipient string) (string, string, error)
- func (s *Service) RegeneratePairingOTP(ctx context.Context, userID, deviceID string) (string, time.Time, error)
- func (s *Service) UnpairDevice(ctx context.Context, userID, deviceID string) error
- func (s *Service) UpdateDevice(ctx context.Context, userID, deviceID, name, description string, ...) error
- type TopicValidator
- type UpdateDeviceRequest
Constants ¶
const ( // PairingOTPTTL is the time-to-live for pairing OTPs. PairingOTPTTL = 5 * time.Minute )
Variables ¶
var ( ErrDeviceNotFound = fmt.Errorf("device not found") ErrPairingCodeInvalid = fmt.Errorf("pairing code invalid or expired") ErrAtLeastOneTopic = fmt.Errorf("at least one topic is required") ErrInvalidTopicSelection = fmt.Errorf("invalid topic selection") ErrInvalidPushEndpoint = fmt.Errorf("invalid push endpoint") ErrInvalidAgeRecipient = fmt.Errorf("invalid age recipient") ErrInvalidDeviceToken = fmt.Errorf("invalid device token") )
Sentinel errors.
Functions ¶
This section is empty.
Types ¶
type CreateDeviceRequest ¶
type CreateDeviceRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Topics []string `json:"topics"`
}
CreateDeviceRequest is the HTTP request for creating a device.
func (*CreateDeviceRequest) Validate ¶
func (r *CreateDeviceRequest) Validate() []error
Validate checks the create device request fields.
type CreatedDeviceResponse ¶
type CreatedDeviceResponse struct {
Device DeviceResponse `json:"device"`
PairingCode string `json:"pairing_code"`
PairingURL string `json:"pairing_url"`
QRCode string `json:"qr_code"`
ExpiresAt time.Time `json:"expires_at"`
}
CreatedDeviceResponse is the HTTP response for a newly created device (includes pairing code + QR).
type Device ¶
type Device struct {
ID string `db:"id"`
UserID string `db:"user_id"`
Name string `db:"name"`
Description string `db:"description"`
IsActive bool `db:"is_active"`
PairingStatus PairingStatus `db:"pairing_status"`
DeviceTokenHash *string `db:"device_token_hash"`
CreatedAt int64 `db:"created_at"`
UpdatedAt int64 `db:"updated_at"`
// Derived from LEFT JOIN push_subscriptions (not a column in devices table).
SubCreatedAt *int64 `db:"sub_created_at"`
SubAgeRecipient *string `db:"sub_age_recipient"`
}
Device is the DB row type for the devices table.
type DeviceKeyDescriptor ¶
type DeviceKeyDescriptor struct {
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
PairedAt time.Time `json:"paired_at"`
AgeRecipient string `json:"age_recipient"`
AgeRecipientFingerprint string `json:"age_recipient_fingerprint"`
}
DeviceKeyDescriptor is the HTTP/API representation of a paired device key.
func ToDeviceKeyDescriptor ¶
func ToDeviceKeyDescriptor(d *Device) DeviceKeyDescriptor
ToDeviceKeyDescriptor converts a paired device row to a device-key descriptor.
type DeviceKeysResponse ¶
type DeviceKeysResponse struct {
Data []DeviceKeyDescriptor `json:"data"`
}
DeviceKeysResponse is the HTTP response for paired device keys.
type DeviceResponse ¶
type DeviceResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
IsActive bool `json:"is_active"`
PairingStatus PairingStatus `json:"pairing_status"`
PairedAt *time.Time `json:"paired_at"`
AgeRecipient *string `json:"age_recipient"`
AgeRecipientFingerprint *string `json:"age_recipient_fingerprint"`
TopicIDs []string `json:"topic_ids"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
DeviceResponse is the HTTP response for a single device.
func ToDeviceResponse ¶
func ToDeviceResponse(d *Device, topicIDs []string) DeviceResponse
ToDeviceResponse converts a Device DB row to a DeviceResponse.
type DeviceTopic ¶
type DeviceTopic struct {
DeviceID string `db:"device_id"`
TopicID string `db:"topic_id"`
CreatedAt int64 `db:"created_at"`
}
DeviceTopic is the DB row type for the device_topics table.
type DevicesListResponse ¶
type DevicesListResponse struct {
Data []DeviceResponse `json:"data"`
}
DevicesListResponse is the HTTP response for a list of devices.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler handles device HTTP requests.
func NewHandler ¶
NewHandler creates a new device handler.
func (*Handler) CreateDevice ¶
func (h *Handler) CreateDevice(w http.ResponseWriter, r *http.Request)
CreateDevice handles POST /v1/devices.
func (*Handler) DeleteDevice ¶
func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request)
DeleteDevice handles DELETE /v1/devices/{deviceID}.
func (*Handler) GetDevices ¶
func (h *Handler) GetDevices(w http.ResponseWriter, r *http.Request)
GetDevices handles GET /v1/devices.
func (*Handler) Pair ¶
func (h *Handler) Pair(w http.ResponseWriter, r *http.Request)
Pair handles POST /v1/pairing (public endpoint).
func (*Handler) PairingStatus ¶
func (h *Handler) PairingStatus(w http.ResponseWriter, r *http.Request)
PairingStatus handles GET /v1/pairing/{deviceID} (public, device-token-authenticated).
func (*Handler) RegeneratePairingOTP ¶
func (h *Handler) RegeneratePairingOTP(w http.ResponseWriter, r *http.Request)
RegeneratePairingOTP handles POST /v1/devices/{deviceID}/pairing-code.
func (*Handler) UnpairDevice ¶
func (h *Handler) UnpairDevice(w http.ResponseWriter, r *http.Request)
UnpairDevice handles POST /v1/devices/{deviceID}/unpair.
func (*Handler) UpdateDevice ¶
func (h *Handler) UpdateDevice(w http.ResponseWriter, r *http.Request)
UpdateDevice handles PATCH /v1/devices/{deviceID}.
type PairRequest ¶
type PairRequest struct {
PairingCode string `json:"pairing_code"`
Endpoint string `json:"endpoint"`
P256dh string `json:"p256dh"`
Auth string `json:"auth"`
AgeRecipient string `json:"age_recipient"`
}
PairRequest is the HTTP request for pairing a device with a push subscription.
func (*PairRequest) Validate ¶
func (r *PairRequest) Validate() []error
Validate checks the pair request fields.
type PairResponse ¶
type PairResponse struct {
DeviceID string `json:"device_id"`
DeviceToken string `json:"device_token"`
}
PairResponse is the HTTP response returned after a successful pairing.
type PairingCode ¶
type PairingCode struct {
CodeHash string `db:"code_hash"`
DeviceID string `db:"device_id"`
ExpiresAt int64 `db:"expires_at"`
UsedAt *int64 `db:"used_at"`
AttemptCount int `db:"attempt_count"`
CreatedAt int64 `db:"created_at"`
}
PairingCode is the DB row type for the device_pairing_codes table.
type PairingCodeResponse ¶
type PairingCodeResponse struct {
PairingCode string `json:"pairing_code"`
PairingURL string `json:"pairing_url"`
QRCode string `json:"qr_code"`
ExpiresAt time.Time `json:"expires_at"`
}
PairingCodeResponse is the HTTP response for a regenerated pairing code.
type PairingStatus ¶
type PairingStatus string
PairingStatus is the canonical device pairing state exposed to clients.
const ( PairingStatusPending PairingStatus = "pending" PairingStatusPaired PairingStatus = "paired" PairingStatusUnpaired PairingStatus = "unpaired" PairingStatusSubscriptionGone PairingStatus = "subscription_gone" )
type PairingStatusResponse ¶
type PairingStatusResponse struct {
DeviceID string `json:"device_id"`
PairingStatus PairingStatus `json:"pairing_status"`
}
PairingStatusResponse is the public Hive pairing status payload.
type PushSubscription ¶
type PushSubscription struct {
DeviceID string `db:"device_id"`
Endpoint string `db:"endpoint"`
P256dh string `db:"p256dh"`
Auth string `db:"auth"`
AgeRecipient string `db:"age_recipient"`
CreatedAt int64 `db:"created_at"`
UpdatedAt int64 `db:"updated_at"`
}
PushSubscription is the DB row type for the push_subscriptions table.
type Repository ¶
type Repository struct {
// contains filtered or unexported fields
}
Repository handles all device-related DB queries.
func NewRepository ¶
func NewRepository(db *sqlx.DB) *Repository
NewRepository creates a new device Repository.
func (*Repository) AddTopicToDevice ¶
func (r *Repository) AddTopicToDevice(ctx context.Context, deviceID, topicID string) error
AddTopicToDevice inserts a device-topic association.
func (*Repository) ClearPushSubscriptionWithStatus ¶
func (r *Repository) ClearPushSubscriptionWithStatus(ctx context.Context, deviceID string, status PairingStatus) error
ClearPushSubscriptionWithStatus deletes the subscription and updates pairing_status atomically.
func (*Repository) ConsumePairingCode ¶
func (r *Repository) ConsumePairingCode(ctx context.Context, codeHash string, sub PushSubscription, deviceTokenHash string) (string, error)
ConsumePairingCode atomically marks a code as used, stores the push subscription, and sets paired_at. All operations run inside a single transaction to prevent burning the code without completing pairing.
func (*Repository) CreateDevice ¶
func (r *Repository) CreateDevice(ctx context.Context, id, userID, name, description string) error
CreateDevice inserts a new device record.
func (*Repository) CreateDeviceWithTopicsAndPairingCode ¶
func (r *Repository) CreateDeviceWithTopicsAndPairingCode(ctx context.Context, id, userID, name, description string, topicIDs []string, codeHash string, expiresAt int64) error
CreateDeviceWithTopicsAndPairingCode atomically creates a device, its topic associations, and its pairing code.
func (*Repository) CreatePairingCode ¶
func (r *Repository) CreatePairingCode(ctx context.Context, codeHash, deviceID string, expiresAt int64) error
CreatePairingCode inserts a new pairing code record.
func (*Repository) DeleteDevice ¶
func (r *Repository) DeleteDevice(ctx context.Context, deviceID string) error
DeleteDevice soft-deletes a device by setting is_active = 0.
func (*Repository) DeletePushSubscription ¶
func (r *Repository) DeletePushSubscription(ctx context.Context, deviceID string) error
DeletePushSubscription deletes a push subscription by device ID.
func (*Repository) DeleteTopicFromDevice ¶
func (r *Repository) DeleteTopicFromDevice(ctx context.Context, deviceID, topicID string) error
DeleteTopicFromDevice removes a device-topic association.
func (*Repository) GetActivePairingCode ¶
func (r *Repository) GetActivePairingCode(ctx context.Context, codeHash string) (*PairingCode, error)
GetActivePairingCode returns the active (unused, not expired) pairing code matching the hash. Returns nil, nil if not found.
func (*Repository) GetDeviceByID ¶
GetDeviceByID returns an active device by ID, or nil if not found.
func (*Repository) GetDeviceByIDAndTokenHash ¶
func (r *Repository) GetDeviceByIDAndTokenHash(ctx context.Context, deviceID, tokenHash string) (*Device, error)
GetDeviceByIDAndTokenHash returns an active device by ID and token hash, or nil if not found.
func (*Repository) GetDeviceByIDAndUser ¶
func (r *Repository) GetDeviceByIDAndUser(ctx context.Context, deviceID, userID string) (*Device, error)
GetDeviceByIDAndUser returns a device by ID scoped to a user, or nil if not found.
func (*Repository) GetDeviceKeysByUser ¶
GetDeviceKeysByUser returns paired devices with their age public keys for a user.
func (*Repository) GetDeviceTopicIDs ¶
GetDeviceTopicIDs returns all topic IDs associated with a device.
func (*Repository) GetPushSubscriptionsByUserAndTopic ¶
func (r *Repository) GetPushSubscriptionsByUserAndTopic(ctx context.Context, userID, topicName string) ([]PushSubscription, error)
GetPushSubscriptionsByUserAndTopic returns push subscriptions for all active devices subscribed to a given topic name for a given user.
func (*Repository) IncrementAndGetAttempts ¶
IncrementAndGetAttempts atomically increments the attempt counter and returns the new count.
func (*Repository) InvalidatePairingCodes ¶
func (r *Repository) InvalidatePairingCodes(ctx context.Context, deviceID string) error
InvalidatePairingCodes marks all unused pairing codes for a device as used.
func (*Repository) ListDevicesByUser ¶
ListDevicesByUser returns all active devices for a user, ordered by created_at DESC.
func (*Repository) UpdateDevice ¶
func (r *Repository) UpdateDevice(ctx context.Context, deviceID, name, description string) error
UpdateDevice updates the name, description, and updated_at of a device.
func (*Repository) UpdateDeviceWithTopics ¶
func (r *Repository) UpdateDeviceWithTopics(ctx context.Context, userID, deviceID, name, description string, topicIDs []string) error
UpdateDeviceWithTopics atomically updates device fields and replaces topic associations.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service handles device business logic.
func NewService ¶
func NewService(repo *Repository, topicValidator TopicValidator, log *slog.Logger) *Service
NewService creates a new device Service.
func (*Service) CreateDevice ¶
func (s *Service) CreateDevice(ctx context.Context, userID, name, description string, topicIDs []string) (*Device, string, time.Time, error)
CreateDevice creates a device with topics and a pairing OTP. Returns device, raw OTP, and expiry.
func (*Service) DeleteDevice ¶
DeleteDevice validates ownership and soft-deletes the device.
func (*Service) DeletePushSubscription ¶
DeletePushSubscription deletes a push subscription by device ID.
func (*Service) GetDeviceKeysByUser ¶
func (s *Service) GetDeviceKeysByUser(ctx context.Context, userID string) ([]DeviceKeyDescriptor, error)
GetDeviceKeysByUser returns paired devices with their age public keys for a user.
func (*Service) GetPairingStatus ¶
func (s *Service) GetPairingStatus(ctx context.Context, deviceID, deviceToken string) (*PairingStatusResponse, error)
GetPairingStatus returns the canonical pairing status for a device ID, authenticated by device token.
func (*Service) GetSubscribedDevices ¶
func (s *Service) GetSubscribedDevices(ctx context.Context, userID, topicName string) ([]PushSubscription, error)
GetSubscribedDevices returns push subscriptions for devices subscribed to a topic for a user.
func (*Service) ListDevices ¶
ListDevices returns all active devices for a user with their topics.
func (*Service) MarkSubscriptionGone ¶
MarkSubscriptionGone removes a push subscription invalidated by the push provider.
func (*Service) Pair ¶
func (s *Service) Pair(ctx context.Context, otp, endpoint, p256dh, authKey, ageRecipient string) (string, string, error)
Pair verifies the OTP and consumes it atomically with push subscription storage.
func (*Service) RegeneratePairingOTP ¶
func (s *Service) RegeneratePairingOTP(ctx context.Context, userID, deviceID string) (string, time.Time, error)
RegeneratePairingOTP validates ownership, invalidates old codes, and creates a new OTP.
func (*Service) UnpairDevice ¶
UnpairDevice validates ownership and deletes the push subscription.
type TopicValidator ¶
type TopicValidator interface {
ValidateTopicIDs(ctx context.Context, userID string, topicIDs []string) error
}
TopicValidator verifies that topic IDs belong to the given user.
type UpdateDeviceRequest ¶
type UpdateDeviceRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Topics []string `json:"topics"`
}
UpdateDeviceRequest is the HTTP request for updating a device.
func (*UpdateDeviceRequest) Validate ¶
func (r *UpdateDeviceRequest) Validate() []error
Validate checks the update device request fields.