Help us shape the future of Go! Take the 2026 Go Developer Survey

packages

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 52 Imported by: 0

Documentation

Overview

Package packages implements the .hmp package format (a zip holding a plugin or a theme), its zip-safety rules, ed25519 signing, the signed registry index and the Postgres store plus manager that installs, activates and removes packages while the server runs. See docs/PACKAGES.md.

Index

Constants

View Source
const (
	PluginManifest = "plugin.json"
	ThemeManifest  = "theme.json"
	PluginWasm     = "plugin.wasm"
	SignatureFile  = "signature.json"
	// Extension is the file extension of a package.
	Extension = ".hmp"
)

Well-known entries of a package.

View Source
const (
	PluginAssetCSS = "assets/plugin.css"
	PluginAssetJS  = "assets/plugin.js"
)

Public asset files a plugin may ship; pages link them when present.

View Source
const (
	TierFirstParty = "first-party"
	TierCommunity  = "community"
)

Package tiers an Entry may carry.

View Source
const (
	MaxScreenshotBytes = 2 << 20
	MinShotW, MaxShotW = 640, 2560
	MinShotH, MaxShotH = 360, 1600
	MaxShotPixels      = 4_200_000
)

Screenshot limits for the optional theme.json "screenshot" key.

View Source
const (
	StateInstalled    = "installed"
	StateUninstalling = "uninstalling"
)

Package row states.

View Source
const Algorithm = "ed25519"

Algorithm is the only signature algorithm of packages and registry indexes.

View Source
const DefaultIndexValidity = 90 * 24 * time.Hour

DefaultIndexValidity is how long a freshly built registry index is believed.

View Source
const DefaultTheme = "default"

DefaultTheme names the built-in theme that ships inside the binary. It is always available and is what runs when no theme package is active.

View Source
const ReservedPluginName = "webhooks"

ReservedPluginName is the name of the former first-party webhooks plugin. Webhooks are core now and the boot step retires a plugin by this name, so no new plugin may take it.

Variables

View Source
var (
	ErrUnsignedNotAllowed = errors.New("this package is not signed; installing unsigned packages is switched off (an owner can allow it in Admin > Plugins)")
	ErrDevDisabled        = errors.New("development packages are disabled in production")
	ErrThemeActive        = errors.New("this theme is active; activate another theme first")
	ErrReservedName       = errors.New("that name is reserved (the built-in theme, or the webhooks that moved into core)")
	ErrPermission         = errors.New("permission not requested by the plugin")
	ErrUnknownSource      = errors.New("unknown package source")
	ErrUninstalling       = errors.New("this package is being uninstalled; finish the uninstall first")
	ErrSignerNotAllowed   = errors.New("only first-party packages can be installed here (public demo)")
)

Errors the admin and the CLI report to the owner.

View Source
var (
	ErrIndexExpired  = errors.New("registry index has expired")
	ErrIndexNotFresh = errors.New("registry index has no valid issued_at / expires_at")
	ErrInsecureURL   = errors.New("registry URLs must use https")
)

Registry index refusals.

View Source
var (
	ErrUnsigned     = errors.New("package is not signed")
	ErrUntrustedKey = errors.New("package is signed by a key you do not trust")
	ErrBadSignature = errors.New("package signature does not match its contents")
)

Signature errors Verify answers; the manager shows them as they are.

View Source
var DefaultLimits = Limits{
	Archive: 64 << 20, Entries: 2048, File: 64 << 20, Total: 128 << 20,
	Ratio: 200, RatioFloor: 1 << 20, Path: 200,
}

DefaultLimits are generous for real plugins (a wasm module is the big file) and far below what a bomb needs.

View Source
var ErrCommunityScripts = pluginhost.ErrCommunityScripts

ErrCommunityScripts is the one sentinel for errors.Is across layers.

View Source
var ErrNotFound = errors.New("package not installed")

ErrNotFound is returned for a package that is not installed.

View Source
var ErrPermNotRequested = errors.New("packages: permission not requested by the plugin")

ErrPermNotRequested is what SetGrants answers for a permission the manifest lacks.

View Source
var ErrRegistryOff = errors.New("no registry is configured")

ErrRegistryOff means no registry is configured.

View Source
var FirstPartyKeyIDs = []string{"a592a1276b3947b1", "adea8e00f5ac74ca"}

FirstPartyKeyIDs are the key ids first-party packages were ever signed under: the key rotated out on 2026-10-16 (docs/PACKAGES.md) and the one that replaced it. config.FirstPartyKey is checked as well. The list is fixed so a later rotation does not strand a package signed under an earlier key.

View Source
var FirstPartyPlugins = []string{"searchconsole", "gtm"}

