verstak-sync-server/internal/server/sync_contract.go

96 lines
2.7 KiB
Go

package server
import (
"encoding/json"
"fmt"
"strings"
)
const (
maxOpIDLength = 128
maxEntityTypeLength = 64
maxEntityIDLength = 4096
maxOpTypeLength = 64
maxIdempotencyKeyLength = 128
maxDeviceIDLength = 128
maxDeviceNameLength = 128
maxClientVersionLength = 256
maxVaultIDLength = 256
maxLoginLength = 320
)
type syncPushOperation struct {
OpID string `json:"op_id"`
EntityType string `json:"entity_type"`
EntityID string `json:"entity_id"`
OpType string `json:"op_type"`
PayloadJSON string `json:"payload_json"`
ClientSequence int `json:"client_sequence"`
LastSeenServerSeq int `json:"last_seen_server_seq"`
CreatedAt string `json:"created_at"`
}
type syncPushRequest struct {
DeviceID string `json:"device_id"`
IdempotencyKey string `json:"idempotency_key"`
Ops []syncPushOperation `json:"ops"`
}
type syncPullRequest struct {
SinceSequence int `json:"since_sequence"`
PageLimit int `json:"page_limit"`
}
func (s *Server) validateSyncPush(req syncPushRequest) (code, message string) {
if len(req.Ops) > s.cfg.Limits.MaxPushOperations {
return "too_many_operations", "too many operations in one push"
}
for name, value := range map[string]struct {
value string
max int
}{
"device_id": {req.DeviceID, maxDeviceIDLength},
"idempotency_key": {req.IdempotencyKey, maxIdempotencyKeyLength},
} {
if err := validateStringLength(name, value.value, value.max); err != nil {
return "field_too_long", err.Error()
}
}
for _, op := range req.Ops {
for _, value := range []struct {
name string
value string
max int
}{
{"op_id", op.OpID, maxOpIDLength},
{"entity_type", op.EntityType, maxEntityTypeLength},
{"entity_id", op.EntityID, maxEntityIDLength},
{"op_type", op.OpType, maxOpTypeLength},
} {
if strings.TrimSpace(value.value) == "" {
return "invalid_operation", fmt.Sprintf("%s is required", value.name)
}
if err := validateStringLength(value.name, value.value, value.max); err != nil {
return "field_too_long", err.Error()
}
}
if len(op.PayloadJSON) > s.cfg.Limits.MaxPayloadJSON {
return "payload_too_large", "operation payload is too large"
}
if op.PayloadJSON != "" && !json.Valid([]byte(op.PayloadJSON)) {
return "invalid_payload", "operation payload_json must be valid JSON"
}
if op.ClientSequence < 0 || op.LastSeenServerSeq < 0 {
return "invalid_operation", "operation sequences must be non-negative"
}
}
return "", ""
}
func (s *Server) pullPageLimit(requested int) int {
if requested <= 0 || requested > s.cfg.Limits.MaxPullPage {
return s.cfg.Limits.MaxPullPage
}
return requested
}