feat: require paired browser receiver

This commit is contained in:
2026-07-10 03:37:12 +08:00
parent 89c261824f
commit 0f293640f9
11 changed files with 316 additions and 26 deletions
+26 -4
View File
@@ -14,14 +14,18 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/verstak/verstak-desktop/internal/core/events"
)
const capturePath = "/api/browser-inbox/v1/captures"
const DefaultAddr = "127.0.0.1:47731"
const receiverTokenHeader = "X-Verstak-Receiver-Token"
const (
capturePath = "/api/browser-inbox/v1/captures"
DefaultAddr = "127.0.0.1:47731"
DefaultCaptureURL = "http://" + DefaultAddr + capturePath
receiverTokenHeader = "X-Verstak-Receiver-Token"
)
const (
maxCaptureBodyBytes = 12 * 1024 * 1024
@@ -44,6 +48,7 @@ const (
type Receiver struct {
bus *events.Bus
workspaceProvider WorkspaceProvider
optionsMu sync.RWMutex
options Options
}
@@ -112,6 +117,17 @@ func NewWithOptions(bus *events.Bus, options Options, providers ...WorkspaceProv
return &Receiver{bus: bus, workspaceProvider: provider, options: options}
}
// SetReceiverToken updates the active token without restarting the local server.
func (r *Receiver) SetReceiverToken(token string) {
if r == nil {
return
}
r.optionsMu.Lock()
defer r.optionsMu.Unlock()
r.options.RequireToken = true
r.options.ReceiverToken = strings.TrimSpace(token)
}
func Start(addr string, receiver *Receiver) (*Server, error) {
if receiver == nil {
return nil, fmt.Errorf("receiver is required")
@@ -213,10 +229,16 @@ func (r *Receiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
func (r *Receiver) validateReceiverToken(req *http.Request) error {
if r == nil || !r.options.RequireToken {
if r == nil {
return nil
}
r.optionsMu.RLock()
requireToken := r.options.RequireToken
expected := strings.TrimSpace(r.options.ReceiverToken)
r.optionsMu.RUnlock()
if !requireToken {
return nil
}
if expected == "" {
return fmt.Errorf("receiver token required")
}
@@ -281,6 +281,38 @@ func TestReceiverAcceptsPairedToken(t *testing.T) {
}
}
func TestReceiverRotatesPairedToken(t *testing.T) {
bus := events.NewBus()
bus.Subscribe("browser.capture.page", func(event events.Event) {})
receiver := NewWithOptions(bus, Options{RequireToken: true, ReceiverToken: "old-token"})
body := `{
"schemaVersion": 1,
"captureId": "capture-rotated-token",
"capturedAt": "2026-06-27T00:00:00.000Z",
"kind": "page",
"page": {"url": "https://example.com"}
}`
request := func(token string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, capturePath, bytes.NewBufferString(body))
req.Header.Set(receiverTokenHeader, token)
res := httptest.NewRecorder()
receiver.ServeHTTP(res, req)
return res
}
if res := request("old-token"); res.Code != http.StatusAccepted {
t.Fatalf("old token before rotation status = %d, want %d", res.Code, http.StatusAccepted)
}
receiver.SetReceiverToken("new-token")
if res := request("old-token"); res.Code != http.StatusUnauthorized {
t.Fatalf("old token after rotation status = %d, want %d", res.Code, http.StatusUnauthorized)
}
if res := request("new-token"); res.Code != http.StatusAccepted {
t.Fatalf("new token after rotation status = %d, want %d", res.Code, http.StatusAccepted)
}
}
func TestServerStartsOnLocalAddressAndAcceptsCapture(t *testing.T) {
bus := events.NewBus()
bus.Subscribe("browser.capture.page", func(event events.Event) {})