FirstPartyPlugins are the plugins the core team maintains. The list is the rule; the author is not checked.

Functions

func BuildIndex

func BuildIndex(dir, urlPrefix string, key Key, now time.Time, validity time.Duration) ([]byte, error)

BuildIndex lists every .hmp in dir as a registry Entry whose URL is urlPrefix + file name (a relative prefix such as "packages/" keeps the registry movable), and wraps the index in an envelope signed with key. It is what `hatamoto registry index` runs. Packages are listed by name.

validity must be positive and at most maxIndexLifetime (180 days); the window is counted from issued_at (one minute before now, to forgive clock skew), so the longest index still passes the freshness check servers apply.

func BuildIndexWith

func BuildIndexWith(dir, urlPrefix string, key Key, now time.Time, validity time.Duration, opts BuildOptions) ([]byte, error)

BuildIndexWith is BuildIndex plus optional per-package metadata. Package names must be unique per kind (a theme and a plugin may share one). Meta is keyed by bare name: every Meta name must match a package in dir, and a name carried by both a plugin and a theme cannot take Meta.

func CheckFresh

func CheckFresh(what, issuedAt, expiresAt string, now time.Time, maxLifetime time.Duration) error

CheckFresh refuses a document whose RFC 3339 window is malformed, longer than maxLifetime, issued in the future (beyond a 10 minute skew) or already expired at now. what names the document in errors. Failures wrap ErrIndexNotFresh or ErrIndexExpired.

func CompareVersions

func CompareVersions(a, b string) int

CompareVersions orders two versions: -1, 0 or 1. Both must be ValidSemver; an unparsable version compares as equal to everything.

func FetchURL

func FetchURL(ctx context.Context, c *http.Client, u string, max int64) ([]byte, error)

Fetch reads a package from a URL (`hatamoto package install <url>`), bounded.

func IsFirstPartyKey

func IsFirstPartyKey(id string) bool

IsFirstPartyKey reports whether id is a first-party signing key id.

func KeyID

func KeyID(pub ed25519.PublicKey) string

KeyID names a public key: the first 16 hex characters of its SHA-256.

func Newer

func Newer(a, b string) bool

Newer reports whether version a is newer than b. Unparsable versions are never newer.

func PackDir

func PackDir(dir string, key *Key) ([]byte, error)

PackDir builds a .hmp from a plugin or theme directory. Only the files a package may hold are included (dot files, node_modules and the like are skipped, not refused). With a key the archive is signed. The result is checked with Parse before it is returned, so what is built is installable.

func PackFiles

func PackFiles(in map[string][]byte, key *Key) ([]byte, error)

PackFiles builds a .hmp from a file map, deterministically (sorted, fixed timestamps), signing it when key is set.

func ParseMeta

func ParseMeta(raw []byte) (map[string]Meta, error)

ParseMeta reads a JSON object of package name to Meta. Unknown fields are refused so a typo cannot silently drop a value.

func ParsePublicKey

func ParsePublicKey(s string) (ed25519.PublicKey, error)

ParsePublicKey decodes a base64 ed25519 public key.

func ReadArchive

func ReadArchive(data []byte, wantSHA256 string, lim Limits) (map[string][]byte, error)

ReadArchive verifies the sha256 of the exact bytes of a zip, then returns its files as a path->bytes map for PackFiles. Everything happens in memory: no entry is extracted and no entry name reaches the file system. It refuses a sha mismatch, unsafe names (checkName), links and special files, duplicate entries (case-insensitive) and anything over lim (entries, file, total, ratio), measured on the bytes actually read. Directories are skipped. When theme.json is not at the root, ONE common wrapper directory is stripped.

func RenderPage

func RenderPage(idx *Index, opts PageOptions) ([]byte, error)

RenderPage renders the plugin entries of idx (sorted as in the index) as one self-contained HTML page: inline style and script, no remote assets. The index is expected to come from ParseIndex.

func SafeHTTPS

func SafeHTTPS(s string) string

SafeHTTPS returns s when it is an https URL with a host and no credentials, and "" otherwise, so a hostile value never becomes a link.

func ScreenshotExt

func ScreenshotExt(rel string) string

ScreenshotExt returns the normalised extension (".png", ".webp" or ".jpg") of rel.

func ServeFile

func ServeFile(w http.ResponseWriter, r *http.Request, p *Package, name, cacheControl, csp string)

ServeFile writes one file of the package (GET and HEAD). name is a slash path inside the package; anything that is not a regular file is a 404. Responses are marked nosniff and, as untrusted content, sandboxed: an SVG or HTML file opened directly runs without script and without the site's origin, unless the caller passes its own csp.

func SignIndex

