Feat v0.6.6 final hardening (#41)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #41.
This commit is contained in:
@@ -29,10 +29,11 @@ var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
|
||||
// PackageHandler manages unified package lifecycle (admin-only).
|
||||
// Replaces SurfaceHandler.
|
||||
type PackageHandler struct {
|
||||
stores store.Stores
|
||||
packagesDir string // e.g. /data/packages — where archives are extracted
|
||||
sandbox *sandbox.Sandbox
|
||||
runner *sandbox.Runner
|
||||
stores store.Stores
|
||||
packagesDir string // e.g. /data/packages — where archives are extracted
|
||||
bundledDir string // e.g. /app/bundled-packages — .pkg archive source
|
||||
sandbox *sandbox.Sandbox
|
||||
runner *sandbox.Runner
|
||||
}
|
||||
|
||||
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
||||
@@ -43,6 +44,12 @@ func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
||||
return &PackageHandler{stores: s, packagesDir: dir}
|
||||
}
|
||||
|
||||
// SetBundledDir sets the path to the bundled packages directory.
|
||||
// Used by dependency auto-activation to resolve missing dependencies.
|
||||
func (h *PackageHandler) SetBundledDir(dir string) {
|
||||
h.bundledDir = dir
|
||||
}
|
||||
|
||||
// SetSandbox attaches a Starlark sandbox for schema migrations.
|
||||
func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
|
||||
h.sandbox = sb
|
||||
@@ -319,79 +326,22 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
pkgID, _ := manifest["id"].(string)
|
||||
title, _ := manifest["title"].(string)
|
||||
if pkgID == "" || title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest must have 'id' and 'title'"})
|
||||
// Validate manifest structure (required fields, type, constraints)
|
||||
mInfo, err := ValidateManifest(manifest)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
pkgID := mInfo.ID
|
||||
title := mInfo.Title
|
||||
pkgType := mInfo.Type
|
||||
|
||||
// Validate package ID is a safe slug
|
||||
if !validPackageID.MatchString(pkgID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "package id must be a lowercase slug (a-z, 0-9, hyphens, 2-64 chars)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Determine type (default: surface for backward compat)
|
||||
pkgType, _ := manifest["type"].(string)
|
||||
if pkgType == "" {
|
||||
pkgType = "surface"
|
||||
}
|
||||
if pkgType != "surface" && pkgType != "extension" && pkgType != "full" && pkgType != "workflow" && pkgType != "library" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest type must be 'surface', 'extension', 'full', 'workflow', or 'library'"})
|
||||
return
|
||||
}
|
||||
|
||||
// Type-specific validation
|
||||
hasRoute := manifest["route"] != nil || manifest["routes"] != nil
|
||||
hasTools := manifest["tools"] != nil
|
||||
hasPipes := manifest["pipes"] != nil
|
||||
hasHooks := manifest["hooks"] != nil
|
||||
hasSettings := manifest["settings"] != nil
|
||||
hasExtBehavior := hasTools || hasPipes || hasHooks
|
||||
|
||||
switch pkgType {
|
||||
case "surface":
|
||||
// route is optional for surfaces (core surfaces don't always have it in manifest)
|
||||
case "extension":
|
||||
if !hasExtBehavior {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "extension packages require at least one of: tools, pipes, hooks"})
|
||||
return
|
||||
}
|
||||
if hasRoute {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "extension packages cannot have a route — use type 'full' for both"})
|
||||
return
|
||||
}
|
||||
case "full":
|
||||
if !hasRoute {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require a route"})
|
||||
return
|
||||
}
|
||||
// or a settings schema. A surface-with-settings is a valid "full" package
|
||||
// (e.g. editor: browser-tier surface + admin-configurable settings).
|
||||
if !hasExtBehavior && !hasSettings {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require at least one of: tools, pipes, hooks, settings"})
|
||||
return
|
||||
}
|
||||
case "workflow":
|
||||
if manifest["workflow_definition"] == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow packages require a 'workflow_definition' in the manifest"})
|
||||
return
|
||||
}
|
||||
case "library":
|
||||
exports, _ := manifest["exports"].([]any)
|
||||
if len(exports) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages require an 'exports' array"})
|
||||
return
|
||||
}
|
||||
if hasTools || hasPipes {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages cannot have tools or pipes"})
|
||||
return
|
||||
}
|
||||
if hasRoute {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages cannot have a route"})
|
||||
return
|
||||
// Package signing hook (schema reserved — no crypto verification yet)
|
||||
if os.Getenv("PACKAGE_VERIFY_SIGNATURES") == "true" {
|
||||
if mInfo.Signature != "" {
|
||||
log.Printf("[packages] %s: signature field present (%s) — verification not yet implemented", pkgID, mInfo.Signature)
|
||||
} else {
|
||||
log.Printf("[packages] %s: unsigned package (PACKAGE_VERIFY_SIGNATURES is enabled)", pkgID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,17 +393,11 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
log.Printf("[packages] Extracted %s to %s", pkgID, destDir)
|
||||
}
|
||||
|
||||
// Extract manifest fields for DB columns
|
||||
version, _ := manifest["version"].(string)
|
||||
if version == "" {
|
||||
version = "0.0.0"
|
||||
}
|
||||
description, _ := manifest["description"].(string)
|
||||
author, _ := manifest["author"].(string)
|
||||
tier, _ := manifest["tier"].(string)
|
||||
if tier == "" {
|
||||
tier = "browser"
|
||||
}
|
||||
// Use validated manifest fields for DB columns
|
||||
version := mInfo.Version
|
||||
description := mInfo.Description
|
||||
author := mInfo.Author
|
||||
tier := mInfo.Tier
|
||||
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
@@ -543,6 +487,7 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Libraries must be installed first; consumers declare them.
|
||||
// If a dependency is missing but available as a bundled package, auto-install it.
|
||||
if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 {
|
||||
// Clear stale dependency records from a previous install of the same consumer.
|
||||
if h.stores.Dependencies != nil {
|
||||
@@ -551,8 +496,25 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
for libID, vSpec := range deps {
|
||||
lib, err := h.stores.Packages.Get(c.Request.Context(), libID)
|
||||
if err != nil || lib == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency not found: " + libID})
|
||||
return
|
||||
// Attempt auto-install from bundled packages
|
||||
if h.bundledDir != "" {
|
||||
pkgPath := filepath.Join(h.bundledDir, libID+".pkg")
|
||||
if _, statErr := os.Stat(pkgPath); statErr == nil {
|
||||
log.Printf("[packages] auto-installing bundled dependency %s for %s", libID, pkgID)
|
||||
if _, installErr := installBundledPackage(pkgPath, h.packagesDir, h.stores, h.runner); installErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("dependency %s auto-install failed: %v", libID, installErr),
|
||||
})
|
||||
return
|
||||
}
|
||||
// Re-fetch after install
|
||||
lib, err = h.stores.Packages.Get(c.Request.Context(), libID)
|
||||
}
|
||||
}
|
||||
if lib == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency not found: " + libID + " (not installed and not available as a bundled package)"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if lib.Type != "library" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency is not a library: " + libID})
|
||||
|
||||
Reference in New Issue
Block a user