Feat v0.9.0 multi surface packages (#73)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m46s
CI/CD / test-sqlite (push) Successful in 2m57s
CI/CD / build-and-deploy (push) Successful in 44s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #73.
This commit is contained in:
2026-04-03 12:40:47 +00:00
committed by xcaliber
parent 98fd3eb3e6
commit 5ad6d77c56
12 changed files with 1075 additions and 62 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
// validManifestID matches lowercase alphanumeric slugs with optional hyphens.
@@ -21,6 +22,7 @@ type ManifestInfo struct {
Tier string
SchemaVersion int
HasRoute bool
HasSurfaces bool
HasTools bool
HasPipes bool
HasHooks bool
@@ -81,7 +83,64 @@ func ValidateManifest(manifest map[string]any) (*ManifestInfo, error) {
}
info.Signature, _ = manifest["signature"].(string)
info.HasRoute = manifest["route"] != nil || manifest["routes"] != nil
// ── Surfaces validation ─────────────────────────────────────
if rawSurfaces, ok := manifest["surfaces"].([]any); ok {
if len(rawSurfaces) == 0 {
return nil, fmt.Errorf("surfaces array cannot be empty")
}
seen := map[string]bool{}
for i, raw := range rawSurfaces {
s, ok := raw.(map[string]any)
if !ok {
return nil, fmt.Errorf("surfaces[%d] must be an object", i)
}
path, _ := s["path"].(string)
if path == "" {
return nil, fmt.Errorf("surfaces[%d] requires a path", i)
}
if !strings.HasPrefix(path, "/") {
return nil, fmt.Errorf("surfaces[%d] path must start with /", i)
}
if seen[path] {
return nil, fmt.Errorf("duplicate surface path: %s", path)
}
seen[path] = true
if access, ok := s["access"].(string); ok {
if !validAccessLevels(access) {
return nil, fmt.Errorf("surfaces[%d] invalid access: %s", i, access)
}
}
}
info.HasSurfaces = true
info.HasRoute = true
} else if manifest["surfaces"] != nil {
// surfaces key present but not an array
return nil, fmt.Errorf("surfaces must be an array")
} else {
// No surfaces declared — synthesize from legacy fields.
// This ensures every routable package always has a surfaces array.
if info.Type == "surface" || info.Type == "full" {
auth, _ := manifest["auth"].(string)
if auth == "" {
auth = "authenticated"
}
layout, _ := manifest["layout"].(string)
if layout == "" {
layout = "single"
}
manifest["surfaces"] = []any{
map[string]any{
"path": "/",
"access": auth,
"layout": layout,
"title": info.Title,
},
}
info.HasSurfaces = true
}
info.HasRoute = manifest["route"] != nil || manifest["routes"] != nil || info.HasSurfaces
}
info.HasTools = manifest["tools"] != nil
info.HasPipes = manifest["pipes"] != nil
info.HasHooks = manifest["hooks"] != nil
@@ -156,6 +215,18 @@ func ValidateManifest(manifest map[string]any) (*ManifestInfo, error) {
return info, nil
}
// validAccessLevels checks whether an access string is a recognized value.
func validAccessLevels(access string) bool {
switch {
case access == "public", access == "authenticated", access == "admin":
return true
case strings.HasPrefix(access, "group:"):
return strings.TrimPrefix(access, "group:") != ""
default:
return false
}
}
// ValidateManifestJSON parses raw JSON bytes into a manifest map and validates.
func ValidateManifestJSON(data []byte) (map[string]any, *ManifestInfo, error) {
var manifest map[string]any