func SignIndex(index []byte, k Key) ([]byte, error)

SignIndex wraps index (the JSON of an Index) in a signed envelope.

func ValidSemver

func ValidSemver(v string) bool

ValidSemver reports whether v parses as a version Newer understands ("v1.2.3", optional pre-release and build metadata).

func ValidateScreenshot

func ValidateScreenshot(rel string, files map[string][]byte) error

ValidateScreenshot checks that rel names an allowed raster image under assets/ in files, within the size and dimension limits. Only the image header is read.

func WithActor

func WithActor(ctx context.Context, a Actor) context.Context

WithActor returns ctx carrying who is making package changes; the audit log records it. Without one the actor is "unknown".

Types

type Actor

type Actor struct {
	ID    uuid.UUID // uuid.Nil for the CLI
	Email string    // "cli" for the CLI
}

Actor is who made a change: a signed-in admin user, or the CLI.

type BuildOptions

type BuildOptions struct {
	Meta map[string]Meta
}

BuildOptions are the optional inputs of BuildIndexWith.

type Config

type Config struct {
	// AllowUnsigned lets unsigned packages be installed (the owner's explicit,
	// warned opt-in).
	AllowUnsigned bool
	// ActiveTheme is the installed theme in use; "" is the built-in default.
	ActiveTheme string
	// ServerEnvironment is the ENVIRONMENT of the server that last booted on
	// this database ("" before the first boot). The CLI reads it so that
	// "production" is the server's decision, not whatever the shell exports.
	ServerEnvironment string
}

Config is the owner's package policy and the active theme.

type ConfirmationRequired

type ConfirmationRequired struct {
	Kind                   Kind
	Name                   string
	OldVersion, NewVersion string
	// OldSigner and NewSigner are key ids ("" for unsigned).
	OldSigner, NewSigner string
	Downgrade            bool // the new version is not newer than the installed one
	SignerChange         bool // another signer than the installed package's
	// WidenedAccess is set when a plugin update reaches further than the
	// installed one: it lists web addresses the installed version did not
	// (NewHosts) or newly asks for http.fetch.any (NewFetchAny).
	WidenedAccess bool
	NewHosts      []string
	NewFetchAny   bool
	// NewIsland: the update newly asks for builder.island (JavaScript on the
	// site's pages); it counts as widened access too.
	NewIsland bool
	// NewScripts: the update newly asks for site.scripts.
	NewScripts bool
	Missing    []MissingSection
}

ConfirmationRequired is returned by Install when replacing the installed package needs the owner's explicit say-so: an older (or same) version, a different signer, or an update of the active theme that loses section types the content uses. Install again with InstallRequest.Confirm to go ahead.

func (*ConfirmationRequired) Error

func (e *ConfirmationRequired) Error() string

type DataDropper

type DataDropper interface {
	Drop(ctx context.Context, plugin string) error
}

DataDropper removes a plugin's own schema, role and stored data.

type Entry

