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
+60
View File
@@ -15,6 +15,7 @@ import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/verstak/verstak-desktop/internal/core/appsettings"
"github.com/verstak/verstak-desktop/internal/core/browserreceiver"
"github.com/verstak/verstak-desktop/internal/core/capability"
"github.com/verstak/verstak-desktop/internal/core/contribution"
"github.com/verstak/verstak-desktop/internal/core/events"
@@ -62,6 +63,7 @@ type App struct {
workbench *coreworkbench.Router
workspace *workspace.Manager
syncSvc *syncsvc.Service
browserReceiver *browserreceiver.Receiver
secretsSession *coresecrets.VaultSession
fileWatcher *filewatcher.Service
debug bool
@@ -87,6 +89,7 @@ func NewApp(
pluginStateMgr *pluginstate.Manager,
workspaceMgr *workspace.Manager,
syncService *syncsvc.Service,
browserReceiverService *browserreceiver.Receiver,
debugEnabled bool,
) *App {
app := &App{
@@ -104,6 +107,7 @@ func NewApp(
workbench: coreworkbench.NewRouter(workbenchPrefsFromSettings(appSettingsMgr)),
workspace: workspaceMgr,
syncSvc: syncService,
browserReceiver: browserReceiverService,
fileWatcher: filewatcher.NewService(bus, 0),
debug: debugEnabled,
activityEvents: make(map[string]bool),
@@ -2105,6 +2109,62 @@ func (a *App) GetPluginAssetContent(pluginID, assetPath string) (string, string)
return string(data), ""
}
// ─── Browser Receiver API ──────────────────────────────────
func (a *App) requirePluginBrowserReceiverAccess(pluginID string) error {
_, err := a.requirePluginAccess(pluginID, "browser.receiver.manage")
return err
}
func (a *App) browserReceiverPairing() (map[string]string, error) {
if a.browserReceiver == nil {
return nil, fmt.Errorf("browser receiver is unavailable")
}
if a.appSettings == nil {
return nil, fmt.Errorf("app settings not initialized")
}
token := strings.TrimSpace(a.appSettings.Get().BrowserReceiver.Token)
if token == "" {
return nil, fmt.Errorf("browser receiver token is unavailable")
}
return map[string]string{
"receiverUrl": browserreceiver.DefaultCaptureURL,
"receiverToken": token,
}, nil
}
// PluginBrowserReceiverPairing returns the local receiver settings for authorized plugins.
func (a *App) PluginBrowserReceiverPairing(pluginID string) (map[string]string, string) {
if err := a.requirePluginBrowserReceiverAccess(pluginID); err != nil {
return nil, err.Error()
}
pairing, err := a.browserReceiverPairing()
if err != nil {
return nil, err.Error()
}
return pairing, ""
}
// PluginRotateBrowserReceiverToken invalidates previous extension pairings.
func (a *App) PluginRotateBrowserReceiverToken(pluginID string) (map[string]string, string) {
if err := a.requirePluginBrowserReceiverAccess(pluginID); err != nil {
return nil, err.Error()
}
if _, err := a.browserReceiverPairing(); err != nil {
return nil, err.Error()
}
token, err := a.appSettings.RotateBrowserReceiverToken()
if err != nil {
return nil, err.Error()
}
a.browserReceiver.SetReceiverToken(token)
pairing, err := a.browserReceiverPairing()
if err != nil {
return nil, err.Error()
}
return pairing, ""
}
// ─── Sync API ──────────────────────────────────────────────
func (a *App) requirePluginSyncAccess(pluginID string, remote bool) error {
+49
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/verstak/verstak-desktop/internal/core/appsettings"
"github.com/verstak/verstak-desktop/internal/core/browserreceiver"
"github.com/verstak/verstak-desktop/internal/core/capability"
"github.com/verstak/verstak-desktop/internal/core/contribution"
"github.com/verstak/verstak-desktop/internal/core/events"
@@ -2000,6 +2001,54 @@ func TestPluginSyncBridgeRequiresDeclaredPermissions(t *testing.T) {
}
}
func TestPluginBrowserReceiverPairingRequiresPermissionAndRotatesToken(t *testing.T) {
app := newBridgeTestApp(t)
app.appSettings = appsettings.NewManager(filepath.Join(t.TempDir(), "config.json"))
if err := app.appSettings.Load(); err != nil {
t.Fatalf("settings Load: %v", err)
}
initialToken, err := app.appSettings.EnsureBrowserReceiverToken()
if err != nil {
t.Fatalf("EnsureBrowserReceiverToken: %v", err)
}
app.browserReceiver = browserreceiver.NewWithOptions(app.eventBus, browserreceiver.Options{
RequireToken: true,
ReceiverToken: initialToken,
})
app.plugins = append(app.plugins, plugin.Plugin{
Manifest: plugin.Manifest{
ID: "browser.local",
Name: "Browser Local",
Version: "1.0.0",
Permissions: []string{"browser.receiver.manage"},
},
Status: plugin.StatusLoaded,
Enabled: true,
})
pairing, errStr := app.PluginBrowserReceiverPairing("browser.local")
if errStr != "" {
t.Fatalf("PluginBrowserReceiverPairing: %s", errStr)
}
if pairing["receiverToken"] != initialToken {
t.Fatalf("pairing token = %q, want %q", pairing["receiverToken"], initialToken)
}
if pairing["receiverUrl"] != browserreceiver.DefaultCaptureURL {
t.Fatalf("pairing receiver URL = %q, want %q", pairing["receiverUrl"], browserreceiver.DefaultCaptureURL)
}
if _, errStr := app.PluginBrowserReceiverPairing("no.storage"); !strings.Contains(errStr, "browser.receiver.manage") {
t.Fatalf("missing permission error = %q", errStr)
}
rotated, errStr := app.PluginRotateBrowserReceiverToken("browser.local")
if errStr != "" {
t.Fatalf("PluginRotateBrowserReceiverToken: %s", errStr)
}
if rotated["receiverToken"] == "" || rotated["receiverToken"] == initialToken {
t.Fatalf("rotated pairing token = %q, want new non-empty token", rotated["receiverToken"])
}
}
func TestPluginSyncStatusReportsPersistedError(t *testing.T) {
app := newBridgeTestApp(t)
app.plugins = append(app.plugins,