Skip to content

Commit 5bda338

Browse files
committed
feat: add VM snapshot/restore, chat pagination, and S3 snapshot storage - Add snapshot lifecycle (create/restore/resume) backed by S3-compatible storage (RustFS), cursor-paginated chat and message listing endpoints, - CORS support, and LLM thinking/answer event types.
1 parent f928b6d commit 5bda338

40 files changed

Lines changed: 2381 additions & 64 deletions

ARCHITECTURE.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -540,12 +540,3 @@ observability:
540540
- [ ] Documentation & deployment configs
541541
542542
---
543-
544-
## Open Questions to Discuss
545-
546-
1. **VM Provider**: Start with Firecracker or use containers (gVisor) for easier dev?
547-
2. **State Persistence**: How long should sessions be kept? Archive strategy?
548-
3. **Tool Framework**: Pre-defined tools vs. user-defined tools?
549-
4. **Multi-tenancy**: Single tenant or multi-tenant from day 1?
550-
5. **Scaling**: Horizontal scaling approach (stateless orchestrators + VM pools)?
551-
6. **Network Policy**: Allow internet access from VMs? If yes, with what restrictions?

cmd/main.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/kumori-sh/spacetrk/src/core/ports"
2323
geminiadapter "github.com/kumori-sh/spacetrk/src/infrastructure/llm/gemini"
2424
"github.com/kumori-sh/spacetrk/src/infrastructure/vm/firecracker"
25+
s3storage "github.com/kumori-sh/spacetrk/src/infrastructure/storage/s3"
2526
postgresrepo "github.com/kumori-sh/spacetrk/src/repository/postgres"
2627
agentsvc "github.com/kumori-sh/spacetrk/src/service/agent"
2728
authservice "github.com/kumori-sh/spacetrk/src/service/auth"
@@ -60,6 +61,7 @@ func main() {
6061
environmentRepo := postgresrepo.NewEnvironmentRepository(db)
6162
vmRepo := postgresrepo.NewVMRepository(db)
6263
vmMetricsHistoryRepo := postgresrepo.NewVMMetricsHistoryRepository(db)
64+
snapRepo := postgresrepo.NewSnapshotRepository(db)
6365
userRepo := postgresrepo.NewUserRepository(db)
6466
authRepo := postgresrepo.NewAuthRepository(db)
6567

@@ -99,13 +101,36 @@ func main() {
99101
vmBackend = provider
100102
}
101103

102-
vmService := vmsvc.NewService(vmRepo, vmMetricsHistoryRepo, vmBackend, environmentRepo, cfg.VM.IdleTimeout)
104+
// ── Snapshot Storage ─────────────────────────────────────────────────────
105+
var snapshotStore ports.SnapshotStore
106+
if cfg.Storage.Endpoint != "" {
107+
ss, err := s3storage.NewStore(context.Background(), s3storage.Config{
108+
Endpoint: cfg.Storage.Endpoint,
109+
Region: cfg.Storage.Region,
110+
AccessKey: cfg.Storage.AccessKey,
111+
SecretKey: cfg.Storage.SecretKey,
112+
Bucket: cfg.Storage.Bucket,
113+
UsePathStyle: cfg.Storage.UsePathStyle,
114+
})
115+
if err != nil {
116+
logger.Warn("S3 snapshot store unavailable, snapshots will be stored locally", slog.Any("error", err))
117+
} else {
118+
if err := ss.EnsureBucket(context.Background()); err != nil {
119+
logger.Warn("Failed to ensure S3 bucket", slog.Any("error", err))
120+
}
121+
snapshotStore = ss
122+
logger.Info("S3 snapshot store configured", slog.String("bucket", cfg.Storage.Bucket))
123+
}
124+
}
125+
126+
vmService := vmsvc.NewService(vmRepo, vmMetricsHistoryRepo, vmBackend, environmentRepo, snapRepo, snapshotStore, cfg.VM.IdleTimeout, cfg.VM.AutoSnapshot, cfg.VM.ResumeGrace)
103127
orchTools := orchestratorsvc.NewInMemoryToolRegistry(nil)
104128
orchTools.Register(toolsvc.NewVMCommandTool(vmService))
105129
orchTools.Register(toolsvc.NewVMCreateTool(vmService))
106130
orchTools.Register(toolsvc.NewVMStartTool(vmService))
107131
orchTools.Register(toolsvc.NewVMListTool(vmService))
108132
orchTools.Register(toolsvc.NewVMStopTool(vmService))
133+
orchTools.Register(toolsvc.NewVMSnapshotTool(vmService))
109134

110135
var planner ports.ToolPlanner
111136
if cfg.LLM.DefaultProvider == "gemini" && cfg.LLM.Gemini.APIKey != "" {
@@ -139,7 +164,7 @@ func main() {
139164
planner,
140165
orchTools,
141166
orchestratorsvc.NewMemoryStateStore(),
142-
orchestratorsvc.NewConfig([]string{"vm.execute_command", "vm.create", "vm.start", "vm.list", "vm.stop"}, cfg.Security.MaxTaskDuration),
167+
orchestratorsvc.NewConfig([]string{"vm.execute_command", "vm.create", "vm.start", "vm.list", "vm.stop", "vm.snapshot"}, cfg.Security.MaxTaskDuration),
143168
)
144169
chatService := chatsvc.New(chatRepo, agentRepo, orchService)
145170

@@ -221,6 +246,18 @@ func (b unavailableBackend) GetMetrics(context.Context, string) (vmdomain.Metric
221246
return vmdomain.Metrics{}, b.err()
222247
}
223248

249+
func (b unavailableBackend) CreateSnapshot(context.Context, string) (string, int64, error) {
250+
return "", 0, b.err()
251+
}
252+
253+
func (b unavailableBackend) RestoreFromSnapshot(context.Context, vmdomain.CreateSpec, string) (string, error) {
254+
return "", b.err()
255+
}
256+
257+
func (b unavailableBackend) StopPreserving(context.Context, string) error {
258+
return b.err()
259+
}
260+
224261
func (b unavailableBackend) err() error {
225262
return fmt.Errorf("vm backend unavailable: %s", b.reason)
226263
}

configs/config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ vm:
2323
network_enabled: false
2424
idle_timeout: 5m
2525
max_lifetime: 1h
26+
auto_snapshot: true
27+
resume_grace: 2m
2628
firecracker:
2729
binary_path: firecracker
2830
kernel_path: /usr/share/firecracker/vmlinux
@@ -51,6 +53,14 @@ llm:
5153
model: gemini-3-flash-preview
5254
max_output_tokens: 4096
5355

56+
storage:
57+
endpoint: "http://rustfs:9000"
58+
region: "us-east-1"
59+
access_key: "rustfsadmin"
60+
secret_key: "rustfsadmin"
61+
bucket: "spacetrk-snapshots"
62+
use_path_style: true
63+
5464
security:
5565
jwt_secret: ${JWT_SECRET:-your-super-secret-jwt-key-change-this-in-production}
5666
access_token_expiry: 1h

docker-compose.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ services:
3232
condition: service_healthy
3333
restart: "no"
3434

35+
rustfs:
36+
container_name: spacetrk-rustfs
37+
image: rustfs/rustfs:latest
38+
restart: unless-stopped
39+
environment:
40+
RUSTFS_ACCESS_KEY: rustfsadmin
41+
RUSTFS_SECRET_KEY: rustfsadmin
42+
RUSTFS_CONSOLE_ENABLE: "true"
43+
ports:
44+
- "9000:9000"
45+
- "9001:9001"
46+
volumes:
47+
- spacetrk-rustfs-data:/data
48+
healthcheck:
49+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
50+
interval: 10s
51+
timeout: 5s
52+
retries: 5
53+
3554
api:
3655
build: .
3756
restart: unless-stopped
@@ -60,6 +79,9 @@ services:
6079
depends_on:
6180
db:
6281
condition: service_healthy
82+
rustfs:
83+
condition: service_started
6384

6485
volumes:
65-
spacetrk-psql-data:
86+
spacetrk-psql-data:
87+
spacetrk-rustfs-data:

go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ module github.com/kumori-sh/spacetrk
33
go 1.24.0
44

55
require (
6+
github.com/aws/aws-sdk-go-v2 v1.41.5
7+
github.com/aws/aws-sdk-go-v2/config v1.32.15
8+
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
9+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14
10+
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
611
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
712
github.com/go-chi/chi/v5 v5.2.5
13+
github.com/go-chi/cors v1.2.2
814
github.com/go-playground/validator/v10 v10.30.1
915
github.com/golang-jwt/jwt/v5 v5.3.1
1016
github.com/google/uuid v1.6.0
@@ -24,6 +30,20 @@ require (
2430
github.com/PuerkitoBio/purell v1.1.1 // indirect
2531
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
2632
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
33+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
34+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
35+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
36+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
37+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
38+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
39+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
40+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
41+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
42+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
43+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
44+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
45+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
46+
github.com/aws/smithy-go v1.24.2 // indirect
2747
github.com/containerd/fifo v1.0.0 // indirect
2848
github.com/containernetworking/cni v1.0.1 // indirect
2949
github.com/containernetworking/plugins v1.0.1 // indirect

go.sum

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,44 @@ github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:W
8181
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
8282
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
8383
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
84+
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
85+
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
86+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
87+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
88+
github.com/aws/aws-sdk-go-v2/config v1.32.15 h1:i7rHbaySnBXGvCkDndaBU8f3EAlRVgViwNfkwFUrXgE=
89+
github.com/aws/aws-sdk-go-v2/config v1.32.15/go.mod h1:yLJzL0IkI9+4BwjPSOueyHzppJj3t0dhK5tbmmcFk5Q=
90+
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
91+
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
92+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
93+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
94+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14 h1:wSr7rcx4tyHFn8C0Due3FJELFc02u6MkfRyVChkqAt4=
95+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.14/go.mod h1:2FslH0Y9FIQw4YKs3TVb5zEK66FvWvVPYNPqPmqWZqs=
96+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
97+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
98+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
99+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
100+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
101+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
102+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
103+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
104+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
105+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
106+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
107+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
108+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
109+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
110+
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
111+
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
112+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
113+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
114+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
115+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
116+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
117+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
118+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
119+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
120+
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
121+
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
84122
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
85123
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
86124
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -269,6 +307,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
269307
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
270308
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
271309
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
310+
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
311+
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
272312
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
273313
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
274314
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE vm_instances DROP COLUMN last_resumed_at;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE vm_instances ADD COLUMN last_resumed_at TIMESTAMPTZ;

pkg/config/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Config struct {
1717
Log LogConfig `yaml:"log"`
1818
VM VMConfig `yaml:"vm"`
1919
LLM LLMConfig `yaml:"llm"`
20+
Storage StorageConfig `yaml:"storage"`
2021
Security SecurityConfig `yaml:"security"`
2122
Observability ObservabilityConfig `yaml:"observability"`
2223
}
@@ -47,6 +48,8 @@ type VMConfig struct {
4748
NetworkEnabled bool `yaml:"network_enabled"`
4849
IdleTimeout time.Duration `yaml:"idle_timeout"`
4950
MaxLifetime time.Duration `yaml:"max_lifetime"`
51+
AutoSnapshot bool `yaml:"auto_snapshot"`
52+
ResumeGrace time.Duration `yaml:"resume_grace"`
5053

5154
Firecracker VMFirecrackerConfig `yaml:"firecracker"`
5255
}
@@ -85,6 +88,15 @@ type GeminiConfig struct {
8588
SystemPrompt string `yaml:"system_prompt"`
8689
}
8790

91+
type StorageConfig struct {
92+
Endpoint string `yaml:"endpoint"`
93+
Region string `yaml:"region"`
94+
AccessKey string `yaml:"access_key"`
95+
SecretKey string `yaml:"secret_key"`
96+
Bucket string `yaml:"bucket"`
97+
UsePathStyle bool `yaml:"use_path_style"`
98+
}
99+
88100
type SecurityConfig struct {
89101
JWTSecret string `yaml:"jwt_secret"`
90102
AccessTokenExpiry time.Duration `yaml:"access_token_expiry"`

src/api/http/server.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/go-chi/chi/v5"
10+
"github.com/go-chi/cors"
1011
httputil "github.com/kumori-sh/spacetrk/pkg/http"
1112
agenthttp "github.com/kumori-sh/spacetrk/src/api/http/v1/agent"
1213
authhttp "github.com/kumori-sh/spacetrk/src/api/http/v1/auth"
@@ -33,6 +34,15 @@ type Config struct {
3334
// ListenAndServe / Shutdown.
3435
func New(cfg Config) *http.Server {
3536
r := chi.NewRouter()
37+
38+
r.Use(cors.Handler(cors.Options{
39+
AllowedOrigins: []string{"http://localhost:5173"},
40+
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
41+
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
42+
AllowCredentials: true,
43+
MaxAge: 300,
44+
}))
45+
3646
registerRoutes(r, cfg)
3747

3848
// Middleware chain (outermost = first to execute):

0 commit comments

Comments
 (0)