type Entry struct {
	Kind        Kind   `json:"kind"`
	Name        string `json:"name"`
	Version     string `json:"version"`
	Description string `json:"description,omitempty"`
	// URL of the .hmp, absolute or relative to the registry index.
	URL string `json:"url"`
	// SHA256 is the hex hash of the .hmp (checked after the download).
	SHA256      string   `json:"sha256"`
	Size        int64    `json:"size,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
	Homepage    string   `json:"homepage,omitempty"`
	// Author, Source (repository URL), Commit (the pinned source revision) and
	// Tier describe where a package came from. All are optional: servers that
	// predate them ignore the keys.
	// Title is an optional display name for the directory page.
	Title  string `json:"title,omitempty"`
	Author string `json:"author,omitempty"`
	Source string `json:"source,omitempty"`
	Commit string `json:"commit,omitempty"`
	Tier   string `json:"tier,omitempty"`
	// Screenshot is a registry-relative path to a theme preview image.
	Screenshot string `json:"screenshot,omitempty"`
}

Entry is one package a registry offers.

type Index

type Index struct {
	Version int `json:"version"`
	// IssuedAt and ExpiresAt (RFC 3339) bound how long a signed index is
	// believed: an old signed copy cannot be replayed once it has expired.
	IssuedAt  string  `json:"issued_at"`
	ExpiresAt string  `json:"expires_at"`
	Packages  []Entry `json:"packages"`
}

Index is the payload of a registry.json envelope.

func ParseIndex

func ParseIndex(doc []byte, trust TrustSet, now time.Time) (*Index, error)

ParseIndex verifies a signed registry.json document against trust, checks its version and freshness at now and drops entries that are malformed. An unsigned, tampered, expired or untrusted index is refused. An unknown tier is cleared; the entry stays.

func (*Index) Find

func (idx *Index) Find(kind Kind, name string) (Entry, bool)

Find returns the entry for kind and name.

type InstallRequest

type InstallRequest struct {
	Source Source
	// Grant lists the permissions the owner approved at the permission prompt
	// (plugins). nil keeps the earlier grant on an update and grants nothing on
	// a new install.
	Grant []string
	// Confirm is the owner's answer to a ConfirmationRequired: replace the
	// installed package anyway.
	Confirm bool
}

InstallRequest says how to install a package.

type Installed

type Installed struct {
	Kind    Kind
	Name    string
	Version string
	Updated bool               // an older version was replaced
	Status  *pluginhost.Status // plugins, when a runtime is attached
}

Installed is the outcome of an install.

type Key

type Key struct {
	ID      string `json:"key_id"`
	Public  string `json:"public"`            // base64, 32 bytes
	Private string `json:"private,omitempty"` // base64, 64 bytes; absent from a public key file
}

Key is a signing key as `hatamoto keys generate` writes it.

func GenerateKey

func GenerateKey() (Key, error)

GenerateKey makes a new signing key.

func (Key) PrivateKey

func (k Key) PrivateKey() (ed25519.PrivateKey, error)

PrivateKey decodes the signing half.

func (Key) PublicKey

func (k Key) PublicKey() (ed25519.PublicKey, error)

PublicKey decodes the verifying half.

type Kind

type Kind string

Kind is what a package holds.

const (
	KindPlugin Kind = "plugin"
	KindTheme  Kind = "theme"
)

The two package kinds, named by their manifest: plugin.json or theme.json.

type Limits

type Limits struct {
	Archive int64 // size of the .hmp file
	Entries int   // files and directories
	File    int64 // one file, uncompressed
	Total   int64 // all files, uncompressed
	// Ratio is the largest uncompressed:compressed ratio of a file bigger than
	// RatioFloor (a zip bomb compresses far beyond any real asset).
	Ratio      int64
	RatioFloor int64
	Path       int // longest entry name
}

Limits bound what an archive may expand to. Parse enforces them on the bytes actually read, never on the sizes the archive declares.

type Manager

type Manager struct {
	Options
	// contains filtered or unexported fields
}

Manager installs, activates and removes packages while the server runs. The database is the source of truth; Reconcile makes the process match it.

func NewManager

func NewManager(o Options) *Manager

NewManager returns a Manager. Call Reconcile once at boot, then Run.

func (*Manager) Activate

func (m *Manager) Activate(ctx context.Context, name string, force bool) error

Activate makes an installed theme (or the built-in default, for "" or DefaultTheme) the site's theme. It first reports section types the theme lacks (MissingSectionsError) unless force is set, and refuses a theme that does not render.

func (*Manager) ActiveTheme

func (m *Manager) ActiveTheme() string

ActiveTheme is the theme the process is serving with (DefaultTheme for the built-in).

func (*Manager) Audit

func (m *Manager) Audit(ctx context.Context, action, target string, detail map[string]any)

Audit writes one row of hatamoto_admin_audit (and a log line) for a change to packages: install, update, uninstall, activate, grants, the unsigned opt-in. The detail never holds a secret.

func (*Manager) Blocked

func (m *Manager) Blocked(kind Kind, name string) string

Blocked says why an installed package is not loaded because of policy ("" when it is not blocked): shown in the admin so a package is not silently dead.

func (*Manager) CheckGrant

func (m *Manager) CheckGrant(plugin string, perms []sdk.Permission) error

CheckGrant is the pluginhost grant policy: a refusal when perms ask a community plugin for site.scripts or builder.island.

func (*Manager) CheckPolicy

func (m *Manager) CheckPolicy(ctx context.Context, p *Package, src Source) error

CheckPolicy says whether p may be installed from src: signed by a trusted key, or unsigned and allowed (the owner's opt-in, or a dev push outside production). The error is one of the Err* values.

func (*Manager) CommunityPlugin

func (m *Manager) CommunityPlugin(name string) bool

CommunityPlugin reports whether name is a community-origin plugin, by the same origins map as CommunityScriptsLive. It does not say whether its script is linked; the render path asks it about scripts a page body already links.

func (*Manager) CommunityScriptsLive

func (m *Manager) CommunityScriptsLive() bool

CommunityScriptsLive reports whether some community plugin's script is linked on pages right now (policy, grant, loaded and manifest all agree). It reads in-memory state only, so it is cheap enough for every HTML response.

func (*Manager) FetchFromRegistry

func (m *Manager) FetchFromRegistry(ctx context.Context, kind Kind, name string) ([]byte, error)

FetchFromRegistry downloads kind/name from the registry, checked against the hash the signed index lists and against the name it was listed under (a registry that serves another package under a listed name is lying).

func (*Manager) Install

func (m *Manager) Install(ctx context.Context, raw []byte, req InstallRequest) (*Installed, error)

Install parses, verifies and stores a package and, with a runtime or engine attached, applies it at once. Signed packages must be signed by a trusted key; unsigned ones are installed only when the owner allowed it (or for a dev push outside production).

func (*Manager) InstallFromRegistry

func (m *Manager) InstallFromRegistry(ctx context.Context, kind Kind, name string, req InstallRequest) (*Installed, error)

InstallFromRegistry downloads kind/name from the registry and installs it.

func (*Manager) LoadDevDir

func (m *Manager) LoadDevDir(ctx context.Context, dir string)

LoadDevDir loads plugins from a directory (HATAMOTO_PLUGIN_DIR, development only) as unsigned in-memory packages. They replace an installed package of the same name and are not stored.

func (*Manager) MarkupStopped

func (m *Manager) MarkupStopped(name string) []string

MarkupStopped lists the plugin's sections and partials that are not registered because they carry script constructs and it lacks site.scripts.

func (*Manager) MissingSections

func (m *Manager) MissingSections(ctx context.Context, theme string) ([]MissingSection, error)

MissingSections lists the section types used by content that theme (an installed theme, or "" / DefaultTheme) cannot render.

func (*Manager) NeedsConfirmation

func (m *Manager) NeedsConfirmation(ctx context.Context, p *Package, src Source) (*ConfirmationRequired, error)

NeedsConfirmation reports, without installing, whether replacing the installed package of the same name with p needs the owner's confirmation (nil when p is new or the replacement is a plain upgrade by the same signer).

func (*Manager) PluginPackage

func (m *Manager) PluginPackage(name string) *Package

PluginPackage returns the parsed package of a loaded plugin (its public assets and ui/ files), nil when it is not loaded.

func (*Manager) PreviewEngine

func (m *Manager) PreviewEngine(ctx context.Context, name string) (*render.Engine, error)

PreviewEngine returns an engine that renders with any installed theme (or the built-in default), whatever is active, cached until the package changes.

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context)

Reconcile makes the running process match the database: plugins that are installed and enabled are loaded (or reloaded when their package changed), removed or disabled ones are unloaded, and the active theme is applied.

func (*Manager) Rows

func (m *Manager) Rows(ctx context.Context) ([]Row, error)

Rows lists the installed packages, plugins first.

func (*Manager) Run

func (m *Manager) Run(ctx context.Context, every time.Duration)

Run reconciles every interval until ctx ends, so a package installed by the CLI (or another replica) goes live without a restart.

func (*Manager) SaveThemeSettings

func (m *Manager) SaveThemeSettings(ctx context.Context, name string, values map[string]any) ([]pluginhost.FieldError, error)

SaveThemeSettings validates values against the theme's settings_schema and stores them. Problems come back as field errors, not as an error. A change to the active theme's settings is live at once.

func (*Manager) ScriptsBlocked

func (m *Manager) ScriptsBlocked(name string) bool

ScriptsBlocked reports whether an installed plugin ships assets/plugin.js that pages do not link because the owner has not granted it site.scripts. A package without plugin.wasm never has its plugin.js linked (only a loaded plugin does), so it is not flagged.

func (*Manager) ScriptsRefused

func (m *Manager) ScriptsRefused(plugin string) bool

ScriptsRefused is scriptsPolicyRefuses for other packages (grant policy and the pluginhost script deny hook).

func (*Manager) SetAllowUnsigned

func (m *Manager) SetAllowUnsigned(ctx context.Context, on bool) error

SetAllowUnsigned stores the owner's opt-in for unsigned packages.

func (*Manager) SetEnabled

func (m *Manager) SetEnabled(ctx context.Context, name string, on bool) error

SetEnabled turns an installed plugin on or off without uninstalling it.

func (*Manager) SetGrants

func (m *Manager) SetGrants(ctx context.Context, name string, perms []sdk.Permission) error

SetGrants replaces the grants of an installed plugin the runtime does not know (a widget-only package has no wasm to load): perms must be listed in its manifest. The registries are reconciled so an island grant applies at once. ErrNotFound is returned for an unknown plugin.

func (*Manager) Store

func (m *Manager) Store() *Store

Store gives access to the underlying rows.

func (*Manager) ThemeError

func (m *Manager) ThemeError() string

ThemeError is why the active theme could not be applied ("" when it was).

func (*Manager) ThemeSettings

func (m *Manager) ThemeSettings(ctx context.Context, name string) (map[string]any, error)

ThemeSettings returns a theme's settings with the schema's defaults merged.

func (*Manager) ThemeSettingsSchema

func (m *Manager) ThemeSettingsSchema(ctx context.Context, name string) (json.RawMessage, error)

ThemeSettingsSchema is the settings_schema of an installed theme (nil: none).

func (*Manager) Uninstall

func (m *Manager) Uninstall(ctx context.Context, kind Kind, name string, dropData bool) error

Uninstall removes a package. A theme is deleted at once. A plugin goes in a recoverable order: its row is marked uninstalling (so it is unloaded and cannot be installed over), its permissions are revoked, with dropData its schema, role, stored values and settings are dropped, and only then is the row deleted. A failure leaves the row uninstalling; call Uninstall again. Core content is never touched.

func (*Manager) Usable

func (m *Manager) Usable(r Row) error

Usable is usable's exported form: nil when the stored row would be loaded by this instance's trust policy, the reason otherwise.

func (*Manager) WidgetCount

func (m *Manager) WidgetCount(name string) int

WidgetCount is widgetCount for callers outside the manager's lock: the widgets the last reconcile found in the plugin's package.

type Meta

type Meta struct {
	Title  string `json:"title,omitempty"`
	Author string `json:"author,omitempty"`
	Source string `json:"source,omitempty"`
	Commit string `json:"commit,omitempty"`
	Tier   string `json:"tier"`
	// Screenshot is screenshots/<name>.(png|webp|jpg); themes only.
	Screenshot string `json:"screenshot,omitempty"`
}

Meta says where a package came from. It is keyed by package name in BuildOptions.Meta and never changes the package's identity.

type MissingSection

type MissingSection struct {
	Type string
	// Uses is how many sections of that type the content holds.
	Uses int
}

MissingSection is a section type content uses that a theme does not render.

type MissingSectionsError

type MissingSectionsError struct{ Missing []MissingSection }

MissingSectionsError is returned by Activate when content uses section types the theme lacks and the activation was not forced.

func (*MissingSectionsError) Error

func (e *MissingSectionsError) Error() string

type Options

type Options struct {
	Pool     *pgxpool.Pool
	Trust    TrustSet
	Registry *Registry
	Plugins  PluginRuntime
	Grants   pluginhost.Grants
	Dropper  DataDropper
	Engine   ThemeEngine
	// NewPreview builds a second engine for previewing an installed theme.
	NewPreview func(layers []fs.FS, settings map[string]any) (*render.Engine, error)
	// AllowDev lets packages from `hatamoto theme dev` be installed and loaded.
	// It is the server's decision (never in production); dev rows are ignored
	// at load time when it is off, whatever was written to the database.
	AllowDev bool
	// OnlySigners, when not empty, limits installs to packages signed by one of
	// these key ids (the public demo allows first-party packages only). It
	// applies to every source and refuses unsigned packages whatever the
	// owner's opt-in says.
	OnlySigners []string
	// OnChange runs after what the site shows changed (theme, settings,
	// plugins): the server clears its page cache.
	OnChange func(ctx context.Context)
	Logger   *slog.Logger
}

Options wires a Manager. Plugins, Engine and Dropper are nil in the CLI, which only writes the database; a running server applies it within a few seconds (see Run).

type Origin

type Origin int

Origin says whose code a plugin is, for the community-script policy.

const (
	// OriginSelfBuilt is unsigned, dev, or signed by another owner-trusted key.
	OriginSelfBuilt Origin = iota
	// OriginFirstParty is signed by a first-party key and on FirstPartyPlugins.
	OriginFirstParty
	// OriginCommunity is signed by a first-party key but not on FirstPartyPlugins
	// (the registry signs community builds with the same key).
	OriginCommunity
)

func Classify

func Classify(name, signerKeyID string) Origin

Classify returns whose code the plugin name signed by signerKeyID is.

func ClassifyRow

func ClassifyRow(r Row) Origin

ClassifyRow classifies a stored package row.

type Package

type Package struct {
	Kind     Kind
	Name     string
	Version  string
	Manifest json.RawMessage // plugin.json or theme.json, as shipped
	// Digest is what the signature covers: a hash over every file name and
	// content hash except signature.json, so it does not depend on how the
	// zip was compressed.
	Digest [32]byte
	// Signature is the embedded signature.json, nil when the package is unsigned.
	Signature *Signature
	// ScreenshotErr is why theme.json's "screenshot" was ignored (nil: valid or
	// absent). Parse stays lenient so installed themes keep loading; publishing
	// paths (theme check --registry) refuse on it.
	ScreenshotErr error
	// contains filtered or unexported fields
}

Package is a parsed, validated .hmp. It is immutable.

func Parse

func Parse(raw []byte, lim Limits) (*Package, error)

Parse validates raw as a .hmp: zip safety (no traversal, links, odd names, oversized or highly compressed entries), the layout, and the manifest. It does not check the signature; see TrustSet.Verify and Manager.Install.

func (*Package) Data

func (p *Package) Data() []byte

Data is the archive bytes the package was parsed from.

func (*Package) FS

func (p *Package) FS() fs.FS

FS is the package's files (without signature.json). Reads decompress on demand; Parse already read every file once, so they cannot fail on a lying header.

func (*Package) Has

func (p *Package) Has(name string) bool

Has reports whether the package holds the file.

func (*Package) PluginManifest

func (p *Package) PluginManifest() (sdk.Manifest, error)

PluginManifest decodes plugin.json of a plugin package.

func (*Package) ReadFile

func (p *Package) ReadFile(name string) ([]byte, error)

ReadFile returns one file of the package.

func (*Package) SHA256

func (p *Package) SHA256() [32]byte

SHA256 is the hash of the archive bytes (what registries list).

func (*Package) ThemeMeta

func (p *Package) ThemeMeta() (ThemeMeta, error)

ThemeMeta decodes theme.json of a theme package.

func (*Package) Wasm

func (p *Package) Wasm() ([]byte, error)

Wasm returns plugin.wasm of a plugin package.

func (*Package) Widgets

func (p *Package) Widgets() (Widgets, error)

Widgets reads the widgets of a plugin package (none for a theme or a package without widgets/).

type PageOptions

type PageOptions struct {
	Title, Intro, WriteURL, SubmitURL, InstallHint string
}

PageOptions are the words and links of a rendered directory page. Empty URLs hide their button; an empty InstallHint hides the hint line.

type ParseError

type ParseError struct{ Msg string }

ParseError is a package that was refused; its message is safe to show.

func (*ParseError) Error

func (e *ParseError) Error() string

type PluginRuntime

type PluginRuntime interface {
	LoadWith(ctx context.Context, m sdk.Manifest, wasm []byte, a pluginhost.Assets) pluginhost.Status
	Unload(ctx context.Context, name string)
	Statuses() []pluginhost.Status
}

PluginRuntime is the part of *pluginhost.Runtime the manager drives.

type Registry

type Registry struct {
	URL    string
	Client *http.Client
	Trust  TrustSet
	Limits Limits
	// AllowInsecure accepts http:// URLs (an explicit development or test
	// override).
	AllowInsecure bool
	// Now is the clock (tests); time.Now when nil.
	Now func() time.Time
}

Registry reads a signed registry index and downloads the packages it lists.

func (*Registry) Download

func (r *Registry) Download(ctx context.Context, e Entry) ([]byte, error)

Download fetches the .hmp of e and checks its hash against the index. The package itself is verified (signature) by Manager.Install.

func (*Registry) Fetch

func (r *Registry) Fetch(ctx context.Context) (*Index, error)

Fetch downloads and verifies the index. An index that is unsigned, signed by an untrusted key or altered is refused; it is never shown.

type Row

type Row struct {
	Kind        Kind
	Name        string
	Version     string
	Manifest    json.RawMessage
	SHA256      string
	SignerKeyID string // "" for an unsigned package
	Source      Source
	Enabled     bool
	// State is StateInstalled, or StateUninstalling for a removal that has
	// started and can be retried (the package is not loaded).
	State       string
	Size        int
	InstalledAt time.Time
	UpdatedAt   time.Time
}

Row is one installed package as stored, without its archive bytes.

func (Row) Signed

func (r Row) Signed() bool

Signed reports whether the package carried a signature when it was installed.

type Signature

type Signature struct {
	Signer    string `json:"signer"` // KeyID of the signing key
	Algorithm string `json:"algorithm"`
	Signature string `json:"signature"` // base64
}

Signature is signature.json inside a package: who signed and the ed25519 signature over the package digest (see Package.Digest).

func NewSignature

func NewSignature(pub ed25519.PublicKey, rawSig []byte) Signature

NewSignature wraps a raw ed25519 signature made by the holder of pub, for signers that never expose their private key (an ssh-agent).

func SignBytes

func SignBytes(k Key, msg []byte) (Signature, error)

SignBytes signs msg, any bytes, with k.

type Source

type Source string

Source says where an installed package came from.

const (
	SourceRegistry Source = "registry"
	SourceUpload   Source = "upload"
	SourceURL      Source = "url"
	SourceDev      Source = "dev"
)

The sources of a package row.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store keeps packages in hatamoto_packages.

func NewStore

func NewStore(pool *pgxpool.Pool) *Store

NewStore returns a store over pool.

func (*Store) Config

func (s *Store) Config(ctx context.Context) (Config, error)

Config reads the package policy.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, kind Kind, name string) error

Delete removes the package row (and a theme's settings). It touches nothing else.

func (*Store) Get

func (s *Store) Get(ctx context.Context, kind Kind, name string) (Row, error)

Get returns one package row.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Row, error)

List returns every installed package, plugins first, by name.

func (*Store) Load

func (s *Store) Load(ctx context.Context, kind Kind, name string) (*Package, Row, error)

Load reads the archive of an installed package and parses it again (the bytes are trusted: they were verified on the way in, but parsing keeps one code path).

func (*Store) Put

func (s *Store) Put(ctx context.Context, p *Package, src Source, enabled bool) error

Put installs or replaces (update) the package of the same kind and name. An update keeps the stored enabled flag; a new package is stored with enabled.

func (*Store) PutThemeSettings

func (s *Store) PutThemeSettings(ctx context.Context, theme string, v json.RawMessage) error

PutThemeSettings stores a theme's settings object.

func (*Store) SetActiveTheme

func (s *Store) SetActiveTheme(ctx context.Context, name string) error

SetActiveTheme stores the active theme ("" selects the built-in default).

func (*Store) SetAllowUnsigned

func (s *Store) SetAllowUnsigned(ctx context.Context, on bool) error

SetAllowUnsigned stores the unsigned-package opt-in.

func (*Store) SetEnabled

func (s *Store) SetEnabled(ctx context.Context, kind Kind, name string, on bool) error

SetEnabled flips a plugin's enabled flag.

func (*Store) SetServerEnvironment

func (s *Store) SetServerEnvironment(ctx context.Context, env string) error

SetServerEnvironment records the booting server's ENVIRONMENT.

func (*Store) SetState

func (s *Store) SetState(ctx context.Context, kind Kind, name, state string) error

SetState marks a package row installed or uninstalling.

func (*Store) ThemeSettings

func (s *Store) ThemeSettings(ctx context.Context, theme string) (json.RawMessage, error)

ThemeSettings returns a theme's stored settings object ({} when none).

func (*Store) ThemeSettingsStamp

func (s *Store) ThemeSettingsStamp(ctx context.Context, theme string) (time.Time, error)

ThemeSettingsStamp is when a theme's settings last changed (zero: never). It is part of the reconcile key, so every replica picks a change up.

type ThemeEngine

type ThemeEngine interface {
	SetTheme(layers ...fs.FS) error
	SetThemeName(name string)
	CheckTheme(layers ...fs.FS) error
	SetSettings(map[string]any)
	SectionTypes(layers ...fs.FS) ([]string, error)
}

ThemeEngine is the part of *render.Engine the manager drives.

type ThemeMeta

type ThemeMeta struct {
	Name           string          `json:"name"`
	Version        string          `json:"version"`
	Description    string          `json:"description,omitempty"`
	SettingsSchema json.RawMessage `json:"settings_schema,omitempty"`
	// Screenshot is an optional preview image under assets/, see ValidateScreenshot.
	Screenshot string `json:"screenshot,omitempty"`
}

ThemeMeta is the part of theme.json the host reads.

type TrustSet

type TrustSet map[string]ed25519.PublicKey

TrustSet is the signer keys the owner trusts, by key id.

func ParseTrustSet

func ParseTrustSet(list string) (TrustSet, error)

ParseTrustSet reads a comma- or space-separated list of base64 public keys.

func (TrustSet) Add

func (t TrustSet) Add(pub ed25519.PublicKey)

Add trusts pub.

func (TrustSet) Verify

func (t TrustSet) Verify(p *Package) error

Verify checks the package's embedded signature against the trusted keys. It returns ErrUnsigned for a package without one, ErrUntrustedKey for an unknown signer and ErrBadSignature when the contents do not match.

func (TrustSet) VerifyBytes

func (t TrustSet) VerifyBytes(signer, algorithm, sig string, msg []byte) error

VerifyBytes checks a base64 signature over msg made by the named signer. algorithm may be empty (ed25519). Errors are ErrUntrustedKey for a signer outside the set and ErrBadSignature when msg does not match.

func (TrustSet) VerifyIndex

func (t TrustSet) VerifyIndex(doc []byte) (json.RawMessage, error)

VerifyIndex checks a registry.json envelope and returns the index JSON.

type Widget

type Widget struct {
	Name, Kind                string
	Schema, HTML, CSS, Island []byte
}

Widget is one widgets/<name>/ directory. Kind is the builder kind.

type Widgets

type Widgets struct {
	Plugin string
	Items  []Widget
}

Widgets are the widgets a plugin package ships, in name order.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL