Feat v0.6.6 final hardening
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Failing after 2m41s
CI/CD / test-sqlite (pull_request) Failing after 2m48s
CI/CD / build-and-deploy (pull_request) Has been skipped
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Failing after 2m41s
CI/CD / test-sqlite (pull_request) Failing after 2m48s
CI/CD / build-and-deploy (pull_request) Has been skipped
Final pass before public release — security, correctness, developer experience. - ValidateManifest() gate: centralized manifest validation (12 unit tests) - Extension dependency auto-activation from bundled packages - OIDC nonce validation: ID token nonce checked against stored state - Schema migration stub replaced with log-only additive policy - OptionalAuth middleware for anonymous workflow visitor routes - Package signing schema reservation (signature field + env var) - PublishAsync event bus counter fix - Health UI tooltips explaining published vs delivered gap - ICD/SDK runner updated for v0.6.x endpoints (metrics, cluster, backups, OpenAPI) - Version bump, ROADMAP, CHANGELOG Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
154
server/handlers/package_validate.go
Normal file
154
server/handlers/package_validate.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// validManifestID matches lowercase alphanumeric slugs with optional hyphens.
|
||||
// Duplicated from packages.go for use in standalone validation.
|
||||
var validManifestID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
|
||||
|
||||
// ManifestInfo holds the parsed and validated fields from a manifest.json.
|
||||
type ManifestInfo struct {
|
||||
ID string
|
||||
Title string
|
||||
Type string // surface, extension, full, workflow, library
|
||||
Version string
|
||||
Description string
|
||||
Author string
|
||||
Tier string
|
||||
SchemaVersion int
|
||||
HasRoute bool
|
||||
HasTools bool
|
||||
HasPipes bool
|
||||
HasHooks bool
|
||||
HasSettings bool
|
||||
HasExports bool
|
||||
Dependencies map[string]any
|
||||
Requires []string
|
||||
Signature string // reserved for future package signing
|
||||
}
|
||||
|
||||
// ValidateManifest parses a manifest map and validates all required fields,
|
||||
// type constraints, and structural rules. Returns a ManifestInfo or an error
|
||||
// describing the first validation failure.
|
||||
func ValidateManifest(manifest map[string]any) (*ManifestInfo, error) {
|
||||
if manifest == nil {
|
||||
return nil, fmt.Errorf("manifest is nil")
|
||||
}
|
||||
|
||||
info := &ManifestInfo{}
|
||||
|
||||
// ── Required fields ──────────────────────────────────────────
|
||||
info.ID, _ = manifest["id"].(string)
|
||||
info.Title, _ = manifest["title"].(string)
|
||||
if info.ID == "" || info.Title == "" {
|
||||
return nil, fmt.Errorf("manifest must have 'id' and 'title'")
|
||||
}
|
||||
|
||||
// ── Package ID slug validation ───────────────────────────────
|
||||
if !validManifestID.MatchString(info.ID) {
|
||||
return nil, fmt.Errorf("package id must be a lowercase slug (a-z, 0-9, hyphens, 2-64 chars)")
|
||||
}
|
||||
|
||||
// ── Type validation ──────────────────────────────────────────
|
||||
info.Type, _ = manifest["type"].(string)
|
||||
if info.Type == "" {
|
||||
info.Type = "surface" // backward compat
|
||||
}
|
||||
validTypes := map[string]bool{
|
||||
"surface": true, "extension": true, "full": true,
|
||||
"workflow": true, "library": true,
|
||||
}
|
||||
if !validTypes[info.Type] {
|
||||
return nil, fmt.Errorf("manifest type must be 'surface', 'extension', 'full', 'workflow', or 'library'")
|
||||
}
|
||||
|
||||
// ── Extract optional fields ──────────────────────────────────
|
||||
info.Version, _ = manifest["version"].(string)
|
||||
if info.Version == "" {
|
||||
info.Version = "0.0.0"
|
||||
}
|
||||
info.Description, _ = manifest["description"].(string)
|
||||
info.Author, _ = manifest["author"].(string)
|
||||
info.Tier, _ = manifest["tier"].(string)
|
||||
if info.Tier == "" {
|
||||
info.Tier = "browser"
|
||||
}
|
||||
info.Signature, _ = manifest["signature"].(string)
|
||||
|
||||
info.HasRoute = manifest["route"] != nil || manifest["routes"] != nil
|
||||
info.HasTools = manifest["tools"] != nil
|
||||
info.HasPipes = manifest["pipes"] != nil
|
||||
info.HasHooks = manifest["hooks"] != nil
|
||||
info.HasSettings = manifest["settings"] != nil
|
||||
info.HasExports = false
|
||||
|
||||
if deps, ok := manifest["dependencies"].(map[string]any); ok {
|
||||
info.Dependencies = deps
|
||||
}
|
||||
if reqs, ok := manifest["requires"].([]any); ok {
|
||||
for _, r := range reqs {
|
||||
if s, ok := r.(string); ok && s != "" {
|
||||
info.Requires = append(info.Requires, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info.SchemaVersion = ParseSchemaVersion(manifest)
|
||||
|
||||
// ── Type-specific constraints ────────────────────────────────
|
||||
hasExtBehavior := info.HasTools || info.HasPipes || info.HasHooks
|
||||
|
||||
switch info.Type {
|
||||
case "surface":
|
||||
// route is optional for surfaces
|
||||
case "extension":
|
||||
if !hasExtBehavior {
|
||||
return nil, fmt.Errorf("extension packages require at least one of: tools, pipes, hooks")
|
||||
}
|
||||
if info.HasRoute {
|
||||
return nil, fmt.Errorf("extension packages cannot have a route — use type 'full' for both")
|
||||
}
|
||||
case "full":
|
||||
if !info.HasRoute {
|
||||
return nil, fmt.Errorf("full packages require a route")
|
||||
}
|
||||
if !hasExtBehavior && !info.HasSettings {
|
||||
return nil, fmt.Errorf("full packages require at least one of: tools, pipes, hooks, settings")
|
||||
}
|
||||
case "workflow":
|
||||
if manifest["workflow_definition"] == nil {
|
||||
return nil, fmt.Errorf("workflow packages require a 'workflow_definition' in the manifest")
|
||||
}
|
||||
case "library":
|
||||
exports, _ := manifest["exports"].([]any)
|
||||
if len(exports) == 0 {
|
||||
return nil, fmt.Errorf("library packages require an 'exports' array")
|
||||
}
|
||||
info.HasExports = true
|
||||
if info.HasTools || info.HasPipes {
|
||||
return nil, fmt.Errorf("library packages cannot have tools or pipes")
|
||||
}
|
||||
if info.HasRoute {
|
||||
return nil, fmt.Errorf("library packages cannot have a route")
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid manifest.json: %w", err)
|
||||
}
|
||||
info, err := ValidateManifest(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return manifest, info, nil
|
||||
}
|
||||
Reference in New Issue
Block a user