Files
silo-server/internal/plugins/manifest.go
T

126 lines
3.7 KiB
Go

package plugins
import (
"fmt"
"path/filepath"
"strings"
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
publicmanifest "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest"
)
const DefaultSiloAPIVersion = "v1"
func ValidateManifest(manifest *pluginv1.PluginManifest) error {
if err := validateManifestShared(manifest); err != nil {
return err
}
if err := validateHostShellPageContract(manifest); err != nil {
return err
}
if manifest.GetChecksum() == "" {
return fmt.Errorf("plugin manifest checksum is required")
}
if len(manifest.GetSupportedPlatforms()) == 0 {
return fmt.Errorf("plugin manifest supported_platforms is required")
}
return nil
}
func ValidateCatalogManifest(manifest *pluginv1.PluginManifest) error {
return validateManifestShared(manifest)
}
func LoadManifestFile(path string) (*pluginv1.PluginManifest, error) {
manifest, err := publicmanifest.LoadFromDisk(path)
if err != nil {
return nil, fmt.Errorf("load manifest file %q: %w", path, err)
}
if err := ValidateManifest(manifest); err != nil {
return nil, err
}
return manifest, nil
}
func InstalledManifestPath(installPath string) string {
return filepath.Join(filepath.Dir(installPath), "manifest.json")
}
func validateRelativePath(path string, label string) error {
if path == "" {
return fmt.Errorf("%s path is required", label)
}
cleaned := filepath.Clean(path)
if filepath.IsAbs(cleaned) || cleaned == "." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) || cleaned == ".." {
return fmt.Errorf("%s path %q must stay within the plugin package", label, path)
}
return nil
}
func capabilityKey(kind, id string) string {
return kind + "\x00" + id
}
func validateManifestShared(manifest *pluginv1.PluginManifest) error {
if err := publicmanifest.Validate(manifest); err != nil {
return err
}
if manifest.GetSiloApiVersion() == "" {
return fmt.Errorf("plugin manifest silo_api_version is required")
}
if len(manifest.GetCapabilities()) == 0 {
return fmt.Errorf("plugin manifest capabilities are required")
}
seenCapabilities := make(map[string]struct{}, len(manifest.GetCapabilities()))
for _, capability := range manifest.GetCapabilities() {
key := capabilityKey(capability.GetType(), capability.GetId())
if _, ok := seenCapabilities[key]; ok {
return fmt.Errorf("duplicate plugin capability %s", key)
}
seenCapabilities[key] = struct{}{}
}
seenAssets := make(map[string]struct{}, len(manifest.GetAssets()))
for _, asset := range manifest.GetAssets() {
if err := validateRelativePath(asset.GetPath(), "plugin asset"); err != nil {
return err
}
if _, ok := seenAssets[asset.GetPath()]; ok {
return fmt.Errorf("duplicate plugin asset %q", asset.GetPath())
}
seenAssets[asset.GetPath()] = struct{}{}
}
for _, route := range manifest.GetHttpRoutes() {
if route == nil {
continue
}
switch route.GetAccess() {
case "public", "authenticated", "admin":
default:
return fmt.Errorf("plugin route %q must declare access as public, authenticated, or admin", route.GetPath())
}
if !strings.HasPrefix(route.GetPath(), "/") {
return fmt.Errorf("plugin route %q must start with /", route.GetPath())
}
if route.GetStaticAsset() && route.GetMethod() != "GET" {
return fmt.Errorf("plugin static asset route %q must use GET", route.GetPath())
}
}
return nil
}
func validateHostShellPageContract(manifest *pluginv1.PluginManifest) error {
for _, route := range manifest.GetHttpRoutes() {
if route == nil || !route.GetNavigable() {
continue
}
if route.GetAccess() == "public" {
return fmt.Errorf("plugin navigable route %q must not declare public access", route.GetPath())
}
}
return nil
}