feat: add core vault layer with capability registration
This commit is contained in:
parent
6832b01b23
commit
d6793e8695
|
|
@ -167,6 +167,14 @@ func main() {
|
|||
fmt.Printf(" ✅ registered %d core capabilities\n", len(coreCaps))
|
||||
}
|
||||
|
||||
// Register vault capability (core service)
|
||||
if err := reg.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil {
|
||||
fmt.Printf(" ❌ register vault capability: %v\n", err)
|
||||
allGood = false
|
||||
} else {
|
||||
fmt.Printf(" ✅ registered vault capability\n")
|
||||
}
|
||||
|
||||
// Register plugin capabilities
|
||||
for _, p := range m.Provides {
|
||||
if err := reg.Register(m.ID, []string{p}); err != nil {
|
||||
|
|
@ -232,10 +240,10 @@ func main() {
|
|||
fmt.Printf("\n[capability count]\n")
|
||||
totalCaps := len(reg.List())
|
||||
fmt.Printf(" total capabilities: %d\n", totalCaps)
|
||||
if totalCaps >= 7 {
|
||||
fmt.Printf(" ✅ total capabilities >= 7 (%d)\n", totalCaps)
|
||||
if totalCaps >= 8 {
|
||||
fmt.Printf(" ✅ total capabilities >= 8 (%d)\n", totalCaps)
|
||||
} else {
|
||||
fmt.Printf(" ❌ total capabilities < 7 (got %d, expected >= 7)\n", totalCaps)
|
||||
fmt.Printf(" ❌ total capabilities < 8 (got %d, expected >= 8)\n", totalCaps)
|
||||
allGood = false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import {api} from '../models';
|
|||
import {permissions} from '../models';
|
||||
import {plugin} from '../models';
|
||||
|
||||
export function CloseVault():Promise<void>;
|
||||
|
||||
export function CreateVault(arg1:string):Promise<void>;
|
||||
|
||||
export function GetCapabilities():Promise<Array<capability.Entry>>;
|
||||
|
||||
export function GetContributions():Promise<api.ContributionSummary>;
|
||||
|
|
@ -13,6 +17,10 @@ export function GetPermissions():Promise<Array<permissions.Entry>>;
|
|||
|
||||
export function GetPlugins():Promise<Array<plugin.Plugin>>;
|
||||
|
||||
export function GetVaultStatus():Promise<Record<string, string>>;
|
||||
|
||||
export function OpenVault(arg1:string):Promise<void>;
|
||||
|
||||
export function ReloadPlugins():Promise<number|string>;
|
||||
|
||||
export function Startup():Promise<void>;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CloseVault() {
|
||||
return window['go']['api']['App']['CloseVault']();
|
||||
}
|
||||
|
||||
export function CreateVault(arg1) {
|
||||
return window['go']['api']['App']['CreateVault'](arg1);
|
||||
}
|
||||
|
||||
export function GetCapabilities() {
|
||||
return window['go']['api']['App']['GetCapabilities']();
|
||||
}
|
||||
|
|
@ -18,6 +26,14 @@ export function GetPlugins() {
|
|||
return window['go']['api']['App']['GetPlugins']();
|
||||
}
|
||||
|
||||
export function GetVaultStatus() {
|
||||
return window['go']['api']['App']['GetVaultStatus']();
|
||||
}
|
||||
|
||||
export function OpenVault(arg1) {
|
||||
return window['go']['api']['App']['OpenVault'](arg1);
|
||||
}
|
||||
|
||||
export function ReloadPlugins() {
|
||||
return window['go']['api']['App']['ReloadPlugins']();
|
||||
}
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -2,14 +2,16 @@ module github.com/verstak/verstak-desktop
|
|||
|
||||
go 1.24.4
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.12.0
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||
)
|
||||
|
||||
// App is the main application struct exposed to the Wails frontend.
|
||||
|
|
@ -22,6 +23,7 @@ type App struct {
|
|||
permRegistry *permissions.Registry
|
||||
eventBus *events.Bus
|
||||
plugins []plugin.Plugin
|
||||
vault *vault.Vault
|
||||
}
|
||||
|
||||
// NewApp creates a new App instance.
|
||||
|
|
@ -31,6 +33,7 @@ func NewApp(
|
|||
permReg *permissions.Registry,
|
||||
bus *events.Bus,
|
||||
plugins []plugin.Plugin,
|
||||
vaultService *vault.Vault,
|
||||
) *App {
|
||||
return &App{
|
||||
capRegistry: capReg,
|
||||
|
|
@ -38,6 +41,7 @@ func NewApp(
|
|||
permRegistry: permReg,
|
||||
eventBus: bus,
|
||||
plugins: plugins,
|
||||
vault: vaultService,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +128,13 @@ func (a *App) ReloadPlugins() (int, string) {
|
|||
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
|
||||
}
|
||||
|
||||
// Re-register vault capability if vault is open
|
||||
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen {
|
||||
if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil {
|
||||
log.Printf("[api] ReloadPlugins: failed to re-register vault capability: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
plugins, errs := plugin.DiscoverPlugins(discoveryDirs)
|
||||
|
||||
// Plugin lifecycle: register capabilities + contributions
|
||||
|
|
@ -184,6 +195,55 @@ func (a *App) ReloadPlugins() (int, string) {
|
|||
return len(plugins), summary
|
||||
}
|
||||
|
||||
// ─── Vault API ──────────────────────────────────────────────
|
||||
|
||||
// GetVaultStatus returns the current vault status, path, and vault ID.
|
||||
func (a *App) GetVaultStatus() map[string]string {
|
||||
status := "not-created"
|
||||
path := ""
|
||||
vaultID := ""
|
||||
|
||||
if a.vault != nil {
|
||||
status = string(a.vault.GetVaultStatus())
|
||||
path = a.vault.GetVaultPath()
|
||||
meta := a.vault.GetVaultMeta()
|
||||
if meta != nil {
|
||||
vaultID = meta.VaultID
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"status": status,
|
||||
"path": path,
|
||||
"vaultId": vaultID,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVault creates a new vault at the given path.
|
||||
func (a *App) CreateVault(path string) error {
|
||||
if a.vault == nil {
|
||||
return fmt.Errorf("vault service not initialized")
|
||||
}
|
||||
return a.vault.CreateVault(path)
|
||||
}
|
||||
|
||||
// OpenVault opens an existing vault at the given path.
|
||||
func (a *App) OpenVault(path string) error {
|
||||
if a.vault == nil {
|
||||
return fmt.Errorf("vault service not initialized")
|
||||
}
|
||||
return a.vault.OpenVault(path)
|
||||
}
|
||||
|
||||
// CloseVault closes the current vault.
|
||||
func (a *App) CloseVault() error {
|
||||
if a.vault == nil {
|
||||
return fmt.Errorf("vault service not initialized")
|
||||
}
|
||||
a.vault.CloseVault()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContributionSummary aggregates all contribution types for the frontend.
|
||||
type ContributionSummary struct {
|
||||
Views []contribution.ContributionView `json:"views"`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
// Package vault provides the core vault service for managing Verstak vaults.
|
||||
// A vault is a directory that stores plugin data, settings, cache, and metadata.
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||
)
|
||||
|
||||
// VaultStatus represents the current state of a vault.
|
||||
type VaultStatus string
|
||||
|
||||
const (
|
||||
StatusNotCreated VaultStatus = "not-created"
|
||||
StatusClosed VaultStatus = "closed"
|
||||
StatusOpen VaultStatus = "open"
|
||||
StatusError VaultStatus = "error"
|
||||
)
|
||||
|
||||
// VaultMeta stores metadata about a vault, persisted in .verstak/vault.json.
|
||||
type VaultMeta struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
VaultID string `json:"vaultId"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
App string `json:"app"`
|
||||
}
|
||||
|
||||
// Vault manages a Verstak vault directory and its layout.
|
||||
type Vault struct {
|
||||
mu sync.RWMutex
|
||||
status VaultStatus
|
||||
path string
|
||||
meta *VaultMeta
|
||||
eventBus *events.Bus
|
||||
}
|
||||
|
||||
// NewVault creates a new Vault instance with the given event bus.
|
||||
func NewVault(bus *events.Bus) *Vault {
|
||||
return &Vault{
|
||||
status: StatusNotCreated,
|
||||
path: "",
|
||||
meta: nil,
|
||||
eventBus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// GetVaultStatus returns the current vault status.
|
||||
func (v *Vault) GetVaultStatus() VaultStatus {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
return v.status
|
||||
}
|
||||
|
||||
// GetVaultPath returns the current vault path.
|
||||
func (v *Vault) GetVaultPath() string {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
return v.path
|
||||
}
|
||||
|
||||
// GetVaultMeta returns the current vault metadata.
|
||||
func (v *Vault) GetVaultMeta() *VaultMeta {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
return v.meta
|
||||
}
|
||||
|
||||
// CreateVault creates a new vault at the given path.
|
||||
func (v *Vault) CreateVault(path string) error {
|
||||
if err := ValidateVaultPath(path); err != nil {
|
||||
return fmt.Errorf("invalid vault path: %w", err)
|
||||
}
|
||||
|
||||
vaultDir := filepath.Join(path, "VerstakVault")
|
||||
|
||||
// Create VerstakVault directory
|
||||
if err := os.MkdirAll(vaultDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Ensure .verstak layout
|
||||
if err := EnsureVaultLayout(vaultDir); err != nil {
|
||||
return fmt.Errorf("failed to create vault layout: %w", err)
|
||||
}
|
||||
|
||||
// Generate metadata
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
meta := &VaultMeta{
|
||||
SchemaVersion: 1,
|
||||
VaultID: uuid.New().String(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
App: "verstak",
|
||||
}
|
||||
|
||||
// Write vault.json
|
||||
metaPath := filepath.Join(vaultDir, ".verstak", "vault.json")
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal vault meta: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(metaPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write vault.json: %w", err)
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
v.status = StatusOpen
|
||||
v.path = vaultDir
|
||||
v.meta = meta
|
||||
v.mu.Unlock()
|
||||
|
||||
// Publish event
|
||||
if v.eventBus != nil {
|
||||
v.eventBus.Publish(events.Event{
|
||||
Name: "vault.created",
|
||||
Payload: map[string]string{"path": v.path, "vaultId": v.meta.VaultID},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenVault opens an existing vault at the given path.
|
||||
func (v *Vault) OpenVault(path string) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(absPath, ".verstak", "vault.json")
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read vault.json: %w", err)
|
||||
}
|
||||
|
||||
var meta VaultMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return fmt.Errorf("failed to parse vault.json: %w", err)
|
||||
}
|
||||
|
||||
// Validate metadata
|
||||
if meta.SchemaVersion != 1 {
|
||||
return fmt.Errorf("unsupported schema version: %d", meta.SchemaVersion)
|
||||
}
|
||||
if meta.VaultID == "" {
|
||||
return errors.New("vault ID is empty")
|
||||
}
|
||||
|
||||
// Ensure layout exists
|
||||
if err := EnsureVaultLayout(absPath); err != nil {
|
||||
return fmt.Errorf("failed to ensure vault layout: %w", err)
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
v.status = StatusOpen
|
||||
v.path = absPath
|
||||
v.meta = &meta
|
||||
v.mu.Unlock()
|
||||
|
||||
// Publish event
|
||||
if v.eventBus != nil {
|
||||
v.eventBus.Publish(events.Event{
|
||||
Name: "vault.opened",
|
||||
Payload: map[string]string{"path": v.path, "vaultId": v.meta.VaultID},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseVault closes the current vault.
|
||||
func (v *Vault) CloseVault() {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
if v.status == StatusClosed {
|
||||
return
|
||||
}
|
||||
|
||||
vaultID := ""
|
||||
if v.meta != nil {
|
||||
vaultID = v.meta.VaultID
|
||||
}
|
||||
|
||||
v.status = StatusClosed
|
||||
v.path = ""
|
||||
v.meta = nil
|
||||
|
||||
if v.eventBus != nil {
|
||||
v.eventBus.Publish(events.Event{
|
||||
Name: "vault.closed",
|
||||
Payload: map[string]string{"vaultId": vaultID},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureVaultLayout creates the .verstak directory and standard subdirectories
|
||||
// if they do not already exist.
|
||||
func EnsureVaultLayout(basePath string) error {
|
||||
subdirs := []string{
|
||||
".verstak/plugin-data",
|
||||
".verstak/plugin-settings",
|
||||
".verstak/plugin-cache",
|
||||
".verstak/trash",
|
||||
".verstak/logs",
|
||||
}
|
||||
|
||||
for _, sub := range subdirs {
|
||||
dir := filepath.Join(basePath, sub)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", sub, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateVaultPath checks that the given path is a valid, safe vault path.
|
||||
func ValidateVaultPath(path string) error {
|
||||
if path == "" {
|
||||
return errors.New("path is empty")
|
||||
}
|
||||
|
||||
cleaned := filepath.Clean(path)
|
||||
|
||||
if !filepath.IsAbs(cleaned) {
|
||||
return errors.New("path must be absolute")
|
||||
}
|
||||
|
||||
// Check for null bytes
|
||||
if strings.Contains(cleaned, "\x00") {
|
||||
return errors.New("path contains null bytes")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveSafePath resolves a relative path within the vault, preventing
|
||||
// path traversal attacks.
|
||||
func (v *Vault) ResolveSafePath(relative string) (string, error) {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
|
||||
if v.status != StatusOpen || v.path == "" {
|
||||
return "", errors.New("vault is not open")
|
||||
}
|
||||
|
||||
result := filepath.Join(v.path, relative)
|
||||
result = filepath.Clean(result)
|
||||
|
||||
if !strings.HasPrefix(result, v.path) {
|
||||
return "", errors.New("path traversal detected")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetPluginDataPath returns the data directory for a plugin, creating it if needed.
|
||||
func (v *Vault) GetPluginDataPath(pluginID string) string {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
|
||||
dir := filepath.Join(v.path, ".verstak", "plugin-data", pluginID)
|
||||
os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetPluginSettingsPath returns the settings directory for a plugin, creating it if needed.
|
||||
func (v *Vault) GetPluginSettingsPath(pluginID string) string {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
|
||||
dir := filepath.Join(v.path, ".verstak", "plugin-settings", pluginID)
|
||||
os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetPluginCachePath returns the cache directory for a plugin, creating it if needed.
|
||||
func (v *Vault) GetPluginCachePath(pluginID string) string {
|
||||
v.mu.RLock()
|
||||
defer v.mu.RUnlock()
|
||||
|
||||
dir := filepath.Join(v.path, ".verstak", "plugin-cache", pluginID)
|
||||
os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||
)
|
||||
|
||||
func TestCreateVault_CreatesLayoutAndMeta(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
err := v.CreateVault(base)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
|
||||
vaultDir := filepath.Join(base, "VerstakVault")
|
||||
|
||||
// Check vault.json exists
|
||||
metaPath := filepath.Join(vaultDir, ".verstak", "vault.json")
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
t.Fatalf("vault.json not found: %v", err)
|
||||
}
|
||||
|
||||
var meta VaultMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
t.Fatalf("failed to parse vault.json: %v", err)
|
||||
}
|
||||
|
||||
if meta.SchemaVersion != 1 {
|
||||
t.Errorf("schemaVersion: got %d, want 1", meta.SchemaVersion)
|
||||
}
|
||||
if meta.VaultID == "" {
|
||||
t.Error("vaultId is empty")
|
||||
}
|
||||
if meta.App != "verstak" {
|
||||
t.Errorf("app: got %q, want %q", meta.App, "verstak")
|
||||
}
|
||||
|
||||
// Check subdirectories
|
||||
expectedDirs := []string{
|
||||
".verstak/plugin-data",
|
||||
".verstak/plugin-settings",
|
||||
".verstak/plugin-cache",
|
||||
".verstak/trash",
|
||||
".verstak/logs",
|
||||
}
|
||||
for _, dir := range expectedDirs {
|
||||
full := filepath.Join(vaultDir, dir)
|
||||
info, err := os.Stat(full)
|
||||
if err != nil {
|
||||
t.Errorf("directory %s not found: %v", dir, err)
|
||||
continue
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Errorf("%s is not a directory", dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenVault_ReadsExistingVaultId(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
// Create vault
|
||||
if err := v.CreateVault(base); err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
|
||||
// Remember the vault ID
|
||||
meta := v.GetVaultMeta()
|
||||
if meta == nil {
|
||||
t.Fatal("meta is nil after CreateVault")
|
||||
}
|
||||
originalID := meta.VaultID
|
||||
|
||||
// Close vault
|
||||
v.CloseVault()
|
||||
|
||||
// Open vault
|
||||
vaultDir := filepath.Join(base, "VerstakVault")
|
||||
if err := v.OpenVault(vaultDir); err != nil {
|
||||
t.Fatalf("OpenVault failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify vault ID matches
|
||||
newMeta := v.GetVaultMeta()
|
||||
if newMeta == nil {
|
||||
t.Fatal("meta is nil after OpenVault")
|
||||
}
|
||||
if newMeta.VaultID != originalID {
|
||||
t.Errorf("vault ID mismatch: got %q, want %q", newMeta.VaultID, originalID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenVault_CorruptJSON_Error(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
vaultDir := filepath.Join(base, "VerstakVault")
|
||||
|
||||
// Create vault directory with corrupt vault.json
|
||||
if err := os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755); err != nil {
|
||||
t.Fatalf("failed to create .verstak dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(vaultDir, ".verstak", "vault.json"), []byte("{corrupt"), 0o644); err != nil {
|
||||
t.Fatalf("failed to write corrupt vault.json: %v", err)
|
||||
}
|
||||
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
err := v.OpenVault(vaultDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for corrupt vault.json, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to parse vault.json") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSafePath_BlocksTraversal(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
if err := v.CreateVault(base); err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
|
||||
// Path traversal should be blocked
|
||||
_, err := v.ResolveSafePath("../../etc/passwd")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for path traversal, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "path traversal detected") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Normal path should work
|
||||
result, err := v.ResolveSafePath("normal/path")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveSafePath failed for normal path: %v", err)
|
||||
}
|
||||
if !strings.Contains(result, "normal") {
|
||||
t.Errorf("unexpected result path: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPluginDataPath_CreatesNamespace(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
if err := v.CreateVault(base); err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
|
||||
path := v.GetPluginDataPath("test-plugin")
|
||||
if !strings.Contains(path, "plugin-data") {
|
||||
t.Errorf("path does not contain plugin-data: %s", path)
|
||||
}
|
||||
if !strings.Contains(path, "test-plugin") {
|
||||
t.Errorf("path does not contain test-plugin: %s", path)
|
||||
}
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("plugin data directory not created: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Error("plugin data path is not a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultStatus_Transitions(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
// New vault → not-created
|
||||
if status := v.GetVaultStatus(); status != StatusNotCreated {
|
||||
t.Errorf("initial status: got %q, want %q", status, StatusNotCreated)
|
||||
}
|
||||
|
||||
// Create → open
|
||||
if err := v.CreateVault(base); err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
if status := v.GetVaultStatus(); status != StatusOpen {
|
||||
t.Errorf("after create: got %q, want %q", status, StatusOpen)
|
||||
}
|
||||
|
||||
// Close → closed
|
||||
v.CloseVault()
|
||||
if status := v.GetVaultStatus(); status != StatusClosed {
|
||||
t.Errorf("after close: got %q, want %q", status, StatusClosed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultEvents_Published(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
bus := events.NewBus()
|
||||
v := NewVault(bus)
|
||||
|
||||
// Collect events
|
||||
var published []string
|
||||
bus.Subscribe("vault.created", func(e events.Event) {
|
||||
published = append(published, e.Name)
|
||||
})
|
||||
bus.Subscribe("vault.opened", func(e events.Event) {
|
||||
published = append(published, e.Name)
|
||||
})
|
||||
bus.Subscribe("vault.closed", func(e events.Event) {
|
||||
published = append(published, e.Name)
|
||||
})
|
||||
|
||||
// Create
|
||||
if err := v.CreateVault(base); err != nil {
|
||||
t.Fatalf("CreateVault failed: %v", err)
|
||||
}
|
||||
|
||||
// Close
|
||||
v.CloseVault()
|
||||
|
||||
// Open
|
||||
vaultDir := filepath.Join(base, "VerstakVault")
|
||||
if err := v.OpenVault(vaultDir); err != nil {
|
||||
t.Fatalf("OpenVault failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify events
|
||||
expected := []string{"vault.created", "vault.closed", "vault.opened"}
|
||||
if len(published) != len(expected) {
|
||||
t.Fatalf("expected %d events, got %d: %v", len(expected), len(published), published)
|
||||
}
|
||||
for i, name := range expected {
|
||||
if published[i] != name {
|
||||
t.Errorf("event %d: got %q, want %q", i, published[i], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
main.go
15
main.go
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
|
|
@ -43,6 +44,9 @@ func main() {
|
|||
permRegistry := permissions.NewRegistry()
|
||||
eventBus := events.NewBus()
|
||||
|
||||
// ─── Initialize Vault ────────────────────────────────────
|
||||
vaultService := vault.NewVault(eventBus)
|
||||
|
||||
// ─── Register Core Capabilities ─────────────────────────
|
||||
// These are provided by the desktop core itself, not by plugins.
|
||||
// Registered before plugin discovery so that plugins can resolve
|
||||
|
|
@ -60,6 +64,12 @@ func main() {
|
|||
}
|
||||
log.Printf("[main] registered %d core capabilities", len(coreCaps))
|
||||
|
||||
// Register vault capability (vault is available as a core service)
|
||||
if err := capRegistry.Register(corePluginID, []string{"verstak/core/vault/v1"}); err != nil {
|
||||
log.Fatalf("[main] failed to register vault capability: %v", err)
|
||||
}
|
||||
log.Printf("[main] registered vault capability")
|
||||
|
||||
// ─── Plugin Discovery ───────────────────────────────────
|
||||
discoveryDirs := []string{
|
||||
"~/.config/verstak/plugins",
|
||||
|
|
@ -136,10 +146,11 @@ func main() {
|
|||
failed++
|
||||
}
|
||||
}
|
||||
log.Printf("[main] lifecycle summary: loaded=%d degraded=%d failed=%d", loaded, degraded, failed)
|
||||
log.Printf("[main] lifecycle summary: loaded=%d degraded=%d failed=%d vault=%s",
|
||||
loaded, degraded, failed, vaultService.GetVaultStatus())
|
||||
|
||||
// Create the App struct
|
||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins)
|
||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService)
|
||||
|
||||
// ─── Wails App ───────────────────────────────────────────
|
||||
err := wails.Run(&options.App{
|
||||
|
|
|
|||
Loading…
Reference in New Issue