feat: require paired browser receiver
This commit is contained in:
parent
89c261824f
commit
0f293640f9
|
|
@ -439,6 +439,21 @@ export function createPluginAPI(pluginId) {
|
|||
}
|
||||
},
|
||||
|
||||
browserReceiver: {
|
||||
pairing: function() {
|
||||
assertActive('browserReceiver.pairing');
|
||||
return callBackend(pluginId, 'browserReceiver.pairing', function() {
|
||||
return App.PluginBrowserReceiverPairing(pluginId);
|
||||
});
|
||||
},
|
||||
rotateToken: function() {
|
||||
assertActive('browserReceiver.rotateToken');
|
||||
return callBackend(pluginId, 'browserReceiver.rotateToken', function() {
|
||||
return App.PluginRotateBrowserReceiverToken(pluginId);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
workbench: {
|
||||
openResource: async function(request) {
|
||||
assertActive('workbench.openResource');
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ export function PluginSecretsUnlock(arg1:string,arg2:string):Promise<string>;
|
|||
|
||||
export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>;
|
||||
|
||||
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
|
||||
|
||||
export function PluginRotateBrowserReceiverToken(arg1:string):Promise<Record<string, string>|string>;
|
||||
|
||||
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
|
||||
|
||||
export function PluginSyncDisconnect(arg1:string):Promise<string>;
|
||||
|
|
|
|||
|
|
@ -174,6 +174,14 @@ export function PluginSecretsWrite(arg1, arg2) {
|
|||
return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function PluginBrowserReceiverPairing(arg1) {
|
||||
return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1);
|
||||
}
|
||||
|
||||
export function PluginRotateBrowserReceiverToken(arg1) {
|
||||
return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1);
|
||||
}
|
||||
|
||||
export function PluginSyncConfigure(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,26 +4,30 @@
|
|||
package appsettings
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config represents the application settings stored in ~/.config/verstak/config.json.
|
||||
type Config struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
CurrentVaultPath string `json:"currentVaultPath"`
|
||||
RecentVaults []string `json:"recentVaults"`
|
||||
Theme string `json:"theme"`
|
||||
DevMode bool `json:"devMode"`
|
||||
UserPluginsDir string `json:"userPluginsDir"`
|
||||
Workbench WorkbenchPreferences `json:"workbench,omitempty"`
|
||||
Sync SyncSettings `json:"sync,omitempty"`
|
||||
WindowState *WindowState `json:"windowState,omitempty"`
|
||||
LastOpenedAt string `json:"lastOpenedAt"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
CurrentVaultPath string `json:"currentVaultPath"`
|
||||
RecentVaults []string `json:"recentVaults"`
|
||||
Theme string `json:"theme"`
|
||||
DevMode bool `json:"devMode"`
|
||||
UserPluginsDir string `json:"userPluginsDir"`
|
||||
Workbench WorkbenchPreferences `json:"workbench,omitempty"`
|
||||
Sync SyncSettings `json:"sync,omitempty"`
|
||||
BrowserReceiver BrowserReceiverSettings `json:"browserReceiver,omitempty"`
|
||||
WindowState *WindowState `json:"windowState,omitempty"`
|
||||
LastOpenedAt string `json:"lastOpenedAt"`
|
||||
}
|
||||
|
||||
type WorkbenchPreferences struct {
|
||||
|
|
@ -44,6 +48,11 @@ type SyncSettings struct {
|
|||
LastError string `json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
// BrowserReceiverSettings holds the installation-local browser capture pairing secret.
|
||||
type BrowserReceiverSettings struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// WindowState stores the last window position and size.
|
||||
type WindowState struct {
|
||||
Width int `json:"width"`
|
||||
|
|
@ -205,6 +214,42 @@ func (m *Manager) UpdateSync(syncSettings SyncSettings) error {
|
|||
return m.saveLocked()
|
||||
}
|
||||
|
||||
// EnsureBrowserReceiverToken returns the persisted pairing token, creating it when absent.
|
||||
func (m *Manager) EnsureBrowserReceiverToken() (string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.updateBrowserReceiverTokenLocked(false)
|
||||
}
|
||||
|
||||
// RotateBrowserReceiverToken replaces the persisted pairing token.
|
||||
func (m *Manager) RotateBrowserReceiverToken() (string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.updateBrowserReceiverTokenLocked(true)
|
||||
}
|
||||
|
||||
func (m *Manager) updateBrowserReceiverTokenLocked(force bool) (string, error) {
|
||||
if m.config == nil {
|
||||
m.config = defaultConfig()
|
||||
}
|
||||
current := strings.TrimSpace(m.config.BrowserReceiver.Token)
|
||||
if current != "" && !force {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("generate browser receiver token: %w", err)
|
||||
}
|
||||
token := base64.RawURLEncoding.EncodeToString(bytes)
|
||||
m.config.BrowserReceiver.Token = token
|
||||
if err := m.saveLocked(); err != nil {
|
||||
m.config.BrowserReceiver.Token = current
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// SetCurrentVault updates the current vault path and adds to recents.
|
||||
func (m *Manager) SetCurrentVault(path string) error {
|
||||
m.mu.Lock()
|
||||
|
|
@ -262,6 +307,7 @@ func copyConfig(c *Config) *Config {
|
|||
UserPluginsDir: c.UserPluginsDir,
|
||||
Workbench: c.Workbench,
|
||||
Sync: c.Sync,
|
||||
BrowserReceiver: c.BrowserReceiver,
|
||||
LastOpenedAt: c.LastOpenedAt,
|
||||
}
|
||||
if c.WindowState != nil {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,43 @@ func TestLoad_CorruptConfig(t *testing.T) {
|
|||
// Just verify no panic
|
||||
}
|
||||
|
||||
func TestBrowserReceiverTokenPersistsAndRotates(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
|
||||
manager := NewManager(path)
|
||||
if err := manager.Load(); err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
firstToken, err := manager.EnsureBrowserReceiverToken()
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureBrowserReceiverToken: %v", err)
|
||||
}
|
||||
if firstToken == "" {
|
||||
t.Fatal("EnsureBrowserReceiverToken returned an empty token")
|
||||
}
|
||||
|
||||
reloaded := NewManager(path)
|
||||
if err := reloaded.Load(); err != nil {
|
||||
t.Fatalf("reload settings: %v", err)
|
||||
}
|
||||
persistedToken, err := reloaded.EnsureBrowserReceiverToken()
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureBrowserReceiverToken after reload: %v", err)
|
||||
}
|
||||
if persistedToken != firstToken {
|
||||
t.Fatalf("persisted token = %q, want %q", persistedToken, firstToken)
|
||||
}
|
||||
|
||||
rotatedToken, err := reloaded.RotateBrowserReceiverToken()
|
||||
if err != nil {
|
||||
t.Fatalf("RotateBrowserReceiverToken: %v", err)
|
||||
}
|
||||
if rotatedToken == "" || rotatedToken == firstToken {
|
||||
t.Fatalf("rotated token = %q, want new non-empty token", rotatedToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrentVault(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
|
|
|
|||
|
|
@ -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) {})
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ func (r *Registry) registerDefaults() {
|
|||
{Name: "secrets.read", Description: "Read secrets from the secret store", Dangerous: true},
|
||||
{Name: "secrets.write", Description: "Write secrets to the secret store", Dangerous: true},
|
||||
{Name: "sync.participate", Description: "Participate in vault sync", Dangerous: true},
|
||||
{Name: "browser.receiver.manage", Description: "View and rotate the local browser receiver pairing token", Dangerous: true},
|
||||
}
|
||||
for _, e := range defaults {
|
||||
r.permissions[e.Name] = e
|
||||
|
|
|
|||
40
main.go
40
main.go
|
|
@ -251,20 +251,36 @@ func main() {
|
|||
if vaultService.GetVaultStatus() == vault.StatusOpen {
|
||||
syncService = syncsvc.NewService(vaultService.GetVaultPath(), "")
|
||||
}
|
||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, debugEnabled)
|
||||
browserReceiver := browserreceiver.New(eventBus, func() string {
|
||||
current := app.GetCurrentWorkspace()
|
||||
if root, ok := current["rootPath"].(string); ok {
|
||||
return root
|
||||
}
|
||||
return ""
|
||||
})
|
||||
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
||||
receiverToken, err := appSettingsMgr.EnsureBrowserReceiverToken()
|
||||
if err != nil {
|
||||
log.Printf("[browserreceiver] local receiver disabled: %v", err)
|
||||
} else {
|
||||
defer browserReceiverServer.Close()
|
||||
log.Printf("[browserreceiver] local receiver listening at %s", browserReceiverServer.URL())
|
||||
}
|
||||
var app *api.App
|
||||
var browserReceiver *browserreceiver.Receiver
|
||||
if receiverToken != "" {
|
||||
browserReceiver = browserreceiver.NewWithOptions(eventBus, browserreceiver.Options{
|
||||
RequireToken: true,
|
||||
ReceiverToken: receiverToken,
|
||||
}, func() string {
|
||||
if app == nil {
|
||||
return ""
|
||||
}
|
||||
current := app.GetCurrentWorkspace()
|
||||
if root, ok := current["rootPath"].(string); ok {
|
||||
return root
|
||||
}
|
||||
return ""
|
||||
})
|
||||
}
|
||||
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
||||
if browserReceiver != nil {
|
||||
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
||||
if err != nil {
|
||||
log.Printf("[browserreceiver] local receiver disabled: %v", err)
|
||||
} else {
|
||||
defer browserReceiverServer.Close()
|
||||
log.Printf("[browserreceiver] paired local receiver listening at %s", browserReceiverServer.URL())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Wails App ───────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue