Files
barkstack/apps/console/server/manifest.go
Shaun Campbell ee80e3322f
Some checks failed
Deploy Website / deploy (push) Successful in 2m7s
Publish Edge Images / images (apps/console/Dockerfile, barkstack) (push) Successful in 12m9s
Publish Edge Images / images (services/pawsql/Dockerfile, pawsql) (push) Successful in 13m7s
Test and Release Barkstack / test (push) Failing after 2m32s
Verify Platform / verify (push) Failing after 48s
Verify Platform / images (apps/console/Dockerfile, console) (push) Has been skipped
Verify Platform / images (services/pawsql/Dockerfile, pawsql) (push) Has been skipped
Verify Platform / images (services/treatvault/Dockerfile, treatvault) (push) Has been skipped
Test and Release Barkstack / release (push) Has been skipped
Publish Edge Images / images (services/treatvault/Dockerfile, treatvault) (push) Successful in 8m50s
docs: track the coding guidelines and finish tree hygiene
AGENT.md now points at .ai/guidelines, so the guidelines move into the
repository and the reference resolves on a fresh clone. Completes the
exported doc comments in the console manifest and registry left from
the lint pass, and ignores local scratch files (.DS_Store, sdkscratch).

The root Barkfile's include files (Barkfile.campbellwireless,
Barkfile.ruckstack) stay local on purpose: they are personal
infrastructure and Barkfile.campbellwireless currently does not parse
(a treatvault block inside a project block, which the parser rejects).
2026-09-19 18:58:08 -04:00

115 lines
3.9 KiB
Go

package console
import (
"fmt"
"net/url"
"path"
"regexp"
"strings"
)
// PluginAPIVersion is the manifest API version this console server accepts.
const PluginAPIVersion = "barkstack.dev/ui/v1"
var (
pluginIDPattern = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
elementPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$`)
)
// PluginEndpoint is one statically configured plugin backend.
type PluginEndpoint struct {
ID string
BaseURL string
}
// NavigationItem is one navigation entry a plugin contributes to the
// console sidebar.
type NavigationItem struct {
Label string `json:"label"`
Path string `json:"path"`
Icon string `json:"icon,omitempty"`
}
// PluginManifest is the manifest document a plugin serves at its manifest
// endpoint.
type PluginManifest struct {
APIVersion string `json:"apiVersion"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
Mount string `json:"mount"`
Entry string `json:"entry,omitempty"`
Element string `json:"element,omitempty"`
Navigation []NavigationItem `json:"navigation"`
Available bool `json:"available"`
Error string `json:"error,omitempty"`
}
// ValidateManifest checks a fetched manifest against the Barkfile-configured
// plugin ID and the naming rules for mounts, entries, and elements.
func ValidateManifest(configuredID string, manifest PluginManifest) error {
if manifest.APIVersion != PluginAPIVersion {
return fmt.Errorf("unsupported API version %q", manifest.APIVersion)
}
if !pluginIDPattern.MatchString(manifest.ID) {
return fmt.Errorf("invalid plugin ID %q", manifest.ID)
}
if manifest.ID != configuredID {
return fmt.Errorf("configured plugin ID %q does not match manifest ID %q", configuredID, manifest.ID)
}
if manifest.Name == "" {
return fmt.Errorf("plugin name is required")
}
if !validConsolePath(manifest.Mount) || manifest.Mount == "/" {
return fmt.Errorf("invalid mount path %q", manifest.Mount)
}
entry, err := url.Parse(manifest.Entry)
if err != nil || entry.Scheme != "" || entry.Host != "" || entry.User != nil || entry.RawQuery != "" || entry.Fragment != "" || !strings.HasPrefix(entry.Path, "/barkstack/ui/") || path.Clean(entry.Path) != entry.Path {
return fmt.Errorf("invalid plugin entry path %q", manifest.Entry)
}
if !elementPattern.MatchString(manifest.Element) {
return fmt.Errorf("invalid custom-element name %q", manifest.Element)
}
if len(manifest.Navigation) == 0 {
return fmt.Errorf("at least one navigation item is required")
}
for index, item := range manifest.Navigation {
if item.Label == "" {
return fmt.Errorf("navigation item %d has no label", index)
}
if !validConsolePath(item.Path) || (item.Path != manifest.Mount && !strings.HasPrefix(item.Path, manifest.Mount+"/")) {
return fmt.Errorf("navigation path %q is outside mount %q", item.Path, manifest.Mount)
}
}
return nil
}
func validConsolePath(value string) bool {
if value == "" || !strings.HasPrefix(value, "/") || strings.HasPrefix(value, "//") || strings.Contains(value, "\\") {
return false
}
parsed, err := url.Parse(value)
return err == nil && parsed.Path == value && parsed.RawQuery == "" && parsed.Fragment == "" && path.Clean(value) == value
}
func publicManifest(manifest PluginManifest) PluginManifest {
manifest.Entry = "/plugins/" + manifest.ID + strings.TrimPrefix(manifest.Entry, "/barkstack")
manifest.Available = true
manifest.Error = ""
return manifest
}
func unavailableManifest(id string) PluginManifest {
name := strings.ToUpper(id[:1]) + id[1:]
return PluginManifest{
APIVersion: PluginAPIVersion,
ID: id,
Name: name,
Mount: "/" + id,
Navigation: []NavigationItem{{Label: name, Path: "/" + id}},
Available: false,
Error: "Service is unreachable from Barkstack.",
}
}